1use crate::emit::{
2 ResolvedLoopStates, StimAnnotationScope, StimCircuitAnnotations, StimDetector,
3 StimEmissionError, StimEmitContext, StimEmitOptions, StimMeasurementMapper, StimRepeatState,
4 emit_stim_circuit, first_unsupported_gate,
5};
6use crate::layout::{QubitLayout, coordinate_index, detector_index};
7use crate::measurement_frame::{MeasurementFrame, stim_record_lookback};
8use crate::text_utils::push_int;
9use bloq_circuit::{
10 Basis, BodyId, CircuitError, CoordCircuit, FlowMarker, GateType, LoopStateId, NoiseModel, Op,
11 Pauli, PauliMap,
12};
13use bloq_ir::{
14 Bloq, BloqNode, BloqNodeId, BloqNodeKind, BodySelector, ClassicalAssignment, ClassicalNode,
15 LevelPath, MomentLane, RegionNode, SubGraph, TemplateId, ValidatedPlans, align_moment_lanes,
16 aligned_moment_segments,
17 lowering::{
18 InstanceMeasurement, InstantiationOptions, NodeEmissionPlan, TemplateDetectorScope,
19 TemplateInstanceId,
20 },
21};
22use glam::IVec2;
23use rustc_hash::{FxHashMap, FxHashSet};
24use std::{
25 borrow::Cow,
26 collections::{BTreeMap, BTreeSet},
27};
28
29#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct IsolatedTFrameRecipe {
33 pub measurements: Vec<u32>,
35 pub sign: bool,
37 pub gap_observable: u32,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub struct IsolatedTFrontierSheet {
44 pub observable: u32,
46 pub basis: Basis,
48 pub sign: bool,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct IsolatedTAttemptManifest {
55 pub rus_path: LevelPath,
57 pub detector_signs: Vec<bool>,
59 pub postselection_detectors: Vec<u32>,
61 pub gap_x_observable: u32,
63 pub gap_z_observable: u32,
65 pub frame_x: IsolatedTFrameRecipe,
67 pub frame_z: IsolatedTFrameRecipe,
69 pub frontier_sheets: Vec<IsolatedTFrontierSheet>,
72 pub exp_val_signs: [i8; 3],
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct IsolatedTAttemptArtifacts {
79 pub physical_s: String,
81 pub physical_t: String,
83 pub companion: String,
85 pub sheets: String,
92 pub manifest: IsolatedTAttemptManifest,
94}
95
96#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
107pub enum InputTrust {
108 #[default]
110 Trusted,
111 Checked,
113}
114
115#[derive(Debug, Clone, Copy, Default)]
137pub struct BloqStimOptions<'a> {
138 noise: Option<&'a NoiseModel>,
139 trust: InputTrust,
140 align_moments: bool,
141}
142
143impl<'a> BloqStimOptions<'a> {
144 #[must_use]
146 pub fn new() -> Self {
147 Self::default()
148 }
149
150 #[must_use]
152 pub fn with_noise(mut self, noise: &'a NoiseModel) -> Self {
153 self.noise = Some(noise);
154 self
155 }
156
157 #[must_use]
159 pub fn with_trust(mut self, trust: InputTrust) -> Self {
160 self.trust = trust;
161 self
162 }
163
164 #[must_use]
166 pub fn with_align_moments(mut self, align: bool) -> Self {
167 self.align_moments = align;
168 self
169 }
170}
171
172pub fn emit_bloq_stim(program: &Bloq) -> Result<String, StimEmissionError> {
183 emit_bloq_stim_with(program, &BloqStimOptions::new())
184}
185
186pub fn emit_bloq_stim_with(
195 program: &Bloq,
196 options: &BloqStimOptions<'_>,
197) -> Result<String, StimEmissionError> {
198 if options.align_moments {
199 if options.noise.is_some() {
200 return Err(StimEmissionError::AlignMomentsWithNoise);
201 }
202 if options.trust == InputTrust::Checked {
203 program.validate()?;
204 }
205 reject_unsupported(program)?;
206 let mut flat = program.clone();
207 flat.flatten()?;
208 let plans = flat.emission_plans(&InstantiationOptions::default())?;
209 let order = flat.deterministic_emit_order()?;
210 return BloqStimEmitter::new(&flat, None, &plans).emit_order_aligned(&order);
211 }
212 let (plans, order) = prepare_untransformed(program, options)?;
213 BloqStimEmitter::new(program, options.noise, &plans).emit_order_untransformed(&order)
214}
215
216fn prepare_untransformed(
222 program: &Bloq,
223 options: &BloqStimOptions<'_>,
224) -> Result<(ValidatedPlans, Vec<BloqNodeId>), StimEmissionError> {
225 if options.trust == InputTrust::Trusted {
226 reject_unsupported(program)?;
227 }
228 let plans = emission_plans(program, options.noise, options.trust)?;
229 if options.trust == InputTrust::Checked {
230 reject_unsupported(program)?;
231 }
232 Ok((plans, program.deterministic_emit_order()?))
233}
234
235pub fn emit_isolated_t_attempts(
248 program: &Bloq,
249 physical_error_probability: f64,
250) -> Result<IsolatedTAttemptArtifacts, StimEmissionError> {
251 if !physical_error_probability.is_finite() || !(0.0..=1.0).contains(&physical_error_probability)
252 {
253 return Err(StimEmissionError::InvalidNoiseProbability);
254 }
255
256 let mut flat = program.clone();
257 flat.flatten()?;
258 let plans = emission_plans(&flat, None, InputTrust::default())?;
261 let path = isolated_rus_path(&flat)?;
262 let body = flat
263 .level_at(&path)
264 .ok_or(StimEmissionError::MalformedGraph(
265 "isolated-T RUS body path is stale",
266 ))?;
267 let order = body.deterministic_emit_order()?;
268 let cultivation = isolated_cultivation_node(&flat, body)?;
269 let terminal = order
270 .iter()
271 .rev()
272 .copied()
273 .find(|&node| body[node].try_quantum().is_some())
274 .ok_or(StimEmissionError::MalformedGraph(
275 "isolated-T RUS body has no quantum node",
276 ))?;
277 let (gap_x, gap_z, gap_records) = isolated_gap_observables(body)?;
278 let frames = flat.output_frames();
279 if frames.len() != 1 || flat.logical_outputs().len() != 1 {
280 return Err(StimEmissionError::MalformedGraph(
281 "isolated-T program must have exactly one output frame and logical output",
282 ));
283 }
284
285 let noise = NoiseModel::uniform_depolarizing(physical_error_probability);
286 let render = |mode| {
287 BloqStimEmitter::new_attempt(&flat, body, path.clone(), &noise, mode, &plans)
288 .emit_order_attempt(&order)
289 };
290 let mut physical_s = render(AttemptMode::SControl)?;
291 let mut physical_t = render(AttemptMode::HonestT)?;
292 let companion = render(AttemptMode::Companion { cultivation })?;
293 ensure_attempt_alignment(&physical_s, &physical_t, &companion)?;
294
295 let frame_x = isolated_frame_recipe(
296 flat.top(),
297 frames[0].x,
298 &companion.measurement_columns,
299 &gap_records,
300 )?;
301 let frame_z = isolated_frame_recipe(
302 flat.top(),
303 frames[0].z,
304 &companion.measurement_columns,
305 &gap_records,
306 )?;
307 if frame_x.gap_observable != gap_z || frame_z.gap_observable != gap_x {
308 return Err(StimEmissionError::MalformedGraph(
309 "isolated-T output frames do not cross-map X to GAP-Z and Z to GAP-X",
310 ));
311 }
312
313 let exp_val_signs = append_direct_exp_vals(
314 &mut physical_s.text,
315 &flat,
316 &flat.logical_outputs()[0].x,
317 &flat.logical_outputs()[0].z,
318 )?;
319 let t_signs = append_direct_exp_vals(
320 &mut physical_t.text,
321 &flat,
322 &flat.logical_outputs()[0].x,
323 &flat.logical_outputs()[0].z,
324 )?;
325 if t_signs != exp_val_signs {
326 return Err(StimEmissionError::MalformedGraph(
327 "isolated-T S/T probe signs disagree",
328 ));
329 }
330
331 let mut sheets = String::new();
332 let frontier_sheets = append_frontier_sheets(
333 &mut sheets,
334 &flat,
335 body,
336 terminal,
337 &companion.measurement_columns,
338 )?;
339 let detector_signs = companion
340 .detectors
341 .iter()
342 .map(|detector| detector.sign)
343 .collect();
344 let postselection_detectors = companion
345 .detectors
346 .iter()
347 .enumerate()
348 .filter(|(_, detector)| detector.postselection)
349 .map(|(index, _)| detector_index(index))
350 .collect::<Result<_, _>>()?;
351
352 Ok(IsolatedTAttemptArtifacts {
353 physical_s: physical_s.text,
354 physical_t: physical_t.text,
355 companion: companion.text,
356 sheets,
357 manifest: IsolatedTAttemptManifest {
358 rus_path: path,
359 detector_signs,
360 postselection_detectors,
361 gap_x_observable: gap_x,
362 gap_z_observable: gap_z,
363 frame_x,
364 frame_z,
365 frontier_sheets,
366 exp_val_signs,
367 },
368 })
369}
370
371fn isolated_rus_path(program: &Bloq) -> Result<LevelPath, StimEmissionError> {
372 let mut rus = program.nodes().filter_map(|(id, node)| {
373 matches!(
374 node.try_region(),
375 Some(RegionNode::RepeatUntilSuccess { .. })
376 )
377 .then_some(id)
378 });
379 let id = rus.next().ok_or(StimEmissionError::MalformedGraph(
380 "isolated-T program has no top-level RepeatUntilSuccess",
381 ))?;
382 if rus.next().is_some() {
383 return Err(StimEmissionError::MalformedGraph(
384 "isolated-T program has more than one top-level RepeatUntilSuccess",
385 ));
386 }
387 Ok(LevelPath::default().child(id, BodySelector::Body))
388}
389
390fn isolated_cultivation_node(
391 program: &Bloq,
392 body: &SubGraph,
393) -> Result<BloqNodeId, StimEmissionError> {
394 let mut cultivation = None;
395 let mut quantum_count = 0;
396 for (node, quantum) in body.quantum_nodes() {
397 quantum_count += 1;
398 if quantum.instances.len() != 1 {
399 return Err(StimEmissionError::MalformedGraph(
400 "isolated-T body quantum nodes must have one template instance",
401 ));
402 }
403 let template = program
404 .templates()
405 .get(quantum.instances[0].template_id)
406 .ok_or(StimEmissionError::MalformedGraph(
407 "isolated-T body instance references a missing template",
408 ))?;
409 if first_unsupported_gate(&template.circuit).is_some()
410 && cultivation.replace(node).is_some()
411 {
412 return Err(StimEmissionError::MalformedGraph(
413 "isolated-T body has more than one non-Clifford stage",
414 ));
415 }
416 }
417 if quantum_count < 2 {
418 return Err(StimEmissionError::MalformedGraph(
419 "isolated-T body must contain cultivation and escape stages",
420 ));
421 }
422 cultivation.ok_or(StimEmissionError::MalformedGraph(
423 "isolated-T body has no non-Clifford cultivation stage",
424 ))
425}
426
427type GapRecordSets = BTreeMap<u32, BTreeSet<InstanceMeasurement>>;
428
429fn isolated_gap_observables(
430 body: &SubGraph,
431) -> Result<(u32, u32, GapRecordSets), StimEmissionError> {
432 let mut indices = BTreeSet::new();
433 for (_, node) in body.nodes() {
434 if let Some(ClassicalNode::Observable {
435 index: Some(index), ..
436 }) = node.try_classical()
437 {
438 indices.insert(*index);
439 }
440 }
441 if indices.len() != 2 {
442 return Err(StimEmissionError::MalformedGraph(
443 "isolated-T body must contain two GAP observables",
444 ));
445 }
446 let mut gap_x = None;
447 let mut gap_z = None;
448 let mut records = BTreeMap::new();
449 for index in indices {
450 let observable = observable_node(body, index)?;
451 let measurements = body
452 .resolve_classical(observable, ClassicalAssignment::Uniform(false))?
453 .measurements;
454 records.insert(index, measurements);
455 match observable_boundary_basis(body, observable)? {
456 Basis::X if gap_x.replace(index).is_none() => {}
457 Basis::Z if gap_z.replace(index).is_none() => {}
458 _ => {
459 return Err(StimEmissionError::MalformedGraph(
460 "isolated-T GAP observables must have one X and one Z sheet",
461 ));
462 }
463 }
464 }
465 Ok((
466 gap_x.expect("two checked GAP bases include X"),
467 gap_z.expect("two checked GAP bases include Z"),
468 records,
469 ))
470}
471
472fn observable_node(level: &SubGraph, index: u32) -> Result<BloqNodeId, StimEmissionError> {
473 let mut nodes = level.nodes().filter_map(|(id, node)| {
474 matches!(
475 node.try_classical(),
476 Some(ClassicalNode::Observable { index: Some(candidate), .. }) if *candidate == index
477 )
478 .then_some(id)
479 });
480 let node = nodes.next().ok_or(StimEmissionError::MalformedGraph(
481 "decoder references a missing isolated-T observable",
482 ))?;
483 if nodes.next().is_some() {
484 return Err(StimEmissionError::MalformedGraph(
485 "isolated-T graph repeats an observable index in one level",
486 ));
487 }
488 Ok(node)
489}
490
491fn observable_boundary_basis(
492 level: &SubGraph,
493 observable: BloqNodeId,
494) -> Result<Basis, StimEmissionError> {
495 let mut basis = None;
496 let mut stack = vec![observable];
497 let mut visited = FxHashSet::default();
498 while let Some(producer) = stack.pop() {
499 if !visited.insert(producer) {
500 continue;
501 }
502 let Some(ClassicalNode::Observable { operators, .. }) = level[producer].try_classical()
503 else {
504 continue;
505 };
506 stack.extend(
507 level
508 .data_inputs(producer)
509 .filter(|input| input.output.is_none())
510 .map(|input| input.producer),
511 );
512 for operator in operators {
513 for (_, pauli) in &operator.operator {
514 let next = match pauli {
515 Pauli::X => Basis::X,
516 Pauli::Z => Basis::Z,
517 Pauli::I | Pauli::Y => {
518 return Err(StimEmissionError::MalformedGraph(
519 "isolated-T boundary sheet is not homogeneous X or Z",
520 ));
521 }
522 };
523 if basis.is_some_and(|current| current != next) {
524 return Err(StimEmissionError::MalformedGraph(
525 "isolated-T boundary sheet mixes X and Z support",
526 ));
527 }
528 basis = Some(next);
529 }
530 }
531 }
532 basis.ok_or(StimEmissionError::MalformedGraph(
533 "isolated-T GAP observable has no boundary sheet",
534 ))
535}
536
537fn isolated_frame_recipe(
538 top: &SubGraph,
539 frame: BloqNodeId,
540 columns: &FxHashMap<InstanceMeasurement, u32>,
541 gap_records: &GapRecordSets,
542) -> Result<IsolatedTFrameRecipe, StimEmissionError> {
543 let value = top.resolve_classical(frame, ClassicalAssignment::Uniform(false))?;
544 let records = value.measurements;
545 let gap_observable = gap_records
546 .iter()
547 .find_map(|(&index, candidate)| (candidate == &records).then_some(index))
548 .ok_or(StimEmissionError::MalformedGraph(
549 "isolated-T output frame records do not match either GAP observable",
550 ))?;
551 let mut measurements = records
552 .iter()
553 .map(|measurement| {
554 columns
555 .get(measurement)
556 .copied()
557 .ok_or(StimEmissionError::MalformedGraph(
558 "isolated-T frame references a measurement outside the RUS body",
559 ))
560 })
561 .collect::<Result<Vec<_>, _>>()?;
562 measurements.sort_unstable();
563 Ok(IsolatedTFrameRecipe {
564 measurements,
565 sign: value.sign,
566 gap_observable,
567 })
568}
569
570fn ensure_attempt_alignment(
571 physical_s: &AttemptStimRender,
572 physical_t: &AttemptStimRender,
573 companion: &AttemptStimRender,
574) -> Result<(), StimEmissionError> {
575 if physical_s.measurement_columns != physical_t.measurement_columns
576 || physical_s.measurement_columns != companion.measurement_columns
577 || physical_s.detectors != physical_t.detectors
578 || physical_s.detectors != companion.detectors
579 {
580 return Err(StimEmissionError::MalformedGraph(
581 "isolated-T S, T, and companion measurement/detector recipes disagree",
582 ));
583 }
584 Ok(())
585}
586
587fn append_frontier_sheets(
588 output: &mut String,
589 program: &Bloq,
590 body: &SubGraph,
591 terminal: BloqNodeId,
592 columns: &FxHashMap<InstanceMeasurement, u32>,
593) -> Result<Vec<IsolatedTFrontierSheet>, StimEmissionError> {
594 let quantum = body[terminal].expect_quantum();
595 let instance = quantum
596 .instances
597 .first()
598 .ok_or(StimEmissionError::MalformedGraph(
599 "isolated-T terminal node has no template instance",
600 ))?;
601 let template =
602 program
603 .templates()
604 .get(instance.template_id)
605 .ok_or(StimEmissionError::MalformedGraph(
606 "isolated-T terminal instance references a missing template",
607 ))?;
608 let measurement_count = columns
609 .values()
610 .copied()
611 .max()
612 .and_then(|last| last.checked_add(1))
613 .ok_or(StimEmissionError::MalformedGraph(
614 "isolated-T attempt emitted no measurements",
615 ))?;
616 let coords = program.sorted_layout_coords()?;
617 let layout = QubitLayout::new(coordinate_index(coords.iter().copied())?)?;
618 let max_observable = program
619 .levels()
620 .flat_map(|(_, level)| level.nodes())
621 .filter_map(|(_, node)| match node.try_classical() {
622 Some(ClassicalNode::Observable { index, .. }) => *index,
623 _ => None,
624 })
625 .max();
626 let mut next_observable = match max_observable {
627 Some(index) => index
628 .checked_add(1)
629 .ok_or(StimEmissionError::MalformedGraph(
630 "isolated-T observable id overflow",
631 ))?,
632 None => 0,
633 };
634 let mut sheets = Vec::new();
635 for flow in template.boundary_flows.iter().filter(|flow| {
636 flow.start.is_empty() && !flow.end.is_empty() && flow.marker == FlowMarker::Detector
637 }) {
638 let basis = homogeneous_basis(&flow.end)?;
639 let mut lookbacks = flow
640 .measurements
641 .iter()
642 .map(|&measurement| {
643 let column = columns
644 .get(&InstanceMeasurement {
645 instance: instance.id,
646 measurement,
647 })
648 .copied()
649 .ok_or(StimEmissionError::MalformedGraph(
650 "terminal frontier references an unemitted measurement",
651 ))?;
652 let lookback = measurement_count - column;
653 stim_record_lookback(lookback).ok_or(StimEmissionError::MalformedGraph(
654 "terminal frontier measurement lookback exceeds Stim range",
655 ))
656 })
657 .collect::<Result<Vec<_>, _>>()?;
658 lookbacks.sort_unstable();
659 crate::emit::emit_observable_include_records(output, next_observable, &lookbacks);
660 let operator = flow.end.try_translated(instance.offset)?;
661 crate::emit::emit_observable_include_pauli_targets(
662 output,
663 next_observable,
664 &operator,
665 &layout,
666 )?;
667 sheets.push(IsolatedTFrontierSheet {
668 observable: next_observable,
669 basis,
670 sign: flow.sign,
671 });
672 next_observable =
673 next_observable
674 .checked_add(1)
675 .ok_or(StimEmissionError::MalformedGraph(
676 "isolated-T observable id overflow",
677 ))?;
678 }
679 if sheets.is_empty() {
680 return Err(StimEmissionError::MalformedGraph(
681 "isolated-T terminal node has no open detector frontier",
682 ));
683 }
684 Ok(sheets)
685}
686
687fn homogeneous_basis(operator: &PauliMap) -> Result<Basis, StimEmissionError> {
688 let mut basis = None;
689 for (_, pauli) in operator {
690 let next = match pauli {
691 Pauli::X => Basis::X,
692 Pauli::Z => Basis::Z,
693 Pauli::I | Pauli::Y => {
694 return Err(StimEmissionError::MalformedGraph(
695 "terminal frontier is not homogeneous X or Z",
696 ));
697 }
698 };
699 if basis.is_some_and(|current| current != next) {
700 return Err(StimEmissionError::MalformedGraph(
701 "terminal frontier mixes X and Z support",
702 ));
703 }
704 basis = Some(next);
705 }
706 basis.ok_or(StimEmissionError::MalformedGraph(
707 "terminal frontier has empty Pauli support",
708 ))
709}
710
711fn append_direct_exp_vals(
712 output: &mut String,
713 program: &Bloq,
714 logical_x: &PauliMap,
715 logical_z: &PauliMap,
716) -> Result<[i8; 3], StimEmissionError> {
717 let coords = program.sorted_layout_coords()?;
718 let layout = coordinate_index(coords.iter().copied())?;
719 let (logical_y, y_sign) = signed_logical_y(logical_x, logical_z)?;
720 for operator in [logical_x, &logical_y, logical_z] {
721 output.push_str("EXP_VAL ");
722 let mut first = true;
723 for (coord, pauli) in operator {
724 if !first {
725 output.push('*');
726 }
727 first = false;
728 output.push_str(&pauli.to_string());
729 push_int(
730 output,
731 *layout.get(coord).ok_or(StimEmissionError::MalformedGraph(
732 "logical output probe names a qubit outside the global layout",
733 ))?,
734 );
735 }
736 if first {
737 return Err(StimEmissionError::MalformedGraph(
738 "logical output probe has empty Pauli support",
739 ));
740 }
741 output.push('\n');
742 }
743 Ok([1, y_sign, 1])
744}
745
746fn signed_logical_y(
747 logical_x: &PauliMap,
748 logical_z: &PauliMap,
749) -> Result<(PauliMap, i8), StimEmissionError> {
750 let phase = (1 + logical_x.product_phase(logical_z)) % 4;
752 let sign = match phase {
753 0 => 1,
754 2 => -1,
755 _ => {
756 return Err(StimEmissionError::MalformedGraph(
757 "logical iXZ probe is not Hermitian",
758 ));
759 }
760 };
761 Ok((logical_x ^ logical_z, sign))
762}
763
764fn emission_plans(
770 program: &Bloq,
771 noise: Option<&NoiseModel>,
772 trust: InputTrust,
773) -> Result<ValidatedPlans, StimEmissionError> {
774 let options = noise.map_or_else(InstantiationOptions::default, InstantiationOptions::noisy);
775 if trust == InputTrust::Checked {
776 return Ok(program.validate_with_plans(&options)?);
777 }
778 Ok(program.emission_plans(&options)?)
779}
780
781fn reject_unsupported(program: &Bloq) -> Result<(), StimEmissionError> {
786 if let Some(node) = first_unsupported_program_node(program) {
794 return Err(StimEmissionError::UnsupportedNode(node));
795 }
796 if let Some(gate) = first_unsupported_program_gate(program) {
797 return Err(StimEmissionError::UnsupportedGate(gate));
798 }
799 Ok(())
800}
801
802fn first_unsupported_program_node(program: &Bloq) -> Option<&'static str> {
805 program
806 .nodes()
807 .find_map(|(_, node)| stim_unsupported_reason(node))
808 .or_else(|| {
809 program
810 .has_conditional_membership()
811 .then_some("conditional quantum seams")
812 })
813}
814
815fn stim_unsupported_reason(node: &BloqNode) -> Option<&'static str> {
819 if node.activation.is_some() {
820 return Some("activation (conditional execution)");
821 }
822 if node
823 .try_quantum()
824 .is_some_and(|quantum| !quantum.guards.is_empty())
825 {
826 return Some("conditional component membership");
827 }
828 match (node.try_classical(), node.try_region()) {
829 (Some(ClassicalNode::Compute { expr }), _) if !expr.is_linear() => {
843 Some("Compute (nonlinear And/Or condition requires conditional execution)")
844 }
845 (Some(ClassicalNode::Discard { .. }), _) => Some("Discard (shot postselection)"),
849 (_, Some(_)) => Some("RepeatUntilSuccess (postselected retry)"),
850 _ => None,
851 }
852}
853
854#[derive(Debug, Clone, PartialEq, Eq)]
861pub struct StimSegment {
862 pub node_id: BloqNodeId,
864 pub text: String,
866 pub measurement_start: usize,
869 pub measurement_count: usize,
871}
872
873#[derive(Debug, Clone)]
879pub struct BloqStimSegments {
880 pub header: String,
882 pub segments: Vec<StimSegment>,
884}
885
886impl BloqStimSegments {
887 pub fn to_text(&self) -> String {
891 let capacity = self.header.len()
892 + self
893 .segments
894 .iter()
895 .map(|segment| segment.text.len())
896 .sum::<usize>();
897 let mut text = String::with_capacity(capacity);
898 text.push_str(&self.header);
899 for segment in &self.segments {
900 text.push_str(&segment.text);
901 }
902 text
903 }
904
905 pub fn num_measurements(&self) -> usize {
907 self.segments.last().map_or(0, |segment| {
908 segment.measurement_start + segment.measurement_count
909 })
910 }
911}
912
913pub fn emit_bloq_stim_segments(program: &Bloq) -> Result<BloqStimSegments, StimEmissionError> {
925 emit_bloq_stim_segments_with(program, &BloqStimOptions::new())
926}
927
928pub fn emit_bloq_stim_segments_with(
934 program: &Bloq,
935 options: &BloqStimOptions<'_>,
936) -> Result<BloqStimSegments, StimEmissionError> {
937 if options.align_moments {
938 return Err(StimEmissionError::AlignMomentsWithSegments);
939 }
940 let (plans, order) = prepare_untransformed(program, options)?;
941 BloqStimEmitter::new(program, options.noise, &plans).emit_order_segments(&order)
942}
943
944pub fn emit_bloq_stim_segments_pair(
960 program: &Bloq,
961 noise: &NoiseModel,
962) -> Result<(BloqStimSegments, BloqStimSegments), StimEmissionError> {
963 reject_unsupported(program)?;
964 let order = program.deterministic_emit_order()?;
965 let trust = InputTrust::default();
966
967 let clean_plans = emission_plans(program, None, trust)?;
968 let clean = BloqStimEmitter::new(program, None, &clean_plans).emit_order_segments(&order)?;
969 let noisy_plans = emission_plans(program, Some(noise), trust)?;
970 let noisy =
971 BloqStimEmitter::new(program, Some(noise), &noisy_plans).emit_order_segments(&order)?;
972
973 let aligned = clean.segments.len() == noisy.segments.len()
974 && std::iter::zip(&clean.segments, &noisy.segments).all(|(clean, noisy)| {
975 clean.node_id == noisy.node_id
976 && clean.measurement_start == noisy.measurement_start
977 && clean.measurement_count == noisy.measurement_count
978 });
979 if !aligned {
980 return Err(StimEmissionError::MalformedGraph(
981 "clean and noisy emission disagree on node order or measurement layout",
982 ));
983 }
984 Ok((clean, noisy))
985}
986
987struct BloqStimEmitter<'a> {
988 program: &'a Bloq,
989 level: &'a SubGraph,
990 path: LevelPath,
991 noise: Option<&'a NoiseModel>,
992 attempt: Option<AttemptRender<'a>>,
993 plans: &'a ValidatedPlans,
997 output: String,
998 frame: MeasurementFrame,
999 num_measurements: u32,
1000 layout: QubitLayout,
1001 indexed_coords: &'a [IVec2],
1002 resolved_loop_detector_states: ResolvedLoopStates,
1003 instance_measurements: FxHashMap<InstanceMeasurement, u32>,
1004 instance_templates: FxHashMap<TemplateInstanceId, TemplateId>,
1005 instance_loop_states: FxHashMap<InstanceLoopState, LoopStateId>,
1006 emitted_observables: FxHashSet<u32>,
1007 detector_metadata: Option<Vec<EmittedDetectorMetadata>>,
1008 annotations_scratch: StimCircuitAnnotations,
1010 boundary_faces: FxHashMap<BloqNodeId, BTreeMap<u32, (PauliMap, PauliMap)>>,
1015}
1016
1017struct AlignedNodeEmission {
1018 circuit: CoordCircuit,
1020 moments: Vec<Vec<Op>>,
1021 annotations: StimCircuitAnnotations,
1022 ends_with_tick: bool,
1023 boundary_faces: BTreeMap<u32, (PauliMap, PauliMap)>,
1024}
1025
1026#[derive(Debug, Clone, Copy)]
1027enum AttemptMode {
1028 HonestT,
1029 SControl,
1030 Companion { cultivation: BloqNodeId },
1031}
1032
1033#[derive(Debug, Clone, Copy)]
1034struct AttemptRender<'a> {
1035 mode: AttemptMode,
1036 noise: &'a NoiseModel,
1037}
1038
1039impl AttemptRender<'_> {
1040 fn emits_observables(self) -> bool {
1041 matches!(self.mode, AttemptMode::Companion { .. })
1042 }
1043
1044 fn enables_clifford_proxy(self) -> bool {
1045 !matches!(self.mode, AttemptMode::HonestT)
1046 }
1047
1048 fn emits_postselection_tags(self) -> bool {
1049 matches!(self.mode, AttemptMode::Companion { .. })
1050 }
1051
1052 fn noises_node(self, node: BloqNodeId) -> bool {
1053 !matches!(self.mode, AttemptMode::Companion { cultivation } if cultivation == node)
1054 }
1055}
1056
1057#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1058struct EmittedDetectorMetadata {
1059 sign: bool,
1060 postselection: bool,
1061}
1062
1063struct AttemptStimRender {
1064 text: String,
1065 measurement_columns: FxHashMap<InstanceMeasurement, u32>,
1066 detectors: Vec<EmittedDetectorMetadata>,
1067}
1068
1069impl<'a> BloqStimEmitter<'a> {
1070 fn new(program: &'a Bloq, noise: Option<&'a NoiseModel>, plans: &'a ValidatedPlans) -> Self {
1071 Self::new_level(program, program.top(), LevelPath::default(), noise, plans)
1072 }
1073
1074 fn new_level(
1075 program: &'a Bloq,
1076 level: &'a SubGraph,
1077 path: LevelPath,
1078 noise: Option<&'a NoiseModel>,
1079 plans: &'a ValidatedPlans,
1080 ) -> Self {
1081 Self {
1082 program,
1083 level,
1084 path,
1085 noise,
1086 attempt: None,
1087 plans,
1088 output: String::new(),
1089 frame: MeasurementFrame::default(),
1090 num_measurements: 0,
1091 layout: QubitLayout::default(),
1092 indexed_coords: &[],
1093 resolved_loop_detector_states: ResolvedLoopStates::default(),
1094 instance_measurements: FxHashMap::default(),
1095 instance_templates: FxHashMap::default(),
1096 instance_loop_states: FxHashMap::default(),
1097 emitted_observables: FxHashSet::default(),
1098 detector_metadata: None,
1099 annotations_scratch: StimCircuitAnnotations::default(),
1100 boundary_faces: FxHashMap::default(),
1101 }
1102 }
1103
1104 fn new_attempt(
1105 program: &'a Bloq,
1106 level: &'a SubGraph,
1107 path: LevelPath,
1108 noise: &'a NoiseModel,
1109 mode: AttemptMode,
1110 plans: &'a ValidatedPlans,
1111 ) -> Self {
1112 let mut emitter = Self::new_level(program, level, path, None, plans);
1113 emitter.attempt = Some(AttemptRender { mode, noise });
1114 emitter.detector_metadata = Some(Vec::new());
1115 emitter
1116 }
1117
1118 fn prepare(&mut self) -> Result<(), StimEmissionError> {
1119 self.instance_templates = self
1120 .program
1121 .levels()
1122 .flat_map(|(_, level)| {
1123 level.quantum_nodes().flat_map(|(_, quantum)| {
1124 quantum
1125 .instances
1126 .iter()
1127 .map(|instance| (instance.id, instance.template_id))
1128 })
1129 })
1130 .collect();
1131 self.build_global_layout()?;
1132 self.build_global_measurements()?;
1133 self.build_global_loop_states()?;
1134 self.build_boundary_faces()?;
1135 self.output
1136 .reserve(estimate_bloq_stim_capacity(self.program, self.layout.len()));
1137 self.emit_qubit_coords();
1138 Ok(())
1139 }
1140
1141 fn emit_order_untransformed(
1142 mut self,
1143 order: &[BloqNodeId],
1144 ) -> Result<String, StimEmissionError> {
1145 self.prepare()?;
1146 for id in order {
1147 self.emit_node(*id, &self.level[*id])?;
1148 }
1149 Ok(self.output)
1150 }
1151
1152 fn emit_order_aligned(mut self, order: &[BloqNodeId]) -> Result<String, StimEmissionError> {
1153 self.prepare()?;
1154 let (lanes, mut nodes) = self.prepare_aligned_nodes(order)?;
1155
1156 for layer in align_moment_lanes(self.level, lanes)? {
1157 for slot in layer.slots {
1158 let mut completed = Vec::new();
1159 for entry in slot.entries {
1160 let emission = nodes
1161 .get_mut(&entry.node)
1162 .expect("alignment returns a submitted node");
1163 if self.emit_aligned_moment(entry.node, entry.moment, emission)? {
1164 completed.push(entry.node);
1165 }
1166 }
1167 self.output.push_str("TICK\n");
1168 let mut needs_boundary_tick = false;
1169 for &node_id in &completed {
1170 let emission = nodes.get_mut(&node_id).expect("completed node is prepared");
1171 if !emission.ends_with_tick {
1172 continue;
1173 }
1174 needs_boundary_tick |=
1175 self.emit_aligned_circuit(&emission.circuit, Some(&emission.annotations))?;
1176 }
1177 if needs_boundary_tick {
1178 self.output.push_str("TICK\n");
1179 }
1180 for node_id in completed {
1181 let emission = nodes.get(&node_id).expect("completed node is prepared");
1182 for (index, (_, operator_out)) in &emission.boundary_faces {
1183 self.emit_observable_pauli_line(*index, operator_out)?;
1184 }
1185 }
1186 }
1187 }
1188
1189 for &node_id in order {
1192 if self.level[node_id].try_quantum().is_none() {
1193 self.emit_node(node_id, &self.level[node_id])?;
1194 }
1195 }
1196 Ok(self.output)
1197 }
1198
1199 fn prepare_aligned_nodes(
1200 &mut self,
1201 order: &[BloqNodeId],
1202 ) -> Result<(Vec<MomentLane>, FxHashMap<BloqNodeId, AlignedNodeEmission>), StimEmissionError>
1203 {
1204 let mut lanes = Vec::with_capacity(order.len());
1205 let mut nodes = FxHashMap::with_capacity_and_hasher(order.len(), Default::default());
1206
1207 for &node_id in order {
1208 let node = &self.level[node_id];
1209 let Some(quantum) = node.try_quantum() else {
1210 continue;
1211 };
1212 let plan =
1213 self.plans
1214 .get(&self.path, node_id)
1215 .ok_or(StimEmissionError::MalformedGraph(
1216 "quantum node has no emission plan",
1217 ))?;
1218 let mut circuit = remapped_plan_circuit(&mut self.instance_measurements, plan);
1219
1220 let mut annotations = StimCircuitAnnotations::default();
1221 node_annotations(
1222 &mut annotations,
1223 self.program,
1224 node,
1225 self.program.templates(),
1226 &self.instance_measurements,
1227 &self.instance_loop_states,
1228 NodeAnnotationOptions {
1229 instance_templates: &self.instance_templates,
1230 bodies: Some(&plan.bodies),
1231 include_restarts: false,
1232 },
1233 )?;
1234
1235 let qubits = circuit.qubits().into_iter().collect();
1236 let entry = circuit.entry_body();
1237 let body = circuit
1238 .body_mut(entry)
1239 .expect("entry body is created with the circuit");
1240 let ops = std::mem::take(body.ops_mut());
1241 let ends_with_tick = matches!(ops.last(), Some(Op::Tick));
1242 let segments = aligned_moment_segments(&ops)?;
1243 let (kinds, moments): (Vec<_>, Vec<_>) = segments
1244 .into_iter()
1245 .map(|segment| {
1246 segment.map_or((None, Vec::new()), |segment| {
1247 (Some(segment.kind), segment.ops)
1248 })
1249 })
1250 .unzip();
1251 let boundary_faces = self.boundary_faces.remove(&node_id).unwrap_or_default();
1252 if moments.is_empty() && quantum.timeline.is_none() {
1253 let has_faces = boundary_faces
1254 .values()
1255 .any(|(input, output)| !input.is_empty() || !output.is_empty());
1256 if !annotations.detectors.is_empty()
1257 || !annotations.repeat_states.is_empty()
1258 || has_faces
1259 {
1260 return Err(StimEmissionError::AlignMomentsWithEmptyMetadata);
1261 }
1262 continue;
1263 }
1264 lanes.push(MomentLane {
1265 node: node_id,
1266 moments: kinds,
1267 qubits,
1268 });
1269 nodes.insert(
1270 node_id,
1271 AlignedNodeEmission {
1272 circuit,
1273 moments,
1274 annotations,
1275 ends_with_tick,
1276 boundary_faces,
1277 },
1278 );
1279 }
1280 Ok((lanes, nodes))
1281 }
1282
1283 fn emit_aligned_moment(
1284 &mut self,
1285 node_id: BloqNodeId,
1286 moment: usize,
1287 emission: &mut AlignedNodeEmission,
1288 ) -> Result<bool, StimEmissionError> {
1289 let moment_count = emission.moments.len();
1290 let ops = emission
1291 .moments
1292 .get_mut(moment)
1293 .expect("alignment returns a submitted moment");
1294 let last = moment + 1 == moment_count;
1295 let finalize_now = last && !emission.ends_with_tick;
1296
1297 if moment == 0 {
1298 let node = &self.level[node_id];
1299 self.emit_node_comment(node_id, node);
1300 for (index, (operator_in, _)) in &emission.boundary_faces {
1301 self.emit_observable_pauli_line(*index, operator_in)?;
1302 }
1303 }
1304
1305 let entry = emission.circuit.entry_body();
1306 std::mem::swap(
1307 emission
1308 .circuit
1309 .body_mut(entry)
1310 .expect("entry body is created with the circuit")
1311 .ops_mut(),
1312 ops,
1313 );
1314 let result = self.emit_aligned_circuit(
1315 &emission.circuit,
1316 finalize_now.then_some(&emission.annotations),
1317 );
1318 std::mem::swap(
1319 emission
1320 .circuit
1321 .body_mut(entry)
1322 .expect("entry body is created with the circuit")
1323 .ops_mut(),
1324 ops,
1325 );
1326 result?;
1327
1328 Ok(last)
1329 }
1330
1331 fn emit_aligned_circuit(
1332 &mut self,
1333 circuit: &CoordCircuit,
1334 annotations: Option<&StimCircuitAnnotations>,
1335 ) -> Result<bool, CircuitError> {
1336 let mut mapper = PassthroughMeasurementMapper {
1337 num_measurements: self.num_measurements,
1338 };
1339 let mut ctx = StimEmitContext {
1340 layout: &self.layout,
1341 frame: &mut self.frame,
1342 resolved_loop_detector_states: &mut self.resolved_loop_detector_states,
1343 output: &mut self.output,
1344 indent: 0,
1345 };
1346 emit_stim_circuit(
1347 circuit,
1348 annotations,
1349 &mut ctx,
1350 &mut mapper,
1351 StimEmitOptions {
1352 emit_qubit_coords: false,
1353 ..StimEmitOptions::default()
1354 },
1355 )
1356 }
1357
1358 fn emit_order_attempt(
1359 mut self,
1360 order: &[BloqNodeId],
1361 ) -> Result<AttemptStimRender, StimEmissionError> {
1362 self.prepare()?;
1363 for id in order {
1364 self.emit_node(*id, &self.level[*id])?;
1365 }
1366 let mut measurement_columns = FxHashMap::with_capacity_and_hasher(
1367 self.instance_measurements.len(),
1368 Default::default(),
1369 );
1370 for (&measurement, &backend) in &self.instance_measurements {
1371 measurement_columns.insert(
1372 measurement,
1373 self.frame.resolve_absolute_measurement(backend)?,
1374 );
1375 }
1376 Ok(AttemptStimRender {
1377 text: self.output,
1378 measurement_columns,
1379 detectors: self
1380 .detector_metadata
1381 .take()
1382 .expect("attempt emitter owns detector metadata"),
1383 })
1384 }
1385
1386 fn emit_order_segments(
1393 mut self,
1394 order: &[BloqNodeId],
1395 ) -> Result<BloqStimSegments, StimEmissionError> {
1396 self.prepare()?;
1397 let header = std::mem::take(&mut self.output);
1398 let mut segments = Vec::with_capacity(order.len());
1399 for &node_id in order {
1400 let measurement_start = self.frame.emitted_count() as usize;
1401 self.emit_node(node_id, &self.level[node_id])?;
1402 segments.push(StimSegment {
1403 node_id,
1404 text: std::mem::take(&mut self.output),
1405 measurement_start,
1406 measurement_count: self.frame.emitted_count() as usize - measurement_start,
1407 });
1408 }
1409 Ok(BloqStimSegments { header, segments })
1410 }
1411
1412 fn emit_node(&mut self, node_id: BloqNodeId, node: &BloqNode) -> Result<(), StimEmissionError> {
1419 let comment_start = self.output.len();
1425 self.emit_node_comment(node_id, node);
1426 let after_comment = self.output.len();
1427
1428 match (node.try_classical(), &node.kind) {
1429 (_, BloqNodeKind::Quantum(_)) => {
1430 let faces = self
1436 .attempt
1437 .is_none_or(AttemptRender::emits_observables)
1438 .then(|| self.boundary_faces.remove(&node_id))
1439 .flatten();
1440 if let Some(faces) = &faces {
1441 for (index, (operator_in, _)) in faces {
1442 self.emit_observable_pauli_line(*index, operator_in)?;
1443 }
1444 }
1445 self.emit_instantiated_node(node_id, node)?;
1446 if let Some(faces) = &faces {
1447 for (index, (_, operator_out)) in faces {
1448 self.emit_observable_pauli_line(*index, operator_out)?;
1449 }
1450 }
1451 }
1452 (
1453 Some(ClassicalNode::Observable {
1454 index: Some(index), ..
1455 }),
1456 _,
1457 ) => {
1458 if self.attempt.is_none_or(AttemptRender::emits_observables) {
1459 self.emit_observable_node(*index, node_id)?;
1460 }
1461 }
1462 (
1463 Some(ClassicalNode::Observable { index: None, .. } | ClassicalNode::Compute { .. }),
1464 _,
1465 ) => {}
1466 _ => {
1470 debug_assert!(
1471 stim_unsupported_reason(node).is_some(),
1472 "unhandled emittable node kind"
1473 );
1474 unreachable!("un-emittable node kind passed reject_unsupported")
1475 }
1476 }
1477 if self.output.len() == after_comment {
1478 self.output.truncate(comment_start);
1479 }
1480 Ok(())
1481 }
1482
1483 fn emit_node_comment(&mut self, node_id: BloqNodeId, node: &BloqNode) {
1486 use std::fmt::Write as _;
1487 let kind: Cow<'static, str> = match (node.try_classical(), &node.kind) {
1488 (_, BloqNodeKind::Quantum(_)) => "quantum".into(),
1489 (
1490 Some(ClassicalNode::Observable {
1491 index: Some(index), ..
1492 }),
1493 _,
1494 ) => format!("observable {index}").into(),
1495 (_, BloqNodeKind::Classical(_)) => "classical".into(),
1496 (_, BloqNodeKind::Region(_)) => "region".into(),
1497 };
1498 let _ = writeln!(
1499 self.output,
1500 "# node {}: {kind} {}",
1501 node_id.0, node.provenance
1502 );
1503 }
1504
1505 fn emit_observable_node(
1509 &mut self,
1510 index: u32,
1511 node_id: BloqNodeId,
1512 ) -> Result<(), StimEmissionError> {
1513 let value = self
1514 .level
1515 .resolve_classical(node_id, ClassicalAssignment::Uniform(false))?;
1516 self.emit_observable_records(index, &value.measurements)?;
1517 if self.emitted_observables.insert(index) {
1518 self.output.push_str("OBSERVABLE_INCLUDE(");
1519 push_int(&mut self.output, index);
1520 self.output.push_str(")\n");
1521 }
1522 Ok(())
1523 }
1524
1525 fn emit_observable_records(
1531 &mut self,
1532 index: u32,
1533 measurements: &BTreeSet<InstanceMeasurement>,
1534 ) -> Result<(), StimEmissionError> {
1535 let mut lookbacks = Vec::with_capacity(measurements.len());
1536 for measurement in measurements {
1537 let backend_id = *self.instance_measurements.get(measurement).ok_or(
1538 StimEmissionError::MalformedGraph(
1539 "observable references an unemitted instance measurement",
1540 ),
1541 )?;
1542 lookbacks.push(self.frame.resolve_measurement(backend_id).map_err(
1543 |error| match error {
1544 CircuitError::InvalidMeasurementId(_) => StimEmissionError::MalformedGraph(
1545 "observable reads a measurement before its producing node",
1546 ),
1547 error => StimEmissionError::Circuit(error),
1548 },
1549 )?);
1550 }
1551 lookbacks.sort_unstable();
1552 crate::emit::emit_observable_include_records(&mut self.output, index, &lookbacks);
1553 if !lookbacks.is_empty() {
1554 self.emitted_observables.insert(index);
1555 }
1556 Ok(())
1557 }
1558
1559 fn build_boundary_faces(&mut self) -> Result<(), StimEmissionError> {
1562 use bloq_ir::BoundaryFace;
1563
1564 let level = self.level;
1565
1566 let has_operators = level.nodes().any(|(_, node)| {
1568 matches!(
1569 node.try_classical(),
1570 Some(ClassicalNode::Observable { operators, .. })
1571 if !operators.is_empty()
1572 )
1573 });
1574 if !has_operators {
1575 return Ok(());
1576 }
1577
1578 let mut consumers: FxHashMap<BloqNodeId, BTreeSet<u32>> = FxHashMap::default();
1581 for id in level.deterministic_emit_order()?.into_iter().rev() {
1582 let mut indices = BTreeSet::new();
1583 if let Some(ClassicalNode::Observable {
1584 index: Some(index), ..
1585 }) = level[id].try_classical()
1586 {
1587 indices.insert(*index);
1588 }
1589 for (consumer, _) in level.compose_consumers(id) {
1590 for index in consumers.get(&consumer).into_iter().flatten() {
1591 if !indices.insert(*index) {
1592 indices.remove(index);
1593 }
1594 }
1595 }
1596 consumers.insert(id, indices);
1597 }
1598
1599 let mut instance_node: FxHashMap<TemplateInstanceId, BloqNodeId> = FxHashMap::default();
1601 for (id, quantum) in level.quantum_nodes() {
1602 for instance in &quantum.instances {
1603 instance_node.insert(instance.id, id);
1604 }
1605 }
1606
1607 let mut faces: FxHashMap<BloqNodeId, BTreeMap<u32, (PauliMap, PauliMap)>> =
1608 FxHashMap::default();
1609 for (node, weight) in level.nodes() {
1610 let Some(ClassicalNode::Observable { operators, .. }) = weight.try_classical() else {
1611 continue;
1612 };
1613 let indices = &consumers[&node];
1614 if indices.is_empty() {
1615 continue;
1616 }
1617 for operator in operators {
1619 let producer = *instance_node.get(&operator.instance).ok_or(
1620 StimEmissionError::MalformedGraph(
1621 "observable boundary operator names an unproduced instance",
1622 ),
1623 )?;
1624 for &index in indices {
1625 let (operator_in, operator_out) =
1626 faces.entry(producer).or_default().entry(index).or_default();
1627 match operator.face {
1628 BoundaryFace::Input => *operator_in = &*operator_in ^ &operator.operator,
1629 BoundaryFace::Output => {
1630 *operator_out = &*operator_out ^ &operator.operator;
1631 }
1632 }
1633 }
1634 }
1635 }
1636 self.boundary_faces = faces;
1637 Ok(())
1638 }
1639
1640 fn emit_observable_pauli_line(
1646 &mut self,
1647 index: u32,
1648 operator: &bloq_circuit::PauliMap,
1649 ) -> Result<(), StimEmissionError> {
1650 if operator.is_empty() {
1651 return Ok(());
1652 }
1653 let output_start = self.output.len();
1654 crate::emit::emit_observable_include_pauli_targets(
1655 &mut self.output,
1656 index,
1657 operator,
1658 &self.layout,
1659 )
1660 .map_err(|_| {
1661 StimEmissionError::MalformedGraph(
1662 "boundary operator names a qubit outside the global qubit layout",
1663 )
1664 })?;
1665 if self.output.len() != output_start {
1666 self.emitted_observables.insert(index);
1667 }
1668 Ok(())
1669 }
1670
1671 fn build_global_layout(&mut self) -> Result<(), StimEmissionError> {
1672 let coords = self.program.sorted_layout_coords()?;
1673
1674 self.layout = QubitLayout::new(coordinate_index(coords.iter().copied())?)?;
1675 self.indexed_coords = coords;
1676 Ok(())
1677 }
1678
1679 fn build_global_measurements(&mut self) -> Result<(), StimEmissionError> {
1685 let mut next_instance_measurement = 0u32;
1689 self.instance_measurements.clear();
1690 for (node_id, quantum) in self.level.quantum_nodes() {
1691 let templates = self
1692 .plans
1693 .get(&self.path, node_id)
1694 .map_or(self.program.templates(), |plan| {
1695 plan.templates(self.program.templates())
1696 });
1697 for instance in &quantum.instances {
1698 let template = templates.get(instance.template_id).ok_or(
1699 StimEmissionError::MalformedGraph("instance references a missing template"),
1700 )?;
1701 for record in template.circuit.meas_registry().records() {
1702 self.instance_measurements.insert(
1703 InstanceMeasurement {
1704 instance: instance.id,
1705 measurement: record.id,
1706 },
1707 next_instance_measurement,
1708 );
1709 next_instance_measurement = next_instance_measurement
1710 .checked_add(1)
1711 .expect("id space is u32; programs stay far below 2^32 measurements");
1712 }
1713 }
1714 }
1715 self.num_measurements = next_instance_measurement;
1716 self.frame
1719 .reserve_measurement_ids(next_instance_measurement as usize);
1720 Ok(())
1721 }
1722
1723 fn build_global_loop_states(&mut self) -> Result<(), StimEmissionError> {
1726 self.instance_loop_states.clear();
1727 let mut next_state = 0;
1728 for (node_id, quantum) in self.level.quantum_nodes() {
1729 let templates = self
1730 .plans
1731 .get(&self.path, node_id)
1732 .map_or(self.program.templates(), |plan| {
1733 plan.templates(self.program.templates())
1734 });
1735 for instance in &quantum.instances {
1736 let template = templates.get(instance.template_id).ok_or(
1737 StimEmissionError::MalformedGraph("instance references a missing template"),
1738 )?;
1739 for state in &template.repeat_states {
1740 let key = InstanceLoopState {
1741 instance: instance.id,
1742 state: state.state,
1743 };
1744 if self.instance_loop_states.contains_key(&key) {
1745 continue;
1746 }
1747 self.instance_loop_states
1748 .insert(key, LoopStateId(next_state));
1749 next_state = next_state
1750 .checked_add(1)
1751 .expect("id space is u32; programs stay far below 2^32 loop states");
1752 }
1753 }
1754 }
1755 Ok(())
1756 }
1757
1758 fn emit_qubit_coords(&mut self) {
1759 let coords = self.indexed_coords;
1764 crate::emit::emit_qubit_coords_lines(
1765 &mut self.output,
1766 coords
1767 .iter()
1768 .enumerate()
1769 .map(|(index, coord)| (*coord, index as u32)),
1770 );
1771 }
1772
1773 fn collect_detector_metadata(&mut self) -> Result<(), StimEmissionError> {
1774 let Some(metadata) = &mut self.detector_metadata else {
1775 return Ok(());
1776 };
1777 if !self.annotations_scratch.repeat_states.is_empty()
1778 || self
1779 .annotations_scratch
1780 .detectors
1781 .iter()
1782 .any(|detector| detector.scope != StimAnnotationScope::TopLevel)
1783 {
1784 return Err(StimEmissionError::MalformedGraph(
1785 "isolated-T attempt must be flattened before detector indexing",
1786 ));
1787 }
1788 metadata.extend(self.annotations_scratch.detectors.iter().map(|detector| {
1789 EmittedDetectorMetadata {
1790 sign: detector.parity.sign(),
1791 postselection: detector.postselection,
1792 }
1793 }));
1794 Ok(())
1795 }
1796
1797 fn emit_instantiated_node(
1798 &mut self,
1799 node_id: BloqNodeId,
1800 node: &BloqNode,
1801 ) -> Result<(), StimEmissionError> {
1802 if node.expect_quantum().instances.len() == 1 && self.noise.is_none() {
1806 return self.emit_single_instance_node(node_id, node);
1807 }
1808 let instantiated =
1809 self.plans
1810 .get(&self.path, node_id)
1811 .ok_or(StimEmissionError::MalformedGraph(
1812 "quantum node has no emission plan",
1813 ))?;
1814 let circuit = remapped_plan_circuit(&mut self.instance_measurements, instantiated);
1815 node_annotations(
1816 &mut self.annotations_scratch,
1817 self.program,
1818 node,
1819 instantiated.templates(self.program.templates()),
1820 &self.instance_measurements,
1821 &self.instance_loop_states,
1822 NodeAnnotationOptions {
1823 instance_templates: &self.instance_templates,
1824 bodies: Some(&instantiated.bodies),
1825 include_restarts: self.attempt.is_some(),
1826 },
1827 )?;
1828 let mut mapper = PassthroughMeasurementMapper {
1829 num_measurements: self.num_measurements,
1830 };
1831 let mut ctx = StimEmitContext {
1832 layout: &self.layout,
1833 frame: &mut self.frame,
1834 resolved_loop_detector_states: &mut self.resolved_loop_detector_states,
1835 output: &mut self.output,
1836 indent: 0,
1837 };
1838 let needs_boundary_tick = emit_stim_circuit(
1839 &circuit,
1840 Some(&self.annotations_scratch),
1841 &mut ctx,
1842 &mut mapper,
1843 StimEmitOptions {
1844 enable_clifford_proxy: false,
1845 emit_qubit_coords: false,
1846 ..StimEmitOptions::default()
1847 },
1848 )?;
1849 self.collect_detector_metadata()?;
1850 if needs_boundary_tick {
1851 self.output.push_str("TICK\n");
1852 }
1853 Ok(())
1854 }
1855
1856 fn emit_single_instance_node(
1857 &mut self,
1858 node_id: BloqNodeId,
1859 node: &BloqNode,
1860 ) -> Result<(), StimEmissionError> {
1861 let instance = node
1862 .expect_quantum()
1863 .instances
1864 .first()
1865 .expect("single-instance path has one instance");
1866 let template = self
1867 .program
1868 .templates()
1869 .get(instance.template_id)
1870 .expect("prepare rejected instances referencing missing templates");
1871 node_annotations(
1875 &mut self.annotations_scratch,
1876 self.program,
1877 node,
1878 self.program.templates(),
1879 &self.instance_measurements,
1880 &self.instance_loop_states,
1881 NodeAnnotationOptions {
1882 instance_templates: &self.instance_templates,
1883 bodies: None,
1884 include_restarts: self.attempt.is_some(),
1885 },
1886 )?;
1887 let noisy_circuit = self
1888 .attempt
1889 .filter(|attempt| attempt.noises_node(node_id))
1890 .map(|attempt| {
1891 let ideal_qubits = if instance.provenance.is_spatial_port_substitution() {
1892 template.qubits()
1893 } else {
1894 &[]
1895 };
1896 attempt
1897 .noise
1898 .noisy_circuit_excluding(&template.circuit, ideal_qubits)
1899 })
1900 .transpose()?;
1901 let circuit = noisy_circuit.as_ref().unwrap_or(&template.circuit);
1902 let enable_clifford_proxy = self
1903 .attempt
1904 .is_some_and(AttemptRender::enables_clifford_proxy);
1905 let allow_non_clifford = matches!(
1906 self.attempt.map(|attempt| attempt.mode),
1907 Some(AttemptMode::HonestT)
1908 );
1909 let emit_postselection_tags = self
1910 .attempt
1911 .is_none_or(AttemptRender::emits_postselection_tags);
1912 let mut mapper = SingleInstanceMeasurementMapper {
1913 instance: instance.id,
1914 measurements: &self.instance_measurements,
1915 num_measurements: self.num_measurements,
1916 };
1917 let mut ctx = StimEmitContext {
1918 layout: &self.layout,
1919 frame: &mut self.frame,
1920 resolved_loop_detector_states: &mut self.resolved_loop_detector_states,
1921 output: &mut self.output,
1922 indent: 0,
1923 };
1924 let needs_boundary_tick = emit_stim_circuit(
1925 circuit,
1926 Some(&self.annotations_scratch),
1927 &mut ctx,
1928 &mut mapper,
1929 StimEmitOptions {
1930 enable_clifford_proxy,
1931 allow_non_clifford,
1932 emit_postselection_tags,
1933 emit_qubit_coords: false,
1934 qubit_offset: instance.offset,
1935 },
1936 )?;
1937 self.collect_detector_metadata()?;
1938 if needs_boundary_tick {
1939 self.output.push_str("TICK\n");
1940 }
1941 Ok(())
1942 }
1943}
1944
1945fn first_unsupported_program_gate(program: &Bloq) -> Option<GateType> {
1957 let templates = program.templates();
1958 let mut scanned: FxHashMap<TemplateId, Option<GateType>> = FxHashMap::default();
1959 for (_, level) in program.levels() {
1960 for (_, quantum) in level.quantum_nodes() {
1961 for instance in &quantum.instances {
1962 let unsupported = *scanned.entry(instance.template_id).or_insert_with(|| {
1963 templates
1964 .get(instance.template_id)
1965 .and_then(|template| first_unsupported_gate(&template.circuit))
1966 });
1967 if unsupported.is_some() {
1968 return unsupported;
1969 }
1970 }
1971 }
1972 }
1973 None
1974}
1975
1976const STIM_BYTES_PER_OP: usize = 128;
1984
1985fn remapped_plan_circuit(
1987 instance_measurements: &mut FxHashMap<InstanceMeasurement, u32>,
1988 plan: &NodeEmissionPlan,
1989) -> CoordCircuit {
1990 let mut circuit = plan.circuit.clone();
1991 let mut local_measurements =
1992 FxHashMap::with_capacity_and_hasher(plan.measurements.len(), Default::default());
1993 for (local, sources) in plan.grouped_measurements() {
1995 let backend = sources
1996 .iter()
1997 .map(|source| {
1998 instance_measurements.get(source).copied().expect(
1999 "build_global_measurements allocated an id for every instance measurement",
2000 )
2001 })
2002 .min()
2003 .expect("a grouped-measurement bucket is never empty");
2004 for source in sources {
2005 instance_measurements.insert(source, backend);
2006 }
2007 local_measurements.insert(local, backend);
2008 }
2009 circuit.remap_measurement_ids(&local_measurements);
2010 circuit
2011}
2012
2013fn estimate_bloq_stim_capacity(program: &Bloq, global_qubits: usize) -> usize {
2014 let qubit_coords = global_qubits * 24;
2015 let templates = program.templates();
2016 let node_bodies = program
2017 .quantum_nodes()
2018 .map(|(_, quantum)| {
2019 let body = quantum
2023 .instances
2024 .iter()
2025 .filter_map(|instance| templates.get(instance.template_id))
2026 .map(|template| estimate_circuit_stim_capacity(&template.circuit))
2027 .sum::<usize>();
2028 body + 8
2029 })
2030 .sum::<usize>();
2031 qubit_coords + node_bodies
2032}
2033
2034fn estimate_circuit_stim_capacity(circuit: &bloq_circuit::CoordCircuit) -> usize {
2039 (0..circuit.body_count())
2040 .map(|index| {
2041 circuit
2042 .body(BodyId(index as u32))
2043 .map_or(0, |body| body.ops().len())
2044 })
2045 .sum::<usize>()
2046 * STIM_BYTES_PER_OP
2047}
2048
2049fn passthrough_live_measurement(
2053 measurement: u32,
2054 num_measurements: u32,
2055) -> Result<u32, bloq_circuit::CircuitError> {
2056 if measurement < num_measurements {
2057 return Ok(measurement);
2058 }
2059 Err(bloq_circuit::CircuitError::InvalidMeasurementId(
2060 measurement,
2061 ))
2062}
2063
2064struct PassthroughMeasurementMapper {
2068 num_measurements: u32,
2069}
2070
2071impl StimMeasurementMapper for PassthroughMeasurementMapper {
2072 fn map_measurement(&mut self, measurement: u32) -> Result<u32, bloq_circuit::CircuitError> {
2073 passthrough_live_measurement(measurement, self.num_measurements)
2074 }
2075}
2076
2077struct SingleInstanceMeasurementMapper<'a> {
2078 instance: TemplateInstanceId,
2079 measurements: &'a FxHashMap<InstanceMeasurement, u32>,
2080 num_measurements: u32,
2081}
2082
2083impl StimMeasurementMapper for SingleInstanceMeasurementMapper<'_> {
2084 fn map_measurement(&mut self, measurement: u32) -> Result<u32, bloq_circuit::CircuitError> {
2085 let measurement = InstanceMeasurement {
2086 instance: self.instance,
2087 measurement,
2088 };
2089 self.measurements.get(&measurement).copied().ok_or(
2090 bloq_circuit::CircuitError::InvalidMeasurementId(measurement.measurement),
2091 )
2092 }
2093
2094 fn map_annotation_measurement(
2095 &mut self,
2096 measurement: u32,
2097 ) -> Result<u32, bloq_circuit::CircuitError> {
2098 passthrough_live_measurement(measurement, self.num_measurements)
2099 }
2100}
2101
2102#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2103struct InstanceLoopState {
2104 instance: TemplateInstanceId,
2105 state: LoopStateId,
2106}
2107
2108struct NodeAnnotationOptions<'a> {
2113 instance_templates: &'a FxHashMap<TemplateInstanceId, TemplateId>,
2114 bodies: Option<&'a FxHashMap<(TemplateInstanceId, BodyId), BodyId>>,
2115 include_restarts: bool,
2116}
2117
2118fn node_annotations(
2119 out: &mut StimCircuitAnnotations,
2120 program: &Bloq,
2121 node: &BloqNode,
2122 templates: &bloq_ir::lowering::BloqTemplatePool,
2123 measurements: &FxHashMap<InstanceMeasurement, u32>,
2124 loop_states: &FxHashMap<InstanceLoopState, LoopStateId>,
2125 options: NodeAnnotationOptions<'_>,
2126) -> Result<(), StimEmissionError> {
2127 let NodeAnnotationOptions {
2128 instance_templates,
2129 bodies,
2130 include_restarts,
2131 } = options;
2132 let map_body = |instance, body| {
2133 bodies.map_or(body, |bodies| {
2134 bodies
2135 .get(&(instance, body))
2136 .copied()
2137 .expect("the emission plan maps every body of every merged instance")
2138 })
2139 };
2140 let StimCircuitAnnotations {
2141 detectors,
2142 repeat_states,
2143 } = out;
2144 detectors.clear();
2145 repeat_states.clear();
2146 let node = node.expect_quantum();
2147 program.check_detector_bundle_bindings(node, |instance| {
2148 instance_templates.get(&instance).copied()
2149 })?;
2150 for instance in &node.instances {
2151 let template = templates
2152 .get(instance.template_id)
2153 .expect("prepare rejected instances referencing missing templates");
2154 for detector in &template.detectors {
2155 let scope = match detector.scope {
2156 TemplateDetectorScope::TopLevel => StimAnnotationScope::TopLevel,
2157 TemplateDetectorScope::RepeatBody { body } => StimAnnotationScope::RepeatBody {
2158 body: map_body(instance.id, body),
2159 },
2160 };
2161 detectors.push(StimDetector {
2162 scope,
2163 parity: remap_template_parity(
2164 &detector.parity,
2165 instance.id,
2166 measurements,
2167 loop_states,
2168 ),
2169 coords: detector
2170 .coords
2171 .as_ref()
2172 .map(|coords| bloq_circuit::translate_detector_coords(coords, instance.offset)),
2173 postselection: false,
2174 });
2175 }
2176 if include_restarts {
2177 detectors.extend(template.restarts.iter().map(|restart| StimDetector {
2178 scope: StimAnnotationScope::TopLevel,
2179 parity: remap_template_parity(
2180 &restart.parity,
2181 instance.id,
2182 measurements,
2183 loop_states,
2184 ),
2185 coords: None,
2186 postselection: true,
2187 }));
2188 }
2189 for state in &template.repeat_states {
2190 let state_id = InstanceLoopState {
2191 instance: instance.id,
2192 state: state.state,
2193 };
2194 repeat_states.push(StimRepeatState {
2195 body: map_body(instance.id, state.body),
2196 state: loop_states
2197 .get(&state_id)
2198 .copied()
2199 .expect("build_global_loop_states allocated an id for every instance state"),
2200 initial: remap_template_parity(
2201 &state.initial,
2202 instance.id,
2203 measurements,
2204 loop_states,
2205 ),
2206 next: remap_template_parity(&state.next, instance.id, measurements, loop_states),
2207 });
2208 }
2209 }
2210 for detector in program.node_detectors(node)? {
2211 detectors.push(StimDetector {
2212 scope: StimAnnotationScope::TopLevel,
2213 parity: remap_instance_parity(&detector.parity(), measurements)?,
2214 coords: detector.coords().map(Iterator::collect),
2215 postselection: false,
2216 });
2217 }
2218 if include_restarts {
2219 for restart in &node.restarts {
2220 detectors.push(StimDetector {
2221 scope: StimAnnotationScope::TopLevel,
2222 parity: remap_instance_parity(&restart.parity, measurements)?,
2223 coords: None,
2224 postselection: true,
2225 });
2226 }
2227 }
2228 repeat_states.sort_by_key(|state| (state.body, state.state));
2229 Ok(())
2230}
2231
2232fn remap_template_parity(
2233 parity: &bloq_ir::lowering::TemplateDetectorParity,
2234 instance: bloq_ir::lowering::TemplateInstanceId,
2235 measurements: &FxHashMap<InstanceMeasurement, u32>,
2236 loop_states: &FxHashMap<InstanceLoopState, LoopStateId>,
2237) -> bloq_circuit::DetectorParity {
2238 bloq_circuit::DetectorParity::from_terms(parity.terms().iter().copied().map(
2242 |term| match term {
2243 bloq_circuit::DetectorTerm::Measurement(measurement) => {
2244 let measurement = InstanceMeasurement {
2245 instance,
2246 measurement,
2247 };
2248 measurements
2249 .get(&measurement)
2250 .copied()
2251 .map(bloq_circuit::DetectorTerm::Measurement)
2252 .expect(
2253 "build_global_measurements allocated an id for every instance measurement",
2254 )
2255 }
2256 bloq_circuit::DetectorTerm::LoopState(state) => {
2257 let state = InstanceLoopState { instance, state };
2258 loop_states
2259 .get(&state)
2260 .copied()
2261 .map(bloq_circuit::DetectorTerm::LoopState)
2262 .expect("build_global_loop_states allocated an id for every instance state")
2263 }
2264 },
2265 ))
2266 .with_sign(parity.sign())
2267}
2268
2269fn remap_instance_parity(
2270 parity: &bloq_ir::NodeDetectorParity,
2271 measurements: &FxHashMap<InstanceMeasurement, u32>,
2272) -> Result<bloq_circuit::DetectorParity, StimEmissionError> {
2273 if let Some(state) = parity.terms().iter().find_map(|term| match term {
2274 bloq_circuit::DetectorTerm::LoopState(state) => Some(*state),
2275 bloq_circuit::DetectorTerm::Measurement(_) => None,
2276 }) {
2277 return Err(StimEmissionError::NodeLoopStateUnsupported(state));
2278 }
2279 parity.try_map_measurements(|measurement| {
2280 measurements
2281 .get(&measurement)
2282 .copied()
2283 .ok_or(StimEmissionError::UnknownInstanceMeasurement(measurement))
2284 })
2285}
2286
2287#[cfg(test)]
2288mod tests {
2289 use super::*;
2290 use bloq_circuit::{CircuitBody, DetectorParity, DetectorTerm, PauliBasis};
2291 use bloq_ir::{
2292 BloqEdge, BoundaryFace, ClassicalExpr, MemoryRoundTarget, NodeDetector, QuantumTimeline,
2293 SourceBlockRef, TemplateDetector,
2294 lowering::{BloqTemplate, InstanceBoundaryOperator, TemplateInstance, TemplateRepeatState},
2295 };
2296 use glam::{ivec2, ivec3};
2297
2298 fn compiled_isolated_t(distance: u32, latency: u32) -> Bloq {
2299 let graph = bloq_graph::parse_blog_to_graph(
2300 "BLOG 1.0\n\n 0: T [0, 0, 0]\n 1: Port [0, 0, 1]\n [0, 0, 0] -> +Z\n",
2301 )
2302 .expect("isolated T BLOG parses");
2303 let mut program =
2304 bloq_compile::CompileContext::new(bloq_compile::CompileConfig::new(distance))
2305 .compile(&graph)
2306 .expect("isolated T compiles")
2307 .bloq;
2308 if latency != 0 {
2309 let path = isolated_rus_path(&program).expect("RUS path");
2310 let terminal = program
2311 .level_at(&path)
2312 .expect("RUS body")
2313 .deterministic_emit_order()
2314 .expect("body order")
2315 .into_iter()
2316 .rev()
2317 .find(|&node| {
2318 program
2319 .level_at(&path)
2320 .expect("RUS body")
2321 .node(node)
2322 .is_some_and(|node| node.try_quantum().is_some())
2323 })
2324 .expect("escape stage");
2325 program
2326 .insert_memory_rounds(
2327 MemoryRoundTarget::After {
2328 path,
2329 node: terminal,
2330 },
2331 latency,
2332 )
2333 .expect("append decoder latency");
2334 }
2335 program
2336 }
2337
2338 #[test]
2339 fn isolated_t_frame_inversions_preserve_columns_and_change_probe_corrections() {
2340 let program = compiled_isolated_t(3, 0);
2341 let baseline = emit_isolated_t_attempts(&program, 0.0).unwrap();
2342 let probe_signs = |manifest: &IsolatedTAttemptManifest| {
2343 let x = manifest.frame_x.sign;
2344 let z = manifest.frame_z.sign;
2345 std::array::from_fn::<_, 3, _>(|axis| {
2346 manifest.exp_val_signs[axis] * if [z, x ^ z, x][axis] { -1 } else { 1 }
2347 })
2348 };
2349 for invert_x in [true, false] {
2350 let mut changed = program.clone();
2351 let frames = changed.output_frames();
2352 let frame = if invert_x { frames[0].x } else { frames[0].z };
2353 let Some(ClassicalNode::Compute { expr }) =
2354 changed.node_mut(frame).unwrap().try_classical_mut()
2355 else {
2356 panic!("output frame is a Compute");
2357 };
2358 *expr = ClassicalExpr::Not(Box::new(expr.clone()));
2359 changed.validate().unwrap();
2360 let artifacts = emit_isolated_t_attempts(&changed, 0.0).unwrap();
2361 let mut expected = baseline.clone();
2362 if invert_x {
2363 expected.manifest.frame_x.sign ^= true;
2364 } else {
2365 expected.manifest.frame_z.sign ^= true;
2366 }
2367 assert_eq!(artifacts, expected);
2368 let before = probe_signs(&baseline.manifest);
2369 let after = probe_signs(&artifacts.manifest);
2370 let multipliers = if invert_x { [1, -1, -1] } else { [-1, -1, 1] };
2371 assert_eq!(
2372 after,
2373 std::array::from_fn(|axis| before[axis] * multipliers[axis])
2374 );
2375 }
2376 }
2377
2378 #[test]
2379 fn isolated_t_attempt_emits_aligned_physical_and_causal_circuits() {
2380 let artifacts = emit_isolated_t_attempts(&compiled_isolated_t(3, 2), 1e-3)
2381 .expect("emit isolated T attempt");
2382 let detector_count = |text: &str| {
2383 text.lines()
2384 .filter(|line| line.starts_with("DETECTOR"))
2385 .count()
2386 };
2387 let exp_count = |text: &str| {
2388 text.lines()
2389 .filter(|line| line.starts_with("EXP_VAL "))
2390 .count()
2391 };
2392
2393 assert!(artifacts.physical_t.contains("T_DAG"));
2394 assert!(!artifacts.physical_s.contains("T_DAG"));
2395 assert!(artifacts.physical_s.contains("S_DAG"));
2396 assert_eq!(exp_count(&artifacts.physical_s), 3);
2397 assert_eq!(exp_count(&artifacts.physical_t), 3);
2398 assert_eq!(exp_count(&artifacts.companion), 0);
2399 assert!(!artifacts.physical_s.contains("OBSERVABLE_INCLUDE"));
2400 assert!(!artifacts.physical_t.contains("OBSERVABLE_INCLUDE"));
2401 assert!(artifacts.companion.contains("OBSERVABLE_INCLUDE"));
2402 let noise_count = |text: &str| {
2403 text.lines()
2404 .filter(|line| line.starts_with("DEPOLARIZE") || line.contains("_ERROR("))
2405 .count()
2406 };
2407 assert_eq!(
2408 noise_count(&artifacts.physical_s),
2409 noise_count(&artifacts.physical_t)
2410 );
2411 assert!(noise_count(&artifacts.physical_s) > noise_count(&artifacts.companion));
2412 assert!(noise_count(&artifacts.companion) > 0);
2413
2414 let expected_detectors = artifacts.manifest.detector_signs.len();
2415 assert_eq!(detector_count(&artifacts.physical_s), expected_detectors);
2416 assert_eq!(detector_count(&artifacts.physical_t), expected_detectors);
2417 assert_eq!(detector_count(&artifacts.companion), expected_detectors);
2418 let sheeted = format!("{}{}", artifacts.companion, artifacts.sheets);
2419 assert_eq!(detector_count(&sheeted), expected_detectors);
2420 assert_eq!(
2421 artifacts
2422 .companion
2423 .lines()
2424 .filter(|line| line.starts_with("DETECTOR[POST-SELECTION]"))
2425 .count(),
2426 artifacts.manifest.postselection_detectors.len()
2427 );
2428 assert!(!artifacts.physical_s.contains("DETECTOR[POST-SELECTION]"));
2429 assert!(!artifacts.physical_t.contains("DETECTOR[POST-SELECTION]"));
2430 assert_eq!(
2431 sheeted
2432 .lines()
2433 .filter(|line| line.starts_with("DETECTOR[POST-SELECTION]"))
2434 .count(),
2435 artifacts.manifest.postselection_detectors.len()
2436 );
2437 assert!(
2440 artifacts
2441 .sheets
2442 .lines()
2443 .all(|line| line.starts_with("OBSERVABLE_INCLUDE(")),
2444 "sheets suffix carries only observables: {}",
2445 artifacts.sheets
2446 );
2447 assert!(!artifacts.manifest.postselection_detectors.is_empty());
2448 assert_eq!(artifacts.manifest.frontier_sheets.len(), 8);
2449 assert_eq!(artifacts.manifest.exp_val_signs, [1, 1, 1]);
2450 assert_eq!(
2451 artifacts.manifest.frame_x.gap_observable,
2452 artifacts.manifest.gap_z_observable
2453 );
2454 assert_eq!(
2455 artifacts.manifest.frame_z.gap_observable,
2456 artifacts.manifest.gap_x_observable
2457 );
2458 }
2459
2460 #[test]
2461 fn isolated_t_attempt_matches_d5_m10_causal_cut() {
2462 let artifacts = emit_isolated_t_attempts(&compiled_isolated_t(5, 10), 1e-3)
2463 .expect("emit d5/m10 isolated T attempt");
2464 let measurement_count = artifacts
2465 .physical_t
2466 .lines()
2467 .filter_map(|line| {
2468 let mut words = line.split_whitespace();
2469 let gate = words.next()?.split('(').next()?;
2470 matches!(gate, "M" | "MX" | "MY" | "MPP").then(|| words.count())
2471 })
2472 .sum::<usize>();
2473 let detector_count = artifacts
2474 .physical_t
2475 .lines()
2476 .filter(|line| line.starts_with("DETECTOR"))
2477 .count();
2478
2479 assert_eq!(measurement_count, 448);
2480 assert_eq!(detector_count, 424);
2483 assert_eq!(artifacts.manifest.detector_signs.len(), 424);
2484 assert_eq!(artifacts.manifest.postselection_detectors.len(), 35);
2485 assert_eq!(
2486 artifacts
2487 .companion
2488 .lines()
2489 .filter(|line| line.starts_with("DETECTOR[POST-SELECTION]"))
2490 .count(),
2491 35
2492 );
2493 assert!(!artifacts.physical_s.contains("DETECTOR[POST-SELECTION]"));
2494 assert!(!artifacts.physical_t.contains("DETECTOR[POST-SELECTION]"));
2495 assert_eq!(artifacts.manifest.frontier_sheets.len(), 24);
2496 assert_ne!(
2497 artifacts.manifest.gap_z_observable,
2498 artifacts.manifest.gap_x_observable
2499 );
2500 assert_eq!(
2501 artifacts.manifest.frame_x.measurements,
2502 [46, 95, 97, 112, 114, 116, 118, 120, 122]
2503 );
2504 assert_eq!(
2505 artifacts.manifest.frame_z.measurements,
2506 [15, 17, 18, 124, 125, 126, 127, 128, 129]
2507 );
2508 assert!(
2509 artifacts
2510 .manifest
2511 .frontier_sheets
2512 .iter()
2513 .all(|sheet| !sheet.sign)
2514 );
2515 }
2516
2517 #[test]
2518 fn isolated_t_attempt_rejects_invalid_noise_probability() {
2519 let program = Bloq::new();
2520 for probability in [f64::NAN, f64::INFINITY, -0.1, 1.1] {
2521 assert_eq!(
2522 emit_isolated_t_attempts(&program, probability),
2523 Err(StimEmissionError::InvalidNoiseProbability)
2524 );
2525 }
2526 }
2527
2528 #[test]
2529 fn signed_logical_y_keeps_global_phase() {
2530 let coord = ivec2(0, 0);
2531 let x = PauliMap::from_unique_entries([(coord, Pauli::Z)]);
2532 let z = PauliMap::from_unique_entries([(coord, Pauli::X)]);
2533 let (y, sign) = signed_logical_y(&x, &z).expect("iZX is Hermitian");
2534
2535 assert_eq!(y.get(&coord), Some(&Pauli::Y));
2536 assert_eq!(sign, -1);
2537 }
2538
2539 #[test]
2540 fn emit_bloq_stim_rejects_repeat_until_success_by_name() {
2541 let mut bloq = Bloq::new();
2544 let body = SubGraph::new();
2545 bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
2546 body,
2547 restart_condition: ClassicalExpr::Const(false),
2548 restart_source: None,
2549 }));
2550 assert_eq!(
2551 emit_bloq_stim(&bloq),
2552 Err(StimEmissionError::UnsupportedNode(
2553 "RepeatUntilSuccess (postselected retry)"
2554 ))
2555 );
2556 }
2557
2558 #[test]
2559 fn emit_bloq_stim_folds_observable_outputs_but_rejects_activation() {
2560 let mut graph = Bloq::new();
2564 let node = measured_program_node(
2565 &mut graph,
2566 ivec3(0, 0, 0),
2567 ivec2(0, 0),
2568 TemplateInstanceId(0),
2569 );
2570 let quantum = graph.add_node(node);
2571 let read_observable = graph.add_node(BloqNode::classical(ClassicalNode::Observable {
2572 index: Some(0),
2573 measurements: vec![InstanceMeasurement {
2574 instance: TemplateInstanceId(0),
2575 measurement: 0,
2576 }],
2577 operators: vec![],
2578 }));
2579 graph.add_edge(quantum, read_observable, BloqEdge::Order);
2580 let corrected_read = graph.add_node(BloqNode::classical(ClassicalNode::observable(1)));
2581 graph.add_edge(read_observable, corrected_read, BloqEdge::value(0));
2582 let composed = graph.add_node(BloqNode::classical(ClassicalNode::observable(2)));
2583 graph.add_edge(read_observable, composed, BloqEdge::compose(0));
2584 let flipped = graph.add_node(BloqNode::classical(ClassicalNode::observable(3)));
2585 graph.add_edge(read_observable, flipped, BloqEdge::flip(0));
2586
2587 let text = emit_bloq_stim(&graph).expect("observable outputs reduce to records");
2588 for index in [0, 1, 2] {
2589 assert!(
2590 text.contains(&format!("OBSERVABLE_INCLUDE({index}) rec[-1]")),
2591 "observable output should fold to its physical record: {text}"
2592 );
2593 }
2594
2595 assert!(text.contains("OBSERVABLE_INCLUDE(3)\n"), "{text}");
2596 assert!(!text.contains("OBSERVABLE_INCLUDE(3) rec"), "{text}");
2597
2598 graph.node_mut(corrected_read).unwrap().activation = Some(1);
2599 graph.add_edge(read_observable, corrected_read, BloqEdge::value(1));
2600 assert!(!graph.has_conditional_membership());
2601 assert_eq!(
2602 emit_bloq_stim(&graph),
2603 Err(StimEmissionError::UnsupportedNode(
2604 "activation (conditional execution)"
2605 ))
2606 );
2607 }
2608
2609 #[test]
2610 fn complete_observable_emits_inline_records_and_owned_boundary_faces() {
2611 let qubit = ivec2(0, 0);
2612 let instance = TemplateInstanceId(0);
2613 let mut graph = Bloq::new();
2614 let quantum = measured_program_node(&mut graph, ivec3(0, 0, 0), qubit, instance);
2615 let quantum = graph.add_node(quantum);
2616 let operator = [(qubit, Pauli::Z)].into_iter().collect::<PauliMap>();
2617 let observable = graph.add_node(BloqNode::classical(ClassicalNode::Observable {
2618 index: Some(3),
2619 measurements: vec![
2620 InstanceMeasurement {
2621 instance,
2622 measurement: 0
2623 };
2624 3
2625 ],
2626 operators: vec![
2627 InstanceBoundaryOperator {
2628 instance,
2629 face: BoundaryFace::Input,
2630 operator: operator.clone(),
2631 },
2632 InstanceBoundaryOperator {
2633 instance,
2634 face: BoundaryFace::Output,
2635 operator: operator.clone(),
2636 },
2637 ],
2638 }));
2639 graph.add_edge(quantum, observable, BloqEdge::Order);
2640
2641 assert_eq!(
2642 observable_boundary_basis(graph.top(), observable),
2643 Ok(Basis::Z)
2644 );
2645 for aligned in [false, true] {
2646 let text =
2647 emit_bloq_stim_with(&graph, &BloqStimOptions::new().with_align_moments(aligned))
2648 .unwrap();
2649 let input = text.find("OBSERVABLE_INCLUDE(3) Z0").unwrap();
2650 let measurement = text.find("M 0").unwrap();
2651 let output = text.rfind("OBSERVABLE_INCLUDE(3) Z0").unwrap();
2652 assert!(input < measurement && measurement < output, "{text}");
2653 assert!(text.contains("OBSERVABLE_INCLUDE(3) rec[-1]"), "{text}");
2654 }
2655
2656 let include = graph.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
2658 vec![],
2659 vec![InstanceBoundaryOperator {
2660 instance,
2661 face: BoundaryFace::Output,
2662 operator,
2663 }],
2664 )));
2665 graph.add_edge(include, observable, BloqEdge::compose(0));
2666 let text = emit_bloq_stim(&graph).unwrap();
2667 assert_eq!(
2668 text.matches("OBSERVABLE_INCLUDE(3) Z0").count(),
2669 1,
2670 "{text}"
2671 );
2672 assert!(text.find("OBSERVABLE_INCLUDE(3) Z0").unwrap() < text.find("M 0").unwrap());
2673 }
2674
2675 #[test]
2676 fn emit_bloq_stim_rejects_discard_node() {
2677 let mut bloq = Bloq::new();
2680 bloq.add_node(BloqNode::classical(ClassicalNode::Discard {
2681 condition: ClassicalExpr::Const(true),
2682 }));
2683 assert_eq!(
2684 emit_bloq_stim(&bloq),
2685 Err(StimEmissionError::UnsupportedNode(
2686 "Discard (shot postselection)"
2687 ))
2688 );
2689 }
2690
2691 #[test]
2692 fn emit_bloq_stim_skips_records_reducible_classical_node() {
2693 let mut bloq = Bloq::new();
2697 let input = bloq.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
2698 vec![],
2699 vec![],
2700 )));
2701 let compute = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
2702 expr: ClassicalExpr::In(0),
2703 }));
2704 bloq.add_edge(input, compute, BloqEdge::value(0));
2705 emit_bloq_stim(&bloq).unwrap();
2706 }
2707
2708 #[test]
2709 fn emit_bloq_stim_audits_only_when_requested() {
2710 let mut bloq = Bloq::new();
2711 bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
2712 expr: ClassicalExpr::In(0),
2713 }));
2714
2715 emit_bloq_stim(&bloq).unwrap();
2718 emit_bloq_stim_segments(&bloq).unwrap();
2719 emit_bloq_stim_with(&bloq, &BloqStimOptions::new().with_align_moments(true)).unwrap();
2720
2721 assert!(matches!(
2722 emit_bloq_stim_with(
2723 &bloq,
2724 &BloqStimOptions::new().with_trust(InputTrust::Checked)
2725 ),
2726 Err(StimEmissionError::InvalidProgram(
2727 bloq_ir::BloqValidationError::MissingClassicalInput { slot: 0, .. }
2728 ))
2729 ));
2730 assert!(matches!(
2731 emit_bloq_stim_with(
2732 &bloq,
2733 &BloqStimOptions::new()
2734 .with_trust(InputTrust::Checked)
2735 .with_align_moments(true)
2736 ),
2737 Err(StimEmissionError::InvalidProgram(
2738 bloq_ir::BloqValidationError::MissingClassicalInput { slot: 0, .. }
2739 ))
2740 ));
2741 }
2742
2743 #[test]
2744 fn static_emission_rejects_unpinned_membership_before_building_plans() {
2745 let mut bloq = Bloq::new();
2746 let mut node = BloqNode::from_members(Vec::new());
2747 node.expect_quantum_mut()
2748 .guards
2749 .push(bloq_ir::QuantumGuard {
2750 input: 0,
2751 ..Default::default()
2752 });
2753 bloq.add_node(node);
2754
2755 assert_eq!(
2756 emit_bloq_stim(&bloq),
2757 Err(StimEmissionError::UnsupportedNode(
2758 "conditional component membership"
2759 ))
2760 );
2761 assert_eq!(
2762 emit_bloq_stim_segments(&bloq).unwrap_err(),
2763 StimEmissionError::UnsupportedNode("conditional component membership")
2764 );
2765 }
2766
2767 #[test]
2768 fn aligned_emission_rejects_noise_and_segments() {
2769 let bloq = Bloq::new();
2770 let noise = NoiseModel::uniform_depolarizing(1e-3);
2771 let options = BloqStimOptions::new().with_align_moments(true);
2772
2773 assert_eq!(
2774 emit_bloq_stim_with(&bloq, &options.with_noise(&noise)),
2775 Err(StimEmissionError::AlignMomentsWithNoise)
2776 );
2777 assert!(matches!(
2778 emit_bloq_stim_segments_with(&bloq, &options),
2779 Err(StimEmissionError::AlignMomentsWithSegments)
2780 ));
2781 }
2782
2783 #[test]
2784 fn attempt_emission_preserves_typed_flatten_resource_errors() {
2785 let mut circuit = CoordCircuit::new();
2786 let body = circuit.add_body(CircuitBody::from_ops(vec![bloq_circuit::Op::Tick]));
2787 circuit.push_repeat(body, u32::MAX);
2788 let mut program = Bloq::new();
2789 let template = program.add_template(BloqTemplate::new(circuit));
2790 let mut node = BloqNode::from_members(Vec::new());
2791 node.expect_quantum_mut()
2792 .instances
2793 .push(TemplateInstance::new(
2794 TemplateInstanceId(0),
2795 template,
2796 ivec2(0, 0),
2797 ));
2798 program.add_node(node);
2799
2800 let error = emit_isolated_t_attempts(&program, 0.0).unwrap_err();
2801 assert!(std::error::Error::source(&error).is_some());
2802 assert!(matches!(error, StimEmissionError::Flatten(error) if error.is_resource_limited()));
2803 }
2804
2805 #[test]
2806 fn emit_bloq_stim_validates_before_capability_checks() {
2807 let mut bloq = Bloq::new();
2808 bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
2809 restart_condition: ClassicalExpr::In(0),
2810 restart_source: None,
2811 body: SubGraph::new(),
2812 }));
2813
2814 assert!(matches!(
2815 emit_bloq_stim_with(
2816 &bloq,
2817 &BloqStimOptions::new().with_trust(InputTrust::Checked)
2818 ),
2819 Err(StimEmissionError::InvalidProgram(
2820 bloq_ir::BloqValidationError::MissingRegionSelectorInput { slot: 0, .. }
2821 ))
2822 ));
2823 assert!(matches!(
2824 emit_bloq_stim_segments_with(
2825 &bloq,
2826 &BloqStimOptions::new().with_trust(InputTrust::Checked)
2827 ),
2828 Err(StimEmissionError::InvalidProgram(
2829 bloq_ir::BloqValidationError::MissingRegionSelectorInput { slot: 0, .. }
2830 ))
2831 ));
2832 }
2833
2834 #[test]
2835 fn emit_bloq_stim_rejects_nonlinear_compute() {
2836 let mut bloq = Bloq::new();
2840 let compute = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
2841 expr: ClassicalExpr::And(Box::new([ClassicalExpr::In(0), ClassicalExpr::In(1)])),
2842 }));
2843 let a = bloq.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
2845 vec![],
2846 vec![],
2847 )));
2848 let b = bloq.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
2849 vec![],
2850 vec![],
2851 )));
2852 bloq.add_edge(a, compute, BloqEdge::value(0));
2853 bloq.add_edge(b, compute, BloqEdge::value(1));
2854 assert_eq!(
2855 emit_bloq_stim(&bloq),
2856 Err(StimEmissionError::UnsupportedNode(
2857 "Compute (nonlinear And/Or condition requires conditional execution)"
2858 ))
2859 );
2860 }
2861
2862 #[test]
2863 fn shared_classical_dag_emits_canonical_record_parity() {
2864 let mut graph = Bloq::new();
2865 let node = measured_program_node(
2866 &mut graph,
2867 ivec3(0, 0, 0),
2868 ivec2(0, 0),
2869 TemplateInstanceId(0),
2870 );
2871 let quantum = graph.add_node(node);
2872 let measured = graph.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
2873 vec![InstanceMeasurement {
2874 instance: TemplateInstanceId(0),
2875 measurement: 0,
2876 }],
2877 vec![],
2878 )));
2879 graph.add_edge(quantum, measured, BloqEdge::Order);
2880 let mut previous = measured;
2881 for _ in 0..25 {
2882 let next = graph.add_node(BloqNode::classical(ClassicalNode::Compute {
2883 expr: ClassicalExpr::Xor(Box::new([ClassicalExpr::In(0), ClassicalExpr::In(1)])),
2884 }));
2885 graph.add_edge(previous, next, BloqEdge::value(0));
2886 graph.add_edge(previous, next, BloqEdge::value(1));
2887 previous = next;
2888 }
2889 let zero = graph.add_node(BloqNode::classical(ClassicalNode::observable(0)));
2890 graph.add_edge(previous, zero, BloqEdge::value(0));
2891 let nonzero = graph.add_node(BloqNode::classical(ClassicalNode::observable(1)));
2892 graph.add_edge(previous, nonzero, BloqEdge::value(0));
2893 graph.add_edge(measured, nonzero, BloqEdge::value(1));
2894 let text = emit_bloq_stim(&graph).unwrap();
2895 let observables = text
2896 .lines()
2897 .filter(|line| line.starts_with("OBSERVABLE_INCLUDE"))
2898 .collect::<Vec<_>>();
2899 assert_eq!(
2900 observables,
2901 ["OBSERVABLE_INCLUDE(0)", "OBSERVABLE_INCLUDE(1) rec[-1]"]
2902 );
2903 }
2904
2905 #[test]
2906 fn emit_bloq_stim_folds_linear_compute_into_observable() {
2907 let mut graph = Bloq::new();
2913 let parent_node = measured_program_node(
2914 &mut graph,
2915 ivec3(0, 0, 0),
2916 ivec2(0, 0),
2917 TemplateInstanceId(1),
2918 );
2919 let qubit = ivec2(0, 0);
2920 let mut template_circuit = CoordCircuit::new();
2921 template_circuit.measure(PauliBasis::Z, [qubit]);
2922 let template = graph.add_template(BloqTemplate::new(template_circuit));
2923 let mut node = BloqNode::from_members(vec![SourceBlockRef {
2924 pos: ivec3(0, 0, 0),
2925 }]);
2926 node.expect_quantum_mut()
2927 .instances
2928 .push(TemplateInstance::new(
2929 TemplateInstanceId(0),
2930 template,
2931 ivec2(1, 0),
2932 ));
2933 let parent = graph.add_node(parent_node);
2934 let child = graph.add_node(node);
2935 graph.add_edge(parent, child, BloqEdge::quantum(vec![]));
2936
2937 let accumulate = graph.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
2938 vec![InstanceMeasurement {
2939 instance: TemplateInstanceId(0),
2940 measurement: 0,
2941 }],
2942 vec![],
2943 )));
2944 graph.add_edge(child, accumulate, BloqEdge::Order);
2945
2946 let compute = graph.add_node(BloqNode::classical(ClassicalNode::Compute {
2949 expr: ClassicalExpr::Not(Box::new(ClassicalExpr::In(0))),
2950 }));
2951 graph.add_edge(accumulate, compute, BloqEdge::value(0));
2952 let observable = graph.add_node(BloqNode::classical(ClassicalNode::observable(1)));
2953 graph.add_edge(compute, observable, BloqEdge::value(0));
2954
2955 let text = emit_bloq_stim(&graph).expect("emit Bloq Stim");
2956 assert!(
2957 text.contains("OBSERVABLE_INCLUDE(1) rec[-1]"),
2958 "linear Compute condition should fold its measurement record into the observable: {text}"
2959 );
2960 }
2961
2962 fn measured_program_node(
2968 graph: &mut Bloq,
2969 block_pos: glam::IVec3,
2970 qubit: glam::IVec2,
2971 instance_id: TemplateInstanceId,
2972 ) -> BloqNode {
2973 let mut circuit = CoordCircuit::new();
2974 circuit.measure(PauliBasis::Z, [ivec2(0, 0)]);
2975 template_node_with_circuit_at(graph, circuit, block_pos, instance_id, qubit)
2976 }
2977
2978 fn template_node_with_circuit(graph: &mut Bloq, circuit: CoordCircuit) -> BloqNode {
2979 template_node_with_circuit_at(
2980 graph,
2981 circuit,
2982 ivec3(0, 0, 0),
2983 TemplateInstanceId(0),
2984 IVec2::ZERO,
2985 )
2986 }
2987
2988 fn template_node_with_circuit_at(
2989 graph: &mut Bloq,
2990 circuit: CoordCircuit,
2991 block_pos: glam::IVec3,
2992 instance_id: TemplateInstanceId,
2993 offset: IVec2,
2994 ) -> BloqNode {
2995 let template = graph.add_template(BloqTemplate::new(circuit));
2996 let mut node = BloqNode::from_members(vec![SourceBlockRef { pos: block_pos }]);
2997 node.expect_quantum_mut()
2998 .instances
2999 .push(TemplateInstance::new(instance_id, template, offset));
3000 node
3001 }
3002
3003 fn aligned_round_circuit(interaction_slots: usize) -> CoordCircuit {
3004 let data = ivec2(0, 0);
3005 let ancilla = ivec2(1, 0);
3006 let mut circuit = CoordCircuit::new();
3007 circuit.do_gate(GateType::RZ, [data, ancilla]).unwrap();
3008 circuit.tick();
3009 for _ in 0..interaction_slots {
3010 circuit.do_gate(GateType::CX, [data, ancilla]).unwrap();
3011 circuit.tick();
3012 }
3013 circuit.measure(PauliBasis::Z, [data, ancilla]);
3014 circuit
3015 }
3016
3017 fn aligned_rotation_rounds(rounds: usize) -> CoordCircuit {
3018 let qubit = ivec2(0, 0);
3019 let mut circuit = CoordCircuit::new();
3020 for round in 0..rounds {
3021 circuit.do_gate(GateType::RZ, [qubit]).unwrap();
3022 circuit.tick();
3023 circuit.do_gate(GateType::H, [qubit]).unwrap();
3024 circuit.tick();
3025 circuit.measure(PauliBasis::Z, [qubit]);
3026 if round + 1 != rounds {
3027 circuit.tick();
3028 }
3029 }
3030 circuit
3031 }
3032
3033 #[test]
3034 fn aligned_emission_merges_same_layer_slots_and_keeps_record_lookbacks() {
3035 let mut graph = Bloq::new();
3036 let mut first_quantum = None;
3037 for (instance, interaction_slots, offset) in [
3038 (TemplateInstanceId(0), 4, ivec2(0, 0)),
3039 (TemplateInstanceId(1), 5, ivec2(10, 0)),
3040 (TemplateInstanceId(2), 6, ivec2(20, 0)),
3041 ] {
3042 let mut template = BloqTemplate::new(aligned_round_circuit(interaction_slots));
3043 template.detectors.push(TemplateDetector {
3044 scope: TemplateDetectorScope::TopLevel,
3045 parity: DetectorParity::from_measurements([0]),
3046 coords: None,
3047 });
3048 let template = graph.add_template(template);
3049 let mut node = BloqNode::from_members(vec![SourceBlockRef {
3050 pos: ivec3(offset.x, 0, 0),
3051 }]);
3052 node.expect_quantum_mut()
3053 .instances
3054 .push(TemplateInstance::new(instance, template, offset));
3055 first_quantum.get_or_insert(graph.add_node(node));
3056 }
3057 let accumulate = graph.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
3058 vec![InstanceMeasurement {
3059 instance: TemplateInstanceId(0),
3060 measurement: 0,
3061 }],
3062 vec![],
3063 )));
3064 let observable = graph.add_node(BloqNode::classical(ClassicalNode::observable(7)));
3065 graph.add_edge(first_quantum.unwrap(), accumulate, BloqEdge::Order);
3066 graph.add_edge(accumulate, observable, BloqEdge::compose(0));
3067
3068 let text = emit_bloq_stim_with(&graph, &BloqStimOptions::new().with_align_moments(true))
3069 .expect("emit aligned Bloq Stim");
3070 let slots = text.split("TICK\n").collect::<Vec<_>>();
3071
3072 assert_eq!(
3073 slots.len() - 1,
3074 8,
3075 "four and five CX slots pad into six: {text}"
3076 );
3077 assert!(slots[0].contains("R 0 1"), "{text}");
3078 assert!(slots[0].contains("R 2 3"), "{text}");
3079 assert!(slots[0].contains("R 4 5"), "{text}");
3080 assert!(slots[7].contains("M 0 1"), "{text}");
3081 assert!(slots[7].contains("M 2 3"), "{text}");
3082 assert!(slots[7].contains("M 4 5"), "{text}");
3083 assert_eq!(text.matches("DETECTOR rec[-2]").count(), 3, "{text}");
3084 assert!(
3085 text.contains("OBSERVABLE_INCLUDE(7) rec[-6]"),
3086 "global frame must include every contributor: {text}"
3087 );
3088 }
3089
3090 #[test]
3091 fn aligned_emission_projects_timeline_rounds_into_source_layers() {
3092 let mut graph = Bloq::new();
3093 let mut tall = template_node_with_circuit_at(
3094 &mut graph,
3095 aligned_rotation_rounds(2),
3096 ivec3(0, 0, 2),
3097 TemplateInstanceId(0),
3098 ivec2(0, 0),
3099 );
3100 tall.expect_quantum_mut().timeline = Some(QuantumTimeline {
3101 layer_round_ends: vec![1, 2],
3102 });
3103 let tall = graph.add_node(tall);
3104 let upper = template_node_with_circuit_at(
3105 &mut graph,
3106 aligned_rotation_rounds(1),
3107 ivec3(1, 0, 3),
3108 TemplateInstanceId(1),
3109 ivec2(10, 0),
3110 );
3111 let upper = graph.add_node(upper);
3112
3113 let text = emit_bloq_stim_with(&graph, &BloqStimOptions::new().with_align_moments(true))
3114 .expect("emit timeline-aligned Bloq Stim");
3115 let slots = text.split("TICK\n").collect::<Vec<_>>();
3116
3117 assert_eq!(
3118 slots.len() - 1,
3119 6,
3120 "second tall round shares upper layer: {text}"
3121 );
3122 assert!(slots[0].contains(&format!("# node {}:", tall.0)), "{text}");
3123 assert!(slots[3].contains("R 0"), "{text}");
3124 assert!(slots[3].contains(&format!("# node {}:", upper.0)), "{text}");
3125 assert!(slots[3].contains("R 1"), "{text}");
3126 }
3127
3128 #[test]
3129 fn aligned_terminal_tick_keeps_detector_boundary_without_phantom_moment() {
3130 let mut circuit = CoordCircuit::new();
3131 circuit.measure(PauliBasis::Z, [ivec2(0, 0)]);
3132 circuit.tick();
3133 let mut graph = Bloq::new();
3134 let mut template = BloqTemplate::new(circuit);
3135 template.detectors.push(TemplateDetector {
3136 scope: TemplateDetectorScope::TopLevel,
3137 parity: DetectorParity::from_measurements([0]),
3138 coords: None,
3139 });
3140 let template = graph.add_template(template);
3141 let mut node = BloqNode::from_members(vec![SourceBlockRef {
3142 pos: ivec3(0, 0, 0),
3143 }]);
3144 node.expect_quantum_mut()
3145 .instances
3146 .push(TemplateInstance::new(
3147 TemplateInstanceId(0),
3148 template,
3149 IVec2::ZERO,
3150 ));
3151 graph.add_node(node);
3152
3153 let ordinary = emit_bloq_stim(&graph).unwrap();
3154 let aligned =
3155 emit_bloq_stim_with(&graph, &BloqStimOptions::new().with_align_moments(true)).unwrap();
3156
3157 assert_eq!(aligned, ordinary);
3158 assert_eq!(aligned.matches("TICK\n").count(), 2, "{aligned}");
3159 assert!(aligned.contains("M 0\nTICK\nDETECTOR rec[-1]\nTICK\n"));
3160 }
3161
3162 #[test]
3163 fn aligned_empty_circuit_is_silent_or_rejects_unplaceable_metadata() {
3164 let mut graph = Bloq::new();
3165 let node = template_node_with_circuit(&mut graph, CoordCircuit::new());
3166 let node = graph.add_node(node);
3167 let options = BloqStimOptions::new().with_align_moments(true);
3168
3169 assert_eq!(
3170 emit_bloq_stim_with(&graph, &options).unwrap(),
3171 emit_bloq_stim(&graph).unwrap()
3172 );
3173
3174 graph
3175 .node_mut(node)
3176 .unwrap()
3177 .expect_quantum_mut()
3178 .detectors
3179 .push(NodeDetector {
3180 parity: DetectorParity::<InstanceMeasurement>::default().with_sign(true),
3181 coords: None,
3182 });
3183 let error = emit_bloq_stim_with(&graph, &options).unwrap_err();
3184 assert_eq!(error, StimEmissionError::AlignMomentsWithEmptyMetadata);
3185 }
3186
3187 #[test]
3188 fn emit_bloq_stim_reports_unsupported_gate() {
3189 let qubit = ivec2(0, 0);
3190 let mut circuit = CoordCircuit::new();
3191 circuit.do_gate(GateType::T, [qubit]).unwrap();
3192 let mut graph = Bloq::new();
3193 let node = template_node_with_circuit(&mut graph, circuit);
3194 graph.add_node(node);
3195
3196 let error = emit_bloq_stim(&graph).unwrap_err();
3197
3198 assert_eq!(error, StimEmissionError::UnsupportedGate(GateType::T));
3199 }
3200
3201 #[test]
3202 fn unused_non_clifford_template_does_not_block_emission() {
3203 let qubit = ivec2(0, 0);
3204 let mut unused = CoordCircuit::new();
3205 unused.do_gate(GateType::T, [qubit]).unwrap();
3206 let mut graph = Bloq::new();
3207 graph.add_template(BloqTemplate::new(unused));
3208
3209 let mut used = CoordCircuit::new();
3210 used.do_gate(GateType::H, [qubit]).unwrap();
3211 let node = template_node_with_circuit(&mut graph, used);
3212 graph.add_node(node);
3213
3214 let text = emit_bloq_stim(&graph).expect("unused templates are not emitted");
3215 assert!(text.lines().any(|line| line == "H 0"), "{text}");
3216 }
3217
3218 #[test]
3219 fn unreachable_non_clifford_body_does_not_block_emission() {
3220 let qubit = ivec2(0, 0);
3221 let mut circuit = CoordCircuit::new();
3222 circuit.do_gate(GateType::H, [qubit]).unwrap();
3223 circuit.add_body(CircuitBody::from_ops(vec![Op::Gate {
3224 gate: GateType::T,
3225 qubits: vec![qubit],
3226 }]));
3227 let mut graph = Bloq::new();
3228 let node = template_node_with_circuit(&mut graph, circuit);
3229 graph.add_node(node);
3230
3231 let text = emit_bloq_stim(&graph).expect("unreachable bodies are not emitted");
3232 assert!(text.lines().any(|line| line == "H 0"), "{text}");
3233 }
3234
3235 #[test]
3236 fn include_boundary_face_fans_out_to_every_observable() {
3237 let qubit = ivec2(0, 0);
3238 let instance = TemplateInstanceId(0);
3239 for nested in [false, true] {
3240 let mut circuit = CoordCircuit::new();
3241 circuit.do_gate(GateType::X, [qubit]).unwrap();
3242 let mut graph = Bloq::new();
3243 let quantum = template_node_with_circuit(&mut graph, circuit);
3244 graph.add_node(quantum);
3245 let include = graph.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
3246 vec![],
3247 vec![InstanceBoundaryOperator {
3248 instance,
3249 face: BoundaryFace::Output,
3250 operator: [(qubit, bloq_circuit::Pauli::Z)].into_iter().collect(),
3251 }],
3252 )));
3253 let producer = if nested {
3254 let recipe = graph.add_node(BloqNode::classical(
3255 ClassicalNode::observable_fragment(vec![], vec![]),
3256 ));
3257 graph.add_edge(include, recipe, BloqEdge::compose(0));
3258 recipe
3259 } else {
3260 include
3261 };
3262 for index in [0, 1] {
3263 let observable =
3264 graph.add_node(BloqNode::classical(ClassicalNode::observable(index)));
3265 graph.add_edge(producer, observable, BloqEdge::compose(0));
3266 }
3267
3268 let text = emit_bloq_stim(&graph).expect("shared fragment emits for both consumers");
3269 let aligned =
3270 emit_bloq_stim_with(&graph, &BloqStimOptions::new().with_align_moments(true))
3271 .unwrap();
3272 for output in [&text, &aligned] {
3273 for index in [0, 1] {
3274 assert!(
3275 output.contains(&format!("OBSERVABLE_INCLUDE({index}) Z0")),
3276 "{output}"
3277 );
3278 }
3279 }
3280 assert!(aligned.contains("X 0\nTICK\nOBSERVABLE_INCLUDE(0) Z0"));
3281 if nested {
3282 for enabled in [false, true] {
3283 let mut guarded = graph.clone();
3284 let gate = guarded.add_node(BloqNode::classical(ClassicalNode::Compute {
3285 expr: ClassicalExpr::Const(enabled),
3286 }));
3287 guarded.node_mut(producer).unwrap().activation = Some(1);
3288 guarded.add_edge(gate, producer, BloqEdge::value(1));
3289 assert_eq!(
3290 emit_bloq_stim(&guarded),
3291 Err(StimEmissionError::UnsupportedNode(
3292 "activation (conditional execution)"
3293 ))
3294 );
3295 }
3296 }
3297 }
3298 }
3299
3300 #[test]
3301 fn indexed_composition_includes_boundaries_but_value_ports_do_not() {
3302 let qubit = ivec2(0, 0);
3303 let mut circuit = CoordCircuit::new();
3304 circuit.do_gate(GateType::X, [qubit]).unwrap();
3305 let mut graph = Bloq::new();
3306 let quantum = template_node_with_circuit(&mut graph, circuit);
3307 let quantum = graph.add_node(quantum);
3308 let child = graph.add_node(BloqNode::classical(ClassicalNode::Observable {
3309 index: Some(0),
3310 measurements: vec![],
3311 operators: vec![InstanceBoundaryOperator {
3312 instance: TemplateInstanceId(0),
3313 face: BoundaryFace::Output,
3314 operator: [(qubit, Pauli::Z)].into_iter().collect(),
3315 }],
3316 }));
3317 graph.add_edge(quantum, child, BloqEdge::Order);
3318 for (index, edge) in [
3319 (1, BloqEdge::compose(0)),
3320 (2, BloqEdge::value(0)),
3321 (3, BloqEdge::flip(0)),
3322 ] {
3323 let parent = graph.add_node(BloqNode::classical(ClassicalNode::observable(index)));
3324 graph.add_edge(child, parent, edge);
3325 }
3326 let text = emit_bloq_stim(&graph).unwrap();
3327 for index in [0, 1] {
3328 assert!(
3329 text.contains(&format!("OBSERVABLE_INCLUDE({index}) Z0")),
3330 "{text}"
3331 );
3332 }
3333 for index in [2, 3] {
3334 assert!(
3335 !text.contains(&format!("OBSERVABLE_INCLUDE({index}) Z0")),
3336 "{text}"
3337 );
3338 }
3339 }
3340
3341 #[test]
3342 fn repeated_recipe_paths_cancel_boundary_faces() {
3343 let qubit = ivec2(0, 0);
3344 let mut circuit = CoordCircuit::new();
3345 circuit.do_gate(GateType::X, [qubit]).unwrap();
3346 let mut graph = Bloq::new();
3347 let quantum = template_node_with_circuit(&mut graph, circuit);
3348 graph.add_node(quantum);
3349 let mut root = graph.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
3350 vec![],
3351 vec![InstanceBoundaryOperator {
3352 instance: TemplateInstanceId(0),
3353 face: BoundaryFace::Output,
3354 operator: [(qubit, bloq_circuit::Pauli::Z)].into_iter().collect(),
3355 }],
3356 )));
3357 for _ in 0..24 {
3358 let next = graph.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
3359 vec![],
3360 vec![],
3361 )));
3362 graph.add_edge(root, next, BloqEdge::compose(0));
3363 graph.add_edge(root, next, BloqEdge::compose(1));
3364 root = next;
3365 }
3366 let observable = graph.add_node(BloqNode::classical(ClassicalNode::observable(0)));
3367 graph.add_edge(root, observable, BloqEdge::compose(0));
3368 let text = emit_bloq_stim(&graph).unwrap();
3369 assert!(!text.contains("OBSERVABLE_INCLUDE(0) Z0"), "{text}");
3370 }
3371
3372 #[test]
3373 fn emit_bloq_stim_instantiates_template_node_side_tables() {
3374 let mut graph = Bloq::new();
3375 let parent_node = measured_program_node(
3376 &mut graph,
3377 ivec3(0, 0, 0),
3378 ivec2(0, 0),
3379 TemplateInstanceId(1),
3380 );
3381 let qubit = ivec2(0, 0);
3382 let mut template_circuit = CoordCircuit::new();
3383 template_circuit.measure(PauliBasis::Z, [qubit]);
3384 let mut bloq_template = BloqTemplate::new(template_circuit);
3385 bloq_template.detectors.push(TemplateDetector {
3386 scope: TemplateDetectorScope::TopLevel,
3387 parity: DetectorParity::from_measurements([0]),
3388 coords: Some(bloq_circuit::DetectorCoords::from_slice(&[3.0, 4.0])),
3389 });
3390 let template = graph.add_template(bloq_template);
3391 let mut node = BloqNode::from_members(vec![SourceBlockRef {
3392 pos: ivec3(0, 0, 0),
3393 }]);
3394 node.expect_quantum_mut()
3395 .instances
3396 .push(TemplateInstance::new(
3397 TemplateInstanceId(0),
3398 template,
3399 ivec2(1, 0),
3400 ));
3401 let parent = graph.add_node(parent_node);
3402 let child = graph.add_node(node);
3403 graph.add_edge(parent, child, BloqEdge::quantum(vec![]));
3404
3405 let accumulate = graph.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
3408 vec![InstanceMeasurement {
3409 instance: TemplateInstanceId(0),
3410 measurement: 0,
3411 }],
3412 vec![],
3413 )));
3414 let observable = graph.add_node(BloqNode::classical(ClassicalNode::observable(0)));
3415 graph.add_edge(child, accumulate, BloqEdge::Order);
3416 graph.add_edge(accumulate, observable, BloqEdge::compose(0));
3417
3418 let text = emit_bloq_stim(&graph).expect("emit Bloq Stim");
3419
3420 insta::assert_snapshot!(text, @"
3421 QUBIT_COORDS(0, 0) 0
3422 QUBIT_COORDS(1, 0) 1
3423 # node 0: quantum blocks (0,0,0)
3424 M 0
3425 TICK
3426 # node 1: quantum blocks (0,0,0)
3427 M 1
3428 DETECTOR(4, 4) rec[-1]
3429 TICK
3430 # node 3: observable 0 synthetic
3431 OBSERVABLE_INCLUDE(0) rec[-1]
3432 ");
3433 }
3434
3435 #[test]
3436 fn idle_noise_expands_repeat_annotations_and_preserves_segment_records() {
3437 let (a, b) = (ivec2(0, 0), ivec2(1, 0));
3438 let mut circuit = CoordCircuit::new();
3439 circuit.do_gate(GateType::RZ, [a]).unwrap();
3440 let measurement = circuit.reserve_measurement_id(b);
3441 let body = circuit.add_body(CircuitBody::from_ops(vec![
3442 Op::Measure {
3443 basis: PauliBasis::Z,
3444 qubits: vec![b],
3445 measurements: vec![measurement],
3446 flip_probability: 0.0,
3447 },
3448 Op::Tick,
3449 ]));
3450 circuit.push_repeat(body, 3);
3451 let mut template = BloqTemplate::new(circuit);
3452 template.detectors.push(TemplateDetector {
3453 scope: TemplateDetectorScope::RepeatBody { body },
3454 parity: DetectorParity::from_terms([
3455 DetectorTerm::Measurement(measurement),
3456 DetectorTerm::LoopState(LoopStateId(0)),
3457 ]),
3458 coords: None,
3459 });
3460 template.repeat_states.push(TemplateRepeatState {
3461 body,
3462 state: LoopStateId(0),
3463 initial: DetectorParity::default(),
3464 next: DetectorParity::from_measurements([measurement]),
3465 });
3466 template.restarts.push(bloq_ir::lowering::TemplateRestart {
3467 parity: DetectorParity::from_terms([DetectorTerm::LoopState(LoopStateId(0))]),
3468 });
3469 let mut without_restart = template.clone();
3470 without_restart.restarts.clear();
3471 let mut program = Bloq::new();
3472 let template_id = program.add_template(template);
3473 let mut node = BloqNode::from_members(vec![]);
3474 node.expect_quantum_mut()
3475 .instances
3476 .push(TemplateInstance::new(
3477 TemplateInstanceId(0),
3478 template_id,
3479 IVec2::ZERO,
3480 ));
3481 let noise = NoiseModel {
3482 p_idle: 0.125,
3483 ..NoiseModel::uniform_depolarizing(0.0)
3484 };
3485 let plan = node
3486 .emission_plan_with_options(program.templates(), InstantiationOptions::noisy(&noise))
3487 .unwrap();
3488 let normalized = &plan.templates(program.templates())[template_id];
3489 assert_eq!(normalized.detectors.len(), 3);
3490 assert!(normalized.repeat_states.is_empty());
3491 assert_eq!(
3492 normalized.restarts[0].parity,
3493 DetectorParity::from_measurements([measurement])
3494 );
3495 assert_eq!(
3496 plan.circuit.expanded_measurement_columns().unwrap().count(),
3497 3
3498 );
3499
3500 node.expect_quantum_mut().instances[0].template_id = program.add_template(without_restart);
3503 let producer = program.add_node(node);
3504 let record = program.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
3505 vec![InstanceMeasurement {
3506 instance: TemplateInstanceId(0),
3507 measurement,
3508 }],
3509 vec![],
3510 )));
3511 let observable = program.add_node(BloqNode::classical(ClassicalNode::observable(0)));
3512 program.add_edge(producer, record, BloqEdge::Order);
3513 program.add_edge(record, observable, BloqEdge::compose(0));
3514 let (clean, noisy) = emit_bloq_stim_segments_pair(&program, &noise).unwrap();
3515 assert_eq!(clean.num_measurements(), 3);
3516 assert_eq!(noisy.num_measurements(), 3);
3517 assert_eq!(
3518 noisy.to_text().matches("DEPOLARIZE1(0.125) 0").count(),
3519 2,
3520 "the leading reset occupies a's first moment, but a idles on later iterations"
3521 );
3522 assert_eq!(noisy.to_text().matches("DETECTOR").count(), 3);
3523 for text in [clean.to_text(), noisy.to_text()] {
3524 assert!(
3525 text.contains("OBSERVABLE_INCLUDE(0) rec[-1]"),
3526 "the original measurement id still names the final loop record: {text}"
3527 );
3528 }
3529 let mut flat = program.clone();
3530 flat.flatten().unwrap();
3531 let expected =
3532 emit_bloq_stim_with(&flat, &BloqStimOptions::new().with_noise(&noise)).unwrap();
3533 assert_eq!(noisy.to_text(), expected);
3534 #[cfg(feature = "verify")]
3535 for text in [clean.to_text(), noisy.to_text()] {
3536 let circuit: stim::Circuit = text.parse().unwrap();
3537 assert_eq!(circuit.num_measurements(), 3);
3538 assert_eq!(circuit.num_detectors(), 3);
3539 }
3540 }
3541
3542 #[test]
3543 fn emit_bloq_stim_instantiates_template_repeat_side_tables() {
3544 let qubit = ivec2(0, 0);
3545 let mut template_circuit = CoordCircuit::new();
3546 let body = template_circuit.add_body(CircuitBody::from_ops(vec![Op::Measure {
3547 basis: PauliBasis::Z,
3548 qubits: vec![qubit],
3549 measurements: vec![0],
3550 flip_probability: 0.0,
3551 }]));
3552 template_circuit.register_measurement_id(0, qubit);
3553 template_circuit.push_repeat(body, 2);
3554 let mut bloq_template = BloqTemplate::new(template_circuit);
3555 bloq_template.detectors.push(TemplateDetector {
3556 scope: TemplateDetectorScope::RepeatBody { body },
3557 parity: DetectorParity::from_measurements([0]),
3558 coords: None,
3559 });
3560 bloq_template.repeat_states.push(TemplateRepeatState {
3561 body,
3562 state: LoopStateId(0),
3563 initial: DetectorParity::default(),
3564 next: DetectorParity::from_measurements([0]),
3565 });
3566 let mut graph = Bloq::new();
3567 let template = graph.add_template(bloq_template);
3568 let mut node = BloqNode::from_members(vec![SourceBlockRef {
3569 pos: ivec3(0, 0, 0),
3570 }]);
3571 node.expect_quantum_mut()
3572 .instances
3573 .push(TemplateInstance::new(
3574 TemplateInstanceId(0),
3575 template,
3576 qubit,
3577 ));
3578 graph.add_node(node);
3579
3580 let text = emit_bloq_stim(&graph).expect("emit Bloq Stim");
3581
3582 insta::assert_snapshot!(text, @"
3583 QUBIT_COORDS(0, 0) 0
3584 # node 0: quantum blocks (0,0,0)
3585 REPEAT 2 {
3586 M 0
3587 DETECTOR rec[-1]
3588 }
3589 ");
3590 }
3591
3592 #[test]
3593 fn emit_bloq_stim_emits_node_circuits_in_topological_order() {
3594 let mut graph = Bloq::new();
3595 let parent_node = measured_program_node(
3596 &mut graph,
3597 ivec3(0, 0, 0),
3598 ivec2(0, 0),
3599 TemplateInstanceId(0),
3600 );
3601 let child_node = measured_program_node(
3602 &mut graph,
3603 ivec3(0, 0, 1),
3604 ivec2(1, 0),
3605 TemplateInstanceId(1),
3606 );
3607 let child = graph.add_node(child_node);
3609 let parent = graph.add_node(parent_node);
3610 graph.add_edge(parent, child, BloqEdge::quantum(vec![]));
3611
3612 let text = emit_bloq_stim(&graph).expect("emit Bloq Stim");
3613
3614 let measurement_lines = text
3615 .lines()
3616 .filter(|line| line.starts_with("M "))
3617 .collect::<Vec<_>>();
3618 assert_eq!(measurement_lines, vec!["M 0", "M 1"], "{text}");
3619 }
3620
3621 #[test]
3622 fn emit_bloq_stim_resolves_cross_node_detector_refs() {
3623 let mut graph = Bloq::new();
3624 let mut parent_circuit = CoordCircuit::new();
3625 parent_circuit.measure(PauliBasis::Z, [ivec2(0, 0)]);
3626 let parent_node = template_node_with_circuit_at(
3627 &mut graph,
3628 parent_circuit,
3629 ivec3(0, 0, 0),
3630 TemplateInstanceId(0),
3631 IVec2::ZERO,
3632 );
3633 let mut child_circuit = CoordCircuit::new();
3634 child_circuit.measure(PauliBasis::Z, [ivec2(1, 0)]);
3635 let mut child_node = template_node_with_circuit_at(
3636 &mut graph,
3637 child_circuit,
3638 ivec3(0, 0, 1),
3639 TemplateInstanceId(1),
3640 IVec2::ZERO,
3641 );
3642 child_node
3643 .expect_quantum_mut()
3644 .detectors
3645 .push(NodeDetector {
3646 parity: DetectorParity::from_measurements([
3647 InstanceMeasurement {
3648 instance: TemplateInstanceId(0),
3649 measurement: 0,
3650 },
3651 InstanceMeasurement {
3652 instance: TemplateInstanceId(1),
3653 measurement: 0,
3654 },
3655 ]),
3656 coords: Some(bloq_circuit::DetectorCoords::from_slice(&[5.0, 6.0])),
3657 });
3658
3659 let parent = graph.add_node(parent_node);
3660 let child = graph.add_node(child_node);
3661 graph.add_edge(parent, child, BloqEdge::quantum(vec![]));
3662
3663 let text = emit_bloq_stim(&graph).expect("emit Bloq Stim");
3664
3665 assert!(text.contains("DETECTOR(5, 6) rec[-2] rec[-1]"), "{text}");
3666 }
3667
3668 #[test]
3669 fn emit_bloq_stim_resolves_shared_cross_node_detector() {
3670 use bloq_ir::{BundleDetector, BundleMeasurement, DetectorBundle, DetectorBundleUse};
3671
3672 let mut graph = Bloq::new();
3673 let mut parent_circuit = CoordCircuit::new();
3674 parent_circuit.measure(PauliBasis::Z, [ivec2(0, 0)]);
3675 let parent_node = template_node_with_circuit_at(
3676 &mut graph,
3677 parent_circuit,
3678 ivec3(0, 0, 0),
3679 TemplateInstanceId(0),
3680 IVec2::ZERO,
3681 );
3682 let mut child_circuit = CoordCircuit::new();
3683 child_circuit.measure(PauliBasis::Z, [ivec2(1, 0)]);
3684 let mut child_node = template_node_with_circuit_at(
3685 &mut graph,
3686 child_circuit,
3687 ivec3(0, 0, 1),
3688 TemplateInstanceId(1),
3689 IVec2::ZERO,
3690 );
3691 let owner_templates = vec![
3692 parent_node.expect_quantum().instances[0].template_id,
3693 child_node.expect_quantum().instances[0].template_id,
3694 ];
3695 let bundle = graph.add_detector_bundle(DetectorBundle::new(
3696 owner_templates,
3697 vec![BundleDetector {
3698 parity: bloq_circuit::DetectorParity::from_measurements([
3699 BundleMeasurement {
3700 owner: 0,
3701 measurement: 0,
3702 },
3703 BundleMeasurement {
3704 owner: 1,
3705 measurement: 0,
3706 },
3707 ]),
3708 coords: Some(bloq_circuit::DetectorCoords::from_slice(&[1.0, 2.0])),
3709 }],
3710 ));
3711 child_node
3712 .expect_quantum_mut()
3713 .detector_bundles
3714 .push(DetectorBundleUse {
3715 bundle,
3716 instances: vec![TemplateInstanceId(0), TemplateInstanceId(1)],
3717 offset: IVec2::new(4, 4),
3718 });
3719
3720 let parent = graph.add_node(parent_node);
3721 let child = graph.add_node(child_node);
3722 graph.add_edge(parent, child, BloqEdge::quantum(vec![]));
3723
3724 let text = emit_bloq_stim(&graph).expect("emit shared detector");
3725 assert!(text.contains("DETECTOR(5, 6) rec[-2] rec[-1]"), "{text}");
3726
3727 let wrong_bundle = graph.add_detector_bundle(DetectorBundle::new(
3728 vec![graph[child].expect_quantum().instances[0].template_id; 2],
3729 vec![],
3730 ));
3731 graph
3732 .node_mut(child)
3733 .unwrap()
3734 .expect_quantum_mut()
3735 .detector_bundles[0]
3736 .bundle = wrong_bundle;
3737 assert!(matches!(
3738 emit_bloq_stim(&graph),
3739 Err(StimEmissionError::DetectorBundle(
3740 bloq_ir::DetectorBundleError::OwnerTemplateMismatch { .. }
3741 ))
3742 ));
3743 }
3744
3745 #[test]
3746 fn ordinary_emission_rejects_malformed_bundle_parity() {
3747 use bloq_ir::{BundleDetector, BundleMeasurement, DetectorBundle, DetectorBundleUse};
3748
3749 for term in [
3750 DetectorTerm::Measurement(BundleMeasurement {
3751 owner: 0,
3752 measurement: 99,
3753 }),
3754 DetectorTerm::LoopState(LoopStateId(7)),
3755 ] {
3756 let mut graph = Bloq::new();
3757 let mut circuit = CoordCircuit::new();
3758 circuit.measure(PauliBasis::Z, [IVec2::ZERO]);
3759 let mut node = template_node_with_circuit_at(
3760 &mut graph,
3761 circuit,
3762 ivec3(0, 0, 0),
3763 TemplateInstanceId(0),
3764 IVec2::ZERO,
3765 );
3766 let template = node.expect_quantum().instances[0].template_id;
3767 let bundle = graph.add_detector_bundle(DetectorBundle::new(
3768 vec![template],
3769 vec![BundleDetector {
3770 parity: DetectorParity::from_terms([term]),
3771 coords: None,
3772 }],
3773 ));
3774 node.expect_quantum_mut()
3775 .detector_bundles
3776 .push(DetectorBundleUse {
3777 bundle,
3778 instances: vec![TemplateInstanceId(0)],
3779 offset: IVec2::ZERO,
3780 });
3781 graph.add_node(node);
3782
3783 let error = emit_bloq_stim(&graph).expect_err("malformed annotation must fail");
3784 match term {
3785 DetectorTerm::Measurement(_) => assert_eq!(
3786 error,
3787 StimEmissionError::UnknownInstanceMeasurement(InstanceMeasurement {
3788 instance: TemplateInstanceId(0),
3789 measurement: 99,
3790 }),
3791 ),
3792 DetectorTerm::LoopState(state) => {
3793 assert_eq!(error, StimEmissionError::NodeLoopStateUnsupported(state),)
3794 }
3795 }
3796 }
3797 }
3798
3799 #[test]
3800 fn emit_bloq_stim_maps_feedforward_control_to_its_instance_record() {
3801 use bloq_circuit::ConditionalCorrection;
3802 let mut graph = Bloq::new();
3803 let mut parent_circuit = CoordCircuit::new();
3804 parent_circuit.measure(PauliBasis::Z, [ivec2(0, 0)]);
3805 let parent_node = template_node_with_circuit_at(
3806 &mut graph,
3807 parent_circuit,
3808 ivec3(0, 0, 0),
3809 TemplateInstanceId(0),
3810 IVec2::ZERO,
3811 );
3812
3813 let mut child_circuit = CoordCircuit::new();
3814 let control = child_circuit.measure(PauliBasis::Z, [ivec2(1, 0)])[0];
3815 child_circuit
3816 .body_mut(child_circuit.entry_body())
3817 .expect("entry body exists")
3818 .ops_mut()
3819 .push(Op::ConditionalPauli(vec![ConditionalCorrection {
3820 pauli: PauliBasis::X,
3821 control,
3822 target: ivec2(2, 0),
3823 }]));
3824 child_circuit
3825 .do_gate(bloq_circuit::GateType::RZ, [ivec2(2, 0)])
3826 .unwrap();
3827 let child_node = template_node_with_circuit_at(
3828 &mut graph,
3829 child_circuit,
3830 ivec3(0, 0, 1),
3831 TemplateInstanceId(1),
3832 IVec2::ZERO,
3833 );
3834
3835 let parent = graph.add_node(parent_node);
3836 let child = graph.add_node(child_node);
3837 graph.add_edge(parent, child, BloqEdge::quantum(vec![]));
3838
3839 for aligned in [false, true] {
3840 let text =
3841 emit_bloq_stim_with(&graph, &BloqStimOptions::new().with_align_moments(aligned))
3842 .expect("emit measurement feed-forward");
3843 assert!(text.contains("M 1\nCX rec[-1] 2"), "{text}");
3844 assert!(!text.contains("CX rec[-2] 2"), "{text}");
3845 assert!(
3846 text.find("CX rec[-1] 2").unwrap() < text.find("R 2").unwrap(),
3847 "{text}"
3848 );
3849 #[cfg(feature = "verify")]
3850 text.parse::<stim::Circuit>()
3851 .expect("program feed-forward should parse as Stim");
3852 }
3853 }
3854
3855 #[test]
3856 fn emit_bloq_stim_summarizes_nested_large_repeat_frames() {
3857 let qubit = ivec2(0, 0);
3861 let mut circuit = CoordCircuit::new();
3862 let measurement = circuit.reserve_measurement_id(qubit);
3863 let inner = circuit.add_body(bloq_circuit::CircuitBody::from_ops(vec![Op::Measure {
3864 basis: PauliBasis::Z,
3865 qubits: vec![qubit],
3866 measurements: vec![measurement],
3867 flip_probability: 0.0,
3868 }]));
3869 let outer = circuit.add_body(bloq_circuit::CircuitBody::from_ops(vec![Op::Repeat {
3870 body: inner,
3871 repetitions: 1_000_000,
3872 }]));
3873 circuit.push_repeat(outer, 2);
3874 let mut graph = Bloq::new();
3875 let mut node = template_node_with_circuit(&mut graph, circuit);
3876 node.expect_quantum_mut().detectors.push(NodeDetector {
3877 parity: DetectorParity::from_measurements([InstanceMeasurement {
3878 instance: TemplateInstanceId(0),
3879 measurement,
3880 }]),
3881 coords: None,
3882 });
3883 graph.add_node(node);
3884
3885 let text = emit_bloq_stim(&graph).expect("emit Bloq Stim");
3886
3887 assert!(text.contains("REPEAT 2 {"), "{text}");
3888 assert!(text.contains("REPEAT 1000000 {"), "{text}");
3889 assert_eq!(
3890 text.lines().filter(|line| line.trim() == "M 0").count(),
3891 1,
3892 "{text}"
3893 );
3894 assert!(text.contains("DETECTOR rec[-1]"), "{text}");
3895 }
3896}