Skip to content

Commit 44ba5af

Browse files
authored
Decompose measurement in QIR Base profile processing (microsoft#2230)
This change simplifies the decomposition of `M` and `Measure` in the stdlib to no longer conditionally compile to different versions based on qubit reuse capability, instead offloading that to QIR processing, which already has similar logic. One effect of this is simulation and circuit generation will match user expectation and patterns in the Q# code rather than more closely showing what occurs on hardware.
1 parent e0df7cc commit 44ba5af

File tree

8 files changed

+83
-249
lines changed

8 files changed

+83
-249
lines changed

compiler/qsc/src/interpret.rs

-9
Original file line numberDiff line numberDiff line change
@@ -759,7 +759,6 @@ impl Interpreter {
759759
sim.chained.finish()
760760
} else {
761761
let mut sim = CircuitBuilder::new(CircuitConfig {
762-
base_profile: self.capabilities.is_empty(),
763762
max_operations: CircuitConfig::DEFAULT_MAX_OPERATIONS,
764763
});
765764

@@ -1004,14 +1003,6 @@ fn sim_circuit_backend() -> BackendChain<SparseSim, CircuitBuilder> {
10041003
BackendChain::new(
10051004
SparseSim::new(),
10061005
CircuitBuilder::new(CircuitConfig {
1007-
// When using in conjunction with the simulator,
1008-
// the circuit builder should *not* perform base profile
1009-
// decompositions, in order to match the simulator's behavior.
1010-
//
1011-
// Note that conditional compilation (e.g. @Config(Base) attributes)
1012-
// will still respect the selected profile. This also
1013-
// matches the behavior of the simulator.
1014-
base_profile: false,
10151006
max_operations: CircuitConfig::DEFAULT_MAX_OPERATIONS,
10161007
}),
10171008
)

compiler/qsc/src/interpret/circuit_tests.rs

+22-36
Original file line numberDiff line numberDiff line change
@@ -202,11 +202,10 @@ fn m_base_profile() {
202202
.circuit(CircuitEntryPoint::EntryPoint, false)
203203
.expect("circuit generation should succeed");
204204

205-
expect![[r"
206-
q_0 ── H ──── Z ────────────────
207-
q_1 ── H ──── ● ──── H ──── M ──
208-
╘═══
209-
"]]
205+
expect![[r#"
206+
q_0 ── H ──── M ──
207+
╘═══
208+
"#]]
210209
.assert_eq(&circ.to_string());
211210
}
212211

@@ -287,10 +286,10 @@ fn mresetz_base_profile() {
287286
.circuit(CircuitEntryPoint::EntryPoint, false)
288287
.expect("circuit generation should succeed");
289288

290-
expect![[r"
291-
q_0 ── H ──── M ──
292-
╘═══
293-
"]]
289+
expect![[r#"
290+
q_0 ── H ──── M ──── |0〉 ──
291+
╘════════════
292+
"#]]
294293
.assert_eq(&circ.to_string());
295294
}
296295

@@ -597,14 +596,12 @@ fn operation_with_qubits_base_profile() {
597596
.circuit(CircuitEntryPoint::Operation("Test.Test".into()), false)
598597
.expect("circuit generation should succeed");
599598

600-
expect![[r"
601-
q_0 ── H ──── ● ──── Z ──────────────────────────────
602-
q_1 ───────── X ─────┼──────────── Z ────────────────
603-
q_2 ── H ─────────── ● ──── H ─────┼───── M ─────────
604-
│ ╘══════════
605-
q_3 ── H ───────────────────────── ● ──── H ──── M ──
606-
╘═══
607-
"]]
599+
expect![[r#"
600+
q_0 ── H ──── ● ──── M ──
601+
│ ╘═══
602+
q_1 ───────── X ──── M ──
603+
╘═══
604+
"#]]
608605
.assert_eq(&circ.to_string());
609606
}
610607

@@ -1067,33 +1064,22 @@ mod debugger_stepping {
10671064
Profile::Base,
10681065
);
10691066

1070-
// Surprising but expected: Reset gates would *not* normally
1071-
// be generated in Base Profile, but they are here, since
1072-
// when running in tandem with the simulator, the resulting
1073-
// circuit is intended to match the calls into the simulator.
1074-
//
1075-
// Note the circuit still looks different than what would be
1076-
// generated in Unrestricted Profile for the same code,
1077-
// due to conditional compilation in the standard library.
1078-
expect![["
1067+
expect![[r#"
10791068
step:
10801069
step:
10811070
q_0
10821071
step:
10831072
q_0 ── H ──
10841073
step:
1085-
q_0 ── H ──── Z ─────────────────────────
1086-
q_1 ── H ──── ● ──── H ──── M ──── |0〉 ──
1087-
╘════════════
1074+
q_0 ── H ──── M ──
1075+
╘═══
10881076
step:
1089-
q_0 ── H ──── Z ──── |0〉 ──────────────────
1090-
q_1 ── H ──── ● ───── H ───── M ──── |0〉 ──
1091-
╘════════════
1077+
q_0 ── H ──── M ──── |0〉 ──
1078+
╘════════════
10921079
step:
1093-
q_0 ── H ──── Z ──── |0〉 ──────────────────
1094-
q_1 ── H ──── ● ───── H ───── M ──── |0〉 ──
1095-
╘════════════
1096-
"]]
1080+
q_0 ── H ──── M ──── |0〉 ──
1081+
╘════════════
1082+
"#]]
10971083
.assert_eq(&circs);
10981084
}
10991085

compiler/qsc/src/interpret/tests.rs

+20-40
Original file line numberDiff line numberDiff line change
@@ -884,21 +884,17 @@ mod given_interpreter {
884884
885885
define void @ENTRYPOINT__main() #0 {
886886
block_0:
887-
call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 1 to %Qubit*))
888-
call void @__quantum__qis__cz__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Qubit* inttoptr (i64 0 to %Qubit*))
889-
call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 1 to %Qubit*))
890-
call void @__quantum__qis__m__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Result* inttoptr (i64 0 to %Result*))
887+
call void @__quantum__qis__cx__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Qubit* inttoptr (i64 1 to %Qubit*))
888+
call void @__quantum__qis__m__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Result* inttoptr (i64 0 to %Result*))
891889
call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 0 to %Result*), i8* null)
892890
ret void
893891
}
894892
895-
declare void @__quantum__qis__h__body(%Qubit*)
896-
897-
declare void @__quantum__qis__cz__body(%Qubit*, %Qubit*)
893+
declare void @__quantum__qis__m__body(%Qubit*, %Result*) #1
898894
899895
declare void @__quantum__rt__result_record_output(%Result*, i8*)
900896
901-
declare void @__quantum__qis__m__body(%Qubit*, %Result*) #1
897+
declare void @__quantum__qis__cx__body(%Qubit*, %Qubit*)
902898
903899
attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="base_profile" "required_num_qubits"="2" "required_num_results"="1" }
904900
attributes #1 = { "irreversible" }
@@ -1083,21 +1079,17 @@ mod given_interpreter {
10831079
10841080
define void @ENTRYPOINT__main() #0 {
10851081
block_0:
1086-
call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 1 to %Qubit*))
1087-
call void @__quantum__qis__cz__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Qubit* inttoptr (i64 0 to %Qubit*))
1088-
call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 1 to %Qubit*))
1089-
call void @__quantum__qis__m__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Result* inttoptr (i64 0 to %Result*))
1082+
call void @__quantum__qis__cx__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Qubit* inttoptr (i64 1 to %Qubit*))
1083+
call void @__quantum__qis__m__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Result* inttoptr (i64 0 to %Result*))
10901084
call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 0 to %Result*), i8* null)
10911085
ret void
10921086
}
10931087
1094-
declare void @__quantum__qis__h__body(%Qubit*)
1095-
1096-
declare void @__quantum__qis__cz__body(%Qubit*, %Qubit*)
1088+
declare void @__quantum__qis__m__body(%Qubit*, %Result*) #1
10971089
10981090
declare void @__quantum__rt__result_record_output(%Result*, i8*)
10991091
1100-
declare void @__quantum__qis__m__body(%Qubit*, %Result*) #1
1092+
declare void @__quantum__qis__cx__body(%Qubit*, %Qubit*)
11011093
11021094
attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="base_profile" "required_num_qubits"="2" "required_num_results"="1" }
11031095
attributes #1 = { "irreversible" }
@@ -1131,21 +1123,17 @@ mod given_interpreter {
11311123
11321124
define void @ENTRYPOINT__main() #0 {
11331125
block_0:
1134-
call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 1 to %Qubit*))
1135-
call void @__quantum__qis__cz__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Qubit* inttoptr (i64 0 to %Qubit*))
1136-
call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 1 to %Qubit*))
1137-
call void @__quantum__qis__m__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Result* inttoptr (i64 0 to %Result*))
1126+
call void @__quantum__qis__cx__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Qubit* inttoptr (i64 1 to %Qubit*))
1127+
call void @__quantum__qis__m__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Result* inttoptr (i64 0 to %Result*))
11381128
call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 0 to %Result*), i8* null)
11391129
ret void
11401130
}
11411131
1142-
declare void @__quantum__qis__h__body(%Qubit*)
1143-
1144-
declare void @__quantum__qis__cz__body(%Qubit*, %Qubit*)
1132+
declare void @__quantum__qis__m__body(%Qubit*, %Result*) #1
11451133
11461134
declare void @__quantum__rt__result_record_output(%Result*, i8*)
11471135
1148-
declare void @__quantum__qis__m__body(%Qubit*, %Result*) #1
1136+
declare void @__quantum__qis__cx__body(%Qubit*, %Qubit*)
11491137
11501138
attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="base_profile" "required_num_qubits"="2" "required_num_results"="1" }
11511139
attributes #1 = { "irreversible" }
@@ -1211,21 +1199,17 @@ mod given_interpreter {
12111199
12121200
define void @ENTRYPOINT__main() #0 {
12131201
block_0:
1214-
call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 1 to %Qubit*))
1215-
call void @__quantum__qis__cz__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Qubit* inttoptr (i64 0 to %Qubit*))
1216-
call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 1 to %Qubit*))
1217-
call void @__quantum__qis__m__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Result* inttoptr (i64 0 to %Result*))
1202+
call void @__quantum__qis__cx__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Qubit* inttoptr (i64 1 to %Qubit*))
1203+
call void @__quantum__qis__m__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Result* inttoptr (i64 0 to %Result*))
12181204
call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 0 to %Result*), i8* null)
12191205
ret void
12201206
}
12211207
1222-
declare void @__quantum__qis__h__body(%Qubit*)
1223-
1224-
declare void @__quantum__qis__cz__body(%Qubit*, %Qubit*)
1208+
declare void @__quantum__qis__m__body(%Qubit*, %Result*) #1
12251209
12261210
declare void @__quantum__rt__result_record_output(%Result*, i8*)
12271211
1228-
declare void @__quantum__qis__m__body(%Qubit*, %Result*) #1
1212+
declare void @__quantum__qis__cx__body(%Qubit*, %Qubit*)
12291213
12301214
attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="base_profile" "required_num_qubits"="2" "required_num_results"="1" }
12311215
attributes #1 = { "irreversible" }
@@ -1271,21 +1255,17 @@ mod given_interpreter {
12711255
12721256
define void @ENTRYPOINT__main() #0 {
12731257
block_0:
1274-
call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 1 to %Qubit*))
1275-
call void @__quantum__qis__cz__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Qubit* inttoptr (i64 0 to %Qubit*))
1276-
call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 1 to %Qubit*))
1277-
call void @__quantum__qis__m__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Result* inttoptr (i64 0 to %Result*))
1258+
call void @__quantum__qis__cx__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Qubit* inttoptr (i64 1 to %Qubit*))
1259+
call void @__quantum__qis__m__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Result* inttoptr (i64 0 to %Result*))
12781260
call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 0 to %Result*), i8* null)
12791261
ret void
12801262
}
12811263
1282-
declare void @__quantum__qis__h__body(%Qubit*)
1283-
1284-
declare void @__quantum__qis__cz__body(%Qubit*, %Qubit*)
1264+
declare void @__quantum__qis__m__body(%Qubit*, %Result*) #1
12851265
12861266
declare void @__quantum__rt__result_record_output(%Result*, i8*)
12871267
1288-
declare void @__quantum__qis__m__body(%Qubit*, %Result*) #1
1268+
declare void @__quantum__qis__cx__body(%Qubit*, %Qubit*)
12891269
12901270
attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="base_profile" "required_num_qubits"="2" "required_num_results"="1" }
12911271
attributes #1 = { "irreversible" }

compiler/qsc_circuit/src/builder.rs

+21-57
Original file line numberDiff line numberDiff line change
@@ -56,48 +56,34 @@ impl Backend for Builder {
5656
}
5757

5858
fn m(&mut self, q: usize) -> Self::ResultType {
59-
if self.config.base_profile {
60-
// defer the measurement and reset the qubit
61-
self.remapper.mreset(q)
62-
} else {
63-
let mapped_q = self.map(q);
64-
// In the Circuit schema, result id is per-qubit
65-
let res_id = self.num_measurements_for_qubit(mapped_q);
66-
let id = self.remapper.m(q);
59+
let mapped_q = self.map(q);
60+
// In the Circuit schema, result id is per-qubit
61+
let res_id = self.num_measurements_for_qubit(mapped_q);
62+
let id = self.remapper.m(q);
6763

68-
self.push_gate(measurement_gate(mapped_q.0, res_id));
69-
id
70-
}
64+
self.push_gate(measurement_gate(mapped_q.0, res_id));
65+
id
7166
}
7267

7368
fn mresetz(&mut self, q: usize) -> Self::ResultType {
74-
if self.config.base_profile {
75-
// defer the measurement
76-
self.remapper.mreset(q)
77-
} else {
78-
let mapped_q = self.map(q);
79-
// In the Circuit schema, result id is per-qubit
80-
let res_id = self.num_measurements_for_qubit(mapped_q);
81-
// We don't actually need the Remapper since we're not
82-
// remapping any qubits, but it's handy for keeping track of measurements
83-
let id = self.remapper.m(q);
84-
85-
// Ideally MResetZ would be atomic but we don't currently have
86-
// a way to visually represent that. So decompose it into
87-
// a measurement and a reset gate.
88-
self.push_gate(measurement_gate(mapped_q.0, res_id));
89-
self.push_gate(gate(KET_ZERO, [mapped_q]));
90-
id
91-
}
69+
let mapped_q = self.map(q);
70+
// In the Circuit schema, result id is per-qubit
71+
let res_id = self.num_measurements_for_qubit(mapped_q);
72+
// We don't actually need the Remapper since we're not
73+
// remapping any qubits, but it's handy for keeping track of measurements
74+
let id = self.remapper.m(q);
75+
76+
// Ideally MResetZ would be atomic but we don't currently have
77+
// a way to visually represent that. So decompose it into
78+
// a measurement and a reset gate.
79+
self.push_gate(measurement_gate(mapped_q.0, res_id));
80+
self.push_gate(gate(KET_ZERO, [mapped_q]));
81+
id
9282
}
9383

9484
fn reset(&mut self, q: usize) {
95-
if self.config.base_profile {
96-
self.remapper.reset(q);
97-
} else {
98-
let mapped_q = self.map(q);
99-
self.push_gate(gate(KET_ZERO, [mapped_q]));
100-
}
85+
let mapped_q = self.map(q);
86+
self.push_gate(gate(KET_ZERO, [mapped_q]));
10187
}
10288

10389
fn rx(&mut self, theta: f64, q: usize) {
@@ -267,18 +253,6 @@ impl Builder {
267253
}
268254

269255
fn finish_circuit(&self, mut circuit: Circuit) -> Circuit {
270-
// add deferred measurements
271-
if self.config.base_profile {
272-
for (qubit, _) in &self.remapper.qubit_measurement_counts {
273-
if self.max_ops_exceeded || circuit.operations.len() >= self.config.max_operations {
274-
break;
275-
}
276-
277-
// guaranteed one measurement per qubit, so result is always 0
278-
circuit.operations.push(measurement_gate(qubit.0, 0));
279-
}
280-
}
281-
282256
// add qubit declarations
283257
for i in 0..self.remapper.num_qubits() {
284258
let num_measurements = self.num_measurements_for_qubit(WireId(i));
@@ -408,16 +382,6 @@ impl Remapper {
408382
id
409383
}
410384

411-
fn mreset(&mut self, q: usize) -> usize {
412-
let id = self.m(q);
413-
self.reset(q);
414-
id
415-
}
416-
417-
fn reset(&mut self, q: usize) {
418-
self.qubit_map.remove(q);
419-
}
420-
421385
fn qubit_allocate(&mut self) -> usize {
422386
let id = self.next_qubit_id;
423387
self.next_qubit_id += 1;

compiler/qsc_circuit/src/builder/tests.rs

+5-12
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@ use expect_test::expect;
66

77
#[test]
88
fn exceed_max_operations() {
9-
let mut builder = Builder::new(Config {
10-
base_profile: false,
11-
max_operations: 2,
12-
});
9+
let mut builder = Builder::new(Config { max_operations: 2 });
1310

1411
let q = builder.qubit_allocate();
1512

@@ -31,10 +28,7 @@ fn exceed_max_operations() {
3128

3229
#[test]
3330
fn exceed_max_operations_deferred_measurements() {
34-
let mut builder = Builder::new(Config {
35-
base_profile: true, // deferred measurements
36-
max_operations: 2,
37-
});
31+
let mut builder = Builder::new(Config { max_operations: 2 });
3832

3933
let q = builder.qubit_allocate();
4034

@@ -48,11 +42,10 @@ fn exceed_max_operations_deferred_measurements() {
4842

4943
// The current behavior is to silently truncate the circuit
5044
// if it exceeds the maximum allowed number of operations.
51-
// The measurement will be dropped.
45+
// The second X will be dropped.
5246
expect![[r#"
53-
q_0 ── X ──
54-
55-
q_1 ── X ──
47+
q_0 ── X ──── M ──
48+
╘═══
5649
"#]]
5750
.assert_eq(&circuit.to_string());
5851
}

0 commit comments

Comments
 (0)