1use std::cmp::Reverse;
4use std::collections::BTreeMap;
5use std::ops::Range;
6use std::sync::Arc;
7
8use bloq_circuit::Op;
9use glam::IVec2;
10use thiserror::Error;
11
12use crate::{
13 BloqEdge, BloqNodeId, CycleDetected, FxMap, FxSet, MomentKind, MomentSegment, NodeProvenance,
14 QuantumTimeline, SubGraph,
15};
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct MomentLane {
20 pub node: BloqNodeId,
22 pub moments: Vec<Option<MomentKind>>,
24 pub qubits: Vec<IVec2>,
26}
27
28pub fn aligned_moment_segments(
44 ops: &[Op],
45) -> Result<Vec<Option<MomentSegment>>, MomentAlignmentError> {
46 let mut slots = Vec::new();
47 let mut start = 0;
48 let mut segmenter = crate::MomentSegmenter::default();
49
50 let mut append_interval = |interval: &[Op],
51 slots: &mut Vec<Option<MomentSegment>>|
52 -> Result<(), MomentAlignmentError> {
53 for op in interval {
54 let unsupported = match op {
55 Op::Measure {
56 flip_probability, ..
57 } if *flip_probability != 0.0 => Some("measurement flip probability"),
58 Op::Gate { .. } | Op::Measure { .. } | Op::MPP { .. } | Op::ConditionalPauli(_) => {
59 None
60 }
61 Op::Depolarize1 { .. } => Some("DEPOLARIZE1"),
62 Op::Depolarize2 { .. } => Some("DEPOLARIZE2"),
63 Op::PauliError { .. } => Some("Pauli error"),
64 Op::Repeat { .. } => Some("REPEAT"),
65 Op::Tick => Some("TICK"),
66 };
67 if let Some(operation) = unsupported {
68 return Err(MomentAlignmentError::UnsupportedCircuitOperation { operation });
69 }
70 }
71 let segments = segmenter.extend(interval);
72 if segments.is_empty() {
73 slots.push(None);
74 } else {
75 slots.extend(segments.into_iter().map(Some));
76 }
77 Ok(())
78 };
79
80 for (index, op) in ops.iter().enumerate() {
81 if matches!(op, Op::Tick) {
82 append_interval(&ops[start..index], &mut slots)?;
83 start = index + 1;
84 }
85 }
86 if start < ops.len() {
87 append_interval(&ops[start..], &mut slots)?;
88 }
89 if let Some(last) = slots.iter_mut().rev().find_map(Option::as_mut) {
90 segmenter.finish(std::slice::from_mut(last));
91 } else if ops
92 .iter()
93 .any(|op| matches!(op, Op::ConditionalPauli(corrections) if !corrections.is_empty()))
94 {
95 return Err(MomentAlignmentError::UnsupportedCircuitOperation {
96 operation: "conditional Pauli without a visible operation",
97 });
98 }
99 Ok(slots)
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
104pub struct AlignedMomentRef {
105 pub node: BloqNodeId,
107 pub moment: usize,
109}
110
111#[derive(Debug, Clone, Default, PartialEq, Eq)]
113pub struct AlignedSlot {
114 pub kind: Option<MomentKind>,
116 pub entries: Vec<AlignedMomentRef>,
118}
119
120#[derive(Debug, Clone, Default, PartialEq, Eq)]
122pub struct AlignedLayer {
123 pub layer: i64,
125 pub slots: Vec<AlignedSlot>,
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, Error)]
131#[non_exhaustive]
132pub enum MomentAlignmentError {
133 #[error("{0}")]
135 Cycle(#[from] CycleDetected),
136 #[error("moment lane references missing node {node:?}")]
138 UnknownNode {
139 node: BloqNodeId,
141 },
142 #[error("moment lane references non-quantum node {node:?}")]
144 NonQuantumNode {
145 node: BloqNodeId,
147 },
148 #[error("moment lanes contain duplicate node {node:?}")]
150 DuplicateLane {
151 node: BloqNodeId,
153 },
154 #[error("node {node:?} has invalid quantum timeline metadata")]
156 InvalidTimeline {
157 node: BloqNodeId,
159 },
160 #[error(
162 "node {node:?} timeline ends at round {expected}, but its moments mark {actual} round ends"
163 )]
164 TimelineRoundCountMismatch {
165 node: BloqNodeId,
167 expected: u32,
169 actual: usize,
171 },
172 #[error("moment alignment does not support circuit operation {operation}")]
174 UnsupportedCircuitOperation {
175 operation: &'static str,
177 },
178 #[error(
180 "node {predecessor:?} ends on aligned layer {predecessor_layer}, after dependent node {dependent:?} starts on layer {dependent_layer}"
181 )]
182 LayerOrderConflict {
183 predecessor: BloqNodeId,
185 dependent: BloqNodeId,
187 predecessor_layer: i64,
189 dependent_layer: i64,
191 },
192 #[error("moment alignment pair is too large ({left_moments} by {right_moments} moments)")]
194 AlignmentTooLarge {
195 left_moments: usize,
197 right_moments: usize,
199 },
200}
201
202#[derive(PartialEq)]
203enum Anchor {
204 Start,
205 End,
206}
207
208struct LaneSlice {
209 node: BloqNodeId,
210 moments: Arc<Vec<Option<MomentKind>>>,
211 range: Range<usize>,
212 qubits: Arc<FxSet<IVec2>>,
213 anchor: Anchor,
214 rank: usize,
215}
216
217impl LaneSlice {
218 fn len(&self) -> usize {
219 self.range.len()
220 }
221
222 fn slots(&self) -> Vec<AlignedSlot> {
223 self.moments[self.range.clone()]
224 .iter()
225 .enumerate()
226 .map(|(offset, moment)| AlignedSlot {
227 kind: *moment,
228 entries: vec![AlignedMomentRef {
229 node: self.node,
230 moment: self.range.start + offset,
231 }],
232 })
233 .collect()
234 }
235}
236
237pub fn align_moment_lanes(
257 graph: &SubGraph,
258 lanes: Vec<MomentLane>,
259) -> Result<Vec<AlignedLayer>, MomentAlignmentError> {
260 let order = graph.deterministic_emit_order()?;
261 let ranks: FxMap<_, _> = order
262 .iter()
263 .copied()
264 .enumerate()
265 .map(|(rank, node)| (node, rank))
266 .collect();
267 let mut seen = FxSet::default();
268 let mut by_layer = BTreeMap::<i64, Vec<LaneSlice>>::new();
269 let hadamard_source_layers: FxSet<_> = graph
270 .nodes()
271 .filter_map(|(_, node)| match node.provenance {
272 NodeProvenance::TemporalPipe { pipe } if pipe.hadamard => {
273 Some(pipe.src.z.min(pipe.dst.z))
274 }
275 _ => None,
276 })
277 .collect();
278
279 for lane in lanes {
280 if !seen.insert(lane.node) {
281 return Err(MomentAlignmentError::DuplicateLane { node: lane.node });
282 }
283 let node = graph
284 .node(lane.node)
285 .ok_or(MomentAlignmentError::UnknownNode { node: lane.node })?;
286 let quantum = node
287 .try_quantum()
288 .ok_or(MomentAlignmentError::NonQuantumNode { node: lane.node })?;
289 let incoming = graph
290 .incoming(lane.node)
291 .any(|edge| matches!(edge.edge, BloqEdge::Quantum(_)));
292 let outgoing = graph
293 .outgoing(lane.node)
294 .any(|edge| matches!(edge.edge, BloqEdge::Quantum(_)));
295 let moments = Arc::new(lane.moments);
296 let qubits = Arc::new(lane.qubits.into_iter().collect());
297 let rank = ranks[&lane.node];
298 let base_layer = match node.provenance {
299 NodeProvenance::TemporalPipe { pipe } | NodeProvenance::MemoryPadding { pipe, .. }
300 if hadamard_source_layers.contains(&pipe.src.z.min(pipe.dst.z)) =>
301 {
302 2 * i64::from(pipe.src.z.min(pipe.dst.z))
303 }
304 _ => node.layer(),
305 };
306
307 match &quantum.timeline {
308 Some(timeline) => {
309 if !crate::validation::valid_quantum_timeline(timeline, &node.provenance) {
310 return Err(MomentAlignmentError::InvalidTimeline { node: lane.node });
311 }
312 let ranges = timeline_ranges(lane.node, &moments, timeline)?;
313 let count = ranges.len();
314 for (index, range) in ranges.into_iter().enumerate() {
315 let anchor = if index == 0 {
316 if incoming { Anchor::Start } else { Anchor::End }
317 } else if index + 1 == count && (!incoming || outgoing) {
318 Anchor::End
319 } else {
320 Anchor::Start
321 };
322 let offset = i64::try_from(index)
323 .expect("validated timeline span fits the source z range");
324 let layer = base_layer + 2 * offset;
325 by_layer.entry(layer).or_default().push(LaneSlice {
326 node: lane.node,
327 moments: Arc::clone(&moments),
328 range,
329 qubits: Arc::clone(&qubits),
330 anchor,
331 rank,
332 });
333 }
334 }
335 None => {
336 let moment_count = moments.len();
337 by_layer.entry(base_layer).or_default().push(LaneSlice {
338 node: lane.node,
339 moments,
340 range: 0..moment_count,
341 qubits,
342 anchor: if incoming { Anchor::Start } else { Anchor::End },
343 rank,
344 });
345 }
346 }
347 }
348
349 let mut layer_bounds = FxMap::<_, (i64, i64)>::default();
350 for (&layer, slices) in &by_layer {
351 for slice in slices.iter().filter(|slice| !slice.range.is_empty()) {
352 layer_bounds
353 .entry(slice.node)
354 .and_modify(|bounds| {
355 bounds.0 = bounds.0.min(layer);
356 bounds.1 = bounds.1.max(layer);
357 })
358 .or_insert((layer, layer));
359 }
360 }
361
362 let mut scratch = graph.path_scratch();
365 for (source_index, &source) in order.iter().enumerate() {
366 let Some(&(_, source_layer)) = layer_bounds.get(&source) else {
367 continue;
368 };
369 for &target in &order[source_index + 1..] {
370 let Some(&(target_layer, _)) = layer_bounds.get(&target) else {
371 continue;
372 };
373 if source_layer > target_layer
374 && graph.has_path_with_scratch(source, target, &mut scratch)
375 {
376 return Err(MomentAlignmentError::LayerOrderConflict {
377 predecessor: source,
378 dependent: target,
379 predecessor_layer: source_layer,
380 dependent_layer: target_layer,
381 });
382 }
383 }
384 }
385
386 by_layer
387 .into_iter()
388 .map(|(layer, mut slices)| {
389 slices.sort_unstable_by_key(|slice| slice.rank);
390 let slots = partition_batches(graph, slices, &mut scratch)
391 .into_iter()
392 .map(align_batch)
393 .collect::<Result<Vec<_>, _>>()?
394 .into_iter()
395 .flatten()
396 .collect();
397 Ok(AlignedLayer { layer, slots })
398 })
399 .collect()
400}
401
402fn timeline_ranges(
403 node: BloqNodeId,
404 moments: &[Option<MomentKind>],
405 timeline: &QuantumTimeline,
406) -> Result<Vec<Range<usize>>, MomentAlignmentError> {
407 let ends = &timeline.layer_round_ends;
408 let round_ends: Vec<_> = moments
409 .iter()
410 .enumerate()
411 .filter_map(|(index, &kind)| (kind == Some(MomentKind::Measurement)).then_some(index + 1))
412 .collect();
413 let expected = *ends.last().expect("timeline was validated");
414 if round_ends.len() != expected as usize {
415 return Err(MomentAlignmentError::TimelineRoundCountMismatch {
416 node,
417 expected,
418 actual: round_ends.len(),
419 });
420 }
421
422 let mut start = 0;
423 let mut ranges = Vec::with_capacity(ends.len());
424 for &end in &ends[..ends.len() - 1] {
425 let cut = if end == 0 {
426 0
427 } else {
428 round_ends[end as usize - 1]
429 };
430 ranges.push(start..cut);
431 start = cut;
432 }
433 ranges.push(start..moments.len());
434 Ok(ranges)
435}
436
437fn partition_batches(
438 graph: &SubGraph,
439 slices: Vec<LaneSlice>,
440 scratch: &mut crate::PathScratch,
441) -> Vec<Vec<LaneSlice>> {
442 let mut batches: Vec<Vec<LaneSlice>> = Vec::new();
445 for slice in slices {
446 let minimum = batches
447 .iter()
448 .enumerate()
449 .filter(|(_, batch)| {
450 batch
451 .iter()
452 .any(|other| slices_conflict(graph, other, &slice, scratch))
453 })
454 .map(|(index, _)| index + 1)
455 .max()
456 .unwrap_or(0);
457 if minimum == batches.len() {
458 batches.push(Vec::new());
459 }
460 batches[minimum].push(slice);
461 }
462 batches
463}
464
465fn slices_conflict(
466 graph: &SubGraph,
467 left: &LaneSlice,
468 right: &LaneSlice,
469 scratch: &mut crate::PathScratch,
470) -> bool {
471 left.qubits.iter().any(|qubit| right.qubits.contains(qubit))
472 || graph.has_path_with_scratch(left.node, right.node, scratch)
473}
474
475fn align_batch(mut slices: Vec<LaneSlice>) -> Result<Vec<AlignedSlot>, MomentAlignmentError> {
476 slices.sort_unstable_by_key(|slice| (Reverse(slice.len()), slice.rank));
477 let mut slices = slices.into_iter();
478 let first = slices.next().expect("a batch has at least one lane");
479 let mut slots = first.slots();
480 for slice in slices {
481 let mut lane = slice.slots();
482 if slice.anchor == Anchor::End {
483 slots.reverse();
484 lane.reverse();
485 slots = align_from_start(&slots, &lane)?;
486 slots.reverse();
487 } else {
488 slots = align_from_start(&slots, &lane)?;
489 }
490 }
491 for slot in &mut slots {
492 slot.entries
493 .sort_unstable_by_key(|entry| (entry.node, entry.moment));
494 }
495 Ok(slots)
496}
497
498fn align_from_start(
499 left: &[AlignedSlot],
500 right: &[AlignedSlot],
501) -> Result<Vec<AlignedSlot>, MomentAlignmentError> {
502 const MAX_ALIGNMENT_CELLS: usize = 4 * 1024 * 1024;
503 let width = right
504 .len()
505 .checked_add(1)
506 .ok_or(MomentAlignmentError::AlignmentTooLarge {
507 left_moments: left.len(),
508 right_moments: right.len(),
509 })?;
510 let cells = left
511 .len()
512 .checked_add(1)
513 .and_then(|height| height.checked_mul(width))
514 .filter(|&cells| cells <= MAX_ALIGNMENT_CELLS)
515 .ok_or(MomentAlignmentError::AlignmentTooLarge {
516 left_moments: left.len(),
517 right_moments: right.len(),
518 })?;
519 let mut costs = vec![0usize; cells];
522 let at = |i: usize, j: usize| i * width + j;
523 for i in (0..left.len()).rev() {
524 costs[at(i, right.len())] = left.len() - i;
525 }
526 for j in (0..right.len()).rev() {
527 costs[at(left.len(), j)] = right.len() - j;
528 }
529 for i in (0..left.len()).rev() {
530 for j in (0..right.len()).rev() {
531 let mut best = costs[at(i + 1, j)].min(costs[at(i, j + 1)]) + 1;
532 if compatible(left[i].kind, right[j].kind) {
533 best = best.min(costs[at(i + 1, j + 1)] + 1);
534 }
535 costs[at(i, j)] = best;
536 }
537 }
538
539 let mut out = Vec::with_capacity(costs[0]);
540 let (mut i, mut j) = (0, 0);
541 while i < left.len() && j < right.len() {
542 let best = costs[at(i, j)];
543 if compatible(left[i].kind, right[j].kind) && costs[at(i + 1, j + 1)] + 1 == best {
544 out.push(merge_slots(&left[i], &right[j]));
545 i += 1;
546 j += 1;
547 } else if costs[at(i, j + 1)] <= costs[at(i + 1, j)] {
548 out.push(right[j].clone());
551 j += 1;
552 } else {
553 out.push(left[i].clone());
554 i += 1;
555 }
556 }
557 out.extend_from_slice(&left[i..]);
558 out.extend_from_slice(&right[j..]);
559 Ok(out)
560}
561
562fn compatible(left: Option<MomentKind>, right: Option<MomentKind>) -> bool {
563 left.is_none() || right.is_none() || left == right
564}
565
566fn merge_slots(left: &AlignedSlot, right: &AlignedSlot) -> AlignedSlot {
567 let mut entries = left.entries.clone();
568 entries.extend_from_slice(&right.entries);
569 AlignedSlot {
570 kind: left.kind.or(right.kind),
571 entries,
572 }
573}
574
575#[cfg(test)]
576mod tests {
577 use std::collections::BTreeSet;
578
579 use bloq_circuit::{GateType, PauliBasis};
580 use glam::{ivec2, ivec3};
581
582 use super::*;
583 use crate::{BloqNode, ClassicalExpr, RegionNode, SourceBlockRef, TemporalPipeRef};
584
585 fn add_node(graph: &mut SubGraph, x: i32, z: i32) -> BloqNodeId {
586 graph.add_node(BloqNode::from_members(vec![SourceBlockRef {
587 pos: ivec3(x, 0, z),
588 }]))
589 }
590
591 fn temporal_pipe(x: i32, z: i32, hadamard: bool) -> TemporalPipeRef {
592 TemporalPipeRef {
593 src: ivec3(x, 0, z),
594 dst: ivec3(x, 0, z + 1),
595 hadamard,
596 }
597 }
598
599 fn rounds(count: usize) -> Vec<Option<MomentKind>> {
600 (0..count)
601 .flat_map(|_| {
602 [
603 Some(MomentKind::Reset),
604 Some(MomentKind::Interaction),
605 Some(MomentKind::Measurement),
606 ]
607 })
608 .collect()
609 }
610
611 fn lane(node: BloqNodeId, moments: Vec<Option<MomentKind>>, x: i32) -> MomentLane {
612 MomentLane {
613 node,
614 moments,
615 qubits: vec![ivec2(x, 0)],
616 }
617 }
618
619 fn positions(slots: &[AlignedSlot], node: BloqNodeId) -> Vec<usize> {
620 slots
621 .iter()
622 .enumerate()
623 .filter_map(|(slot, aligned)| {
624 aligned
625 .entries
626 .iter()
627 .any(|entry| entry.node == node)
628 .then_some(slot)
629 })
630 .collect()
631 }
632
633 fn nodes(slot: &AlignedSlot) -> BTreeSet<BloqNodeId> {
634 slot.entries.iter().map(|entry| entry.node).collect()
635 }
636
637 #[test]
638 fn circuit_segments_split_kinds_and_treat_terminal_ticks_as_closures() {
639 let reset = Op::Gate {
640 gate: GateType::RZ,
641 qubits: vec![ivec2(0, 0)],
642 };
643 let interaction = Op::Gate {
644 gate: GateType::CX,
645 qubits: vec![ivec2(0, 0), ivec2(1, 0)],
646 };
647
648 let segments =
649 aligned_moment_segments(&[reset.clone(), interaction, Op::Tick, Op::Tick]).unwrap();
650
651 assert_eq!(segments.len(), 3);
652 assert_eq!(segments[0].as_ref().unwrap().kind, MomentKind::Reset);
653 assert_eq!(segments[1].as_ref().unwrap().kind, MomentKind::Interaction);
654 assert!(segments[2].is_none());
655 assert!(aligned_moment_segments(&[]).unwrap().is_empty());
656 assert!(aligned_moment_segments(&[Op::Tick]).unwrap()[0].is_none());
657 assert_eq!(
658 aligned_moment_segments(&[Op::Tick, reset]).unwrap()[1]
659 .as_ref()
660 .unwrap()
661 .kind,
662 MomentKind::Reset
663 );
664
665 let source_order = aligned_moment_segments(&[
666 Op::Gate {
667 gate: GateType::H,
668 qubits: vec![ivec2(0, 0)],
669 },
670 Op::Gate {
671 gate: GateType::RZ,
672 qubits: vec![ivec2(0, 0)],
673 },
674 ])
675 .unwrap();
676 assert_eq!(source_order[0].as_ref().unwrap().kind, MomentKind::Rotation);
677 assert_eq!(source_order[1].as_ref().unwrap().kind, MomentKind::Reset);
678
679 assert_eq!(
680 aligned_moment_segments(&[Op::Depolarize1 {
681 probability: 0.01,
682 qubits: vec![ivec2(0, 0)],
683 }]),
684 Err(MomentAlignmentError::UnsupportedCircuitOperation {
685 operation: "DEPOLARIZE1",
686 })
687 );
688 assert_eq!(
689 aligned_moment_segments(&[Op::Measure {
690 basis: PauliBasis::Z,
691 qubits: vec![ivec2(0, 0)],
692 measurements: vec![0],
693 flip_probability: 0.01,
694 }]),
695 Err(MomentAlignmentError::UnsupportedCircuitOperation {
696 operation: "measurement flip probability",
697 })
698 );
699 }
700
701 #[test]
702 fn aligned_feedforward_keeps_control_ids_order_and_tracker_moments() {
703 let measure = Op::Measure {
704 basis: PauliBasis::Z,
705 qubits: vec![ivec2(0, 0)],
706 measurements: vec![7],
707 flip_probability: 0.0,
708 };
709 let correction = Op::ConditionalPauli(vec![bloq_circuit::ConditionalCorrection {
710 pauli: PauliBasis::X,
711 control: 7,
712 target: ivec2(1, 0),
713 }]);
714 let reset = Op::Gate {
715 gate: GateType::RZ,
716 qubits: vec![ivec2(1, 0)],
717 };
718 for ops in [
719 vec![measure.clone(), correction.clone(), reset.clone()],
720 vec![
721 measure.clone(),
722 Op::Tick,
723 correction.clone(),
724 Op::Tick,
725 reset,
726 ],
727 vec![measure, Op::Tick, correction.clone(), Op::Tick],
728 ] {
729 let aligned = aligned_moment_segments(&ops).unwrap();
730 let visible: Vec<_> = aligned.into_iter().flatten().collect();
731 assert_eq!(visible, crate::moment_segments(&ops));
732 assert_eq!(
733 visible
734 .into_iter()
735 .flat_map(|segment| segment.ops)
736 .collect::<Vec<_>>(),
737 ops.into_iter()
738 .filter(|op| !matches!(op, Op::Tick))
739 .collect::<Vec<_>>(),
740 );
741 }
742 assert_eq!(
743 aligned_moment_segments(&[correction]),
744 Err(MomentAlignmentError::UnsupportedCircuitOperation {
745 operation: "conditional Pauli without a visible operation",
746 }),
747 );
748 }
749
750 #[test]
751 fn pairwise_alignment_refuses_an_oversized_dp_table() {
752 let lane = vec![AlignedSlot::default(); 2048];
753
754 assert_eq!(
755 align_from_start(&lane, &lane),
756 Err(MomentAlignmentError::AlignmentTooLarge {
757 left_moments: 2048,
758 right_moments: 2048,
759 })
760 );
761 }
762
763 #[test]
764 fn compact_round_aligns_with_six_slot_round_and_keeps_idle_slots() {
765 let mut graph = SubGraph::new();
766 let compact = add_node(&mut graph, 0, 0);
767 let extended = add_node(&mut graph, 1, 0);
768 let compact_moments = std::iter::once(Some(MomentKind::Reset))
769 .chain((0..4).map(|_| Some(MomentKind::Interaction)))
770 .chain(std::iter::once(Some(MomentKind::Measurement)))
771 .collect();
772 let extended_moments = vec![
773 Some(MomentKind::Reset),
774 Some(MomentKind::Interaction),
775 None,
776 Some(MomentKind::Interaction),
777 Some(MomentKind::Interaction),
778 Some(MomentKind::Interaction),
779 None,
780 Some(MomentKind::Measurement),
781 ];
782
783 let aligned = align_moment_lanes(
784 &graph,
785 vec![
786 lane(compact, compact_moments, 0),
787 lane(extended, extended_moments, 1),
788 ],
789 )
790 .unwrap();
791
792 assert_eq!(aligned.len(), 1);
793 let slots = &aligned[0].slots;
794 assert_eq!(slots.len(), 8);
795 assert_eq!(slots[0].kind, Some(MomentKind::Reset));
796 assert_eq!(slots[7].kind, Some(MomentKind::Measurement));
797 assert_eq!(positions(slots, compact).len(), 6);
798 assert_eq!(positions(slots, extended), (0..8).collect::<Vec<_>>());
799 }
800
801 #[test]
802 fn shorter_lanes_follow_input_and_output_anchors() {
803 let mut graph = SubGraph::new();
804 let source = add_node(&mut graph, -1, -1);
805 let long = add_node(&mut graph, 0, 0);
806 let start = add_node(&mut graph, 1, 0);
807 let end = add_node(&mut graph, 2, 0);
808 let sink = add_node(&mut graph, 3, 1);
809 graph.add_edge(source, start, BloqEdge::quantum(Vec::new()));
810 graph.add_edge(end, sink, BloqEdge::quantum(Vec::new()));
811
812 let aligned = align_moment_lanes(
813 &graph,
814 vec![
815 lane(long, rounds(3), 0),
816 lane(start, rounds(2), 1),
817 lane(end, rounds(2), 2),
818 ],
819 )
820 .unwrap();
821 let slots = &aligned[0].slots;
822
823 assert_eq!(slots.len(), 9);
824 assert_eq!(positions(slots, start), (0..6).collect::<Vec<_>>());
825 assert_eq!(positions(slots, end), (3..9).collect::<Vec<_>>());
826 }
827
828 #[test]
829 fn timeline_cuts_lane_at_cumulative_round_ends() {
830 let mut graph = SubGraph::new();
831 let node = add_node(&mut graph, 0, 2);
832 graph.node_mut(node).unwrap().expect_quantum_mut().timeline = Some(QuantumTimeline {
833 layer_round_ends: vec![1, 3],
834 });
835 let moments = (0..3)
836 .flat_map(|_| [Some(MomentKind::Reset), Some(MomentKind::Measurement)])
837 .collect();
838
839 let aligned = align_moment_lanes(&graph, vec![lane(node, moments, 0)]).unwrap();
840
841 assert_eq!(
842 aligned.iter().map(|layer| layer.layer).collect::<Vec<_>>(),
843 vec![4, 6]
844 );
845 assert_eq!(positions(&aligned[0].slots, node), vec![0, 1]);
846 assert_eq!(
847 aligned[1]
848 .slots
849 .iter()
850 .flat_map(|slot| &slot.entries)
851 .map(|entry| entry.moment)
852 .collect::<Vec<_>>(),
853 vec![2, 3, 4, 5]
854 );
855 }
856
857 #[test]
858 fn temporal_hadamard_seam_projects_into_its_source_layer() {
859 let mut graph = SubGraph::new();
860 let hadamard = graph.add_node(BloqNode::from_temporal_pipe(temporal_pipe(0, 0, true)));
861 let wait = graph.add_node(BloqNode::memory_padding(temporal_pipe(1, 0, false), 1));
862 let aligned = align_moment_lanes(
863 &graph,
864 vec![
865 lane(hadamard, vec![Some(MomentKind::Reset)], 0),
866 lane(wait, vec![Some(MomentKind::Reset)], 1),
867 ],
868 )
869 .unwrap();
870
871 assert_eq!(aligned.len(), 1);
872 assert_eq!(aligned[0].layer, 0);
873 assert_eq!(aligned[0].slots.len(), 1);
874 assert_eq!(
875 nodes(&aligned[0].slots[0]),
876 BTreeSet::from([hadamard, wait])
877 );
878 }
879
880 #[test]
881 fn temporal_hadamard_follows_the_last_slice_of_a_tall_source() {
882 let mut graph = SubGraph::new();
883 let tall = add_node(&mut graph, 0, 0);
884 graph.node_mut(tall).unwrap().expect_quantum_mut().timeline = Some(QuantumTimeline {
885 layer_round_ends: vec![1, 2],
886 });
887 let pipe = temporal_pipe(0, 1, true);
888 let hadamard = graph.add_node(BloqNode::from_temporal_pipe(pipe));
889 graph.add_edge(tall, hadamard, BloqEdge::quantum(vec![pipe]));
890
891 let aligned = align_moment_lanes(
892 &graph,
893 vec![
894 lane(
895 tall,
896 vec![
897 Some(MomentKind::Reset),
898 Some(MomentKind::Measurement),
899 Some(MomentKind::Reset),
900 Some(MomentKind::Measurement),
901 ],
902 0,
903 ),
904 lane(hadamard, vec![Some(MomentKind::Rotation)], 0),
905 ],
906 )
907 .unwrap();
908
909 assert_eq!(
910 aligned.iter().map(|layer| layer.layer).collect::<Vec<_>>(),
911 vec![0, 2]
912 );
913 assert_eq!(positions(&aligned[1].slots, tall), vec![0, 1]);
914 assert_eq!(positions(&aligned[1].slots, hadamard), vec![2]);
915 }
916
917 #[test]
918 fn qubit_overlap_and_causal_reachability_serialize_lanes() {
919 let mut graph = SubGraph::new();
920 let overlap_a = add_node(&mut graph, 0, 0);
921 let overlap_b = add_node(&mut graph, 1, 0);
922 let causal_a = add_node(&mut graph, 2, 0);
923 let causal_b = add_node(&mut graph, 3, 0);
924 let barrier = graph.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
925 restart_source: None,
926 restart_condition: ClassicalExpr::Const(false),
927 body: SubGraph::new(),
928 }));
929 graph.add_edge(causal_a, barrier, BloqEdge::Order);
930 graph.add_edge(barrier, causal_b, BloqEdge::Order);
931 let one = || vec![Some(MomentKind::Reset)];
932
933 let aligned = align_moment_lanes(
934 &graph,
935 vec![
936 lane(overlap_a, one(), 0),
937 lane(overlap_b, one(), 0),
938 lane(causal_a, one(), 2),
939 lane(causal_b, one(), 3),
940 ],
941 )
942 .unwrap();
943
944 let slots = &aligned[0].slots;
945 assert_eq!(slots.len(), 2);
946 assert_eq!(nodes(&slots[0]), BTreeSet::from([overlap_a, causal_a]));
947 assert_eq!(nodes(&slots[1]), BTreeSet::from([overlap_b, causal_b]));
948 }
949
950 #[test]
951 fn rejects_causality_that_descends_across_aligned_layers() {
952 let mut graph = SubGraph::new();
953 let source = add_node(&mut graph, 0, 1);
954 let target = add_node(&mut graph, 1, 0);
955 graph.add_edge(source, target, BloqEdge::quantum(Vec::new()));
956
957 let error = align_moment_lanes(
958 &graph,
959 vec![
960 lane(source, vec![Some(MomentKind::Reset)], 0),
961 lane(target, vec![Some(MomentKind::Reset)], 1),
962 ],
963 )
964 .unwrap_err();
965
966 assert_eq!(
967 error,
968 MomentAlignmentError::LayerOrderConflict {
969 predecessor: source,
970 dependent: target,
971 predecessor_layer: 2,
972 dependent_layer: 0,
973 }
974 );
975 }
976
977 #[test]
978 fn zero_round_timeline_tail_does_not_extend_causal_bounds() {
979 let mut graph = SubGraph::new();
980 let source = add_node(&mut graph, 0, 0);
981 let target = add_node(&mut graph, 1, 0);
982 graph
983 .node_mut(source)
984 .unwrap()
985 .expect_quantum_mut()
986 .timeline = Some(QuantumTimeline {
987 layer_round_ends: vec![1, 1],
988 });
989 graph.add_edge(source, target, BloqEdge::quantum(Vec::new()));
990
991 let aligned = align_moment_lanes(
992 &graph,
993 vec![
994 lane(source, vec![Some(MomentKind::Measurement)], 0),
995 lane(target, vec![Some(MomentKind::Reset)], 1),
996 ],
997 )
998 .unwrap();
999
1000 assert_eq!(aligned[0].slots.len(), 2);
1001 }
1002}