Skip to main content

bloq_ir/
alignment.rs

1//! Deterministic raw-moment alignment shared by circuit emitters and viewers.
2
3use 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/// One node's raw moments and whole-circuit qubit footprint.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct MomentLane {
20    /// Node that owns this lane.
21    pub node: BloqNodeId,
22    /// Node-local moment kinds, with `None` for idle slots.
23    pub moments: Vec<Option<MomentKind>>,
24    /// Physical qubits occupied by the lane.
25    pub qubits: Vec<IVec2>,
26}
27
28/// Split a straight-line circuit into alignable kind buckets and idle gaps.
29///
30/// A tick closes the preceding interval; it does not open a trailing empty
31/// interval. Mixed intervals become consecutive same-kind runs, while a leading
32/// or between-ticks empty interval is retained as `None`. Measurement-controlled
33/// corrections stay in source order inside the adjacent visible runs, matching
34/// [`crate::moment_segments`]; they do not add visible moments. A correction-only
35/// interval retains an idle slot and carries its corrections to the next run
36/// (or the last run if trailing). Correction-only circuits have no such anchor
37/// and are rejected.
38///
39/// # Errors
40///
41/// Returns [`MomentAlignmentError::UnsupportedCircuitOperation`] for an
42/// operation that cannot be represented as an aligned moment.
43pub 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/// One node-local moment placed into an aligned slot.
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
104pub struct AlignedMomentRef {
105    /// Node that owns the moment.
106    pub node: BloqNodeId,
107    /// Node-local moment index.
108    pub moment: usize,
109}
110
111/// Concurrent compatible moments. `None` is a real idle slot.
112#[derive(Debug, Clone, Default, PartialEq, Eq)]
113pub struct AlignedSlot {
114    /// Shared moment kind, or `None` for an idle slot.
115    pub kind: Option<MomentKind>,
116    /// Node-local moments placed in this slot.
117    pub entries: Vec<AlignedMomentRef>,
118}
119
120/// Serial aligned slots projected onto one doubled-z display layer.
121#[derive(Debug, Clone, Default, PartialEq, Eq)]
122pub struct AlignedLayer {
123    /// Doubled-z display layer.
124    pub layer: i64,
125    /// Serial slots within the layer.
126    pub slots: Vec<AlignedSlot>,
127}
128
129/// Why raw node moments could not be aligned against their graph level.
130#[derive(Debug, Clone, PartialEq, Eq, Error)]
131#[non_exhaustive]
132pub enum MomentAlignmentError {
133    /// The graph has no causal order.
134    #[error("{0}")]
135    Cycle(#[from] CycleDetected),
136    /// A lane names no live node in this graph level.
137    #[error("moment lane references missing node {node:?}")]
138    UnknownNode {
139        /// Missing node id.
140        node: BloqNodeId,
141    },
142    /// Only quantum nodes own physical moments.
143    #[error("moment lane references non-quantum node {node:?}")]
144    NonQuantumNode {
145        /// Non-quantum node id.
146        node: BloqNodeId,
147    },
148    /// A node has exactly one raw moment lane.
149    #[error("moment lanes contain duplicate node {node:?}")]
150    DuplicateLane {
151        /// Duplicated node id.
152        node: BloqNodeId,
153    },
154    /// Hand-built timeline metadata violates its cumulative-layer contract.
155    #[error("node {node:?} has invalid quantum timeline metadata")]
156    InvalidTimeline {
157        /// Node with invalid timeline metadata.
158        node: BloqNodeId,
159    },
160    /// Timeline cuts count logical rounds, so the descriptor count must agree.
161    #[error(
162        "node {node:?} timeline ends at round {expected}, but its moments mark {actual} round ends"
163    )]
164    TimelineRoundCountMismatch {
165        /// Affected node.
166        node: BloqNodeId,
167        /// Round count declared by the timeline.
168        expected: u32,
169        /// Round count found in the moments.
170        actual: usize,
171    },
172    /// The circuit operation cannot be represented by aligned kind buckets.
173    #[error("moment alignment does not support circuit operation {operation}")]
174    UnsupportedCircuitOperation {
175        /// Unsupported operation name.
176        operation: &'static str,
177    },
178    /// Layer-major emission would start a dependent node before its source ends.
179    #[error(
180        "node {predecessor:?} ends on aligned layer {predecessor_layer}, after dependent node {dependent:?} starts on layer {dependent_layer}"
181    )]
182    LayerOrderConflict {
183        /// Earlier causal node.
184        predecessor: BloqNodeId,
185        /// Dependent node scheduled too early.
186        dependent: BloqNodeId,
187        /// Final predecessor layer.
188        predecessor_layer: i64,
189        /// Initial dependent layer.
190        dependent_layer: i64,
191    },
192    /// The pairwise dynamic-programming table is deliberately bounded.
193    #[error("moment alignment pair is too large ({left_moments} by {right_moments} moments)")]
194    AlignmentTooLarge {
195        /// Left sequence length.
196        left_moments: usize,
197        /// Right sequence length.
198        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
237/// Align raw node moments into deterministic concurrent layer batches.
238///
239/// Timeline-bearing nodes are cut after the cumulative logical-round ends in
240/// [`QuantumTimeline::layer_round_ends`]. Nodes without a timeline stay whole
241/// at [`crate::BloqNode::layer`], except a temporal-Hadamard seam (and seam
242/// waits beside it), which is displayed on its lower/source block layer.
243/// Within a layer, qubit overlap or causal reachability forces separate serial
244/// batches; compatible lanes in one batch fold by decreasing length, using a
245/// shortest-common-supersequence alignment for each pair of moment-kind
246/// sequences.
247///
248/// # Errors
249///
250/// Returns [`MomentAlignmentError`] for invalid lanes, timelines, graph order,
251/// unsupported operations, or an alignment that exceeds its work bound.
252///
253/// # Panics
254///
255/// Panics only if a validated timeline index cannot fit the signed layer type.
256pub 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    // Layer-major output is only valid when each source lane fully precedes
363    // every dependent lane. Same-layer paths are serialized below.
364    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    // ponytail: O(lanes²) reachability probes suit small physical layers;
443    // cache a transitive closure only if profiling shows large layers here.
444    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    // ponytail: pairwise O(n²) DP is bounded to 4M cells; use linear-memory
520    // reconstruction only if real compiled lanes hit this ceiling.
521    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            // On an equal-cost choice, place the new lane first: Start lanes
549            // take the earliest optimal match. End lanes run this in reverse.
550            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}