1use std::cmp::Ordering;
7
8use bloq_circuit::{ComposedChain, DetectorCoords, DetectorTerm, FlowEngine, OffsetFlows, Op};
9use glam::IVec2;
10use petgraph::Direction;
11use petgraph::stable_graph::NodeIndex;
12use petgraph::visit::EdgeRef;
13use thiserror::Error;
14
15use crate::{
16 Bloq, BloqEdge, BloqNode, BloqNodeId, BloqNodeKind, BloqTemplate, BloqTemplatePool,
17 BoundaryFace, ClassicalNode, FxMap, FxSet, InstanceMeasurement, LevelPath, NodeDetector,
18 NodeRestart, PipePadding, PipeSeam, QuantumEdge, SubGraph, TemplateId, TemplateInstance,
19 TemplateInstanceId, TemporalPipeRef,
20};
21
22#[derive(Debug, Error, Clone, PartialEq, Eq)]
24#[non_exhaustive]
25pub enum EditError {
26 #[error("{0}")]
28 DetectorBundle(#[from] crate::DetectorBundleError),
29 #[error("memory edits require pinned component membership; call Bloq::pin_membership first")]
31 MembershipSelectionRequired,
32 #[error("{0}")]
34 MembershipAnalysis(#[from] crate::NodeTemplateInstanceMergeError),
35 #[error("memory-round target has invalid level path {path:?}")]
37 InvalidLevelPath {
38 path: LevelPath,
40 },
41 #[error("memory-round target node {node:?} does not exist at {path:?}")]
43 MemoryRoundTargetMissing {
44 path: LevelPath,
46 node: BloqNodeId,
48 },
49 #[error("memory-round target node {node:?} at {path:?} is not quantum")]
51 MemoryRoundTargetNotQuantum {
52 path: LevelPath,
54 node: BloqNodeId,
56 },
57 #[error(
59 "quantum edge {from:?} -> {to:?} is not a single non-empty seam between quantum nodes or into a region"
60 )]
61 InvalidQuantumEdge {
62 from: BloqNodeId,
64 to: BloqNodeId,
66 },
67 #[error("padding must be non-empty")]
69 EmptyPadding,
70 #[error("not enough template-instance ids remain for padding")]
72 TemplateInstanceIdExhausted,
73 #[error("padding references unknown template {template:?}")]
75 UnknownTemplate {
76 template: TemplateId,
78 },
79 #[error("seam flow composition failed: {source}")]
81 SeamCompositionFailed {
82 #[source]
84 source: bloq_circuit::FlowError,
85 },
86 #[error("quantum edge {from:?} -> {to:?} has no padding provenance")]
88 EdgePaddingMissing {
89 from: BloqNodeId,
91 to: BloqNodeId,
93 },
94 #[error("looped padding template {template:?} must contain one top-level REPEAT")]
96 PaddingTemplateNotLooped {
97 template: TemplateId,
99 },
100 #[error("template {template:?} has {repeats} top-level REPEATs, expected exactly one")]
102 TemplateNotSingleLooped {
103 template: TemplateId,
105 repeats: usize,
107 },
108 #[error("stored seam is inconsistent with template boundary flows")]
110 InvalidStoredSeam,
111 #[error("padding does not reproduce the stored seam")]
113 SeamRecompositionMismatch,
114 #[error("memory rounds can only be appended after a node inside a region")]
116 AfterTargetTopLevel,
117 #[error("node {node:?} is not terminal: it already has an outgoing quantum edge")]
119 AfterTargetNotTerminal {
120 node: BloqNodeId,
122 },
123 #[error("node {node:?} has no matching padding provenance on its enclosing region boundary")]
125 AfterPaddingMissing {
126 node: BloqNodeId,
128 },
129 #[error("node {node:?} has ambiguous terminal instance placement at offset {offset:?}")]
131 AfterPaddingAmbiguous {
132 node: BloqNodeId,
134 offset: IVec2,
136 },
137 #[error("region boundary target {node:?} is not quantum")]
139 AfterBoundaryTargetNotQuantum {
140 node: BloqNodeId,
142 },
143 #[error("terminal padding does not reproduce the enclosing region boundary")]
145 TerminalRecompositionMismatch,
146}
147
148fn demote_touched_bundles(
151 program: &Bloq,
152 quantum: &mut crate::QuantumNode,
153 touched: &FxSet<TemplateInstanceId>,
154) -> Result<(), EditError> {
155 if quantum.detector_bundles.is_empty() {
156 return Ok(());
157 }
158 program.node_detector_count(quantum)?;
159 let mut registered = FxSet::default();
160 for guard in &quantum.guards {
161 for &index in &guard.detector_bundles {
162 if index as usize >= quantum.detector_bundles.len() || !registered.insert(index) {
163 return Err(
164 crate::NodeTemplateInstanceMergeError::InvalidMembership(format!(
165 "bundle use {index} is missing or registered more than once"
166 ))
167 .into(),
168 );
169 }
170 }
171 }
172 let mut remap = Vec::with_capacity(quantum.detector_bundles.len());
173 let mut inline = Vec::with_capacity(quantum.detector_bundles.len());
174 let mut kept = Vec::with_capacity(quantum.detector_bundles.len());
175 for use_ in &quantum.detector_bundles {
176 let bundle = program
177 .detector_bundles()
178 .get(use_.bundle)
179 .ok_or(crate::DetectorBundleError::UnknownBundle(use_.bundle))?;
180 if bundle
181 .used_owners()?
182 .iter()
183 .any(|&owner| touched.contains(&use_.instances[owner as usize]))
184 {
185 let one = crate::QuantumNode {
186 detector_bundles: vec![use_.clone()],
187 ..Default::default()
188 };
189 let start = u32::try_from(quantum.detectors.len())
190 .map_err(|_| crate::DetectorBundleError::CountOverflow)?;
191 let end = start
192 .checked_add(
193 u32::try_from(bundle.detectors().len())
194 .map_err(|_| crate::DetectorBundleError::CountOverflow)?,
195 )
196 .ok_or(crate::DetectorBundleError::CountOverflow)?;
197 let indices = start..end;
198 inline.push(Some(indices));
199 quantum
200 .detectors
201 .extend(program.node_detectors(&one)?.map(|row| row.to_owned()));
202 remap.push(None);
203 } else {
204 remap.push(Some(kept.len() as u32));
205 inline.push(None);
206 kept.push(use_.clone());
207 }
208 }
209 quantum.detector_bundles = kept;
210 for guard in &mut quantum.guards {
211 let mut uses = Vec::new();
212 for &index in &guard.detector_bundles {
213 if let Some(indices) = &inline[index as usize] {
214 guard.detectors.extend(indices.clone());
215 }
216 if let Some(index) = remap[index as usize] {
217 uses.push(index);
218 }
219 }
220 guard.detector_bundles = uses;
221 }
222 Ok(())
223}
224
225#[derive(Debug, Clone, PartialEq, Eq, Hash)]
227#[non_exhaustive]
228pub enum MemoryRoundTarget {
229 Edge {
231 path: LevelPath,
233 from: BloqNodeId,
235 to: BloqNodeId,
237 },
238 After {
240 path: LevelPath,
242 node: BloqNodeId,
244 },
245}
246
247#[derive(Debug, Clone, Copy)]
251pub struct PaddingInstance {
252 pub template: TemplateId,
254 pub offset: IVec2,
256}
257
258#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
261pub(crate) struct PaddingVariant {
262 pub(crate) base: TemplateId,
263 pub(crate) rounds: u32,
264}
265
266#[derive(Clone, Copy)]
267struct TerminalMember {
268 source: TemplateInstance,
269 pipe: TemporalPipeRef,
270 padding: PipePadding,
271}
272
273struct BoundaryRewrite {
274 target: BloqNodeId,
275 quantum: crate::QuantumNode,
276}
277
278struct QuantumBoundaryEntry {
279 path: LevelPath,
280 target: BloqNodeId,
281}
282
283struct QuantumBoundaryRewrite {
284 path: LevelPath,
285 target: BloqNodeId,
286 quantum: crate::QuantumNode,
287}
288
289#[derive(Debug, Default)]
291struct SeamComposition {
292 detectors: Vec<(usize, NodeDetector)>,
293 restarts: Vec<(usize, NodeRestart)>,
294}
295
296impl SeamComposition {
297 fn split_first(self) -> [(Vec<NodeDetector>, Vec<NodeRestart>); 2] {
299 let mut entries = [(Vec::new(), Vec::new()), (Vec::new(), Vec::new())];
300 for (position, detector) in self.detectors {
301 entries[usize::from(position != 0)].0.push(detector);
302 }
303 for (position, restart) in self.restarts {
304 entries[usize::from(position != 0)].1.push(restart);
305 }
306 entries
307 }
308
309 fn after(self, position: usize) -> (Vec<NodeDetector>, Vec<NodeRestart>) {
314 let keep = |candidate: usize| candidate > position;
315 (
316 self.detectors
317 .into_iter()
318 .filter_map(|(at, detector)| keep(at).then_some(detector))
319 .collect(),
320 self.restarts
321 .into_iter()
322 .filter_map(|(at, restart)| keep(at).then_some(restart))
323 .collect(),
324 )
325 }
326}
327
328fn compose_seam_detectors(
334 templates: &BloqTemplatePool,
335 nodes: &[&[TemplateInstance]],
336) -> Result<SeamComposition, EditError> {
337 let mut engine: FlowEngine<'_, DetectorTerm<InstanceMeasurement>> = FlowEngine::new();
338 let mut composition = SeamComposition::default();
339 for (position, instances) in nodes.iter().enumerate() {
340 for instance in *instances {
341 let template =
342 templates
343 .get(instance.template_id)
344 .ok_or(EditError::UnknownTemplate {
345 template: instance.template_id,
346 })?;
347 if template.boundary_flows.is_empty() {
348 continue;
349 }
350 let parts = [OffsetFlows::new(&template.boundary_flows, instance.offset)];
351 engine
352 .append_group_skipping_unmatched_inputs(&parts, |_, &measurement| {
353 DetectorTerm::Measurement(InstanceMeasurement {
354 instance: instance.id,
355 measurement,
356 })
357 })
358 .map_err(|source| EditError::SeamCompositionFailed { source })?;
359 let (detectors, restarts) = drain_composed_chains(&mut engine);
360 composition
361 .detectors
362 .extend(detectors.into_iter().map(|detector| (position, detector)));
363 composition
364 .restarts
365 .extend(restarts.into_iter().map(|restart| (position, restart)));
366 }
367 }
368 Ok(composition)
370}
371
372fn removal_mask<T: PartialEq>(stored: &[T], seam: &[T]) -> Option<Vec<bool>> {
374 let mut remove = vec![false; stored.len()];
375 for expected in seam {
376 let index = stored
377 .iter()
378 .enumerate()
379 .find_map(|(index, item)| (!remove[index] && item == expected).then_some(index))?;
380 remove[index] = true;
381 }
382 Some(remove)
383}
384
385fn compare_centers(left: &Option<DetectorCoords>, right: &Option<DetectorCoords>) -> Ordering {
389 match (left, right) {
390 (None, None) => Ordering::Equal,
391 (None, Some(_)) => Ordering::Less,
392 (Some(_), None) => Ordering::Greater,
393 (Some(left), Some(right)) => left.len().cmp(&right.len()).then_with(|| {
394 std::iter::zip(left, right)
395 .map(|(left, right)| left.total_cmp(right))
396 .find(|order| order.is_ne())
397 .unwrap_or(Ordering::Equal)
398 }),
399 }
400}
401
402fn seam_reproduced(original: &[NodeDetector], replacement: &[NodeDetector]) -> bool {
414 fn centers(detectors: &[NodeDetector]) -> Vec<Option<DetectorCoords>> {
415 let mut centers: Vec<_> = detectors
416 .iter()
417 .map(|detector| detector.coords.clone())
418 .collect();
419 centers.sort_by(compare_centers);
420 centers
421 }
422 centers(original) == centers(replacement)
423}
424
425fn set_single_repeat_count(template: &mut BloqTemplate, repetitions: u32) {
427 let entry = template.circuit.entry_body();
428 for op in template
429 .circuit
430 .body_mut(entry)
431 .expect("every circuit has an entry body")
432 .ops_mut()
433 {
434 if let Op::Repeat { repetitions: r, .. } = op {
435 *r = repetitions;
436 }
437 }
438}
439
440fn retain_unmasked<T>(stored: &mut Vec<T>, remove: &[bool]) {
441 debug_assert_eq!(stored.len(), remove.len());
442 let mut remove = remove.iter();
443 stored.retain(|_| !*remove.next().expect("removal mask matches side table"));
444}
445
446fn retarget_output_operators(
452 level: &mut SubGraph,
453 replacements: &FxMap<TemplateInstanceId, TemplateInstanceId>,
454) {
455 for node in level.graph_mut().node_weights_mut() {
456 match &mut node.kind {
457 BloqNodeKind::Classical(data)
458 if data.operators().iter().any(|operator| {
459 operator.face == BoundaryFace::Output
460 && replacements.contains_key(&operator.instance)
461 }) =>
462 {
463 let ClassicalNode::Observable { operators, .. } = std::sync::Arc::make_mut(data)
464 else {
465 unreachable!("classical output checked above")
466 };
467 for operator in operators {
468 if operator.face == BoundaryFace::Output
469 && let Some(&replacement) = replacements.get(&operator.instance)
470 {
471 operator.instance = replacement;
472 }
473 }
474 }
475 BloqNodeKind::Region(region) => {
476 for (_, body) in region.bodies_mut() {
477 retarget_output_operators(body, replacements);
478 }
479 }
480 BloqNodeKind::Quantum(_) | BloqNodeKind::Classical(_) => {}
481 }
482 }
483}
484
485fn retarget_outputs(bloq: &mut Bloq, replacements: &FxMap<TemplateInstanceId, TemplateInstanceId>) {
487 retarget_output_operators(bloq.top_mut(), replacements);
488 let mut outputs = bloq.logical_outputs().to_vec();
489 for output in &mut outputs {
490 if let Some(&replacement) = replacements.get(&output.instance) {
491 output.instance = replacement;
492 }
493 }
494 bloq.set_logical_outputs(outputs);
495}
496
497pub fn drain_composed_chains(
501 engine: &mut FlowEngine<'_, DetectorTerm<InstanceMeasurement>>,
502) -> (Vec<NodeDetector>, Vec<NodeRestart>) {
503 let chains = engine.drain_composed_chains();
504 let mut detectors = Vec::with_capacity(chains.size_hint().1.unwrap_or(0));
505 let mut restarts = Vec::new();
506 for chain in chains {
507 match chain {
508 ComposedChain::Restart { parity } => restarts.push(NodeRestart { parity }),
509 ComposedChain::Detector { parity, center } => {
510 detectors.push(NodeDetector {
511 parity,
512 coords: center
513 .map(|coords| [coords.x as f64, coords.y as f64].into_iter().collect()),
514 });
515 }
516 }
517 }
518 (detectors, restarts)
519}
520
521impl Bloq {
522 pub fn insert_memory_rounds(
534 &mut self,
535 target: MemoryRoundTarget,
536 rounds: u32,
537 ) -> Result<BloqNodeId, EditError> {
538 Ok(self.insert_memory_rounds_batch(&[target], rounds)?[0])
539 }
540
541 fn insert_memory_rounds_in_place(
542 &mut self,
543 target: &MemoryRoundTarget,
544 rounds: u32,
545 ) -> Result<BloqNodeId, EditError> {
546 if self.timing_requires_pins()? {
547 return Err(EditError::MembershipSelectionRequired);
548 }
549 if rounds == 0 {
550 return Err(EditError::EmptyPadding);
551 }
552 match target {
553 MemoryRoundTarget::Edge { path, from, to } => {
554 self.splice_seam_node_at(path, *from, *to, rounds)
555 }
556 MemoryRoundTarget::After { path, node } => {
557 self.append_memory_rounds_at(path, *node, rounds)
558 }
559 }
560 }
561
562 fn splice_seam_node_at(
565 &mut self,
566 path: &LevelPath,
567 from: BloqNodeId,
568 to: BloqNodeId,
569 rounds: u32,
570 ) -> Result<BloqNodeId, EditError> {
571 let level = self
572 .level_at(path)
573 .ok_or_else(|| EditError::InvalidLevelPath { path: path.clone() })?;
574 let seams = level
575 .edges_between(from, to)
576 .find_map(|edge| match edge.edge {
577 BloqEdge::Quantum(edge) => Some(edge.pipes.clone()),
578 BloqEdge::Value { .. } | BloqEdge::Compose { .. } | BloqEdge::Order => None,
579 })
580 .ok_or(EditError::InvalidQuantumEdge { from, to })?;
581 let padding_refs: Option<Vec<_>> = seams.iter().map(|seam| seam.padding).collect();
584 let padding_refs = padding_refs
585 .filter(|refs| !refs.is_empty())
586 .ok_or(EditError::EdgePaddingMissing { from, to })?;
587 let padding = self.padding_for_refs(&padding_refs, rounds)?;
588 self.subdivide_quantum_edge_at(path, from, to, &padding, rounds)
589 }
590
591 pub fn set_template_repetitions(
618 &mut self,
619 template: TemplateId,
620 repetitions: u32,
621 ) -> Result<(), EditError> {
622 let repeats = self
623 .templates()
624 .get(template)
625 .ok_or(EditError::UnknownTemplate { template })?
626 .circuit
627 .entry_top_level_repeats()
628 .len();
629 if repeats != 1 {
630 return Err(EditError::TemplateNotSingleLooped { template, repeats });
631 }
632 let template = self
633 .templates_mut()
634 .make_mut(template)
635 .expect("the template was just read from the pool");
636 set_single_repeat_count(template, repetitions);
637 self.specialized_padding.clear();
638 Ok(())
639 }
640
641 pub fn insert_memory_rounds_batch(
656 &mut self,
657 targets: &[MemoryRoundTarget],
658 rounds: u32,
659 ) -> Result<Vec<BloqNodeId>, EditError> {
660 if rounds == 0 {
661 return Err(EditError::EmptyPadding);
662 }
663 if self.timing_requires_pins()? {
664 return Err(EditError::MembershipSelectionRequired);
665 }
666 let mut edited = self.clone();
667 let padding = targets
668 .iter()
669 .map(|target| edited.insert_memory_rounds_in_place(target, rounds))
670 .collect::<Result<Vec<_>, _>>()?;
671 *self = edited;
672 Ok(padding)
673 }
674
675 fn padding_for_refs(
676 &mut self,
677 padding_refs: &[PipePadding],
678 rounds: u32,
679 ) -> Result<Vec<PaddingInstance>, EditError> {
680 padding_refs
681 .iter()
682 .map(|entry| {
683 let template = self.padding_template_for_rounds(entry, rounds)?;
684 Ok(PaddingInstance {
685 template,
686 offset: entry.offset,
687 })
688 })
689 .collect()
690 }
691
692 fn padding_template_for_rounds(
696 &mut self,
697 entry: &PipePadding,
698 rounds: u32,
699 ) -> Result<TemplateId, EditError> {
700 if rounds == 0 {
701 return Err(EditError::EmptyPadding);
702 }
703 if rounds == 1 {
704 return Ok(entry.one_round);
705 }
706 let repetitions = rounds - 1;
707 let template = self
708 .templates()
709 .get(entry.looped)
710 .ok_or(EditError::UnknownTemplate {
711 template: entry.looped,
712 })?;
713 let [current] = template.circuit.entry_top_level_repeats()[..] else {
714 return Err(EditError::PaddingTemplateNotLooped {
715 template: entry.looped,
716 });
717 };
718 if current == repetitions {
719 return Ok(entry.looped);
720 }
721 let key = PaddingVariant {
722 base: entry.looped,
723 rounds,
724 };
725 if let Some(&specialized) = self.specialized_padding.get(&key) {
726 return Ok(specialized);
727 }
728
729 let mut specialized = template.clone();
730 set_single_repeat_count(&mut specialized, repetitions);
731 let specialized = self.add_template(specialized);
732 self.specialized_padding.insert(key, specialized);
733 Ok(specialized)
734 }
735
736 pub fn subdivide_quantum_edge(
753 &mut self,
754 from: BloqNodeId,
755 to: BloqNodeId,
756 padding: &[PaddingInstance],
757 rounds: u32,
758 ) -> Result<BloqNodeId, EditError> {
759 self.subdivide_quantum_edge_at(&LevelPath::default(), from, to, padding, rounds)
760 }
761
762 fn subdivide_quantum_edge_at(
763 &mut self,
764 path: &LevelPath,
765 from: BloqNodeId,
766 to: BloqNodeId,
767 padding: &[PaddingInstance],
768 rounds: u32,
769 ) -> Result<BloqNodeId, EditError> {
770 if self.timing_requires_pins()? {
771 return Err(EditError::MembershipSelectionRequired);
772 }
773 if rounds == 0 || padding.is_empty() {
774 return Err(EditError::EmptyPadding);
775 }
776
777 let (from_index, to_index) = (
779 NodeIndex::new(from.0 as usize),
780 NodeIndex::new(to.0 as usize),
781 );
782 let level = self
783 .level_at(path)
784 .ok_or_else(|| EditError::InvalidLevelPath { path: path.clone() })?;
785 let mut quantum_edges = level
786 .graph()
787 .edges_connecting(from_index, to_index)
788 .filter(|edge| matches!(edge.weight(), BloqEdge::Quantum(_)));
789 let edge = quantum_edges
790 .next()
791 .ok_or(EditError::InvalidQuantumEdge { from, to })?;
792 if quantum_edges.next().is_some() {
794 return Err(EditError::InvalidQuantumEdge { from, to });
795 }
796 let edge_id = edge.id();
797 let BloqEdge::Quantum(quantum_edge) = edge.weight() else {
798 unreachable!("filtered to quantum edges")
799 };
800 let quantum_edge = quantum_edge.clone();
801 if quantum_edge.pipes.is_empty() {
802 return Err(EditError::InvalidQuantumEdge { from, to });
803 }
804
805 let lower = level
806 .node(from)
807 .expect("a graph edge's source node is live");
808 let upper = level.node(to).expect("a graph edge's target node is live");
809 let lower_quantum = lower
810 .try_quantum()
811 .ok_or(EditError::InvalidQuantumEdge { from, to })?;
812 if let Some(region) = upper.try_region() {
813 let mut entries = Vec::new();
814 for (body_selector, body) in region.bodies() {
815 let mut covered = vec![0usize; padding.len()];
816 for (target, node) in body.nodes() {
817 let Some(quantum) = node.try_quantum() else {
818 continue;
819 };
820 if body
821 .incoming(target)
822 .any(|edge| matches!(edge.edge, BloqEdge::Quantum(_)))
823 {
824 continue;
825 }
826 let mut matched = false;
827 for (index, padding) in padding.iter().enumerate() {
828 if quantum
829 .instances
830 .iter()
831 .any(|instance| instance.offset == padding.offset)
832 {
833 covered[index] += 1;
834 matched = true;
835 }
836 }
837 if matched {
838 entries.push(QuantumBoundaryEntry {
839 path: path.child(to, body_selector),
840 target,
841 });
842 }
843 }
844 if covered.iter().any(|&count| count != 1) {
845 return Err(EditError::InvalidQuantumEdge { from, to });
846 }
847 }
848 let lower_quantum = lower_quantum.clone();
849 return self.subdivide_quantum_boundary_input_at(
850 path,
851 from,
852 to,
853 padding,
854 rounds,
855 quantum_edge,
856 lower_quantum,
857 entries,
858 );
859 }
860 let upper_quantum = upper
861 .try_quantum()
862 .ok_or(EditError::InvalidQuantumEdge { from, to })?;
863 if !upper_quantum.guards.is_empty() {
864 let entries = vec![QuantumBoundaryEntry {
865 path: path.clone(),
866 target: to,
867 }];
868 let lower_quantum = lower_quantum.clone();
869 return self.subdivide_quantum_boundary_input_at(
870 path,
871 from,
872 to,
873 padding,
874 rounds,
875 quantum_edge,
876 lower_quantum,
877 entries,
878 );
879 }
880
881 let lower_before: Vec<_> = lower_quantum
884 .instances
885 .iter()
886 .filter(|instance| padding.iter().any(|item| item.offset == instance.offset))
887 .copied()
888 .collect();
889
890 let original =
892 compose_seam_detectors(self.templates(), &[&lower_before, &upper_quantum.instances])?;
893 let [
894 (lower_detectors, lower_restarts),
895 (original_detectors, original_restarts),
896 ] = original.split_first();
897 let touched = lower_before
898 .iter()
899 .map(|instance| instance.id)
900 .chain(
901 upper_quantum
902 .instances
903 .iter()
904 .filter(|instance| padding.iter().any(|item| item.offset == instance.offset))
905 .map(|instance| instance.id),
906 )
907 .collect::<FxSet<_>>();
908 let mut lower_quantum = lower_quantum.clone();
909 let mut upper_quantum = upper_quantum.clone();
910 demote_touched_bundles(self, &mut lower_quantum, &touched)?;
911 demote_touched_bundles(self, &mut upper_quantum, &touched)?;
912 if removal_mask(&lower_quantum.detectors, &lower_detectors).is_none()
913 || removal_mask(&lower_quantum.restarts, &lower_restarts).is_none()
914 {
915 return Err(EditError::InvalidStoredSeam);
916 }
917 let detector_remove = removal_mask(&upper_quantum.detectors, &original_detectors)
918 .ok_or(EditError::InvalidStoredSeam)?;
919 let restart_remove = removal_mask(&upper_quantum.restarts, &original_restarts)
920 .ok_or(EditError::InvalidStoredSeam)?;
921
922 let padding_instances = self.allocate_padding_instances(padding)?;
923 let mut replacements = FxMap::default();
924 for (padding, instance) in padding.iter().zip(&padding_instances) {
925 let mut sources = lower_quantum
926 .instances
927 .iter()
928 .filter(|source| source.offset == padding.offset);
929 let Some(source) = sources.next() else {
930 return Err(EditError::InvalidQuantumEdge { from, to });
931 };
932 if sources.next().is_some() || replacements.insert(source.id, *instance).is_some() {
933 return Err(EditError::InvalidQuantumEdge { from, to });
934 }
935 }
936 let lower_after: Vec<_> = lower_before
937 .iter()
938 .map(|instance| replacements.get(&instance.id).copied().unwrap_or(*instance))
939 .collect();
940
941 let internal =
942 compose_seam_detectors(self.templates(), &[&lower_before, &padding_instances])?;
943 let [
944 (composed_lower_detectors, composed_lower_restarts),
945 (mid_detectors, mid_restarts),
946 ] = internal.split_first();
947 if removal_mask(&lower_quantum.detectors, &composed_lower_detectors).is_none()
948 || removal_mask(&lower_quantum.restarts, &composed_lower_restarts).is_none()
949 {
950 return Err(EditError::InvalidStoredSeam);
951 }
952
953 let replacement =
954 compose_seam_detectors(self.templates(), &[&lower_after, &upper_quantum.instances])?;
955 let (upper_detectors, upper_restarts) = replacement.after(0);
956
957 if !seam_reproduced(&original_detectors, &upper_detectors)
964 || upper_restarts.len() != original_restarts.len()
965 {
966 return Err(EditError::SeamRecompositionMismatch);
967 }
968 let partial_seam = padding_instances.len() < lower_quantum.instances.len();
969
970 let mut node = BloqNode::memory_padding(quantum_edge.pipes[0].pipe, rounds);
972 {
973 let quantum = node.expect_quantum_mut();
974 quantum.instances = padding_instances;
975 quantum.detectors = mid_detectors;
976 quantum.restarts = mid_restarts;
977 }
978 let upper_work = upper_quantum;
979 let level = self
980 .level_at_mut(path)
981 .expect("validated level path remains live during insertion");
982 let mid = level.add_node(node);
983 let graph = level.graph_mut();
984 graph.remove_edge(edge_id);
985 *graph[from_index].expect_quantum_mut() = lower_quantum;
986 let upper_quantum = graph[to_index].expect_quantum_mut();
987 *upper_quantum = upper_work;
988 retain_unmasked(&mut upper_quantum.detectors, &detector_remove);
989 retain_unmasked(&mut upper_quantum.restarts, &restart_remove);
990 upper_quantum.detectors.extend(upper_detectors);
991 upper_quantum.restarts.extend(upper_restarts);
992
993 level.add_edge(from, mid, BloqEdge::Quantum(quantum_edge.clone()));
994 level.add_edge(mid, to, BloqEdge::Quantum(quantum_edge));
995 if partial_seam {
996 let output_replacements = replacements
997 .into_iter()
998 .map(|(source, padding)| (source, padding.id))
999 .collect();
1000 retarget_outputs(self, &output_replacements);
1001 }
1002 Ok(mid)
1003 }
1004
1005 #[expect(
1006 clippy::too_many_arguments,
1007 reason = "the seam edit needs both endpoints, payloads, and replacement state"
1008 )]
1009 fn subdivide_quantum_boundary_input_at(
1010 &mut self,
1011 path: &LevelPath,
1012 from: BloqNodeId,
1013 to: BloqNodeId,
1014 padding: &[PaddingInstance],
1015 rounds: u32,
1016 quantum_edge: Box<QuantumEdge>,
1017 mut source: crate::QuantumNode,
1018 entries: Vec<QuantumBoundaryEntry>,
1019 ) -> Result<BloqNodeId, EditError> {
1020 let source_before: Vec<_> = source
1021 .instances
1022 .iter()
1023 .filter(|instance| padding.iter().any(|item| item.offset == instance.offset))
1024 .copied()
1025 .collect();
1026 let padding_instances = self.allocate_padding_instances(padding)?;
1027 let mut replacements = FxMap::default();
1028 for (padding, instance) in padding.iter().zip(&padding_instances) {
1029 let mut sources = source
1030 .instances
1031 .iter()
1032 .filter(|source| source.offset == padding.offset);
1033 let Some(source) = sources.next() else {
1034 return Err(EditError::InvalidQuantumEdge { from, to });
1035 };
1036 if sources.next().is_some() || replacements.insert(source.id, *instance).is_some() {
1037 return Err(EditError::InvalidQuantumEdge { from, to });
1038 }
1039 }
1040 let lower_after: Vec<_> = source_before
1041 .iter()
1042 .map(|instance| replacements.get(&instance.id).copied().unwrap_or(*instance))
1043 .collect();
1044
1045 let internal =
1046 compose_seam_detectors(self.templates(), &[&source_before, &padding_instances])?;
1047 let [
1048 (source_detectors, source_restarts),
1049 (padding_detectors, padding_restarts),
1050 ] = internal.split_first();
1051 let touched = source_before.iter().map(|instance| instance.id).collect();
1052 demote_touched_bundles(self, &mut source, &touched)?;
1053 if removal_mask(&source.detectors, &source_detectors).is_none()
1054 || removal_mask(&source.restarts, &source_restarts).is_none()
1055 {
1056 return Err(EditError::InvalidStoredSeam);
1057 }
1058
1059 let mut rewrites = Vec::with_capacity(entries.len());
1060 for entry in entries {
1061 let quantum = self.recompose_boundary_at(
1062 &entry.path,
1063 entry.target,
1064 &source_before,
1065 &lower_after,
1066 )?;
1067 rewrites.push(QuantumBoundaryRewrite {
1068 path: entry.path,
1069 target: entry.target,
1070 quantum,
1071 });
1072 }
1073
1074 for rewrite in rewrites {
1075 *self
1076 .level_at_mut(&rewrite.path)
1077 .expect("validated level remains live")
1078 .node_mut(rewrite.target)
1079 .expect("validated boundary target remains live")
1080 .expect_quantum_mut() = rewrite.quantum;
1081 }
1082
1083 let mut node = BloqNode::memory_padding(quantum_edge.pipes[0].pipe, rounds);
1084 {
1085 let quantum = node.expect_quantum_mut();
1086 quantum.instances = padding_instances;
1087 quantum.detectors = padding_detectors;
1088 quantum.restarts = padding_restarts;
1089 }
1090 let level = self
1091 .level_at_mut(path)
1092 .expect("validated level path remains live during insertion");
1093 *level
1094 .node_mut(from)
1095 .expect("validated source remains live")
1096 .expect_quantum_mut() = source;
1097 let mid = level.add_node(node);
1098 let edge = level
1099 .graph()
1100 .edges_connecting(
1101 NodeIndex::new(from.0 as usize),
1102 NodeIndex::new(to.0 as usize),
1103 )
1104 .find(|edge| matches!(edge.weight(), BloqEdge::Quantum(_)))
1105 .expect("validated quantum edge remains live")
1106 .id();
1107 level.graph_mut().remove_edge(edge);
1108 level.add_edge(from, mid, BloqEdge::Quantum(quantum_edge.clone()));
1109 level.add_edge(mid, to, BloqEdge::Quantum(quantum_edge));
1110 Ok(mid)
1111 }
1112
1113 fn recompose_boundary_at(
1117 &self,
1118 path: &LevelPath,
1119 target: BloqNodeId,
1120 before: &[TemplateInstance],
1121 after: &[TemplateInstance],
1122 ) -> Result<crate::QuantumNode, EditError> {
1123 let level = self.level_at(path).expect("validated boundary level");
1124 let mut node = level[target].clone();
1125 let touched = before
1126 .iter()
1127 .chain(after)
1128 .map(|instance| instance.id)
1129 .chain(
1130 node.expect_quantum()
1131 .instances
1132 .iter()
1133 .filter(|instance| before.iter().any(|source| source.offset == instance.offset))
1134 .map(|instance| instance.id),
1135 )
1136 .collect::<FxSet<_>>();
1137 demote_touched_bundles(self, node.expect_quantum_mut(), &touched)?;
1138 let quantum = node.expect_quantum();
1139 let mut updated = quantum.clone();
1140 let choices = if quantum.guards.is_empty() {
1141 vec![crate::membership::QuantumSelection {
1142 input: 0,
1143 values: FxMap::default(),
1144 }]
1145 } else {
1146 crate::membership::PredicateAnalysis::new(level)
1147 .quantum_selections(target)?
1148 .ok_or(EditError::MembershipSelectionRequired)?
1149 };
1150 for choice in choices {
1151 let selected = node.select_quantum_members(|slot| choice.values.get(&slot).copied())?;
1152 let selected = selected.expect_quantum();
1153 let original =
1154 compose_seam_detectors(self.templates(), &[before, &selected.instances])?;
1155 let (original_detectors, original_restarts) = original.after(0);
1156 let detector_remove = removal_mask(&selected.detectors, &original_detectors)
1157 .ok_or(EditError::InvalidStoredSeam)?;
1158 let restart_remove = removal_mask(&selected.restarts, &original_restarts)
1159 .ok_or(EditError::InvalidStoredSeam)?;
1160 let replacement =
1161 compose_seam_detectors(self.templates(), &[after, &selected.instances])?;
1162 let (mut detectors, restarts) = replacement.after(0);
1163 if !seam_reproduced(&original_detectors, &detectors)
1164 || restarts.len() != original_restarts.len()
1165 {
1166 return Err(EditError::SeamRecompositionMismatch);
1167 }
1168 if quantum.guards.is_empty() {
1169 retain_unmasked(&mut updated.detectors, &detector_remove);
1170 retain_unmasked(&mut updated.restarts, &restart_remove);
1171 updated.detectors.extend(detectors);
1172 updated.restarts.extend(restarts);
1173 continue;
1174 }
1175 let detector_indices = (0..quantum.detectors.len() as u32).filter(|index| {
1176 !quantum
1177 .guards
1178 .iter()
1179 .any(|guard| !choice.values[&guard.input] && guard.detectors.contains(index))
1180 });
1181 let mut changed = detector_indices
1182 .zip(&selected.detectors)
1183 .zip(detector_remove)
1184 .filter_map(|((index, row), remove)| remove.then_some((index, row)))
1185 .collect::<Vec<_>>();
1186 changed.sort_by(|(_, left), (_, right)| compare_centers(&left.coords, &right.coords));
1187 detectors.sort_by(|left, right| compare_centers(&left.coords, &right.coords));
1188 let guard = updated
1189 .guards
1190 .iter_mut()
1191 .find(|guard| guard.input == choice.input)
1192 .expect("a choice is a member registration");
1193 for ((index, old), new) in changed.into_iter().zip(detectors) {
1194 let mut delta = old.parity.clone();
1195 delta.xor_assign(&new.parity);
1196 if !delta.is_empty() {
1197 guard.detector_parities.push((index, delta));
1198 }
1199 }
1200 let restart_indices = (0..quantum.restarts.len() as u32).filter(|index| {
1201 !quantum
1202 .guards
1203 .iter()
1204 .any(|guard| !choice.values[&guard.input] && guard.restarts.contains(index))
1205 });
1206 let changed = restart_indices
1207 .zip(&selected.restarts)
1208 .zip(restart_remove)
1209 .filter_map(|((index, row), remove)| remove.then_some((index, row)));
1210 for ((index, old), new) in changed.zip(restarts) {
1211 let mut delta = old.parity.clone();
1212 delta.xor_assign(&new.parity);
1213 if !delta.is_empty() {
1214 guard.restart_parities.push((index, delta));
1215 }
1216 }
1217 }
1218 Ok(updated)
1219 }
1220
1221 fn append_memory_rounds_at(
1222 &mut self,
1223 path: &LevelPath,
1224 source: BloqNodeId,
1225 rounds: u32,
1226 ) -> Result<BloqNodeId, EditError> {
1227 let level = self
1228 .level_at(path)
1229 .ok_or_else(|| EditError::InvalidLevelPath { path: path.clone() })?;
1230 let source_node =
1231 level
1232 .node(source)
1233 .ok_or_else(|| EditError::MemoryRoundTargetMissing {
1234 path: path.clone(),
1235 node: source,
1236 })?;
1237 let source_quantum =
1238 source_node
1239 .try_quantum()
1240 .ok_or_else(|| EditError::MemoryRoundTargetNotQuantum {
1241 path: path.clone(),
1242 node: source,
1243 })?;
1244 if !source_quantum.guards.is_empty() {
1245 return Err(EditError::MembershipSelectionRequired);
1246 }
1247 if level
1248 .outgoing(source)
1249 .any(|edge| matches!(edge.edge, BloqEdge::Quantum(_)))
1250 {
1251 return Err(EditError::AfterTargetNotTerminal { node: source });
1252 }
1253 let source_instances = source_quantum.instances.clone();
1254
1255 let Some((segment, parent_path)) = path.segments().split_last() else {
1256 return Err(EditError::AfterTargetTopLevel);
1257 };
1258 let region = segment.region;
1259 let (mut members, mut boundary_targets) = (Vec::<TerminalMember>::new(), Vec::new());
1260 {
1261 let parent = self
1262 .level_at_segments(parent_path)
1263 .ok_or_else(|| EditError::InvalidLevelPath { path: path.clone() })?;
1264 for edge_ref in parent.outgoing(region) {
1265 let BloqEdge::Quantum(edge) = edge_ref.edge else {
1266 continue;
1267 };
1268 let mut matched = false;
1269 for seam in &edge.pipes {
1270 let Some(padding) = seam.padding else {
1271 continue;
1272 };
1273 let mut instances = source_instances
1274 .iter()
1275 .filter(|instance| instance.offset == padding.offset);
1276 let Some(&instance) = instances.next() else {
1277 continue;
1278 };
1279 if instances.next().is_some()
1280 || members.iter().any(|member| member.source.id == instance.id)
1281 {
1282 return Err(EditError::AfterPaddingAmbiguous {
1283 node: source,
1284 offset: padding.offset,
1285 });
1286 }
1287 members.push(TerminalMember {
1288 source: instance,
1289 pipe: seam.pipe,
1290 padding,
1291 });
1292 matched = true;
1293 }
1294 if matched {
1295 if parent
1296 .node(edge_ref.target)
1297 .and_then(BloqNode::try_quantum)
1298 .is_none()
1299 {
1300 return Err(EditError::AfterBoundaryTargetNotQuantum {
1301 node: edge_ref.target,
1302 });
1303 }
1304 if boundary_targets.contains(&edge_ref.target) {
1305 return Err(EditError::TerminalRecompositionMismatch);
1306 }
1307 boundary_targets.push(edge_ref.target);
1308 }
1309 }
1310 }
1311 if members.is_empty() {
1312 return Err(EditError::AfterPaddingMissing { node: source });
1313 }
1314 let mut source_quantum = source_quantum.clone();
1315 let touched = members.iter().map(|member| member.source.id).collect();
1316 demote_touched_bundles(self, &mut source_quantum, &touched)?;
1317 let source_detectors = source_quantum.detectors.clone();
1318 let source_restarts = source_quantum.restarts.clone();
1319 let layer = members[0].pipe.src.z.min(members[0].pipe.dst.z);
1320 if members
1321 .iter()
1322 .any(|member| member.pipe.src.z.min(member.pipe.dst.z) != layer)
1323 {
1324 return Err(EditError::TerminalRecompositionMismatch);
1325 }
1326
1327 let refs: Vec<_> = members.iter().map(|member| member.padding).collect();
1328 let padding = self.padding_for_refs(&refs, rounds)?;
1329 let padding_instances = self.allocate_padding_instances(&padding)?;
1330 let replacements: FxMap<_, _> = members
1331 .iter()
1332 .zip(&padding_instances)
1333 .map(|(member, &padding)| (member.source.id, padding))
1334 .collect();
1335 let lower_after: Vec<_> = source_instances
1336 .iter()
1337 .map(|instance| replacements.get(&instance.id).copied().unwrap_or(*instance))
1338 .collect();
1339
1340 let internal =
1341 compose_seam_detectors(self.templates(), &[&source_instances, &padding_instances])?;
1342 let [
1343 (source_composed_detectors, source_composed_restarts),
1344 (padding_detectors, padding_restarts),
1345 ] = internal.split_first();
1346 if removal_mask(&source_detectors, &source_composed_detectors).is_none()
1347 || removal_mask(&source_restarts, &source_composed_restarts).is_none()
1348 {
1349 return Err(EditError::InvalidStoredSeam);
1350 }
1351
1352 let mut boundary_rewrites = Vec::with_capacity(boundary_targets.len());
1353 let boundary_path = parent_path
1354 .iter()
1355 .fold(LevelPath::default(), |path, segment| {
1356 path.child(segment.region, segment.body)
1357 });
1358 for target in boundary_targets {
1359 boundary_rewrites.push(BoundaryRewrite {
1360 target,
1361 quantum: self.recompose_boundary_at(
1362 &boundary_path,
1363 target,
1364 &source_instances,
1365 &lower_after,
1366 )?,
1367 });
1368 }
1369
1370 {
1372 let parent = self
1373 .level_at_segments_mut(parent_path)
1374 .expect("validated parent level remains live during insertion");
1375 for rewrite in boundary_rewrites {
1376 let upper = parent
1377 .node_mut(rewrite.target)
1378 .expect("validated boundary target remains live")
1379 .expect_quantum_mut();
1380 *upper = rewrite.quantum;
1381 }
1382 }
1383
1384 let mut padding_node = BloqNode::memory_padding(members[0].pipe, rounds);
1385 {
1386 let quantum = padding_node.expect_quantum_mut();
1387 quantum.instances = padding_instances;
1388 quantum.detectors = padding_detectors;
1389 quantum.restarts = padding_restarts;
1390 }
1391 let padding_node = {
1392 let level = self
1393 .level_at_mut(path)
1394 .expect("validated target level remains live during insertion");
1395 *level
1396 .node_mut(source)
1397 .expect("validated source remains live")
1398 .expect_quantum_mut() = source_quantum;
1399 let order_edges: Vec<_> = level
1400 .graph()
1401 .edges_directed(NodeIndex::new(source.0 as usize), Direction::Outgoing)
1402 .filter(|edge| matches!(edge.weight(), BloqEdge::Order))
1403 .map(|edge| (edge.id(), BloqNodeId(edge.target().index() as u32)))
1404 .collect();
1405 let padding_node = level.add_node(padding_node);
1406 for (edge, _) in &order_edges {
1407 level.graph_mut().remove_edge(*edge);
1408 }
1409 level.add_edge(
1410 source,
1411 padding_node,
1412 BloqEdge::Quantum(Box::new(QuantumEdge {
1413 guard: None,
1414 pipes: members
1415 .iter()
1416 .map(|member| PipeSeam {
1417 pipe: member.pipe,
1418 padding: Some(member.padding),
1419 })
1420 .collect(),
1421 })),
1422 );
1423 for (_, target) in order_edges {
1424 level.add_edge(padding_node, target, BloqEdge::Order);
1425 }
1426 padding_node
1427 };
1428
1429 let output_replacements: FxMap<_, _> = replacements
1430 .into_iter()
1431 .map(|(source, padding)| (source, padding.id))
1432 .collect();
1433 retarget_outputs(self, &output_replacements);
1434 Ok(padding_node)
1435 }
1436
1437 fn allocate_padding_instances(
1438 &self,
1439 padding: &[PaddingInstance],
1440 ) -> Result<Vec<TemplateInstance>, EditError> {
1441 let base = match self.max_template_instance_id() {
1442 Some(id) => {
1443 id.0.checked_add(1)
1444 .ok_or(EditError::TemplateInstanceIdExhausted)?
1445 }
1446 None => 0,
1447 };
1448 let last_index =
1449 u32::try_from(padding.len() - 1).map_err(|_| EditError::TemplateInstanceIdExhausted)?;
1450 let last = base
1451 .checked_add(last_index)
1452 .ok_or(EditError::TemplateInstanceIdExhausted)?;
1453 Ok((base..=last)
1454 .zip(padding)
1455 .map(|(id, instance)| {
1456 TemplateInstance::new(TemplateInstanceId(id), instance.template, instance.offset)
1457 })
1458 .collect())
1459 }
1460
1461 fn max_template_instance_id(&self) -> Option<TemplateInstanceId> {
1463 self.levels()
1464 .flat_map(|(_, level)| level.quantum_nodes())
1465 .flat_map(|(_, quantum)| quantum.instances.iter().map(|i| i.id))
1466 .max()
1467 }
1468}
1469
1470#[cfg(test)]
1471mod tests {
1472 use bloq_circuit::CoordCircuit;
1473
1474 use super::*;
1475 use crate::{
1476 BodySelector, ClassicalExpr, InstanceBoundaryOperator, NodeDetectorParity, RegionNode,
1477 };
1478 use glam::IVec3;
1479
1480 fn centered(center: Option<[f64; 2]>) -> NodeDetector {
1483 NodeDetector {
1484 parity: NodeDetectorParity::default(),
1485 coords: center.map(|center| center.into_iter().collect()),
1486 }
1487 }
1488
1489 #[test]
1490 fn touched_bundle_demotion_transfers_guard_and_preserves_shared_snapshot() {
1491 use crate::{
1492 BundleDetector, BundleMeasurement, DetectorBundle, DetectorBundleUse, QuantumGuard,
1493 };
1494
1495 let mut program = Bloq::new();
1496 let bundle = program.add_detector_bundle(DetectorBundle::new(
1497 vec![TemplateId(0)],
1498 vec![BundleDetector {
1499 parity: bloq_circuit::DetectorParity::from_measurements([BundleMeasurement {
1500 owner: 0,
1501 measurement: 0,
1502 }]),
1503 coords: Some([1.0, 2.0].into_iter().collect()),
1504 }],
1505 ));
1506 let mut quantum = crate::QuantumNode {
1507 detector_bundles: vec![
1508 DetectorBundleUse {
1509 bundle,
1510 instances: vec![TemplateInstanceId(5)],
1511 offset: IVec2::ZERO,
1512 },
1513 DetectorBundleUse {
1514 bundle,
1515 instances: vec![TemplateInstanceId(6)],
1516 offset: IVec2::ZERO,
1517 },
1518 ],
1519 guards: vec![QuantumGuard {
1520 input: 7,
1521 detector_bundles: vec![0, 1],
1522 ..Default::default()
1523 }],
1524 ..Default::default()
1525 };
1526 let snapshot = quantum.clone();
1527 demote_touched_bundles(
1528 &program,
1529 &mut quantum,
1530 &FxSet::from_iter([TemplateInstanceId(5)]),
1531 )
1532 .unwrap();
1533 assert_eq!(
1534 quantum.detectors[0]
1535 .parity
1536 .measurements()
1537 .next()
1538 .unwrap()
1539 .instance,
1540 TemplateInstanceId(5)
1541 );
1542 assert_eq!(quantum.guards[0].detectors, vec![0]);
1543 assert_eq!(quantum.guards[0].detector_bundles, vec![0]);
1544 assert_eq!(
1545 quantum.detector_bundles[0].instances,
1546 vec![TemplateInstanceId(6)]
1547 );
1548 assert_eq!(snapshot.detector_bundles.len(), 2);
1549 assert!(snapshot.detectors.is_empty());
1550 }
1551
1552 #[test]
1556 fn seam_reproduction_counts_repeated_centers() {
1557 let original = [
1558 centered(Some([2.0, 0.0])),
1559 centered(Some([2.0, 0.0])),
1560 centered(Some([4.0, 2.0])),
1561 ];
1562
1563 assert!(seam_reproduced(&original, &original));
1564 assert!(seam_reproduced(
1566 &original,
1567 &[
1568 centered(Some([4.0, 2.0])),
1569 centered(Some([2.0, 0.0])),
1570 centered(Some([2.0, 0.0])),
1571 ]
1572 ));
1573 assert!(!seam_reproduced(
1575 &original,
1576 &[centered(Some([2.0, 0.0])), centered(Some([4.0, 2.0]))]
1577 ));
1578 assert!(!seam_reproduced(
1580 &original,
1581 &[
1582 centered(Some([2.0, 0.0])),
1583 centered(Some([2.0, 0.0])),
1584 centered(Some([6.0, 2.0])),
1585 ]
1586 ));
1587 assert!(!seam_reproduced(
1589 &[centered(None)],
1590 &[centered(Some([0.0, 0.0]))]
1591 ));
1592 }
1593
1594 fn plain_pipe() -> TemporalPipeRef {
1595 TemporalPipeRef {
1596 src: IVec3::new(0, 0, 0),
1597 dst: IVec3::new(0, 0, 1),
1598 hadamard: false,
1599 }
1600 }
1601
1602 fn seam_bloq(pipe: TemporalPipeRef) -> (Bloq, BloqNodeId, BloqNodeId, TemplateId) {
1604 let mut bloq = Bloq::new();
1605 let template = bloq.add_template(BloqTemplate::new(CoordCircuit::new()));
1606 let mut lower = BloqNode::from_members(vec![]);
1607 lower
1608 .expect_quantum_mut()
1609 .instances
1610 .push(TemplateInstance::new(
1611 TemplateInstanceId(0),
1612 template,
1613 IVec2::ZERO,
1614 ));
1615 let lower = bloq.add_node(lower);
1616 let upper = bloq.add_node(BloqNode::from_members(vec![]));
1617 bloq.add_edge(lower, upper, BloqEdge::quantum(vec![pipe]));
1618 (bloq, lower, upper, template)
1619 }
1620
1621 #[test]
1622 fn empty_padding_is_rejected() {
1623 let (mut bloq, lower, upper, _) = seam_bloq(plain_pipe());
1624 assert!(matches!(
1625 bloq.subdivide_quantum_edge(lower, upper, &[], 1),
1626 Err(EditError::EmptyPadding)
1627 ));
1628 }
1629
1630 #[test]
1631 fn exhausted_template_instance_ids_are_rejected_without_mutation() {
1632 for (existing_id, padding_count) in [(u32::MAX, 1), (u32::MAX - 1, 2)] {
1633 let (mut bloq, lower, upper, template) = seam_bloq(plain_pipe());
1634 bloq.node_mut(lower).unwrap().expect_quantum_mut().instances[0].id =
1635 TemplateInstanceId(existing_id);
1636 let padding = vec![
1637 PaddingInstance {
1638 template,
1639 offset: IVec2::ZERO,
1640 };
1641 padding_count
1642 ];
1643
1644 assert!(matches!(
1645 bloq.subdivide_quantum_edge(lower, upper, &padding, 1),
1646 Err(EditError::TemplateInstanceIdExhausted)
1647 ));
1648 assert_eq!(bloq.node_count(), 2, "failed edit leaves graph unchanged");
1649 assert_eq!(
1650 bloq.graph()
1651 .edges_connecting(
1652 NodeIndex::new(lower.0 as usize),
1653 NodeIndex::new(upper.0 as usize),
1654 )
1655 .count(),
1656 1,
1657 "failed edit preserves the original seam"
1658 );
1659 }
1660 }
1661
1662 #[test]
1663 fn quantum_edge_without_pipes_is_rejected() {
1664 let (mut bloq, lower, upper, template) = seam_bloq(plain_pipe());
1665 let edge = bloq
1666 .graph()
1667 .edges_connecting(
1668 NodeIndex::new(lower.0 as usize),
1669 NodeIndex::new(upper.0 as usize),
1670 )
1671 .next()
1672 .unwrap()
1673 .id();
1674 *bloq.graph_mut_internal().edge_weight_mut(edge).unwrap() = BloqEdge::quantum(vec![]);
1675 let padding = [PaddingInstance {
1676 template,
1677 offset: IVec2::ZERO,
1678 }];
1679
1680 assert!(matches!(
1681 bloq.subdivide_quantum_edge(lower, upper, &padding, 1),
1682 Err(EditError::InvalidQuantumEdge { .. })
1683 ));
1684 assert_eq!(bloq.node_count(), 2, "failed edit leaves graph unchanged");
1685 }
1686
1687 #[test]
1688 fn missing_quantum_edge_is_rejected() {
1689 let (mut bloq, lower, upper, template) = seam_bloq(plain_pipe());
1690 let padding = [PaddingInstance {
1691 template,
1692 offset: IVec2::ZERO,
1693 }];
1694 assert!(matches!(
1696 bloq.subdivide_quantum_edge(upper, lower, &padding, 1),
1697 Err(EditError::InvalidQuantumEdge { .. })
1698 ));
1699 }
1700
1701 #[test]
1702 fn hadamard_pipe_can_be_subdivided() {
1703 let (mut bloq, lower, upper, template) = seam_bloq(TemporalPipeRef {
1704 hadamard: true,
1705 ..plain_pipe()
1706 });
1707 let padding = [PaddingInstance {
1708 template,
1709 offset: IVec2::ZERO,
1710 }];
1711 bloq.subdivide_quantum_edge(lower, upper, &padding, 1)
1712 .expect("Hadamard provenance on an edge does not block subdivision");
1713 }
1714
1715 #[test]
1716 fn parallel_quantum_edges_are_rejected() {
1717 let (mut bloq, lower, upper, template) = seam_bloq(plain_pipe());
1718 bloq.add_edge(lower, upper, BloqEdge::quantum(vec![plain_pipe()]));
1719 let padding = [PaddingInstance {
1720 template,
1721 offset: IVec2::ZERO,
1722 }];
1723 assert!(matches!(
1724 bloq.subdivide_quantum_edge(lower, upper, &padding, 1),
1725 Err(EditError::InvalidQuantumEdge { .. })
1726 ));
1727 }
1728
1729 #[test]
1730 fn self_closing_lower_template_is_rejected() {
1731 use bloq_circuit::{Flow, Pauli, PauliMap};
1732
1733 let key: PauliMap = [(IVec2::ZERO, Pauli::Z)].into_iter().collect();
1735 let mut creator = BloqTemplate::new(CoordCircuit::new());
1736 creator.boundary_flows =
1737 vec![Flow::new(PauliMap::empty(), key.clone()).with_measurements([0])];
1738 let mut consumer = BloqTemplate::new(CoordCircuit::new());
1739 consumer.boundary_flows = vec![Flow::new(key, PauliMap::empty()).with_measurements([0])];
1740
1741 let mut bloq = Bloq::new();
1742 let creator = bloq.add_template(creator);
1743 let consumer = bloq.add_template(consumer);
1744 let empty = bloq.add_template(BloqTemplate::new(CoordCircuit::new()));
1745 let mut lower = BloqNode::from_members(vec![]);
1746 lower.expect_quantum_mut().instances.extend([
1747 TemplateInstance::new(TemplateInstanceId(0), creator, IVec2::ZERO),
1748 TemplateInstance::new(TemplateInstanceId(1), consumer, IVec2::ZERO),
1749 ]);
1750 let lower = bloq.add_node(lower);
1751 let upper = bloq.add_node(BloqNode::from_members(vec![]));
1752 bloq.add_edge(lower, upper, BloqEdge::quantum(vec![plain_pipe()]));
1753
1754 assert!(matches!(
1755 bloq.subdivide_quantum_edge(
1756 lower,
1757 upper,
1758 &[PaddingInstance {
1759 template: empty,
1760 offset: IVec2::ZERO,
1761 }],
1762 1,
1763 ),
1764 Err(EditError::InvalidStoredSeam)
1765 ));
1766 }
1767
1768 fn looped_template(repetitions: u32) -> BloqTemplate {
1770 let mut circuit = CoordCircuit::new();
1771 let body = circuit.add_body(bloq_circuit::CircuitBody::new());
1772 let entry = circuit.entry_body();
1773 circuit
1774 .body_mut(entry)
1775 .expect("entry body exists")
1776 .ops_mut()
1777 .push(Op::Repeat { body, repetitions });
1778 BloqTemplate::new(circuit)
1779 }
1780
1781 fn seam_bloq_with_padding() -> (Bloq, BloqNodeId, BloqNodeId) {
1783 let (mut bloq, lower, upper, one_round) = seam_bloq(plain_pipe());
1784 let looped = bloq.add_template(looped_template(2));
1785 assert!(bloq.set_quantum_edge_padding(
1786 lower,
1787 upper,
1788 vec![crate::PipePadding {
1789 offset: IVec2::ZERO,
1790 one_round,
1791 looped,
1792 }],
1793 ));
1794 (bloq, lower, upper)
1795 }
1796
1797 #[test]
1798 fn selective_padding_preserves_shared_rows_and_both_record_choices() {
1799 let mut bloq = Bloq::from_text(
1800 "BLOQIR 1
1801template t0 {
1802 circuit {
1803 M (0,0):m0
1804 }
1805 flow _ -> Z(0,0) meas m0
1806}
1807template t1 {
1808 circuit {
1809 M (0,0):m0
1810 }
1811 flow Z(0,0) -> _ meas m0
1812}
1813template t2 {
1814 circuit {
1815 M (0,0):m0
1816 TICK
1817 M (0,0):m1
1818 }
1819 flow Z(0,0) -> _ meas m1
1820}
1821template t3 {
1822 circuit {
1823 M (0,0):m0
1824 }
1825 flow Z(0,0) -> _ meas m0
1826 flow _ -> Z(0,0) meas m0
1827}
1828graph {
1829 n0 quantum {
1830 instance i0 t0 @ (0,0)
1831 }
1832 n1 quantum {
1833 instance i1 t1 @ (0,0)
1834 instance i2 t2 @ (0,0)
1835 detector i0:m0
1836 guard 0 i1 xd0=i1:m0
1837 guard 1 i2 xd0=i2:m1
1838 }
1839 n2 observable fragment measurements i0:m0
1840 n3 compute !in0
1841 n0 -> n1 quantum (0,0,0)>(0,0,1)
1842 n0 -> n2 order
1843 n2 -> n3 value 0
1844 n2 -> n1 value 0
1845 n3 -> n1 value 1
1846}
1847",
1848 )
1849 .unwrap();
1850 bloq.validate().unwrap();
1851 let selection = BloqNodeId(1);
1852 let padding = bloq
1853 .subdivide_quantum_edge(
1854 BloqNodeId(0),
1855 selection,
1856 &[PaddingInstance {
1857 template: TemplateId(3),
1858 offset: IVec2::ZERO,
1859 }],
1860 1,
1861 )
1862 .unwrap();
1863 bloq.validate().unwrap();
1864 assert_eq!(bloq[selection].expect_quantum().detectors.len(), 1);
1865 assert_eq!(bloq.value_inputs(selection).count(), 2);
1866 for value in [false, true] {
1867 let selected = bloq[selection]
1868 .select_quantum_members(|slot| Some(if slot == 0 { value } else { !value }))
1869 .unwrap();
1870 let selected = selected.expect_quantum();
1871 let (expected, _) = compose_seam_detectors(
1872 bloq.templates(),
1873 &[
1874 &bloq[padding].expect_quantum().instances,
1875 &selected.instances,
1876 ],
1877 )
1878 .unwrap()
1879 .after(0);
1880 assert_eq!(selected.detectors, expected);
1881 assert!(
1882 !selected.detectors[0]
1883 .parity
1884 .measurements()
1885 .any(|term| term.instance == TemplateInstanceId(0))
1886 );
1887 }
1888 }
1889
1890 fn body_path(bloq: &Bloq, region: BloqNodeId) -> LevelPath {
1891 bloq.levels()
1892 .find_map(|(path, _)| {
1893 (path == LevelPath::default().child(region, BodySelector::Body)).then_some(path)
1894 })
1895 .expect("region body path exists")
1896 }
1897
1898 fn spliced_repetitions(bloq: &Bloq, node: BloqNodeId) -> Option<u32> {
1900 let instance = &bloq
1901 .node(node)
1902 .expect("node exists")
1903 .expect_quantum()
1904 .instances[0];
1905 bloq.templates()[instance.template_id]
1906 .circuit
1907 .entry_top_level_repeats()
1908 .first()
1909 .copied()
1910 }
1911
1912 #[test]
1913 fn memory_round_insertion_of_zero_rounds_is_rejected() {
1914 let (mut bloq, lower, upper) = seam_bloq_with_padding();
1915 assert!(matches!(
1916 bloq.insert_memory_rounds(
1917 MemoryRoundTarget::Edge {
1918 path: LevelPath::default(),
1919 from: lower,
1920 to: upper
1921 },
1922 0
1923 ),
1924 Err(EditError::EmptyPadding)
1925 ));
1926 }
1927
1928 #[test]
1929 fn memory_round_edge_target_supports_nested_levels() {
1930 let mut bloq = Bloq::new();
1931 let one_round = bloq.add_template(BloqTemplate::new(CoordCircuit::new()));
1932 let looped = bloq.add_template(looped_template(2));
1933 let pipe = plain_pipe();
1934 let mut body = SubGraph::new();
1935 let mut lower = BloqNode::from_members(vec![]);
1936 lower
1937 .expect_quantum_mut()
1938 .instances
1939 .push(TemplateInstance::new(
1940 TemplateInstanceId(0),
1941 one_round,
1942 IVec2::ZERO,
1943 ));
1944 let lower = body.add_node(lower);
1945 let upper = body.add_node(BloqNode::from_members(vec![]));
1946 body.add_edge(
1947 lower,
1948 upper,
1949 BloqEdge::Quantum(Box::new(QuantumEdge {
1950 guard: None,
1951 pipes: vec![PipeSeam {
1952 pipe,
1953 padding: Some(PipePadding {
1954 offset: IVec2::ZERO,
1955 one_round,
1956 looped,
1957 }),
1958 }],
1959 })),
1960 );
1961 let region = bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
1962 restart_source: None,
1963 restart_condition: ClassicalExpr::Const(false),
1964 body,
1965 }));
1966 let path = body_path(&bloq, region);
1967
1968 let padding = bloq
1969 .insert_memory_rounds(
1970 MemoryRoundTarget::Edge {
1971 path: path.clone(),
1972 from: lower,
1973 to: upper,
1974 },
1975 2,
1976 )
1977 .expect("nested seam padding");
1978
1979 let body = bloq.level_at(&path).expect("body remains addressable");
1980 assert_eq!(body[padding].memory_rounds(), Some(2));
1981 assert!(
1982 body.edges_between(lower, padding)
1983 .any(|edge| { matches!(edge.edge, BloqEdge::Quantum(_)) })
1984 );
1985 assert!(
1986 body.edges_between(padding, upper)
1987 .any(|edge| { matches!(edge.edge, BloqEdge::Quantum(_)) })
1988 );
1989
1990 let guard = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
1992 expr: ClassicalExpr::Const(true),
1993 }));
1994 bloq.node_mut(region).unwrap().activation = Some(0);
1995 bloq.add_edge(guard, region, BloqEdge::value(0));
1996 let unchanged = bloq.to_binary();
1997 assert!(matches!(
1998 bloq.insert_memory_rounds(
1999 MemoryRoundTarget::Edge {
2000 path,
2001 from: lower,
2002 to: padding,
2003 },
2004 1,
2005 ),
2006 Err(EditError::MembershipSelectionRequired)
2007 ));
2008 assert_eq!(bloq.to_binary(), unchanged);
2009 }
2010
2011 #[test]
2012 fn memory_round_target_rejects_a_stale_level_path() {
2013 let mut bloq = Bloq::new();
2014 let mut body = SubGraph::new();
2015 let source = body.add_node(BloqNode::from_members(vec![]));
2016 let region = bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
2017 restart_source: None,
2018 restart_condition: ClassicalExpr::Const(false),
2019 body,
2020 }));
2021 let path = body_path(&bloq, region);
2022 bloq.remove_node(region);
2023
2024 assert!(matches!(
2025 bloq.insert_memory_rounds(MemoryRoundTarget::After { path, node: source }, 1),
2026 Err(EditError::InvalidLevelPath { .. })
2027 ));
2028 }
2029
2030 #[test]
2031 fn memory_round_after_target_rejects_malformed_nodes_without_mutation() {
2032 let mut bloq = Bloq::new();
2033 let mut body = SubGraph::new();
2034 let classical = body.add_node(BloqNode::classical(ClassicalNode::Compute {
2035 expr: ClassicalExpr::Const(true),
2036 }));
2037 let missing = body.add_node(BloqNode::from_members(vec![]));
2038 body.remove_node(missing);
2039 let region = bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
2040 restart_source: None,
2041 restart_condition: ClassicalExpr::Const(false),
2042 body,
2043 }));
2044 let path = body_path(&bloq, region);
2045 let unchanged = bloq.to_binary();
2046
2047 assert!(matches!(
2048 bloq.insert_memory_rounds(
2049 MemoryRoundTarget::After {
2050 path: path.clone(),
2051 node: missing,
2052 },
2053 1,
2054 ),
2055 Err(EditError::MemoryRoundTargetMissing { .. })
2056 ));
2057 assert!(matches!(
2058 bloq.insert_memory_rounds(
2059 MemoryRoundTarget::After {
2060 path,
2061 node: classical,
2062 },
2063 1,
2064 ),
2065 Err(EditError::MemoryRoundTargetNotQuantum { .. })
2066 ));
2067 assert_eq!(bloq.to_binary(), unchanged);
2068 }
2069
2070 #[test]
2071 fn memory_round_after_target_appends_a_real_terminal_node() {
2072 let mut bloq = Bloq::new();
2073 let one_round = bloq.add_template(BloqTemplate::new(CoordCircuit::new()));
2074 let looped = bloq.add_template(looped_template(2));
2075 let pipe = plain_pipe();
2076 let mut body = SubGraph::new();
2077 let mut source_node = BloqNode::from_members(vec![]);
2078 source_node
2079 .expect_quantum_mut()
2080 .instances
2081 .push(TemplateInstance::new(
2082 TemplateInstanceId(0),
2083 one_round,
2084 IVec2::ZERO,
2085 ));
2086 let source = body.add_node(source_node);
2087 let include = body.add_node(BloqNode::classical(ClassicalNode::Observable {
2088 index: None,
2089 measurements: Vec::new(),
2090 operators: vec![InstanceBoundaryOperator {
2091 instance: TemplateInstanceId(0),
2092 face: BoundaryFace::Output,
2093 operator: bloq_circuit::PauliMap::empty(),
2094 }],
2095 }));
2096 let guard = body.add_node(BloqNode::classical(ClassicalNode::Compute {
2097 expr: ClassicalExpr::Const(false),
2098 }));
2099 body.node_mut(include).unwrap().activation = Some(0);
2100 body.add_edge(guard, include, BloqEdge::value(0));
2101 body.add_edge(source, include, BloqEdge::Order);
2102 let region = bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
2103 restart_source: None,
2104 restart_condition: ClassicalExpr::Const(false),
2105 body,
2106 }));
2107 let upper = bloq.add_node(BloqNode::from_members(vec![]));
2108 bloq.add_edge(
2109 region,
2110 upper,
2111 BloqEdge::Quantum(Box::new(QuantumEdge {
2112 guard: None,
2113 pipes: vec![PipeSeam {
2114 pipe,
2115 padding: Some(PipePadding {
2116 offset: IVec2::ZERO,
2117 one_round,
2118 looped,
2119 }),
2120 }],
2121 })),
2122 );
2123 let path = body_path(&bloq, region);
2124 bloq.set_logical_outputs(vec![crate::LogicalOutput {
2125 port: IVec3::ZERO,
2126 instance: TemplateInstanceId(0),
2127 x: bloq_circuit::PauliMap::empty(),
2128 z: bloq_circuit::PauliMap::empty(),
2129 }]);
2130 let original = bloq.clone();
2131
2132 let padding = bloq
2133 .insert_memory_rounds(
2134 MemoryRoundTarget::After {
2135 path: path.clone(),
2136 node: source,
2137 },
2138 2,
2139 )
2140 .expect("terminal padding");
2141
2142 let body = bloq.level_at(&path).expect("body remains addressable");
2143 let padding_instance = body[padding].expect_quantum().instances[0].id;
2144 assert_eq!(body[padding].memory_rounds(), Some(2));
2145 assert!(
2146 body.edges_between(source, padding)
2147 .any(|edge| { matches!(edge.edge, BloqEdge::Quantum(_)) })
2148 );
2149 assert!(
2150 body.edges_between(padding, include)
2151 .any(|edge| matches!(edge.edge, BloqEdge::Order))
2152 );
2153 assert!(
2154 !body
2155 .edges_between(source, include)
2156 .any(|edge| matches!(edge.edge, BloqEdge::Order))
2157 );
2158 let Some(ClassicalNode::Observable { operators, .. }) = body[include].try_classical()
2159 else {
2160 panic!("include remains classical")
2161 };
2162 assert_eq!(operators[0].instance, padding_instance);
2163 assert_eq!(body[include].activation, Some(0));
2164 assert!(
2165 body.value_inputs(include)
2166 .any(|input| input.producer == guard)
2167 );
2168 assert_eq!(bloq.logical_outputs()[0].instance, padding_instance);
2169 let original_body = original.level_at(&path).unwrap();
2170 let Some(ClassicalNode::Observable { operators, .. }) =
2171 original_body[include].try_classical()
2172 else {
2173 panic!("shared observable remains classical")
2174 };
2175 assert_eq!(operators[0].instance, TemplateInstanceId(0));
2176 assert_eq!(original_body[include].activation, Some(0));
2177 assert_eq!(
2178 original.logical_outputs()[0].instance,
2179 TemplateInstanceId(0)
2180 );
2181 }
2182
2183 #[test]
2184 fn partial_seam_padding_moves_only_its_logical_output_owner() {
2185 let (mut bloq, lower, upper, template) = seam_bloq(plain_pipe());
2186 bloq.node_mut(lower)
2187 .unwrap()
2188 .expect_quantum_mut()
2189 .instances
2190 .push(TemplateInstance::new(
2191 TemplateInstanceId(1),
2192 template,
2193 IVec2::X,
2194 ));
2195 bloq.set_logical_outputs(
2196 (0..2)
2197 .map(|instance| crate::LogicalOutput {
2198 port: IVec3::new(instance as i32, 0, 0),
2199 instance: TemplateInstanceId(instance),
2200 x: bloq_circuit::PauliMap::empty(),
2201 z: bloq_circuit::PauliMap::empty(),
2202 })
2203 .collect(),
2204 );
2205 let padding = bloq
2206 .subdivide_quantum_edge(
2207 lower,
2208 upper,
2209 &[PaddingInstance {
2210 template,
2211 offset: IVec2::ZERO,
2212 }],
2213 1,
2214 )
2215 .unwrap();
2216 let owner = bloq[padding].expect_quantum().instances[0].id;
2217 assert_eq!(bloq.logical_outputs()[0].instance, owner);
2218 assert_eq!(bloq.logical_outputs()[1].instance, TemplateInstanceId(1));
2219 }
2220
2221 #[test]
2222 fn memory_round_insertion_without_padding_provenance_is_rejected() {
2223 let (mut bloq, lower, upper, _) = seam_bloq(plain_pipe());
2224 assert!(matches!(
2225 bloq.insert_memory_rounds(
2226 MemoryRoundTarget::Edge {
2227 path: LevelPath::default(),
2228 from: lower,
2229 to: upper
2230 },
2231 2
2232 ),
2233 Err(EditError::EdgePaddingMissing { .. })
2234 ));
2235 }
2236
2237 #[test]
2238 fn memory_round_insertion_uses_recorded_templates_without_cloning() {
2239 for (rounds, expect_repeat) in [(1, None), (3, Some(2))] {
2241 let (mut bloq, lower, upper) = seam_bloq_with_padding();
2242 let pool_before = bloq.templates().len();
2243 let mid = bloq
2244 .insert_memory_rounds(
2245 MemoryRoundTarget::Edge {
2246 path: LevelPath::default(),
2247 from: lower,
2248 to: upper,
2249 },
2250 rounds,
2251 )
2252 .expect("flowless decoder wait splices");
2253 assert_eq!(
2254 bloq.node(mid).expect("mid exists").memory_rounds(),
2255 Some(rounds)
2256 );
2257 assert_eq!(spliced_repetitions(&bloq, mid), expect_repeat);
2258 assert_eq!(bloq.templates().len(), pool_before);
2259 }
2260 }
2261
2262 #[test]
2263 fn template_edits_invalidate_cached_padding_specializations() {
2264 let (mut bloq, lower, upper) = seam_bloq_with_padding();
2265 let pool_before = bloq.templates().len();
2266 let mid = bloq
2267 .insert_memory_rounds(
2268 MemoryRoundTarget::Edge {
2269 path: LevelPath::default(),
2270 from: lower,
2271 to: upper,
2272 },
2273 5,
2274 )
2275 .expect("specialized decoder wait splices");
2276 assert_eq!(spliced_repetitions(&bloq, mid), Some(4));
2277 assert_eq!(bloq.templates().len(), pool_before + 1);
2278
2279 let specialized = bloq[mid].expect_quantum().instances[0].template_id;
2280 bloq.set_template_repetitions(specialized, 99)
2281 .expect("retuning succeeds");
2282
2283 let second = bloq
2285 .insert_memory_rounds(
2286 MemoryRoundTarget::Edge {
2287 path: LevelPath::default(),
2288 from: mid,
2289 to: upper,
2290 },
2291 5,
2292 )
2293 .expect("second decoder wait splices");
2294 assert_eq!(spliced_repetitions(&bloq, second), Some(4));
2295 assert_eq!(bloq.templates().len(), pool_before + 2);
2296 }
2297
2298 #[test]
2299 fn memory_round_insertion_on_unlooped_looped_template_is_rejected() {
2300 let (mut bloq, lower, upper, flowless) = seam_bloq(plain_pipe());
2302 assert!(bloq.set_quantum_edge_padding(
2303 lower,
2304 upper,
2305 vec![crate::PipePadding {
2306 offset: IVec2::ZERO,
2307 one_round: flowless,
2308 looped: flowless,
2309 }],
2310 ));
2311 assert!(matches!(
2312 bloq.insert_memory_rounds(
2313 MemoryRoundTarget::Edge {
2314 path: LevelPath::default(),
2315 from: lower,
2316 to: upper
2317 },
2318 2
2319 ),
2320 Err(EditError::PaddingTemplateNotLooped { .. })
2321 ));
2322 }
2323
2324 fn seam_landmarks(padded_second_selection: bool) -> Bloq {
2332 let second = match padded_second_selection {
2333 true => " padding offset (4,0) one t0 loop t1",
2334 false => "",
2335 };
2336 Bloq::from_text(&format!(
2337 "\
2338BLOQIR 1
2339
2340template t0 {{
2341 circuit {{
2342 MPP X(0,0):m0
2343 }}
2344}}
2345
2346template t1 {{
2347 circuit {{
2348 REPEAT 2 b1
2349 }}
2350 body b1 {{
2351 MPP X(0,0):m0
2352 }}
2353}}
2354
2355graph {{
2356 n0 rus in0 source n2 {{
2357 body {{
2358 n0 quantum {{
2359 instance i0 t0 @ (0,0)
2360 }}
2361 n1 quantum {{
2362 instance i1 t0 @ (0,0)
2363 }}
2364 n2 observable fragment measurements i1:m0 from generator 0
2365 n0 -> n1 quantum (0,0,0)>(0,0,1)
2366 n1 -> n2 order
2367 }}
2368 }}
2369 n1 quantum {{
2370 instance i2 t0 @ (0,0)
2371 from blocks (0,0,2)
2372 }}
2373 n2 quantum {{
2374 instance i3 t0 @ (0,0)
2375 instance i9 t0 @ (4,0)
2376 instance i4 t0 @ (0,0)
2377 instance i11 t0 @ (0,0)
2378 instance i10 t0 @ (4,0)
2379 guard 0 i3 i9
2380 guard 1 i4 i11 i10
2381 }}
2382 n6 compute 0
2383 n7 compute !in0
2384 n6 -> n7 value 0
2385 n6 -> n2 value 0
2386 n7 -> n2 value 1
2387 n3 quantum {{
2388 instance i5 t0 @ (4,0)
2389 from blocks (1,0,0)
2390 }}
2391 n4 quantum {{
2392 instance i6 t0 @ (4,0)
2393 from blocks (1,0,1)
2394 }}
2395 n5 quantum {{
2396 instance i7 t0 @ (4,0)
2397 instance i8 t0 @ (4,0)
2398 guard 0 i7
2399 guard 1 i8
2400 }}
2401 n6 -> n5 value 0
2402 n7 -> n5 value 1
2403 n0 -> n1 quantum (0,0,1)>(0,0,2) padding offset (0,0) one t0 loop t1
2404 n1 -> n2 quantum (0,0,2)>(0,0,3) padding offset (0,0) one t0 loop t1
2405 n4 -> n2 quantum (1,0,1)>(1,0,2) padding offset (4,0) one t0 loop t1
2406 n3 -> n4 quantum (1,0,0)>(1,0,1) padding offset (4,0) one t0 loop t1
2407 n4 -> n5 quantum (1,0,1)>(1,0,2){second}
2408}}
2409"
2410 ))
2411 .expect("valid .bloqir text")
2412 }
2413
2414 fn selection_targets(bloq: &Bloq) -> Vec<MemoryRoundTarget> {
2415 bloq.selection_seams()
2416 .unwrap()
2417 .into_iter()
2418 .map(|(from, to)| MemoryRoundTarget::Edge {
2419 path: LevelPath::default(),
2420 from,
2421 to,
2422 })
2423 .collect()
2424 }
2425
2426 #[test]
2427 fn memory_round_batch_subdivides_every_selected_seam() {
2428 let mut bloq = seam_landmarks(true);
2429 let targets = selection_targets(&bloq);
2430 let padding = bloq.insert_memory_rounds_batch(&targets, 3).unwrap();
2431
2432 assert_eq!(padding.len(), 3, "one padding node per selection seam");
2433 for (&padding, selection) in
2434 padding
2435 .iter()
2436 .zip([BloqNodeId(2), BloqNodeId(2), BloqNodeId(5)])
2437 {
2438 assert_eq!(bloq[padding].memory_rounds(), Some(3));
2439 assert!(
2440 bloq.edges_between(padding, selection)
2441 .any(|edge| matches!(edge.edge, BloqEdge::Quantum(_)))
2442 );
2443 }
2444 }
2445
2446 #[test]
2447 fn memory_round_batch_is_all_or_nothing() {
2448 let mut bloq = seam_landmarks(false);
2449 let targets = selection_targets(&bloq);
2450 let before = bloq.to_binary();
2451 assert!(matches!(
2453 bloq.insert_memory_rounds_batch(&targets, 1),
2454 Err(EditError::EdgePaddingMissing { .. })
2455 ));
2456 assert_eq!(bloq.to_binary(), before);
2457 }
2458
2459 #[test]
2460 fn memory_round_batch_mixes_edges_and_region_terminals() {
2461 let mut bloq = seam_landmarks(true);
2462 let path = LevelPath::default().child(BloqNodeId(0), BodySelector::Body);
2463 let escape = bloq.level_at(&path).unwrap().quantum_tail().unwrap();
2464 let mut targets = selection_targets(&bloq);
2465 targets.push(MemoryRoundTarget::After {
2466 path: path.clone(),
2467 node: escape,
2468 });
2469 let padding = bloq.insert_memory_rounds_batch(&targets, 2).unwrap();
2470
2471 assert_eq!(padding.len(), 4);
2472 for &node in &padding[..3] {
2473 assert_eq!(bloq[node].memory_rounds(), Some(2));
2474 }
2475 let body = bloq.level_at(&path).unwrap();
2476 let inserted = padding[3];
2477 assert_eq!(body[inserted].memory_rounds(), Some(2));
2478 assert!(
2479 body.edges_between(escape, inserted)
2480 .any(|edge| matches!(edge.edge, BloqEdge::Quantum(_)))
2481 );
2482 assert_eq!(body.quantum_tail(), Ok(inserted));
2483 }
2484
2485 #[test]
2486 fn memory_round_batch_rolls_back_a_region_terminal_edit() {
2487 let mut bloq = seam_landmarks(false);
2488 let path = LevelPath::default().child(BloqNodeId(0), BodySelector::Body);
2489 let escape = bloq.level_at(&path).unwrap().quantum_tail().unwrap();
2490 let bad_edge = selection_targets(&bloq).pop().unwrap();
2491 let targets = [MemoryRoundTarget::After { path, node: escape }, bad_edge];
2492 let before = bloq.to_binary();
2493 assert!(matches!(
2494 bloq.insert_memory_rounds_batch(&targets, 2),
2495 Err(EditError::EdgePaddingMissing { .. })
2496 ));
2497 assert_eq!(bloq.to_binary(), before);
2498 }
2499
2500 #[test]
2501 fn memory_round_batch_handles_empty_zero_and_duplicate_targets() {
2502 let mut bloq = seam_landmarks(true);
2503 let before = bloq.to_binary();
2504 assert_eq!(bloq.insert_memory_rounds_batch(&[], 1), Ok(Vec::new()));
2505 assert_eq!(bloq.to_binary(), before);
2506 assert_eq!(
2507 bloq.insert_memory_rounds_batch(&[], 0),
2508 Err(EditError::EmptyPadding)
2509 );
2510 let target = selection_targets(&bloq).remove(0);
2511 assert!(matches!(
2512 bloq.insert_memory_rounds_batch(&[target.clone(), target], 1),
2513 Err(EditError::InvalidQuantumEdge { .. })
2514 ));
2515 assert_eq!(bloq.to_binary(), before);
2516 }
2517
2518 #[test]
2519 fn template_repetitions_retune_in_place_without_touching_a_shared_pool() {
2520 let mut bloq = Bloq::new();
2521 let looped = bloq.add_shared_template(std::sync::Arc::new(looped_template(8)));
2522 let mut shared = Bloq::new();
2525 let shared_id = shared.add_shared_template(std::sync::Arc::clone(
2526 bloq.templates().iter_shared().next().unwrap().1,
2527 ));
2528 assert!(std::ptr::eq(
2529 &bloq.templates()[looped],
2530 &shared.templates()[shared_id]
2531 ));
2532
2533 bloq.set_template_repetitions(looped, 3)
2534 .expect("a single-loop template retunes");
2535 assert!(!std::ptr::eq(
2536 &bloq.templates()[looped],
2537 &shared.templates()[shared_id]
2538 ));
2539
2540 assert_eq!(
2541 bloq.templates()[looped].circuit.entry_top_level_repeats(),
2542 [3]
2543 );
2544 assert_eq!(
2545 shared.templates()[shared_id]
2546 .circuit
2547 .entry_top_level_repeats(),
2548 [8],
2549 "retuning is copy-on-write"
2550 );
2551 }
2552
2553 #[test]
2554 fn retuning_rejects_a_template_with_no_single_loop() {
2555 let mut bloq = Bloq::new();
2556 let flat = bloq.add_template(BloqTemplate::new(CoordCircuit::new()));
2557 let mut twice = looped_template(2);
2558 let body = twice.circuit.entry_body();
2559 let repeat = twice.circuit.body(body).expect("entry body").ops()[0].clone();
2560 twice
2561 .circuit
2562 .body_mut(body)
2563 .expect("entry body")
2564 .ops_mut()
2565 .push(repeat);
2566 let twice = bloq.add_template(twice);
2567
2568 assert_eq!(
2569 bloq.set_template_repetitions(flat, 1),
2570 Err(EditError::TemplateNotSingleLooped {
2571 template: flat,
2572 repeats: 0,
2573 })
2574 );
2575 assert_eq!(
2576 bloq.set_template_repetitions(twice, 1),
2577 Err(EditError::TemplateNotSingleLooped {
2578 template: twice,
2579 repeats: 2,
2580 })
2581 );
2582 assert_eq!(
2583 bloq.set_template_repetitions(TemplateId(9), 1),
2584 Err(EditError::UnknownTemplate {
2585 template: TemplateId(9),
2586 })
2587 );
2588 }
2589
2590 #[test]
2591 fn splice_rewires_edge_and_allocates_fresh_instance_id() {
2592 let (mut bloq, lower, upper, template) = seam_bloq(plain_pipe());
2593 let padding = [PaddingInstance {
2594 template,
2595 offset: IVec2::ZERO,
2596 }];
2597 let mid = bloq
2598 .subdivide_quantum_edge(lower, upper, &padding, 1)
2599 .expect("flowless seam splice succeeds");
2600
2601 assert!(matches!(
2603 bloq.subdivide_quantum_edge(lower, upper, &padding, 1),
2604 Err(EditError::InvalidQuantumEdge { .. })
2605 ));
2606 let order = bloq
2607 .deterministic_emit_order()
2608 .expect("spliced program is acyclic");
2609 assert_eq!(order, vec![lower, mid, upper]);
2610
2611 let mid_node = bloq.node(mid).expect("padding node exists");
2612 assert_eq!(mid_node.layer(), 1, "padding sits on the pipe layer");
2613 assert_eq!(
2614 mid_node.memory_rounds(),
2615 Some(1),
2616 "padding is identifiable and carries its round count"
2617 );
2618 assert_eq!(
2620 mid_node.expect_quantum().instances[0].id,
2621 TemplateInstanceId(1)
2622 );
2623 bloq.validate().expect("spliced program validates");
2624 }
2625}