1use std::cell::RefCell;
49
50use bloq_ir::circuit::{DetectorParity, DetectorTerm, PauliMap};
51#[cfg(test)]
52use bloq_ir::lowering::TemplateDetectorScope;
53use bloq_ir::lowering::{InstanceBoundaryOperator, InstanceMeasurement, TemplateInstanceId};
54use bloq_ir::{
55 Bloq, BloqNodeId, BloqNodeKind, BodySelector, ClassicalNode, LevelPath, NodeDetectorAddress,
56 ObservableOutput, RegionNode, SubGraph,
57};
58use glam::{IVec2, IVec3};
59use rustc_hash::{FxHashMap, FxHashSet};
60
61use super::circuit::ShotRecord;
62use super::prepared::PreparedProgramCore;
63use super::{ExecError, MAX_PHYSICAL_BOUNDARY_BINDINGS};
64use crate::backend::{Pauli, PauliString, Simulator};
65use crate::verify::{FramePairReport, ObservableReport, VerifyReport, detector_report};
66
67pub const DEFAULT_RUS_ATTEMPT_CAP: u32 = 100;
74
75const DETERMINISTIC_EXPECTATION_TOLERANCE: f64 = 1e-9;
79
80struct Program {
82 core: PreparedProgramCore,
83 inputs: Vec<InputPatch>,
84 rus_restarts: FxHashMap<LevelPath, Vec<(LevelPath, ResolvedDetector)>>,
87 early_reads: Vec<super::closure::EarlyOutputRead>,
91 output_cuts: Vec<Vec<(LevelPath, BloqNodeId)>>,
93 early_output_captures: Vec<EarlyOutputCapture>,
96 boundary_scratch: RefCell<PauliString>,
99}
100
101struct EarlyOutputCapture {
102 before: Vec<(LevelPath, BloqNodeId)>,
103 instance: TemplateInstanceId,
104 port: IVec3,
105 operators: Vec<EarlyOutputPaulis>,
106}
107
108struct EarlyOutputPaulis {
109 logical_x: PauliString,
110 logical_z: PauliString,
111 ancilla_x: PauliString,
112 ancilla_z: PauliString,
113}
114
115#[derive(Clone, Copy)]
116enum RunMode {
117 Plain,
118 PrepareInputs,
119 CaptureOutputs,
120}
121
122struct ProgramReportLayout {
124 detectors: Vec<(LevelPath, ResolvedDetector)>,
125 observable_nodes: Vec<BloqNodeId>,
126 outputs: Vec<OutputLogical>,
127 frames: Vec<bloq_ir::FramePair>,
128}
129
130type StagedInputSeeds = FxHashMap<TemplateInstanceId, Vec<(usize, usize)>>;
131
132#[derive(Clone)]
133struct DeferredCcz {
134 seeds: [usize; 3],
135 prepare_before: [Vec<TemplateInstanceId>; 3],
136 prepared: bool,
137}
138
139#[derive(Debug, Clone)]
140struct InputPatch {
141 input_instance: TemplateInstanceId,
142 prepare_before: Vec<TemplateInstanceId>,
143 port: IVec3,
144 data_qubits: Vec<usize>,
145 stabilizers: Vec<PauliString>,
146 logical_x: PauliString,
147 logical_z: PauliString,
148}
149
150impl InputPatch {
151 fn prepare_injection_scaffold(&self, sim: &mut Simulator) -> Result<usize, ExecError> {
152 let mut intersection = None;
153 for &qubit in &self.data_qubits {
154 let on_x = self.logical_x.xbit(qubit);
155 if on_x {
156 sim.reset_x(qubit)?;
157 } else {
158 sim.reset(qubit)?;
159 }
160 if on_x && self.logical_z.zbit(qubit) && intersection.replace(qubit).is_some() {
161 return Err(ExecError::InvalidInjectionPatch { port: self.port });
162 }
163 }
164 intersection.ok_or(ExecError::InvalidInjectionPatch { port: self.port })
165 }
166
167 fn postselect_injection_patch(&self, sim: &mut Simulator) -> Result<(), ExecError> {
168 for stabilizer in &self.stabilizers {
169 sim.postselect_observable(stabilizer, false)?;
170 }
171 Ok(())
172 }
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
178pub struct InputSeed {
179 pub port: IVec3,
181 pub qubit: usize,
183}
184
185#[derive(Debug)]
187pub struct PreparationContext<'a> {
188 pub shot: usize,
190 pub inputs: &'a [InputSeed],
192 deferred_ccz: &'a RefCell<Vec<[IVec3; 3]>>,
193}
194
195impl PreparationContext<'_> {
196 pub fn prepare_ccz(&self, ports: [IVec3; 3]) -> Result<(), ExecError> {
204 if ports[0] == ports[1] || ports[0] == ports[2] || ports[1] == ports[2] {
205 return Err(ExecError::InvalidCczPreparation);
206 }
207 self.deferred_ccz.borrow_mut().push(ports);
208 Ok(())
209 }
210}
211
212struct Shot {
214 sim: Simulator,
215 staged_inputs: StagedInputSeeds,
216 injected_inputs: Vec<bool>,
217 deferred_ccz: Vec<DeferredCcz>,
218 record: ShotRecord,
219 discarded: bool,
220 peak_rank: usize,
231 excluded_instances: rustc_hash::FxHashSet<TemplateInstanceId>,
235 executed_scopes: rustc_hash::FxHashSet<LevelPath>,
238 active_membership_inputs: rustc_hash::FxHashSet<(LevelPath, BloqNodeId, u32)>,
239 branch_choices: Vec<bool>,
241 early_faces: Vec<Option<bool>>,
245 classical_values: FxHashMap<(LevelPath, BloqNodeId), Option<bool>>,
247 saved_outputs: Vec<Option<usize>>,
249 seed: u64,
251}
252
253struct CompletedShot<'a> {
254 state: Shot,
255 top_values: FxHashMap<u32, Option<bool>>,
256 top_bindings: FxHashMap<u32, BoundOperators<'a>>,
257}
258
259enum BoundOperators<'a> {
260 Owned(Vec<&'a InstanceBoundaryOperator>),
262 Recipe(Vec<&'a InstanceBoundaryOperator>, Box<[BloqNodeId]>),
263}
264
265impl BoundOperators<'_> {
266 fn has_output(&self) -> bool {
267 match self {
268 Self::Owned(operators) => !operators.is_empty(),
269 Self::Recipe(operators, children) => !operators.is_empty() || !children.is_empty(),
270 }
271 }
272}
273
274fn collect_bound_operators<'a>(
275 bindings: &FxHashMap<u32, BoundOperators<'a>>,
276 producer: BloqNodeId,
277 out: &mut Vec<&'a InstanceBoundaryOperator>,
278 expanded: &mut usize,
279) -> Result<(), ExecError> {
280 let mut stack = vec![(producer, false, false)];
281 let mut active = FxHashSet::default();
282 while let Some((producer, exit, nested_recipe)) = stack.pop() {
283 if exit {
284 active.remove(&producer);
285 continue;
286 }
287 if nested_recipe {
288 *expanded = expanded.saturating_add(1);
289 if *expanded > MAX_PHYSICAL_BOUNDARY_BINDINGS {
290 return Err(ExecError::BoundaryBindingExpansionLimit {
291 limit: MAX_PHYSICAL_BOUNDARY_BINDINGS,
292 });
293 }
294 }
295 if !active.insert(producer) {
296 return Err(ExecError::MalformedGraph("cyclic binding recipe"));
297 }
298 match bindings.get(&producer.0) {
299 Some(BoundOperators::Owned(operators)) => {
300 if nested_recipe {
301 *expanded = expanded.saturating_add(operators.len());
302 if *expanded > MAX_PHYSICAL_BOUNDARY_BINDINGS {
303 return Err(ExecError::BoundaryBindingExpansionLimit {
304 limit: MAX_PHYSICAL_BOUNDARY_BINDINGS,
305 });
306 }
307 }
308 out.extend(operators);
309 active.remove(&producer);
310 }
311 Some(BoundOperators::Recipe(operators, children)) => {
312 *expanded = expanded.saturating_add(operators.len());
313 if *expanded > MAX_PHYSICAL_BOUNDARY_BINDINGS {
314 return Err(ExecError::BoundaryBindingExpansionLimit {
315 limit: MAX_PHYSICAL_BOUNDARY_BINDINGS,
316 });
317 }
318 out.extend(operators);
319 stack.push((producer, true, nested_recipe));
320 stack.extend(children.iter().rev().map(|child| (*child, false, true)));
321 }
322 None => {
323 active.remove(&producer);
324 }
325 }
326 }
327 Ok(())
328}
329
330struct ShotSnapshot {
333 sim: Simulator,
334 injected_inputs: Vec<bool>,
335 deferred_ccz: Vec<DeferredCcz>,
336 record: ShotRecord,
337 discarded: bool,
338 excluded_instances: rustc_hash::FxHashSet<TemplateInstanceId>,
339 executed_scopes: rustc_hash::FxHashSet<LevelPath>,
340 active_membership_inputs: rustc_hash::FxHashSet<(LevelPath, BloqNodeId, u32)>,
341 branch_choices: Vec<bool>,
342 early_faces: Vec<Option<bool>>,
343 classical_values: FxHashMap<(LevelPath, BloqNodeId), Option<bool>>,
345 saved_outputs: Vec<Option<usize>>,
346}
347
348impl ShotSnapshot {
349 fn capture(state: &Shot) -> Self {
350 Self {
351 sim: state.sim.clone(),
352 injected_inputs: state.injected_inputs.clone(),
353 deferred_ccz: state.deferred_ccz.clone(),
354 record: state.record.clone(),
355 discarded: state.discarded,
356 excluded_instances: state.excluded_instances.clone(),
357 executed_scopes: state.executed_scopes.clone(),
358 active_membership_inputs: state.active_membership_inputs.clone(),
359 branch_choices: state.branch_choices.clone(),
360 early_faces: state.early_faces.clone(),
361 classical_values: state.classical_values.clone(),
362 saved_outputs: state.saved_outputs.clone(),
363 }
364 }
365
366 fn restore(&self, state: &mut Shot) {
367 state.sim.clone_from(&self.sim);
368 state.injected_inputs.clone_from(&self.injected_inputs);
369 state.deferred_ccz.clone_from(&self.deferred_ccz);
370 state.record.clone_from(&self.record);
371 state.discarded = self.discarded;
372 state
373 .excluded_instances
374 .clone_from(&self.excluded_instances);
375 state.executed_scopes.clone_from(&self.executed_scopes);
376 state
377 .active_membership_inputs
378 .clone_from(&self.active_membership_inputs);
379 state.branch_choices.clone_from(&self.branch_choices);
380 state.early_faces.clone_from(&self.early_faces);
381 state.classical_values.clone_from(&self.classical_values);
382 state.saved_outputs.clone_from(&self.saved_outputs);
383 }
384}
385
386#[derive(Debug, Clone)]
395pub struct OutputLogical {
396 pub port: IVec3,
398 pub logical_x: PauliString,
400 pub logical_z: PauliString,
402 pub consumed: bool,
406}
407
408#[derive(Debug, Clone, Copy, PartialEq, Eq)]
410pub struct ShotFrame {
411 pub port: IVec3,
413 pub x: Option<bool>,
415 pub z: Option<bool>,
417}
418
419#[derive(Debug, Clone, Copy, PartialEq, Eq)]
422pub struct SavedOutput {
423 pub port: IVec3,
425 pub qubit: usize,
427}
428
429#[derive(Debug)]
437pub struct ShotContext<'a> {
438 pub shot: usize,
440 pub record: &'a ShotRecord,
442 pub branch_selectors: &'a [bool],
445 pub named_branch_selectors: &'a [(&'a str, bool)],
447 pub outputs: &'a [OutputLogical],
449 pub frames: &'a [ShotFrame],
451 pub saved_outputs: &'a [SavedOutput],
453 pub qubit_count: usize,
456}
457
458pub fn run_bloq(bloq: &Bloq, shots: usize, seed: u64) -> Result<VerifyReport, ExecError> {
467 run_bloq_internal(
468 bloq,
469 shots,
470 seed,
471 RunMode::Plain,
472 &mut |_, _| Ok(()),
473 &mut |_, _| Ok(()),
474 )
475}
476
477pub fn run_bloq_with_hook<F>(
493 bloq: &Bloq,
494 shots: usize,
495 seed: u64,
496 mut hook: F,
497) -> Result<VerifyReport, ExecError>
498where
499 F: FnMut(&mut Simulator, &ShotContext<'_>) -> Result<(), ExecError>,
500{
501 run_bloq_internal(
502 bloq,
503 shots,
504 seed,
505 RunMode::Plain,
506 &mut |_, _| Ok(()),
507 &mut hook,
508 )
509}
510
511pub fn run_bloq_with_io<P, F>(
521 bloq: &Bloq,
522 shots: usize,
523 seed: u64,
524 mut prepare: P,
525 mut peek: F,
526) -> Result<VerifyReport, ExecError>
527where
528 P: FnMut(&mut Simulator, &PreparationContext<'_>) -> Result<(), ExecError>,
529 F: FnMut(&mut Simulator, &ShotContext<'_>) -> Result<(), ExecError>,
530{
531 run_bloq_internal(
532 bloq,
533 shots,
534 seed,
535 RunMode::PrepareInputs,
536 &mut prepare,
537 &mut peek,
538 )
539}
540
541pub fn run_bloq_with_captured_outputs<F>(
553 bloq: &Bloq,
554 shots: usize,
555 seed: u64,
556 mut hook: F,
557) -> Result<VerifyReport, ExecError>
558where
559 F: FnMut(&mut Simulator, &ShotContext<'_>) -> Result<(), ExecError>,
560{
561 run_bloq_internal(
562 bloq,
563 shots,
564 seed,
565 RunMode::CaptureOutputs,
566 &mut |_, _| Ok(()),
567 &mut hook,
568 )
569}
570
571fn run_bloq_internal<P, F>(
575 bloq: &Bloq,
576 shots: usize,
577 seed: u64,
578 mode: RunMode,
579 prepare: &mut P,
580 hook: &mut F,
581) -> Result<VerifyReport, ExecError>
582where
583 P: FnMut(&mut Simulator, &PreparationContext<'_>) -> Result<(), ExecError>,
584 F: FnMut(&mut Simulator, &ShotContext<'_>) -> Result<(), ExecError>,
585{
586 let program = Program::prepare(bloq, mode)?;
587 let layout = program.report_layout()?;
588
589 let mut detector_shots: Vec<Vec<bool>> = vec![Vec::new(); layout.detectors.len()];
590 let mut observable_shots: Vec<Vec<bool>> = vec![Vec::new(); layout.observable_nodes.len()];
591 let mut frame_x: Vec<Vec<Option<bool>>> = vec![Vec::new(); layout.frames.len()];
592 let mut frame_z: Vec<Vec<Option<bool>>> = vec![Vec::new(); layout.frames.len()];
593 let mut discarded = 0usize;
594 let mut max_rank = 0usize;
595 let mut branch_selectors: Vec<bool> = Vec::new();
596 for shot in 0..shots {
597 let mut completed =
598 program.execute_shot(shot, seed, matches!(mode, RunMode::PrepareInputs), prepare)?;
599 max_rank = max_rank.max(completed.state.peak_rank);
600 branch_selectors.extend(completed.state.branch_choices.iter().copied());
601 if completed.state.discarded {
602 discarded += 1;
603 continue;
604 }
605 if program.core.bloq.logical_outputs().iter().any(|output| {
606 completed
607 .state
608 .excluded_instances
609 .contains(&output.instance)
610 }) {
611 return Err(ExecError::MalformedGraph(
612 "logical output owner is not selected",
613 ));
614 }
615
616 {
621 let shot_frames = layout
622 .frames
623 .iter()
624 .map(|frame| ShotFrame {
625 port: frame.port,
626 x: completed.top_values.get(&frame.x.0).copied().flatten(),
627 z: completed.top_values.get(&frame.z.0).copied().flatten(),
628 })
629 .collect::<Vec<_>>();
630 let saved_outputs = program
631 .early_output_captures
632 .iter()
633 .zip(&completed.state.saved_outputs)
634 .filter_map(|(capture, &qubit)| {
635 qubit.map(|qubit| SavedOutput {
636 port: capture.port,
637 qubit,
638 })
639 })
640 .collect::<Vec<_>>();
641 let named_branch_selectors = program
642 .core
643 .bloq
644 .nodes()
645 .filter_map(|(id, node)| {
646 let bloq_ir::NodeProvenance::BranchSelector { name } = &node.provenance else {
647 return None;
648 };
649 let value = completed
650 .state
651 .classical_values
652 .get(&(LevelPath::default(), id))
653 .copied()
654 .flatten()?;
655 Some((name.as_str(), value))
656 })
657 .collect::<Vec<_>>();
658 let ctx = ShotContext {
659 shot,
660 record: &completed.state.record,
661 branch_selectors: &completed.state.branch_choices,
662 named_branch_selectors: &named_branch_selectors,
663 outputs: &layout.outputs,
664 frames: &shot_frames,
665 saved_outputs: &saved_outputs,
666 qubit_count: completed.state.sim.num_qubits(),
667 };
668 hook(&mut completed.state.sim, &ctx)?;
669 }
670
671 for ((scope, parity), out) in layout.detectors.iter().zip(&mut detector_shots) {
674 if !parity.applies(scope, &completed.state) {
675 continue;
676 }
677 let value = xor_records(parity, scope, &completed.state)?;
678 out.push(value);
679 }
680 for (node_id, out) in layout.observable_nodes.iter().zip(&mut observable_shots) {
681 if let Some(value) = program.observable_value(
682 *node_id,
683 &completed.top_values,
684 &completed.top_bindings,
685 &mut completed.state,
686 )? {
687 out.push(value);
688 }
689 }
690 for (i, frame) in layout.frames.iter().enumerate() {
691 frame_x[i].push(completed.top_values.get(&frame.x.0).copied().flatten());
692 frame_z[i].push(completed.top_values.get(&frame.z.0).copied().flatten());
693 }
694 }
695
696 Ok(VerifyReport {
697 shots,
698 discarded,
699 detectors: detector_shots.into_iter().map(detector_report).collect(),
700 observables: observable_shots
701 .into_iter()
702 .map(|per_shot| ObservableReport { per_shot })
703 .collect(),
704 frame_pairs: layout
705 .frames
706 .iter()
707 .zip(frame_x)
708 .zip(frame_z)
709 .map(|((frame, x_bits), z_bits)| FramePairReport {
710 port: frame.port,
711 x_bits,
712 z_bits,
713 })
714 .collect(),
715 max_rank,
716 branch_selectors,
717 })
718}
719
720impl Program {
721 fn prepare(bloq: &Bloq, mode: RunMode) -> Result<Self, ExecError> {
722 let core = PreparedProgramCore::build(bloq)?;
723 let rus_restarts = collect_rus_restarts(&core.bloq, &core.global_meas)?;
724 let early_reads = super::closure::plan_early_output_reads(&core.bloq)?;
725 let output_cuts = super::closure::logical_output_cuts(&core.bloq)?;
726 let mut program = Self {
727 boundary_scratch: RefCell::new(PauliString::new(core.coord_index.len())),
728 core,
729 inputs: Vec::new(),
730 rus_restarts,
731 early_reads,
732 output_cuts,
733 early_output_captures: Vec::new(),
734 };
735 if matches!(mode, RunMode::PrepareInputs) {
736 program.inputs = program.input_patches()?;
737 }
738 if matches!(mode, RunMode::CaptureOutputs) {
739 program.early_output_captures = program.plan_early_output_captures()?;
740 }
741 if program.early_output_captures.is_empty() {
742 program.validate_early_read_compatibility()?;
743 }
744 Ok(program)
745 }
746
747 fn validate_early_read_compatibility(&self) -> Result<(), ExecError> {
748 let operators = self
749 .early_reads
750 .iter()
751 .map(|read| {
752 let mut product = PauliString::new(self.core.coord_index.len());
753 for binding in &read.bindings {
754 product = &product
755 * self
756 .core
757 .boundary_operator(&binding.operator(&self.core.bloq).operator)?;
758 }
759 Ok(product)
760 })
761 .collect::<Result<Vec<_>, ExecError>>()?;
762 for (index, first) in self.early_reads.iter().enumerate() {
763 for (other, second) in self.early_reads[..index].iter().enumerate() {
764 if first.before == second.before
765 && (first.incomplete || second.incomplete)
766 && !operators[index].commutes_with(&operators[other])
767 {
768 return Err(ExecError::IncompatibleEarlyOutputReads {
769 first: first.observable.0,
770 second: second.observable.0,
771 before: first.before.1.0,
772 });
773 }
774 }
775 }
776 Ok(())
777 }
778
779 fn report_layout(&self) -> Result<ProgramReportLayout, ExecError> {
780 let mut detectors = collect_detectors(
781 &self.core.bloq,
782 self.core.bloq.top(),
783 &self.core.global_meas,
784 )?;
785 detectors.retain(|(_, detector)| {
787 !self.inputs.iter().any(|input| {
788 !input.prepare_before.contains(&input.input_instance)
789 && detector
790 .terms
791 .iter()
792 .any(|(instance, _)| *instance == input.input_instance)
793 && detector
794 .terms
795 .iter()
796 .any(|(instance, _)| input.prepare_before.contains(instance))
797 })
798 });
799 let observable_nodes = collect_observables(&self.core.bloq);
800 super::closure::guard_decode_observable_closure(&self.core.bloq)?;
801 let outputs = self.output_logicals()?;
802 let frames = self.core.bloq.output_frames();
803 for output in self.core.bloq.logical_outputs() {
804 if !frames.iter().any(|frame| frame.port == output.port) {
805 return Err(ExecError::MalformedGraph(
806 "logical output has no complete frame pair",
807 ));
808 }
809 }
810 Ok(ProgramReportLayout {
811 detectors,
812 observable_nodes,
813 outputs,
814 frames,
815 })
816 }
817
818 fn execute_shot<P>(
819 &self,
820 shot: usize,
821 seed: u64,
822 stage_inputs: bool,
823 prepare: &mut P,
824 ) -> Result<CompletedShot<'_>, ExecError>
825 where
826 P: FnMut(&mut Simulator, &PreparationContext<'_>) -> Result<(), ExecError>,
827 {
828 let shot_seed = seed ^ shot as u64;
829 let base_qubits = self.core.coord_index.len();
830 let mut state = Shot {
831 sim: Simulator::with_seed(base_qubits, shot_seed),
832 staged_inputs: StagedInputSeeds::default(),
833 injected_inputs: vec![false; self.inputs.len()],
834 deferred_ccz: Vec::new(),
835 record: ShotRecord::with_len(self.core.record_count),
836 discarded: false,
837 peak_rank: 1,
838 excluded_instances: rustc_hash::FxHashSet::default(),
839 executed_scopes: rustc_hash::FxHashSet::default(),
840 active_membership_inputs: rustc_hash::FxHashSet::default(),
841 branch_choices: Vec::new(),
842 early_faces: vec![None; self.early_reads.len()],
843 classical_values: FxHashMap::default(),
844 saved_outputs: vec![None; self.early_output_captures.len()],
845 seed: shot_seed,
846 };
847 if stage_inputs {
848 let mut seeds = Vec::with_capacity(self.inputs.len());
849 for input in &self.inputs {
850 let qubit = state.sim.num_qubits();
851 state.sim.reset_x(qubit)?;
852 seeds.push(InputSeed {
853 port: input.port,
854 qubit,
855 });
856 }
857 let deferred_ccz = RefCell::new(Vec::new());
858 prepare(
859 &mut state.sim,
860 &PreparationContext {
861 shot,
862 inputs: &seeds,
863 deferred_ccz: &deferred_ccz,
864 },
865 )?;
866 for ports in deferred_ccz.into_inner() {
867 let mut prepared = DeferredCcz {
868 seeds: [0; 3],
869 prepare_before: std::array::from_fn(|_| Vec::new()),
870 prepared: false,
871 };
872 for (index, port) in ports.into_iter().enumerate() {
873 let input_index = self
874 .inputs
875 .iter()
876 .position(|input| input.port == port)
877 .ok_or(ExecError::UnknownPreparationInput { port })?;
878 prepared.seeds[index] = seeds[input_index].qubit;
879 prepared.prepare_before[index] =
880 self.inputs[input_index].prepare_before.clone();
881 }
882 state.deferred_ccz.push(prepared);
883 }
884 for (index, (input, seed)) in self.inputs.iter().zip(seeds).enumerate() {
885 for &target in &input.prepare_before {
886 state
887 .staged_inputs
888 .entry(target)
889 .or_default()
890 .push((index, seed.qubit));
891 }
892 }
893 }
894 state.peak_rank = state.peak_rank.max(state.sim.rank());
895 let mut top_values = FxHashMap::default();
896 let mut top_bindings = FxHashMap::default();
897 self.execute_level(
898 self.core.bloq.top(),
899 &LevelPath::default(),
900 &mut state,
901 &mut top_values,
902 &mut top_bindings,
903 )?;
904 Ok(CompletedShot {
905 state,
906 top_values,
907 top_bindings,
908 })
909 }
910
911 fn restart_signaled(&self, scope: &LevelPath, state: &Shot) -> Result<bool, ExecError> {
912 for (owner, parity) in self.rus_restarts.get(scope).into_iter().flatten() {
913 if parity.applies(owner, state) && xor_records(parity, owner, state)? {
914 return Ok(true);
915 }
916 }
917 Ok(false)
918 }
919
920 fn execute_level<'a>(
924 &'a self,
925 level: &'a SubGraph,
926 scope: &LevelPath,
927 state: &mut Shot,
928 values: &mut FxHashMap<u32, Option<bool>>,
929 bindings: &mut FxHashMap<u32, BoundOperators<'a>>,
930 ) -> Result<(), ExecError> {
931 state.executed_scopes.insert(scope.clone());
932 for &id in self.core.emit_order(scope)? {
933 let node = level.node(id).expect("emit order yields real ids");
934 if let Some(slot) = node.activation {
935 let inputs = self.slot_inputs(level, id, values);
936 let active = inputs.get(&slot).copied().ok_or(ExecError::MalformedGraph(
937 "classical activation input is unavailable",
938 ))?;
939 if !active {
940 if let Some(region) = node.try_region() {
941 for (_, body) in region.bodies() {
942 collect_instances(body, &mut state.excluded_instances);
943 }
944 } else if let Some(quantum) = node.try_quantum() {
945 state
946 .excluded_instances
947 .extend(quantum.instances.iter().map(|instance| instance.id));
948 }
949 values.insert(id.0, Some(false));
950 state
951 .classical_values
952 .insert((scope.clone(), id), Some(false));
953 continue;
954 }
955 }
956 if !self.early_output_captures.is_empty() {
957 self.capture_early_outputs(scope, id, state)?;
958 } else if !self.early_reads.is_empty() {
959 self.read_early_faces(scope, id, state)?;
960 }
961 match (node.try_classical(), &node.kind) {
962 (_, BloqNodeKind::Quantum(quantum)) => {
963 if quantum.guards.is_empty() {
964 self.inject_input_seeds(quantum, state)?;
965 self.run_quantum_node(id, scope, state)?;
966 } else {
967 let inputs = self.slot_inputs(level, id, values);
968 let selected = self.core.selected_ops(scope, id, node, &inputs)?;
969 for guard in &quantum.guards {
970 if inputs[&guard.input] {
971 state.active_membership_inputs.insert((
972 scope.clone(),
973 id,
974 guard.input,
975 ));
976 } else {
977 state.excluded_instances.extend(&guard.instances);
978 }
979 }
980 state.record.select_aliases(&selected.aliases);
981 self.inject_input_seeds(&selected.quantum, state)?;
982 let rank = selected.ops.run(&mut state.sim, &mut state.record)?;
983 state.peak_rank = state.peak_rank.max(rank);
984 }
985 }
986 (Some(ClassicalNode::Discard { condition }), _) => {
987 let inputs = self.slot_inputs(level, id, values);
988 state.discarded = condition
989 .eval(&mut |slot| inputs.get(&slot).copied())
990 .ok_or(ExecError::MalformedGraph(
991 "discard condition is unavailable",
992 ))?;
993 }
994 (_, BloqNodeKind::Classical(classical)) => {
995 let classical = classical.as_ref();
996 let value = self.classical_value(
998 level,
999 id,
1000 classical,
1001 values,
1002 &state.record,
1003 &state.excluded_instances,
1004 )?;
1005 values.insert(id.0, value);
1006 if matches!(
1007 node.provenance,
1008 bloq_ir::NodeProvenance::BranchSelector { .. }
1009 ) {
1010 state
1011 .branch_choices
1012 .push(value.ok_or(ExecError::MalformedGraph(
1013 "branch selector has no value",
1014 ))?);
1015 state.classical_values.insert((scope.clone(), id), value);
1016 }
1017 if let ClassicalNode::Observable { operators, .. } = classical {
1018 let children = level
1019 .data_inputs(id)
1020 .filter(|input| input.output.is_none())
1021 .map(|input| input.producer)
1022 .filter(|child| {
1023 bindings
1024 .get(&child.0)
1025 .is_some_and(BoundOperators::has_output)
1026 })
1027 .collect();
1028 bindings.insert(
1029 id.0,
1030 BoundOperators::Recipe(
1031 operators
1032 .iter()
1033 .filter(|operator| {
1034 operator.face == bloq_ir::BoundaryFace::Output
1035 })
1036 .collect(),
1037 children,
1038 ),
1039 );
1040 }
1041 }
1042 (_, BloqNodeKind::Region(region)) => {
1043 self.execute_region(level, id, region, scope, state, values, bindings)?
1044 }
1045 }
1046 if !self.early_reads.is_empty()
1047 && let Some(&value) = values.get(&id.0)
1048 {
1049 state.classical_values.insert((scope.clone(), id), value);
1050 }
1051 if state.discarded {
1052 break;
1054 }
1055 }
1056 Ok(())
1057 }
1058
1059 #[expect(
1063 clippy::too_many_arguments,
1064 reason = "recursive execution threads one region-walk state"
1065 )]
1066 fn execute_region<'a>(
1067 &'a self,
1068 level: &'a SubGraph,
1069 id: BloqNodeId,
1070 region: &'a RegionNode,
1071 scope: &LevelPath,
1072 state: &mut Shot,
1073 values: &mut FxHashMap<u32, Option<bool>>,
1074 bindings: &mut FxHashMap<u32, BoundOperators<'a>>,
1075 ) -> Result<(), ExecError> {
1076 let RegionNode::RepeatUntilSuccess {
1077 body,
1078 restart_condition,
1079 restart_source,
1080 } = region;
1081 let child = scope.child(id, BodySelector::Body);
1082 let snapshot = ShotSnapshot::capture(state);
1083 let mut attempt = 0;
1084 loop {
1085 if attempt != 0 {
1086 snapshot.restore(state);
1087 }
1088 state
1089 .sim
1090 .reseed_rng(rus_attempt_seed(state.seed, &child, attempt));
1091
1092 let mut body_values = FxHashMap::default();
1093 let mut body_bindings = FxHashMap::default();
1094 self.execute_level(body, &child, state, &mut body_values, &mut body_bindings)?;
1095 let restart_detector = self.restart_signaled(&child, state)?;
1099 let open = restart_source.and_then(|source| {
1100 zero_decoder_output(
1101 body_values.get(&source.node.0).copied().flatten(),
1102 Some(source.output),
1103 )
1104 });
1105 let restart_predicate = restart_condition
1106 .eval(&mut |slot| {
1107 let input = level.value_inputs(id).find(|input| input.slot == slot);
1108 match input {
1109 Some(input) => zero_decoder_output(
1110 values.get(&input.producer.0).copied().flatten(),
1111 input.output,
1112 ),
1113 None => open,
1114 }
1115 })
1116 .ok_or(ExecError::MalformedGraph(
1117 "RUS restart condition is not evaluable",
1118 ))?;
1119
1120 if !(restart_detector || restart_predicate) {
1121 state.sim.restore_rng_from(&snapshot.sim);
1123 values.insert(id.0, bound_body_value(body, &body_values));
1124 bindings.insert(
1125 id.0,
1126 BoundOperators::Owned(bound_body_bindings(body, &body_bindings)?),
1127 );
1128 break;
1129 }
1130 attempt += 1;
1131 if attempt >= DEFAULT_RUS_ATTEMPT_CAP {
1132 state.discarded = true;
1133 break;
1134 }
1135 }
1136 Ok(())
1137 }
1138
1139 fn inject_input_seeds(
1140 &self,
1141 quantum: &bloq_ir::QuantumNode,
1142 state: &mut Shot,
1143 ) -> Result<(), ExecError> {
1144 for preparation in &mut state.deferred_ccz {
1145 if !preparation.prepared
1146 && preparation
1147 .prepare_before
1148 .iter()
1149 .flatten()
1150 .any(|cut| quantum.instances.iter().any(|instance| instance.id == *cut))
1151 {
1152 let [a, b, c] = preparation.seeds;
1153 state.sim.ccz(a, b, c)?;
1154 preparation.prepared = true;
1155 }
1156 }
1157 for instance in &quantum.instances {
1158 for &(input_index, seed) in state.staged_inputs.get(&instance.id).into_iter().flatten()
1159 {
1160 if state.injected_inputs[input_index] {
1161 continue;
1162 }
1163 let input = &self.inputs[input_index];
1164 let corner = input.prepare_injection_scaffold(&mut state.sim)?;
1165 state.sim.swap(seed, corner);
1166 input.postselect_injection_patch(&mut state.sim)?;
1167 state.injected_inputs[input_index] = true;
1168 }
1169 }
1170 Ok(())
1171 }
1172
1173 fn run_quantum_node(
1175 &self,
1176 id: BloqNodeId,
1177 scope: &LevelPath,
1178 state: &mut Shot,
1179 ) -> Result<(), ExecError> {
1180 let max_rank = self
1181 .core
1182 .prepared_ops(scope, id)?
1183 .run(&mut state.sim, &mut state.record)?;
1184 state.peak_rank = state.peak_rank.max(max_rank);
1185 Ok(())
1186 }
1187
1188 fn classical_value(
1190 &self,
1191 level: &SubGraph,
1192 id: BloqNodeId,
1193 classical: &ClassicalNode,
1194 values: &FxHashMap<u32, Option<bool>>,
1195 record: &ShotRecord,
1196 excluded: &rustc_hash::FxHashSet<TemplateInstanceId>,
1197 ) -> Result<Option<bool>, ExecError> {
1198 let mut record_value = false;
1199 for im in classical.measurements() {
1200 if excluded.contains(&im.instance) {
1201 continue;
1202 }
1203 let gid = *self
1204 .core
1205 .global_meas
1206 .get(im)
1207 .ok_or(ExecError::MalformedGraph(
1208 "observable names an unknown measurement",
1209 ))?;
1210 record_value ^= record.require(gid)?;
1211 }
1212 match classical {
1213 ClassicalNode::Compute { expr } => {
1214 let inputs = self.slot_inputs(level, id, values);
1215 Ok(expr.eval(&mut |slot| inputs.get(&slot).copied()))
1216 }
1217 ClassicalNode::Observable { .. } => {
1221 let mut value = record_value;
1222 for input in level.data_inputs(id) {
1223 match zero_decoder_output(
1224 values.get(&input.producer.0).copied().flatten(),
1225 input.output,
1226 ) {
1227 Some(bit) => value ^= bit,
1228 None => return Ok(None),
1229 }
1230 }
1231 Ok(Some(value))
1232 }
1233 ClassicalNode::Discard { .. } => Ok(None),
1234 }
1235 }
1236
1237 fn slot_inputs(
1240 &self,
1241 level: &SubGraph,
1242 id: BloqNodeId,
1243 values: &FxHashMap<u32, Option<bool>>,
1244 ) -> FxHashMap<u32, bool> {
1245 let mut inputs = FxHashMap::default();
1246 for input in level.value_inputs(id) {
1247 if let Some(bit) = zero_decoder_output(
1248 values.get(&input.producer.0).copied().flatten(),
1249 input.output,
1250 ) {
1251 inputs.insert(input.slot, bit);
1252 }
1253 }
1254 inputs
1255 }
1256
1257 fn observable_value(
1273 &self,
1274 obs: BloqNodeId,
1275 top_values: &FxHashMap<u32, Option<bool>>,
1276 top_bindings: &FxHashMap<u32, BoundOperators<'_>>,
1277 state: &mut Shot,
1278 ) -> Result<Option<bool>, ExecError> {
1279 let guard = self.core.bloq.top()[obs].activation;
1280 if let Some(slot) = guard {
1281 let inputs = self.slot_inputs(self.core.bloq.top(), obs, top_values);
1282 if inputs.get(&slot) != Some(&true) {
1283 return Ok(None);
1284 }
1285 }
1286 let Some(mut value) = top_values.get(&obs.0).copied().flatten() else {
1287 return Ok(None);
1288 };
1289 let mut terminal_operator: Option<&PauliString> = None;
1290 let mut combined = false;
1291 let mut boundary_scratch = self.boundary_scratch.borrow_mut();
1292 let mut output_operators = self.core.bloq.top()[obs]
1293 .try_classical()
1294 .expect("observable is classical")
1295 .operators()
1296 .iter()
1297 .filter(|operator| operator.face == bloq_ir::BoundaryFace::Output)
1298 .collect::<Vec<_>>();
1299 let mut expanded = 0;
1300 for input in self
1301 .core
1302 .bloq
1303 .top()
1304 .data_inputs(obs)
1305 .filter(|input| input.output.is_none())
1306 {
1307 collect_bound_operators(
1308 top_bindings,
1309 input.producer,
1310 &mut output_operators,
1311 &mut expanded,
1312 )?;
1313 }
1314 let mut cached_bindings = Vec::new();
1315 for (read, bit) in self.early_reads.iter().zip(&state.early_faces) {
1316 if let Some(bit) = bit
1317 && read.observable == obs
1318 && read.bindings.iter().all(|binding| {
1319 output_operators
1320 .iter()
1321 .any(|operator| std::ptr::eq(binding.operator(&self.core.bloq), *operator))
1322 })
1323 {
1324 value ^= bit;
1325 cached_bindings.extend(&read.bindings);
1326 }
1327 }
1328 for operator in output_operators {
1329 if !cached_bindings
1330 .iter()
1331 .any(|binding| std::ptr::eq(binding.operator(&self.core.bloq), operator))
1332 {
1333 let prepared = self.core.boundary_operator(&operator.operator)?;
1334 if let Some(first) = terminal_operator {
1335 if !combined {
1336 boundary_scratch.clone_from(first);
1337 combined = true;
1338 }
1339 *boundary_scratch = &*boundary_scratch * prepared;
1341 } else {
1342 terminal_operator = Some(prepared);
1343 }
1344 }
1345 }
1346 if let Some(first) = terminal_operator {
1347 let pauli = if combined { &boundary_scratch } else { first };
1348 value ^= Self::measure_boundary_pauli(pauli, &mut state.sim)?;
1349 }
1350 Ok(Some(value))
1351 }
1352
1353 fn plan_early_output_captures(&self) -> Result<Vec<EarlyOutputCapture>, ExecError> {
1354 let captures = self
1355 .core
1356 .bloq
1357 .logical_outputs()
1358 .iter()
1359 .zip(&self.output_cuts)
1360 .filter(|(_, cuts)| !cuts.is_empty())
1361 .map(|(output, cuts)| {
1362 Ok((
1363 cuts.clone(),
1364 output.instance,
1365 output.port,
1366 self.joint_pauli(&output.x)?,
1367 self.joint_pauli(&output.z)?,
1368 ))
1369 })
1370 .collect::<Result<Vec<_>, ExecError>>()?;
1371 let base_width = self.core.coord_index.len();
1372 let variant_count = captures.len();
1373 Ok(captures
1376 .into_iter()
1377 .map(
1378 |(before, instance, port, logical_x, logical_z)| EarlyOutputCapture {
1379 before,
1380 instance,
1381 port,
1382 operators: (0..variant_count)
1383 .map(|extra| {
1384 let qubit = base_width + extra;
1385 let width = qubit + 1;
1386 EarlyOutputPaulis {
1387 logical_x: super::circuit::widen_pauli(&logical_x, width),
1388 logical_z: super::circuit::widen_pauli(&logical_z, width),
1389 ancilla_x: PauliString::single(width, qubit, Pauli::X),
1390 ancilla_z: PauliString::single(width, qubit, Pauli::Z),
1391 }
1392 })
1393 .collect(),
1394 },
1395 )
1396 .collect())
1397 }
1398
1399 fn capture_early_outputs(
1402 &self,
1403 scope: &LevelPath,
1404 id: BloqNodeId,
1405 state: &mut Shot,
1406 ) -> Result<(), ExecError> {
1407 for (slot, capture) in self.early_output_captures.iter().enumerate() {
1408 if !capture
1409 .before
1410 .iter()
1411 .any(|(cut_scope, cut_id)| *cut_id == id && cut_scope == scope)
1412 || state.saved_outputs[slot].is_some()
1413 || state.excluded_instances.contains(&capture.instance)
1414 {
1415 continue;
1416 }
1417 let qubit = state.sim.num_qubits();
1418 let operators = capture
1419 .operators
1420 .get(qubit.saturating_sub(self.core.coord_index.len()))
1421 .ok_or(ExecError::MalformedGraph(
1422 "early-output capture has no prepared width",
1423 ))?;
1424 state.sim.reset(qubit)?;
1425 state
1426 .sim
1427 .controlled_pauli(&operators.logical_z, &operators.ancilla_x)?;
1428 state
1429 .sim
1430 .controlled_pauli(&operators.ancilla_z, &operators.logical_x)?;
1431 state
1432 .sim
1433 .controlled_pauli(&operators.logical_z, &operators.ancilla_x)?;
1434 state.saved_outputs[slot] = Some(qubit);
1435 }
1436 Ok(())
1437 }
1438
1439 fn available_value(
1442 &self,
1443 scope: &LevelPath,
1444 id: BloqNodeId,
1445 state: &Shot,
1446 memo: &mut FxHashMap<(LevelPath, BloqNodeId), Option<bool>>,
1447 ) -> Result<Option<bool>, ExecError> {
1448 let key = (scope.clone(), id);
1449 if let Some(&value) = state.classical_values.get(&key).or_else(|| memo.get(&key)) {
1450 return Ok(value);
1451 }
1452 let level = self
1453 .core
1454 .bloq
1455 .level_at(scope)
1456 .expect("binding scope exists");
1457 let Some(classical) = level[id].try_classical() else {
1458 return Ok(None);
1459 };
1460 if let Some(slot) = level[id].activation {
1461 let input = level
1462 .value_inputs(id)
1463 .find(|input| input.slot == slot)
1464 .ok_or(ExecError::MalformedGraph(
1465 "classical guard has no value input",
1466 ))?;
1467 match zero_decoder_output(
1468 self.available_value(scope, input.producer, state, memo)?,
1469 input.output,
1470 ) {
1471 Some(true) => {}
1472 Some(false) => {
1473 memo.insert(key, Some(false));
1474 return Ok(Some(false));
1475 }
1476 None => return Ok(None),
1477 }
1478 }
1479 let mut values = FxHashMap::default();
1480 for input in level.data_inputs(id) {
1481 values.insert(
1482 input.producer.0,
1483 self.available_value(scope, input.producer, state, memo)?,
1484 );
1485 }
1486 let value = match self.classical_value(
1487 level,
1488 id,
1489 classical,
1490 &values,
1491 &state.record,
1492 &state.excluded_instances,
1493 ) {
1494 Err(ExecError::MissingRecord(_)) => Ok(None),
1495 result => result,
1496 }?;
1497 memo.insert(key, value);
1498 Ok(value)
1499 }
1500
1501 fn binding_active(
1502 &self,
1503 binding: &super::closure::OutputBinding,
1504 state: &Shot,
1505 memo: &mut FxHashMap<(LevelPath, BloqNodeId), Option<bool>>,
1506 ) -> Result<Option<bool>, ExecError> {
1507 let mut active = Some(true);
1508 for (scope, guard) in &binding.guards {
1509 match zero_decoder_output(
1510 self.available_value(scope, guard.node, state, memo)?,
1511 Some(guard.output),
1512 ) {
1513 Some(false) => return Ok(Some(false)),
1514 None => active = None,
1515 Some(true) => {}
1516 }
1517 }
1518 Ok(active)
1519 }
1520
1521 fn read_early_faces(
1524 &self,
1525 scope: &LevelPath,
1526 id: BloqNodeId,
1527 state: &mut Shot,
1528 ) -> Result<(), ExecError> {
1529 let mut memo = FxHashMap::default();
1532 let reads = self
1533 .early_reads
1534 .iter()
1535 .enumerate()
1536 .filter(|(slot, read)| {
1537 read.before == (scope.clone(), id) && state.early_faces[*slot].is_none()
1538 })
1539 .map(|(slot, read)| {
1540 let active = self
1541 .binding_active(&read.bindings[0], state, &mut memo)?
1542 .ok_or(ExecError::EarlyOutputGateUnavailable {
1543 observable: read.observable.0,
1544 before: id.0,
1545 })?;
1546 Ok((slot, read, active))
1547 })
1548 .collect::<Result<Vec<_>, ExecError>>()?;
1549 for (slot, read, active) in reads {
1550 if !active {
1551 continue;
1552 }
1553 let operators = read
1554 .bindings
1555 .iter()
1556 .filter(|binding| {
1557 !self
1558 .early_reads
1559 .iter()
1560 .zip(&state.early_faces)
1561 .any(|(earlier, bit)| {
1562 earlier.observable == read.observable
1563 && bit.is_some()
1564 && earlier.bindings.contains(binding)
1565 })
1566 })
1567 .map(|binding| binding.operator(&self.core.bloq))
1568 .collect::<Vec<_>>();
1569 if operators.is_empty() {
1570 continue;
1571 }
1572 let bit = if let [operator] = operators.as_slice() {
1573 self.measure_boundary(operator, &mut state.sim)?
1574 } else {
1575 let mut pauli = PauliString::new(self.core.coord_index.len());
1576 for operator in operators {
1577 pauli = &pauli * self.core.boundary_operator(&operator.operator)?;
1578 }
1579 Self::measure_boundary_pauli(&pauli, &mut state.sim)?
1580 };
1581 state.early_faces[slot] = Some(bit);
1582 }
1583 Ok(())
1584 }
1585
1586 fn measure_boundary(
1598 &self,
1599 operator: &bloq_ir::lowering::InstanceBoundaryOperator,
1600 sim: &mut Simulator,
1601 ) -> Result<bool, ExecError> {
1602 if !self.core.instance_offset.contains_key(&operator.instance) {
1603 return Err(ExecError::MalformedGraph(
1604 "boundary operator names an unknown instance",
1605 ));
1606 }
1607 if operator.operator.is_empty() {
1608 return Err(ExecError::EmptyMppProduct);
1609 }
1610 let pauli = self.core.boundary_operator(&operator.operator)?;
1611 Self::measure_boundary_pauli(pauli, sim)
1612 }
1613
1614 fn measure_boundary_pauli(pauli: &PauliString, sim: &mut Simulator) -> Result<bool, ExecError> {
1615 let expectation = sim.peek_observable_expectation(pauli)?;
1616 if (1.0 - expectation.abs()) < DETERMINISTIC_EXPECTATION_TOLERANCE {
1617 return Ok(expectation < 0.0);
1618 }
1619 Ok(sim.measure_observable(pauli)?.outcome)
1620 }
1621
1622 fn joint_pauli(&self, map: &PauliMap) -> Result<PauliString, ExecError> {
1628 super::circuit::product_pauli(
1629 map,
1630 &super::circuit::LayoutCtx {
1631 coord_to_index: &self.core.coord_index,
1632 offset: IVec2::ZERO,
1633 qubit_count: self.core.coord_index.len(),
1634 },
1635 )
1636 }
1637
1638 fn input_patches(&self) -> Result<Vec<InputPatch>, ExecError> {
1641 let mut placements = FxHashMap::default();
1642 let mut spatial_cubes: FxHashMap<_, Vec<_>> = FxHashMap::default();
1643 let mut spatial_waits = FxHashMap::default();
1644 self.core.bloq.walk(|cx| {
1645 if let Some(quantum) = cx.node.try_quantum() {
1646 placements.extend(
1647 quantum
1648 .instances
1649 .iter()
1650 .map(|instance| (instance.id, (instance.template_id, instance.offset))),
1651 );
1652 for instance in &quantum.instances {
1653 if matches!(
1654 instance.provenance,
1655 bloq_ir::InstanceProvenance::SpatialPortSubstitution {
1656 role: bloq_ir::PortRole::Input,
1657 part: bloq_ir::SpatialPortPart::TemporalPort,
1658 ..
1659 }
1660 ) {
1661 let wait = cx.level.outgoing(cx.id).find_map(|edge| {
1663 if !matches!(edge.edge, bloq_ir::BloqEdge::Quantum(_)) {
1664 return None;
1665 }
1666 let node = &cx.level[edge.target];
1667 node.memory_rounds()?;
1668 node.try_quantum()?
1669 .instances
1670 .iter()
1671 .find(|wait| wait.offset == instance.offset)
1672 });
1673 if let Some(wait) = wait {
1674 spatial_waits.insert(instance.id, wait.id);
1675 }
1676 }
1677 if let bloq_ir::InstanceProvenance::SpatialPortSubstitution {
1678 source,
1679 role,
1680 part: bloq_ir::SpatialPortPart::Cube,
1681 } = instance.provenance
1682 && role == bloq_ir::PortRole::Input
1683 {
1684 spatial_cubes.entry(source).or_default().push(instance.id);
1685 }
1686 }
1687 }
1688 bloq_ir::WalkControl::Continue
1689 });
1690 self.core
1691 .bloq
1692 .logical_inputs()
1693 .iter()
1694 .map(|input| {
1695 let &(template_id, offset) =
1696 placements
1697 .get(&input.instance)
1698 .ok_or(ExecError::MalformedGraph(
1699 "logical input names an unknown instance",
1700 ))?;
1701 let template = &self.core.bloq.templates()[template_id];
1702 let maps = template
1703 .boundary_flows
1704 .iter()
1705 .filter(|flow| flow.start.is_empty() && !flow.end.is_empty())
1706 .map(|flow| {
1707 if flow.sign {
1708 return Err(ExecError::MalformedGraph(
1709 "input Port has a signed create stabilizer",
1710 ));
1711 }
1712 Ok(flow.end.try_translated(offset)?)
1713 })
1714 .collect::<Result<Vec<_>, ExecError>>()?;
1715 let mut data = rustc_hash::FxHashSet::default();
1716 for map in &maps {
1717 data.extend(map.iter().map(|(coordinate, _)| *coordinate));
1718 }
1719 let mut data = data.into_iter().collect::<Vec<_>>();
1720 data.sort_by_key(glam::IVec2::to_array);
1721 let data_qubits = data
1722 .into_iter()
1723 .map(|coordinate| {
1724 self.core
1725 .coord_index
1726 .get(&coordinate)
1727 .map(|&index| index as usize)
1728 .ok_or(ExecError::UnknownCoord(coordinate))
1729 })
1730 .collect::<Result<Vec<_>, _>>()?;
1731 let stabilizers = maps
1732 .iter()
1733 .map(|map| self.joint_pauli(map))
1734 .collect::<Result<Vec<_>, _>>()?;
1735 Ok(InputPatch {
1736 input_instance: input.instance,
1737 prepare_before: spatial_waits
1738 .get(&input.instance)
1739 .copied()
1740 .map(|instance| vec![instance])
1741 .or_else(|| spatial_cubes.get(&input.port).cloned())
1742 .unwrap_or_else(|| vec![input.instance]),
1743 port: input.port,
1744 data_qubits,
1745 stabilizers,
1746 logical_x: self.joint_pauli(&input.x)?,
1747 logical_z: self.joint_pauli(&input.z)?,
1748 })
1749 })
1750 .collect()
1751 }
1752
1753 fn output_logicals(&self) -> Result<Vec<OutputLogical>, ExecError> {
1758 let outputs = self.core.bloq.logical_outputs();
1759 outputs
1760 .iter()
1761 .zip(&self.output_cuts)
1762 .map(|(output, cuts)| {
1763 Ok(OutputLogical {
1764 port: output.port,
1765 logical_x: self.joint_pauli(&output.x)?,
1766 logical_z: self.joint_pauli(&output.z)?,
1767 consumed: !cuts.is_empty(),
1768 })
1769 })
1770 .collect()
1771 }
1772}
1773
1774pub(super) fn collect_rus_restarts(
1779 bloq: &Bloq,
1780 global_meas: &FxHashMap<InstanceMeasurement, u32>,
1781) -> Result<FxHashMap<LevelPath, Vec<(LevelPath, ResolvedDetector)>>, ExecError> {
1782 fn visit_regions(
1783 bloq: &Bloq,
1784 level: &SubGraph,
1785 scope: &LevelPath,
1786 global_meas: &FxHashMap<InstanceMeasurement, u32>,
1787 out: &mut FxHashMap<LevelPath, Vec<(LevelPath, ResolvedDetector)>>,
1788 ) -> Result<(), ExecError> {
1789 for (id, node) in level.nodes() {
1790 let Some(region) = node.try_region() else {
1791 continue;
1792 };
1793 for (selector, body) in region.bodies() {
1794 let child = scope.child(id, selector);
1795 if matches!(region, RegionNode::RepeatUntilSuccess { .. }) {
1796 let mut parities = Vec::new();
1797 collect_restart_parities(bloq, body, &child, global_meas, &mut parities)?;
1798 out.insert(child.clone(), parities);
1799 }
1800 visit_regions(bloq, body, &child, global_meas, out)?;
1801 }
1802 }
1803 Ok(())
1804 }
1805
1806 fn collect_restart_parities(
1807 bloq: &Bloq,
1808 level: &SubGraph,
1809 scope: &LevelPath,
1810 global_meas: &FxHashMap<InstanceMeasurement, u32>,
1811 out: &mut Vec<(LevelPath, ResolvedDetector)>,
1812 ) -> Result<(), ExecError> {
1813 for (id, node) in level.nodes() {
1814 if let Some(quantum) = node.try_quantum() {
1815 for instance in &quantum.instances {
1816 let template = bloq.templates().get(instance.template_id).ok_or(
1817 ExecError::MalformedGraph("instance references a missing template"),
1818 )?;
1819 for restart in &template.restarts {
1820 out.push((
1821 scope.clone(),
1822 resolve_template_parity(&restart.parity, instance.id, global_meas)?
1823 .guarded(
1824 id,
1825 quantum
1826 .guards
1827 .iter()
1828 .find(|guard| guard.instances.contains(&instance.id))
1829 .map(|guard| guard.input),
1830 ),
1831 ));
1832 }
1833 }
1834 for (index, restart) in quantum.restarts.iter().enumerate() {
1835 let restart_index =
1836 u32::try_from(index).map_err(|_| ExecError::IdOverflow("restart"))?;
1837 out.push((
1838 scope.clone(),
1839 resolve_node_parity(&restart.parity, global_meas)?
1840 .with_contributions(
1841 id,
1842 quantum.guards.iter().flat_map(|guard| {
1843 guard
1844 .restart_parities
1845 .iter()
1846 .filter(move |(row, _)| *row as usize == index)
1847 .map(move |(_, parity)| {
1848 (guard.input, resolve_node_parity(parity, global_meas))
1849 })
1850 }),
1851 )?
1852 .guarded(
1853 id,
1854 quantum
1855 .guards
1856 .iter()
1857 .find(|guard| guard.restarts.contains(&restart_index))
1858 .map(|guard| guard.input),
1859 ),
1860 ));
1861 }
1862 }
1863 if let Some(region) = node.try_region()
1864 && !matches!(region, RegionNode::RepeatUntilSuccess { .. })
1865 {
1866 for (selector, body) in region.bodies() {
1867 let child = scope.child(id, selector);
1868 collect_restart_parities(bloq, body, &child, global_meas, out)?;
1869 }
1870 }
1871 }
1872 Ok(())
1873 }
1874
1875 let mut out = FxHashMap::default();
1876 visit_regions(
1877 bloq,
1878 bloq.top(),
1879 &LevelPath::default(),
1880 global_meas,
1881 &mut out,
1882 )?;
1883 Ok(out)
1884}
1885
1886fn collect_detectors(
1889 bloq: &Bloq,
1890 level: &SubGraph,
1891 global_meas: &FxHashMap<InstanceMeasurement, u32>,
1892) -> Result<Vec<(LevelPath, ResolvedDetector)>, ExecError> {
1893 let mut detectors = Vec::new();
1894 collect_detectors_into(
1895 bloq,
1896 level,
1897 &LevelPath::default(),
1898 global_meas,
1899 &mut detectors,
1900 )?;
1901 Ok(detectors)
1902}
1903
1904fn collect_detectors_into(
1905 bloq: &Bloq,
1906 level: &SubGraph,
1907 scope: &LevelPath,
1908 global_meas: &FxHashMap<InstanceMeasurement, u32>,
1909 out: &mut Vec<(LevelPath, ResolvedDetector)>,
1910) -> Result<(), ExecError> {
1911 for (id, node) in level.nodes() {
1912 if let Some(quantum) = node.try_quantum() {
1913 for instance in &quantum.instances {
1914 let template =
1915 bloq.templates()
1916 .get(instance.template_id)
1917 .ok_or(ExecError::MalformedGraph(
1918 "instance references a missing template",
1919 ))?;
1920 for detector in &template.detectors {
1921 out.push((
1922 scope.clone(),
1923 resolve_template_parity(&detector.parity, instance.id, global_meas)?
1924 .guarded(
1925 id,
1926 quantum
1927 .guards
1928 .iter()
1929 .find(|guard| guard.instances.contains(&instance.id))
1930 .map(|guard| guard.input),
1931 ),
1932 ));
1933 }
1934 }
1935 for detector in bloq.node_detectors(quantum)? {
1936 let parity = resolve_node_parity(&detector.parity(), global_meas)?;
1937 let (membership, parity) = match detector.address() {
1938 NodeDetectorAddress::Inline(index) => {
1939 let membership = quantum
1940 .guards
1941 .iter()
1942 .find(|guard| guard.detectors.contains(&index))
1943 .map(|guard| guard.input);
1944 let parity = parity.with_contributions(
1945 id,
1946 quantum.guards.iter().flat_map(|guard| {
1947 guard
1948 .detector_parities
1949 .iter()
1950 .filter(move |(row, _)| *row == index)
1951 .map(move |(_, parity)| {
1952 (guard.input, resolve_node_parity(parity, global_meas))
1953 })
1954 }),
1955 )?;
1956 (membership, parity)
1957 }
1958 NodeDetectorAddress::Bundle { use_index, .. } => {
1959 let membership = quantum
1960 .guards
1961 .iter()
1962 .find(|guard| guard.detector_bundles.contains(&use_index))
1963 .map(|guard| guard.input);
1964 (membership, parity)
1965 }
1966 };
1967 out.push((scope.clone(), parity.guarded(id, membership)));
1968 }
1969 }
1970 if let Some(region) = node.try_region() {
1971 for (selector, body) in region.bodies() {
1972 let child = scope.child(id, selector);
1973 collect_detectors_into(bloq, body, &child, global_meas, out)?;
1974 }
1975 }
1976 }
1977 Ok(())
1978}
1979
1980fn collect_observables(bloq: &Bloq) -> Vec<BloqNodeId> {
1982 bloq.top()
1983 .nodes()
1984 .filter_map(|(id, node)| match node.try_classical() {
1985 Some(ClassicalNode::Observable { index: Some(_), .. }) => Some(id),
1986 _ => None,
1987 })
1988 .collect()
1989}
1990
1991#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
1995pub(super) struct ResolvedDetector {
1996 pub sign: bool,
1997 pub terms: Vec<(TemplateInstanceId, u32)>,
1998 membership: Option<(BloqNodeId, u32)>,
1999 conditioned: Vec<(BloqNodeId, u32, ResolvedDetector)>,
2000}
2001
2002impl ResolvedDetector {
2003 fn with_contributions(
2004 mut self,
2005 node: BloqNodeId,
2006 contributions: impl Iterator<Item = (u32, Result<ResolvedDetector, ExecError>)>,
2007 ) -> Result<Self, ExecError> {
2008 for (input, parity) in contributions {
2009 let parity = parity?;
2010 self.conditioned.push((node, input, parity));
2011 }
2012 Ok(self)
2013 }
2014 fn guarded(mut self, node: BloqNodeId, input: Option<u32>) -> Self {
2015 self.membership = input.map(|slot| (node, slot));
2016 self
2017 }
2018
2019 fn applies(&self, scope: &LevelPath, state: &Shot) -> bool {
2020 state.executed_scopes.contains(scope)
2021 && self.membership.is_none_or(|(node, slot)| {
2022 state
2023 .active_membership_inputs
2024 .contains(&(scope.clone(), node, slot))
2025 })
2026 }
2027}
2028
2029fn resolve_parity<T: Copy + Ord>(
2034 parity: &DetectorParity<T>,
2035 global_meas: &FxHashMap<InstanceMeasurement, u32>,
2036 owner: impl Fn(T) -> InstanceMeasurement,
2037 unknown: &'static str,
2038) -> Result<ResolvedDetector, ExecError> {
2039 let mut ids = Vec::with_capacity(parity.terms().len());
2040 for term in parity.terms() {
2041 match term {
2042 DetectorTerm::Measurement(m) => {
2043 let key = owner(*m);
2044 let global = *global_meas
2045 .get(&key)
2046 .ok_or(ExecError::MalformedGraph(unknown))?;
2047 ids.push((key.instance, global));
2048 }
2049 DetectorTerm::LoopState(state) => return Err(ExecError::UnresolvedLoopState(state.0)),
2050 }
2051 }
2052 Ok(ResolvedDetector {
2053 sign: parity.sign(),
2054 terms: ids,
2055 membership: None,
2056 conditioned: Vec::new(),
2057 })
2058}
2059
2060fn resolve_template_parity(
2061 parity: &DetectorParity<u32>,
2062 instance: TemplateInstanceId,
2063 global_meas: &FxHashMap<InstanceMeasurement, u32>,
2064) -> Result<ResolvedDetector, ExecError> {
2065 resolve_parity(
2066 parity,
2067 global_meas,
2068 |measurement| InstanceMeasurement {
2069 instance,
2070 measurement,
2071 },
2072 "template detector names an unknown measurement",
2073 )
2074}
2075
2076fn resolve_node_parity(
2077 parity: &DetectorParity<InstanceMeasurement>,
2078 global_meas: &FxHashMap<InstanceMeasurement, u32>,
2079) -> Result<ResolvedDetector, ExecError> {
2080 resolve_parity(
2081 parity,
2082 global_meas,
2083 |im| im,
2084 "node detector names an unknown measurement",
2085 )
2086}
2087
2088pub(crate) fn collect_instances(
2090 level: &SubGraph,
2091 out: &mut rustc_hash::FxHashSet<TemplateInstanceId>,
2092) {
2093 for (_, node) in level.nodes() {
2094 if let Some(quantum) = node.try_quantum() {
2095 out.extend(quantum.instances.iter().map(|i| i.id));
2096 }
2097 if let Some(region) = node.try_region() {
2098 for (_, body) in region.bodies() {
2099 collect_instances(body, out);
2100 }
2101 }
2102 }
2103}
2104
2105fn bound_body_value(body: &SubGraph, body_values: &FxHashMap<u32, Option<bool>>) -> Option<bool> {
2108 match body.value_output() {
2109 Some(producer) => zero_decoder_output(
2110 body_values.get(&producer.node.0).copied().flatten(),
2111 Some(producer.output),
2112 ),
2113 None => Some(false),
2114 }
2115}
2116
2117fn zero_decoder_output(value: Option<bool>, output: Option<ObservableOutput>) -> Option<bool> {
2119 value.map(|bit| bit && output != Some(ObservableOutput::Flip))
2120}
2121
2122fn bound_body_bindings<'a>(
2124 body: &'a SubGraph,
2125 body_bindings: &FxHashMap<u32, BoundOperators<'a>>,
2126) -> Result<Vec<&'a InstanceBoundaryOperator>, ExecError> {
2127 let mut operators = Vec::new();
2128 let mut expanded = 0;
2129 for &producer in body.boundary_outputs() {
2130 collect_bound_operators(body_bindings, producer, &mut operators, &mut expanded)?;
2131 }
2132 Ok(operators)
2133}
2134
2135fn xor_records(
2140 parity: &ResolvedDetector,
2141 scope: &LevelPath,
2142 state: &Shot,
2143) -> Result<bool, ExecError> {
2144 let mut value = parity.sign;
2145 for &(instance, id) in &parity.terms {
2146 if state.excluded_instances.contains(&instance) {
2147 continue;
2148 }
2149 value ^= state.record.require(id)?;
2150 }
2151 for (node, input, contribution) in &parity.conditioned {
2152 if !state
2153 .active_membership_inputs
2154 .contains(&(scope.clone(), *node, *input))
2155 {
2156 continue;
2157 }
2158 value ^= contribution.sign;
2159 for &(instance, id) in &contribution.terms {
2160 if !state.excluded_instances.contains(&instance) {
2161 value ^= state.record.require(id)?;
2162 }
2163 }
2164 }
2165 Ok(value)
2166}
2167
2168fn rus_attempt_seed(seed: u64, scope: &LevelPath, attempt: u32) -> u64 {
2169 let mut state = splitmix64(seed ^ 0x5255_535F_5254_5259);
2170 for segment in scope.segments() {
2171 state = splitmix64(state ^ u64::from(segment.region.0));
2172 state = splitmix64(state);
2173 }
2174 splitmix64(state ^ u64::from(attempt))
2175}
2176
2177#[inline]
2178const fn splitmix64(mut value: u64) -> u64 {
2179 value = value.wrapping_add(0x9E37_79B9_7F4A_7C15);
2180 value = (value ^ (value >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
2181 value = (value ^ (value >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
2182 value ^ (value >> 31)
2183}
2184
2185#[cfg(test)]
2186mod tests {
2187 use super::*;
2188 use crate::test_support::{guarded_node, one_qubit_template, quantum_node};
2189 use bloq_ir::BoundaryFace;
2190 use bloq_ir::circuit::{CoordCircuit, GateType, Pauli as CircuitPauli, PauliBasis};
2191 use bloq_ir::lowering::{BloqTemplate, NodeRestart};
2192 use bloq_ir::{BloqEdge, BloqNode, ClassicalExpr, NodeDetector, TemplateDetector};
2193
2194 #[test]
2195 fn early_cut_activation_uses_the_selected_observable_port() {
2196 for output in [ObservableOutput::Corrected, ObservableOutput::Flip] {
2197 let mut bloq = Bloq::new();
2198 let bit = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
2199 expr: ClassicalExpr::Const(true),
2200 }));
2201 let observable = bloq.add_node(BloqNode::classical(ClassicalNode::observable(0)));
2202 bloq.add_edge(bit, observable, BloqEdge::value(0));
2203 let mut guarded = BloqNode::classical(ClassicalNode::Compute {
2204 expr: ClassicalExpr::Const(true),
2205 });
2206 guarded.activation = Some(0);
2207 let guarded = bloq.add_node(guarded);
2208 bloq.add_edge(
2209 observable,
2210 guarded,
2211 BloqEdge::Value {
2212 output,
2213 slot: 0,
2214 role: bloq_ir::ValueRole::Data,
2215 },
2216 );
2217 bloq.validate().unwrap();
2218 let program = Program::prepare(&bloq, RunMode::Plain).unwrap();
2219 let mut completed = program
2220 .execute_shot(0, 0, false, &mut |_, _| Ok(()))
2221 .unwrap();
2222 let expected = Some(output == ObservableOutput::Corrected);
2223 assert_eq!(completed.top_values[&guarded.0], expected);
2224
2225 completed.state.classical_values.clear();
2226 let scope = LevelPath::default();
2227 let mut memo = FxHashMap::default();
2228 assert_eq!(
2229 program
2230 .available_value(&scope, guarded, &completed.state, &mut memo)
2231 .unwrap(),
2232 expected
2233 );
2234 completed
2235 .state
2236 .classical_values
2237 .insert((scope.clone(), observable), None);
2238 memo.clear();
2239 assert_eq!(
2240 program
2241 .available_value(&scope, guarded, &completed.state, &mut memo)
2242 .unwrap(),
2243 None
2244 );
2245 }
2246 }
2247
2248 #[test]
2249 fn unavailable_discard_condition_errors_when_executed() {
2250 let mut bloq = Bloq::new();
2251 bloq.add_node(BloqNode::classical(ClassicalNode::Discard {
2252 condition: ClassicalExpr::In(0),
2253 }));
2254
2255 assert!(matches!(
2256 bloq.validate(),
2257 Err(bloq_ir::BloqValidationError::MissingClassicalInput { slot: 0, .. })
2258 ));
2259 assert!(matches!(
2260 run_bloq(&bloq, 1, 0),
2261 Err(ExecError::MalformedGraph(
2262 "discard condition is unavailable"
2263 ))
2264 ));
2265 }
2266
2267 #[test]
2268 fn conditional_members_execute_once_with_selected_record_aliases() {
2269 let mut bloq = Bloq::new();
2270 let mut coin = CoordCircuit::new();
2271 coin.do_gate(GateType::H, [IVec2::X]).unwrap();
2272 let bit = coin.measure(PauliBasis::Z, [IVec2::X])[0];
2273 let coin_template = bloq.add_template(BloqTemplate::new(coin));
2274 let coin = bloq.add_node(quantum_instance(TemplateInstanceId(0), coin_template));
2275 let selector = bloq.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
2276 vec![InstanceMeasurement {
2277 instance: TemplateInstanceId(0),
2278 measurement: bit,
2279 }],
2280 Vec::new(),
2281 )));
2282 bloq.add_edge(coin, selector, BloqEdge::Order);
2283 let opposite = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
2284 expr: ClassicalExpr::Not(Box::new(ClassicalExpr::In(0))),
2285 }));
2286 bloq.add_edge(selector, opposite, BloqEdge::value(0));
2287
2288 let mut measure = CoordCircuit::new();
2291 let measurement = measure.measure(PauliBasis::Z, [IVec2::ZERO])[0];
2292 let template = bloq.add_template(BloqTemplate::new(measure));
2293 let mut registry = quantum_instance(TemplateInstanceId(1), template);
2294 registry.expect_quantum_mut().instances.push(
2295 quantum_instance(TemplateInstanceId(2), template)
2296 .expect_quantum()
2297 .instances[0],
2298 );
2299 registry
2300 .expect_quantum_mut()
2301 .guards
2302 .push(bloq_ir::QuantumGuard {
2303 input: 0,
2304 instances: vec![TemplateInstanceId(1)],
2305 detectors: vec![0],
2306 restarts: vec![],
2307 ..Default::default()
2308 });
2309 registry.expect_quantum_mut().detectors.push(NodeDetector {
2310 parity: DetectorParity::default().with_sign(true),
2311 coords: None,
2312 });
2313 let registry = bloq.add_node(registry);
2314 bloq.add_edge(selector, registry, BloqEdge::value(0));
2315 let parity = bloq.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
2316 vec![InstanceMeasurement {
2317 instance: TemplateInstanceId(2),
2318 measurement,
2319 }],
2320 Vec::new(),
2321 )));
2322 bloq.add_edge(registry, parity, BloqEdge::Order);
2323 let observable = bloq.add_node(BloqNode::classical(ClassicalNode::observable(0)));
2324 bloq.add_edge(parity, observable, BloqEdge::value(0));
2325 let choice = bloq.add_node(BloqNode::classical(ClassicalNode::observable(1)));
2326 bloq.add_edge(selector, choice, BloqEdge::value(0));
2327 let report = run_bloq(&bloq, 64, 0).unwrap();
2328 assert_eq!(report.observables[0].per_shot, [false; 64]);
2329 let enabled = report.observables[1]
2330 .per_shot
2331 .iter()
2332 .filter(|&&bit| bit)
2333 .count();
2334 assert!(enabled > 0 && enabled < 64);
2335 assert_eq!(report.detectors[0].per_shot, vec![true; enabled]);
2336 }
2337
2338 #[test]
2339 fn retry_result_is_independent_of_restart_and_internal_values() {
2340 let program = Bloq::from_text(
2341 "BLOQIR 1
2342graph {
2343 n0 rus in0 source n0 {
2344 body {
2345 n0 compute 0
2346 n1 compute 1
2347 n2 compute !in0
2348 n3 compute 0
2349 n1 -> n2 value 0
2350 result n1
2351 }
2352 }
2353 n1 observable 0
2354 n0 -> n1 value 0
2355}",
2356 )
2357 .unwrap();
2358 for mut program in [
2359 program.clone(),
2360 Bloq::from_binary(&program.to_binary()).unwrap(),
2361 ] {
2362 program.optimize().expect("acyclic test program");
2363 let report = run_bloq(&program, 8, 0).unwrap();
2364 assert_eq!(report.observables[0].per_shot, [true; 8]);
2365 }
2366 }
2367
2368 #[test]
2369 fn region_exports_its_declared_observable_measurement() {
2370 for bit in [false, true] {
2371 let mut circuit = CoordCircuit::new();
2372 if bit {
2373 circuit.do_gate(GateType::X, [IVec2::ZERO]).unwrap();
2374 }
2375 let measurement = circuit.measure(PauliBasis::Z, [IVec2::ZERO])[0];
2376 let mut bloq = Bloq::new();
2377 let template = bloq.add_template(BloqTemplate::new(circuit));
2378 let mut body = SubGraph::new();
2379 let quantum = body.add_node(quantum_instance(TemplateInstanceId(0), template));
2380 let parity = body.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
2381 vec![InstanceMeasurement {
2382 instance: TemplateInstanceId(0),
2383 measurement,
2384 }],
2385 Vec::new(),
2386 )));
2387 let observable = body.add_node(BloqNode::classical(ClassicalNode::observable(1)));
2388 body.add_edge(quantum, parity, BloqEdge::Order);
2389 body.add_edge(parity, observable, BloqEdge::value(0));
2390 body.set_value_output(Some(observable.into()));
2391 let region = bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
2392 restart_condition: ClassicalExpr::Const(false),
2393 restart_source: None,
2394 body,
2395 }));
2396 let output = bloq.add_node(BloqNode::classical(ClassicalNode::observable(0)));
2397 bloq.add_edge(region, output, BloqEdge::value(0));
2398 let report = run_bloq(&bloq, 1, 0).unwrap();
2399 assert_eq!(report.observables[0].per_shot, [bit]);
2400 }
2401 }
2402
2403 #[test]
2404 fn discarded_shot_keeps_evaluated_branch_selector() {
2405 let mut bloq = Bloq::new();
2406 let branch = bloq.add_node(selector_node(ClassicalExpr::Const(true)));
2407 let discard = bloq.add_node(BloqNode::classical(ClassicalNode::Discard {
2408 condition: ClassicalExpr::Const(true),
2409 }));
2410 bloq.add_edge(branch, discard, BloqEdge::Order);
2411
2412 let report = run_bloq(&bloq, 1, 0).expect("valid program executes");
2413 assert_eq!(report.discarded, 1);
2414 assert_eq!(report.branch_selectors, [true]);
2415 }
2416
2417 fn selector_node(expr: ClassicalExpr) -> BloqNode {
2418 BloqNode::classical(ClassicalNode::Compute { expr }).with_provenance(
2419 bloq_ir::NodeProvenance::BranchSelector {
2420 name: "choice".into(),
2421 },
2422 )
2423 }
2424
2425 fn quantum_instance(instance: TemplateInstanceId, template: bloq_ir::TemplateId) -> BloqNode {
2426 quantum_node(template, instance.0, IVec2::ZERO)
2427 }
2428
2429 fn guarded_detector_program(take_arm: bool) -> Bloq {
2430 let mut prefix_circuit = CoordCircuit::new();
2431 let prefix_measurement = prefix_circuit.measure(PauliBasis::Z, [IVec2::ZERO])[0];
2432 let mut bloq = Bloq::new();
2433 let prefix_template = bloq.add_template(BloqTemplate::new(prefix_circuit));
2434 let prefix_instance = TemplateInstanceId(0);
2435 let prefix = bloq.add_node(quantum_instance(prefix_instance, prefix_template));
2436
2437 let mut arm_circuit = CoordCircuit::new();
2438 arm_circuit.measure(PauliBasis::Z, [IVec2::ZERO]);
2439 let mut arm_template = BloqTemplate::new(arm_circuit);
2440 arm_template.detectors.push(TemplateDetector {
2441 scope: TemplateDetectorScope::TopLevel,
2442 parity: DetectorParity::default().with_sign(true),
2443 coords: None,
2444 });
2445 let arm_template = bloq.add_template(arm_template);
2446 let mut arm_node = quantum_instance(TemplateInstanceId(1), arm_template);
2447 arm_node.expect_quantum_mut().detectors.push(NodeDetector {
2448 parity: DetectorParity::from_measurements([InstanceMeasurement {
2449 instance: prefix_instance,
2450 measurement: prefix_measurement,
2451 }])
2452 .with_sign(true),
2453 coords: None,
2454 });
2455 let selector = bloq.add_node(selector_node(ClassicalExpr::Const(take_arm)));
2456 let selected = bloq.add_node(guarded_node(arm_node, 0));
2457 bloq.add_edge(prefix, selected, BloqEdge::Order);
2458 bloq.add_edge(selector, selected, BloqEdge::value(0));
2459 bloq
2460 }
2461
2462 fn selected_detector_contribution_program(take_arm: bool) -> Bloq {
2463 let mut bloq = Bloq::new();
2464 let selector = bloq.add_node(selector_node(ClassicalExpr::Const(take_arm)));
2465 let opposite = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
2466 expr: ClassicalExpr::Not(Box::new(ClassicalExpr::In(0))),
2467 }));
2468 bloq.add_edge(selector, opposite, BloqEdge::value(0));
2469 let mut downstream = BloqNode::from_members(Vec::new());
2470 downstream
2471 .expect_quantum_mut()
2472 .detectors
2473 .push(NodeDetector {
2474 parity: DetectorParity::default(),
2475 coords: None,
2476 });
2477 let mut owners = Vec::new();
2478 for (instance, active) in [(0, opposite), (1, selector)] {
2479 let mut circuit = CoordCircuit::new();
2480 if instance == 1 {
2481 circuit.do_gate(GateType::X, [IVec2::ZERO]).unwrap();
2482 }
2483 let measurement = circuit.measure(PauliBasis::Z, [IVec2::ZERO])[0];
2484 let template = bloq.add_template(BloqTemplate::new(circuit));
2485 let selected = bloq.add_node(guarded_node(
2486 quantum_node(template, instance, IVec2::ZERO),
2487 0,
2488 ));
2489 bloq.add_edge(active, selected, BloqEdge::value(0));
2490 owners.push(selected);
2491 downstream
2492 .expect_quantum_mut()
2493 .guards
2494 .push(bloq_ir::QuantumGuard {
2495 input: instance,
2496 detector_parities: vec![(
2497 0,
2498 DetectorParity::from_measurements([InstanceMeasurement {
2499 instance: TemplateInstanceId(instance),
2500 measurement,
2501 }]),
2502 )],
2503 ..Default::default()
2504 });
2505 }
2506 let downstream = bloq.add_node(downstream);
2507 bloq.add_edge(opposite, downstream, BloqEdge::value(0));
2508 bloq.add_edge(selector, downstream, BloqEdge::value(1));
2509 for owner in owners {
2510 bloq.add_edge(owner, downstream, BloqEdge::Order);
2511 }
2512 bloq
2513 }
2514
2515 fn signed_detector_node() -> BloqNode {
2516 let mut node = BloqNode::from_members(Vec::new());
2517 node.expect_quantum_mut().detectors.push(NodeDetector {
2518 parity: DetectorParity::default().with_sign(true),
2519 coords: None,
2520 });
2521 node
2522 }
2523
2524 fn signed_restart_node() -> BloqNode {
2525 let mut node = BloqNode::from_members(Vec::new());
2526 node.expect_quantum_mut().restarts.push(NodeRestart {
2527 parity: DetectorParity::default().with_sign(true),
2528 });
2529 node
2530 }
2531
2532 #[test]
2533 fn guarded_detectors_only_report_when_selected() {
2534 let skipped = run_bloq(&guarded_detector_program(false), 3, 0).unwrap();
2535 assert_eq!(skipped.detectors.len(), 2);
2536 assert!(skipped.detectors.iter().all(|detector| {
2537 detector.constant && detector.value.is_none() && detector.per_shot.is_empty()
2538 }));
2539
2540 let taken = run_bloq(&guarded_detector_program(true), 3, 0).unwrap();
2541 assert_eq!(taken.detectors.len(), 2);
2542 assert!(
2543 taken
2544 .detectors
2545 .iter()
2546 .all(|detector| detector.per_shot == [true; 3])
2547 );
2548 }
2549
2550 #[test]
2551 fn guarded_shared_detector_reads_two_owner_instances() {
2552 use bloq_ir::{BundleDetector, BundleMeasurement, DetectorBundle, DetectorBundleUse};
2553
2554 for enabled in [false, true] {
2555 let mut bloq = Bloq::new();
2556 let mut circuit = CoordCircuit::new();
2557 let measurement = circuit.measure(PauliBasis::Z, [IVec2::ZERO])[0];
2558 let template = bloq.add_template(BloqTemplate::new(circuit));
2559 let bundle = bloq.add_detector_bundle(DetectorBundle::new(
2560 vec![template, template],
2561 vec![BundleDetector {
2562 parity: DetectorParity::from_measurements([
2563 BundleMeasurement {
2564 owner: 0,
2565 measurement,
2566 },
2567 BundleMeasurement {
2568 owner: 1,
2569 measurement,
2570 },
2571 ])
2572 .with_sign(true),
2573 coords: None,
2574 }],
2575 ));
2576 let mut node = BloqNode::from_members(Vec::new());
2577 let quantum = node.expect_quantum_mut();
2578 quantum.instances = vec![
2579 bloq_ir::lowering::TemplateInstance::new(
2580 TemplateInstanceId(0),
2581 template,
2582 IVec2::ZERO,
2583 ),
2584 bloq_ir::lowering::TemplateInstance::new(TemplateInstanceId(1), template, IVec2::X),
2585 ];
2586 quantum.detector_bundles.push(DetectorBundleUse {
2587 bundle,
2588 instances: vec![TemplateInstanceId(0), TemplateInstanceId(1)],
2589 offset: IVec2::ZERO,
2590 });
2591 quantum.guards.push(bloq_ir::QuantumGuard {
2592 input: 0,
2593 detector_bundles: vec![0],
2594 ..Default::default()
2595 });
2596 let selector = bloq.add_node(selector_node(ClassicalExpr::Const(enabled)));
2597 let node = bloq.add_node(node);
2598 bloq.add_edge(selector, node, BloqEdge::value(0));
2599
2600 bloq.validate().expect("shared detector binding validates");
2601 let report = run_bloq(&bloq, 2, 0).expect("program executes");
2602 assert_eq!(report.detectors.len(), 1);
2603 assert_eq!(
2604 report.detectors[0].per_shot,
2605 if enabled { vec![true; 2] } else { vec![] }
2606 );
2607
2608 let other = bloq.add_template(BloqTemplate::new(CoordCircuit::new()));
2609 let wrong_bundle =
2610 bloq.add_detector_bundle(DetectorBundle::new(vec![other, template], vec![]));
2611 bloq.node_mut(node)
2612 .unwrap()
2613 .expect_quantum_mut()
2614 .detector_bundles[0]
2615 .bundle = wrong_bundle;
2616 assert!(matches!(
2617 run_bloq(&bloq, 1, 0),
2618 Err(ExecError::DetectorBundle(
2619 bloq_ir::DetectorBundleError::OwnerTemplateMismatch { .. }
2620 ))
2621 ));
2622 assert!(matches!(
2623 crate::lower::lower(&bloq, &crate::lower::LoweringConfig::default()),
2624 Err(crate::lower::LowerError::DetectorBundle(
2625 bloq_ir::DetectorBundleError::OwnerTemplateMismatch { .. }
2626 ))
2627 ));
2628 }
2629 }
2630
2631 #[test]
2632 fn downstream_detector_folds_only_selected_contribution() {
2633 for chosen in [false, true] {
2634 let bloq = selected_detector_contribution_program(chosen);
2635 bloq.validate()
2636 .expect("conditional detector program is valid");
2637
2638 let report = run_bloq(&bloq, 1, 0).expect("guarded program executes");
2639 assert_eq!(report.detectors[0].per_shot, [chosen]);
2640 }
2641 }
2642
2643 #[test]
2646 fn rejected_rus_attempt_does_not_leave_active_detectors() {
2647 let mut circuit = CoordCircuit::new();
2648 circuit.do_gate(GateType::H, [IVec2::ZERO]).unwrap();
2649 let measurement = circuit.measure(PauliBasis::Z, [IVec2::ZERO])[0];
2650 let mut bloq = Bloq::new();
2651 let template = bloq.add_template(BloqTemplate::new(circuit));
2652 let mut body = SubGraph::new();
2653 let measure = body.add_node(quantum_instance(TemplateInstanceId(0), template));
2654 let selector = body.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
2655 vec![InstanceMeasurement {
2656 instance: TemplateInstanceId(0),
2657 measurement,
2658 }],
2659 Vec::new(),
2660 )));
2661 let opposite = body.add_node(BloqNode::classical(ClassicalNode::Compute {
2662 expr: ClassicalExpr::Not(Box::new(ClassicalExpr::In(0))),
2663 }));
2664 body.add_edge(measure, selector, BloqEdge::Order);
2665 body.add_edge(selector, opposite, BloqEdge::value(0));
2666 for (active, restarting) in [(opposite, true), (selector, false)] {
2667 let mut quantum = signed_detector_node();
2668 if restarting {
2669 quantum.expect_quantum_mut().restarts.push(NodeRestart {
2670 parity: DetectorParity::default().with_sign(true),
2671 });
2672 }
2673 let node = guarded_node(quantum, 0);
2674 let selected = body.add_node(node);
2675 body.add_edge(active, selected, BloqEdge::value(0));
2676 }
2677 let restart_source = body.add_node(BloqNode::classical(ClassicalNode::Compute {
2678 expr: ClassicalExpr::Const(false),
2679 }));
2680 bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
2681 body,
2682 restart_condition: ClassicalExpr::Const(false),
2683 restart_source: Some(restart_source.into()),
2684 }));
2685 let report = run_bloq(&bloq, 8, 0).unwrap();
2686 assert_eq!(report.discarded, 0);
2687 assert!(report.detectors[0].per_shot.is_empty());
2688 assert_eq!(report.detectors[1].per_shot, [true; 8]);
2689 }
2690
2691 #[test]
2692 fn guarded_restart_only_applies_when_selected() {
2693 let program = |take_restart_arm| {
2694 let mut body = SubGraph::new();
2695 let selector = body.add_node(selector_node(ClassicalExpr::Const(take_restart_arm)));
2696 let restart = body.add_node(guarded_node(signed_restart_node(), 0));
2697 body.add_edge(selector, restart, BloqEdge::value(0));
2698 let restart_source = body.add_node(BloqNode::classical(ClassicalNode::Compute {
2699 expr: ClassicalExpr::Const(false),
2700 }));
2701 let mut bloq = Bloq::new();
2702 bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
2703 body,
2704 restart_condition: ClassicalExpr::Const(false),
2705 restart_source: Some(restart_source.into()),
2706 }));
2707 bloq
2708 };
2709 let kept = run_bloq(&program(false), 1, 0).unwrap();
2710 assert_eq!(kept.discarded, 0);
2711 let rejected = run_bloq(&program(true), 1, 0).unwrap();
2712 assert_eq!(rejected.discarded, 1);
2713 }
2714
2715 #[test]
2721 fn transparent_rus_retry_preserves_downstream_randomness() {
2722 let coin = |coord: IVec2| {
2724 let mut circuit = CoordCircuit::new();
2725 circuit.do_gate(GateType::H, [coord]).expect("H is valid");
2726 let measurement = circuit.measure(PauliBasis::Z, [coord])[0];
2727 (BloqTemplate::new(circuit), measurement)
2728 };
2729
2730 let program = |with_prefix: bool| {
2731 let mut bloq = Bloq::new();
2732
2733 let prefix = with_prefix.then(|| {
2735 let (template, measurement) = coin(IVec2::new(2, 0));
2736 let template = bloq.add_template(template);
2737 let instance = TemplateInstanceId(1);
2738 let mut body = SubGraph::new();
2739 let flip = body.add_node(quantum_instance(instance, template));
2740 let restart_source =
2741 body.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
2742 vec![InstanceMeasurement {
2743 instance,
2744 measurement,
2745 }],
2746 Vec::new(),
2747 )));
2748 body.add_edge(flip, restart_source, BloqEdge::Order);
2749 bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
2750 body,
2751 restart_condition: ClassicalExpr::Not(Box::new(ClassicalExpr::In(0))),
2752 restart_source: Some(restart_source.into()),
2753 }))
2754 });
2755
2756 let (template, measurement) = coin(IVec2::ZERO);
2757 let template = bloq.add_template(template);
2758 let instance = TemplateInstanceId(0);
2759 let quantum = bloq.add_node(quantum_instance(instance, template));
2760 let selector = bloq.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
2761 vec![InstanceMeasurement {
2762 instance,
2763 measurement,
2764 }],
2765 Vec::new(),
2766 )));
2767 let branch = bloq.add_node(selector_node(ClassicalExpr::In(0)));
2768 if let Some(prefix) = prefix {
2769 bloq.add_edge(prefix, quantum, BloqEdge::Order);
2770 }
2771 bloq.add_edge(quantum, selector, BloqEdge::Order);
2772 bloq.add_edge(selector, branch, BloqEdge::value(0));
2773 bloq
2774 };
2775
2776 let plain = run_bloq(&program(false), 64, 17).expect("prefix-free program executes");
2777 let retried = run_bloq(&program(true), 64, 17).expect("retrying prefix executes");
2778
2779 assert_eq!(plain.branch_selectors.len(), 64);
2780 assert_eq!(retried.discarded, 0);
2781 assert_eq!(retried.branch_selectors, plain.branch_selectors);
2782 }
2783
2784 #[test]
2791 fn measure_boundary_does_not_double_apply_offset() {
2792 let coord = IVec2::new(24, 0);
2793 let instance = TemplateInstanceId(0);
2794
2795 let mut bloq = Bloq::new();
2796 let template_id = one_qubit_template(&mut bloq, GateType::X);
2797 bloq.add_node(quantum_node(template_id, instance.0, coord));
2798
2799 let operator = InstanceBoundaryOperator {
2802 instance,
2803 face: BoundaryFace::Output,
2804 operator: [(coord, CircuitPauli::Z)].into_iter().collect(),
2805 };
2806 bloq.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
2807 Vec::new(),
2808 vec![operator.clone()],
2809 )));
2810 let program = Program::prepare(&bloq, RunMode::Plain).unwrap();
2811
2812 let mut sim = Simulator::with_seed(1, 0);
2814 sim.x(0);
2815 let outcome = program
2816 .measure_boundary(&operator, &mut sim)
2817 .expect("boundary resolves at the true global coordinate (24, 0)");
2818 assert!(outcome, "Z on |1> is the −1 eigenvalue");
2819 }
2820
2821 fn nested_boundary_program(depth: usize) -> Bloq {
2822 let coord = IVec2::ZERO;
2823 let instance = TemplateInstanceId(0);
2824 let mut bloq = Bloq::new();
2825 let template = one_qubit_template(&mut bloq, GateType::X);
2826 let quantum = bloq.add_node(quantum_node(template, instance.0, IVec2::ZERO));
2827 let operator = InstanceBoundaryOperator {
2828 instance,
2829 face: BoundaryFace::Output,
2830 operator: [(coord, CircuitPauli::Z)].into_iter().collect(),
2831 };
2832 let mut body = SubGraph::new();
2833 body.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
2834 Vec::new(),
2835 vec![operator],
2836 )));
2837 let internal = body.add_node(body[BloqNodeId(0)].clone());
2838 let local = body.add_node(BloqNode::classical(ClassicalNode::observable(1)));
2839 body.add_edge(BloqNodeId(0), local, BloqEdge::value(0));
2840 body.add_edge(internal, local, BloqEdge::value(1));
2841 body.set_boundary_outputs(vec![BloqNodeId(0)]);
2842 let mut region = RegionNode::RepeatUntilSuccess {
2843 restart_condition: ClassicalExpr::Const(false),
2844 restart_source: None,
2845 body,
2846 };
2847 for _ in 1..depth {
2848 let mut body = SubGraph::new();
2849 let nested = body.add_node(BloqNode::region(region));
2850 body.set_boundary_outputs(vec![nested]);
2851 let consume = body.add_node(BloqNode::classical(ClassicalNode::Compute {
2852 expr: ClassicalExpr::In(0),
2853 }));
2854 body.add_edge(nested, consume, BloqEdge::value(0));
2855 region = RegionNode::RepeatUntilSuccess {
2856 restart_condition: ClassicalExpr::Const(false),
2857 restart_source: None,
2858 body,
2859 };
2860 }
2861 let region = bloq.add_node(BloqNode::region(region));
2862 let observable = bloq.add_node(BloqNode::classical(ClassicalNode::observable(0)));
2863 bloq.add_edge(quantum, region, BloqEdge::Order);
2864 bloq.add_edge(region, observable, BloqEdge::compose(0));
2865 bloq
2866 }
2867
2868 #[test]
2869 fn nested_rus_include_bindings_reach_observable() {
2870 for depth in [1, 2, 3] {
2871 let mut bloq = nested_boundary_program(depth);
2872 bloq.optimize().expect("acyclic test program");
2873 let report = run_bloq(&bloq, 1, 0).expect("nested boundary program executes");
2874 assert_eq!(
2875 report.observables[0].per_shot,
2876 [true],
2877 "the selected body's Z binding reads the prepared |1>"
2878 );
2879 }
2880 }
2881
2882 #[test]
2883 fn early_output_reads_preserve_joint_bell_observables() {
2884 for reuse in [false, true] {
2885 let mut bloq = Bloq::new();
2886 let mut circuit = CoordCircuit::new();
2887 circuit.do_gate(GateType::H, [IVec2::ZERO]).unwrap();
2888 circuit
2889 .do_gate(GateType::CX, [IVec2::ZERO, IVec2::X])
2890 .unwrap();
2891 let template = bloq.add_template(BloqTemplate::new(circuit));
2892 let owner = bloq.add_node(quantum_node(template, 0, IVec2::ZERO));
2893 for (index, basis) in [CircuitPauli::X, CircuitPauli::Z].into_iter().enumerate() {
2894 let operators = [IVec2::ZERO, IVec2::X]
2895 .into_iter()
2896 .map(|coord| InstanceBoundaryOperator {
2897 instance: TemplateInstanceId(0),
2898 face: BoundaryFace::Output,
2899 operator: [(coord, basis)].into_iter().collect(),
2900 })
2901 .collect();
2902 let include = bloq.add_node(BloqNode::classical(
2903 ClassicalNode::observable_fragment(Vec::new(), operators),
2904 ));
2905 let observable =
2906 bloq.add_node(BloqNode::classical(ClassicalNode::observable(index as u32)));
2907 bloq.add_edge(owner, include, BloqEdge::Order);
2908 bloq.add_edge(include, observable, BloqEdge::compose(0));
2909 }
2910 if reuse {
2911 let template = one_qubit_template(&mut bloq, GateType::RZ);
2912 let reset = bloq.add_node(quantum_node(template, 1, IVec2::ZERO));
2913 bloq.add_edge(owner, reset, BloqEdge::Order);
2914 }
2915 let report = run_bloq(&bloq, 64, 0).unwrap();
2916 for observable in report.observables {
2917 assert_eq!(
2918 observable.per_shot, [false; 64],
2919 "both Bell stabilizers remain +1 (reuse={reuse})"
2920 );
2921 }
2922 }
2923 }
2924
2925 #[test]
2926 fn repeated_output_captures_use_owner_cuts_without_observables() {
2927 let mut bloq = Bloq::new();
2928 let mut previous = None;
2929 let mut outputs = Vec::new();
2930 for z in 0..3 {
2931 let mut circuit = CoordCircuit::new();
2932 circuit.do_gate(GateType::RZ, [IVec2::ZERO]).unwrap();
2933 circuit.do_gate(GateType::X, [IVec2::ZERO]).unwrap();
2934 let template = bloq.add_template(BloqTemplate::new(circuit));
2935 let instance = TemplateInstanceId(z as u32);
2936 let owner = bloq.add_node(quantum_node(template, instance.0, IVec2::ZERO));
2937 if let Some(previous) = previous {
2938 bloq.add_edge(previous, owner, BloqEdge::Order);
2939 }
2940 previous = Some(owner);
2941 let port = IVec3::new(0, 0, z);
2942 outputs.push(bloq_ir::LogicalOutput {
2943 port,
2944 instance,
2945 x: [(IVec2::ZERO, CircuitPauli::X)].into_iter().collect(),
2946 z: [(IVec2::ZERO, CircuitPauli::Z)].into_iter().collect(),
2947 });
2948 for basis in [bloq_ir::Basis::X, bloq_ir::Basis::Z] {
2949 bloq.add_node(
2950 BloqNode::classical(ClassicalNode::Compute {
2951 expr: ClassicalExpr::Const(false),
2952 })
2953 .with_provenance(bloq_ir::NodeProvenance::OutputFrame { port, basis }),
2954 );
2955 }
2956 }
2957 bloq.set_logical_outputs(outputs);
2958 let bloq = Bloq::from_binary(&bloq.to_binary()).unwrap();
2960 let bloq = Bloq::from_text(&bloq.to_text()).unwrap();
2961 run_bloq_with_captured_outputs(&bloq, 1, 0, |sim, context| {
2962 assert_eq!(context.saved_outputs.len(), 2);
2963 for saved in context.saved_outputs {
2964 let z = PauliString::single(sim.num_qubits(), saved.qubit, Pauli::Z);
2965 assert_eq!(
2966 sim.peek_observable_expectation(&z)?,
2967 -1.0,
2968 "output {} was prepared in |1>",
2969 saved.port.z
2970 );
2971 }
2972 Ok(())
2973 })
2974 .unwrap();
2975 }
2976
2977 #[test]
2978 fn report_requires_a_complete_frame_pair_for_each_logical_output() {
2979 let mut bloq = Bloq::new();
2980 let template = one_qubit_template(&mut bloq, GateType::RZ);
2981 bloq.add_node(quantum_node(template, 0, IVec2::ZERO));
2982 let port = IVec3::ZERO;
2983 bloq.set_logical_outputs(vec![bloq_ir::LogicalOutput {
2984 port,
2985 instance: TemplateInstanceId(0),
2986 x: [(IVec2::ZERO, CircuitPauli::X)].into_iter().collect(),
2987 z: [(IVec2::ZERO, CircuitPauli::Z)].into_iter().collect(),
2988 }]);
2989 bloq.add_node(
2990 BloqNode::classical(ClassicalNode::Compute {
2991 expr: ClassicalExpr::Const(false),
2992 })
2993 .with_provenance(bloq_ir::NodeProvenance::OutputFrame {
2994 port,
2995 basis: bloq_ir::Basis::X,
2996 }),
2997 );
2998 assert!(matches!(
2999 run_bloq(&bloq, 1, 0),
3000 Err(ExecError::MalformedGraph(
3001 "logical output has no complete frame pair"
3002 ))
3003 ));
3004 }
3005
3006 #[test]
3007 fn output_capture_requires_a_selected_owner() {
3008 for take_other_arm in [false, true] {
3009 let mut bloq = Bloq::new();
3010 let prepared = one_qubit_template(&mut bloq, GateType::X);
3011 let reset = one_qubit_template(&mut bloq, GateType::RZ);
3012 let consumer = bloq.add_node(quantum_node(reset, 2, IVec2::ZERO));
3013 for (instance, template, selected) in
3014 [(0, prepared, !take_other_arm), (1, reset, take_other_arm)]
3015 {
3016 let selector = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
3017 expr: ClassicalExpr::Const(selected),
3018 }));
3019 let owner = bloq.add_node(guarded_node(
3020 quantum_node(template, instance, IVec2::ZERO),
3021 0,
3022 ));
3023 bloq.add_edge(selector, owner, BloqEdge::value(0));
3024 bloq.add_edge(owner, consumer, BloqEdge::Order);
3025 }
3026 let port = IVec3::ZERO;
3027 bloq.set_logical_outputs(vec![bloq_ir::LogicalOutput {
3028 port,
3029 instance: TemplateInstanceId(u32::from(take_other_arm)),
3030 x: [(IVec2::ZERO, CircuitPauli::X)].into_iter().collect(),
3031 z: [(IVec2::ZERO, CircuitPauli::Z)].into_iter().collect(),
3032 }]);
3033 for basis in [bloq_ir::Basis::X, bloq_ir::Basis::Z] {
3034 bloq.add_node(
3035 BloqNode::classical(ClassicalNode::Compute {
3036 expr: ClassicalExpr::Const(false),
3037 })
3038 .with_provenance(bloq_ir::NodeProvenance::OutputFrame { port, basis }),
3039 );
3040 }
3041 run_bloq_with_captured_outputs(&bloq, 1, 0, |sim, context| {
3042 assert_eq!(context.saved_outputs.len(), 1);
3043 let saved = &context.saved_outputs[0];
3044 let z = PauliString::single(sim.num_qubits(), saved.qubit, Pauli::Z);
3045 assert_eq!(
3046 sim.peek_observable_expectation(&z)?,
3047 if take_other_arm { 1.0 } else { -1.0 },
3048 );
3049 Ok(())
3050 })
3051 .unwrap();
3052
3053 let mut outputs = bloq.logical_outputs().to_vec();
3056 outputs[0].instance = TemplateInstanceId(u32::from(!take_other_arm));
3057 bloq.set_logical_outputs(outputs);
3058 assert!(matches!(
3059 bloq.validate(),
3060 Err(
3061 bloq_ir::BloqValidationError::InvalidInstanceMergeStructure {
3062 source:
3063 bloq_ir::lowering::NodeTemplateInstanceMergeError::InvalidMembership(_),
3064 ..
3065 }
3066 )
3067 ));
3068 assert!(matches!(
3069 run_bloq_with_captured_outputs(&bloq, 1, 0, |_, _| unreachable!()),
3070 Err(ExecError::MalformedGraph(
3071 "logical output owner is not selected"
3072 ))
3073 ));
3074 }
3075 }
3076
3077 #[test]
3078 fn early_output_reads_keep_identical_activated_bindings_distinct() {
3079 let mut bloq = Bloq::new();
3080 let template = one_qubit_template(&mut bloq, GateType::X);
3081 let owner = bloq.add_node(quantum_node(template, 0, IVec2::ZERO));
3082 let observable = bloq.add_node(BloqNode::classical(ClassicalNode::observable(0)));
3083 let template = one_qubit_template(&mut bloq, GateType::RZ);
3084 let reset = bloq.add_node(quantum_node(template, 1, IVec2::ZERO));
3085 for (slot, selected) in [false, true].into_iter().enumerate() {
3086 let selector = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
3087 expr: ClassicalExpr::Const(selected),
3088 }));
3089 let mut fragment = BloqNode::classical(ClassicalNode::observable_fragment(
3090 Vec::new(),
3091 vec![InstanceBoundaryOperator {
3092 instance: TemplateInstanceId(0),
3093 face: BoundaryFace::Output,
3094 operator: [(IVec2::ZERO, CircuitPauli::Z)].into_iter().collect(),
3095 }],
3096 ));
3097 fragment.activation = Some(0);
3098 let region = bloq.add_node(fragment);
3099 bloq.add_edge(owner, region, BloqEdge::Order);
3100 bloq.add_edge(selector, region, BloqEdge::value(0));
3101 bloq.add_edge(region, reset, BloqEdge::Order);
3102 bloq.add_edge(region, observable, BloqEdge::compose(slot as u32));
3103 }
3104 bloq.add_edge(reset, observable, BloqEdge::Order);
3105 let report = run_bloq(&bloq, 8, 0).unwrap();
3106 assert_eq!(report.observables[0].per_shot, [true; 8]);
3107 }
3108
3109 #[test]
3110 fn anticommuting_partial_output_reads_fail_before_projection() {
3111 let mut bloq = Bloq::new();
3112 let template = one_qubit_template(&mut bloq, GateType::H);
3113 let owner = bloq.add_node(quantum_node(template, 0, IVec2::ZERO));
3114 let template = one_qubit_template(&mut bloq, GateType::RZ);
3115 let reset = bloq.add_node(quantum_node(template, 1, IVec2::ZERO));
3116 let future = bloq.add_node(quantum_node(template, 2, IVec2::X));
3117 bloq.add_edge(owner, reset, BloqEdge::Order);
3118 bloq.add_edge(reset, future, BloqEdge::Order);
3119 for (index, basis) in [CircuitPauli::X, CircuitPauli::Z].into_iter().enumerate() {
3120 let include = bloq.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
3121 Vec::new(),
3122 [(0, IVec2::ZERO), (2, IVec2::X)]
3123 .into_iter()
3124 .map(|(instance, coord)| InstanceBoundaryOperator {
3125 instance: TemplateInstanceId(instance),
3126 face: BoundaryFace::Output,
3127 operator: [(coord, basis)].into_iter().collect(),
3128 })
3129 .collect(),
3130 )));
3131 let observable =
3132 bloq.add_node(BloqNode::classical(ClassicalNode::observable(index as u32)));
3133 bloq.add_edge(future, include, BloqEdge::Order);
3134 bloq.add_edge(include, observable, BloqEdge::compose(0));
3135 }
3136 for result in [
3137 run_bloq(&bloq, 1, 0),
3138 run_bloq_with_captured_outputs(&bloq, 1, 0, |_, _| Ok(())),
3139 ] {
3140 assert!(matches!(
3141 result,
3142 Err(ExecError::IncompatibleEarlyOutputReads { .. })
3143 ));
3144 }
3145 }
3146
3147 #[test]
3148 fn joint_boundary_products_keep_their_pauli_sign() {
3149 for reuse in [false, true] {
3150 for split_includes in [false, true] {
3151 let mut bloq = Bloq::new();
3152 let mut circuit = CoordCircuit::new();
3153 circuit.do_gate(GateType::H, [IVec2::ZERO]).unwrap();
3154 circuit
3155 .do_gate(GateType::CX, [IVec2::ZERO, IVec2::X])
3156 .unwrap();
3157 let template = bloq.add_template(BloqTemplate::new(circuit));
3158 let owner = bloq.add_node(quantum_node(template, 0, IVec2::ZERO));
3159 let observable = bloq.add_node(BloqNode::classical(ClassicalNode::observable(0)));
3160 let operators =
3161 [CircuitPauli::X, CircuitPauli::Z].map(|basis| InstanceBoundaryOperator {
3162 instance: TemplateInstanceId(0),
3163 face: BoundaryFace::Output,
3164 operator: [(IVec2::ZERO, basis), (IVec2::X, basis)]
3165 .into_iter()
3166 .collect(),
3167 });
3168 let includes = if split_includes {
3169 operators
3170 .into_iter()
3171 .map(|operator| vec![operator])
3172 .collect::<Vec<_>>()
3173 } else {
3174 vec![operators.into_iter().collect()]
3175 };
3176 for (slot, operators) in includes.into_iter().enumerate() {
3177 let include = bloq.add_node(BloqNode::classical(
3178 ClassicalNode::observable_fragment(Vec::new(), operators),
3179 ));
3180 bloq.add_edge(owner, include, BloqEdge::Order);
3181 bloq.add_edge(include, observable, BloqEdge::compose(slot as u32));
3182 }
3183 if reuse {
3184 let template = one_qubit_template(&mut bloq, GateType::RZ);
3185 let reset = bloq.add_node(quantum_node(template, 1, IVec2::ZERO));
3186 bloq.add_edge(owner, reset, BloqEdge::Order);
3187 bloq.add_edge(reset, observable, BloqEdge::Order);
3188 }
3189 let report = run_bloq(&bloq, 8, 0).unwrap();
3190 assert_eq!(
3191 report.observables[0].per_shot, [false; 8],
3192 "Bell XX * ZZ = -YY has eigenvalue +1 (reuse={reuse}, split={split_includes})"
3193 );
3194 }
3195 }
3196 }
3197}