Skip to content

Commit fb71248

Browse files
authored
Add optional prune_classical_qubits setting for circuit generation (#2802)
This change adds a parameter to Q# and OpenQASM circuit generation called `prune_classical_qubits` that will generate a simplified circuit diagram where qubits that are unused or purely classical (never enter superposition) are removed. This can be handy for places where the program comes from generated logic that may use more qubits than strictly needed. This is purely for visualizing the simplified circuit and does not perform any kind of execution optimization. So a program like this that has nine qubits but doesn't really use all of them: <img width="376" height="297" alt="image" src="https://github.com/user-attachments/assets/1ebf9bbd-8485-47d9-ad8b-26f392e2d789" /> will by default show the full circuit as it does today: <img width="1081" height="650" alt="image" src="https://github.com/user-attachments/assets/bbbe4238-d8ac-4a75-9ff2-00cfc30225ca" /> but with `prune_classical_qubits = True` will remove the purely classical q1, q2, q4, q5, and q8: <img width="663" height="422" alt="image" src="https://github.com/user-attachments/assets/7713fdea-8ade-417d-bf75-15dde3534c56" />
1 parent aa9424f commit fb71248

File tree

13 files changed

+666
-27
lines changed

13 files changed

+666
-27
lines changed

source/compiler/qsc/src/interpret/circuit_tests.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ fn grouping_nested_callables() {
213213
max_operations: usize::MAX,
214214
source_locations: false,
215215
group_by_scope: true,
216+
prune_classical_qubits: false,
216217
},
217218
)
218219
.expect("circuit generation should succeed");
@@ -1072,6 +1073,7 @@ fn operation_declared_in_eval() {
10721073
max_operations: usize::MAX,
10731074
source_locations: false,
10741075
group_by_scope: true,
1076+
prune_classical_qubits: false,
10751077
},
10761078
)
10771079
.expect("circuit generation should succeed");

source/compiler/qsc_circuit/src/builder.rs

Lines changed: 175 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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

4749
impl 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

316481
fn first_user_code_location(
@@ -332,6 +497,7 @@ fn first_user_code_location(
332497
fn 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

463631
impl 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
}

source/compiler/qsc_circuit/src/builder/tests.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
#![allow(clippy::unicode_not_nfc)]
5+
46
mod group_scopes;
7+
mod prune_classical_qubits;
58

69
use std::vec;
710

@@ -128,6 +131,7 @@ fn exceed_max_operations() {
128131
max_operations: 2,
129132
source_locations: false,
130133
group_by_scope: false,
134+
prune_classical_qubits: false,
131135
},
132136
&FakeCompilation::user_package_ids(),
133137
);
@@ -156,6 +160,7 @@ fn source_locations_enabled() {
156160
max_operations: 10,
157161
source_locations: true,
158162
group_by_scope: false,
163+
prune_classical_qubits: false,
159164
},
160165
&FakeCompilation::user_package_ids(),
161166
);
@@ -193,6 +198,7 @@ fn source_locations_disabled() {
193198
max_operations: 10,
194199
source_locations: false,
195200
group_by_scope: false,
201+
prune_classical_qubits: false,
196202
},
197203
&FakeCompilation::user_package_ids(),
198204
);
@@ -224,6 +230,7 @@ fn source_locations_multiple_user_frames() {
224230
max_operations: 10,
225231
source_locations: true,
226232
group_by_scope: false,
233+
prune_classical_qubits: false,
227234
},
228235
&FakeCompilation::user_package_ids(),
229236
);
@@ -262,6 +269,7 @@ fn source_locations_library_frames_excluded() {
262269
max_operations: 10,
263270
source_locations: true,
264271
group_by_scope: false,
272+
prune_classical_qubits: false,
265273
},
266274
&FakeCompilation::user_package_ids(),
267275
);
@@ -294,6 +302,7 @@ fn source_locations_only_library_frames() {
294302
max_operations: 10,
295303
source_locations: true,
296304
group_by_scope: false,
305+
prune_classical_qubits: false,
297306
},
298307
&FakeCompilation::user_package_ids(),
299308
);
@@ -326,6 +335,7 @@ fn source_locations_enabled_no_stack() {
326335
max_operations: 10,
327336
source_locations: true,
328337
group_by_scope: false,
338+
prune_classical_qubits: false,
329339
},
330340
&FakeCompilation::user_package_ids(),
331341
);
@@ -351,6 +361,7 @@ fn qubit_source_locations_via_stack() {
351361
max_operations: 10,
352362
source_locations: true,
353363
group_by_scope: false,
364+
prune_classical_qubits: false,
354365
},
355366
&FakeCompilation::user_package_ids(),
356367
);
@@ -375,6 +386,7 @@ fn qubit_labels_for_preallocated_qubits() {
375386
max_operations: 10,
376387
source_locations: true,
377388
group_by_scope: false,
389+
prune_classical_qubits: false,
378390
},
379391
&FakeCompilation::user_package_ids(),
380392
Some((
@@ -414,6 +426,7 @@ fn measurement_target_propagated_to_group() {
414426
max_operations: usize::MAX,
415427
source_locations: false,
416428
group_by_scope: true,
429+
prune_classical_qubits: false,
417430
},
418431
&FakeCompilation::user_package_ids(),
419432
);

source/compiler/qsc_circuit/src/builder/tests/group_scopes.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ fn check_groups(c: &FakeCompilation, instructions: &[(Vec<Frame>, &str)], expect
1212
max_operations: usize::MAX,
1313
source_locations: false,
1414
group_by_scope: true,
15+
prune_classical_qubits: false,
1516
},
1617
&FakeCompilation::user_package_ids(),
1718
);

0 commit comments

Comments
 (0)