Skip to main content

bloq_graph/zx/
graph.rs

1//! ZX graph storage, validation, and stabilizer entry points.
2
3use super::ZXLayerView;
4use super::modular::{StabilizerPhaseBasis, canonical_external_basis};
5use super::stabilizer::{
6    CanonicalStabilizerTable, SearchBudget, Stabilizer, StabilizerError, StabilizerGenerator,
7    StabilizerGenerators, StabilizerRowKind, canonicalize_stabilizer_table,
8    canonicalize_stabilizer_table_legacy, canonicalize_stabilizer_table_with_external_basis,
9    canonicalize_stabilizer_table_with_measurement_prefix,
10    canonicalize_stabilizer_table_with_tagged_prefix, complete_stabilizer_search, contracted_sign,
11    has_forced_direct_action_cycle, stabilizers_satisfy_global_constraints,
12};
13use crate::{
14    Action, ActionDag, BlockGraphError, CubeKind, FeedbackTarget, MeasureTarget,
15    MeasurementObservable, ModuleCertificationLimits, SelectiveKind,
16};
17use bloq_utils::{Pauli, PauliBasis, PauliString, PhasedPauliString};
18use glam::IVec3;
19#[cfg(feature = "gltf")]
20use glam::Vec3;
21use rustc_hash::FxHashMap;
22use smallvec::SmallVec;
23use std::collections::HashMap;
24use std::ops::Range;
25use std::sync::{Arc, OnceLock};
26use strum::Display;
27use thiserror::Error;
28
29/// The kind of a [`ZXNode`]: an X/Y/Z spider or a boundary node.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Display)]
31pub enum NodeKind {
32    /// An X spider.
33    X,
34    /// A Y spider.
35    Y,
36    /// A Z spider.
37    Z,
38    /// An open boundary port.
39    Port,
40    /// A T (magic-state) boundary node.
41    T,
42    /// A selective boundary node resolved at runtime.
43    #[strum(to_string = "Selective({0})")]
44    Selective(SelectiveKind),
45}
46
47impl NodeKind {
48    /// Returns whether this is a [`Port`](NodeKind::Port).
49    pub fn is_port(&self) -> bool {
50        matches!(self, NodeKind::Port)
51    }
52
53    /// Returns whether this is a [`T`](NodeKind::T) node.
54    pub fn is_t(&self) -> bool {
55        matches!(self, NodeKind::T)
56    }
57
58    /// Returns true for all boundary node kinds: Port, T, and Selective.
59    pub fn is_boundary(&self) -> bool {
60        matches!(self, NodeKind::Port | NodeKind::T | NodeKind::Selective(_))
61    }
62
63    /// The Pauli that crosses an X or Z spider without being absorbed.
64    pub(crate) fn cross_pauli(self) -> Pauli {
65        match self {
66            NodeKind::X => Pauli::X,
67            NodeKind::Z => Pauli::Z,
68            _ => unreachable!("only X/Z spiders have a cross Pauli"),
69        }
70    }
71
72    /// Returns true for boundary nodes that can be resolved into concrete ZX spiders.
73    pub(crate) fn is_fillable_boundary(&self) -> bool {
74        matches!(self, NodeKind::Port | NodeKind::Selective(_))
75    }
76}
77
78/// An error from constructing, validating, or filling a [`ZXGraph`].
79#[derive(Debug, Clone, Error)]
80#[non_exhaustive]
81pub enum ZXError {
82    /// An edge spans non-neighboring lattice positions.
83    #[error("ZX edge endpoints are not neighboring positions: {src} -> {dst}")]
84    NonNeighboringZXEdge {
85        /// Edge source.
86        src: IVec3,
87        /// Edge target.
88        dst: IVec3,
89    },
90    /// A node forms a non-planar corner.
91    #[error("ZX graph has a 3D corner at {pos}")]
92    ThreeDimensionalCorner {
93        /// Corner position.
94        pos: IVec3,
95    },
96    /// A special node does not have degree one.
97    #[error("ZX node at {pos} of kind {kind} must be dangling, got degree {degree}")]
98    SpecialNodeNotDangling {
99        /// Node position.
100        pos: IVec3,
101        /// Node kind.
102        kind: NodeKind,
103        /// Observed degree.
104        degree: usize,
105    },
106    /// A special node is not connected along time.
107    #[error("ZX node at {pos} of kind {kind} must connect along the time axis")]
108    SpecialNodeNotTimeLike {
109        /// Node position.
110        pos: IVec3,
111        /// Node kind.
112        kind: NodeKind,
113    },
114    /// A T node does not point toward the future.
115    #[error("T node at {pos} must connect to the future time direction")]
116    TNodeNotFutureDirected {
117        /// T-node position.
118        pos: IVec3,
119    },
120    /// Cube-kind inference produced conflicting values.
121    #[error("conflicting inferred cube kinds at {pos}: existing {existing}, inferred {inferred}")]
122    ConflictingCubeKinds {
123        /// Node position.
124        pos: IVec3,
125        /// Existing cube kind.
126        existing: CubeKind,
127        /// Newly inferred cube kind.
128        inferred: CubeKind,
129    },
130    /// Cube-kind inference failed.
131    #[error("cannot infer a cube kind for ZX node at {pos}")]
132    InvalidInferredCubeKind {
133        /// Node position.
134        pos: IVec3,
135        /// Underlying block error.
136        #[source]
137        source: crate::BlockError,
138    },
139    /// A fill was requested on a non-port node.
140    #[error("cannot fill non-port node at {pos} of kind {kind}")]
141    FillNonPort {
142        /// Node position.
143        pos: IVec3,
144        /// Requested fill kind.
145        kind: NodeKind,
146    },
147    /// A port was filled with another port.
148    #[error("cannot fill port at {pos} with another port kind {kind}")]
149    FillPortWithPort {
150        /// Port position.
151        pos: IVec3,
152        /// Requested fill kind.
153        kind: NodeKind,
154    },
155    /// No port exists at the requested position.
156    #[error("no port at {0} to fill")]
157    NoPort(IVec3),
158    /// A measurement names an invalid ZX site.
159    #[error("measurement {name:?} target {target:?} is not a valid ZX measurement site")]
160    InvalidMeasurementTarget {
161        /// Measurement name.
162        name: String,
163        /// Invalid target.
164        target: MeasureTarget,
165    },
166    /// A measurement target has no observable.
167    #[error("measurement {name:?} target {target:?} is missing an observable")]
168    MissingMeasurementObservable {
169        /// Measurement name.
170        name: String,
171        /// Target lacking an observable.
172        target: MeasureTarget,
173    },
174    /// A measurement observable conflicts with its target.
175    #[error("measurement {name:?} target {target:?} has incompatible observable {observable:?}")]
176    IncompatibleMeasurementObservable {
177        /// Measurement name.
178        name: String,
179        /// Measurement target.
180        target: MeasureTarget,
181        /// Incompatible observable.
182        observable: MeasurementObservable,
183    },
184    /// A resolve target is not selective.
185    #[error("resolve target at {pos} is not a selective ZX node")]
186    InvalidResolveTarget {
187        /// Invalid target position.
188        pos: IVec3,
189    },
190    /// Source block graph is invalid.
191    #[error("{0}")]
192    Graph(#[source] Box<BlockGraphError>),
193}
194
195/// A node in a [`ZXGraph`]: a spider or boundary at a lattice position.
196#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
197pub struct ZXNode {
198    /// Stable node id, used to index neighbor and edge tables.
199    pub id: usize,
200    /// Lattice position of the node.
201    pub pos: IVec3,
202    /// Node kind (spider color or boundary).
203    pub kind: NodeKind,
204    /// Explicit source Port direction, or `Auto` for non-Ports and temporal
205    /// Ports whose direction follows geometry.
206    pub role: crate::PortRole,
207}
208
209impl ZXNode {
210    /// Create a node with automatic port role.
211    pub const fn new(id: usize, pos: IVec3, kind: NodeKind) -> Self {
212        Self {
213            id,
214            pos,
215            kind,
216            role: crate::PortRole::Auto,
217        }
218    }
219
220    /// Set the source port role.
221    pub const fn with_role(mut self, role: crate::PortRole) -> Self {
222        self.role = role;
223        self
224    }
225
226    /// Returns whether this Port participates in the output interface.
227    pub fn is_output_port(&self, graph: &ZXGraph) -> bool {
228        if self.kind != NodeKind::Port {
229            return false;
230        }
231        match self.role {
232            crate::PortRole::Auto => graph.neighbors(self.id).is_some_and(|neighbors| {
233                !neighbors.is_empty()
234                    && neighbors.iter().all(|&n| graph.nodes[n].pos.z < self.pos.z)
235            }),
236            role => role.has_output_boundary(),
237        }
238    }
239
240    /// Returns whether this Port participates in the input interface.
241    pub fn is_input_port(&self, graph: &ZXGraph) -> bool {
242        if self.kind != NodeKind::Port {
243            return false;
244        }
245        match self.role {
246            crate::PortRole::Auto => graph.neighbors(self.id).is_some_and(|neighbors| {
247                !neighbors.is_empty()
248                    && neighbors.iter().all(|&n| graph.nodes[n].pos.z > self.pos.z)
249            }),
250            role => role.has_input_boundary(),
251        }
252    }
253}
254
255/// An edge in a [`ZXGraph`] connecting two nodes, optionally Hadamard-typed.
256#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
257pub struct ZXEdge {
258    /// First endpoint node id.
259    pub n1: usize,
260    /// Second endpoint node id.
261    pub n2: usize,
262    /// Stable edge id.
263    pub id: usize,
264    /// Whether the edge carries a Hadamard.
265    pub hadamard: bool,
266}
267
268impl ZXEdge {
269    fn sorted_nodes(&self) -> (usize, usize) {
270        if self.n1 < self.n2 {
271            (self.n1, self.n2)
272        } else {
273            (self.n2, self.n1)
274        }
275    }
276}
277
278/// Dense node adjacency paired with each directed edge-column id.
279#[derive(Debug, Clone)]
280pub(super) struct CsrAdjacency {
281    offsets: Vec<usize>,
282    neighbors: Vec<usize>,
283    edge_ids: Vec<usize>,
284    has_parallel_edges: bool,
285}
286
287impl CsrAdjacency {
288    pub(super) fn from_edges(node_count: usize, edges: &[(usize, usize, usize)]) -> Self {
289        let mut offsets = vec![0; node_count + 1];
290        let mut last_edge_ids = FxHashMap::default();
291        for &(source, target, edge_id) in edges {
292            offsets[source + 1] += 1;
293            last_edge_ids.insert((source, target), edge_id);
294        }
295        for node in 0..node_count {
296            offsets[node + 1] += offsets[node];
297        }
298
299        let mut neighbors = vec![0; edges.len()];
300        let mut edge_ids = vec![0; edges.len()];
301        let mut next = offsets[..node_count].to_vec();
302        for &(source, target, _) in edges {
303            let index = next[source];
304            neighbors[index] = target;
305            // Preserve the retired `nn2e` map's parallel-edge contract: every
306            // duplicate neighbor entry resolves to the last edge for that pair.
307            edge_ids[index] = last_edge_ids[&(source, target)];
308            next[source] += 1;
309        }
310        Self {
311            offsets,
312            neighbors,
313            edge_ids,
314            has_parallel_edges: edges.len() != last_edge_ids.len(),
315        }
316    }
317
318    pub(super) fn has_parallel_edges(&self) -> bool {
319        self.has_parallel_edges
320    }
321
322    fn range(&self, node: usize) -> Option<Range<usize>> {
323        let end = node.checked_add(1)?;
324        Some(*self.offsets.get(node)?..*self.offsets.get(end)?)
325    }
326
327    fn neighbors(&self, node: usize) -> Option<&[usize]> {
328        Some(&self.neighbors[self.range(node)?])
329    }
330
331    fn edge_id(&self, source: usize, target: usize) -> Option<usize> {
332        let range = self.range(source)?;
333        self.neighbors[range.clone()]
334            .iter()
335            .rposition(|&neighbor| neighbor == target)
336            .map(|index| self.edge_ids[range.start + index])
337    }
338
339    fn neighbor_edges(&self, node: usize) -> Option<impl Iterator<Item = (usize, usize)> + '_> {
340        let range = self.range(node)?;
341        Some(
342            self.neighbors[range.clone()]
343                .iter()
344                .copied()
345                .zip(self.edge_ids[range].iter().copied()),
346        )
347    }
348}
349
350/// A ZX-calculus graph derived from a [`BlockGraph`](crate::BlockGraph).
351///
352/// Nodes and edges use dense integer ids; the graph carries the source action
353/// DAG so measurement and resolve sites can be located by position. It is the
354/// input to stabilizer computation and output-correction analysis.
355#[derive(Debug, Clone)]
356pub struct ZXGraph {
357    pub(crate) nodes: Vec<ZXNode>,
358    pub(crate) edges: Vec<ZXEdge>,
359    pub(super) adjacency: CsrAdjacency,
360    /// Block-center position to node id, for O(1) [`ZXGraph::node_at`].
361    /// Endpoint offsets of multi-cell blocks are deliberately absent.
362    pub(crate) pos_to_node: FxHashMap<IVec3, usize>,
363    pub(crate) action_graph: ActionDag,
364    pub(crate) total_ids: usize,
365    pub(crate) cross_incident: OnceLock<Arc<[CrossIncident]>>,
366    /// Composed sign reference or parallel-edge fallback, shared through X/Z fills.
367    pub(crate) stabilizer_phase_basis: OnceLock<Arc<StabilizerPhaseBasis>>,
368}
369
370pub(crate) type CrossIncident = SmallVec<[(usize, bool); 6]>;
371
372impl ZXGraph {
373    /// Present a stabilizer basis supplied by module composition. This never
374    /// regenerates the external row space from the linked graph.
375    pub(crate) fn stabilizers_from_composed_basis(
376        &self,
377        external_basis: &[PhasedPauliString],
378        adjustment_basis: &[PauliString],
379        cached_prefix: &[(StabilizerRowKind, PauliString)],
380        limits: ModuleCertificationLimits,
381    ) -> Result<StabilizerGenerators, StabilizerError> {
382        self.validate_feedback_targets()?;
383        let mut budget = SearchBudget::with_limits(limits);
384        budget.check_matrix(external_basis.len().saturating_mul(3), self.total_ids, 0, 0)?;
385        let external_basis = canonical_external_basis(external_basis.to_vec(), self.total_ids);
386        let raw_external = external_basis
387            .iter()
388            .map(|row| row.paulis.clone())
389            .collect::<Vec<_>>();
390        let mut rows = canonicalize_stabilizer_table_with_tagged_prefix(
391            self,
392            &raw_external,
393            &external_basis,
394            adjustment_basis,
395            cached_prefix,
396            &mut budget,
397        )
398        .and_then(|table| table.into_stabilizers(self))?;
399        // Runtime recipes use the same composed signed certificate.
400        rows.zx_graph
401            .stabilizer_phase_basis
402            .get_or_init(|| Arc::new(StabilizerPhaseBasis::new(external_basis)));
403        if stabilizers_satisfy_global_constraints(self, &rows)? {
404            match rows.validate_measurements_close_before_outputs_with_limits(limits) {
405                Ok(()) => return Ok(rows),
406                Err(super::RuntimeBasisError::Stabilizer(error)) if error.is_interrupted() => {
407                    return Err(error);
408                }
409                Err(_) => {}
410            }
411        }
412        rows.plan_readouts(limits).map_err(|error| match error {
413            super::RuntimeBasisError::Stabilizer(error) => error,
414            _ => StabilizerError::ComposedPresentationUnsatisfied,
415        })?;
416        let mut dag = self.action_graph().clone();
417        dag.attach_readout_dependencies(&rows.generators, Some(self))
418            .map_err(|error| match error {
419                BlockGraphError::Stabilizer(error) if error.is_interrupted() => error,
420                _ => StabilizerError::ComposedPresentationUnsatisfied,
421            })?;
422        Ok(rows)
423    }
424
425    /// Returns all nodes.
426    pub fn nodes(&self) -> &[ZXNode] {
427        &self.nodes
428    }
429
430    /// Returns all edges.
431    pub fn edges(&self) -> &[ZXEdge] {
432        &self.edges
433    }
434
435    /// Returns the neighbor node ids of `node_id`, if the node exists.
436    pub fn neighbors(&self, node_id: usize) -> Option<&[usize]> {
437        self.adjacency.neighbors(node_id)
438    }
439
440    pub(super) fn neighbor_ids(&self, node_id: usize) -> &[usize] {
441        self.neighbors(node_id).expect("ZX node ids are dense")
442    }
443
444    /// Returns the id of the edge between nodes `n1` and `n2`, if any.
445    pub fn edge_id(&self, n1: usize, n2: usize) -> Option<usize> {
446        self.adjacency.edge_id(n1, n2)
447    }
448
449    pub(super) fn neighbor_edges(
450        &self,
451        node_id: usize,
452    ) -> impl Iterator<Item = (usize, usize)> + '_ {
453        self.adjacency
454            .neighbor_edges(node_id)
455            .expect("ZX node ids are dense")
456    }
457
458    pub(super) fn edge_by_id(&self, edge_id: usize) -> &ZXEdge {
459        &self.edges[edge_id - self.nodes.len()]
460    }
461
462    /// Returns the total number of node and edge ids, which share one id space.
463    pub fn total_ids(&self) -> usize {
464        self.total_ids
465    }
466
467    /// Returns the node at `pos`, if any.
468    pub fn node_at(&self, pos: IVec3) -> Option<&ZXNode> {
469        self.pos_to_node.get(&pos).map(|&id| &self.nodes[id])
470    }
471
472    /// Returns the sorted, deduplicated set of occupied time layers.
473    pub fn z_layers(&self) -> Vec<i32> {
474        let mut zs = self.nodes.iter().map(|node| node.pos.z).collect::<Vec<_>>();
475        zs.sort_unstable();
476        zs.dedup();
477        zs
478    }
479
480    /// Returns a read-only view of the time layer at `z`.
481    pub fn layer(&self, z: i32) -> ZXLayerView {
482        ZXLayerView::new(self, z)
483    }
484
485    /// Returns the action DAG carried over from the source graph.
486    pub fn action_graph(&self) -> &ActionDag {
487        &self.action_graph
488    }
489
490    /// Returns the actions in ordinal order.
491    pub fn actions(&self) -> Vec<Action> {
492        self.action_graph
493            .ordered_nodes()
494            .map(|node| node.action.clone())
495            .collect()
496    }
497
498    /// Returns the stabilizer-table column (node or edge id) that a measurement
499    /// `target` observes, if it maps to a graph element.
500    pub fn measurement_column(&self, target: &MeasureTarget) -> Option<usize> {
501        match target {
502            MeasureTarget::Node(pos) => self.node_at(*pos).map(|node| node.id),
503            MeasureTarget::Edge { src, dir } => {
504                let dst = crate::checked_add_position(*src, dir.to_ivec3()).ok()?;
505                let src_id = self.node_at(*src)?.id;
506                let dst_id = self.node_at(dst)?.id;
507                self.edge_id(src_id, dst_id)
508            }
509        }
510    }
511
512    /// The wire carrying a source Pauli action, oriented away from its target.
513    /// Node feedback uses the first outgoing temporal wire, then the last
514    /// incoming temporal wire, then a sole spatial wire. Ordering is by source
515    /// position, so local and full graphs make the same choice (SEM-READ).
516    pub(crate) fn feedback_edge(&self, target: &FeedbackTarget) -> Option<&ZXEdge> {
517        let node = self.node_at(target.target)?;
518        let neighbor = if let Some(direction) = target.direction {
519            let position = crate::checked_add_position(target.target, direction.to_ivec3()).ok()?;
520            self.node_at(position)?.id
521        } else {
522            let neighbors = self.neighbor_ids(node.id);
523            neighbors
524                .iter()
525                .copied()
526                .filter(|&neighbor| self.nodes[neighbor].pos.z > node.pos.z)
527                .min_by_key(|&neighbor| self.nodes[neighbor].pos.to_array())
528                .or_else(|| {
529                    neighbors
530                        .iter()
531                        .copied()
532                        .filter(|&neighbor| self.nodes[neighbor].pos.z < node.pos.z)
533                        .max_by_key(|&neighbor| self.nodes[neighbor].pos.to_array())
534                })
535                .or_else(|| (neighbors.len() == 1).then(|| neighbors[0]))?
536        };
537        self.edge_between(node.id, neighbor)
538    }
539
540    /// A feedback wire and Pauli in the external table's smaller-endpoint frame.
541    pub(crate) fn feedback_column(&self, target: &FeedbackTarget) -> Option<(usize, Pauli)> {
542        let edge = self.feedback_edge(target)?;
543        Some((
544            edge.id,
545            pauli_at_small_node(edge, edge.n1, Pauli::from(target.pauli)),
546        ))
547    }
548
549    fn validate_feedback_targets(&self) -> Result<(), StabilizerError> {
550        for action in self.action_graph.ordered_nodes() {
551            let Action::Feedback { targets, .. } = &action.action else {
552                continue;
553            };
554            for target in targets {
555                if self.feedback_edge(target).is_none() {
556                    return Err(StabilizerError::FeedbackTargetWithoutWire {
557                        target: target.target,
558                        pauli: target.pauli,
559                    });
560                }
561            }
562        }
563        Ok(())
564    }
565
566    /// Returns a map from each measurement variable name to its column id.
567    pub fn measurement_columns(&self) -> HashMap<String, usize> {
568        let mut cols = HashMap::new();
569
570        for node in self.action_graph.ordered_nodes() {
571            let Action::Measure { name, target } = &node.action else {
572                continue;
573            };
574            let Some(col) = self.measurement_column(target) else {
575                continue;
576            };
577            cols.insert(name.clone(), col);
578        }
579
580        cols
581    }
582
583    /// Returns whether the graph is Clifford (contains no T nodes).
584    pub fn is_clifford_computation(&self) -> bool {
585        self.nodes.iter().all(|node| !node.kind.is_t())
586    }
587
588    /// Returns the edge between nodes `n1` and `n2`, if any.
589    pub fn edge_between(&self, n1: usize, n2: usize) -> Option<&ZXEdge> {
590        self.edge_id(n1, n2)
591            .and_then(|edge_id| self.edges.get(edge_id - self.nodes.len()))
592    }
593
594    /// Returns the sorted output-port positions.
595    pub fn output_ports(&self) -> Vec<glam::IVec3> {
596        let mut result: Vec<glam::IVec3> = self
597            .nodes
598            .iter()
599            .filter(|node| node.is_output_port(self))
600            .map(|node| node.pos)
601            .collect();
602        result.sort_by_key(IVec3::to_array);
603        result.dedup();
604        result
605    }
606
607    /// Validates that every measurement and resolve action targets a legal ZX
608    /// site with a resolved observable, as required before program lowering.
609    ///
610    /// # Errors
611    ///
612    /// Returns a [`ZXError`] describing the first invalid measurement or resolve
613    /// action.
614    pub fn validate_for_program(&self) -> Result<(), ZXError> {
615        for action in self.action_graph.ordered_nodes() {
616            match &action.action {
617                Action::Measure { target, name } => {
618                    let Some(observable) = action.measurement else {
619                        return Err(ZXError::MissingMeasurementObservable {
620                            name: name.clone(),
621                            target: *target,
622                        });
623                    };
624                    let Some(node_id) = self.measurement_column(target) else {
625                        return Err(ZXError::InvalidMeasurementTarget {
626                            name: name.clone(),
627                            target: *target,
628                        });
629                    };
630                    match target {
631                        MeasureTarget::Node(_) => {
632                            let node = &self.nodes[node_id];
633                            let compatible = match node.kind {
634                                NodeKind::Selective(kind) => {
635                                    observable == MeasurementObservable::Selective(kind)
636                                }
637                                NodeKind::X | NodeKind::Z
638                                    if self.neighbor_ids(node.id).len() >= 2 =>
639                                {
640                                    matches!(
641                                        observable,
642                                        MeasurementObservable::Concrete(
643                                            PauliBasis::X | PauliBasis::Z
644                                        )
645                                    )
646                                }
647                                _ => measurement_observable_for_node_kind(node.kind)
648                                    .is_some_and(|expected| expected == observable),
649                            };
650                            if !compatible {
651                                return Err(ZXError::IncompatibleMeasurementObservable {
652                                    name: name.clone(),
653                                    target: *target,
654                                    observable,
655                                });
656                            }
657                        }
658                        MeasureTarget::Edge { .. } => {
659                            if !matches!(
660                                observable,
661                                MeasurementObservable::Concrete(PauliBasis::X)
662                                    | MeasurementObservable::Concrete(PauliBasis::Z)
663                            ) {
664                                return Err(ZXError::IncompatibleMeasurementObservable {
665                                    name: name.clone(),
666                                    target: *target,
667                                    observable,
668                                });
669                            }
670                        }
671                    }
672                }
673                Action::Resolve { target, .. } => {
674                    let Some(node) = self.node_at(*target) else {
675                        return Err(ZXError::InvalidResolveTarget { pos: *target });
676                    };
677                    let NodeKind::Selective(_) = node.kind else {
678                        return Err(ZXError::InvalidResolveTarget { pos: *target });
679                    };
680                }
681                _ => {}
682            }
683        }
684        Ok(())
685    }
686
687    /// Lookup twin of [`Self::edge_between`] for node pairs the caller already
688    /// knows are connected (e.g. endpoints read off an existing edge).
689    ///
690    /// # Panics
691    ///
692    /// Panics when no edge connects `n1` and `n2`.
693    pub(super) fn get_edge(&self, n1: usize, n2: usize) -> &ZXEdge {
694        self.edge_between(n1, n2)
695            .expect("callers pass node pairs read off an existing edge")
696    }
697
698    pub(super) fn edge_column_pair(&self, edge: &ZXEdge) -> (usize, usize) {
699        (
700            self.edge_id(edge.n1, edge.n2)
701                .expect("edge endpoints came from the graph"),
702            self.edge_id(edge.n2, edge.n1)
703                .expect("edge endpoints came from the graph"),
704        )
705    }
706
707    #[cfg(feature = "gltf")]
708    pub(crate) fn sink_position(&self, sink: usize) -> Option<Vec3> {
709        if sink < self.nodes.len() {
710            return Some(self.nodes[sink].pos.as_vec3());
711        }
712        let edge_idx = sink.checked_sub(self.nodes.len())?;
713        let edge = self.edges.get(edge_idx)?;
714        debug_assert_eq!(edge.id, sink);
715        let p1 = self.nodes[edge.n1].pos.as_vec3();
716        let p2 = self.nodes[edge.n2].pos.as_vec3();
717        Some((p1 + p2) / 2.0)
718    }
719
720    pub(crate) fn fill_ports(
721        &self,
722        port_map: &HashMap<IVec3, NodeKind>,
723    ) -> Result<ZXGraph, ZXError> {
724        let mut new_zx = self.clone();
725        // X/Z fills restrict the existing signed relation to I/the chosen
726        // axis. Every surviving raw row retains its phase in that relation.
727        if port_map
728            .values()
729            .any(|kind| !matches!(kind, NodeKind::X | NodeKind::Z))
730        {
731            new_zx.stabilizer_phase_basis.take();
732        }
733        for (&pos, kind) in port_map {
734            let Some(node_id) = self.pos_to_node.get(&pos).copied() else {
735                return Err(ZXError::NoPort(pos));
736            };
737            let node = self.nodes[node_id];
738            if !node.kind.is_fillable_boundary() {
739                return Err(ZXError::FillNonPort {
740                    pos,
741                    kind: node.kind,
742                });
743            }
744            if kind.is_port() {
745                return Err(ZXError::FillPortWithPort { pos, kind: *kind });
746            }
747            let node = &mut new_zx.nodes[node_id];
748            node.kind = *kind;
749        }
750        new_zx.refresh_measurement_metadata(port_map);
751        Ok(new_zx)
752    }
753
754    /// Clone a selected analysis graph and resolve its selective boundaries.
755    /// This diagnostic helper keeps the exact graph available for feedback
756    /// checks after [`GuardedReadoutPlan`](super::GuardedReadoutPlan) unit audit.
757    ///
758    /// # Errors
759    /// Returns an error for an unknown or non-fillable position or invalid fill.
760    #[doc(hidden)]
761    pub fn fill_ports_for_diagnostics(
762        &self,
763        port_map: &HashMap<IVec3, NodeKind>,
764    ) -> Result<ZXGraph, ZXError> {
765        self.fill_ports(port_map)
766    }
767
768    pub(crate) fn local_stabilizer_flow_supports_for_node_kind(
769        &self,
770        node_id: usize,
771        kind: NodeKind,
772    ) -> Vec<Vec<(usize, Pauli)>> {
773        let mut rows = Vec::new();
774
775        match kind {
776            NodeKind::Y => {
777                if let Some((_, edge_id)) = self.neighbor_edges(node_id).next() {
778                    rows.push(vec![(node_id, Pauli::Y), (edge_id, Pauli::Y)]);
779                }
780            }
781            NodeKind::X | NodeKind::Z => {
782                let cross_pauli = kind.cross_pauli();
783                let broadcast_pauli = cross_pauli.flip();
784                let mut neighbor_edges = self.neighbor_edges(node_id).collect::<Vec<_>>();
785                neighbor_edges.sort_unstable_by_key(|&(neighbor, _)| {
786                    (self.nodes[neighbor].pos - self.nodes[node_id].pos).to_array()
787                });
788
789                for neighbor_pair in neighbor_edges.windows(2) {
790                    rows.push(
791                        neighbor_pair
792                            .iter()
793                            .map(|&(_, edge_id)| {
794                                let edge = self.edge_by_id(edge_id);
795                                (edge.id, pauli_at_small_node(edge, node_id, cross_pauli))
796                            })
797                            .collect(),
798                    );
799                }
800
801                let mut broadcast = vec![(node_id, broadcast_pauli)];
802                broadcast.extend(neighbor_edges.iter().map(|&(_, edge_id)| {
803                    let edge = self.edge_by_id(edge_id);
804                    (edge.id, pauli_at_small_node(edge, node_id, broadcast_pauli))
805                }));
806                rows.push(broadcast);
807            }
808            NodeKind::Port | NodeKind::T | NodeKind::Selective(_) => {
809                if let Some((_, edge_id)) = self.neighbor_edges(node_id).next() {
810                    let edge = self.edge_by_id(edge_id);
811                    for pauli in [Pauli::X, Pauli::Z] {
812                        rows.push(vec![
813                            (node_id, pauli),
814                            (edge.id, pauli_at_small_node(edge, node_id, pauli)),
815                        ]);
816                    }
817                }
818            }
819        }
820
821        rows
822    }
823
824    pub(crate) fn row_has_external_edge_pair_support(&self, row: &PauliString) -> bool {
825        let pair_matches = |edge: &ZXEdge| {
826            let (left, right) = self.edge_column_pair(edge);
827            row.get(left) == row.get(right)
828        };
829        // Every mismatch has a nonzero side. Sparse rows need only those
830        // pairs; dense rows keep the single check per undirected edge.
831        if row.weight() < self.edges.len() / 4 {
832            return row
833                .iter_support()
834                .filter(|(col, _)| *col >= self.nodes.len())
835                .all(|(col, _)| pair_matches(self.edge_by_id(col)));
836        }
837        self.edges
838            .iter()
839            .filter(|edge| edge.n1 < edge.n2)
840            .all(pair_matches)
841    }
842
843    /// Recover the linear tensor-center factors used by signed multiplication.
844    /// Crossing-center support belongs to physical materialization and is not
845    /// included here: its OR over incident arms is not a linear Pauli factor.
846    pub(crate) fn reconstruct_raw_centers(&self, row: &mut PauliString) {
847        let mut candidates = row
848            .iter_support()
849            .map(|(column, _)| {
850                if column < self.nodes.len() {
851                    column
852                } else {
853                    self.edge_by_id(column).n1
854                }
855            })
856            .collect::<SmallVec<[usize; 32]>>();
857        candidates.sort_unstable();
858        candidates.dedup();
859        for node_id in candidates {
860            let Some((_, edge_id)) = self.neighbor_edges(node_id).next() else {
861                continue;
862            };
863            let edge = self.edge_by_id(edge_id);
864            let mut pauli = row.get(edge_id);
865            if edge.hadamard && node_id == edge.sorted_nodes().1 {
866                pauli = pauli.flip();
867            }
868            let node = &self.nodes[node_id];
869            if matches!(node.kind, NodeKind::X | NodeKind::Z) {
870                let broadcast = node.kind.cross_pauli().flip();
871                pauli = if pauli & broadcast {
872                    broadcast
873                } else {
874                    Pauli::I
875                };
876            }
877            row.set(node_id, pauli);
878        }
879    }
880
881    /// Removes derived cross-axis center support before raw-table phase recovery.
882    pub(crate) fn clear_cross_centers(&self, table: &mut [PauliString]) {
883        let mut dense = SmallVec::<[&mut PauliString; 8]>::new();
884        let mut supported = SmallVec::<[(usize, Pauli); 32]>::new();
885        for row in table {
886            if row.weight() >= self.nodes.len() / 4 {
887                dense.push(row);
888                continue;
889            }
890            supported.clear();
891            supported.extend(
892                row.iter_support()
893                    .take_while(|(col, _)| *col < self.nodes.len()),
894            );
895            for &(col, current) in &supported {
896                let cross = match self.nodes[col].kind {
897                    NodeKind::X | NodeKind::Z => self.nodes[col].kind.cross_pauli(),
898                    NodeKind::Y | NodeKind::Port | NodeKind::T | NodeKind::Selective(_) => continue,
899                };
900                if current & cross {
901                    row.set(col, current ^ cross);
902                }
903            }
904        }
905        if dense.is_empty() {
906            return;
907        }
908        // Preserve the node-first batch walk for dense rows. Gathering their
909        // support into temporary buffers regresses measured wire builds.
910        for node in &self.nodes {
911            let cross = match node.kind {
912                NodeKind::X | NodeKind::Z => node.kind.cross_pauli(),
913                NodeKind::Y | NodeKind::Port | NodeKind::T | NodeKind::Selective(_) => continue,
914            };
915            for row in &mut dense {
916                let current = row.get(node.id);
917                if current & cross {
918                    row.set(node.id, current ^ cross);
919                }
920            }
921        }
922    }
923
924    pub(crate) fn reconstruct_cross_center(&self, table: &mut [PauliString]) {
925        let incident = self.cross_incident.get_or_init(|| {
926            self.nodes
927                .iter()
928                .map(|node| {
929                    self.neighbor_edges(node.id)
930                        .map(|(_, edge_id)| {
931                            let edge = self.edge_by_id(edge_id);
932                            (edge.id, edge.hadamard && node.id == edge.sorted_nodes().1)
933                        })
934                        .collect()
935                })
936                .collect::<Vec<_>>()
937                .into()
938        });
939        let mut candidates = SmallVec::<[usize; 32]>::new();
940        for row in table {
941            // ponytail: retain the dense walk above this conservative density;
942            // tune only if paired profiles show sparse gathering wins there too.
943            if row.weight() < self.nodes.len() / 4 {
944                candidates.clear();
945                candidates.extend(row.iter_support().map(|(col, _)| {
946                    if col < self.nodes.len() {
947                        col // Include stale centers even when all their arms cancelled.
948                    } else {
949                        self.edges[col - self.nodes.len()].n1
950                    }
951                }));
952                candidates.sort_unstable();
953                candidates.dedup();
954                self.reconstruct_cross_centers_at(row, incident, candidates.iter().copied());
955            } else {
956                self.reconstruct_cross_centers_at(row, incident, 0..self.nodes.len());
957            }
958        }
959    }
960
961    fn reconstruct_cross_centers_at(
962        &self,
963        row: &mut PauliString,
964        incident: &[CrossIncident],
965        nodes: impl Iterator<Item = usize>,
966    ) {
967        for node_id in nodes {
968            let node = &self.nodes[node_id];
969            let cross_pauli = match node.kind {
970                NodeKind::X | NodeKind::Z => node.kind.cross_pauli(),
971                NodeKind::Y | NodeKind::Port | NodeKind::T | NodeKind::Selective(_) => continue,
972            };
973            // Edge Paulis use the smaller endpoint's frame. Retain the cached
974            // incidence lookup, including its last-edge rule for parallel edges.
975            let crosses = incident[node.id].iter().any(|&(edge_col, flip)| {
976                row.get(edge_col)
977                    & if flip {
978                        cross_pauli.flip()
979                    } else {
980                        cross_pauli
981                    }
982            });
983            // Recompute exactly, preserving the arm axis and removing phantom
984            // centers when previously reconstructed crossing edges have cancelled.
985            let current = row.get(node.id);
986            let arm = if current & cross_pauli {
987                current ^ cross_pauli
988            } else {
989                current
990            };
991            row.set(node.id, if crosses { arm | cross_pauli } else { arm });
992        }
993    }
994    #[cfg(test)]
995    pub(crate) fn to_stabilizer_table(&self) -> Result<CanonicalStabilizerTable, StabilizerError> {
996        self.to_stabilizer_table_with_budget(&mut SearchBudget::new(usize::MAX))
997    }
998
999    fn to_stabilizer_table_with_budget(
1000        &self,
1001        budget: &mut SearchBudget,
1002    ) -> Result<CanonicalStabilizerTable, StabilizerError> {
1003        canonicalize_stabilizer_table(self, budget)
1004    }
1005
1006    /// Materializes a Pauli row into graph-position support for rendering and
1007    /// lowering. Row role and measurement identity live on
1008    /// [`StabilizerGenerator`](super::StabilizerGenerator), not this algebraic
1009    /// support value.
1010    pub(crate) fn pauli_string_to_stabilizer(&self, ps: PauliString) -> Stabilizer {
1011        let row_phase = self.stabilizer_row_phases(std::slice::from_ref(&ps))[0];
1012        self.pauli_string_to_stabilizer_with_row_phase(ps, row_phase)
1013    }
1014
1015    /// Rebuild cross centers before materializing a dense row.
1016    pub fn materialize_stabilizer(&self, mut ps: PauliString) -> Stabilizer {
1017        self.reconstruct_cross_center(std::slice::from_mut(&mut ps));
1018        self.pauli_string_to_stabilizer(ps)
1019    }
1020
1021    /// Rebuild cross centers while preserving an already-derived sign.
1022    pub fn materialize_stabilizer_with_sign(&self, mut ps: PauliString, sign: bool) -> Stabilizer {
1023        self.reconstruct_cross_center(std::slice::from_mut(&mut ps));
1024        let mut stabilizer = self.pauli_string_to_stabilizer_with_row_phase(ps, 0);
1025        stabilizer.sign = sign;
1026        stabilizer
1027    }
1028
1029    pub(crate) fn pauli_string_to_stabilizer_with_row_phase(
1030        &self,
1031        ps: PauliString,
1032        row_phase: u8,
1033    ) -> Stabilizer {
1034        let num_nodes = self.nodes.len();
1035        let mut port_stabilizer = FxHashMap::default();
1036        let mut interior_nodes = FxHashMap::default();
1037        let mut interior_edges = FxHashMap::default();
1038        for (col, pauli) in ps.iter_support() {
1039            if col >= num_nodes {
1040                let edge = self.edges[col - num_nodes];
1041                if edge.n1 > edge.n2 {
1042                    continue;
1043                }
1044                debug_assert_eq!(
1045                    pauli,
1046                    ps.get(
1047                        self.edge_id(edge.n2, edge.n1)
1048                            .expect("edge endpoints came from the graph")
1049                    )
1050                );
1051                let pos1 = self.nodes[edge.n1].pos;
1052                let pos2 = self.nodes[edge.n2].pos;
1053                interior_edges.insert((pos1, pos2), pauli);
1054                continue;
1055            }
1056            let node = self.nodes[col];
1057            interior_nodes.insert(node.pos, pauli);
1058            if node.kind.is_port() {
1059                port_stabilizer.insert(node.pos, pauli);
1060            }
1061        }
1062        Stabilizer {
1063            paulis: ps,
1064            sign: contracted_sign(row_phase, &interior_edges),
1065            port_stabilizer,
1066            interior_nodes,
1067            interior_edges,
1068        }
1069    }
1070
1071    #[cfg(feature = "gltf")]
1072    pub(crate) fn measurement_outcome_position(&self, name: &str) -> Option<Vec3> {
1073        self.action_graph.ordered_nodes().find_map(|node| {
1074            let Action::Measure {
1075                name: candidate,
1076                target,
1077            } = &node.action
1078            else {
1079                return None;
1080            };
1081            (candidate == name)
1082                .then(|| {
1083                    self.measurement_column(target)
1084                        .and_then(|col| self.sink_position(col))
1085                })
1086                .flatten()
1087        })
1088    }
1089
1090    fn refresh_measurement_metadata(&mut self, port_map: &HashMap<IVec3, NodeKind>) {
1091        let ordinals: Vec<usize> = self
1092            .action_graph
1093            .ordered_nodes()
1094            .map(|node| node.ordinal)
1095            .collect();
1096        for ordinal in ordinals {
1097            let Some(node) = self.action_graph.node_by_ordinal(ordinal) else {
1098                continue;
1099            };
1100            let Action::Measure {
1101                target: MeasureTarget::Node(pos),
1102                ..
1103            } = &node.action
1104            else {
1105                continue;
1106            };
1107            if !port_map.contains_key(pos) {
1108                continue;
1109            }
1110            let Some(&node_id) = self.pos_to_node.get(pos) else {
1111                continue;
1112            };
1113            if let Some(observable) = measurement_observable_for_node_kind(self.nodes[node_id].kind)
1114            {
1115                self.action_graph
1116                    .set_measurement_observable(ordinal, observable)
1117                    .expect("ordinal comes from ordered_nodes so it is always valid");
1118            }
1119        }
1120    }
1121
1122    /// Returns whether the graph has any open port node.
1123    pub fn is_open(&self) -> bool {
1124        self.nodes.iter().any(|node| node.kind.is_port())
1125    }
1126
1127    /// Computes generators from the external table for both open and closed
1128    /// graphs under [`ModuleCertificationLimits::DEFAULT`]. An affine fallback
1129    /// follows the deterministic path within the same search budget.
1130    ///
1131    /// # Errors
1132    ///
1133    /// Returns a [`StabilizerError`] if the stabilizer table cannot be derived,
1134    /// or cancellation when run inside [`crate::CancellationToken::run`].
1135    pub fn stabilizers(&self) -> Result<StabilizerGenerators, StabilizerError> {
1136        self.stabilizers_with_limits(ModuleCertificationLimits::DEFAULT)
1137    }
1138
1139    pub(crate) fn stabilizers_reusing_measurements(
1140        &self,
1141        measurements: &[&StabilizerGenerator],
1142        limits: ModuleCertificationLimits,
1143    ) -> Result<Option<StabilizerGenerators>, StabilizerError> {
1144        self.validate_feedback_targets()?;
1145        let mut budget = SearchBudget::with_limits(limits);
1146        budget.check_matrix(measurements.len(), self.total_ids(), 0, 0)?;
1147        let cached = measurements
1148            .iter()
1149            .map(|generator| {
1150                Some((
1151                    generator.measurement_name()?,
1152                    self.remap_stabilizer_row(&generator.stabilizer)?,
1153                ))
1154            })
1155            .collect::<Option<Vec<_>>>();
1156        let Some(cached) = cached.filter(|cached| !cached.is_empty()) else {
1157            return Ok(None);
1158        };
1159        self.check_external_table_limits(limits)?;
1160        let (raw_external, signed_external) = self.to_external_generator_table_with_signed();
1161        let Some(table) = canonicalize_stabilizer_table_with_measurement_prefix(
1162            self,
1163            &raw_external,
1164            &signed_external,
1165            &cached,
1166            &mut budget,
1167        )?
1168        else {
1169            return Ok(None);
1170        };
1171        let candidate = match table.into_stabilizers(self) {
1172            Ok(candidate) => candidate,
1173            Err(error) if error.is_interrupted() => return Err(error),
1174            Err(_) => return Ok(None),
1175        };
1176        Ok(stabilizers_satisfy_global_constraints(self, &candidate)?.then_some(candidate))
1177    }
1178
1179    fn remap_stabilizer_row(&self, stabilizer: &Stabilizer) -> Option<PauliString> {
1180        let mut row = PauliString::new(self.total_ids);
1181        for (&pos, &pauli) in &stabilizer.interior_nodes {
1182            row.set(self.node_at(pos)?.id, pauli);
1183        }
1184        for (&(src, dst), &pauli) in &stabilizer.interior_edges {
1185            let src = self.node_at(src)?.id;
1186            let dst = self.node_at(dst)?.id;
1187            row.set(self.edge_id(src, dst)?, pauli);
1188            row.set(self.edge_id(dst, src)?, pauli);
1189        }
1190        Some(row)
1191    }
1192
1193    /// Computes generators under explicit certification limits.
1194    ///
1195    /// # Errors
1196    ///
1197    /// Returns a [`StabilizerError`] for unavailable surfaces or exhausted
1198    /// search, matrix, or selective-domain resources, or request cancellation.
1199    pub fn stabilizers_with_limits(
1200        &self,
1201        limits: ModuleCertificationLimits,
1202    ) -> Result<StabilizerGenerators, StabilizerError> {
1203        if self.total_ids > limits.max_local_columns {
1204            return Err(StabilizerError::ResourceLimited {
1205                phase: "local ZX columns",
1206                observed: self.total_ids,
1207                limit: limits.max_local_columns,
1208            });
1209        }
1210        self.stabilizers_with_external_basis(None, limits)
1211    }
1212
1213    pub(crate) fn stabilizers_from_external_basis_with_limits(
1214        &self,
1215        external_basis: &[PhasedPauliString],
1216        limits: ModuleCertificationLimits,
1217    ) -> Result<StabilizerGenerators, StabilizerError> {
1218        self.stabilizers_with_external_basis(Some(external_basis), limits)
1219    }
1220
1221    pub(crate) fn check_external_table_limits(
1222        &self,
1223        limits: ModuleCertificationLimits,
1224    ) -> Result<(), StabilizerError> {
1225        SearchBudget::with_limits(limits).check_matrix(
1226            // A phase query retains its input and scratch rows alongside the
1227            // external phase basis; each rank is bounded by the source rows.
1228            self.external_source_row_count().saturating_mul(3),
1229            self.total_ids,
1230            0,
1231            0,
1232        )
1233    }
1234
1235    fn external_source_row_count(&self) -> usize {
1236        self.nodes
1237            .iter()
1238            .map(|node| {
1239                let degree = self.neighbor_ids(node.id).len();
1240                match node.kind {
1241                    NodeKind::X | NodeKind::Z => degree.max(1),
1242                    NodeKind::Y => usize::from(degree != 0),
1243                    NodeKind::Port | NodeKind::T | NodeKind::Selective(_) => {
1244                        2 * usize::from(degree != 0)
1245                    }
1246                }
1247            })
1248            .fold(0, usize::saturating_add)
1249    }
1250
1251    fn stabilizers_with_external_basis(
1252        &self,
1253        external_basis: Option<&[PhasedPauliString]>,
1254        limits: ModuleCertificationLimits,
1255    ) -> Result<StabilizerGenerators, StabilizerError> {
1256        self.validate_feedback_targets()?;
1257        let mut budget = SearchBudget::with_limits(limits);
1258        let source_rows = external_basis.map_or_else(
1259            || self.external_source_row_count(),
1260            <[PhasedPauliString]>::len,
1261        );
1262        // Signed and raw external tables coexist during canonicalization.
1263        budget.check_matrix(source_rows.saturating_mul(2), self.total_ids, 0, 0)?;
1264        let supplied_raw = external_basis.map(|basis| {
1265            basis
1266                .iter()
1267                .map(|row| row.paulis.clone())
1268                .collect::<Vec<_>>()
1269        });
1270        let table = match (supplied_raw.as_deref(), external_basis) {
1271            (Some(raw), Some(signed)) => {
1272                canonicalize_stabilizer_table_with_external_basis(self, raw, signed, &mut budget)
1273            }
1274            _ => self.to_stabilizer_table_with_budget(&mut budget),
1275        };
1276        let used_temporal = table
1277            .as_ref()
1278            .is_ok_and(CanonicalStabilizerTable::used_temporal_measurements);
1279        let mut greedy = table.and_then(|table| table.into_stabilizers(self));
1280        if greedy.as_ref().is_err_and(StabilizerError::is_interrupted) {
1281            return greedy;
1282        }
1283        if let Ok(rows) = &greedy
1284            && stabilizers_satisfy_global_constraints(self, rows)?
1285        {
1286            return greedy;
1287        }
1288        let raw_external = supplied_raw.unwrap_or_else(|| self.to_external_generator_table());
1289        if used_temporal {
1290            greedy = canonicalize_stabilizer_table_legacy(self, &raw_external, &mut budget)
1291                .and_then(|table| table.into_stabilizers(self));
1292            if greedy.as_ref().is_err_and(StabilizerError::is_interrupted) {
1293                return greedy;
1294            }
1295            if let Ok(rows) = &greedy
1296                && stabilizers_satisfy_global_constraints(self, rows)?
1297            {
1298                return greedy;
1299            }
1300        }
1301        if greedy.is_ok() && has_forced_direct_action_cycle(self, &raw_external, &mut budget)? {
1302            return greedy;
1303        }
1304
1305        let complete =
1306            complete_stabilizer_search(self, &raw_external, external_basis, &mut budget)?;
1307        if let Some(error) = &complete.failure
1308            && error.is_interrupted()
1309        {
1310            return Err(error.clone());
1311        }
1312        if let Some(rows) = complete.representable {
1313            return Ok(rows);
1314        }
1315        match complete.witness {
1316            Some(rows) => Ok(rows),
1317            None => greedy.map_err(|error| complete.failure.unwrap_or(error)),
1318        }
1319    }
1320}
1321
1322pub(super) fn pauli_at_small_node(edge: &ZXEdge, current_id: usize, current_pauli: Pauli) -> Pauli {
1323    let (_, n2) = edge.sorted_nodes();
1324    if current_id == n2 && edge.hadamard {
1325        current_pauli.flip()
1326    } else {
1327        current_pauli
1328    }
1329}
1330
1331fn measurement_observable_for_node_kind(kind: NodeKind) -> Option<MeasurementObservable> {
1332    match kind {
1333        NodeKind::X => Some(MeasurementObservable::Concrete(PauliBasis::Z)),
1334        NodeKind::Y => Some(MeasurementObservable::Concrete(PauliBasis::Y)),
1335        NodeKind::Z => Some(MeasurementObservable::Concrete(PauliBasis::X)),
1336        _ => None,
1337    }
1338}
1339
1340/// The boundary spider a chosen measurement basis fills a selective node with.
1341pub(super) fn filled_node_kind(chosen: PauliBasis) -> NodeKind {
1342    match chosen {
1343        PauliBasis::X => NodeKind::Z,
1344        PauliBasis::Y => NodeKind::Y,
1345        PauliBasis::Z => NodeKind::X,
1346    }
1347}
1348
1349#[cfg(test)]
1350mod tests {
1351    use std::collections::HashMap;
1352
1353    use bloq_utils::{Pauli, PauliString, PhasedPauliString};
1354    use glam::ivec3;
1355    use rustc_hash::FxHashMap;
1356
1357    use crate::{
1358        Action, ActionDag, Direction, GalleryItem, MeasureTarget, MeasurementObservable,
1359        ModuleCertificationLimits,
1360    };
1361
1362    use super::{CsrAdjacency, NodeKind, ZXEdge, ZXError, ZXGraph, ZXNode};
1363    use crate::zx::StabilizerError;
1364
1365    #[test]
1366    fn scalar_matrix_limit_precedes_dense_table_allocation() {
1367        let graph = GalleryItem::CNOT
1368            .build()
1369            .materialize_root_graph()
1370            .expect("gallery flat projection");
1371        let zx = ZXGraph::from_block_graph_for_analysis(&graph).unwrap();
1372        assert!(matches!(
1373            zx.stabilizers_with_limits(ModuleCertificationLimits {
1374                max_matrix_words: 0,
1375                ..ModuleCertificationLimits::UNLIMITED
1376            }),
1377            Err(StabilizerError::ResourceLimited {
1378                phase: "dense matrix words",
1379                limit: 0,
1380                ..
1381            })
1382        ));
1383        zx.stabilizers_with_limits(ModuleCertificationLimits::UNLIMITED)
1384            .unwrap();
1385    }
1386
1387    #[test]
1388    fn csr_preserves_last_edge_lookup_for_parallel_neighbors() {
1389        let adjacency = CsrAdjacency::from_edges(2, &[(0, 1, 2), (1, 0, 3), (0, 1, 4), (1, 0, 5)]);
1390
1391        assert!(adjacency.has_parallel_edges());
1392        assert_eq!(adjacency.edge_id(0, 1), Some(4));
1393        assert_eq!(
1394            adjacency
1395                .neighbor_edges(0)
1396                .expect("node exists")
1397                .collect::<Vec<_>>(),
1398            vec![(1, 4), (1, 4)]
1399        );
1400    }
1401
1402    #[test]
1403    fn local_phase_queries_preserve_parallel_edge_reduction() {
1404        for hadamard in [false, true] {
1405            let mut zx =
1406                manual_two_node_edge_graph(MeasurementObservable::Concrete(crate::PauliBasis::Z));
1407            for node in &mut zx.nodes {
1408                node.kind = NodeKind::Port;
1409            }
1410            for mut edge in [zx.edges[0], zx.edges[1]] {
1411                edge.id = zx.total_ids;
1412                zx.total_ids += 1;
1413                zx.edges.push(edge);
1414            }
1415            for edge in &mut zx.edges {
1416                edge.hadamard = hadamard;
1417            }
1418            zx.adjacency = CsrAdjacency::from_edges(
1419                zx.nodes.len(),
1420                &zx.edges
1421                    .iter()
1422                    .map(|edge| (edge.n1, edge.n2, edge.id))
1423                    .collect::<Vec<_>>(),
1424            );
1425            let signed = zx.to_external_generator_table_with_signed().1;
1426            let reference = zx.clone();
1427            reference
1428                .stabilizer_phase_basis
1429                .set(std::sync::Arc::new(super::StabilizerPhaseBasis::new(
1430                    signed.clone(),
1431                )))
1432                .unwrap();
1433            for bits in 0..4096 {
1434                let row = PauliString::from_terms(
1435                    zx.total_ids,
1436                    (0..zx.total_ids).map(|column| {
1437                        (
1438                            column,
1439                            [Pauli::I, Pauli::X, Pauli::Z, Pauli::Y][(bits >> (2 * column)) & 3],
1440                        )
1441                    }),
1442                );
1443                let rows = std::slice::from_ref(&row);
1444                let expected = zx.row_phases_against(rows, &signed);
1445                assert_eq!(zx.external_stabilizer_row_phases(rows), expected);
1446                assert_eq!(zx.stabilizer_row_phases(rows), expected);
1447                assert_eq!(
1448                    zx.contains_stabilizer_support(&row),
1449                    reference.contains_stabilizer_support(&row)
1450                );
1451            }
1452        }
1453    }
1454
1455    #[test]
1456    fn local_phase_queries_reject_support_outside_the_graph() {
1457        let zx = manual_two_node_edge_graph(MeasurementObservable::Concrete(crate::PauliBasis::Z));
1458        let mut row = PauliString::new(zx.total_ids + 1);
1459        assert!(zx.contains_stabilizer_support(&row));
1460        row.set(zx.total_ids, Pauli::X);
1461        assert!(!zx.contains_stabilizer_support(&row));
1462        assert_eq!(zx.stabilizer_row_phases(&[row]), [0]);
1463    }
1464
1465    #[test]
1466    fn composed_basis_does_not_fall_back_when_measurement_rows_are_missing() {
1467        let zx = manual_single_node_graph(
1468            NodeKind::Z,
1469            MeasurementObservable::Concrete(crate::PauliBasis::X),
1470        );
1471        let external = zx
1472            .to_external_generator_table()
1473            .into_iter()
1474            .map(PhasedPauliString::positive)
1475            .collect::<Vec<_>>();
1476
1477        assert!(matches!(
1478            zx.stabilizers_from_composed_basis(
1479                &external,
1480                &[],
1481                &[],
1482                ModuleCertificationLimits::UNLIMITED
1483            ),
1484            Err(StabilizerError::ComposedPresentationUnsatisfied)
1485        ));
1486    }
1487
1488    #[test]
1489    fn measurement_column_rejects_an_edge_step_past_the_coordinate_boundary() {
1490        let mut graph = manual_single_node_graph(
1491            NodeKind::Z,
1492            MeasurementObservable::Concrete(crate::PauliBasis::Z),
1493        );
1494        let boundary = ivec3(i32::MAX, 0, 0);
1495        graph.nodes[0].pos = boundary;
1496        graph.pos_to_node = FxHashMap::from_iter([(boundary, 0)]);
1497
1498        assert_eq!(
1499            graph.measurement_column(&MeasureTarget::Edge {
1500                src: boundary,
1501                dir: Direction::XPLUS,
1502            }),
1503            None
1504        );
1505    }
1506
1507    #[cfg(feature = "gltf")]
1508    #[test]
1509    fn sink_position_widens_boundary_edge_endpoints_before_averaging() {
1510        let mut graph =
1511            manual_two_node_edge_graph(MeasurementObservable::Concrete(crate::PauliBasis::Z));
1512        graph.nodes[0].pos = ivec3(i32::MAX - 1, 0, 0);
1513        graph.nodes[1].pos = ivec3(i32::MAX, 0, 0);
1514
1515        let position = graph.sink_position(graph.edges[0].id).unwrap();
1516        assert!(position.x > 1.0e9, "boundary midpoint must not wrap");
1517    }
1518
1519    #[test]
1520    fn fill_ports_rejects_t_nodes() {
1521        let graph = GalleryItem::T
1522            .build()
1523            .materialize_root_graph()
1524            .expect("gallery flat projection");
1525        let zx = ZXGraph::try_from(&graph).unwrap();
1526        let port_map = HashMap::from([(ivec3(1, 0, 0), NodeKind::X)]);
1527
1528        let err = zx
1529            .fill_ports(&port_map)
1530            .expect_err("T nodes must not be fillable");
1531
1532        assert!(matches!(
1533            err,
1534            ZXError::FillNonPort {
1535                pos,
1536            kind: NodeKind::T
1537        } if pos == ivec3(1, 0, 0)
1538        ));
1539    }
1540
1541    #[test]
1542    fn fill_ports_reuses_cached_topology_for_new_cross_centers() {
1543        let mut zx =
1544            manual_two_node_edge_graph(MeasurementObservable::Concrete(crate::PauliBasis::Z));
1545        zx.nodes[0].kind = NodeKind::Port;
1546        zx.reconstruct_cross_center(&mut [PauliString::new(zx.total_ids)]);
1547        zx.stabilizer_phase_basis
1548            .set(std::sync::Arc::new(super::StabilizerPhaseBasis::new(
1549                zx.to_external_generator_table_with_signed().1,
1550            )))
1551            .unwrap();
1552        assert!(zx.stabilizer_phase_basis.get().is_some());
1553
1554        let edge = zx.get_edge(0, 1);
1555        let mut row = PauliString::new(zx.total_ids);
1556        row.set(edge.id, Pauli::X);
1557        let filled = zx
1558            .fill_ports(&HashMap::from([(zx.nodes[0].pos, NodeKind::X)]))
1559            .expect("port fills");
1560        assert!(filled.cross_incident.get().is_some());
1561        assert!(std::sync::Arc::ptr_eq(
1562            zx.stabilizer_phase_basis.get().unwrap(),
1563            filled.stabilizer_phase_basis.get().unwrap()
1564        ));
1565        let rows = filled.to_external_generator_table();
1566        let cached_phases = filled.stabilizer_row_phases(&rows);
1567        let mut fresh = filled.clone();
1568        fresh.stabilizer_phase_basis.take();
1569        assert_eq!(cached_phases, fresh.stabilizer_row_phases(&rows));
1570        filled.reconstruct_cross_center(std::slice::from_mut(&mut row));
1571
1572        assert!(row.get(0) & Pauli::X);
1573    }
1574
1575    #[test]
1576    fn sparse_edge_pair_checks_match_dense_reference_with_parallel_edges() {
1577        let mut zx = ZXGraph::try_from(
1578            &GalleryItem::CCZFactoryWithTels
1579                .build()
1580                .materialize_root_graph()
1581                .expect("gallery flat projection"),
1582        )
1583        .unwrap();
1584        // Parallel endpoint lookups intentionally select the last edge pair.
1585        for mut edge in [zx.edges[0], zx.edges[1]] {
1586            edge.id = zx.total_ids;
1587            zx.total_ids += 1;
1588            zx.edges.push(edge);
1589        }
1590        zx.adjacency = CsrAdjacency::from_edges(
1591            zx.nodes.len(),
1592            &zx.edges
1593                .iter()
1594                .map(|e| (e.n1, e.n2, e.id))
1595                .collect::<Vec<_>>(),
1596        );
1597        let dense_reference = |row: &PauliString| {
1598            zx.edges.iter().filter(|e| e.n1 < e.n2).all(|edge| {
1599                let (left, right) = zx.edge_column_pair(edge);
1600                row.get(left) == row.get(right)
1601            })
1602        };
1603        let columns = [
1604            zx.edges[0].id,
1605            zx.edges[1].id,
1606            zx.edges[2].id,
1607            zx.edges[3].id,
1608            zx.total_ids - 2,
1609            zx.total_ids - 1,
1610        ];
1611        assert!(zx.edges.len() / 4 > columns.len() + 1);
1612        assert!(zx.total_ids > 64);
1613        for bits in 0..4096 {
1614            let mut row = PauliString::new(zx.total_ids);
1615            row.set(0, Pauli::Y); // Node support does not constrain edge pairs.
1616            for (shift, col) in columns.into_iter().enumerate() {
1617                row.set(
1618                    col,
1619                    [Pauli::I, Pauli::X, Pauli::Y, Pauli::Z][(bits >> (2 * shift)) & 3],
1620                );
1621            }
1622            assert_eq!(
1623                zx.row_has_external_edge_pair_support(&row),
1624                dense_reference(&row),
1625                "{bits}"
1626            );
1627        }
1628        for pauli in [Pauli::X, Pauli::Y, Pauli::Z] {
1629            let mut row =
1630                PauliString::from_terms(zx.total_ids, (0..zx.total_ids).map(|col| (col, pauli)));
1631            assert!(zx.row_has_external_edge_pair_support(&row));
1632            row.set(zx.total_ids - 1, Pauli::I);
1633            assert!(!zx.row_has_external_edge_pair_support(&row));
1634        }
1635    }
1636
1637    #[test]
1638    fn supported_cross_centers_match_dense_reference_with_phantom_and_directed_arms() {
1639        // Padding puts the exhaustive four-column cases on the sparse path,
1640        // across packed-word boundaries. The all-Y row exercises the dense path.
1641        const N: usize = 129;
1642        for hadamard in [false, true] {
1643            let mut zx =
1644                manual_two_node_edge_graph(MeasurementObservable::Concrete(crate::PauliBasis::Z));
1645            for id in 2..N {
1646                zx.nodes
1647                    .push(ZXNode::new(id, ivec3(id as i32, 0, 0), NodeKind::X));
1648            }
1649            for (index, edge) in zx.edges.iter_mut().enumerate() {
1650                edge.id = N + index;
1651                edge.hadamard = hadamard;
1652            }
1653            zx.adjacency = CsrAdjacency::from_edges(N, &[(0, 1, N), (1, 0, N + 1)]);
1654            zx.total_ids = N + 2;
1655            for kinds in [
1656                [NodeKind::X, NodeKind::Z],
1657                [NodeKind::Z, NodeKind::X],
1658                [NodeKind::Port, NodeKind::Y],
1659                [NodeKind::T, NodeKind::X],
1660            ] {
1661                zx.nodes[0].kind = kinds[0];
1662                zx.nodes[1].kind = kinds[1];
1663                let mut rows = (0..256)
1664                    .map(|bits| {
1665                        let mut row = PauliString::new(zx.total_ids);
1666                        for (shift, col) in [0, 1, N, N + 1].into_iter().enumerate() {
1667                            row.set(
1668                                col,
1669                                [Pauli::I, Pauli::X, Pauli::Y, Pauli::Z][(bits >> (2 * shift)) & 3],
1670                            );
1671                        }
1672                        row.set(N - 1, Pauli::Y); // Isolated phantom cross plus a real arm.
1673                        row
1674                    })
1675                    .collect::<Vec<_>>();
1676                rows.push(PauliString::new(zx.total_ids));
1677                rows.push(PauliString::from_terms(
1678                    zx.total_ids,
1679                    (0..zx.total_ids).map(|col| (col, Pauli::Y)),
1680                ));
1681                let mut expected = rows.clone();
1682                for row in &mut expected {
1683                    for node in &zx.nodes {
1684                        if !matches!(node.kind, NodeKind::X | NodeKind::Z) {
1685                            continue;
1686                        }
1687                        let cross = node.kind.cross_pauli();
1688                        let crosses = zx.neighbor_edges(node.id).any(|(_, edge_id)| {
1689                            let edge = zx.edge_by_id(edge_id);
1690                            super::pauli_at_small_node(edge, node.id, row.get(edge_id)) & cross
1691                        });
1692                        let arm = if row.get(node.id) & cross.flip() {
1693                            cross.flip()
1694                        } else {
1695                            Pauli::I
1696                        };
1697                        row.set(node.id, arm | if crosses { cross } else { Pauli::I });
1698                    }
1699                }
1700                zx.reconstruct_cross_center(&mut rows);
1701                assert_eq!(rows, expected);
1702                for row in &mut expected {
1703                    for node in &zx.nodes {
1704                        if matches!(node.kind, NodeKind::X | NodeKind::Z) {
1705                            let arm = node.kind.cross_pauli().flip();
1706                            row.set(
1707                                node.id,
1708                                if row.get(node.id) & arm {
1709                                    arm
1710                                } else {
1711                                    Pauli::I
1712                                },
1713                            );
1714                        }
1715                    }
1716                }
1717                zx.clear_cross_centers(&mut rows);
1718                assert_eq!(rows, expected);
1719            }
1720        }
1721    }
1722
1723    #[test]
1724    fn stale_measurement_prefix_is_rejected() {
1725        let graph = GalleryItem::CCZGateTeleport
1726            .build()
1727            .materialize_root_graph()
1728            .expect("gallery flat projection");
1729        let baseline = ZXGraph::from_block_graph_for_analysis(&graph)
1730            .unwrap()
1731            .stabilizers()
1732            .unwrap();
1733        let projection = graph.branch_projections().unwrap().pop().unwrap();
1734        let zx = ZXGraph::from_block_graph_for_analysis(projection.graph()).unwrap();
1735        let mut measurements = baseline
1736            .generators
1737            .into_iter()
1738            .filter(crate::StabilizerGenerator::is_measurement)
1739            .collect::<Vec<_>>();
1740        measurements[0]
1741            .stabilizer
1742            .interior_nodes
1743            .insert(ivec3(i32::MAX, 0, 0), Pauli::X);
1744        let measurements = measurements.iter().collect::<Vec<_>>();
1745
1746        assert!(
1747            zx.stabilizers_reusing_measurements(&measurements, ModuleCertificationLimits::DEFAULT)
1748                .unwrap()
1749                .is_none()
1750        );
1751    }
1752
1753    #[test]
1754    fn validate_for_program_rejects_selective_edge_observables() {
1755        let zx =
1756            manual_two_node_edge_graph(MeasurementObservable::Selective(crate::SelectiveKind::XY));
1757
1758        let err = zx
1759            .validate_for_program()
1760            .expect_err("selective edge observables should be rejected");
1761        assert!(matches!(
1762            err,
1763            ZXError::IncompatibleMeasurementObservable {
1764                target: MeasureTarget::Edge { .. },
1765                observable: MeasurementObservable::Selective(_),
1766                ..
1767            }
1768        ));
1769    }
1770
1771    #[test]
1772    fn validate_for_program_rejects_concrete_y_edge_observables() {
1773        let zx = manual_two_node_edge_graph(MeasurementObservable::Concrete(crate::PauliBasis::Y));
1774
1775        let err = zx
1776            .validate_for_program()
1777            .expect_err("concrete Y edge observables should be rejected");
1778        assert!(matches!(
1779            err,
1780            ZXError::IncompatibleMeasurementObservable {
1781                target: MeasureTarget::Edge { .. },
1782                observable: MeasurementObservable::Concrete(crate::PauliBasis::Y),
1783                ..
1784            }
1785        ));
1786    }
1787
1788    #[test]
1789    fn validate_for_program_rejects_node_observable_kind_mismatch() {
1790        let zx = manual_single_node_graph(
1791            NodeKind::X,
1792            MeasurementObservable::Concrete(crate::PauliBasis::X),
1793        );
1794
1795        let err = zx
1796            .validate_for_program()
1797            .expect_err("node observable mismatches should be rejected");
1798        assert!(matches!(
1799            err,
1800            ZXError::IncompatibleMeasurementObservable {
1801                target: MeasureTarget::Node(_),
1802                observable: MeasurementObservable::Concrete(crate::PauliBasis::X),
1803                ..
1804            }
1805        ));
1806    }
1807
1808    fn manual_single_node_graph(kind: NodeKind, observable: MeasurementObservable) -> ZXGraph {
1809        let action_graph = {
1810            let mut dag = ActionDag::from_actions(&[Action::Measure {
1811                target: MeasureTarget::Node(ivec3(0, 0, 0)),
1812                name: "m".into(),
1813            }]);
1814            dag.set_measurement_observable(0, observable).unwrap();
1815            dag
1816        };
1817        ZXGraph {
1818            nodes: vec![ZXNode::new(0, ivec3(0, 0, 0), kind)],
1819            edges: vec![],
1820            adjacency: CsrAdjacency::from_edges(1, &[]),
1821            pos_to_node: FxHashMap::from_iter([(ivec3(0, 0, 0), 0)]),
1822            action_graph,
1823            total_ids: 1,
1824            cross_incident: Default::default(),
1825            stabilizer_phase_basis: Default::default(),
1826        }
1827    }
1828
1829    fn manual_two_node_edge_graph(observable: MeasurementObservable) -> ZXGraph {
1830        let action_graph = {
1831            let mut dag = ActionDag::from_actions(&[Action::Measure {
1832                target: MeasureTarget::Edge {
1833                    src: ivec3(0, 0, 0),
1834                    dir: Direction::XPLUS,
1835                },
1836                name: "m".into(),
1837            }]);
1838            dag.set_measurement_observable(0, observable).unwrap();
1839            dag
1840        };
1841        ZXGraph {
1842            nodes: vec![
1843                ZXNode::new(0, ivec3(0, 0, 0), NodeKind::X),
1844                ZXNode::new(1, ivec3(1, 0, 0), NodeKind::Z),
1845            ],
1846            edges: vec![
1847                ZXEdge {
1848                    n1: 0,
1849                    n2: 1,
1850                    id: 2,
1851                    hadamard: false,
1852                },
1853                ZXEdge {
1854                    n1: 1,
1855                    n2: 0,
1856                    id: 3,
1857                    hadamard: false,
1858                },
1859            ],
1860            adjacency: CsrAdjacency::from_edges(2, &[(0, 1, 2), (1, 0, 3)]),
1861            pos_to_node: FxHashMap::from_iter([(ivec3(0, 0, 0), 0), (ivec3(1, 0, 0), 1)]),
1862            action_graph,
1863            total_ids: 4,
1864            cross_incident: Default::default(),
1865            stabilizer_phase_basis: Default::default(),
1866        }
1867    }
1868}