1use std::collections::BTreeMap;
2
3use bloq_utils::Pauli;
4use glam::IVec2;
5use itertools::Itertools;
6use rustc_hash::FxHashMap;
7
8use bloq_circuit::{
9 BodyId, CircuitError, ConditionalCorrection, CoordCircuit, DetectorParity, DetectorTerm,
10 GateType, LoopCarriedDetectorState, LoopStateId, MeasurementFrameError, Op, PauliBasis,
11 PauliMap, measurement_gate_name,
12};
13use thiserror::Error;
14
15use crate::layout::QubitLayout;
16use crate::measurement_frame::MeasurementFrame;
17use crate::text_utils::{push_int, push_number_prefer_int};
18
19#[derive(Debug, Error, Clone, PartialEq, Eq)]
21pub enum StimEmissionError {
22 #[error("invalid Bloq IR: {0}")]
24 InvalidProgram(#[from] bloq_ir::BloqValidationError),
25 #[error("{0}")]
27 DetectorBundle(#[from] bloq_ir::DetectorBundleError),
28 #[error("node annotation names unknown measurement {0:?}")]
30 UnknownInstanceMeasurement(bloq_ir::lowering::InstanceMeasurement),
31 #[error("node annotation references unsupported loop state {0:?}")]
33 NodeLoopStateUnsupported(LoopStateId),
34 #[error("unsupported Stim backend gate: {0}")]
37 UnsupportedGate(GateType),
38 #[error("unsupported Stim backend node: {0}")]
41 UnsupportedNode(&'static str),
42 #[error("moment-aligned Stim emission does not support noise")]
44 AlignMomentsWithNoise,
45 #[error("moment-aligned Stim emission does not support segmented output")]
47 AlignMomentsWithSegments,
48 #[error("moment-aligned Stim emission cannot place metadata for an empty quantum circuit")]
50 AlignMomentsWithEmptyMetadata,
51 #[error("{0}")]
53 MomentAlignment(#[from] bloq_ir::MomentAlignmentError),
54 #[error("malformed graph: {0}")]
59 MalformedGraph(&'static str),
60 #[error("{0}")]
64 InstanceMerge(#[from] bloq_ir::lowering::NodeTemplateInstanceMergeError),
65 #[error("failed to flatten Stim emission: {0}")]
67 Flatten(#[from] bloq_ir::FlattenError),
68 #[error("physical error probability must be finite and in [0, 1]")]
70 InvalidNoiseProbability,
71 #[error("Stim {space} index {index} exceeds the u32 index range")]
73 IndexOutOfRange {
74 space: &'static str,
76 index: usize,
78 },
79 #[error("Stim qubit index {index} exceeds the target limit {max}")]
81 QubitIndexOutOfRange {
82 index: usize,
84 max: u32,
86 },
87 #[error("{0}")]
90 CoordinateOverflow(#[from] bloq_ir::CoordinateOverflowError),
91 #[error("{0}")]
94 Circuit(#[from] CircuitError),
95 #[error("{0}")]
97 ClassicalResolution(#[from] bloq_ir::ResolveError),
98 #[error("{0}")]
102 CyclicGraph(#[from] bloq_ir::CycleDetected),
103}
104
105#[derive(Debug, Clone, Default)]
106pub(crate) struct StimCircuitAnnotations {
107 pub detectors: Vec<StimDetector>,
108 pub repeat_states: Vec<StimRepeatState>,
109}
110
111#[derive(Debug, Clone, PartialEq)]
112pub(crate) struct StimDetector {
113 pub scope: StimAnnotationScope,
114 pub parity: DetectorParity,
115 pub coords: Option<bloq_circuit::DetectorCoords>,
116 pub postselection: bool,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
120pub(crate) struct StimRepeatState {
121 pub body: BodyId,
122 pub state: LoopStateId,
123 pub initial: DetectorParity,
124 pub next: DetectorParity,
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub(crate) enum StimAnnotationScope {
129 TopLevel,
130 RepeatBody { body: BodyId },
131}
132
133#[derive(Debug, Clone, Copy)]
134pub(crate) struct StimEmitOptions {
135 pub enable_clifford_proxy: bool,
136 pub allow_non_clifford: bool,
137 pub emit_postselection_tags: bool,
138 pub emit_qubit_coords: bool,
139 pub qubit_offset: IVec2,
140}
141
142impl Default for StimEmitOptions {
143 fn default() -> Self {
144 Self {
145 enable_clifford_proxy: false,
146 allow_non_clifford: false,
147 emit_postselection_tags: true,
148 emit_qubit_coords: true,
149 qubit_offset: IVec2::ZERO,
150 }
151 }
152}
153
154pub(crate) struct StimEmitContext<'a> {
155 pub layout: &'a QubitLayout,
156 pub frame: &'a mut MeasurementFrame,
157 pub resolved_loop_detector_states: &'a mut ResolvedLoopStates,
158 pub output: &'a mut String,
159 pub indent: usize,
160}
161
162pub(crate) type ResolvedLoopStates = FxHashMap<LoopStateId, Vec<u32>>;
164
165type LoopStateFrame = BTreeMap<LoopStateId, Vec<u32>>;
169
170fn push_lookback(output: &mut String, lookback: i32) {
172 output.push_str(" rec[");
173 push_int(output, lookback);
174 output.push(']');
175}
176
177pub(crate) trait StimMeasurementMapper {
178 fn map_measurement(&mut self, measurement: u32) -> Result<u32, CircuitError>;
183
184 fn map_annotation_measurement(&mut self, measurement: u32) -> Result<u32, CircuitError> {
185 self.map_measurement(measurement)
186 }
187}
188
189#[cfg(any(test, feature = "verify"))]
193#[derive(Debug, Clone, Copy, Default)]
194pub(crate) struct IdentityMeasurementMapper;
195
196#[cfg(any(test, feature = "verify"))]
197impl StimMeasurementMapper for IdentityMeasurementMapper {
198 fn map_measurement(&mut self, measurement: u32) -> Result<u32, CircuitError> {
199 Ok(measurement)
200 }
201}
202
203#[cfg(any(test, feature = "verify"))]
208pub(crate) fn emit_standalone_stim(
209 circuit: &CoordCircuit,
210 annotations: Option<&StimCircuitAnnotations>,
211 layout: &QubitLayout,
212 enable_clifford_proxy: bool,
213) -> Result<(String, MeasurementFrame), CircuitError> {
214 let mut output = String::new();
215 let mut frame = MeasurementFrame::default();
216 let mut resolved_loop_detector_states = ResolvedLoopStates::default();
217 let mut ctx = StimEmitContext {
218 layout,
219 frame: &mut frame,
220 resolved_loop_detector_states: &mut resolved_loop_detector_states,
221 output: &mut output,
222 indent: 0,
223 };
224 emit_stim_circuit(
225 circuit,
226 annotations,
227 &mut ctx,
228 &mut IdentityMeasurementMapper,
229 StimEmitOptions {
230 enable_clifford_proxy,
231 emit_qubit_coords: true,
232 ..StimEmitOptions::default()
233 },
234 )?;
235 Ok((output, frame))
236}
237
238pub(crate) fn emit_stim_circuit<M: StimMeasurementMapper>(
239 circuit: &CoordCircuit,
240 annotations: Option<&StimCircuitAnnotations>,
241 ctx: &mut StimEmitContext<'_>,
242 mapper: &mut M,
243 options: StimEmitOptions,
244) -> Result<bool, CircuitError> {
245 let base_indent = ctx.indent;
246 let mut emitter = StimTextEmitter {
247 source: circuit,
248 annotations,
249 ctx,
250 mapper,
251 options,
252 detector_state_stack: Vec::new(),
253 base_indent,
254 needs_node_boundary_tick: false,
255 };
256 if emitter.options.emit_qubit_coords {
257 emitter.emit_qubit_coords();
258 }
259 emitter.emit_body(circuit.entry_body())?;
260 emitter.emit_side_table_annotations(circuit.entry_body())?;
261 Ok(emitter.needs_node_boundary_tick)
262}
263
264struct StimTextEmitter<'circuit, 'ctx, 'data, 'mapper, M>
265where
266 M: StimMeasurementMapper,
267{
268 source: &'circuit CoordCircuit,
269 annotations: Option<&'circuit StimCircuitAnnotations>,
270 ctx: &'ctx mut StimEmitContext<'data>,
271 mapper: &'mapper mut M,
272 options: StimEmitOptions,
273 detector_state_stack: Vec<LoopStateFrame>,
274 base_indent: usize,
276 needs_node_boundary_tick: bool,
277}
278
279#[derive(Debug, Clone, Copy, PartialEq, Eq)]
280enum StimTarget {
281 Rec(i32),
282 Pauli { pauli: Pauli, qubit: u32 },
283}
284
285impl<M> StimTextEmitter<'_, '_, '_, '_, M>
286where
287 M: StimMeasurementMapper,
288{
289 fn note_instruction(&mut self, needs_node_boundary_tick: bool) {
290 if self.ctx.indent == self.base_indent {
291 self.needs_node_boundary_tick = needs_node_boundary_tick;
292 }
293 }
294
295 fn emit_qubit_coords(&mut self) {
296 if self.ctx.layout.coord_index().is_empty() {
300 return;
301 }
302 self.note_instruction(true);
303 let coords = self
304 .ctx
305 .layout
306 .coord_index()
307 .iter()
308 .map(|(coord, qubit)| (*coord, *qubit))
309 .sorted_by_key(|&(_, qubit)| qubit);
310 emit_qubit_coords_lines(self.ctx.output, coords);
311 }
312
313 fn emit_body(&mut self, body: BodyId) -> Result<(), CircuitError> {
314 let Some(source) = self.source.body(body) else {
315 return Err(CircuitError::InvalidCircuitBody(body));
316 };
317 for op in source.ops() {
318 self.emit_op(op)?;
319 }
320 Ok(())
321 }
322
323 fn emit_side_table_annotations(&mut self, body: BodyId) -> Result<(), CircuitError> {
324 let Some(annotations) = self.annotations else {
325 return Ok(());
326 };
327 for detector in &annotations.detectors {
328 if detector.scope == scope_for_body(self.source, body) {
329 self.emit_detector_annotation(detector)?;
330 }
331 }
332 Ok(())
333 }
334
335 fn emit_op(&mut self, op: &Op) -> Result<(), CircuitError> {
336 match op {
337 Op::Gate { gate, qubits } => self.emit_gate(*gate, qubits),
338 Op::Measure {
339 basis,
340 qubits,
341 measurements,
342 flip_probability,
343 } => {
344 let records = self.map_measurements(measurements)?;
345 self.emit_measure(*basis, qubits, &records, *flip_probability)
346 }
347 Op::Tick => {
348 self.note_instruction(false);
349 self.write_line("TICK");
350 Ok(())
351 }
352 Op::MPP {
353 products,
354 measurements,
355 } => {
356 let records = self.map_measurements(measurements)?;
357 self.emit_mpp(products, &records)
358 }
359 Op::Repeat { body, repetitions } => self.emit_repeat(*body, *repetitions),
360 Op::ConditionalPauli(corrections) => self.emit_conditional_paulis(corrections),
361 Op::Depolarize1 {
362 probability,
363 qubits,
364 } => self.emit_noise("DEPOLARIZE1", *probability, qubits),
365 Op::Depolarize2 {
366 probability,
367 qubits,
368 } => self.emit_noise("DEPOLARIZE2", *probability, qubits),
369 Op::PauliError {
370 probability,
371 pauli,
372 qubits,
373 } => self.emit_noise(pauli_error_gate_name(*pauli), *probability, qubits),
374 }
375 }
376
377 fn emit_conditional_paulis(
378 &mut self,
379 corrections: &[ConditionalCorrection],
380 ) -> Result<(), CircuitError> {
381 for correction in corrections {
382 let measurement = self.mapper.map_measurement(correction.control)?;
383 let lookback = self.ctx.frame.resolve_measurement(measurement)?;
384 let coord = correction.target + self.options.qubit_offset;
385 let Some(qubit) = self.ctx.layout.get(coord) else {
386 return Err(CircuitError::QubitNotFoundInLayout(coord));
387 };
388
389 self.write_instruction_prefix(conditional_pauli_gate_name(correction.pauli), None);
390 push_lookback(self.ctx.output, lookback);
391 self.ctx.output.push(' ');
392 self.ctx.output.push_str(self.ctx.layout.label(qubit));
393 self.ctx.output.push('\n');
394 }
395 Ok(())
396 }
397
398 fn map_measurements(&mut self, measurements: &[u32]) -> Result<Vec<u32>, CircuitError> {
400 measurements
401 .iter()
402 .map(|&measurement| self.mapper.map_measurement(measurement))
403 .collect()
404 }
405
406 fn emit_mpp(&mut self, products: &[PauliMap], records: &[u32]) -> Result<(), CircuitError> {
410 self.write_instruction_prefix("MPP", None);
411 for product in products {
412 let mut first = true;
413 for (coord, pauli) in product
414 .iter()
415 .filter(|&(_, &pauli)| pauli != Pauli::I)
416 .sorted_by_key(|(coord, _)| {
417 let coord = **coord + self.options.qubit_offset;
418 self.ctx.layout.get(coord).unwrap_or(0)
419 })
420 {
421 let coord = *coord + self.options.qubit_offset;
422 let Some(qubit) = self.ctx.layout.get(coord) else {
423 return Err(CircuitError::QubitNotFoundInLayout(coord));
424 };
425 self.ctx.output.push(if first { ' ' } else { '*' });
426 first = false;
427 self.ctx.output.push(match pauli {
428 Pauli::X => 'X',
429 Pauli::Y => 'Y',
430 Pauli::Z => 'Z',
431 Pauli::I => unreachable!("identity Paulis are filtered out above"),
432 });
433 self.ctx.output.push_str(self.ctx.layout.label(qubit));
434 }
435 }
436 self.ctx.output.push('\n');
437 for &record in records {
438 self.ctx.frame.record_emitted(record)?;
439 }
440 Ok(())
441 }
442
443 fn emit_repeat(&mut self, body: BodyId, repetitions: u32) -> Result<(), CircuitError> {
444 if repetitions == 0 {
445 return Ok(());
446 }
447
448 let side_table_detector_states = self.repeat_detector_states(body);
449 let detector_states = side_table_detector_states.as_slice();
450 let stack_depth = self.detector_state_stack.len();
451 let initial_states = self.resolve_initial_detector_states(detector_states)?;
452 self.detector_state_stack.push(initial_states);
453
454 let can_jump = can_jump_loop_detector_states(detector_states);
458 let mut stable_body = None;
459 let mut remaining = repetitions;
460 while remaining > 1 {
461 if can_jump
462 && let Some(rendered) = self.stable_repeat_body_render(body, detector_states)?
463 {
464 stable_body = Some(rendered);
465 break;
466 }
467 self.emit_repeat_iteration(body, detector_states)?;
468 remaining -= 1;
469 }
470
471 if remaining == 1 {
472 self.emit_repeat_iteration(body, detector_states)?;
473 self.finish_loop_detector_states(stack_depth, detector_states)?;
474 return Ok(());
475 }
476
477 let rendered_body =
480 stable_body.expect("remaining > 1 only survives the loop via the stable-body break");
481 self.note_instruction(false);
482 self.ctx.output.push_str(indent_prefix(self.ctx.indent));
483 self.ctx.output.push_str("REPEAT ");
484 push_int(self.ctx.output, remaining);
485 self.ctx.output.push_str(" {\n");
486 self.ctx.output.push_str(&rendered_body);
487 self.apply_rendered_repeat_body_effects(body, remaining - 1, detector_states)?;
488 self.write_line("}");
489 self.finish_loop_detector_states(stack_depth, detector_states)?;
490 Ok(())
491 }
492
493 fn stable_repeat_body_render(
494 &mut self,
495 body: BodyId,
496 detector_states: &[LoopCarriedDetectorState],
497 ) -> Result<Option<String>, CircuitError> {
498 let checkpoint = self.ctx.frame.checkpoint();
499 let mut state_stack = self.detector_state_stack.clone();
504 let mut frame = std::mem::take(self.ctx.frame);
505 let mut first = String::new();
506 let mut second = String::new();
507 let result = self
508 .render_repeat_iteration_for_stability(
509 body,
510 detector_states,
511 &mut frame,
512 &mut state_stack,
513 &mut first,
514 )
515 .and_then(|()| {
516 self.render_repeat_iteration_for_stability(
517 body,
518 detector_states,
519 &mut frame,
520 &mut state_stack,
521 &mut second,
522 )
523 })
524 .map(|()| (first == second).then_some(first));
525 frame.rollback_to(checkpoint);
526 *self.ctx.frame = frame;
527 result
528 }
529
530 fn apply_rendered_repeat_body_effects(
531 &mut self,
532 body: BodyId,
533 tail_repetitions: u32,
534 detector_states: &[LoopCarriedDetectorState],
535 ) -> Result<(), CircuitError> {
536 let (offsets, body_measurements) =
537 repeat_body_measurement_offsets(self.source, body, self.mapper)?;
538 self.record_repeat_body_measurements(body_measurements, 1, offsets.iter().copied())?;
539 self.update_loop_detector_states(detector_states)?;
540 self.record_repeat_body_measurements(body_measurements, tail_repetitions, offsets)?;
541 self.update_loop_detector_states(detector_states)
542 }
543
544 fn record_repeat_body_measurements(
545 &mut self,
546 body_measurements: u32,
547 repetitions: u32,
548 offsets: impl IntoIterator<Item = (u32, u32)>,
549 ) -> Result<(), CircuitError> {
550 if body_measurements == 0 || repetitions == 0 {
551 return Ok(());
552 }
553 self.ctx
554 .frame
555 .record_repeated_measurements(body_measurements, repetitions, offsets)
556 }
557
558 fn render_repeat_iteration_for_stability(
559 &mut self,
560 body: BodyId,
561 detector_states: &[LoopCarriedDetectorState],
562 frame: &mut MeasurementFrame,
563 detector_state_stack: &mut Vec<LoopStateFrame>,
564 output: &mut String,
565 ) -> Result<(), CircuitError> {
566 debug_assert!(!detector_state_stack.is_empty());
567 output.clear();
568 let mut ctx = StimEmitContext {
569 layout: self.ctx.layout,
570 frame,
571 resolved_loop_detector_states: self.ctx.resolved_loop_detector_states,
572 output,
573 indent: self.ctx.indent + 1,
574 };
575 let mut emitter = StimTextEmitter {
579 source: self.source,
580 annotations: self.annotations,
581 ctx: &mut ctx,
582 mapper: self.mapper,
583 options: self.options,
584 detector_state_stack: std::mem::take(detector_state_stack),
585 base_indent: self.base_indent,
586 needs_node_boundary_tick: false,
587 };
588 let result = emitter.emit_repeat_iteration(body, detector_states);
589 *detector_state_stack = std::mem::take(&mut emitter.detector_state_stack);
590 result
591 }
592
593 fn emit_repeat_iteration(
594 &mut self,
595 body: BodyId,
596 detector_states: &[LoopCarriedDetectorState],
597 ) -> Result<(), CircuitError> {
598 self.emit_body(body)?;
599 self.emit_side_table_annotations(body)?;
600 self.update_loop_detector_states(detector_states)
601 }
602
603 fn emit_detector_annotation(&mut self, detector: &StimDetector) -> Result<(), CircuitError> {
604 let lookbacks = self.resolve_detector_lookbacks(&detector.parity)?;
605 let targets: Vec<StimTarget> = lookbacks.into_iter().map(StimTarget::Rec).collect();
606 self.write_targets_line(
607 if detector.postselection && self.options.emit_postselection_tags {
608 "DETECTOR[POST-SELECTION]"
609 } else {
610 "DETECTOR"
611 },
612 detector.coords.as_deref(),
613 &targets,
614 );
615 Ok(())
616 }
617
618 fn repeat_detector_states(&self, body: BodyId) -> Vec<LoopCarriedDetectorState> {
619 let Some(annotations) = self.annotations else {
620 return Vec::new();
621 };
622 annotations
623 .repeat_states
624 .iter()
625 .filter(|state| state.body == body)
626 .map(|state| LoopCarriedDetectorState {
627 state: state.state,
628 initial: state.initial.clone(),
629 next: state.next.clone(),
630 })
631 .collect()
632 }
633
634 fn emit_gate(&mut self, gate: GateType, qubits: &[IVec2]) -> Result<(), CircuitError> {
635 let gate = if self.options.enable_clifford_proxy {
636 gate.clifford_proxy()
637 } else {
638 gate
639 };
640 debug_assert!(
641 self.options.allow_non_clifford || !gate.is_non_clifford(),
642 "unsupported Stim gate should be preflighted"
643 );
644 self.write_qubit_instruction_line(gate.into(), None, qubits)?;
645 Ok(())
646 }
647
648 fn emit_measure(
649 &mut self,
650 basis: PauliBasis,
651 qubits: &[IVec2],
652 records: &[u32],
653 flip_probability: f64,
654 ) -> Result<(), CircuitError> {
655 let args = (flip_probability != 0.0).then_some(std::slice::from_ref(&flip_probability));
656 self.write_qubit_instruction_line(measurement_gate_name(basis), args, qubits)?;
657 for &record in records {
658 self.ctx.frame.record_emitted(record)?;
659 }
660 Ok(())
661 }
662
663 fn emit_noise(
664 &mut self,
665 name: &str,
666 probability: f64,
667 qubits: &[IVec2],
668 ) -> Result<(), CircuitError> {
669 self.write_qubit_instruction_line(name, Some(std::slice::from_ref(&probability)), qubits)?;
670 Ok(())
671 }
672
673 fn resolve_initial_detector_states(
674 &mut self,
675 detector_states: &[LoopCarriedDetectorState],
676 ) -> Result<LoopStateFrame, CircuitError> {
677 let mut resolved = LoopStateFrame::default();
678 for state in detector_states {
679 let parity = self.resolve_parity_absolute(&state.initial)?;
680 resolved.insert(state.state, parity);
681 }
682 Ok(resolved)
683 }
684
685 fn update_loop_detector_states(
686 &mut self,
687 detector_states: &[LoopCarriedDetectorState],
688 ) -> Result<(), CircuitError> {
689 if detector_states.is_empty() {
690 return Ok(());
691 }
692 if detector_states.iter().all(is_identity_update) {
695 debug_assert!(detector_states.iter().all(|state| {
696 self.detector_state_stack
697 .last()
698 .is_some_and(|states| states.contains_key(&state.state))
699 }));
700 return Ok(());
701 }
702 let mut next = LoopStateFrame::new();
703 for state in detector_states {
704 let parity = if is_identity_update(state) {
705 let Some(current) = self
706 .detector_state_stack
707 .last()
708 .and_then(|states| states.get(&state.state))
709 else {
710 return Err(CircuitError::InvalidLoopDetectorState(state.state));
711 };
712 current.clone()
713 } else {
714 self.resolve_parity_absolute(&state.next)?
715 };
716 next.insert(state.state, parity);
717 }
718 let Some(current) = self.detector_state_stack.last_mut() else {
719 return Err(CircuitError::InvalidLoopDetectorState(
720 detector_states[0].state,
721 ));
722 };
723 *current = next;
724 Ok(())
725 }
726
727 fn finish_loop_detector_states(
728 &mut self,
729 stack_depth: usize,
730 detector_states: &[LoopCarriedDetectorState],
731 ) -> Result<(), CircuitError> {
732 if detector_states.is_empty() {
733 debug_assert_eq!(self.detector_state_stack.len(), stack_depth + 1);
734 self.detector_state_stack.pop();
735 return Ok(());
736 }
737 let Some(final_states) = self.detector_state_stack.pop() else {
738 return Err(CircuitError::InvalidLoopDetectorState(
739 detector_states[0].state,
740 ));
741 };
742 debug_assert_eq!(self.detector_state_stack.len(), stack_depth);
743 debug_assert!(
748 detector_states
749 .windows(2)
750 .all(|pair| pair[0].state < pair[1].state),
751 "loop detector states are allocated in ascending id order"
752 );
753 let mut listed = detector_states.iter().map(|state| state.state).peekable();
754 let mut keep = |state: LoopStateId| {
755 while listed.next_if(|&id| id < state).is_some() {}
756 listed.next_if_eq(&state).is_some()
757 };
758 if let Some(parent) = self.detector_state_stack.last_mut() {
759 for (state, value) in final_states {
760 if keep(state) {
761 parent.insert(state, value);
762 }
763 }
764 } else {
765 for (state, value) in final_states {
766 if keep(state) {
767 self.ctx.resolved_loop_detector_states.insert(state, value);
768 }
769 }
770 }
771 Ok(())
772 }
773
774 fn resolve_parity_absolute(
775 &mut self,
776 parity: &DetectorParity,
777 ) -> Result<Vec<u32>, CircuitError> {
778 let mut absolutes = Vec::with_capacity(parity.terms().len());
779 for &term in parity.terms() {
780 match term {
781 DetectorTerm::Measurement(measurement) => {
782 let measurement = self.mapper.map_annotation_measurement(measurement)?;
783 absolutes.push(self.ctx.frame.resolve_absolute_measurement(measurement)?);
784 }
785 DetectorTerm::LoopState(state) => {
786 let Some(resolved) = self
787 .detector_state_stack
788 .iter()
789 .rev()
790 .find_map(|states| states.get(&state))
791 .or_else(|| self.ctx.resolved_loop_detector_states.get(&state))
792 else {
793 return Err(CircuitError::InvalidLoopDetectorState(state));
794 };
795 absolutes.extend(resolved.iter().copied());
796 }
797 }
798 }
799 canonicalize_parity(&mut absolutes);
800 Ok(absolutes)
801 }
802
803 fn resolve_detector_lookbacks(
804 &mut self,
805 parity: &DetectorParity,
806 ) -> Result<Vec<i32>, CircuitError> {
807 if parity
815 .terms()
816 .iter()
817 .all(|term| matches!(term, DetectorTerm::Measurement(_)))
818 {
819 let mut lookbacks = Vec::with_capacity(parity.terms().len());
820 for &term in parity.terms() {
821 let DetectorTerm::Measurement(measurement) = term else {
822 unreachable!("the all-Measurement guard admits no LoopState terms")
823 };
824 let measurement = self.mapper.map_annotation_measurement(measurement)?;
825 lookbacks.push(self.ctx.frame.resolve_measurement(measurement)?);
826 }
827 lookbacks.sort_unstable();
828 canonicalize_parity(&mut lookbacks);
829 return Ok(lookbacks);
830 }
831
832 let absolutes = self.resolve_parity_absolute(parity)?;
835 let mut lookbacks = absolutes
836 .iter()
837 .map(|&abs| self.ctx.frame.lookback_to_absolute(abs))
838 .collect::<Result<Vec<_>, CircuitError>>()?;
839 lookbacks.sort_unstable();
840 Ok(lookbacks)
841 }
842
843 fn write_line(&mut self, line: &str) {
844 self.ctx.output.push_str(indent_prefix(self.ctx.indent));
845 self.ctx.output.push_str(line);
846 self.ctx.output.push('\n');
847 }
848
849 fn write_qubit_instruction_line(
850 &mut self,
851 gate: &str,
852 args: Option<&[f64]>,
853 qubits: &[IVec2],
854 ) -> Result<(), CircuitError> {
855 self.write_instruction_prefix(gate, args);
856 for &coord in qubits {
857 let coord = coord + self.options.qubit_offset;
858 let Some(qubit) = self.ctx.layout.get(coord) else {
859 return Err(CircuitError::QubitNotFoundInLayout(coord));
860 };
861 self.ctx.output.push(' ');
862 self.ctx.output.push_str(self.ctx.layout.label(qubit));
863 }
864 self.ctx.output.push('\n');
865 Ok(())
866 }
867
868 fn write_targets_line(&mut self, gate: &str, args: Option<&[f64]>, targets: &[StimTarget]) {
869 self.write_instruction_prefix(gate, args);
870 for target in targets {
871 target.write_to(self.ctx.output);
872 }
873 self.ctx.output.push('\n');
874 }
875
876 fn write_instruction_prefix(&mut self, gate: &str, args: Option<&[f64]>) {
877 self.note_instruction(true);
878 self.ctx.output.push_str(indent_prefix(self.ctx.indent));
879 self.ctx.output.push_str(gate);
880 if let Some(args) = args
881 && !args.is_empty()
882 {
883 self.ctx.output.push('(');
884 for (index, arg) in args.iter().enumerate() {
885 if index > 0 {
886 self.ctx.output.push_str(", ");
887 }
888 push_number_prefer_int(self.ctx.output, *arg);
889 }
890 self.ctx.output.push(')');
891 }
892 }
893}
894
895impl StimTarget {
896 fn write_to(self, output: &mut String) {
898 match self {
899 StimTarget::Rec(lookback) => push_lookback(output, lookback),
900 StimTarget::Pauli { pauli, qubit } => {
901 output.push(' ');
902 output.push(match pauli {
903 Pauli::X => 'X',
904 Pauli::Y => 'Y',
905 Pauli::Z => 'Z',
906 Pauli::I => {
909 unreachable!("identity Paulis are filtered before StimTarget::Pauli")
910 }
911 });
912 push_int(output, qubit);
913 }
914 }
915 }
916}
917
918pub(crate) fn conditional_pauli_gate_name(pauli: PauliBasis) -> &'static str {
919 match pauli {
920 PauliBasis::X => "CX",
921 PauliBasis::Y => "CY",
922 PauliBasis::Z => "CZ",
923 }
924}
925
926pub(crate) fn pauli_error_gate_name(pauli: PauliBasis) -> &'static str {
927 match pauli {
928 PauliBasis::X => "X_ERROR",
929 PauliBasis::Y => "Y_ERROR",
930 PauliBasis::Z => "Z_ERROR",
931 }
932}
933
934#[cfg(feature = "verify")]
939pub(crate) fn emit_detector_records(output: &mut String, lookbacks: &[i32]) {
940 output.push_str("DETECTOR");
941 for &lookback in lookbacks {
942 push_lookback(output, lookback);
943 }
944 output.push('\n');
945}
946
947pub(crate) fn emit_qubit_coords_lines(
954 output: &mut String,
955 coords: impl IntoIterator<Item = (IVec2, u32)>,
956) {
957 for (coord, qubit_index) in coords {
958 output.push_str("QUBIT_COORDS(");
959 push_int(output, coord.x);
960 output.push_str(", ");
961 push_int(output, coord.y);
962 output.push_str(") ");
963 push_int(output, qubit_index);
964 output.push('\n');
965 }
966}
967
968pub(crate) fn emit_observable_include_records(output: &mut String, index: u32, lookbacks: &[i32]) {
975 if lookbacks.is_empty() {
976 return;
977 }
978 output.push_str("OBSERVABLE_INCLUDE(");
979 push_int(output, index);
980 output.push(')');
981 for &lookback in lookbacks {
982 StimTarget::Rec(lookback).write_to(output);
983 }
984 output.push('\n');
985}
986
987pub(crate) fn emit_observable_include_pauli_targets(
996 output: &mut String,
997 index: u32,
998 operator: &PauliMap,
999 layout: &QubitLayout,
1000) -> Result<(), CircuitError> {
1001 let mut targets = Vec::new();
1002 append_pauli_map_stim_targets(operator, layout, &mut targets)?;
1003 if targets.is_empty() {
1004 return Ok(());
1005 }
1006 output.push_str("OBSERVABLE_INCLUDE(");
1007 push_int(output, index);
1008 output.push(')');
1009 for target in &targets {
1010 target.write_to(output);
1011 }
1012 output.push('\n');
1013 Ok(())
1014}
1015
1016fn append_pauli_map_stim_targets(
1017 pauli_map: &PauliMap,
1018 layout: &QubitLayout,
1019 targets: &mut Vec<StimTarget>,
1020) -> Result<(), CircuitError> {
1021 for (coord, pauli) in pauli_map
1022 .iter()
1023 .filter(|&(_, &pauli)| pauli != Pauli::I)
1024 .sorted_by_key(|(coord, _)| layout.get(**coord).unwrap_or(0))
1025 {
1026 let Some(qubit) = layout.get(*coord) else {
1027 return Err(CircuitError::QubitNotFoundInLayout(*coord));
1028 };
1029 targets.push(StimTarget::Pauli {
1030 pauli: *pauli,
1031 qubit,
1032 });
1033 }
1034 Ok(())
1035}
1036
1037fn can_jump_loop_detector_states(detector_states: &[LoopCarriedDetectorState]) -> bool {
1051 detector_states
1052 .iter()
1053 .all(|state| loop_state_update_kind(state).is_some())
1054}
1055
1056fn canonicalize_parity<T: Ord + Copy>(values: &mut Vec<T>) {
1061 if values.len() < 2 {
1062 return;
1063 }
1064 if values.windows(2).all(|pair| pair[0] < pair[1]) {
1065 return;
1066 }
1067
1068 values.sort_unstable();
1069 let mut write = 0;
1070 let mut read = 0;
1071 while read < values.len() {
1072 let value = values[read];
1073 let mut count = 1;
1074 read += 1;
1075 while read < values.len() && values[read] == value {
1076 count += 1;
1077 read += 1;
1078 }
1079 if count % 2 == 1 {
1080 values[write] = value;
1081 write += 1;
1082 }
1083 }
1084 values.truncate(write);
1085}
1086
1087fn is_identity_update(state: &LoopCarriedDetectorState) -> bool {
1088 matches!(
1089 loop_state_update_kind(state),
1090 Some(LoopStateUpdate::Identity)
1091 )
1092}
1093
1094fn loop_state_update_kind(state: &LoopCarriedDetectorState) -> Option<LoopStateUpdate> {
1095 match state.next.terms() {
1096 [] => Some(LoopStateUpdate::Clear),
1097 [DetectorTerm::LoopState(loop_state)] if *loop_state == state.state => {
1098 Some(LoopStateUpdate::Identity)
1099 }
1100 terms
1101 if terms
1102 .iter()
1103 .all(|term| !matches!(term, DetectorTerm::LoopState(_))) =>
1104 {
1105 Some(LoopStateUpdate::Measurements)
1106 }
1107 _ => None,
1108 }
1109}
1110
1111fn scope_for_body(circuit: &CoordCircuit, body: BodyId) -> StimAnnotationScope {
1112 if body == circuit.entry_body() {
1113 StimAnnotationScope::TopLevel
1114 } else {
1115 StimAnnotationScope::RepeatBody { body }
1116 }
1117}
1118
1119pub(crate) fn first_unsupported_gate(circuit: &CoordCircuit) -> Option<GateType> {
1120 let mut pending = vec![circuit.entry_body()];
1121 let mut seen = rustc_hash::FxHashSet::default();
1122 while let Some(body) = pending.pop() {
1123 if !seen.insert(body) {
1124 continue;
1125 }
1126 let Some(body) = circuit.body(body) else {
1127 continue;
1128 };
1129 for op in body.ops() {
1130 match op {
1131 Op::Gate { gate, .. } if gate.is_non_clifford() => return Some(*gate),
1132 Op::Repeat { body, repetitions } if *repetitions != 0 => pending.push(*body),
1133 _ => {}
1134 }
1135 }
1136 }
1137 None
1138}
1139
1140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1141enum LoopStateUpdate {
1142 Clear,
1143 Identity,
1144 Measurements,
1145}
1146
1147fn repeat_body_measurement_offsets(
1158 circuit: &CoordCircuit,
1159 body: BodyId,
1160 mapper: &mut impl StimMeasurementMapper,
1161) -> Result<(Vec<(u32, u32)>, u32), CircuitError> {
1162 let mut offsets = Vec::new();
1163 let mut body_measurements = 0;
1164 collect_repeat_body_measurement_offsets(
1165 circuit,
1166 body,
1167 mapper,
1168 &mut offsets,
1169 &mut body_measurements,
1170 )?;
1171 Ok((offsets, body_measurements))
1172}
1173
1174fn collect_repeat_body_measurement_offsets(
1175 circuit: &CoordCircuit,
1176 body: BodyId,
1177 mapper: &mut impl StimMeasurementMapper,
1178 offsets: &mut Vec<(u32, u32)>,
1179 next_offset: &mut u32,
1180) -> Result<(), CircuitError> {
1181 let Some(body) = circuit.body(body) else {
1182 return Err(CircuitError::InvalidCircuitBody(body));
1183 };
1184 for op in body.ops() {
1185 match op {
1186 Op::Measure { measurements, .. } | Op::MPP { measurements, .. } => {
1187 for &measurement in measurements {
1188 offsets.push((mapper.map_measurement(measurement)?, *next_offset));
1189 let Some(next) = next_offset.checked_add(1) else {
1190 return Err(MeasurementFrameError::RepeatMeasurementOffsetOverflow.into());
1191 };
1192 *next_offset = next;
1193 }
1194 }
1195 Op::Repeat {
1196 body, repetitions, ..
1197 } => {
1198 if *repetitions == 0 {
1199 continue;
1200 }
1201 let mut nested_offsets = Vec::new();
1202 let mut nested_body_measurements = 0;
1203 collect_repeat_body_measurement_offsets(
1204 circuit,
1205 *body,
1206 mapper,
1207 &mut nested_offsets,
1208 &mut nested_body_measurements,
1209 )?;
1210 if nested_body_measurements == 0 {
1211 continue;
1212 }
1213 let Some(last_iteration_offset) =
1218 nested_body_measurements.checked_mul(*repetitions - 1)
1219 else {
1220 return Err(MeasurementFrameError::RepeatedMeasurementCountOverflow {
1221 measurements_per_iteration: nested_body_measurements,
1222 repetitions: *repetitions,
1223 }
1224 .into());
1225 };
1226 for &(measurement, nested_offset) in &nested_offsets {
1227 let Some(offset) = next_offset
1228 .checked_add(last_iteration_offset)
1229 .and_then(|offset| offset.checked_add(nested_offset))
1230 else {
1231 return Err(MeasurementFrameError::RepeatMeasurementOffsetOverflow.into());
1232 };
1233 offsets.push((measurement, offset));
1234 }
1235 let Some(additional) = nested_body_measurements.checked_mul(*repetitions) else {
1236 return Err(MeasurementFrameError::RepeatedMeasurementCountOverflow {
1237 measurements_per_iteration: nested_body_measurements,
1238 repetitions: *repetitions,
1239 }
1240 .into());
1241 };
1242 let Some(next) = next_offset.checked_add(additional) else {
1243 return Err(MeasurementFrameError::RepeatMeasurementOffsetOverflow.into());
1244 };
1245 *next_offset = next;
1246 }
1247 Op::Gate { .. }
1248 | Op::Tick
1249 | Op::ConditionalPauli(_)
1250 | Op::Depolarize1 { .. }
1251 | Op::Depolarize2 { .. }
1252 | Op::PauliError { .. } => {}
1253 }
1254 }
1255 Ok(())
1256}
1257
1258#[inline]
1259fn indent_prefix(level: usize) -> &'static str {
1260 const INDENT_TABLE: [&str; 9] = [
1261 "",
1262 " ",
1263 " ",
1264 " ",
1265 " ",
1266 " ",
1267 " ",
1268 " ",
1269 " ",
1270 ];
1271
1272 INDENT_TABLE[level.min(INDENT_TABLE.len() - 1)]
1273}
1274
1275#[cfg(test)]
1276mod tests {
1277 use glam::ivec2;
1278
1279 use super::*;
1280 use bloq_circuit::CircuitBody;
1281
1282 fn emit_stim_text(
1286 circuit: &CoordCircuit,
1287 annotations: Option<&StimCircuitAnnotations>,
1288 layout: FxHashMap<IVec2, u32>,
1289 enable_clifford_proxy: bool,
1290 ) -> Result<String, StimEmissionError> {
1291 if !enable_clifford_proxy && let Some(gate) = first_unsupported_gate(circuit) {
1292 return Err(StimEmissionError::UnsupportedGate(gate));
1293 }
1294 let layout = QubitLayout::new(layout)?;
1295 let (output, _frame) =
1296 emit_standalone_stim(circuit, annotations, &layout, enable_clifford_proxy)?;
1297 Ok(output)
1298 }
1299
1300 #[test]
1301 fn to_stim_text_preserves_repeat_blocks() {
1302 let qubit = ivec2(0, 0);
1303 let mut circuit = CoordCircuit::new();
1304 let measurement = circuit.reserve_measurement_id(qubit);
1305 let body = circuit.add_body(CircuitBody::from_ops(vec![Op::Measure {
1306 basis: PauliBasis::Z,
1307 qubits: vec![qubit],
1308 measurements: vec![measurement],
1309 flip_probability: 0.0,
1310 }]));
1311 circuit.push_repeat(body, 3);
1312
1313 let text = emit_stim_text(&circuit, None, circuit.build_coord_to_index(), true).unwrap();
1314
1315 insta::assert_snapshot!(text, @"
1318 QUBIT_COORDS(0, 0) 0
1319 REPEAT 3 {
1320 M 0
1321 }
1322 ");
1323 }
1324
1325 #[test]
1326 fn emits_first_class_noise_instructions_and_measurement_flip() {
1327 let q0 = ivec2(0, 0);
1328 let q1 = ivec2(1, 0);
1329 let mut circuit = CoordCircuit::new();
1330 circuit
1331 .body_mut(circuit.entry_body())
1332 .unwrap()
1333 .ops_mut()
1334 .extend([
1335 Op::Depolarize1 {
1336 probability: 0.1,
1337 qubits: vec![q0],
1338 },
1339 Op::Depolarize2 {
1340 probability: 0.2,
1341 qubits: vec![q0, q1],
1342 },
1343 Op::PauliError {
1344 probability: 0.3,
1345 pauli: PauliBasis::Z,
1346 qubits: vec![q1],
1347 },
1348 Op::Measure {
1349 basis: PauliBasis::X,
1350 qubits: vec![q0],
1351 measurements: vec![0],
1352 flip_probability: 0.4,
1353 },
1354 ]);
1355 circuit.register_measurement_id(0, q0);
1356
1357 let text = emit_stim_text(&circuit, None, circuit.build_coord_to_index(), true).unwrap();
1358
1359 assert!(text.contains("DEPOLARIZE1(0.1) 0"));
1360 assert!(text.contains("DEPOLARIZE2(0.2) 0 1"));
1361 assert!(text.contains("Z_ERROR(0.3) 1"));
1362 assert!(text.contains("MX(0.4) 0"));
1363 }
1364
1365 #[test]
1366 fn mpp_emits_pauli_product_targets_with_combiners() {
1367 use bloq_circuit::{Pauli, PauliMap};
1368 let x_product =
1370 PauliMap::from_unique_entries([(ivec2(0, 0), Pauli::X), (ivec2(2, 0), Pauli::X)]);
1371 let z_product =
1372 PauliMap::from_unique_entries([(ivec2(0, 0), Pauli::Z), (ivec2(0, 2), Pauli::Z)]);
1373 let mut circuit = CoordCircuit::new();
1374 let ids = circuit
1375 .measure_pauli_products([x_product, z_product])
1376 .unwrap();
1377 assert_eq!(ids.len(), 2, "one record per product");
1378 assert_eq!(circuit.num_measurements(), 2);
1379
1380 let text = emit_stim_text(&circuit, None, circuit.build_coord_to_index(), true).unwrap();
1381 insta::assert_snapshot!(text, @"
1384 QUBIT_COORDS(0, 0) 0
1385 QUBIT_COORDS(0, 2) 1
1386 QUBIT_COORDS(2, 0) 2
1387 MPP X0*X2 Z0*Z1
1388 ");
1389 }
1390
1391 #[test]
1392 fn conditional_pauli_emits_record_controlled_xyz() {
1393 use bloq_circuit::{ConditionalCorrection, PauliBasis};
1394 let control_qubit = ivec2(0, 0);
1395 let mut circuit = CoordCircuit::new();
1396 let control = circuit.measure(PauliBasis::Z, [control_qubit])[0];
1397 circuit
1398 .body_mut(circuit.entry_body())
1399 .unwrap()
1400 .ops_mut()
1401 .push(Op::ConditionalPauli(
1402 [PauliBasis::X, PauliBasis::Y, PauliBasis::Z]
1403 .into_iter()
1404 .enumerate()
1405 .map(|(index, pauli)| ConditionalCorrection {
1406 pauli,
1407 control,
1408 target: ivec2(index as i32 + 1, 0),
1409 })
1410 .collect(),
1411 ));
1412
1413 let text = emit_stim_text(&circuit, None, circuit.build_coord_to_index(), true).unwrap();
1414
1415 insta::assert_snapshot!(text, @"
1416 QUBIT_COORDS(0, 0) 0
1417 QUBIT_COORDS(1, 0) 1
1418 QUBIT_COORDS(2, 0) 2
1419 QUBIT_COORDS(3, 0) 3
1420 M 0
1421 CX rec[-1] 1
1422 CY rec[-1] 2
1423 CZ rec[-1] 3
1424 ");
1425 #[cfg(feature = "verify")]
1426 text.parse::<stim::Circuit>()
1427 .expect("record-controlled Pauli gates should parse as Stim");
1428 }
1429
1430 #[test]
1431 fn conditional_pauli_keeps_repeat_body_stable() {
1432 use bloq_circuit::ConditionalCorrection;
1433 let qubit = ivec2(0, 0);
1434 let mut circuit = CoordCircuit::new();
1435 let measurement = circuit.reserve_measurement_id(qubit);
1436 let body = circuit.add_body(CircuitBody::from_ops(vec![
1437 Op::Measure {
1438 basis: PauliBasis::Z,
1439 qubits: vec![qubit],
1440 measurements: vec![measurement],
1441 flip_probability: 0.0,
1442 },
1443 Op::ConditionalPauli(vec![ConditionalCorrection {
1444 pauli: PauliBasis::X,
1445 control: measurement,
1446 target: qubit,
1447 }]),
1448 ]));
1449 circuit.push_repeat(body, 3);
1450
1451 let text = emit_stim_text(&circuit, None, circuit.build_coord_to_index(), true).unwrap();
1452
1453 insta::assert_snapshot!(text, @"
1454 QUBIT_COORDS(0, 0) 0
1455 REPEAT 3 {
1456 M 0
1457 CX rec[-1] 0
1458 }
1459 ");
1460 #[cfg(feature = "verify")]
1461 text.parse::<stim::Circuit>()
1462 .expect("feed-forward inside REPEAT should parse as Stim");
1463 }
1464
1465 #[test]
1466 fn side_table_repeat_keeps_following_lookbacks_relative_to_final_iteration() {
1467 let qubit = ivec2(0, 0);
1468 let mut circuit = CoordCircuit::new();
1469 let seed = circuit.measure(PauliBasis::Z, [qubit])[0];
1470 let repeated = circuit.reserve_measurement_id(qubit);
1471 let state = LoopStateId(0);
1472 let body = circuit.add_body(CircuitBody::from_ops(vec![Op::Measure {
1473 basis: PauliBasis::Z,
1474 qubits: vec![qubit],
1475 measurements: vec![repeated],
1476 flip_probability: 0.0,
1477 }]));
1478 circuit.push_repeat(body, 3);
1479 let annotations = StimCircuitAnnotations {
1480 detectors: vec![
1481 StimDetector {
1482 scope: StimAnnotationScope::RepeatBody { body },
1483 parity: DetectorParity::from_terms([
1484 DetectorTerm::Measurement(repeated),
1485 DetectorTerm::LoopState(state),
1486 ]),
1487 coords: None,
1488 postselection: false,
1489 },
1490 StimDetector {
1491 scope: StimAnnotationScope::TopLevel,
1492 parity: DetectorParity::from_measurements([repeated]),
1493 coords: None,
1494 postselection: false,
1495 },
1496 ],
1497 repeat_states: vec![StimRepeatState {
1498 body,
1499 state,
1500 initial: DetectorParity::from_measurements([seed]),
1501 next: DetectorParity::from_measurements([repeated]),
1502 }],
1503 };
1504
1505 let text = emit_stim_text(
1506 &circuit,
1507 Some(&annotations),
1508 circuit.build_coord_to_index(),
1509 true,
1510 )
1511 .unwrap();
1512
1513 assert!(text.contains("REPEAT 3 {"), "{text}");
1514 assert!(text.ends_with("}\nDETECTOR rec[-1]\n"), "{text}");
1515 }
1516
1517 #[test]
1518 fn side_table_repeat_partially_unrolls_until_loop_state_lookbacks_are_stable() {
1519 let seed_qubit = ivec2(0, 0);
1520 let repeated_qubit = ivec2(1, 0);
1521 let mut circuit = CoordCircuit::new();
1522 let seed = circuit.measure(PauliBasis::Z, [seed_qubit])[0];
1523 let repeated = circuit.reserve_measurement_id(repeated_qubit);
1524 let state = LoopStateId(0);
1525 let body = circuit.add_body(CircuitBody::from_ops(vec![Op::Measure {
1526 basis: PauliBasis::Z,
1527 qubits: vec![repeated_qubit],
1528 measurements: vec![repeated],
1529 flip_probability: 0.0,
1530 }]));
1531 circuit.measure(PauliBasis::Z, [seed_qubit]);
1532 circuit.push_repeat(body, 4);
1533 let annotations = StimCircuitAnnotations {
1534 detectors: vec![StimDetector {
1535 scope: StimAnnotationScope::RepeatBody { body },
1536 parity: DetectorParity::from_terms([
1537 DetectorTerm::Measurement(repeated),
1538 DetectorTerm::LoopState(state),
1539 ]),
1540 coords: Some(bloq_circuit::DetectorCoords::from_slice(&[1.0, 0.0, 1.0])),
1541 postselection: false,
1542 }],
1543 repeat_states: vec![StimRepeatState {
1544 body,
1545 state,
1546 initial: DetectorParity::from_measurements([seed]),
1547 next: DetectorParity::from_measurements([repeated]),
1548 }],
1549 };
1550
1551 let text = emit_stim_text(
1552 &circuit,
1553 Some(&annotations),
1554 circuit.build_coord_to_index(),
1555 true,
1556 )
1557 .unwrap();
1558
1559 assert!(
1560 text.contains("M 0\nM 0\nM 1\nDETECTOR(1, 0, 1) rec[-3] rec[-1]\nREPEAT 3 {"),
1561 "{text}"
1562 );
1563 assert!(
1564 text.contains(" DETECTOR(1, 0, 1) rec[-2] rec[-1]"),
1565 "{text}"
1566 );
1567 }
1568
1569 #[test]
1570 fn side_table_detector_cancels_loop_state_against_measurement() {
1571 let qubit = ivec2(0, 0);
1572 let mut circuit = CoordCircuit::new();
1573 let seed = circuit.measure(PauliBasis::Z, [qubit])[0];
1574 let repeated = circuit.reserve_measurement_id(qubit);
1575 let state = LoopStateId(0);
1576 let body = circuit.add_body(CircuitBody::from_ops(vec![Op::Measure {
1577 basis: PauliBasis::Z,
1578 qubits: vec![qubit],
1579 measurements: vec![repeated],
1580 flip_probability: 0.0,
1581 }]));
1582 circuit.push_repeat(body, 1);
1583 let annotations = StimCircuitAnnotations {
1584 detectors: vec![StimDetector {
1585 scope: StimAnnotationScope::RepeatBody { body },
1586 parity: DetectorParity::from_terms([
1587 DetectorTerm::Measurement(seed),
1588 DetectorTerm::LoopState(state),
1589 ]),
1590 coords: None,
1591 postselection: false,
1592 }],
1593 repeat_states: vec![StimRepeatState {
1594 body,
1595 state,
1596 initial: DetectorParity::from_measurements([seed]),
1597 next: DetectorParity::from_measurements([repeated]),
1598 }],
1599 };
1600
1601 let text = emit_stim_text(
1602 &circuit,
1603 Some(&annotations),
1604 circuit.build_coord_to_index(),
1605 true,
1606 )
1607 .unwrap();
1608
1609 assert!(text.contains("M 0\nM 0\nDETECTOR\n"), "{text}");
1610 }
1611}