1use bloq_ir::circuit::{
11 ConditionalCorrection, CoordCircuit, Op, Pauli as BloqPauli, PauliBasis, PauliMap,
12};
13use bloq_ir::lowering::validate_template_circuit;
14use glam::IVec2;
15use rustc_hash::FxHashMap;
16
17use super::ExecError;
18use super::gate_table::single_qubit_instruction;
19use crate::backend::{Instruction, Pauli, PauliBasis as EnginePauliBasis, PauliString, Simulator};
20
21#[derive(Debug, Clone, Default, PartialEq, Eq)]
25pub struct ShotRecord {
26 records: Vec<Option<bool>>,
27 aliases: FxHashMap<u32, u32>,
28}
29
30impl ShotRecord {
31 #[must_use]
33 pub fn get(&self, id: u32) -> Option<bool> {
34 let id = self.aliases.get(&id).copied().unwrap_or(id);
35 self.records.get(id as usize).copied().flatten()
36 }
37
38 pub fn require(&self, id: u32) -> Result<bool, ExecError> {
43 self.get(id).ok_or(ExecError::MissingRecord(id))
44 }
45
46 pub(crate) fn set(&mut self, id: u32, value: bool) {
47 let id = id as usize;
48 if id >= self.records.len() {
49 self.records.resize(id + 1, None);
50 }
51 self.records[id] = Some(value);
52 }
53
54 pub(crate) fn with_len(len: u32) -> Self {
55 Self {
56 records: vec![None; len as usize],
57 aliases: FxHashMap::default(),
58 }
59 }
60
61 pub(crate) fn select_aliases(&mut self, aliases: &[(u32, u32)]) {
64 self.aliases.extend(aliases.iter().copied());
65 }
66}
67
68pub(crate) struct LayoutCtx<'a> {
71 pub(crate) coord_to_index: &'a FxHashMap<IVec2, u32>,
72 pub(crate) offset: IVec2,
73 pub(crate) qubit_count: usize,
74}
75
76impl LayoutCtx<'_> {
77 pub(crate) fn index(&self, coord: IVec2) -> Result<usize, ExecError> {
79 let global = coord + self.offset;
80 self.coord_to_index
81 .get(&global)
82 .map(|&i| i as usize)
83 .ok_or(ExecError::UnknownCoord(global))
84 }
85}
86
87#[derive(Debug, Clone, Default, PartialEq)]
98pub(crate) struct PreparedOps {
99 instructions: Vec<Instruction>,
100 record_ids: Vec<u32>,
101}
102
103impl PreparedOps {
104 pub(crate) fn run(
112 &self,
113 sim: &mut Simulator,
114 record: &mut ShotRecord,
115 ) -> Result<usize, ExecError> {
116 let outcome = sim.apply_batch(&self.instructions)?;
117 debug_assert_eq!(
118 outcome.records.len(),
119 self.record_ids.len(),
120 "preparation counted the batch's measurements"
121 );
122 for (&id, result) in self.record_ids.iter().zip(&outcome.records) {
123 record.set(id, result.outcome);
124 }
125 Ok(outcome.max_rank)
126 }
127
128 #[cfg(test)]
130 pub(crate) fn record_ids(&self) -> &[u32] {
131 &self.record_ids
132 }
133}
134
135pub(crate) fn prepare_ops(
146 ops: &[Op],
147 layout: &LayoutCtx<'_>,
148 remap: &impl Fn(u32) -> u32,
149) -> Result<PreparedOps, ExecError> {
150 let mut prepared = PreparedOps::default();
151 let mut produced: FxHashMap<u32, usize> = FxHashMap::default();
157
158 for op in ops {
159 match op {
160 Op::Gate { gate, qubits } => {
161 if gate.is_two_qubit_gate() {
162 let (control, target) =
163 gate.two_qubit_bases().expect("two-qubit gate has bases");
164 debug_assert!(qubits.len().is_multiple_of(2), "two-qubit gate arg count");
165 for pair in qubits.as_chunks::<2>().0 {
166 prepared.instructions.push(Instruction::Gate2 {
167 control: sim_basis(control),
168 target: sim_basis(target),
169 control_qubit: layout.index(pair[0])?,
170 target_qubit: layout.index(pair[1])?,
171 });
172 }
173 } else {
174 for &coord in qubits {
175 let index = layout.index(coord)?;
176 prepared
177 .instructions
178 .extend(single_qubit_instruction(*gate, index));
179 }
180 }
181 }
182 Op::Measure {
183 basis,
184 qubits,
185 measurements,
186 flip_probability,
187 } => {
188 for (coord, &id) in qubits.iter().zip(measurements) {
189 let obs = basis_pauli(*basis, layout.index(*coord)?, layout.qubit_count);
190 push_measurement(
191 &mut prepared,
192 &mut produced,
193 id,
194 remap(id),
195 obs,
196 *flip_probability,
197 );
198 }
199 }
200 Op::MPP {
201 products,
202 measurements,
203 } => {
204 for (product, &id) in products.iter().zip(measurements) {
205 let obs = product_pauli(product, layout)?;
206 push_measurement(&mut prepared, &mut produced, id, remap(id), obs, 0.0);
207 }
208 }
209 Op::ConditionalPauli(corrections) => {
210 for correction in corrections {
211 prepared
212 .instructions
213 .push(conditional(correction, layout, remap, &produced)?);
214 }
215 }
216 Op::Depolarize1 {
217 probability,
218 qubits,
219 } => {
220 let probabilities = vec![probability / 3.0; 3];
221 for &qubit in qubits {
222 let qubit = layout.index(qubit)?;
223 push_random_pauli(
224 &mut prepared,
225 probabilities.clone(),
226 [Pauli::X, Pauli::Y, Pauli::Z]
227 .map(|pauli| PauliString::single(layout.qubit_count, qubit, pauli))
228 .into(),
229 );
230 }
231 }
232 Op::Depolarize2 {
233 probability,
234 qubits,
235 } => {
236 debug_assert!(qubits.len().is_multiple_of(2), "two-qubit noise arg count");
237 for pair in qubits.as_chunks::<2>().0 {
238 let first = layout.index(pair[0])?;
239 let second = layout.index(pair[1])?;
240 let mut alternatives = Vec::with_capacity(15);
241 for a in [Pauli::I, Pauli::X, Pauli::Y, Pauli::Z] {
242 for b in [Pauli::I, Pauli::X, Pauli::Y, Pauli::Z] {
243 if a != Pauli::I || b != Pauli::I {
244 alternatives.push(PauliString::from_terms(
245 layout.qubit_count,
246 [(first, a), (second, b)],
247 ));
248 }
249 }
250 }
251 push_random_pauli(&mut prepared, vec![probability / 15.0; 15], alternatives);
252 }
253 }
254 Op::PauliError {
255 probability,
256 pauli,
257 qubits,
258 } => {
259 let pauli: Pauli = sim_basis(*pauli).into();
260 for &qubit in qubits {
261 let alternative =
262 PauliString::single(layout.qubit_count, layout.index(qubit)?, pauli);
263 push_random_pauli(&mut prepared, vec![*probability], vec![alternative]);
264 }
265 }
266 Op::Tick => {}
267 Op::Repeat { .. } => return Err(ExecError::UnflattenedRepeat),
268 }
269 }
270 Ok(prepared)
271}
272
273fn push_measurement(
274 prepared: &mut PreparedOps,
275 produced: &mut FxHashMap<u32, usize>,
276 local: u32,
277 global: u32,
278 observable: PauliString,
279 flip_probability: f64,
280) {
281 produced.insert(local, prepared.record_ids.len());
282 prepared.record_ids.push(global);
283 if flip_probability == 0.0 {
284 prepared.instructions.push(Instruction::Measure(observable));
285 } else {
286 prepared
287 .instructions
288 .push(Instruction::MeasureWithReadoutError {
289 observable,
290 probability: flip_probability,
291 });
292 }
293}
294
295fn push_random_pauli(
296 prepared: &mut PreparedOps,
297 probabilities: Vec<f64>,
298 alternatives: Vec<PauliString>,
299) {
300 prepared.instructions.push(Instruction::RandomPauli {
301 probabilities,
302 alternatives,
303 heralded: false,
304 });
305}
306
307fn conditional(
308 correction: &ConditionalCorrection,
309 layout: &LayoutCtx<'_>,
310 remap: &impl Fn(u32) -> u32,
311 produced: &FxHashMap<u32, usize>,
312) -> Result<Instruction, ExecError> {
313 let control = *produced
314 .get(&correction.control)
315 .ok_or_else(|| ExecError::MissingRecord(remap(correction.control)))?;
316 Ok(Instruction::ConditionalPauli {
317 basis: sim_basis(correction.pauli),
318 qubit: layout.index(correction.target)?,
319 control,
320 })
321}
322
323pub(crate) fn validate_record_remap(
325 ops: &[Op],
326 remap: &FxHashMap<u32, u32>,
327) -> Result<(), ExecError> {
328 for op in ops {
329 let records: &[u32] = match op {
330 Op::Measure { measurements, .. } | Op::MPP { measurements, .. } => measurements,
331 _ => &[],
332 };
333 for &record in records {
334 if !remap.contains_key(&record) {
335 return Err(ExecError::MissingRecordRemap(record));
336 }
337 }
338 if let Op::ConditionalPauli(corrections) = op {
339 for correction in corrections {
340 if !remap.contains_key(&correction.control) {
341 return Err(ExecError::MissingRecordRemap(correction.control));
342 }
343 }
344 }
345 }
346 Ok(())
347}
348
349pub(super) fn product_pauli(
355 product: &PauliMap,
356 layout: &LayoutCtx<'_>,
357) -> Result<PauliString, ExecError> {
358 if product.is_empty() {
359 return Err(ExecError::EmptyMppProduct);
360 }
361 prepare_boundary_operator(product, layout)
362}
363
364pub(super) fn prepare_boundary_operator(
366 product: &PauliMap,
367 layout: &LayoutCtx<'_>,
368) -> Result<PauliString, ExecError> {
369 let mut pauli = PauliString::new(layout.qubit_count);
370 for (coord, bloq) in product.iter() {
371 pauli.set(layout.index(*coord)?, bloq_pauli(*bloq));
372 }
373 Ok(pauli)
374}
375
376pub(super) fn widen_pauli(pauli: &PauliString, nqubits: usize) -> PauliString {
379 debug_assert!(nqubits >= pauli.nqubits);
380 let mut widened = PauliString::from_terms(
381 nqubits,
382 (0..pauli.nqubits).map(|qubit| (qubit, pauli.get(qubit))),
383 );
384 widened.set_phase(pauli.phase_exponent());
385 widened
386}
387
388#[derive(Debug)]
399pub struct CircuitExecutor {
400 qubit_count: usize,
401 prepared: PreparedOps,
402}
403
404impl CircuitExecutor {
405 pub fn new(circuit: &CoordCircuit) -> Result<Self, ExecError> {
419 if circuit.has_repeats() {
420 return Err(ExecError::UnflattenedRepeat);
421 }
422 validate_template_circuit(circuit).map_err(ExecError::InvalidCircuit)?;
423 let coord_to_index = circuit.build_coord_to_index();
424 let qubit_count = coord_to_index.len();
425 let layout = LayoutCtx {
426 coord_to_index: &coord_to_index,
427 offset: IVec2::ZERO,
428 qubit_count,
429 };
430 let body = circuit
431 .body(circuit.entry_body())
432 .expect("entry body always exists on a CoordCircuit");
433 let prepared = prepare_ops(body.ops(), &layout, &|id| id)?;
434 Ok(Self {
435 qubit_count,
436 prepared,
437 })
438 }
439
440 #[must_use]
442 pub fn qubit_count(&self) -> usize {
443 self.qubit_count
444 }
445
446 pub fn run_shot(&self, sim: &mut Simulator) -> Result<ShotRecord, ExecError> {
454 let mut record = ShotRecord::default();
455 self.prepared.run(sim, &mut record)?;
456 Ok(record)
457 }
458}
459
460fn basis_pauli(basis: PauliBasis, idx: usize, n: usize) -> PauliString {
462 PauliString::single(n, idx, sim_basis(basis).into())
463}
464
465pub(crate) fn bloq_pauli(pauli: BloqPauli) -> Pauli {
467 match pauli {
468 BloqPauli::X => Pauli::X,
469 BloqPauli::Y => Pauli::Y,
470 BloqPauli::Z => Pauli::Z,
471 BloqPauli::I => unreachable!("PauliMap omits identity terms"),
472 }
473}
474
475pub(super) fn sim_basis(basis: PauliBasis) -> EnginePauliBasis {
478 match basis {
479 PauliBasis::X => EnginePauliBasis::X,
480 PauliBasis::Y => EnginePauliBasis::Y,
481 PauliBasis::Z => EnginePauliBasis::Z,
482 }
483}
484
485#[cfg(test)]
486mod tests {
487 use super::*;
488 use bloq_ir::circuit::{GateType, Pauli};
489 use bloq_ir::lowering::NodeTemplateInstanceMergeError;
490
491 fn push_op(circuit: &mut CoordCircuit, op: Op) {
494 let entry = circuit.entry_body();
495 circuit
496 .body_mut(entry)
497 .expect("entry body")
498 .ops_mut()
499 .push(op);
500 }
501
502 fn invalid_executor(op: Op) -> ExecError {
503 let mut circuit = CoordCircuit::new();
504 push_op(&mut circuit, op);
505 match CircuitExecutor::new(&circuit) {
506 Err(error) => error,
507 Ok(_) => panic!("malformed circuit was accepted"),
508 }
509 }
510
511 #[test]
512 fn rejects_malformed_operation_arities() {
513 let q = IVec2::ZERO;
514 assert!(matches!(
515 invalid_executor(Op::Gate {
516 gate: GateType::CX,
517 qubits: vec![q],
518 }),
519 ExecError::InvalidCircuit(NodeTemplateInstanceMergeError::OddTwoQubitTargetCount {
520 gate: GateType::CX,
521 targets: 1,
522 })
523 ));
524 assert!(matches!(
525 invalid_executor(Op::Measure {
526 basis: PauliBasis::Z,
527 qubits: vec![q],
528 measurements: vec![],
529 flip_probability: 0.0,
530 }),
531 ExecError::InvalidCircuit(NodeTemplateInstanceMergeError::MeasureOutputCountMismatch {
532 qubits: 1,
533 measurements: 0,
534 })
535 ));
536 assert!(matches!(
537 invalid_executor(Op::MPP {
538 products: vec![[(q, Pauli::Z)].into_iter().collect()],
539 measurements: vec![],
540 }),
541 ExecError::InvalidCircuit(
542 NodeTemplateInstanceMergeError::MppMeasurementCountMismatch {
543 products: 1,
544 measurements: 0,
545 },
546 )
547 ));
548 assert!(matches!(
549 invalid_executor(Op::Gate {
550 gate: GateType::CX,
551 qubits: vec![q, q],
552 }),
553 ExecError::InvalidCircuit(
554 NodeTemplateInstanceMergeError::RepeatedTwoQubitGateTarget {
555 gate: GateType::CX,
556 qubit,
557 }
558 ) if qubit == q
559 ));
560 assert!(matches!(
561 invalid_executor(Op::Depolarize2 {
562 probability: 0.1,
563 qubits: vec![q, q],
564 }),
565 ExecError::InvalidCircuit(
566 NodeTemplateInstanceMergeError::RepeatedDepolarize2Target(qubit)
567 ) if qubit == q
568 ));
569 }
570
571 #[test]
572 fn missing_record_remap_is_typed() {
573 let ops = [Op::Measure {
574 basis: PauliBasis::Z,
575 qubits: vec![IVec2::ZERO],
576 measurements: vec![7],
577 flip_probability: 0.0,
578 }];
579
580 assert!(matches!(
581 validate_record_remap(&ops, &FxHashMap::default()),
582 Err(ExecError::MissingRecordRemap(7))
583 ));
584 }
585
586 #[test]
591 fn conditional_pauli_reading_ahead_is_rejected_at_preparation() {
592 let q = IVec2::ZERO;
593 let coord_to_index: FxHashMap<IVec2, u32> = [(q, 0)].into_iter().collect();
594 let layout = LayoutCtx {
595 coord_to_index: &coord_to_index,
596 offset: IVec2::ZERO,
597 qubit_count: 1,
598 };
599 let correction = Op::ConditionalPauli(vec![ConditionalCorrection {
600 pauli: PauliBasis::X,
601 control: 4,
602 target: q,
603 }]);
604 let measure = Op::Measure {
605 basis: PauliBasis::Z,
606 qubits: vec![q],
607 measurements: vec![4],
608 flip_probability: 0.0,
609 };
610
611 assert!(matches!(
612 prepare_ops(&[correction.clone(), measure.clone()], &layout, &|id| id
613 + 10),
614 Err(ExecError::MissingRecord(14))
615 ));
616 let prepared = prepare_ops(&[measure, correction], &layout, &|id| id + 10)
618 .expect("a correction after its control resolves");
619 assert_eq!(prepared.record_ids(), [14]);
620 }
621
622 #[test]
626 fn conditional_pauli_corrects_the_measured_qubit() {
627 let data = IVec2::ZERO;
628 let mut circuit = CoordCircuit::new();
629 circuit.do_gate(GateType::H, [data]).expect("H is valid");
630 let m0 = circuit.measure(PauliBasis::Z, [data])[0];
631 push_op(
632 &mut circuit,
633 Op::ConditionalPauli(vec![ConditionalCorrection {
634 pauli: PauliBasis::X,
635 control: m0,
636 target: data,
637 }]),
638 );
639 let m1 = circuit.measure(PauliBasis::Z, [data])[0];
640
641 let executor = CircuitExecutor::new(&circuit).expect("valid circuit");
642 let mut corrected_something = false;
643 for seed in 0..16 {
644 let mut sim = Simulator::with_seed(executor.qubit_count(), seed);
645 let record = executor.run_shot(&mut sim).expect("shot runs");
646 assert_eq!(record.get(m1), Some(false), "the correction restores |0>");
647 corrected_something |= record.get(m0) == Some(true);
648 }
649 assert!(
650 corrected_something,
651 "the uncorrected outcome should vary across seeds"
652 );
653 }
654
655 #[test]
656 fn explicit_noise_ops_lower_to_backend_channels() {
657 let q0 = IVec2::ZERO;
658 let q1 = IVec2::X;
659 let coord_to_index = [(q0, 0), (q1, 1)].into_iter().collect();
660 let layout = LayoutCtx {
661 coord_to_index: &coord_to_index,
662 offset: IVec2::ZERO,
663 qubit_count: 2,
664 };
665 let prepared = prepare_ops(
666 &[
667 Op::Depolarize1 {
668 probability: 0.3,
669 qubits: vec![q0],
670 },
671 Op::Depolarize2 {
672 probability: 0.15,
673 qubits: vec![q0, q1],
674 },
675 Op::PauliError {
676 probability: 0.4,
677 pauli: PauliBasis::Y,
678 qubits: vec![q1],
679 },
680 Op::Measure {
681 basis: PauliBasis::Z,
682 qubits: vec![q0],
683 measurements: vec![0],
684 flip_probability: 0.25,
685 },
686 ],
687 &layout,
688 &|id| id,
689 )
690 .unwrap();
691
692 let channel_sizes: Vec<_> = prepared
693 .instructions
694 .iter()
695 .filter_map(|instruction| match instruction {
696 Instruction::RandomPauli {
697 probabilities,
698 alternatives,
699 heralded,
700 } => Some((probabilities.len(), alternatives.len(), *heralded)),
701 _ => None,
702 })
703 .collect();
704 assert_eq!(
705 channel_sizes,
706 [(3, 3, false), (15, 15, false), (1, 1, false)]
707 );
708 assert!(matches!(
709 prepared.instructions.last(),
710 Some(Instruction::MeasureWithReadoutError {
711 probability,
712 ..
713 }) if *probability == 0.25
714 ));
715 assert_eq!(prepared.record_ids(), [0]);
716 }
717
718 #[test]
719 fn pauli_error_changes_state_while_readout_error_only_changes_record() {
720 let qubit = IVec2::ZERO;
721 let mut circuit = CoordCircuit::new();
722 push_op(
723 &mut circuit,
724 Op::PauliError {
725 probability: 1.0,
726 pauli: PauliBasis::X,
727 qubits: vec![qubit],
728 },
729 );
730 let measurement = circuit.measure(PauliBasis::Z, [qubit])[0];
731 let entry = circuit.entry_body();
732 let Op::Measure {
733 flip_probability, ..
734 } = circuit
735 .body_mut(entry)
736 .expect("entry body")
737 .ops_mut()
738 .last_mut()
739 .expect("measurement")
740 else {
741 panic!("last op is the measurement just appended");
742 };
743 *flip_probability = 1.0;
744
745 let executor = CircuitExecutor::new(&circuit).expect("valid noisy circuit");
746 let mut simulator = Simulator::with_seed(executor.qubit_count(), 7);
747 let record = executor.run_shot(&mut simulator).unwrap();
748 assert_eq!(record.get(measurement), Some(false));
749 assert_eq!(simulator.peek_z(0).unwrap(), -1.0);
750 }
751
752 #[test]
756 fn mpp_reads_a_bell_stabilizer() {
757 let (d0, d1) = (IVec2::ZERO, IVec2::new(1, 0));
758 let mut circuit = CoordCircuit::new();
759 circuit.do_gate(GateType::H, [d0]).expect("H is valid");
760 circuit
761 .do_gate(GateType::CX, [d0, d1])
762 .expect("CX is valid");
763 let xx = || PauliMap::from_iter([(d0, Pauli::X), (d1, Pauli::X)]);
764 let first = circuit.measure_pauli_products([xx()]).expect("XX product")[0];
765 let again = circuit.measure_pauli_products([xx()]).expect("XX product")[0];
766
767 let executor = CircuitExecutor::new(&circuit).expect("valid circuit");
768 let mut sim = Simulator::with_seed(executor.qubit_count(), 7);
769 let record = executor.run_shot(&mut sim).expect("shot runs");
770 assert_eq!(record.get(first), Some(false), "XX stabilizes a Bell pair");
771 assert_eq!(record.get(again), record.get(first));
772 }
773}