@@ -42,6 +42,8 @@ pub struct CircuitTracer {
4242 circuit_builder : OperationListBuilder ,
4343 next_result_id : usize ,
4444 user_package_ids : Vec < PackageId > ,
45+ superposition_qubits : FxHashSet < QubitWire > ,
46+ classical_one_qubits : FxHashSet < QubitWire > ,
4547}
4648
4749impl Tracer for CircuitTracer {
@@ -69,6 +71,13 @@ impl Tracer for CircuitTracer {
6971 ) {
7072 let called_at = map_stack_frames_to_locations ( stack) ;
7173 let display_args: Vec < String > = theta. map ( |p| format ! ( "{p:.4}" ) ) . into_iter ( ) . collect ( ) ;
74+ let controls = if self . config . prune_classical_qubits {
75+ // Any controls that are known to be classically one can be removed, so this
76+ // will return the updated controls list.
77+ & self . update_qubit_status ( name, targets, controls)
78+ } else {
79+ controls
80+ } ;
7281 self . circuit_builder . gate (
7382 self . wire_map_builder . current ( ) ,
7483 name,
@@ -91,6 +100,8 @@ impl Tracer for CircuitTracer {
91100 } ;
92101 self . wire_map_builder . link_result_to_qubit ( q, r) ;
93102 if name == "MResetZ" {
103+ self . classical_one_qubits
104+ . remove ( & self . wire_map_builder . wire_map . qubit_wire ( q) ) ;
94105 self . circuit_builder
95106 . mresetz ( self . wire_map_builder . current ( ) , q, r, called_at) ;
96107 } else {
@@ -101,6 +112,8 @@ impl Tracer for CircuitTracer {
101112
102113 fn reset ( & mut self , stack : & [ Frame ] , q : usize ) {
103114 let called_at = map_stack_frames_to_locations ( stack) ;
115+ self . classical_one_qubits
116+ . remove ( & self . wire_map_builder . wire_map . qubit_wire ( q) ) ;
104117 self . circuit_builder
105118 . reset ( self . wire_map_builder . current ( ) , q, called_at) ;
106119 }
@@ -152,6 +165,8 @@ impl CircuitTracer {
152165 ) ,
153166 next_result_id : 0 ,
154167 user_package_ids : user_package_ids. to_vec ( ) ,
168+ superposition_qubits : FxHashSet :: default ( ) ,
169+ classical_one_qubits : FxHashSet :: default ( ) ,
155170 }
156171 }
157172
@@ -191,6 +206,8 @@ impl CircuitTracer {
191206 ) ,
192207 next_result_id : 0 ,
193208 user_package_ids : user_package_ids. to_vec ( ) ,
209+ superposition_qubits : FxHashSet :: default ( ) ,
210+ classical_one_qubits : FxHashSet :: default ( ) ,
194211 }
195212 }
196213
@@ -220,16 +237,57 @@ impl CircuitTracer {
220237 operations : & [ OperationOrGroup ] ,
221238 source_lookup : & impl SourceLookup ,
222239 ) -> Circuit {
240+ let mut operations: Vec < OperationOrGroup > = operations. to_vec ( ) ;
241+ let mut qubits = self . wire_map_builder . wire_map . to_qubits ( ) ;
242+ // We need to pass the original number of qubits, before any trimming, to finish the circuit below.
243+ let num_qubits = qubits. len ( ) ;
244+
245+ if self . config . prune_classical_qubits {
246+ // Remove qubits that are always classical.
247+ qubits. retain ( |q| self . superposition_qubits . contains ( & q. id . into ( ) ) ) ;
248+
249+ // Remove operations that don't use any non-classical qubits.
250+ operations. retain_mut ( |op| self . should_keep_operation_mut ( op) ) ;
251+ }
252+
223253 let operations = operations
224254 . iter ( )
225255 . map ( |o| o. clone ( ) . into_operation ( source_lookup) )
226256 . collect ( ) ;
227257
228- finish_circuit (
229- self . wire_map_builder . wire_map . to_qubits ( ) ,
230- operations,
231- source_lookup,
232- )
258+ finish_circuit ( qubits, operations, num_qubits, source_lookup)
259+ }
260+
261+ fn should_keep_operation_mut ( & self , op : & mut OperationOrGroup ) -> bool {
262+ if matches ! ( op. kind, OperationOrGroupKind :: Single ) {
263+ // This is a normal gate operation, so only keep it if all the qubits are non-classical.
264+ op. all_qubits ( )
265+ . iter ( )
266+ . all ( |q| self . superposition_qubits . contains ( q) )
267+ } else {
268+ // This is a grouped operation, so process the children recursively.
269+ let mut used_qubits = FxHashSet :: default ( ) ;
270+ op. children_mut ( )
271+ . expect ( "operation should be a group with children" )
272+ . retain_mut ( |child_op| {
273+ // Prune out child ops that don't use any non-classical qubits.
274+ // This has the side effect of updating each child op's target qubits.
275+ if self . should_keep_operation_mut ( child_op) {
276+ for q in child_op. all_qubits ( ) {
277+ used_qubits. insert ( q) ;
278+ }
279+ true
280+ } else {
281+ false
282+ }
283+ } ) ;
284+ // Update the targets of this grouped operation to only include qubits actually used by child operations.
285+ op. op
286+ . targets_mut ( )
287+ . retain ( |q| used_qubits. contains ( & q. qubit . into ( ) ) ) ;
288+ // Only keep this grouped operation if any of its targets were kept.
289+ !op. op . targets_mut ( ) . is_empty ( )
290+ }
233291 }
234292
235293 /// Splits the qubit arguments from classical arguments so that the qubits
@@ -311,6 +369,113 @@ impl CircuitTracer {
311369 }
312370 first_user_code_location ( & self . user_package_ids , stack)
313371 }
372+
373+ fn mark_qubit_in_superposition ( & mut self , wire : QubitWire ) {
374+ assert ! (
375+ self . config. prune_classical_qubits,
376+ "should only be called when pruning is enabled"
377+ ) ;
378+ self . superposition_qubits . insert ( wire) ;
379+ self . classical_one_qubits . remove ( & wire) ;
380+ }
381+
382+ fn flip_classical_qubit ( & mut self , wire : QubitWire ) {
383+ assert ! (
384+ self . config. prune_classical_qubits,
385+ "should only be called when pruning is enabled"
386+ ) ;
387+ if self . classical_one_qubits . contains ( & wire) {
388+ self . classical_one_qubits . remove ( & wire) ;
389+ } else {
390+ self . classical_one_qubits . insert ( wire) ;
391+ }
392+ }
393+
394+ fn update_qubit_status (
395+ & mut self ,
396+ name : & str ,
397+ targets : & [ usize ] ,
398+ controls : & [ usize ] ,
399+ ) -> Vec < usize > {
400+ match name {
401+ "H" | "Rx" | "Ry" | "SX" | "Rxx" | "Ryy" => {
402+ // These gates create superpositions, so mark the qubits as non-trimmable
403+ for & q in targets {
404+ let mapped_q = self . wire_map_builder . wire_map . qubit_wire ( q) ;
405+ self . mark_qubit_in_superposition ( mapped_q) ;
406+ }
407+ }
408+ "X" | "Y" => {
409+ let mapped_target = self . wire_map_builder . wire_map . qubit_wire ( targets[ 0 ] ) ;
410+ let controls: Vec < usize > = controls
411+ . iter ( )
412+ . filter ( |c| !self . classical_one_qubits . contains ( & ( * * c) . into ( ) ) )
413+ . copied ( )
414+ . collect ( ) ;
415+ if !self . superposition_qubits . contains ( & mapped_target) {
416+ // The target is not yet marked as non-trimmable, so check the controls.
417+ let superposition_controls_count = controls
418+ . iter ( )
419+ . filter ( |c| self . superposition_qubits . contains ( & ( * * c) . into ( ) ) )
420+ . count ( ) ;
421+
422+ if controls. is_empty ( ) {
423+ // If all controls are classical 1 or there are no controls, the target is flipped
424+ self . flip_classical_qubit ( mapped_target) ;
425+ } else if superposition_controls_count == controls. len ( ) {
426+ // If all controls are in superposition, the target is also in superposition
427+ self . mark_qubit_in_superposition ( mapped_target) ;
428+ }
429+ }
430+ return controls;
431+ }
432+ "Z" => {
433+ // Only clean up the classical 1 qubits from the controls list. No need to update the target,
434+ // since Z does not introduce superpositions.
435+ return controls
436+ . iter ( )
437+ . filter ( |c| !self . classical_one_qubits . contains ( & ( * * c) . into ( ) ) )
438+ . copied ( )
439+ . collect ( ) ;
440+ }
441+ "SWAP" => {
442+ // If either qubit is non-trimmable, both become non-trimmable
443+ let q0_mapped = self . wire_map_builder . wire_map . qubit_wire ( targets[ 0 ] ) ;
444+ let q1_mapped = self . wire_map_builder . wire_map . qubit_wire ( targets[ 1 ] ) ;
445+ if self . superposition_qubits . contains ( & q0_mapped)
446+ || self . superposition_qubits . contains ( & q1_mapped)
447+ {
448+ self . mark_qubit_in_superposition ( q0_mapped) ;
449+ self . mark_qubit_in_superposition ( q1_mapped) ;
450+ } else {
451+ match (
452+ self . classical_one_qubits . contains ( & q0_mapped) ,
453+ self . classical_one_qubits . contains ( & q1_mapped) ,
454+ ) {
455+ ( true , false ) | ( false , true ) => {
456+ self . flip_classical_qubit ( q0_mapped) ;
457+ self . flip_classical_qubit ( q1_mapped) ;
458+ }
459+ _ => {
460+ // Nothing to do if both are classical 0 or both are in superposition
461+ }
462+ }
463+ }
464+ }
465+ "S" | "T" | "Rz" | "Rzz" => {
466+ // These gates don't create superpositions on their own, so do nothing
467+ }
468+ _ => {
469+ // For any other gate, conservatively mark all target qubits as non-trimmable
470+ for & q in targets. iter ( ) . chain ( controls. iter ( ) ) {
471+ let mapped_q = self . wire_map_builder . wire_map . qubit_wire ( q) ;
472+ self . mark_qubit_in_superposition ( mapped_q) ;
473+ }
474+ }
475+ }
476+ // Return the normal controls list if no changes were made.
477+ controls. to_vec ( )
478+ }
314479}
315480
316481fn first_user_code_location (
@@ -332,6 +497,7 @@ fn first_user_code_location(
332497fn finish_circuit (
333498 mut qubits : Vec < Qubit > ,
334499 mut operations : Vec < Operation > ,
500+ num_qubits : usize ,
335501 source_location_lookup : & impl SourceLookup ,
336502) -> Circuit {
337503 resolve_locations ( & mut operations, source_location_lookup) ;
@@ -342,7 +508,7 @@ fn finish_circuit(
342508 }
343509 }
344510
345- let component_grid = operation_list_to_grid ( operations, qubits . len ( ) ) ;
511+ let component_grid = operation_list_to_grid ( operations, num_qubits ) ;
346512 Circuit {
347513 qubits,
348514 component_grid,
@@ -458,6 +624,8 @@ pub struct TracerConfig {
458624 pub source_locations : bool ,
459625 /// Group operations according to call graph in the circuit diagram
460626 pub group_by_scope : bool ,
627+ /// Prune purely classical or unused qubits
628+ pub prune_classical_qubits : bool ,
461629}
462630
463631impl TracerConfig {
@@ -476,6 +644,7 @@ impl Default for TracerConfig {
476644 max_operations : Self :: DEFAULT_MAX_OPERATIONS ,
477645 source_locations : true ,
478646 group_by_scope : true ,
647+ prune_classical_qubits : false ,
479648 }
480649 }
481650}
0 commit comments