1use std::collections::BTreeMap;
17
18use bloq_circuit::{
19 BodyId, CircuitError, ConditionalCorrection, CoordCircuit, GateType, MeasurementFrameError, Op,
20 Pauli, PauliBasis, PauliMap, measurement_gate_name,
21};
22use bloq_ir::lowering::{InstanceMeasurement, NodeEmissionPlan};
23use glam::IVec2;
24use rustc_hash::FxHashMap;
25
26use crate::dialect::{HONEST_T_TAG, StimDialect};
27use crate::emit::{StimEmissionError, conditional_pauli_gate_name, pauli_error_gate_name};
28use crate::layout::QubitLayout;
29use crate::measurement_frame::{MAX_STIM_RECORD_LOOKBACK, stim_record_lookback};
30use crate::text_utils::{push_int, push_number_prefer_int, write_stim_tag};
31
32#[derive(Debug, Clone, Copy)]
34pub struct PlanStimOptions<'a> {
35 qubit_map: &'a FxHashMap<IVec2, u32>,
36 dialect: StimDialect,
37 tag: Option<&'a str>,
38}
39
40impl<'a> PlanStimOptions<'a> {
41 pub fn new(qubit_map: &'a FxHashMap<IVec2, u32>) -> Self {
51 Self {
52 qubit_map,
53 dialect: StimDialect::default(),
54 tag: None,
55 }
56 }
57
58 #[must_use]
60 pub fn with_dialect(mut self, dialect: StimDialect) -> Self {
61 self.dialect = dialect;
62 self
63 }
64
65 #[must_use]
72 pub fn with_tag(mut self, tag: &'a str) -> Self {
73 self.tag = Some(tag);
74 self
75 }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct PlanStim {
81 pub text: String,
83 pub measurement_columns: BTreeMap<InstanceMeasurement, u32>,
89 pub measurement_count: u32,
92}
93
94pub fn emit_plan_stim(
131 plan: &NodeEmissionPlan,
132 options: &PlanStimOptions<'_>,
133) -> Result<PlanStim, StimEmissionError> {
134 let layout = QubitLayout::new(options.qubit_map.clone())?;
135 let mut emitter = PlanStimEmitter {
136 circuit: &plan.circuit,
137 layout: &layout,
138 dialect: options.dialect,
139 tag: options.tag,
140 text: String::new(),
141 columns: FxHashMap::default(),
142 count: 0,
143 stack: Vec::new(),
144 };
145 emitter.emit_body(plan.circuit.entry_body())?;
146
147 debug_assert_eq!(
152 plan.circuit
153 .expanded_measurement_columns()
154 .map(|expanded| expanded.count()),
155 Ok(emitter.count),
156 "plan lowering and the column walk disagree on the record count",
157 );
158
159 let measurement_columns =
160 plan.measurements
161 .iter()
162 .map(|(&measurement, local)| {
163 let column = emitter.columns.get(local).copied().ok_or(
164 StimEmissionError::MalformedGraph(
165 "emission plan names a measurement its circuit never emits",
166 ),
167 )?;
168 Ok((measurement, column))
169 })
170 .collect::<Result<BTreeMap<_, _>, StimEmissionError>>()?;
171
172 Ok(PlanStim {
173 text: emitter.text,
174 measurement_columns,
175 measurement_count: emitter.count,
176 })
177}
178
179struct PlanStimEmitter<'a> {
180 circuit: &'a CoordCircuit,
181 layout: &'a QubitLayout,
182 dialect: StimDialect,
183 tag: Option<&'a str>,
184 text: String,
185 columns: FxHashMap<u32, u32>,
191 count: u32,
192 stack: Vec<BodyId>,
195}
196
197impl PlanStimEmitter<'_> {
198 fn emit_body(&mut self, body: BodyId) -> Result<(), StimEmissionError> {
199 if self.stack.contains(&body) {
200 return Err(CircuitError::InvalidCircuitBody(body).into());
201 }
202 self.stack.push(body);
203 let circuit = self.circuit;
205 let source = circuit
206 .body(body)
207 .ok_or(CircuitError::InvalidCircuitBody(body))?;
208 for op in source.ops() {
209 self.emit_op(op)?;
210 }
211 self.stack.pop();
212 Ok(())
213 }
214
215 fn emit_op(&mut self, op: &Op) -> Result<(), StimEmissionError> {
216 match op {
217 Op::Gate { gate, qubits } => self.emit_gate(*gate, qubits),
218 Op::Measure {
219 basis,
220 qubits,
221 measurements,
222 flip_probability,
223 } => {
224 self.record_measurements(measurements)?;
225 let arg = (*flip_probability != 0.0).then_some(*flip_probability);
226 self.write_qubit_line(measurement_gate_name(*basis), self.tag, arg, qubits)
227 }
228 Op::MPP {
229 products,
230 measurements,
231 } => {
232 self.record_measurements(measurements)?;
233 self.emit_mpp(products)
234 }
235 Op::ConditionalPauli(corrections) => self.emit_conditional_paulis(corrections),
236 Op::Depolarize1 {
237 probability,
238 qubits,
239 } => self.write_qubit_line("DEPOLARIZE1", self.tag, Some(*probability), qubits),
240 Op::Depolarize2 {
241 probability,
242 qubits,
243 } => self.write_qubit_line("DEPOLARIZE2", self.tag, Some(*probability), qubits),
244 Op::PauliError {
245 probability,
246 pauli,
247 qubits,
248 } => self.write_qubit_line(
249 pauli_error_gate_name(*pauli),
250 self.tag,
251 Some(*probability),
252 qubits,
253 ),
254 Op::Tick => self.write_qubit_line("TICK", self.tag, None, &[]),
255 Op::Repeat { body, repetitions } => {
256 for _ in 0..*repetitions {
257 self.emit_body(*body)?;
258 }
259 Ok(())
260 }
261 }
262 }
263
264 fn emit_gate(&mut self, gate: GateType, qubits: &[IVec2]) -> Result<(), StimEmissionError> {
265 let (name, tag) = match (self.dialect, gate) {
266 (StimDialect::Stim, GateType::T) => ("S", Some(HONEST_T_TAG)),
267 (StimDialect::Stim, GateType::T_DAG) => ("S_DAG", Some(HONEST_T_TAG)),
268 (StimDialect::Stim, gate) if gate.is_non_clifford() => {
272 return Err(StimEmissionError::UnsupportedGate(gate));
273 }
274 (_, gate) => (gate.into(), self.tag),
275 };
276 self.write_qubit_line(name, tag, None, qubits)
277 }
278
279 fn emit_mpp(&mut self, products: &[PauliMap]) -> Result<(), StimEmissionError> {
284 let mut targets = String::new();
285 for product in products {
286 let mut first = true;
287 for (coord, pauli) in product.iter().filter(|&(_, &pauli)| pauli != Pauli::I) {
288 let qubit = self.qubit(*coord)?;
289 targets.push(if first { ' ' } else { '*' });
290 first = false;
291 targets.push(match pauli {
292 Pauli::X => 'X',
293 Pauli::Y => 'Y',
294 Pauli::Z => 'Z',
295 Pauli::I => unreachable!("identity Paulis are filtered out above"),
296 });
297 targets.push_str(self.layout.label(qubit));
298 }
299 }
300 if targets.is_empty() {
301 return Ok(());
302 }
303 self.write_instruction_prefix("MPP", self.tag, None);
304 self.text.push_str(&targets);
305 self.text.push('\n');
306 Ok(())
307 }
308
309 fn emit_conditional_paulis(
310 &mut self,
311 corrections: &[ConditionalCorrection],
312 ) -> Result<(), StimEmissionError> {
313 for correction in corrections {
314 let lookback = self.lookback_to(correction.control)?;
315 let qubit = self.qubit(correction.target)?;
316 match correction.pauli {
317 PauliBasis::Y => {
322 self.write_feedback("CX", lookback, qubit);
323 self.write_feedback("CZ", lookback, qubit);
324 }
325 pauli => self.write_feedback(conditional_pauli_gate_name(pauli), lookback, qubit),
326 }
327 }
328 Ok(())
329 }
330
331 fn record_measurements(&mut self, measurements: &[u32]) -> Result<(), StimEmissionError> {
336 for &measurement in measurements {
337 let next = self.count.checked_add(1).ok_or_else(|| {
338 CircuitError::from(MeasurementFrameError::EmittedMeasurementCountOverflow {
339 emitted_count: self.count,
340 additional: 1,
341 })
342 })?;
343 self.columns.insert(measurement, self.count);
344 self.count = next;
345 }
346 Ok(())
347 }
348
349 fn lookback_to(&self, measurement: u32) -> Result<i32, StimEmissionError> {
352 let column =
353 self.columns
354 .get(&measurement)
355 .copied()
356 .ok_or(StimEmissionError::MalformedGraph(
357 "conditional Pauli control is read before its measurement is emitted",
358 ))?;
359 let distance = self.count - column;
360 stim_record_lookback(distance).ok_or_else(|| {
361 CircuitError::from(MeasurementFrameError::RecordLookbackOutOfRange {
362 lookback: distance,
363 max: MAX_STIM_RECORD_LOOKBACK,
364 })
365 .into()
366 })
367 }
368
369 fn qubit(&self, coord: IVec2) -> Result<u32, StimEmissionError> {
370 self.layout
371 .get(coord)
372 .ok_or_else(|| CircuitError::QubitNotFoundInLayout(coord).into())
373 }
374
375 fn write_feedback(&mut self, gate: &str, lookback: i32, qubit: u32) {
376 self.write_instruction_prefix(gate, self.tag, None);
377 self.text.push_str(" rec[");
378 push_int(&mut self.text, lookback);
379 self.text.push_str("] ");
380 self.text.push_str(self.layout.label(qubit));
381 self.text.push('\n');
382 }
383
384 fn write_qubit_line(
385 &mut self,
386 gate: &str,
387 tag: Option<&str>,
388 arg: Option<f64>,
389 qubits: &[IVec2],
390 ) -> Result<(), StimEmissionError> {
391 let indices = qubits
394 .iter()
395 .map(|&coord| self.qubit(coord))
396 .collect::<Result<Vec<_>, _>>()?;
397 self.write_instruction_prefix(gate, tag, arg);
398 for qubit in indices {
399 self.text.push(' ');
400 self.text.push_str(self.layout.label(qubit));
401 }
402 self.text.push('\n');
403 Ok(())
404 }
405
406 fn write_instruction_prefix(&mut self, gate: &str, tag: Option<&str>, arg: Option<f64>) {
407 self.text.push_str(gate);
408 if let Some(tag) = tag {
409 write_stim_tag(&mut self.text, tag);
410 }
411 if let Some(arg) = arg {
412 self.text.push('(');
413 push_number_prefer_int(&mut self.text, arg);
414 self.text.push(')');
415 }
416 }
417}
418
419#[cfg(test)]
420mod tests {
421 use bloq_circuit::CircuitBody;
422 use bloq_ir::lowering::TemplateInstanceId;
423 use glam::ivec2;
424
425 use super::*;
426
427 fn instance_measurement(measurement: u32) -> InstanceMeasurement {
428 InstanceMeasurement {
429 instance: TemplateInstanceId(0),
430 measurement,
431 }
432 }
433
434 fn plan_of(circuit: CoordCircuit, measurements: &[u32]) -> NodeEmissionPlan {
435 NodeEmissionPlan {
436 normalized_templates: None,
437 circuit,
438 measurements: measurements
439 .iter()
440 .map(|&id| (instance_measurement(id), id))
441 .collect(),
442 bodies: FxHashMap::default(),
443 }
444 }
445
446 fn layout_of(circuit: &CoordCircuit) -> FxHashMap<IVec2, u32> {
447 circuit.build_coord_to_index()
448 }
449
450 fn every_op_circuit() -> (CoordCircuit, Vec<u32>) {
453 let [a, b] = [ivec2(0, 0), ivec2(1, 0)];
454 let mut circuit = CoordCircuit::new();
455 circuit.do_gate(GateType::RZ, [a, b]).unwrap();
456 circuit.tick();
457 circuit.do_gate(GateType::CX, [a, b]).unwrap();
458 circuit
459 .body_mut(circuit.entry_body())
460 .unwrap()
461 .ops_mut()
462 .extend([
463 Op::Depolarize1 {
464 probability: 0.001,
465 qubits: vec![a],
466 },
467 Op::Depolarize2 {
468 probability: 0.002,
469 qubits: vec![a, b],
470 },
471 Op::PauliError {
472 probability: 0.003,
473 pauli: PauliBasis::Z,
474 qubits: vec![b],
475 },
476 ]);
477 let mpp = circuit
478 .measure_pauli_products([PauliMap::from_unique_entries([
479 (a, Pauli::X),
480 (b, Pauli::X),
481 ])])
482 .unwrap();
483 let measured = circuit.measure(PauliBasis::Z, [a])[0];
484 circuit
485 .body_mut(circuit.entry_body())
486 .unwrap()
487 .ops_mut()
488 .push(Op::ConditionalPauli(vec![
489 ConditionalCorrection {
490 pauli: PauliBasis::X,
491 control: measured,
492 target: b,
493 },
494 ConditionalCorrection {
495 pauli: PauliBasis::Y,
496 control: mpp[0],
497 target: b,
498 },
499 ]));
500 let looped = circuit.reserve_measurement_id(b);
501 let body = circuit.add_body(CircuitBody::from_ops(vec![
502 Op::Gate {
503 gate: GateType::T,
504 qubits: vec![a],
505 },
506 Op::Measure {
507 basis: PauliBasis::X,
508 qubits: vec![b],
509 measurements: vec![looped],
510 flip_probability: 0.004,
511 },
512 ]));
513 circuit.push_repeat(body, 2);
514 (circuit, vec![mpp[0], measured, looped])
515 }
516
517 #[test]
518 fn every_op_lowers_and_unrolls_its_repeat() {
519 let (circuit, measurements) = every_op_circuit();
520 let qubit_map = layout_of(&circuit);
521 let plan = plan_of(circuit, &measurements);
522
523 let lowered = emit_plan_stim(&plan, &PlanStimOptions::new(&qubit_map)).unwrap();
524
525 insta::assert_snapshot!(lowered.text, @r"
526 R 0 1
527 TICK
528 CX 0 1
529 DEPOLARIZE1(0.001) 0
530 DEPOLARIZE2(0.002) 0 1
531 Z_ERROR(0.003) 1
532 MPP X0*X1
533 M 0
534 CX rec[-1] 1
535 CX rec[-2] 1
536 CZ rec[-2] 1
537 S[HONEST_T] 0
538 MX(0.004) 1
539 S[HONEST_T] 0
540 MX(0.004) 1
541 ");
542 assert_eq!(lowered.measurement_count, 4);
545 assert_eq!(
546 lowered.measurement_columns[&instance_measurement(measurements[2])],
547 3,
548 "a looped measurement reports its final occurrence",
549 );
550 }
551
552 #[test]
555 fn the_two_dialects_are_related_by_the_text_codec() {
556 let (circuit, measurements) = every_op_circuit();
557 let qubit_map = layout_of(&circuit);
558 let plan = plan_of(circuit, &measurements);
559 let options = PlanStimOptions::new(&qubit_map);
560
561 let as_stim = emit_plan_stim(&plan, &options).unwrap();
562 let as_clifft = emit_plan_stim(&plan, &options.with_dialect(StimDialect::Clifft)).unwrap();
563
564 assert!(as_clifft.text.contains("T 0"), "{}", as_clifft.text);
565 assert_eq!(crate::stim_to_clifft_text(&as_stim.text), as_clifft.text);
566 assert_eq!(crate::clifft_to_stim_text(&as_clifft.text), as_stim.text);
567 assert_eq!(as_stim.measurement_columns, as_clifft.measurement_columns);
568 }
569
570 #[test]
571 fn plan_columns_agree_with_the_standalone_column_walk() {
572 let (circuit, measurements) = every_op_circuit();
573 let qubit_map = layout_of(&circuit);
574 let expanded = circuit.expanded_measurement_columns().unwrap();
575 let plan = plan_of(circuit, &measurements);
576
577 let lowered = emit_plan_stim(&plan, &PlanStimOptions::new(&qubit_map)).unwrap();
578
579 assert_eq!(lowered.measurement_count, expanded.count());
580 for (measurement, column) in &lowered.measurement_columns {
581 assert_eq!(expanded.column(measurement.measurement), Some(*column));
582 }
583 }
584
585 #[test]
586 fn a_caller_tag_marks_every_instruction_but_yields_to_honest_t() {
587 let (circuit, measurements) = every_op_circuit();
588 let qubit_map = layout_of(&circuit);
589 let plan = plan_of(circuit, &measurements);
590
591 let lowered = emit_plan_stim(
592 &plan,
593 &PlanStimOptions::new(&qubit_map).with_tag("T_SOURCE"),
594 )
595 .unwrap();
596
597 assert!(lowered.text.contains("TICK[T_SOURCE]"));
598 assert!(lowered.text.contains("MPP[T_SOURCE] X0*X1"));
599 assert!(lowered.text.contains("CX[T_SOURCE] rec[-1] 1"));
600 assert!(
601 lowered.text.contains("S[HONEST_T] 0"),
602 "the dialect tag is load-bearing and wins: {}",
603 lowered.text
604 );
605 }
606
607 #[test]
608 fn a_qubit_outside_the_map_is_reported_rather_than_emitted() {
609 let mut circuit = CoordCircuit::new();
610 circuit.do_gate(GateType::H, [ivec2(4, 4)]).unwrap();
611 let plan = plan_of(circuit, &[]);
612 let qubit_map = FxHashMap::from_iter([(ivec2(0, 0), 0)]);
613
614 assert!(matches!(
615 emit_plan_stim(&plan, &PlanStimOptions::new(&qubit_map)),
616 Err(StimEmissionError::Circuit(CircuitError::QubitNotFoundInLayout(
617 coord
618 ))) if coord == ivec2(4, 4)
619 ));
620 }
621
622 #[test]
625 fn stim_dialect_rejects_a_non_clifford_without_an_s_proxy() {
626 let qubit = ivec2(0, 0);
627 let mut circuit = CoordCircuit::new();
628 circuit.do_gate(GateType::T_XZ, [qubit]).unwrap();
629 let qubit_map = layout_of(&circuit);
630 let plan = plan_of(circuit, &[]);
631
632 assert!(matches!(
633 emit_plan_stim(&plan, &PlanStimOptions::new(&qubit_map)),
634 Err(StimEmissionError::UnsupportedGate(GateType::T_XZ))
635 ));
636 let clifft = PlanStimOptions::new(&qubit_map).with_dialect(StimDialect::Clifft);
637 assert_eq!(emit_plan_stim(&plan, &clifft).unwrap().text, "T_XZ 0\n");
638 }
639
640 #[test]
641 fn a_feedback_control_read_before_its_measurement_is_reported() {
642 let qubit = ivec2(0, 0);
643 let mut circuit = CoordCircuit::new();
644 let measurement = circuit.reserve_measurement_id(qubit);
645 circuit
646 .body_mut(circuit.entry_body())
647 .unwrap()
648 .ops_mut()
649 .push(Op::ConditionalPauli(vec![ConditionalCorrection {
650 pauli: PauliBasis::X,
651 control: measurement,
652 target: qubit,
653 }]));
654 let qubit_map = layout_of(&circuit);
655 let plan = plan_of(circuit, &[]);
656
657 assert!(matches!(
658 emit_plan_stim(&plan, &PlanStimOptions::new(&qubit_map)),
659 Err(StimEmissionError::MalformedGraph(_))
660 ));
661 }
662
663 #[test]
664 fn feedback_lookbacks_obey_stims_limit() {
665 let circuit = CoordCircuit::new();
666 let layout = QubitLayout::default();
667 let emitter = PlanStimEmitter {
668 circuit: &circuit,
669 layout: &layout,
670 dialect: StimDialect::Stim,
671 tag: None,
672 text: String::new(),
673 columns: FxHashMap::from_iter([(0, 0)]),
674 count: u32::MAX,
675 stack: Vec::new(),
676 };
677
678 assert!(matches!(
679 emitter.lookback_to(0),
680 Err(StimEmissionError::Circuit(CircuitError::MeasurementFrame(
681 MeasurementFrameError::RecordLookbackOutOfRange {
682 lookback,
683 max: MAX_STIM_RECORD_LOOKBACK,
684 }
685 ))) if lookback == u32::MAX
686 ));
687 }
688
689 #[test]
690 fn standalone_plan_rejects_qubit_indices_outside_stim_target_payload() {
691 let qubit = ivec2(0, 0);
692 let mut circuit = CoordCircuit::new();
693 circuit.do_gate(GateType::H, [qubit]).unwrap();
694 let plan = plan_of(circuit, &[]);
695 for index in [1 << 24, u32::MAX] {
696 let qubit_map = FxHashMap::from_iter([(qubit, index)]);
697 for dialect in [StimDialect::Stim, StimDialect::Clifft] {
698 let options = PlanStimOptions::new(&qubit_map).with_dialect(dialect);
699 assert_eq!(
700 emit_plan_stim(&plan, &options),
701 Err(StimEmissionError::QubitIndexOutOfRange {
702 index: index as usize,
703 max: 16_777_215,
704 })
705 );
706 }
707 }
708 }
709
710 #[test]
711 fn standalone_plan_preserves_sparse_caller_qubit_indices() {
712 let qubit = ivec2(0, 0);
713 let mut circuit = CoordCircuit::new();
714 circuit.do_gate(GateType::H, [qubit]).unwrap();
715 let plan = plan_of(circuit, &[]);
716 for index in [100_000, 16_777_215] {
717 let qubit_map = FxHashMap::from_iter([(qubit, index)]);
718 let output = emit_plan_stim(&plan, &PlanStimOptions::new(&qubit_map)).unwrap();
719 assert_eq!(output.text, format!("H {index}\n"));
720 }
721 }
722
723 #[test]
724 fn standalone_plan_emits_sparse_measurement_ids_without_dense_allocation() {
725 let qubit = ivec2(0, 0);
726 let measurement = u32::MAX - 1;
727 let mut circuit = CoordCircuit::new();
728 circuit.register_measurement_id(measurement, qubit);
729 circuit
730 .body_mut(circuit.entry_body())
731 .unwrap()
732 .ops_mut()
733 .extend([
734 Op::Measure {
735 basis: PauliBasis::Z,
736 qubits: vec![qubit],
737 measurements: vec![measurement],
738 flip_probability: 0.0,
739 },
740 Op::ConditionalPauli(vec![ConditionalCorrection {
741 pauli: PauliBasis::X,
742 control: measurement,
743 target: qubit,
744 }]),
745 ]);
746 bloq_ir::lowering::validate_template_circuit(&circuit).unwrap();
747 let layout = layout_of(&circuit);
748 let plan = plan_of(circuit, &[measurement]);
749
750 let emitted = emit_plan_stim(&plan, &PlanStimOptions::new(&layout)).unwrap();
751
752 assert_eq!(emitted.text, "M 0\nCX rec[-1] 0\n");
753 assert_eq!(emitted.measurement_count, 1);
754 assert_eq!(
755 emitted.measurement_columns[&instance_measurement(measurement)],
756 0
757 );
758 }
759
760 #[cfg(feature = "verify")]
761 #[test]
762 fn arbitrary_caller_tags_round_trip_through_stim() {
763 let mut circuit = CoordCircuit::new();
764 circuit.do_gate(GateType::H, [ivec2(0, 0)]).unwrap();
765 let qubit_map = layout_of(&circuit);
766 let plan = plan_of(circuit, &[]);
767 let tag = "a]b\\c\nd\rλ";
768 let lowered =
769 emit_plan_stim(&plan, &PlanStimOptions::new(&qubit_map).with_tag(tag)).unwrap();
770 let parsed: stim::Circuit = lowered.text.parse().unwrap();
771 let instructions = parsed.into_iter().collect::<Vec<_>>();
772 let [stim::CircuitItem::Instruction(instruction)] = instructions.as_slice() else {
773 panic!("one tagged gate expected");
774 };
775 assert_eq!(instruction.tag(), tag);
776 }
777
778 #[cfg(feature = "verify")]
779 #[test]
780 fn lowered_text_parses_as_stim_with_the_reported_record_count() {
781 let (circuit, measurements) = every_op_circuit();
782 let qubit_map = layout_of(&circuit);
783 let plan = plan_of(circuit, &measurements);
784
785 let lowered = emit_plan_stim(&plan, &PlanStimOptions::new(&qubit_map)).unwrap();
786
787 let parsed: stim::Circuit = lowered.text.parse().expect("lowered text is valid Stim");
788 assert_eq!(
789 parsed.num_measurements(),
790 u64::from(lowered.measurement_count)
791 );
792 }
793}