1use std::sync::Arc;
13
14use bloq_circuit::{
15 DetectorCoords, DetectorParity, DetsliceError, Op, Pauli, PauliBasis, PauliMap,
16 RegionBreakKind, RegionTerm, SliceDetector, SliceRegionSeed, detector_slices_with_seeds,
17};
18use glam::IVec2;
19use rustc_hash::{FxHashMap, FxHashSet};
20use thiserror::Error;
21
22use crate::instantiation::InstantiationOptions;
23use crate::{
24 Bloq, BloqNodeId, BloqNodeKind, BloqValidationError, BodySelector, BoundaryFace, ClassicalExpr,
25 ClassicalNode, CycleDetected, FlattenError, InstanceMeasurement, LevelPath, NodeDetectorParity,
26 SubGraph, TemplateInstanceId, ValidatedPlans,
27};
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub enum MomentKind {
36 Reset,
38 Rotation,
40 Interaction,
42 Measurement,
44}
45
46fn op_moment_kind(op: &Op) -> Option<MomentKind> {
59 match op {
60 Op::Gate { gate, .. } if gate.is_reset() => Some(MomentKind::Reset),
61 Op::Gate { gate, .. } if gate.is_two_qubit_gate() => Some(MomentKind::Interaction),
62 Op::Gate { .. } => Some(MomentKind::Rotation),
63 Op::Measure { .. } | Op::MPP { .. } => Some(MomentKind::Measurement),
64 Op::Tick
68 | Op::ConditionalPauli(_)
69 | Op::Repeat { .. }
70 | Op::Depolarize1 { .. }
71 | Op::Depolarize2 { .. }
72 | Op::PauliError { .. } => None,
73 }
74}
75
76#[derive(Debug, Clone, PartialEq)]
79pub struct MomentSegment {
80 pub kind: MomentKind,
82 pub ops: Vec<Op>,
84}
85
86#[derive(Debug, Default)]
93pub struct MomentSegmenter {
94 carried_corrections: Vec<Op>,
95}
96
97impl MomentSegmenter {
98 pub fn extend(&mut self, ops: &[Op]) -> Vec<MomentSegment> {
101 self.extend_owned(ops.to_vec())
102 }
103
104 fn extend_owned(&mut self, ops: Vec<Op>) -> Vec<MomentSegment> {
107 let mut out = Vec::new();
108 let mut segment = Vec::new();
109 for op in ops {
110 if matches!(op, Op::Tick) {
111 bucket_tick_segment(
112 std::mem::take(&mut segment),
113 &mut self.carried_corrections,
114 &mut out,
115 );
116 } else {
117 segment.push(op);
118 }
119 }
120 bucket_tick_segment(segment, &mut self.carried_corrections, &mut out);
121 out
122 }
123
124 pub fn finish(mut self, out: &mut [MomentSegment]) {
127 if let Some(last) = out.last_mut() {
128 last.ops.append(&mut self.carried_corrections);
129 }
130 }
131}
132
133pub fn moment_segments(ops: &[Op]) -> Vec<MomentSegment> {
144 moment_segments_owned(ops.to_vec())
145}
146
147fn moment_segments_owned(ops: Vec<Op>) -> Vec<MomentSegment> {
148 let mut segmenter = MomentSegmenter::default();
149 let mut out = segmenter.extend_owned(ops);
150 segmenter.finish(&mut out);
151 out
152}
153
154fn bucket_tick_segment(
159 segment: Vec<Op>,
160 carried_corrections: &mut Vec<Op>,
161 out: &mut Vec<MomentSegment>,
162) {
163 let mut runs = Vec::<MomentSegment>::new();
164 let mut pending = std::mem::take(carried_corrections);
165
166 for op in segment {
167 let op = match op {
168 Op::ConditionalPauli(corrections) => {
169 if !corrections.is_empty() {
170 let correction = Op::ConditionalPauli(corrections);
171 if let Some(run) = runs.last_mut() {
172 run.ops.push(correction);
173 } else {
174 pending.push(correction);
175 }
176 }
177 continue;
178 }
179 op => op,
180 };
181
182 let Some(kind) = op_moment_kind(&op) else {
183 continue;
184 };
185 if runs.last().is_none_or(|run| run.kind != kind) {
186 runs.push(MomentSegment {
187 kind,
188 ops: std::mem::take(&mut pending),
189 });
190 }
191 runs.last_mut()
192 .expect("a visible op creates a source-order run")
193 .ops
194 .push(op);
195 }
196
197 if runs.is_empty() {
198 carried_corrections.append(&mut pending);
199 } else {
200 debug_assert!(
201 pending.is_empty(),
202 "the final visible run must absorb pending corrections"
203 );
204 out.extend(runs);
205 }
206}
207
208#[derive(Debug, Clone, PartialEq, Eq, Hash)]
219pub struct NodeRef {
220 pub path: LevelPath,
223 pub node: BloqNodeId,
225}
226
227impl NodeRef {
228 pub fn top_level(node: BloqNodeId) -> Self {
230 Self {
231 path: LevelPath::default(),
232 node,
233 }
234 }
235
236 pub fn is_top_level(&self) -> bool {
238 self.path.is_top_level()
239 }
240}
241
242#[derive(Debug, Clone, Default, PartialEq, Eq)]
244pub struct ProgramSliceOptions {
245 detectors_only: bool,
246 physical_observables: bool,
247}
248
249impl ProgramSliceOptions {
250 pub fn set_physical_observables(&mut self, enabled: bool) {
254 self.physical_observables = enabled;
255 }
256
257 pub fn set_detectors_only(&mut self, enabled: bool) {
259 self.detectors_only = enabled;
260 }
261}
262
263#[derive(Debug, Clone, PartialEq, Eq, Hash)]
265pub enum ProgramRegionId {
266 Detector {
268 owner: NodeRef,
270 detector: u32,
272 },
273 Observable {
275 index: u32,
277 },
278}
279
280#[derive(Debug, Clone, PartialEq)]
282pub struct ProgramRegion {
283 pub id: ProgramRegionId,
285 pub coords: Option<Arc<DetectorCoords>>,
287}
288
289#[derive(Debug, Clone, PartialEq)]
291pub struct RegionView {
292 pub region: u32,
294 pub terms: Vec<RegionTerm>,
296}
297
298#[derive(Debug, Clone, Default, PartialEq)]
304pub struct NodeSlices(pub Vec<Vec<RegionView>>);
305
306#[derive(Debug, Clone, PartialEq)]
312pub struct ProgramRegionBreak {
313 pub node: NodeRef,
315 pub moment: usize,
317 pub region: ProgramRegionId,
319 pub qubit: IVec2,
321 pub kind: RegionBreakKind,
323}
324
325#[derive(Debug, Clone, Default)]
327pub struct ProgramSlices {
328 pub regions: Vec<ProgramRegion>,
331 pub per_node: FxHashMap<NodeRef, NodeSlices>,
334 pub breaks: Vec<ProgramRegionBreak>,
336 pub skipped_cross_tape: usize,
339}
340
341#[derive(Debug, Error)]
343#[non_exhaustive]
344pub enum ProgramSliceError {
345 #[error("detector-slice region id space overflowed")]
347 RegionIdOverflow,
348 #[error("{0}")]
350 DetectorBundle(#[from] crate::DetectorBundleError),
351 #[error("detector slices require pinned component membership")]
353 MembershipSelectionRequired,
354 #[error("quantum plan materialization failed: {0}")]
356 Materialization(#[from] BloqValidationError),
357 #[error("region {region:?} references unavailable measurement {measurement:?}")]
359 UnknownMeasurement {
360 region: ProgramRegionId,
362 measurement: InstanceMeasurement,
364 },
365 #[error("region {region:?} references unavailable instance {instance:?}")]
367 UnknownInstance {
368 region: ProgramRegionId,
370 instance: TemplateInstanceId,
372 },
373 #[error("flattening the program failed: {0}")]
375 Flatten(#[from] FlattenError),
376 #[error("a graph level has no topological emit order (cycle)")]
378 Cycle(#[from] CycleDetected),
379 #[error("node {node:?} folds a nonlinear observable expression (SEM-FOLD)")]
381 NonLinearExpr {
382 node: NodeRef,
384 },
385 #[error("detector-slice tracking failed: {0}")]
387 Detslice(#[from] DetsliceError),
388 #[error("region {region:?} produced a gauge break outside every circuit moment")]
390 BreakOutsideTimeline {
391 region: ProgramRegionId,
393 },
394}
395
396fn checked_region_base(base: usize, additional: usize) -> Result<u32, ProgramSliceError> {
397 let count = base
398 .checked_add(additional)
399 .ok_or(ProgramSliceError::RegionIdOverflow)?;
400 u32::try_from(count).map_err(|_| ProgramSliceError::RegionIdOverflow)?;
401 u32::try_from(base).map_err(|_| ProgramSliceError::RegionIdOverflow)
402}
403
404pub fn program_detector_slices(program: &Bloq) -> Result<ProgramSlices, ProgramSliceError> {
422 program_detector_slices_with_options(program, &ProgramSliceOptions::default())
423}
424
425pub fn program_detector_slices_with_options(
437 program: &Bloq,
438 options: &ProgramSliceOptions,
439) -> Result<ProgramSlices, ProgramSliceError> {
440 if program.has_conditional_membership() {
441 return Err(ProgramSliceError::MembershipSelectionRequired);
442 }
443 let mut program = program.clone();
444 program.flatten()?;
445 let plans = program.emission_plans(&InstantiationOptions::default())?;
448 let known_measurements: FxHashSet<_> = plans
449 .iter()
450 .flat_map(|(_, plan)| plan.measurements.keys().copied())
451 .collect();
452 let known_instances: FxHashMap<_, _> = program
453 .levels()
454 .flat_map(|(_, level)| {
455 level.quantum_nodes().flat_map(|(_, quantum)| {
456 quantum
457 .instances
458 .iter()
459 .map(|instance| (instance.id, instance.template_id))
460 })
461 })
462 .collect();
463 for (_, level) in program.levels() {
464 for (_, quantum) in level.quantum_nodes() {
465 program.check_detector_bundle_bindings(quantum, |instance| {
466 known_instances.get(&instance).copied()
467 })?;
468 }
469 }
470
471 let mut out = ProgramSlices::default();
472 let mut tape = TapeBuilder::default();
473 add_level_to_tape(
474 &program,
475 program.top(),
476 &LevelPath::default(),
477 &plans,
478 options,
479 &mut tape,
480 )?;
481 tape.finish(&known_measurements, &known_instances, &mut out)?;
482 Ok(out)
483}
484
485fn add_level_to_tape(
486 program: &Bloq,
487 level: &SubGraph,
488 path: &LevelPath,
489 plans: &ValidatedPlans,
490 options: &ProgramSliceOptions,
491 tape: &mut TapeBuilder,
492) -> Result<(), ProgramSliceError> {
493 for id in level.deterministic_emit_order()? {
494 match &level[id].kind {
495 BloqNodeKind::Quantum(_) => tape.add_node(program, path, id, &level[id], plans)?,
496 BloqNodeKind::Region(region) => {
497 for (selector, subgraph) in region.bodies() {
498 add_level_to_tape(
499 program,
500 subgraph,
501 &path.child(id, selector),
502 plans,
503 options,
504 tape,
505 )?;
506 }
507 }
508 BloqNodeKind::Classical(classical) => {
509 if !options.detectors_only
510 && let ClassicalNode::Observable {
511 index: Some(index), ..
512 } = classical.as_ref()
513 {
514 tape.stage_observable(level, path, id, *index, options)?;
515 }
516 }
517 }
518 }
519 Ok(())
520}
521
522fn evaluate_expression(
523 level: &SubGraph,
524 path: &LevelPath,
525 node: BloqNodeId,
526 expr: &ClassicalExpr,
527) -> Option<bool> {
528 evaluate_expression_cached(level, path, node, expr, &mut FxHashMap::default())
529}
530
531fn evaluate_expression_cached(
532 level: &SubGraph,
533 path: &LevelPath,
534 node: BloqNodeId,
535 expr: &ClassicalExpr,
536 cache: &mut FxHashMap<NodeRef, Option<bool>>,
537) -> Option<bool> {
538 expr.eval(&mut |slot| {
539 let producer = level
540 .value_inputs(node)
541 .find(|input| input.slot == slot)?
542 .producer;
543 evaluate_producer(level, path, producer, cache)
544 })
545}
546
547fn evaluate_producer(
548 level: &SubGraph,
549 path: &LevelPath,
550 producer: BloqNodeId,
551 cache: &mut FxHashMap<NodeRef, Option<bool>>,
552) -> Option<bool> {
553 let node_ref = NodeRef {
554 path: path.clone(),
555 node: producer,
556 };
557 if let Some(value) = cache.get(&node_ref) {
558 return *value;
559 }
560 cache.insert(node_ref.clone(), None);
561 if let Some(slot) = level[producer].activation
562 && evaluate_expression_cached(level, path, producer, &ClassicalExpr::In(slot), cache)
563 == Some(false)
564 {
565 cache.insert(node_ref, Some(false));
566 return Some(false);
567 }
568 let value = match level[producer].try_classical()? {
569 ClassicalNode::Compute { expr } => {
570 evaluate_expression_cached(level, path, producer, expr, cache)
571 }
572 ClassicalNode::Observable { .. } | ClassicalNode::Discard { .. } => None,
573 };
574 cache.insert(node_ref, value);
575 value
576}
577
578#[derive(Clone)]
584struct PendingPauliSeed {
585 instance: TemplateInstanceId,
586 face: BoundaryFace,
587 operator: PauliMap,
588}
589
590struct PendingRegion {
593 id: ProgramRegionId,
594 coords: Option<DetectorCoords>,
595 terms: Vec<InstanceMeasurement>,
597 seeds: Vec<PendingPauliSeed>,
599}
600
601#[derive(Default)]
605struct TapeBuilder {
606 segments: Vec<Vec<Op>>,
609 segment_owner: Vec<(NodeRef, usize)>,
612 node_ranges: Vec<(NodeRef, usize, usize)>,
615 instance_to_global: FxHashMap<InstanceMeasurement, u32>,
618 instance_boundaries: FxHashMap<TemplateInstanceId, (usize, usize)>,
620 pending: Vec<PendingRegion>,
623 next_global: u32,
625}
626
627impl TapeBuilder {
628 fn add_node(
630 &mut self,
631 program: &Bloq,
632 path: &LevelPath,
633 id: BloqNodeId,
634 node: &crate::BloqNode,
635 plans: &ValidatedPlans,
636 ) -> Result<(), ProgramSliceError> {
637 let node_ref = NodeRef {
638 path: path.clone(),
639 node: id,
640 };
641 let plan = plans
642 .get(path, id)
643 .expect("materialization returns one emission plan per quantum node (IR-02)");
644 let ops = plan
645 .circuit
646 .body(plan.circuit.entry_body())
647 .map(bloq_circuit::CircuitBody::ops)
648 .unwrap_or_default();
649
650 let mut local_to_global: FxHashMap<u32, u32> = FxHashMap::default();
653 for op in ops {
654 for &local in op_measurement_ids(op) {
655 local_to_global.entry(local).or_insert_with(|| {
656 let global = self.next_global;
657 self.next_global += 1;
658 global
659 });
660 }
661 }
662 let rewritten: Vec<Op> = ops
663 .iter()
664 .map(|op| rewrite_measurement_ids(op, &local_to_global))
665 .collect();
666
667 for (&instance_measurement, &local) in &plan.measurements {
671 if let Some(&global) = local_to_global.get(&local) {
672 self.instance_to_global.insert(instance_measurement, global);
673 }
674 }
675
676 let moments = moment_segments_owned(rewritten);
678 let start = self.segments.len();
679 for (moment, segment) in moments.into_iter().enumerate() {
680 self.segment_owner.push((node_ref.clone(), moment));
681 self.segments.push(segment.ops);
682 }
683 let len = self.segments.len() - start;
684 self.node_ranges.push((node_ref.clone(), start, len));
685
686 let quantum = node.expect_quantum();
687 for instance in &quantum.instances {
688 self.instance_boundaries
689 .insert(instance.id, (start, start + len));
690 }
691 self.stage_detectors(program, &node_ref, quantum)?;
692 Ok(())
693 }
694
695 fn stage_detectors(
700 &mut self,
701 program: &Bloq,
702 node_ref: &NodeRef,
703 quantum: &crate::QuantumNode,
704 ) -> Result<(), ProgramSliceError> {
705 let template_count = quantum
706 .instances
707 .iter()
708 .try_fold(0usize, |count, instance| {
709 let rows = program
710 .templates()
711 .get(instance.template_id)
712 .expect("emission plan resolved template")
713 .detectors
714 .len();
715 count
716 .checked_add(rows)
717 .ok_or(crate::DetectorBundleError::CountOverflow)
718 })?;
719 let count = template_count
720 .checked_add(program.node_detector_count(quantum)?)
721 .ok_or(crate::DetectorBundleError::CountOverflow)?;
722 u32::try_from(count).map_err(|_| crate::DetectorBundleError::CountOverflow)?;
723 let mut index = 0u32;
724 for instance in &quantum.instances {
725 let template = program
726 .templates()
727 .get(instance.template_id)
728 .expect("emission plan already resolved every instance template");
729 for detector in &template.detectors {
730 let terms = detector
732 .parity
733 .measurements()
734 .map(|measurement| InstanceMeasurement {
735 instance: instance.id,
736 measurement,
737 })
738 .collect();
739 self.pending.push(PendingRegion {
740 id: ProgramRegionId::Detector {
741 owner: node_ref.clone(),
742 detector: index,
743 },
744 coords: detector.coords.as_ref().map(|coords| {
745 bloq_circuit::translate_detector_coords(coords, instance.offset)
746 }),
747 terms,
748 seeds: Vec::new(),
749 });
750 index += 1;
751 }
752 }
753 for detector in program.node_detectors(quantum)? {
754 self.pending.push(PendingRegion {
755 id: ProgramRegionId::Detector {
756 owner: node_ref.clone(),
757 detector: index,
758 },
759 coords: detector
761 .coords()
762 .map(|coords| DetectorCoords::from_slice(&coords.collect::<Vec<_>>())),
763 terms: detector.measurements().collect(),
764 seeds: Vec::new(),
765 });
766 index += 1;
767 }
768 Ok(())
769 }
770
771 fn stage_observable(
774 &mut self,
775 level: &SubGraph,
776 path: &LevelPath,
777 node: BloqNodeId,
778 index: u32,
779 options: &ProgramSliceOptions,
780 ) -> Result<(), ProgramSliceError> {
781 let content = ObservableCollector {
782 cache: FxHashMap::default(),
783 physical_observables: options.physical_observables,
784 }
785 .inputs(level, path, node)?;
786
787 self.pending.push(PendingRegion {
788 id: ProgramRegionId::Observable { index },
789 coords: None,
790 terms: content.parity.measurements().collect(),
791 seeds: content.seeds,
792 });
793 Ok(())
794 }
795
796 fn finish(
799 self,
800 known_measurements: &FxHashSet<InstanceMeasurement>,
801 known_instances: &FxHashMap<TemplateInstanceId, crate::TemplateId>,
802 out: &mut ProgramSlices,
803 ) -> Result<(), ProgramSliceError> {
804 let (detectors, seeds, meta) =
807 {
808 let mut detectors = Vec::with_capacity(self.pending.len());
809 let mut seeds = Vec::new();
810 let mut meta: Vec<ProgramRegion> = Vec::with_capacity(self.pending.len());
811 for pending in self.pending {
812 let PendingRegion {
813 id: region_id,
814 coords,
815 terms,
816 seeds: pending_seeds,
817 } = pending;
818 let parity = DetectorParity::from_measurements(terms);
819 let mut measurements = Vec::with_capacity(parity.terms().len());
820 let mut cross_tape = false;
821 for term in parity.measurements() {
822 match self.instance_to_global.get(&term) {
823 Some(&global) => measurements.push(global),
824 None => {
825 if !known_measurements.contains(&term) {
826 return Err(ProgramSliceError::UnknownMeasurement {
827 region: region_id,
828 measurement: term,
829 });
830 }
831 cross_tape = true;
832 break;
833 }
834 }
835 }
836 let pending_seeds = canonicalize_pauli_seeds(pending_seeds);
837 let mut resolved_seeds = Vec::with_capacity(pending_seeds.len());
838 for seed in pending_seeds {
839 let Some(&(input, output)) = self.instance_boundaries.get(&seed.instance)
840 else {
841 if !known_instances.contains_key(&seed.instance) {
842 return Err(ProgramSliceError::UnknownInstance {
843 region: region_id,
844 instance: seed.instance,
845 });
846 }
847 cross_tape = true;
848 break;
849 };
850 resolved_seeds.push((
851 match seed.face {
852 BoundaryFace::Input => input,
853 BoundaryFace::Output => output,
854 },
855 pauli_region_terms(&seed.operator),
856 ));
857 }
858 if cross_tape {
859 out.skipped_cross_tape += 1;
860 continue;
861 }
862 let id = u32::try_from(meta.len())
863 .map_err(|_| ProgramSliceError::RegionIdOverflow)?;
864 detectors.push(SliceDetector { id, measurements });
865 seeds.extend(resolved_seeds.into_iter().map(|(boundary, terms)| {
866 SliceRegionSeed {
867 id,
868 boundary,
869 terms,
870 }
871 }));
872 meta.push(ProgramRegion {
873 id: region_id,
874 coords: coords.map(Arc::new),
875 });
876 }
877 (detectors, seeds, meta)
878 };
879
880 let region_base = checked_region_base(out.regions.len(), meta.len())?;
881 let segment_refs: Vec<&[Op]> = self.segments.iter().map(Vec::as_slice).collect();
882 let mut slices = detector_slices_with_seeds(&segment_refs, &detectors, &seeds)?;
883
884 for (node_ref, start, len) in &self.node_ranges {
886 let mut node_slices = vec![Vec::new(); *len];
887 for (moment, slots) in node_slices.iter_mut().enumerate() {
888 for (detector_id, terms) in std::mem::take(&mut slices.slices[start + moment]) {
889 slots.push(RegionView {
890 region: region_base + detector_id,
891 terms,
892 });
893 }
894 }
895 out.per_node
896 .insert(node_ref.clone(), NodeSlices(node_slices));
897 }
898
899 for brk in slices.breaks {
900 let meta = &meta[brk.detector as usize];
901 let Some((node, moment)) = self.segment_owner.get(brk.segment).cloned() else {
902 return Err(ProgramSliceError::BreakOutsideTimeline {
903 region: meta.id.clone(),
904 });
905 };
906 out.breaks.push(ProgramRegionBreak {
907 node,
908 moment,
909 region: meta.id.clone(),
910 qubit: brk.qubit,
911 kind: brk.kind,
912 });
913 }
914 out.regions.extend(meta);
915 Ok(())
916 }
917}
918
919fn canonicalize_pauli_seeds(seeds: Vec<PendingPauliSeed>) -> Vec<PendingPauliSeed> {
923 let mut groups: FxHashMap<(TemplateInstanceId, BoundaryFace), PauliMap> = FxHashMap::default();
924 for seed in seeds {
925 groups
926 .entry((seed.instance, seed.face))
927 .and_modify(|operator| *operator = &*operator ^ &seed.operator)
928 .or_insert(seed.operator);
929 }
930 let mut seeds: Vec<PendingPauliSeed> = groups
931 .into_iter()
932 .filter_map(|((instance, face), operator)| {
933 (!operator.is_empty()).then_some(PendingPauliSeed {
934 instance,
935 face,
936 operator,
937 })
938 })
939 .collect();
940 seeds.sort_unstable_by_key(|seed| {
941 (
942 seed.instance.0,
943 match seed.face {
944 BoundaryFace::Input => 0,
945 BoundaryFace::Output => 1,
946 },
947 )
948 });
949 seeds
950}
951
952#[derive(Clone, Default)]
955struct ObservableContent {
956 parity: NodeDetectorParity,
957 seeds: Vec<PendingPauliSeed>,
958}
959
960impl ObservableContent {
961 fn xor_assign(&mut self, other: Self) {
962 self.parity.xor_assign(&other.parity);
963 self.seeds.extend(other.seeds);
964 self.seeds = canonicalize_pauli_seeds(std::mem::take(&mut self.seeds));
965 }
966}
967
968#[derive(Clone, Copy, PartialEq, Eq, Hash)]
969enum ContentKind {
970 Value,
971 Boundary,
972}
973
974struct ObservableCollector {
975 physical_observables: bool,
976 cache: FxHashMap<(NodeRef, ContentKind), ObservableContent>,
977}
978
979impl ObservableCollector {
980 fn inactive(&self, level: &SubGraph, path: &LevelPath, node: BloqNodeId) -> bool {
982 level[node].activation.is_some_and(|slot| {
983 evaluate_expression(level, path, node, &ClassicalExpr::In(slot)) == Some(false)
984 })
985 }
986
987 fn inputs(
988 &mut self,
989 level: &SubGraph,
990 path: &LevelPath,
991 node: BloqNodeId,
992 ) -> Result<ObservableContent, ProgramSliceError> {
993 let mut content = ObservableContent::default();
994 if self.inactive(level, path, node) {
995 return Ok(content);
996 }
997 if let Some(ClassicalNode::Observable {
998 measurements,
999 operators,
1000 ..
1001 }) = level[node].try_classical()
1002 {
1003 content.parity = NodeDetectorParity::from_measurements(measurements.iter().copied());
1004 content.seeds = canonicalize_pauli_seeds(
1005 operators
1006 .iter()
1007 .map(|operator| PendingPauliSeed {
1008 instance: operator.instance,
1009 face: operator.face,
1010 operator: operator.operator.clone(),
1011 })
1012 .collect(),
1013 );
1014 }
1015 for input in level.data_inputs(node) {
1016 if self.physical_observables && *input.role != crate::ValueRole::Data {
1019 continue;
1020 }
1021 if input.output != Some(crate::ObservableOutput::Flip) {
1022 content.xor_assign(self.producer(
1023 level,
1024 path,
1025 input.producer,
1026 ContentKind::Value,
1027 )?);
1028 }
1029 if input.output.is_none() {
1030 content.xor_assign(self.producer(
1031 level,
1032 path,
1033 input.producer,
1034 ContentKind::Boundary,
1035 )?);
1036 }
1037 }
1038 Ok(content)
1039 }
1040
1041 fn producer(
1042 &mut self,
1043 level: &SubGraph,
1044 path: &LevelPath,
1045 producer: BloqNodeId,
1046 kind: ContentKind,
1047 ) -> Result<ObservableContent, ProgramSliceError> {
1048 let node_ref = NodeRef {
1049 path: path.clone(),
1050 node: producer,
1051 };
1052 let key = (node_ref.clone(), kind);
1053 if let Some(content) = self.cache.get(&key) {
1054 return Ok(content.clone());
1055 }
1056 let mut content = ObservableContent::default();
1057 if self.inactive(level, path, producer) {
1058 self.cache.insert(key, content.clone());
1059 return Ok(content);
1060 }
1061 match (level[producer].try_classical(), &level[producer].kind) {
1062 (Some(ClassicalNode::Observable { .. }), _) => {
1063 content = self.inputs(level, path, producer)?;
1064 match kind {
1065 ContentKind::Value => content.seeds.clear(),
1066 ContentKind::Boundary => content.parity = NodeDetectorParity::default(),
1067 }
1068 }
1069 (Some(ClassicalNode::Compute { expr }), _) if kind == ContentKind::Value => {
1070 if !expr.is_linear() {
1073 return Err(ProgramSliceError::NonLinearExpr { node: node_ref });
1074 }
1075 content = self.inputs(level, path, producer)?;
1076 }
1077 (_, BloqNodeKind::Region(region)) => {
1078 let selected = BodySelector::Body;
1079 let body = region
1080 .bodies()
1081 .find_map(|(selector, body)| (selector == selected).then_some(body))
1082 .expect("regions have one body");
1083 let child_path = path.child(producer, selected);
1084 match kind {
1085 ContentKind::Boundary => {
1086 for &output in body.boundary_outputs() {
1087 content.xor_assign(self.producer(body, &child_path, output, kind)?);
1088 }
1089 }
1090 ContentKind::Value => {
1091 if let Some(output) = body.value_output()
1092 && output.output != crate::ObservableOutput::Flip
1093 {
1094 content = self.producer(body, &child_path, output.node, kind)?;
1095 }
1096 }
1097 }
1098 }
1099 (_, BloqNodeKind::Classical(_) | BloqNodeKind::Quantum(_)) => {}
1101 }
1102 self.cache.insert(key, content.clone());
1103 Ok(content)
1104 }
1105}
1106
1107fn pauli_region_terms(operator: &bloq_circuit::PauliMap) -> Vec<RegionTerm> {
1108 operator
1109 .iter()
1110 .filter_map(|(&qubit, &pauli)| {
1111 let pauli = match pauli {
1112 Pauli::X => PauliBasis::X,
1113 Pauli::Y => PauliBasis::Y,
1114 Pauli::Z => PauliBasis::Z,
1115 Pauli::I => return None,
1116 };
1117 Some(RegionTerm { qubit, pauli })
1118 })
1119 .collect()
1120}
1121
1122fn op_measurement_ids(op: &Op) -> &[u32] {
1125 match op {
1126 Op::Measure { measurements, .. } | Op::MPP { measurements, .. } => measurements,
1127 Op::Gate { .. }
1128 | Op::Tick
1129 | Op::Repeat { .. }
1130 | Op::ConditionalPauli(_)
1131 | Op::Depolarize1 { .. }
1132 | Op::Depolarize2 { .. }
1133 | Op::PauliError { .. } => &[],
1134 }
1135}
1136
1137fn rewrite_measurement_ids(op: &Op, map: &FxHashMap<u32, u32>) -> Op {
1141 let remap = |ids: &[u32]| -> Vec<u32> {
1142 ids.iter()
1143 .map(|id| {
1144 *map.get(id)
1145 .expect("every op measurement id was assigned a global id")
1146 })
1147 .collect()
1148 };
1149 match op {
1150 Op::Measure {
1151 basis,
1152 qubits,
1153 measurements,
1154 flip_probability,
1155 } => Op::Measure {
1156 basis: *basis,
1157 qubits: qubits.clone(),
1158 measurements: remap(measurements),
1159 flip_probability: *flip_probability,
1160 },
1161 Op::MPP {
1162 products,
1163 measurements,
1164 } => Op::MPP {
1165 products: products.clone(),
1166 measurements: remap(measurements),
1167 },
1168 Op::ConditionalPauli(corrections) => Op::ConditionalPauli(
1169 corrections
1170 .iter()
1171 .copied()
1172 .map(|mut correction| {
1173 correction.control = *map
1174 .get(&correction.control)
1175 .expect("every measurement control names a produced record");
1176 correction
1177 })
1178 .collect(),
1179 ),
1180 other => other.clone(),
1181 }
1182}
1183
1184#[cfg(test)]
1185mod tests {
1186 #[test]
1187 fn region_indices_reject_overflow_before_scattering() {
1188 let maximum = u32::MAX as usize;
1189 assert_eq!(super::checked_region_base(maximum, 0).unwrap(), u32::MAX);
1190 assert!(matches!(
1191 super::checked_region_base(maximum, 1),
1192 Err(super::ProgramSliceError::RegionIdOverflow)
1193 ));
1194 assert!(matches!(
1195 super::checked_region_base(usize::MAX, 1),
1196 Err(super::ProgramSliceError::RegionIdOverflow)
1197 ));
1198 }
1199 use bloq_circuit::{
1200 CircuitBody, ConditionalCorrection, CoordCircuit, DetectorTerm, GateType, LoopStateId,
1201 };
1202 use glam::ivec2;
1203
1204 use super::*;
1205 use crate::test_fixture::instance_measurement;
1206 use crate::{
1207 BloqEdge, BloqNode, BloqTemplate, InstanceBoundaryOperator, RegionNode, TemplateDetector,
1208 TemplateDetectorScope, TemplateInstance, TemplateRepeatState,
1209 };
1210
1211 fn z_measure_template(q: IVec2) -> BloqTemplate {
1213 let mut circuit = CoordCircuit::new();
1214 circuit.measure(PauliBasis::Z, [q]);
1215 BloqTemplate::new(circuit)
1216 }
1217
1218 fn single_instance(id: u32, template: crate::TemplateId) -> Vec<TemplateInstance> {
1219 vec![TemplateInstance::new(
1220 TemplateInstanceId(id),
1221 template,
1222 ivec2(0, 0),
1223 )]
1224 }
1225
1226 #[test]
1227 fn pinned_membership_slices_only_the_selected_observable_records() {
1228 let program = Bloq::from_text(
1229 "BLOQIR 1
1230 template t0 {
1231 circuit {
1232 Z (0,0)
1233 M (0,0):m0
1234 }
1235 }
1236 graph {
1237 n0 quantum {
1238 instance i0 t0 @ (0,0)
1239 }
1240 n1 observable fragment measurements i0:m0
1241 n2 compute in0 from selector choice
1242 n3 compute !in0
1243 n4 quantum {
1244 instance i1 t0 @ (1,0)
1245 instance i2 t0 @ (2,0)
1246 guard 0 i1
1247 guard 1 i2
1248 }
1249 n5 observable fragment measurements i1:m0 when v0
1250 n6 observable fragment measurements i2:m0 when v0
1251 n7 observable 0
1252 n0 -> n1 order
1253 n1 -> n2 value 0
1254 n2 -> n3 value 0
1255 n2 -> n4 value 0
1256 n3 -> n4 value 1
1257 n2 -> n5 value 0
1258 n3 -> n6 value 0
1259 n4 -> n5 order
1260 n4 -> n6 order
1261 n5 -> n7 compose 0
1262 n6 -> n7 compose 1
1263 }",
1264 )
1265 .unwrap();
1266 assert!(matches!(
1267 program_detector_slices(&program),
1268 Err(ProgramSliceError::MembershipSelectionRequired)
1269 ));
1270 for choice in [false, true] {
1271 let pinned = program
1272 .pin_membership(&[("choice".into(), choice)].into())
1273 .unwrap();
1274 let slices = program_detector_slices(&pinned).unwrap();
1275 let timeline = &slices.per_node[&NodeRef::top_level(BloqNodeId(4))];
1276 assert_eq!(timeline.0.len(), 2);
1277 assert_eq!(timeline.0[0].len(), 1);
1278 assert_eq!(
1279 timeline.0[0][0].terms,
1280 vec![RegionTerm {
1281 qubit: ivec2(if choice { 1 } else { 2 }, 0),
1282 pauli: PauliBasis::Z
1283 }]
1284 );
1285 assert_eq!(
1286 slices.regions[timeline.0[0][0].region as usize].id,
1287 ProgramRegionId::Observable { index: 0 }
1288 );
1289 }
1290 }
1291
1292 #[test]
1293 fn physical_observable_support_excludes_runtime_sign_folds() {
1294 for role in ["feedback 0", "readout"] {
1295 for expression in ["in0 & in1", "in0 ^ in1"] {
1296 let program = Bloq::from_text(&format!(
1297 "BLOQIR 1
1298 template t0 {{
1299 circuit {{
1300 RX (0,0) (1,0) (2,0)
1301 TICK
1302 M (0,0):m0 (1,0):m1 (2,0):m2
1303 }}
1304 }}
1305 graph {{
1306 n0 quantum {{
1307 instance i0 t0 @ (0,0)
1308 }}
1309 n1 observable fragment measurements i0:m0
1310 n2 observable fragment measurements i0:m1
1311 n3 observable fragment measurements i0:m2
1312 n4 compute {expression}
1313 n5 observable 0
1314 n0 -> n1 order
1315 n0 -> n2 order
1316 n0 -> n3 order
1317 n2 -> n4 value 0
1318 n3 -> n4 value 1
1319 n1 -> n5 compose 0
1320 n4 -> n5 value 1 {role}
1321 }}"
1322 ))
1323 .unwrap();
1324 let mut options = ProgramSliceOptions::default();
1325 options.set_physical_observables(true);
1326 let slices = program_detector_slices_with_options(&program, &options).unwrap();
1327 let timeline = &slices.per_node[&NodeRef::top_level(BloqNodeId(0))];
1328 assert_eq!(timeline.0[0].len(), 1);
1329 assert_eq!(
1330 timeline.0[0][0].terms,
1331 vec![RegionTerm {
1332 qubit: ivec2(0, 0),
1333 pauli: PauliBasis::Z,
1334 }]
1335 );
1336 if expression.contains('&') {
1337 assert!(matches!(
1338 program_detector_slices(&program),
1339 Err(ProgramSliceError::NonLinearExpr { .. })
1340 ));
1341 } else {
1342 let full = program_detector_slices(&program).unwrap();
1343 assert_eq!(
1344 full.per_node[&NodeRef::top_level(BloqNodeId(0))].0[0][0]
1345 .terms
1346 .len(),
1347 3
1348 );
1349 }
1350 }
1351 }
1352 }
1353
1354 #[test]
1355 fn nested_recipe_preserves_physical_roles_and_boundary_bindings() {
1356 let source = "BLOQIR 1
1357template t0 {
1358 circuit {
1359 RX (0,0) (1,0)
1360 TICK
1361 M (0,0):m0 (1,0):m1
1362 }
1363}
1364graph {
1365 n0 quantum {
1366 instance i0 t0 @ (0,0)
1367 }
1368 n1 observable fragment measurements i0:m0
1369 n2 observable fragment measurements i0:m1
1370 n3 observable 0
1371 n4 compute in0
1372 n5 observable fragment operators i0 output Z(0,0)
1373 n6 observable fragment
1374 n7 observable fragment
1375 n8 observable 1
1376 n9 compute in0
1377 n10 observable 2
1378 n11 compute in0
1379 n12 compute 0
1380 n13 observable fragment operators i0 output X(1,0) when v0
1381 n0 -> n1 order
1382 n0 -> n2 order
1383 n0 -> n5 order
1384 n0 -> n13 order
1385 n12 -> n13 value 0
1386 n2 -> n3 compose 0
1387 n3 -> n4 value 0
1388 n1 -> n6 compose 0
1389 n4 -> n6 value 1 readout
1390 n1 -> n6 value 2 feedback 7
1391 n5 -> n6 compose 3
1392 n13 -> n6 compose 4
1393 n6 -> n7 compose 0
1394 __OBS_EDGES__
1395 n8 -> n9 value 0
1396 n10 -> n11 value 0
1397}";
1398 let recipe = Bloq::from_text(
1399 &source.replace("__OBS_EDGES__", "n7 -> n8 compose 0\n n7 -> n10 compose 0"),
1400 )
1401 .unwrap();
1402 let flat = Bloq::from_text(&source.replace(
1403 "__OBS_EDGES__",
1404 "n1 -> n8 compose 0\n n4 -> n8 value 1 readout\n n1 -> n8 value 2 feedback 7\n n5 -> n8 compose 3\n n13 -> n8 compose 4\n n1 -> n10 compose 0\n n4 -> n10 value 1 readout\n n1 -> n10 value 2 feedback 7\n n5 -> n10 compose 3\n n13 -> n10 compose 4",
1405 ))
1406 .unwrap();
1407 recipe.validate().unwrap();
1408 flat.validate().unwrap();
1409 for node in [BloqNodeId(8), BloqNodeId(10)] {
1410 for physical_observables in [false, true] {
1411 let collect = |program: &Bloq| {
1412 ObservableCollector {
1413 physical_observables,
1414 cache: FxHashMap::default(),
1415 }
1416 .inputs(program.top(), &LevelPath::default(), node)
1417 .unwrap()
1418 };
1419 let shared = collect(&recipe);
1420 let direct = collect(&flat);
1421 assert_eq!(shared.parity, direct.parity);
1422 assert_eq!(shared.seeds.len(), direct.seeds.len());
1423 assert_eq!(shared.seeds.len(), 1);
1424 assert_eq!(shared.seeds[0].instance, direct.seeds[0].instance);
1425 assert_eq!(shared.seeds[0].face, direct.seeds[0].face);
1426 assert_eq!(shared.seeds[0].operator, direct.seeds[0].operator);
1427 if physical_observables {
1428 assert_eq!(
1429 shared.parity,
1430 NodeDetectorParity::from_measurements([instance_measurement(0, 0)])
1431 );
1432 }
1433 }
1434 }
1435 for physical_observables in [false, true] {
1436 let mut options = ProgramSliceOptions::default();
1437 options.set_physical_observables(physical_observables);
1438 let shared = program_detector_slices_with_options(&recipe, &options).unwrap();
1439 let direct = program_detector_slices_with_options(&flat, &options).unwrap();
1440 assert_eq!(shared.regions, direct.regions);
1441 assert_eq!(shared.per_node, direct.per_node);
1442 assert_eq!(shared.breaks, direct.breaks);
1443 assert_eq!(shared.skipped_cross_tape, direct.skipped_cross_tape);
1444 }
1445 }
1446
1447 #[test]
1448 fn nonlinear_observable_record_folds_are_rejected() {
1449 for expression in ["in0 & 0", "in0 | 1", "in0 & in1", "select(in0, 0, 1)"] {
1450 let second = if expression.contains("in1") {
1451 "n1 -> n2 value 1"
1452 } else {
1453 ""
1454 };
1455 let mut program = Bloq::from_text(&format!(
1456 "BLOQIR 1
1457 template t0 {{
1458 circuit {{
1459 RX (0,0)
1460 TICK
1461 M (0,0):m0
1462 }}
1463 }}
1464 graph {{
1465 n0 quantum {{
1466 instance i0 t0 @ (0,0)
1467 }}
1468 n1 observable fragment measurements i0:m0
1469 n2 compute {expression}
1470 n3 observable 0
1471 n0 -> n1 order
1472 n1 -> n2 value 0
1473 {second}
1474 n2 -> n3 value 0
1475 }}
1476"
1477 ))
1478 .unwrap();
1479 program
1480 .node_mut(BloqNodeId(0))
1481 .unwrap()
1482 .expect_quantum_mut()
1483 .detectors
1484 .push(crate::NodeDetector {
1485 parity: NodeDetectorParity::from_measurements([instance_measurement(0, 0)]),
1486 coords: None,
1487 });
1488 program.validate().unwrap();
1489 assert!(
1490 matches!(program_detector_slices(&program), Err(ProgramSliceError::NonLinearExpr { node }) if node == NodeRef::top_level(BloqNodeId(2)))
1491 );
1492 let mut options = ProgramSliceOptions::default();
1493 options.set_physical_observables(true);
1494 assert!(matches!(
1495 program_detector_slices_with_options(&program, &options),
1496 Err(ProgramSliceError::NonLinearExpr { .. })
1497 ));
1498 options.set_detectors_only(true);
1499 let slices = program_detector_slices_with_options(&program, &options).unwrap();
1500 assert_eq!(
1501 slices
1502 .regions
1503 .iter()
1504 .map(|region| ®ion.id)
1505 .collect::<Vec<_>>(),
1506 [&ProgramRegionId::Detector {
1507 owner: NodeRef::top_level(BloqNodeId(0)),
1508 detector: 0
1509 }]
1510 );
1511 }
1512 }
1513
1514 #[test]
1515 fn upstream_observable_bits_carry_records_without_boundary_operators() {
1516 let program = Bloq::from_text(
1517 "BLOQIR 1
1518 template t0 {
1519 circuit {
1520 RX (0,0)
1521 R (1,0)
1522 TICK
1523 M (1,0):m0
1524 }
1525 }
1526 graph {
1527 n0 quantum {
1528 instance i0 t0 @ (0,0)
1529 }
1530 n1 observable fragment operators i0 output X(0,0)
1531 n2 observable 0
1532 n3 compute in0
1533 n4 observable 1
1534 n5 observable fragment measurements i0:m0
1535 n0 -> n5 order
1536 n1 -> n2 compose 0
1537 n5 -> n2 compose 1
1538 n2 -> n3 value 0
1539 n3 -> n4 value 0
1540 }
1541",
1542 )
1543 .unwrap();
1544 let slices = program_detector_slices(&program).unwrap();
1545 let mut raw_record = false;
1546 let mut direct_boundary = false;
1547 for view in slices
1548 .per_node
1549 .values()
1550 .flat_map(|node| node.0.iter().flatten())
1551 {
1552 match slices.regions[view.region as usize].id {
1553 ProgramRegionId::Observable { index: 0 } => {
1554 direct_boundary |= view.terms.iter().any(|term| term.qubit == ivec2(0, 0))
1555 }
1556 ProgramRegionId::Observable { index: 1 } => {
1557 assert!(view.terms.iter().all(|term| term.qubit != ivec2(0, 0)));
1558 raw_record |= view.terms.iter().any(|term| term.qubit == ivec2(1, 0));
1559 }
1560 _ => {}
1561 }
1562 }
1563 assert!(raw_record && direct_boundary);
1564 assert!(slices.breaks.is_empty());
1565 }
1566
1567 #[test]
1568 fn complete_observable_owns_direct_records_and_boundary_but_consumers_read_only_records() {
1569 let mut program = Bloq::from_text(
1570 "BLOQIR 1
1571 template t0 {
1572 circuit {
1573 RX (0,0)
1574 R (1,0)
1575 TICK
1576 M (1,0):m0
1577 }
1578 }
1579 graph {
1580 n0 quantum {
1581 instance i0 t0 @ (0,0)
1582 }
1583 n1 observable 0
1584 n2 observable fragment
1585 n3 observable 1
1586 n4 observable 2
1587 n0 -> n1 order
1588 n1 -> n2 compose 0
1589 n1 -> n3 value 0
1590 n2 -> n4 value 0
1591 }
1592",
1593 )
1594 .unwrap();
1595 program.node_mut(BloqNodeId(1)).unwrap().kind = BloqNodeKind::Classical(
1596 ClassicalNode::Observable {
1597 index: Some(0),
1598 measurements: vec![instance_measurement(0, 0)],
1599 operators: vec![InstanceBoundaryOperator {
1600 instance: TemplateInstanceId(0),
1601 face: BoundaryFace::Output,
1602 operator: PauliMap::from_unique_entries([(ivec2(0, 0), Pauli::X)]),
1603 }],
1604 }
1605 .into(),
1606 );
1607 program.validate().unwrap();
1608 let mut collector = ObservableCollector {
1609 physical_observables: false,
1610 cache: FxHashMap::default(),
1611 };
1612 let direct = collector
1613 .inputs(program.top(), &LevelPath::default(), BloqNodeId(1))
1614 .unwrap();
1615 assert_eq!(
1616 direct.parity,
1617 NodeDetectorParity::from_measurements([instance_measurement(0, 0)])
1618 );
1619 assert_eq!(direct.seeds.len(), 1);
1620 for node in [BloqNodeId(3), BloqNodeId(4)] {
1621 let consumer = collector
1622 .inputs(program.top(), &LevelPath::default(), node)
1623 .unwrap();
1624 assert_eq!(consumer.parity, direct.parity);
1625 assert!(consumer.seeds.is_empty());
1626 }
1627 let slices = program_detector_slices(&program).unwrap();
1628 let timeline = &slices.per_node[&NodeRef::top_level(BloqNodeId(0))];
1629 let mut seen = FxHashSet::default();
1630 for view in timeline.0.iter().flatten() {
1631 let ProgramRegionId::Observable { index } = slices.regions[view.region as usize].id
1632 else {
1633 continue;
1634 };
1635 seen.insert(index);
1636 if index != 0 {
1637 assert!(view.terms.iter().all(|term| term.qubit != ivec2(0, 0)));
1638 }
1639 }
1640 assert_eq!(seen, FxHashSet::from_iter([0, 1, 2]));
1641 assert!(slices.breaks.is_empty());
1642 }
1643
1644 #[test]
1645 fn shared_observable_dags_cancel_records_and_positioned_operators() {
1646 let mut program = Bloq::from_text(
1647 "BLOQIR 1
1648 template t0 {
1649 circuit {
1650 RX (0,0)
1651 R (1,0)
1652 TICK
1653 M (1,0):m0
1654 }
1655 }
1656 graph {
1657 n0 quantum {
1658 instance i0 t0 @ (0,0)
1659 }
1660 n1 rus 0 {
1661 body {
1662 n0 observable fragment operators i0 output X(0,0)
1663 n1 observable fragment measurements i0:m0
1664 }
1665 }
1666 n2 observable 0
1667 n0 -> n1 order
1668 n1 -> n2 compose 0
1669 }
1670",
1671 )
1672 .unwrap();
1673 let expected = program_detector_slices(&program).unwrap();
1674 let mut previous = BloqNodeId(1);
1675 let mut predicate = program.add_node(BloqNode::classical(ClassicalNode::Compute {
1676 expr: ClassicalExpr::Const(true),
1677 }));
1678 for _ in 0..30 {
1679 for previous in [&mut previous, &mut predicate] {
1680 let next = program.add_node(BloqNode::classical(ClassicalNode::Compute {
1681 expr: ClassicalExpr::Xor(Box::new([
1682 ClassicalExpr::In(0),
1683 ClassicalExpr::In(1),
1684 ])),
1685 }));
1686 program.add_edge(*previous, next, BloqEdge::value(0));
1687 program.add_edge(*previous, next, BloqEdge::value(1));
1688 *previous = next;
1689 }
1690 }
1691 let BloqNodeKind::Region(RegionNode::RepeatUntilSuccess {
1692 restart_condition: condition,
1693 ..
1694 }) = &mut program.top_mut().node_mut(BloqNodeId(1)).unwrap().kind
1695 else {
1696 unreachable!()
1697 };
1698 *condition = ClassicalExpr::Or(Box::new([
1699 ClassicalExpr::Not(Box::new(ClassicalExpr::In(0))),
1700 ClassicalExpr::Const(false),
1701 ]));
1702 program.add_edge(predicate, BloqNodeId(1), BloqEdge::value(0));
1703 program.add_edge(previous, BloqNodeId(2), BloqEdge::value(1));
1704 let actual = program_detector_slices(&program).unwrap();
1705 assert_eq!(actual.regions, expected.regions);
1706 assert_eq!(actual.per_node, expected.per_node);
1707 assert_eq!(actual.breaks, expected.breaks);
1708 }
1709
1710 #[test]
1711 fn post_flatten_validation_is_wf_equivalent_for_looped_detector() {
1712 let q = ivec2(0, 0);
1713 let mut circuit = CoordCircuit::new();
1714 let seed = circuit.measure(PauliBasis::Z, [q])[0];
1715 let repeated = circuit.reserve_measurement_id(q);
1716 let body = circuit.add_body(CircuitBody::from_ops(vec![Op::Measure {
1717 basis: PauliBasis::Z,
1718 qubits: vec![q],
1719 measurements: vec![repeated],
1720 flip_probability: 0.0,
1721 }]));
1722 circuit
1723 .body_mut(circuit.entry_body())
1724 .expect("entry body")
1725 .ops_mut()
1726 .push(Op::Repeat {
1727 body,
1728 repetitions: 3,
1729 });
1730 let state = LoopStateId(0);
1731 let template = BloqTemplate::with_parts(
1732 circuit,
1733 vec![TemplateDetector {
1734 scope: TemplateDetectorScope::RepeatBody { body },
1735 parity: DetectorParity::from_terms([
1736 DetectorTerm::Measurement(repeated),
1737 DetectorTerm::LoopState(state),
1738 ]),
1739 coords: None,
1740 }],
1741 vec![TemplateRepeatState {
1742 body,
1743 state,
1744 initial: DetectorParity::from_measurements([seed]),
1745 next: DetectorParity::from_measurements([repeated]),
1746 }],
1747 Vec::new(),
1748 Vec::new(),
1749 );
1750 let mut program = Bloq::new();
1751 let template = program.add_template(template);
1752 let mut node = BloqNode::from_members(vec![]);
1753 node.expect_quantum_mut().instances = single_instance(0, template);
1754 program.add_node(node);
1755
1756 let before = program.validate();
1757 let mut flattened = program.clone();
1758 flattened.flatten().expect("representative loop flattens");
1759
1760 assert_eq!(before, Ok(()));
1761 assert_eq!(flattened.validate(), before);
1762 }
1763
1764 #[test]
1765 fn malformed_instance_reference_fails_resolution_instead_of_skipping() {
1766 let q = ivec2(0, 0);
1767 let mut bloq = Bloq::new();
1768 let template = bloq.add_template(z_measure_template(q));
1769 let mut node = BloqNode::from_members(vec![]);
1770 node.expect_quantum_mut().instances = single_instance(0, template);
1771 node.expect_quantum_mut().detectors = vec![crate::NodeDetector {
1772 parity: DetectorParity::from_measurements([instance_measurement(7, 0)]),
1773 coords: None,
1774 }];
1775 bloq.add_node(node);
1776
1777 assert!(matches!(
1778 program_detector_slices(&bloq),
1779 Err(ProgramSliceError::UnknownMeasurement {
1780 measurement: InstanceMeasurement {
1781 instance: TemplateInstanceId(7),
1782 ..
1783 },
1784 ..
1785 })
1786 ));
1787 }
1788
1789 #[test]
1790 fn unknown_observable_instance_fails_resolution() {
1791 let program = Bloq::from_text(
1792 "BLOQIR 1
1793template t0 {
1794 circuit {
1795 R (0,0)
1796 }
1797}
1798graph {
1799 n0 quantum {
1800 instance i0 t0 @ (0,0)
1801 }
1802 n1 observable fragment operators i7 output X(0,0)
1803 n2 observable 0
1804 n1 -> n2 compose 0
1805}",
1806 )
1807 .unwrap();
1808 assert!(matches!(
1809 program_detector_slices(&program),
1810 Err(ProgramSliceError::UnknownInstance {
1811 instance: TemplateInstanceId(7),
1812 ..
1813 })
1814 ));
1815 }
1816
1817 #[test]
1818 fn slicing_does_not_audit_unrelated_logical_cut_metadata() {
1819 let mut program = Bloq::new();
1820 program.set_logical_inputs(vec![crate::LogicalInput {
1821 port: glam::IVec3::ZERO,
1822 instance: TemplateInstanceId(7),
1823 x: PauliMap::empty(),
1824 z: PauliMap::empty(),
1825 }]);
1826 assert!(matches!(
1827 program.validate(),
1828 Err(BloqValidationError::LogicalInputUnknownInstance { .. })
1829 ));
1830 let slices = program_detector_slices(&program).unwrap();
1831 assert!(slices.regions.is_empty());
1832 assert!(slices.per_node.is_empty());
1833 }
1834
1835 #[test]
1836 fn cross_node_detector_region_spans_both_node_timelines() {
1837 let q = ivec2(0, 0);
1838 let mut child_circuit = CoordCircuit::new();
1841 child_circuit.do_gate(GateType::Z, [q]).unwrap();
1842 child_circuit.measure(PauliBasis::Z, [q]);
1843
1844 let mut bloq = Bloq::new();
1845 let parent_tpl = bloq.add_template(z_measure_template(q));
1846 let child_tpl = bloq.add_template(BloqTemplate::new(child_circuit));
1847
1848 let mut parent = BloqNode::from_members(vec![]);
1849 parent.expect_quantum_mut().instances = single_instance(0, parent_tpl);
1850 let mut child = BloqNode::from_members(vec![]);
1851 child.expect_quantum_mut().instances = single_instance(1, child_tpl);
1852 child.expect_quantum_mut().detectors = vec![crate::NodeDetector {
1853 parity: DetectorParity::from_measurements([
1854 instance_measurement(0, 0),
1855 instance_measurement(1, 0),
1856 ]),
1857 coords: Some(DetectorCoords::from_slice(&[5.0, 6.0])),
1858 }];
1859
1860 let parent_id = bloq.add_node(parent);
1861 let child_id = bloq.add_node(child);
1862 bloq.add_edge(parent_id, child_id, BloqEdge::quantum(vec![]));
1863
1864 let slices = program_detector_slices(&bloq).unwrap();
1865 assert_eq!(slices.skipped_cross_tape, 0);
1866 assert!(slices.breaks.is_empty());
1867
1868 let parent_ref = NodeRef::top_level(parent_id);
1869 let child_ref = NodeRef::top_level(child_id);
1870 let parent_slices = &slices.per_node[&parent_ref];
1871 let child_slices = &slices.per_node[&child_ref];
1872 assert_eq!(parent_slices.0.len(), 1, "parent: one measurement moment");
1873 assert_eq!(child_slices.0.len(), 2, "child: rotation then measurement");
1874
1875 let expected = vec![RegionTerm {
1876 qubit: q,
1877 pauli: PauliBasis::Z,
1878 }];
1879 assert_eq!(parent_slices.0[0].len(), 1);
1882 assert_eq!(child_slices.0[0].len(), 1);
1883 assert!(child_slices.0[1].is_empty());
1884
1885 let region = &parent_slices.0[0][0];
1886 assert_eq!(
1887 slices.regions[region.region as usize].id,
1888 ProgramRegionId::Detector {
1889 owner: child_ref,
1890 detector: 0,
1891 },
1892 "child's side table owns it"
1893 );
1894 assert_eq!(
1895 slices.regions[region.region as usize]
1896 .coords
1897 .as_ref()
1898 .map(|coords| coords.as_slice()),
1899 Some(&[5.0, 6.0][..])
1900 );
1901 assert_eq!(region.terms, expected);
1902 assert_eq!(child_slices.0[0][0].terms, expected);
1903 }
1904
1905 #[test]
1906 fn measurement_controlled_pauli_cancels_program_detector_dependency() {
1907 let parent_q = ivec2(9, 0);
1908 let q0 = ivec2(0, 0);
1909 let q1 = ivec2(1, 0);
1910
1911 let mut child_circuit = CoordCircuit::new();
1912 child_circuit.do_gate(GateType::RX, [q0]).unwrap();
1913 child_circuit.do_gate(GateType::RZ, [q1]).unwrap();
1914 child_circuit.tick();
1915 let m0 = child_circuit.measure(PauliBasis::Z, [q0])[0];
1916 child_circuit
1917 .body_mut(child_circuit.entry_body())
1918 .unwrap()
1919 .ops_mut()
1920 .push(Op::ConditionalPauli(vec![ConditionalCorrection {
1921 pauli: PauliBasis::X,
1922 control: m0,
1923 target: q1,
1924 }]));
1925 let m1 = child_circuit.measure(PauliBasis::Z, [q1])[0];
1926
1927 let mut bloq = Bloq::new();
1928 let parent_template = bloq.add_template(z_measure_template(parent_q));
1929 let child_template = bloq.add_template(BloqTemplate::new(child_circuit));
1930 let mut parent = BloqNode::from_members(vec![]);
1931 parent.expect_quantum_mut().instances = single_instance(0, parent_template);
1932 let mut child = BloqNode::from_members(vec![]);
1933 child.expect_quantum_mut().instances = single_instance(1, child_template);
1934 child.expect_quantum_mut().detectors = vec![crate::NodeDetector {
1935 parity: DetectorParity::from_measurements([
1936 instance_measurement(1, m0),
1937 instance_measurement(1, m1),
1938 ]),
1939 coords: None,
1940 }];
1941 let parent = bloq.add_node(parent);
1942 let child = bloq.add_node(child);
1943 bloq.add_edge(parent, child, BloqEdge::quantum(vec![]));
1944
1945 let slices = program_detector_slices(&bloq).unwrap();
1946 assert!(slices.breaks.is_empty());
1947 assert!(
1948 slices.per_node[&NodeRef::top_level(parent)]
1949 .0
1950 .iter()
1951 .all(Vec::is_empty),
1952 "the child-local control must not retain the parent's tape-global id"
1953 );
1954 let child_slices = &slices.per_node[&NodeRef::top_level(child)];
1955 assert_eq!(
1956 child_slices.0.len(),
1957 2,
1958 "feedforward adds no visible moment"
1959 );
1960 assert_eq!(
1961 child_slices.0[0],
1962 vec![RegionView {
1963 region: 0,
1964 terms: vec![RegionTerm {
1965 qubit: q1,
1966 pauli: PauliBasis::Z,
1967 }],
1968 }]
1969 );
1970 assert!(child_slices.0[1].is_empty());
1971 }
1972
1973 #[test]
1974 fn same_tick_measurement_keeps_its_place_before_a_rotation() {
1975 let q = ivec2(0, 0);
1976 let mut circuit = CoordCircuit::new();
1977 circuit.do_gate(GateType::RZ, [q]).unwrap();
1978 let measurement = circuit.measure(PauliBasis::Z, [q])[0];
1979 circuit.do_gate(GateType::H, [q]).unwrap();
1980 let ops = circuit.body(circuit.entry_body()).unwrap().ops();
1981 assert_eq!(
1982 moment_segments(ops)
1983 .into_iter()
1984 .flat_map(|segment| segment.ops)
1985 .collect::<Vec<_>>(),
1986 ops
1987 );
1988 let mut bloq = Bloq::new();
1989 let template = bloq.add_template(BloqTemplate::new(circuit));
1990 let mut node = BloqNode::from_members(Vec::new());
1991 node.expect_quantum_mut().instances = single_instance(0, template);
1992 node.expect_quantum_mut()
1993 .detectors
1994 .push(crate::NodeDetector {
1995 parity: DetectorParity::from_measurements([instance_measurement(0, measurement)]),
1996 coords: None,
1997 });
1998 bloq.add_node(node);
1999
2000 let slices = program_detector_slices(&bloq).unwrap();
2001
2002 assert!(slices.breaks.is_empty(), "RZ; M is deterministic before H");
2003 }
2004
2005 #[test]
2006 fn measurement_controlled_pauli_stays_before_a_following_reset() {
2007 let control = ivec2(0, 0);
2008 let target = ivec2(1, 0);
2009 let spectator = ivec2(2, 0);
2010 let mut circuit = CoordCircuit::new();
2011 circuit.do_gate(GateType::RX, [control]).unwrap();
2012 let m0 = circuit.measure(PauliBasis::Z, [control])[0];
2013 circuit.tick();
2014 circuit.measure(PauliBasis::Z, [spectator]);
2015 circuit
2016 .body_mut(circuit.entry_body())
2017 .unwrap()
2018 .ops_mut()
2019 .push(Op::ConditionalPauli(vec![ConditionalCorrection {
2020 pauli: PauliBasis::X,
2021 control: m0,
2022 target,
2023 }]));
2024 circuit.do_gate(GateType::RZ, [target]).unwrap();
2025 circuit.tick();
2026 let final_measurement = circuit.measure(PauliBasis::Z, [target])[0];
2027
2028 let ops = circuit
2029 .body(circuit.entry_body())
2030 .expect("entry body")
2031 .ops();
2032 let moments = moment_segments(ops);
2033 assert!(matches!(
2034 moments[2].ops.as_slice(),
2035 [Op::Measure { .. }, Op::ConditionalPauli(_)]
2036 ));
2037 assert!(matches!(
2038 moments[3].ops.as_slice(),
2039 [Op::Gate {
2040 gate: GateType::RZ,
2041 ..
2042 }]
2043 ));
2044
2045 let mut bloq = Bloq::new();
2046 let template = bloq.add_template(BloqTemplate::new(circuit));
2047 let mut node = BloqNode::from_members(vec![]);
2048 node.expect_quantum_mut().instances = single_instance(0, template);
2049 node.expect_quantum_mut().detectors = vec![crate::NodeDetector {
2050 parity: DetectorParity::from_measurements([instance_measurement(0, final_measurement)]),
2051 coords: None,
2052 }];
2053 let node = bloq.add_node(node);
2054
2055 let slices = program_detector_slices(&bloq).unwrap();
2056 assert!(slices.breaks.is_empty());
2057 let timeline = &slices.per_node[&NodeRef::top_level(node)].0;
2058 assert_eq!(timeline.len(), 5, "feedforward adds no visible moment");
2059 assert!(timeline[..3].iter().all(Vec::is_empty));
2060 assert_eq!(
2061 timeline[3][0].terms,
2062 vec![RegionTerm {
2063 qubit: target,
2064 pauli: PauliBasis::Z,
2065 }],
2066 "the reset erases the earlier correction before it can depend on m0"
2067 );
2068 }
2069
2070 #[test]
2071 fn same_tick_measurement_control_stays_before_a_following_reset() {
2072 let control = ivec2(0, 0);
2073 let target = ivec2(1, 0);
2074 let mut circuit = CoordCircuit::new();
2075 circuit.do_gate(GateType::RX, [control]).unwrap();
2076 circuit.tick();
2077 let m0 = circuit.measure(PauliBasis::Z, [control])[0];
2078 circuit
2079 .body_mut(circuit.entry_body())
2080 .unwrap()
2081 .ops_mut()
2082 .push(Op::ConditionalPauli(vec![ConditionalCorrection {
2083 pauli: PauliBasis::X,
2084 control: m0,
2085 target,
2086 }]));
2087 circuit.do_gate(GateType::RZ, [target]).unwrap();
2088 circuit.tick();
2089 let final_measurement = circuit.measure(PauliBasis::Z, [target])[0];
2090
2091 let ops = circuit
2092 .body(circuit.entry_body())
2093 .expect("entry body")
2094 .ops();
2095 let moments = moment_segments(ops);
2096 assert!(matches!(
2097 moments[1].ops.as_slice(),
2098 [Op::Measure { .. }, Op::ConditionalPauli(_)]
2099 ));
2100 assert!(matches!(
2101 moments[2].ops.as_slice(),
2102 [Op::Gate {
2103 gate: GateType::RZ,
2104 ..
2105 }]
2106 ));
2107
2108 let mut bloq = Bloq::new();
2109 let template = bloq.add_template(BloqTemplate::new(circuit));
2110 let mut node = BloqNode::from_members(vec![]);
2111 node.expect_quantum_mut().instances = single_instance(0, template);
2112 node.expect_quantum_mut().detectors = vec![crate::NodeDetector {
2113 parity: DetectorParity::from_measurements([instance_measurement(0, final_measurement)]),
2114 coords: None,
2115 }];
2116 let node = bloq.add_node(node);
2117
2118 let slices = program_detector_slices(&bloq).unwrap();
2119 assert!(slices.breaks.is_empty());
2120 let timeline = &slices.per_node[&NodeRef::top_level(node)].0;
2121 assert_eq!(timeline.len(), 4, "feedforward adds no visible moment");
2122 assert!(timeline[..2].iter().all(Vec::is_empty));
2123 assert_eq!(
2124 timeline[2][0].terms,
2125 vec![RegionTerm {
2126 qubit: target,
2127 pauli: PauliBasis::Z,
2128 }],
2129 "the reset erases the correction before it can depend on m0"
2130 );
2131 }
2132
2133 #[test]
2134 fn record_backed_observable_tracks_as_logical_region() {
2135 let q = ivec2(0, 0);
2136 let mut circuit = CoordCircuit::new();
2137 circuit.do_gate(GateType::Z, [q]).unwrap();
2138 circuit.measure(PauliBasis::Z, [q]);
2139
2140 let mut bloq = Bloq::new();
2141 let template = bloq.add_template(BloqTemplate::new(circuit));
2142 let mut node = BloqNode::from_members(vec![]);
2143 node.expect_quantum_mut().instances = single_instance(0, template);
2144 let quantum = bloq.add_node(node);
2145 let accumulate = bloq.add_node(BloqNode::classical(ClassicalNode::Observable {
2146 index: None,
2147 operators: Vec::new(),
2148 measurements: vec![instance_measurement(0, 0)],
2149 }));
2150 let observable = bloq.add_node(BloqNode::classical(ClassicalNode::observable(5)));
2151 bloq.add_edge(quantum, accumulate, BloqEdge::Order);
2152 bloq.add_edge(accumulate, observable, BloqEdge::compose(0));
2153
2154 let slices = program_detector_slices(&bloq).unwrap();
2155 let timeline = &slices.per_node[&NodeRef::top_level(quantum)];
2156
2157 assert_eq!(timeline.0.len(), 2);
2158 assert_eq!(timeline.0[0].len(), 1);
2159 assert_eq!(
2160 timeline.0[0][0],
2161 RegionView {
2162 region: 0,
2163 terms: vec![RegionTerm {
2164 qubit: q,
2165 pauli: PauliBasis::Z,
2166 }],
2167 }
2168 );
2169 assert!(timeline.0[1].is_empty());
2170 assert!(slices.breaks.is_empty());
2171 }
2172
2173 #[test]
2174 fn boundary_observable_seeds_input_and_output_at_node_faces() {
2175 let q = ivec2(0, 0);
2176 let mut circuit = CoordCircuit::new();
2177 circuit.do_gate(GateType::H, [q]).unwrap();
2178
2179 let mut bloq = Bloq::new();
2180 let template = bloq.add_template(BloqTemplate::new(circuit));
2181 let mut node = BloqNode::from_members(vec![]);
2182 node.expect_quantum_mut().instances = single_instance(0, template);
2183 let quantum = bloq.add_node(node);
2184 let include = bloq.add_node(BloqNode::classical(ClassicalNode::Observable {
2185 index: None,
2186 measurements: Vec::new(),
2187 operators: vec![
2188 InstanceBoundaryOperator {
2189 instance: TemplateInstanceId(0),
2190 face: BoundaryFace::Input,
2191 operator: PauliMap::from_unique_entries([(q, Pauli::Z)]),
2192 },
2193 InstanceBoundaryOperator {
2194 instance: TemplateInstanceId(0),
2195 face: BoundaryFace::Output,
2196 operator: PauliMap::from_unique_entries([(q, Pauli::X)]),
2197 },
2198 ],
2199 }));
2200 let observable = bloq.add_node(BloqNode::classical(ClassicalNode::observable(8)));
2201 bloq.add_edge(include, observable, BloqEdge::compose(0));
2202
2203 let slices = program_detector_slices(&bloq).unwrap();
2204 let timeline = &slices.per_node[&NodeRef::top_level(quantum)];
2205
2206 assert_eq!(timeline.0.len(), 1);
2207 assert_eq!(timeline.0[0].len(), 1);
2208 assert_eq!(
2209 slices.regions[timeline.0[0][0].region as usize].id,
2210 ProgramRegionId::Observable { index: 8 }
2211 );
2212 assert_eq!(
2213 timeline.0[0][0].terms,
2214 vec![RegionTerm {
2215 qubit: q,
2216 pauli: PauliBasis::X,
2217 }]
2218 );
2219 assert!(
2220 slices.breaks.is_empty(),
2221 "input Z cancels output X propagated backward through H"
2222 );
2223 }
2224
2225 #[test]
2226 fn boundary_seed_on_zero_moment_instance_returns_typed_error() {
2227 let q = ivec2(0, 0);
2228 let mut bloq = Bloq::new();
2229 let template = bloq.add_template(BloqTemplate::new(CoordCircuit::new()));
2230 let mut node = BloqNode::from_members(vec![]);
2231 node.expect_quantum_mut().instances = single_instance(0, template);
2232 bloq.add_node(node);
2233 let include = bloq.add_node(BloqNode::classical(ClassicalNode::Observable {
2234 index: None,
2235 measurements: Vec::new(),
2236 operators: vec![InstanceBoundaryOperator {
2237 instance: TemplateInstanceId(0),
2238 face: BoundaryFace::Output,
2239 operator: PauliMap::from_unique_entries([(q, Pauli::X)]),
2240 }],
2241 }));
2242 let observable = bloq.add_node(BloqNode::classical(ClassicalNode::observable(4)));
2243 bloq.add_edge(include, observable, BloqEdge::compose(0));
2244
2245 assert!(matches!(
2246 program_detector_slices(&bloq),
2247 Err(ProgramSliceError::BreakOutsideTimeline {
2248 region: ProgramRegionId::Observable { index: 4 }
2249 })
2250 ));
2251 }
2252
2253 #[test]
2254 fn region_exported_observable_content_follows_selected_body() {
2255 let q = ivec2(0, 0);
2256 let mut circuit = CoordCircuit::new();
2257 circuit.do_gate(GateType::RZ, [q]).unwrap();
2258 circuit.tick();
2259 circuit.measure(PauliBasis::X, [q]);
2260 circuit.tick();
2261 circuit.do_gate(GateType::Z, [q]).unwrap();
2262
2263 let mut bloq = Bloq::new();
2264 let template = bloq.add_template(BloqTemplate::new(circuit));
2265 let mut body = SubGraph::new();
2266 let mut node = BloqNode::from_members(vec![]);
2267 node.expect_quantum_mut().instances = single_instance(0, template);
2268 let quantum = body.add_node(node);
2269 let accumulate = body.add_node(BloqNode::classical(ClassicalNode::Observable {
2270 index: None,
2271 operators: Vec::new(),
2272 measurements: vec![instance_measurement(0, 0)],
2273 }));
2274 body.add_edge(quantum, accumulate, BloqEdge::Order);
2275 let binding = body.add_node(BloqNode::classical(ClassicalNode::Observable {
2276 index: None,
2277 measurements: Vec::new(),
2278 operators: vec![InstanceBoundaryOperator {
2279 instance: TemplateInstanceId(0),
2280 face: BoundaryFace::Output,
2281 operator: PauliMap::from_unique_entries([(q, Pauli::X)]),
2282 }],
2283 }));
2284 let unrelated = body.add_node(BloqNode::classical(ClassicalNode::Compute {
2285 expr: ClassicalExpr::And(Box::new([
2286 ClassicalExpr::Const(true),
2287 ClassicalExpr::Const(false),
2288 ])),
2289 }));
2290 let consume = body.add_node(BloqNode::classical(ClassicalNode::Compute {
2293 expr: ClassicalExpr::In(0),
2294 }));
2295 body.add_edge(unrelated, consume, BloqEdge::value(0));
2296 body.set_value_output(Some(accumulate.into()));
2297 body.set_boundary_outputs(vec![binding]);
2298
2299 let region = bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
2300 restart_source: None,
2301 restart_condition: ClassicalExpr::Const(false),
2302 body,
2303 }));
2304 let observable = bloq.add_node(BloqNode::classical(ClassicalNode::observable(9)));
2305 bloq.add_edge(region, observable, BloqEdge::compose(0));
2306
2307 let slices = program_detector_slices(&bloq).unwrap();
2308 assert_eq!(slices.skipped_cross_tape, 0);
2309 assert!(slices.breaks.is_empty());
2310
2311 let body_ref = NodeRef {
2312 path: LevelPath::default().child(region, BodySelector::Body),
2313 node: quantum,
2314 };
2315 let timeline = &slices.per_node[&body_ref];
2316 assert_eq!(timeline.0.len(), 3);
2317 assert_eq!(
2318 timeline.0[2],
2319 vec![RegionView {
2320 region: 0,
2321 terms: vec![RegionTerm {
2322 qubit: q,
2323 pauli: PauliBasis::X,
2324 }],
2325 }]
2326 );
2327 }
2328
2329 #[test]
2332 fn repeat_program_yields_distinct_per_iteration_regions() {
2333 let q = ivec2(0, 0);
2334 let mut circuit = CoordCircuit::new();
2335 let seed = circuit.measure(PauliBasis::Z, [q])[0];
2336 let repeated = circuit.reserve_measurement_id(q);
2337 let body = circuit.add_body(CircuitBody::from_ops(vec![
2340 Op::Tick,
2341 Op::Measure {
2342 basis: PauliBasis::Z,
2343 qubits: vec![q],
2344 measurements: vec![repeated],
2345 flip_probability: 0.0,
2346 },
2347 ]));
2348 circuit
2349 .body_mut(circuit.entry_body())
2350 .unwrap()
2351 .ops_mut()
2352 .push(Op::Repeat {
2353 body,
2354 repetitions: 3,
2355 });
2356 let state = LoopStateId(0);
2357 let template = BloqTemplate::with_parts(
2358 circuit,
2359 vec![TemplateDetector {
2360 scope: TemplateDetectorScope::RepeatBody { body },
2361 parity: DetectorParity::from_terms([
2362 DetectorTerm::Measurement(repeated),
2363 DetectorTerm::LoopState(state),
2364 ]),
2365 coords: Some(DetectorCoords::from_slice(&[1.0, 2.0])),
2366 }],
2367 vec![TemplateRepeatState {
2368 body,
2369 state,
2370 initial: DetectorParity::from_measurements([seed]),
2371 next: DetectorParity::from_measurements([repeated]),
2372 }],
2373 Vec::new(),
2374 Vec::new(),
2375 );
2376
2377 let mut bloq = Bloq::new();
2378 let tpl = bloq.add_template(template);
2379 let mut node = BloqNode::from_members(vec![]);
2380 node.expect_quantum_mut().instances = single_instance(0, tpl);
2381 let node_id = bloq.add_node(node);
2382
2383 let slices = program_detector_slices(&bloq).unwrap();
2384 assert_eq!(slices.skipped_cross_tape, 0);
2385 assert!(slices.breaks.is_empty());
2386
2387 let node_slices = &slices.per_node[&NodeRef::top_level(node_id)];
2388 assert_eq!(node_slices.0.len(), 4);
2390 let expected = vec![RegionTerm {
2391 qubit: q,
2392 pauli: PauliBasis::Z,
2393 }];
2394 for moment in 0..3 {
2397 let regions = &node_slices.0[moment];
2398 assert_eq!(regions.len(), 1, "moment {moment}");
2399 assert_eq!(
2400 slices.regions[regions[0].region as usize].id,
2401 ProgramRegionId::Detector {
2402 owner: NodeRef::top_level(node_id),
2403 detector: moment as u32,
2404 }
2405 );
2406 assert_eq!(regions[0].terms, expected);
2407 assert_eq!(
2408 slices.regions[regions[0].region as usize]
2409 .coords
2410 .as_ref()
2411 .map(|coords| coords.as_slice()),
2412 Some(&[1.0, 2.0][..])
2413 );
2414 }
2415 assert!(
2416 node_slices.0[3].is_empty(),
2417 "regions closed after last round"
2418 );
2419 }
2420
2421 #[test]
2425 fn region_body_seam_detector_resolves_across_boundary() {
2426 let q = ivec2(0, 0);
2427 let mut body_circuit = CoordCircuit::new();
2430 body_circuit.do_gate(GateType::Z, [q]).unwrap();
2431 body_circuit.measure(PauliBasis::Z, [q]);
2432
2433 let mut bloq = Bloq::new();
2434 let top_tpl = bloq.add_template(z_measure_template(q));
2435 let body_tpl = bloq.add_template(BloqTemplate::new(body_circuit));
2436
2437 let mut top_node = BloqNode::from_members(vec![]);
2438 top_node.expect_quantum_mut().instances = single_instance(0, top_tpl);
2439
2440 let mut body = SubGraph::new();
2441 let mut body_node = BloqNode::from_members(vec![]);
2442 body_node.expect_quantum_mut().instances = single_instance(1, body_tpl);
2443 body_node.expect_quantum_mut().detectors = vec![crate::NodeDetector {
2445 parity: DetectorParity::from_measurements([
2446 instance_measurement(0, 0),
2447 instance_measurement(1, 0),
2448 ]),
2449 coords: Some(DetectorCoords::from_slice(&[5.0, 6.0])),
2450 }];
2451 let body_node_id = body.add_node(body_node);
2452
2453 let region = BloqNode::region(RegionNode::RepeatUntilSuccess {
2454 restart_source: None,
2455 restart_condition: ClassicalExpr::Const(false),
2456 body,
2457 });
2458 let top_id = bloq.add_node(top_node);
2459 let region_id = bloq.add_node(region);
2460 bloq.add_edge(top_id, region_id, BloqEdge::quantum(vec![]));
2462
2463 let slices = program_detector_slices(&bloq).unwrap();
2464 assert_eq!(slices.skipped_cross_tape, 0, "seam detector now resolves");
2465 assert!(slices.breaks.is_empty());
2466
2467 let top_ref = NodeRef::top_level(top_id);
2468 let body_ref = NodeRef {
2469 path: LevelPath::default().child(region_id, BodySelector::Body),
2470 node: body_node_id,
2471 };
2472 let top_slices = &slices.per_node[&top_ref];
2473 let body_slices = &slices.per_node[&body_ref];
2474 assert_eq!(top_slices.0.len(), 1, "top: one measurement moment");
2475 assert_eq!(body_slices.0.len(), 2, "body: rotation then measurement");
2476
2477 let expected = vec![RegionTerm {
2478 qubit: q,
2479 pauli: PauliBasis::Z,
2480 }];
2481 assert_eq!(top_slices.0[0].len(), 1);
2484 assert_eq!(body_slices.0[0].len(), 1);
2485 assert!(body_slices.0[1].is_empty());
2486
2487 let region_view = &top_slices.0[0][0];
2488 assert_eq!(
2489 slices.regions[region_view.region as usize].id,
2490 ProgramRegionId::Detector {
2491 owner: body_ref,
2492 detector: 0,
2493 },
2494 "body's side table owns it"
2495 );
2496 assert_eq!(
2497 slices.regions[region_view.region as usize]
2498 .coords
2499 .as_ref()
2500 .map(|coords| coords.as_slice()),
2501 Some(&[5.0, 6.0][..])
2502 );
2503 assert_eq!(region_view.terms, expected);
2504 assert_eq!(body_slices.0[0][0].terms, expected);
2505 }
2506}