Skip to main content

bloq_graph/zx/stabilizer/
mod.rs

1//! Stabilizer computation for ZX graphs: deriving the stabilizer generators of
2//! a graph, filling open ports, and the Pauli-string linear algebra behind it.
3
4mod affine;
5mod basis;
6mod computation;
7mod engine;
8mod measurement;
9mod selective;
10
11use super::{RuntimeBasisError, RuntimeStabilizerBasis, SymbolicStabilizerBasis, ZXGraph};
12use crate::{
13    Action, Block, BlockGraph, BlockGraphError, BlockKind, CubeKind, Direction, FeedbackTarget,
14    ModuleCertificationLimits, Pipe, SelectiveKind,
15};
16use bloq_utils::{Basis, Pauli, PauliString, UDirection};
17use glam::IVec3;
18use rustc_hash::FxHashMap;
19use std::collections::{BTreeMap, HashMap, hash_map::Entry};
20use thiserror::Error;
21
22#[cfg(test)]
23pub(crate) use basis::coeffs_to_sparse;
24pub(crate) use basis::{
25    CoeffVec, DeadlineProjection, axis_pivot_constraints, deadline_projection_with_initial_coeffs,
26    gaussian_elimination_with_tracking, reduce_to_basis, solve_coeff_combination,
27};
28pub(crate) use computation::{
29    CanonicalStabilizerTable, canonicalize_stabilizer_table, canonicalize_stabilizer_table_legacy,
30    canonicalize_stabilizer_table_with_external_basis,
31    canonicalize_stabilizer_table_with_measurement_prefix,
32    canonicalize_stabilizer_table_with_tagged_prefix, complete_stabilizer_search,
33    has_forced_direct_action_cycle, stabilizers_satisfy_global_constraints,
34};
35pub(crate) use engine::StabilizerBasis;
36pub(crate) use selective::{
37    SelectiveConstraint, collect_selective_constraints, normalize_selective_support,
38};
39
40/// An error from filling a graph's open ports with concrete boundary spiders.
41#[derive(Debug, Clone, Error)]
42#[non_exhaustive]
43pub enum FillPortsError {
44    /// No open port exists to anchor a minimal fill.
45    #[error("there are no open ports to fill for minimal simulation")]
46    NoOpenPorts,
47    /// A port does not have exactly one pipe.
48    #[error("port at {pos} requires exactly one pipe to infer a fill, got {degree}")]
49    PortFillRequiresSinglePipe {
50        /// Port position.
51        pos: IVec3,
52        /// Observed degree.
53        degree: usize,
54    },
55    /// The cube kind for a filled port cannot be inferred.
56    #[error("cannot infer a cube kind for filled port at {pos}")]
57    InvalidFilledPortCube {
58        /// Port position.
59        pos: IVec3,
60        /// Underlying block error.
61        #[source]
62        source: crate::BlockError,
63    },
64    /// An action control is unavailable in the selected fill.
65    #[error("action {ordinal} depends on a measurement outside the selected static fill")]
66    UnavailableControl {
67        /// Source action ordinal.
68        ordinal: usize,
69    },
70    /// An external stabilizer contains an invalid character.
71    #[error("invalid external stabilizer character {ch}")]
72    InvalidExternalStabilizerChar {
73        /// Invalid character.
74        ch: char,
75    },
76    /// An external stabilizer requests an unsupported port basis.
77    #[error("unsupported external stabilizer basis {basis} at port {pos}")]
78    UnsupportedPortBasis {
79        /// Port position.
80        pos: IVec3,
81        /// Unsupported basis.
82        basis: Pauli,
83    },
84}
85
86/// An error from deriving stabilizer generators or measurement surfaces.
87#[derive(Debug, Clone, Error)]
88#[non_exhaustive]
89pub enum StabilizerError {
90    /// The caller cancelled this analysis at a safe checkpoint.
91    #[error("{0}")]
92    Cancelled(#[from] crate::ComputationCancelled),
93    /// Stabilizer derivation exceeded a resource limit.
94    #[error(
95        "stabilizer derivation exceeded {phase} limit: {observed} > {limit}; {help}",
96        help = ModuleCertificationLimits::RESOURCE_LIMIT_HELP
97    )]
98    ResourceLimited {
99        /// Derivation phase.
100        phase: &'static str,
101        /// Observed resource use.
102        observed: usize,
103        /// Configured limit.
104        limit: usize,
105    },
106    /// The canonical basis has insufficient rank.
107    #[error("canonical stabilizer basis lost rank: expected {expected}, got {actual}")]
108    BasisRankDeficient {
109        /// Required rank.
110        expected: usize,
111        /// Actual rank.
112        actual: usize,
113    },
114    /// Selective support cannot be normalized.
115    #[error("selective support at {pos} for {kind} cannot be normalized")]
116    SelectiveSupportUnsatisfiable {
117        /// Selective position.
118        pos: IVec3,
119        /// Selective kind.
120        kind: SelectiveKind,
121    },
122    /// A control parity is unavailable before its deadline.
123    #[error("control parity for mvar '{mvar}' is not available at deadline {deadline}")]
124    UnavailableControlParity {
125        /// Measurement variable.
126        mvar: String,
127        /// Required deadline.
128        deadline: i64,
129    },
130    /// No valid measurement surface exists.
131    #[error("measurement surface for '{mvar}' is not available at any valid deadline")]
132    MeasurementSurfaceUnavailable {
133        /// Measurement variable.
134        mvar: String,
135    },
136    /// A feedback target has no compatible wire.
137    #[error("feedback target {target} has no wire on which to apply {pauli}")]
138    FeedbackTargetWithoutWire {
139        /// Target position.
140        target: IVec3,
141        /// Feedback Pauli.
142        pauli: bloq_utils::PauliBasis,
143    },
144    /// A measurement surface reaches a logical output.
145    #[error("measurement surface for '{name}' reaches output port {port}")]
146    MeasurementSurfaceTouchesOutputPort {
147        /// Measurement name.
148        name: String,
149        /// Touched output port.
150        port: IVec3,
151    },
152    /// A measurement surface extends beyond its causal deadline.
153    #[error(
154        "measurement surface for '{name}' has interior support at z {max_z}, past its accepted deadline {deadline}"
155    )]
156    MeasurementSurfaceExtendsPastDeadline {
157        /// Measurement name.
158        name: String,
159        /// Latest interior layer.
160        max_z: i32,
161        /// Accepted deadline.
162        deadline: i64,
163    },
164    /// Two selective fixing rows are coupled.
165    #[error(
166        "selective fixing row for site {row:?} anticommutes with the flip axis of selective site {site:?}"
167    )]
168    SelectiveFixingRowsCoupled {
169        /// Fixing-row site.
170        row: IVec3,
171        /// Coupled selective site.
172        site: IVec3,
173    },
174    /// No deterministic presentation satisfies the composed certificate.
175    #[error("composed module certificate has no admissible deterministic presentation")]
176    ComposedPresentationUnsatisfied,
177}
178
179impl StabilizerError {
180    /// Incomplete searches must not enter fallbacks for unsatisfied constraints.
181    pub(crate) fn is_interrupted(&self) -> bool {
182        matches!(self, Self::ResourceLimited { .. } | Self::Cancelled(_))
183    }
184}
185
186pub(crate) struct SearchBudget {
187    visited: usize,
188    limits: ModuleCertificationLimits,
189    cancellation: Option<crate::CancellationToken>,
190}
191
192impl SearchBudget {
193    #[cfg(test)]
194    pub(crate) fn new(limit: usize) -> Self {
195        Self::with_limits(ModuleCertificationLimits {
196            max_normalization_states: limit,
197            ..ModuleCertificationLimits::UNLIMITED
198        })
199    }
200
201    pub(crate) fn with_limits(limits: ModuleCertificationLimits) -> Self {
202        Self {
203            visited: 0,
204            limits,
205            cancellation: crate::CancellationToken::current(),
206        }
207    }
208
209    pub(crate) fn visit(&mut self, phase: &'static str) -> Result<(), StabilizerError> {
210        let next = self.visited.checked_add(1);
211        self.visited = next.unwrap_or(usize::MAX);
212        self.check_cancellation()?;
213        let limit = self.limits.max_normalization_states;
214        if next.is_none() || self.visited > limit {
215            Err(StabilizerError::ResourceLimited {
216                phase,
217                observed: self.visited,
218                limit,
219            })
220        } else {
221            Ok(())
222        }
223    }
224
225    /// Pauli strings own two arrays rounded to binar's 512-bit blocks.
226    fn pauli_words(width: usize) -> u128 {
227        width.div_ceil(512) as u128 * 16
228    }
229
230    fn check_words(&self, words: u128) -> Result<(), StabilizerError> {
231        self.check_cancellation()?;
232        let limit = self.limits.max_matrix_words;
233        if words > limit as u128 {
234            return Err(StabilizerError::ResourceLimited {
235                phase: "dense matrix words",
236                observed: usize::try_from(words).unwrap_or(usize::MAX),
237                limit,
238            });
239        }
240        Ok(())
241    }
242
243    fn check_cancellation(&self) -> Result<(), StabilizerError> {
244        if let Some(token) = &self.cancellation {
245            token.check()?;
246        }
247        Ok(())
248    }
249
250    pub(crate) fn check_matrix(
251        &self,
252        pauli_rows: usize,
253        pauli_width: usize,
254        coefficient_rows: usize,
255        coefficient_width: usize,
256    ) -> Result<(), StabilizerError> {
257        self.check_words(
258            pauli_rows as u128 * Self::pauli_words(pauli_width)
259                + coefficient_rows as u128 * coefficient_width.div_ceil(64) as u128,
260        )
261    }
262
263    fn check_affine(
264        &self,
265        rows: &[PauliString],
266        coeffs: &[CoeffVec],
267        constraints: usize,
268    ) -> Result<(), StabilizerError> {
269        let rank = rows.len();
270        let width = rows.first().map_or(0, PauliString::len);
271        let coeff_width = coeffs.iter().map(CoeffVec::len).max().unwrap_or(0);
272        // Input/normalized/output coefficient rows, plus the equations and
273        // their coefficient-space kernel, coexist during the affine solve.
274        self.check_words(
275            rank as u128 * Self::pauli_words(width)
276                + (3 * rank as u128 + 1) * coeff_width.div_ceil(64) as u128
277                + (constraints as u128 + rank as u128 + 1) * (rank as u128 + 1).div_ceil(64),
278        )
279    }
280
281    fn check_prefix_storage(
282        &self,
283        depth: usize,
284        rank: usize,
285        width: usize,
286    ) -> Result<(), StabilizerError> {
287        // Prefix recursion retains earlier basis/span snapshots. Account for
288        // their triangular growth before cloning the next prefix.
289        let rows = (depth as u128 * (depth as u128 + 1) / 2).saturating_mul(3);
290        self.check_words(rows.saturating_mul(Self::pauli_words(width) + rank.div_ceil(64) as u128))
291    }
292}
293
294/// A stabilizer of a ZX graph, with its Pauli support broken out by the graph
295/// elements (ports, interior nodes, interior edges) it acts on for rendering.
296#[derive(Debug, Clone, PartialEq)]
297pub struct Stabilizer {
298    /// The stabilizer as a dense Pauli string over the graph's id space.
299    pub paulis: PauliString,
300    /// Constant sign (`true` means `-1`).
301    pub sign: bool,
302    /// Pauli acting at each boundary port position.
303    pub port_stabilizer: FxHashMap<IVec3, Pauli>,
304    /// Pauli acting at each interior node position.
305    pub interior_nodes: FxHashMap<IVec3, Pauli>,
306    /// Pauli acting on each interior edge, keyed by its endpoint positions.
307    pub interior_edges: FxHashMap<(IVec3, IVec3), Pauli>,
308}
309
310impl Stabilizer {
311    /// Latest gate needed to read this surface: z for selective caps, z+1
312    /// for ordinary nodes. Empty surfaces have no measurement deadline.
313    ///
314    /// # Panics
315    ///
316    /// Panics if this materialized surface names a position absent from `zx`.
317    pub fn measurement_deadline(&self, zx: &ZXGraph) -> Option<i64> {
318        self.interior_nodes
319            .keys()
320            .chain(self.port_stabilizer.keys())
321            .copied()
322            .chain(
323                self.interior_edges
324                    .keys()
325                    .flat_map(|&(left, right)| [left, right]),
326            )
327            .map(|position| {
328                let node = zx
329                    .node_at(position)
330                    .expect("materialized surface names graph positions");
331                i64::from(position.z)
332                    + i64::from(!matches!(node.kind, crate::NodeKind::Selective(_)))
333            })
334            .max()
335    }
336
337    /// XORs this stabilizer with `other`, ignoring Pauli-product phase.
338    ///
339    /// Shared interior edges must use the same endpoint order.
340    pub fn phase_free_product(&self, other: &Stabilizer) -> Stabilizer {
341        let mut product = self.clone();
342        product.phase_free_mul_assign(other);
343        product
344    }
345
346    /// XORs `other` into this stabilizer, ignoring Pauli-product phase.
347    ///
348    /// Shared interior edges must use the same endpoint order.
349    pub fn phase_free_mul_assign(&mut self, other: &Stabilizer) {
350        self.paulis ^= &other.paulis;
351        xor_pauli_maps(&mut self.interior_nodes, &other.interior_nodes);
352        xor_pauli_maps(&mut self.port_stabilizer, &other.port_stabilizer);
353        let mut sign = self.sign ^ other.sign ^ yy_edge_sign(&other.interior_edges);
354        for (&edge, &pauli) in &other.interior_edges {
355            let entry = self.interior_edges.entry(edge).or_insert(Pauli::I);
356            let was_y = *entry == Pauli::Y;
357            *entry = *entry ^ pauli;
358            sign ^= was_y ^ (*entry == Pauli::Y);
359            if *entry == Pauli::I {
360                self.interior_edges.remove(&edge);
361            }
362        }
363        self.sign = sign;
364    }
365
366    /// Whether feedback anticommutes with this row at an odd number of targets.
367    ///
368    /// # Panics
369    ///
370    /// Panics if wire feedback has no `zx`, or validated feedback metadata is inconsistent.
371    pub fn odd_anticommutes_feedback(
372        &self,
373        targets: &[FeedbackTarget],
374        zx: Option<&ZXGraph>,
375    ) -> bool {
376        targets
377            .iter()
378            .filter(|target| {
379                if let Some(zx) = zx {
380                    let (column, pauli) =
381                        zx.feedback_column(target).expect("validated feedback wire");
382                    return self.paulis.get(column).anticommutes(pauli);
383                }
384                assert!(
385                    target.direction.is_none(),
386                    "wire feedback requires its graph"
387                );
388                self.interior_nodes
389                    .get(&target.target)
390                    .or_else(|| self.port_stabilizer.get(&target.target))
391                    .is_some_and(|support| support.anticommutes(Pauli::from(target.pauli)))
392            })
393            .count()
394            % 2
395            == 1
396    }
397
398    /// Test fixture with only interior-node support.
399    #[doc(hidden)]
400    pub fn from_interior_nodes(interior_nodes: impl IntoIterator<Item = (IVec3, Pauli)>) -> Self {
401        Stabilizer {
402            paulis: PauliString::new(0),
403            sign: false,
404            port_stabilizer: FxHashMap::default(),
405            interior_nodes: interior_nodes.into_iter().collect(),
406            interior_edges: FxHashMap::default(),
407        }
408    }
409
410    /// Adds interior-edge support to a test fixture.
411    #[doc(hidden)]
412    pub fn with_interior_edges(
413        mut self,
414        interior_edges: impl IntoIterator<Item = ((IVec3, IVec3), Pauli)>,
415    ) -> Self {
416        let raw_negative = self.sign ^ yy_edge_sign(&self.interior_edges);
417        self.interior_edges = interior_edges.into_iter().collect();
418        self.sign = raw_negative ^ yy_edge_sign(&self.interior_edges);
419        self
420    }
421}
422
423pub(crate) fn xor_pauli_maps<K: Copy + Eq + std::hash::Hash>(
424    left: &mut FxHashMap<K, Pauli>,
425    right: &FxHashMap<K, Pauli>,
426) {
427    for (&key, &pauli) in right {
428        let entry = left.entry(key).or_insert(Pauli::I);
429        *entry = *entry ^ pauli;
430        if *entry == Pauli::I {
431            left.remove(&key);
432        }
433    }
434}
435
436fn yy_edge_sign(edges: &FxHashMap<(IVec3, IVec3), Pauli>) -> bool {
437    edges.values().filter(|&&pauli| pauli == Pauli::Y).count() % 2 == 1
438}
439
440pub(crate) fn contracted_sign(row_phase: u8, edges: &FxHashMap<(IVec3, IVec3), Pauli>) -> bool {
441    debug_assert_eq!(row_phase % 2, 0, "stabilizer rows are Hermitian");
442    (row_phase == 2) ^ yy_edge_sign(edges)
443}
444
445/// A selective node and the Pauli basis it is forbidden from taking, used to
446/// constrain how selective supports are normalized.
447#[derive(Debug, Clone, PartialEq, Eq)]
448pub struct SelectiveFixingTarget {
449    /// Position of the selective node.
450    pub pos: IVec3,
451    /// Pauli the node may not be fixed to.
452    pub forbidden: Pauli,
453}
454
455/// The canonical fixing-row binding for one selective site.
456#[derive(Debug, Clone, Copy)]
457pub struct SelectiveFixing<'a> {
458    /// Stabilizer row fixing the site.
459    pub row: &'a Stabilizer,
460    /// The site's branch-flip axis, pinned by `row`.
461    pub forbidden: Pauli,
462}
463
464/// Selective site to its fixing-row binding.
465pub type SelectiveFixings<'a> = FxHashMap<IVec3, SelectiveFixing<'a>>;
466
467/// Builds the canonical selective-site fixing map from generator row kinds.
468pub fn selective_fixings(generators: &[StabilizerGenerator]) -> SelectiveFixings<'_> {
469    let mut fixings = FxHashMap::default();
470    for generator in generators {
471        for target in generator.kind.selective_fixing_targets() {
472            fixings.insert(
473                target.pos,
474                SelectiveFixing {
475                    row: &generator.stabilizer,
476                    forbidden: target.forbidden,
477                },
478            );
479        }
480    }
481    fixings
482}
483
484/// The role a stabilizer generator row plays in the basis.
485#[derive(Debug, Clone, PartialEq, Eq)]
486pub enum StabilizerRowKind {
487    /// Witnesses a named measurement variable.
488    Measurement {
489        /// Measurement name.
490        name: String,
491    },
492    /// Fixes the basis of one or more selective nodes.
493    SelectiveFixing {
494        /// Selective sites fixed by the row.
495        targets: Vec<SelectiveFixingTarget>,
496    },
497    /// A plain logical generator.
498    Logical,
499}
500
501impl StabilizerRowKind {
502    /// Returns the selective-fixing targets, or an empty slice for other kinds.
503    pub fn selective_fixing_targets(&self) -> &[SelectiveFixingTarget] {
504        match self {
505            StabilizerRowKind::SelectiveFixing { targets } => targets,
506            _ => &[],
507        }
508    }
509
510    pub(crate) fn fixes_selective(&self, pos: IVec3) -> bool {
511        self.selective_fixing_targets()
512            .iter()
513            .any(|target| target.pos == pos)
514    }
515
516    pub(crate) fn is_selective_fixing(&self) -> bool {
517        matches!(self, StabilizerRowKind::SelectiveFixing { .. })
518    }
519
520    /// Whether this row carries a readable measurement or logical value.
521    pub fn is_readout(&self) -> bool {
522        matches!(
523            self,
524            StabilizerRowKind::Measurement { .. } | StabilizerRowKind::Logical
525        )
526    }
527}
528
529/// A stabilizer generator paired with the [`StabilizerRowKind`] it plays.
530///
531/// `PartialEq` but not `Eq`, following [`Stabilizer`]: comparing rows is how a
532/// caller checks that two derivations produced the same basis.
533#[derive(Debug, Clone, PartialEq)]
534pub struct StabilizerGenerator {
535    /// The generator's stabilizer.
536    pub stabilizer: Stabilizer,
537    /// The role this generator plays in the basis.
538    pub kind: StabilizerRowKind,
539    readout_plan: Option<std::sync::Arc<super::ReadoutPlan>>,
540}
541
542impl StabilizerGenerator {
543    /// Pairs a stabilizer with its row kind.
544    pub fn new(stabilizer: Stabilizer, kind: StabilizerRowKind) -> Self {
545        Self {
546            stabilizer,
547            kind,
548            readout_plan: None,
549        }
550    }
551
552    /// Frozen branch recipes used instead of this row for physical named readout.
553    pub fn readout_plan(&self) -> Option<&super::ReadoutPlan> {
554        self.readout_plan.as_deref()
555    }
556
557    /// Authoritative physical recipe for a named outcome.
558    pub fn named_readout(&self) -> Option<super::NamedReadout<'_>> {
559        self.measurement_name()?;
560        Some(match self.readout_plan() {
561            Some(recipe) => super::NamedReadout::Guarded(recipe),
562            None => super::NamedReadout::Fixed(&self.stabilizer),
563        })
564    }
565
566    /// Whether the recipe's ordinals name earlier corrected source outcomes.
567    pub fn uses_source_outcomes(&self) -> bool {
568        self.readout_plan()
569            .is_some_and(|recipe| recipe.coordinates == super::ReadoutCoordinates::SourceOutcomes)
570    }
571
572    /// Whether this row witnesses a measurement variable.
573    pub fn is_measurement(&self) -> bool {
574        matches!(self.kind, StabilizerRowKind::Measurement { .. })
575    }
576
577    /// The witnessed measurement variable name, if this is a measurement row.
578    pub fn measurement_name(&self) -> Option<&str> {
579        match &self.kind {
580            StabilizerRowKind::Measurement { name } => Some(name),
581            _ => None,
582        }
583    }
584}
585
586/// Stabilizer state for a ZX graph: a stable public presentation plus private
587/// algebraic rank completion.
588#[derive(Debug, Clone)]
589pub struct StabilizerGenerators {
590    /// The ZX graph the generators were derived from.
591    pub zx_graph: ZXGraph,
592    /// Stable public readable/fixing rows. This presentation need not span the
593    /// full output projection by itself.
594    pub generators: Vec<StabilizerGenerator>,
595    /// Private rank-completion directions. They are not readable identities,
596    /// but remain part of full-basis output and feedback semantics.
597    auxiliary_rows: Vec<PauliString>,
598}
599
600impl StabilizerGenerators {
601    /// Builds a public generator set with no internal rank-completion rows.
602    pub fn new(zx_graph: ZXGraph, generators: Vec<StabilizerGenerator>) -> Self {
603        Self {
604            zx_graph,
605            generators,
606            auxiliary_rows: Vec::new(),
607        }
608    }
609
610    pub(crate) fn from_parts(
611        zx_graph: ZXGraph,
612        generators: Vec<StabilizerGenerator>,
613        auxiliary_rows: Vec<PauliString>,
614    ) -> Self {
615        Self {
616            zx_graph,
617            generators,
618            auxiliary_rows,
619        }
620    }
621
622    pub(crate) fn auxiliary_rows(&self) -> &[PauliString] {
623        &self.auxiliary_rows
624    }
625
626    pub(crate) fn plan_readouts(
627        &mut self,
628        limits: ModuleCertificationLimits,
629    ) -> Result<(), RuntimeBasisError> {
630        let readouts = super::readout::plan_readouts(self, limits)?;
631        self.install_readouts(readouts);
632        Ok(())
633    }
634
635    /// Freeze physical named-readout choices after action analysis and C0
636    /// certification. The DAG must belong to this generator set's graph.
637    /// Returns whether new recipes require rebuilding the action dependencies.
638    ///
639    /// # Errors
640    ///
641    /// Returns an error if a causal readout is unavailable or exceeds `limits`.
642    pub fn prepare_readouts(
643        &mut self,
644        dag: &crate::ActionDag,
645        limits: ModuleCertificationLimits,
646    ) -> Result<bool, RuntimeBasisError> {
647        let readouts = super::readout::plan_causal_readouts(
648            self,
649            dag,
650            limits,
651            super::readout::ReadoutClosure::Required,
652        )?;
653        Ok(self.install_readouts(readouts))
654    }
655
656    #[cfg(feature = "verify")]
657    pub(crate) fn prepare_analysis_readouts(
658        &mut self,
659        dag: &crate::ActionDag,
660    ) -> Result<(), RuntimeBasisError> {
661        let readouts = super::readout::plan_causal_readouts(
662            self,
663            dag,
664            ModuleCertificationLimits::DEFAULT,
665            super::readout::ReadoutClosure::Analysis,
666        )?;
667        self.install_readouts(readouts);
668        Ok(())
669    }
670
671    fn install_readouts(&mut self, mut readouts: BTreeMap<String, super::ReadoutPlan>) -> bool {
672        let changed = !readouts.is_empty();
673        for generator in &mut self.generators {
674            if let Some(recipe) = generator
675                .measurement_name()
676                .and_then(|name| readouts.remove(name))
677            {
678                generator.readout_plan = Some(std::sync::Arc::new(recipe));
679            }
680        }
681        changed
682    }
683
684    /// Total basis rows, including private rank completion.
685    pub(crate) fn basis_rank(&self) -> usize {
686        self.generators.len() + self.auxiliary_rows.len()
687    }
688
689    /// Returns the number of public generator rows.
690    pub fn len(&self) -> usize {
691        self.generators.len()
692    }
693
694    /// Returns whether there are no public generator rows.
695    pub fn is_empty(&self) -> bool {
696        self.generators.is_empty()
697    }
698
699    /// Checks that fixing rows do not couple independent selective sites.
700    ///
701    /// # Errors
702    ///
703    /// Returns [`StabilizerError::SelectiveFixingRowsCoupled`] for coupled sites.
704    pub fn validate_selective_decoupling(&self) -> Result<(), StabilizerError> {
705        let fixings = selective_fixings(&self.generators);
706        let mut sites = fixings.keys().copied().collect::<Vec<_>>();
707        sites.sort_unstable_by_key(|pos| (pos.x, pos.y, pos.z));
708        for &site in &sites {
709            let flip = fixings[&site].forbidden;
710            for &row in &sites {
711                if row == site {
712                    continue;
713                }
714                let support = fixings[&row]
715                    .row
716                    .interior_nodes
717                    .get(&site)
718                    .copied()
719                    .unwrap_or(Pauli::I);
720                if support.anticommutes(flip) {
721                    return Err(StabilizerError::SelectiveFixingRowsCoupled { row, site });
722                }
723            }
724        }
725        Ok(())
726    }
727
728    /// Requires an output-free representative for every measurement in every
729    /// reachable selective branch. Pure ZX analysis may retain output frames.
730    ///
731    /// # Errors
732    ///
733    /// Returns an unavailable-surface, output-support, or resource-limit error.
734    pub fn validate_measurements_close_before_outputs(&self) -> Result<(), RuntimeBasisError> {
735        self.validate_measurements_close_before_outputs_with_limits(
736            ModuleCertificationLimits::DEFAULT,
737        )
738    }
739
740    #[cfg(any(test, feature = "verify"))]
741    pub(crate) fn validate_measurements_close_before_outputs_with_limit(
742        &self,
743        max_guarded_domain_size: usize,
744    ) -> Result<(), RuntimeBasisError> {
745        self.validate_measurements_close_before_outputs_with_limits(ModuleCertificationLimits {
746            max_guarded_domain_size,
747            ..ModuleCertificationLimits::DEFAULT
748        })
749    }
750
751    /// Certifies C0 with explicit selective-domain, Boolean, and matrix limits.
752    ///
753    /// # Errors
754    ///
755    /// Returns an unavailable surface, an output-supported measurement, or a
756    /// resource-limit error if the proof cannot finish within these limits.
757    ///
758    /// # Panics
759    ///
760    /// Panics if internally stored generator rows and roles have different lengths.
761    pub fn validate_measurements_close_before_outputs_with_limits(
762        &self,
763        limits: ModuleCertificationLimits,
764    ) -> Result<(), RuntimeBasisError> {
765        if self
766            .generators
767            .iter()
768            .any(StabilizerGenerator::uses_source_outcomes)
769        {
770            for generator in &self.generators {
771                let Some(recipe) = generator.named_readout() else {
772                    continue;
773                };
774                let name = generator.measurement_name().expect("named readout");
775                for surface in recipe.surfaces() {
776                    for node in self
777                        .zx_graph
778                        .nodes()
779                        .iter()
780                        .filter(|node| node.is_output_port(&self.zx_graph))
781                    {
782                        let support = surface.paulis.get(node.id);
783                        if support != Pauli::I
784                            && !(node.role == crate::PortRole::Multiplex && support == Pauli::Z)
785                        {
786                            return Err(StabilizerError::MeasurementSurfaceTouchesOutputPort {
787                                name: name.to_owned(),
788                                port: node.pos,
789                            }
790                            .into());
791                        }
792                    }
793                }
794            }
795            return Ok(());
796        }
797        SearchBudget::with_limits(limits).check_matrix(
798            self.generators.len().saturating_mul(2),
799            self.zx_graph.total_ids(),
800            0,
801            0,
802        )?;
803        if let Ok(basis) = SymbolicStabilizerBasis::from_generators(self)
804            && basis.measurements_close_before_outputs(&self.zx_graph, limits.boolean_limits())?
805        {
806            return Ok(());
807        }
808
809        let sites = collect_selective_constraints(&self.zx_graph);
810        let targets = sites.iter().map(|site| site.pos).collect::<Vec<_>>();
811        let outputs = self.zx_graph.output_ports();
812        let rank = self.basis_rank();
813        SearchBudget::with_limits(limits).check_matrix(
814            rank.saturating_mul(3),
815            self.zx_graph.total_ids(),
816            rank,
817            self.generators.len(),
818        )?;
819        let basis = RuntimeStabilizerBasis::from_generators(self).with_t_nodes_as_ports();
820        let readout_width = basis.generators().len();
821
822        let domain = self
823            .zx_graph
824            .action_graph()
825            .resolve_value_domain_bounded_with_limits(
826                &targets,
827                limits.max_guarded_domain_size,
828                limits.boolean_limits(),
829            )
830            .map_err(|error| {
831                error.into_stabilizer("guarded-domain branches", limits.max_guarded_domain_size)
832            })?;
833        for values in domain.values() {
834            let fills = sites
835                .iter()
836                .zip(values)
837                .map(|(site, &value)| {
838                    let basis = if value {
839                        site.kind.pauli_if_true()
840                    } else {
841                        site.kind.pauli_if_false()
842                    };
843                    (site.pos, basis)
844                })
845                .collect::<Vec<_>>();
846            let branch = basis.apply_selective_fills(&fills)?;
847            for (ordinal, generator) in self.generators.iter().enumerate() {
848                let Some(name) = generator.measurement_name() else {
849                    continue;
850                };
851                if let Some(port) =
852                    branch.output_free_readout_failure(name, ordinal, readout_width, &outputs)?
853                {
854                    return Err(StabilizerError::MeasurementSurfaceTouchesOutputPort {
855                        name: name.into(),
856                        port,
857                    }
858                    .into());
859                }
860            }
861        }
862        Ok(())
863    }
864}
865
866fn stabilizer_external_string(stabilizer: &Stabilizer, ordered_ports: &[IVec3]) -> String {
867    ordered_ports
868        .iter()
869        .map(|pos| {
870            stabilizer
871                .interior_nodes
872                .get(pos)
873                .copied()
874                .unwrap_or(Pauli::I)
875        })
876        .map(|pauli| match pauli {
877            Pauli::I => 'I',
878            Pauli::X => 'X',
879            Pauli::Y => 'Y',
880            Pauli::Z => 'Z',
881        })
882        .collect()
883}
884
885pub(crate) fn ordered_port_positions(graph: &BlockGraph) -> Vec<IVec3> {
886    let mut ports: Vec<&Block> = graph
887        .blocks()
888        .filter(|block| {
889            matches!(
890                block.kind,
891                BlockKind::Port | BlockKind::T | BlockKind::Selective(_)
892            )
893        })
894        .collect();
895    ports.sort();
896    ports.into_iter().map(|block| block.pos).collect()
897}
898
899pub(crate) fn fill_ports_auto(
900    graph: &BlockGraph,
901    limits: ModuleCertificationLimits,
902) -> Result<Vec<(BlockGraph, Vec<StabilizerGenerator>)>, BlockGraphError> {
903    let ordered_ports = ordered_port_positions(graph);
904    if ordered_ports.is_empty() {
905        return Err(BlockGraphError::from(FillPortsError::NoOpenPorts));
906    }
907
908    let mut generator_map = HashMap::<String, StabilizerGenerator>::new();
909    let mut generator_order = Vec::new();
910    let mut measurement_support = HashMap::new();
911    for generator in graph.stabilizers_with_limits(limits)?.generators {
912        let external = stabilizer_external_string(&generator.stabilizer, &ordered_ports);
913        if let Some(name) = generator.measurement_name() {
914            measurement_support.insert(name.to_owned(), external.clone());
915        }
916        if let Entry::Vacant(slot) = generator_map.entry(external.clone()) {
917            generator_order.push(external);
918            slot.insert(generator);
919        }
920    }
921
922    let mut filled_graphs = Vec::new();
923    for stabilizer_keys in compatible_stabilizer_cliques(&generator_order) {
924        let filled_graph = fill_ports_for_stabilizer_clique(
925            graph,
926            &ordered_ports,
927            &stabilizer_keys,
928            &measurement_support,
929            limits,
930        )?;
931        let stabilizers = stabilizer_keys
932            .into_iter()
933            .map(|key| generator_map[&key].clone())
934            .collect();
935        filled_graphs.push((filled_graph, stabilizers));
936    }
937    Ok(filled_graphs)
938}
939
940fn compatible_stabilizer_cliques(generators: &[String]) -> Vec<Vec<String>> {
941    let len = generators.len();
942    let mut conflicts = vec![vec![false; len]; len];
943    for i in 0..len {
944        for j in (i + 1)..len {
945            let conflict = !are_compatible_stabilizers(&generators[i], &generators[j]);
946            conflicts[i][j] = conflict;
947            conflicts[j][i] = conflict;
948        }
949    }
950
951    let mut order: Vec<usize> = (0..len).collect();
952    order.sort_by(|&lhs, &rhs| {
953        let lhs_degree = conflicts[lhs].iter().filter(|&&conflict| conflict).count();
954        let rhs_degree = conflicts[rhs].iter().filter(|&&conflict| conflict).count();
955        rhs_degree
956            .cmp(&lhs_degree)
957            .then_with(|| generators[lhs].cmp(&generators[rhs]))
958    });
959
960    let mut colors = vec![usize::MAX; len];
961    for node in order {
962        let mut color = 0;
963        loop {
964            let blocked =
965                (0..len).any(|neighbor| conflicts[node][neighbor] && colors[neighbor] == color);
966            if !blocked {
967                colors[node] = color;
968                break;
969            }
970            color += 1;
971        }
972    }
973
974    let mut cliques: BTreeMap<usize, Vec<String>> = BTreeMap::new();
975    for (node, &color) in colors.iter().enumerate() {
976        cliques
977            .entry(color)
978            .or_default()
979            .push(generators[node].clone());
980    }
981    cliques.into_values().collect()
982}
983
984fn are_compatible_stabilizers(lhs: &str, rhs: &str) -> bool {
985    debug_assert_eq!(lhs.len(), rhs.len());
986    lhs.bytes()
987        .zip(rhs.bytes())
988        .all(|(l, r)| l == b'I' || r == b'I' || l == r)
989}
990
991fn fill_ports_for_stabilizer_clique(
992    graph: &BlockGraph,
993    ordered_ports: &[IVec3],
994    stabilizers: &[String],
995    measurement_support: &HashMap<String, String>,
996    limits: ModuleCertificationLimits,
997) -> Result<BlockGraph, BlockGraphError> {
998    let mut port_ops = vec![Pauli::I; ordered_ports.len()];
999    for stabilizer in stabilizers {
1000        overlay_port_ops(&mut port_ops, stabilizer)?;
1001    }
1002
1003    let mut filled = graph.clone();
1004    for (port_pos, pauli) in ordered_ports.iter().copied().zip(port_ops) {
1005        let fill_kind = infer_port_fill_kind(
1006            &filled,
1007            port_pos,
1008            if pauli == Pauli::I { Pauli::Z } else { pauli },
1009        )?;
1010        let block = filled
1011            .get_block_mut(port_pos)
1012            .expect("ordered port positions always exist in the graph");
1013        block.kind = fill_kind;
1014    }
1015    let actions = filled.actions();
1016    let mut removed = actions
1017        .iter()
1018        .map(|action| match action {
1019            Action::Measure { name, .. } => !stabilizers.contains(&measurement_support[name]),
1020            _ => false,
1021        })
1022        .collect::<Vec<_>>();
1023    let dag = crate::ActionDag::from_actions(&actions);
1024    let mut dependents = vec![Vec::new(); actions.len()];
1025    for (from, to, dependency) in dag.dependencies() {
1026        if dependency == crate::ActionDependency::Classical {
1027            dependents[from].push(to);
1028        }
1029    }
1030    let mut pending = removed
1031        .iter()
1032        .enumerate()
1033        .filter_map(|(index, &remove)| remove.then_some(index))
1034        .collect::<Vec<_>>();
1035    while let Some(predecessor) = pending.pop() {
1036        for &consumer in &dependents[predecessor] {
1037            if !removed[consumer] {
1038                removed[consumer] = true;
1039                pending.push(consumer);
1040            }
1041        }
1042    }
1043    for (ordinal, action) in actions.iter().enumerate() {
1044        if removed[ordinal] && matches!(action, Action::DiscardIf(_) | Action::Branch { .. }) {
1045            return Err(FillPortsError::UnavailableControl { ordinal }.into());
1046        }
1047    }
1048    let actions = actions
1049        .into_iter()
1050        .enumerate()
1051        .filter_map(|(ordinal, action)| {
1052            (!removed[ordinal]
1053                && match &action {
1054                    Action::Feedback { .. } => false,
1055                    Action::Resolve { target, .. } => !ordered_ports.contains(target),
1056                    _ => true,
1057                })
1058            .then_some(action)
1059        })
1060        .collect();
1061    filled.set_actions_with_limits(actions, limits)?;
1062    crate::validate::validate_with_limits(&filled, limits)?;
1063    Ok(filled)
1064}
1065
1066fn overlay_port_ops(port_ops: &mut [Pauli], stabilizer: &str) -> Result<(), FillPortsError> {
1067    debug_assert_eq!(port_ops.len(), stabilizer.len());
1068    for (slot, ch) in port_ops.iter_mut().zip(stabilizer.chars()) {
1069        let pauli = pauli_from_char(ch)?;
1070        if pauli != Pauli::I {
1071            *slot = pauli;
1072        }
1073    }
1074    Ok(())
1075}
1076
1077fn infer_port_fill_kind(
1078    graph: &BlockGraph,
1079    port_pos: IVec3,
1080    supported_basis: Pauli,
1081) -> Result<BlockKind, FillPortsError> {
1082    match supported_basis {
1083        Pauli::Y => {
1084            let pipes = pipes_at(graph, port_pos);
1085            if let Some(pipe) = pipes.first()
1086                && pipe_direction_at(pipe, port_pos)?.as_udirection() != UDirection::Z
1087            {
1088                return Err(FillPortsError::UnsupportedPortBasis {
1089                    pos: port_pos,
1090                    basis: supported_basis,
1091                });
1092            }
1093            Ok(BlockKind::Y)
1094        }
1095        Pauli::X | Pauli::Z => {
1096            let basis = Basis::try_from(supported_basis).expect("X/Z always convert to Basis");
1097            let pipes = pipes_at(graph, port_pos);
1098            if pipes.len() != 1 {
1099                return Err(FillPortsError::PortFillRequiresSinglePipe {
1100                    pos: port_pos,
1101                    degree: pipes.len(),
1102                });
1103            }
1104            let pipe = pipes[0];
1105            let bases = graph.infer_pipe_basis_from_endpoint(pipe, port_pos);
1106            let cube_bases = bases.map(|entry| entry.unwrap_or(basis));
1107            let cube_kind = CubeKind::try_from(cube_bases).map_err(|e| {
1108                FillPortsError::InvalidFilledPortCube {
1109                    pos: port_pos,
1110                    source: e,
1111                }
1112            })?;
1113            Ok(BlockKind::Cube(cube_kind))
1114        }
1115        _ => Err(FillPortsError::UnsupportedPortBasis {
1116            pos: port_pos,
1117            basis: supported_basis,
1118        }),
1119    }
1120}
1121
1122fn pauli_from_char(ch: char) -> Result<Pauli, FillPortsError> {
1123    match ch {
1124        'I' => Ok(Pauli::I),
1125        'X' => Ok(Pauli::X),
1126        'Y' => Ok(Pauli::Y),
1127        'Z' => Ok(Pauli::Z),
1128        _ => Err(FillPortsError::InvalidExternalStabilizerChar { ch }),
1129    }
1130}
1131
1132fn pipes_at(graph: &BlockGraph, pos: IVec3) -> Vec<&Pipe> {
1133    graph
1134        .block_pairs_with_pipe()
1135        .filter_map(|(u, v, pipe)| ((u.pos == pos) || (v.pos == pos)).then_some(pipe))
1136        .collect()
1137}
1138
1139fn pipe_direction_at(pipe: &Pipe, pos: IVec3) -> Result<Direction, FillPortsError> {
1140    if pipe.src == pos {
1141        Ok(pipe.dir)
1142    } else if pipe.dst() == pos {
1143        Ok(pipe.dir.negate())
1144    } else {
1145        Err(FillPortsError::PortFillRequiresSinglePipe { pos, degree: 0 })
1146    }
1147}
1148
1149/// Fixtures shared by the stabilizer submodules' test modules and by
1150/// `zx::modular`, which each exercise a different slice of the same graphs and
1151/// coefficient vectors.
1152#[cfg(test)]
1153pub(super) mod test_support {
1154    use super::super::stabilizer::basis::CoeffVec;
1155    use bloq_utils::PauliString;
1156
1157    /// A T injection feeding an `XY` selective block resolved by a joint `X`
1158    /// measurement — the smallest graph with both a T site and a selective one.
1159    pub(crate) fn build_t_selective_graph() -> crate::BlockGraph {
1160        crate::BlockGraph::from_blog_text(
1161            "BLOG 1.0\n\n\
1162             0: Port [0, 0, 0]\n\
1163             1: XZX [0, 0, 1]\n\
1164             2: Port [0, 0, 2]\n\
1165             3: T [1, 0, 0]\n\
1166             4: XZX [1, 0, 1]\n\
1167             5: XY [1, 0, 2]\n\
1168             [0, 0, 0] -> +Z\n\
1169             [0, 0, 1] -> +Z\n\
1170             [0, 0, 1] -> +X\n\
1171             [1, 0, 0] -> +Z\n\
1172             [1, 0, 2] -> -Z\n\n\
1173             mzz = measure 1 -> +X\n\
1174             resolve 5 if mzz\n",
1175        )
1176        .expect("canonical T-selective graph must parse")
1177    }
1178
1179    pub(crate) fn sparse_to_coeff(combination: &[usize], width: usize) -> CoeffVec {
1180        let mut coeff = CoeffVec::zeros(width);
1181        for &index in combination {
1182            coeff.set_bit(index, true);
1183        }
1184        coeff
1185    }
1186
1187    /// Replays a coefficient vector against the raw external table, giving the
1188    /// row a tracked solution *should* materialize to.
1189    pub(crate) fn algebraic_row_from_coeff(
1190        coeff: &CoeffVec,
1191        raw_external: &[PauliString],
1192        width: usize,
1193    ) -> PauliString {
1194        let mut row = PauliString::new(width);
1195        for index in coeff.to_indices() {
1196            row ^= &raw_external[index];
1197        }
1198        row
1199    }
1200
1201    pub(crate) fn algebraic_rows_from_coeffs(
1202        coeffs: &[CoeffVec],
1203        raw_external: &[PauliString],
1204        width: usize,
1205    ) -> Vec<PauliString> {
1206        coeffs
1207            .iter()
1208            .map(|coeff| algebraic_row_from_coeff(coeff, raw_external, width))
1209            .collect()
1210    }
1211}
1212
1213#[cfg(test)]
1214mod tests {
1215    #[test]
1216    fn cancellation_preserves_spent_normalization_budget_and_restores_the_scope() {
1217        for _ in 0..2 {
1218            let token = crate::CancellationToken::new();
1219            token.scope(|| {
1220                let mut budget = SearchBudget::new(1);
1221                budget.visit("normalization").unwrap();
1222                token.cancel();
1223                assert!(matches!(
1224                    budget.visit("normalization"),
1225                    Err(StabilizerError::Cancelled(_))
1226                ));
1227                assert_eq!(budget.visited, 2);
1228                assert!(matches!(
1229                    budget.check_matrix(1, 1, 0, 0),
1230                    Err(StabilizerError::Cancelled(_))
1231                ));
1232            });
1233        }
1234        SearchBudget::new(1).visit("normalization").unwrap();
1235    }
1236
1237    #[test]
1238    fn active_normalization_worker_observes_cancellation_from_another_thread() {
1239        let token = crate::CancellationToken::new();
1240        let (started, ready) = std::sync::mpsc::channel();
1241        let (resume, resumed) = std::sync::mpsc::channel();
1242        std::thread::scope(|threads| {
1243            let worker_token = token.clone();
1244            let worker = threads.spawn(move || {
1245                worker_token.run(|| {
1246                    let mut budget = SearchBudget::new(10);
1247                    budget.visit("normalization").unwrap();
1248                    started.send(()).unwrap();
1249                    resumed.recv().unwrap();
1250                    budget.visit("normalization")
1251                })
1252            });
1253            ready.recv().unwrap();
1254            token.cancel();
1255            resume.send(()).unwrap();
1256            assert!(matches!(
1257                worker.join().unwrap(),
1258                Err(StabilizerError::Cancelled(_))
1259            ));
1260        });
1261        SearchBudget::new(1).visit("normalization").unwrap();
1262    }
1263
1264    #[test]
1265    fn normalization_counter_overflow_is_a_resource_failure() {
1266        let mut budget = SearchBudget::with_limits(ModuleCertificationLimits::UNLIMITED);
1267        budget.visited = usize::MAX - 1;
1268        budget
1269            .visit("normalization")
1270            .expect("last representable state is allowed");
1271        assert!(matches!(
1272            budget.visit("normalization"),
1273            Err(StabilizerError::ResourceLimited {
1274                phase: "normalization",
1275                observed: usize::MAX,
1276                limit: usize::MAX,
1277            })
1278        ));
1279        assert_eq!(budget.visited, usize::MAX);
1280    }
1281
1282    use glam::ivec3;
1283    use std::collections::HashSet;
1284    use std::sync::OnceLock;
1285
1286    use crate::zx::graph::CsrAdjacency;
1287    use crate::zx::{NodeKind, ZXNode};
1288    use crate::{ActionDag, Expr, GalleryItem, PauliBasis, PortRole};
1289
1290    use super::*;
1291
1292    #[test]
1293    fn selective_decoupling_rejects_foreign_anticommutation() {
1294        let a = ivec3(0, 0, 0);
1295        let b = ivec3(1, 0, 0);
1296        let rows = vec![
1297            StabilizerGenerator::new(
1298                Stabilizer::from_interior_nodes([(a, Pauli::Y)]),
1299                StabilizerRowKind::SelectiveFixing {
1300                    targets: vec![SelectiveFixingTarget {
1301                        pos: a,
1302                        forbidden: Pauli::Y,
1303                    }],
1304                },
1305            ),
1306            StabilizerGenerator::new(
1307                Stabilizer::from_interior_nodes([(b, Pauli::Y), (a, Pauli::X)]),
1308                StabilizerRowKind::SelectiveFixing {
1309                    targets: vec![SelectiveFixingTarget {
1310                        pos: b,
1311                        forbidden: Pauli::Y,
1312                    }],
1313                },
1314            ),
1315        ];
1316        let zx = GalleryItem::CNOT
1317            .build()
1318            .materialize_root_graph()
1319            .expect("gallery flat projection")
1320            .to_zx_graph()
1321            .unwrap();
1322        let stabilizers = StabilizerGenerators::new(zx, rows);
1323
1324        assert!(matches!(
1325            stabilizers.validate_selective_decoupling(),
1326            Err(StabilizerError::SelectiveFixingRowsCoupled { row, site })
1327                if row == b && site == a
1328        ));
1329    }
1330
1331    #[test]
1332    fn measurement_must_close_in_every_selective_branch() {
1333        let site = ivec3(0, 0, 0);
1334        let output = ivec3(1, 0, 0);
1335        let zx_graph = ZXGraph {
1336            nodes: vec![
1337                ZXNode::new(0, site, NodeKind::Selective(SelectiveKind::XZ)),
1338                ZXNode::new(1, output, NodeKind::Port).with_role(PortRole::Output),
1339            ],
1340            edges: Vec::new(),
1341            adjacency: CsrAdjacency::from_edges(2, &[]),
1342            pos_to_node: FxHashMap::from_iter([(site, 0), (output, 1)]),
1343            action_graph: ActionDag::from_actions(&[Action::Resolve {
1344                target: site,
1345                condition: Expr::Var("m".into()),
1346            }]),
1347            total_ids: 2,
1348            cross_incident: OnceLock::new(),
1349            stabilizer_phase_basis: OnceLock::new(),
1350        };
1351        let measurement = zx_graph.pauli_string_to_stabilizer(PauliString::try_from("Z_").unwrap());
1352        let fixing = zx_graph.pauli_string_to_stabilizer(PauliString::try_from("YX").unwrap());
1353        let generators = vec![
1354            StabilizerGenerator::new(
1355                measurement,
1356                StabilizerRowKind::Measurement { name: "m".into() },
1357            ),
1358            StabilizerGenerator::new(
1359                fixing,
1360                StabilizerRowKind::SelectiveFixing {
1361                    targets: vec![SelectiveFixingTarget {
1362                        pos: site,
1363                        forbidden: Pauli::Y,
1364                    }],
1365                },
1366            ),
1367        ];
1368        let stabilizers = StabilizerGenerators::new(zx_graph.clone(), generators.clone());
1369
1370        assert!(matches!(
1371            stabilizers.validate_measurements_close_before_outputs_with_limit(1),
1372            Err(RuntimeBasisError::Stabilizer(
1373                StabilizerError::ResourceLimited {
1374                    phase: "guarded-domain branches",
1375                    observed: 2,
1376                    limit: 1,
1377                }
1378            ))
1379        ));
1380
1381        assert!(matches!(
1382            stabilizers.validate_measurements_close_before_outputs(),
1383            Err(RuntimeBasisError::Stabilizer(
1384                StabilizerError::MeasurementSurfaceTouchesOutputPort { ref name, port }
1385            )) if name == "m" && port == output
1386        ));
1387
1388        let with_auxiliary = StabilizerGenerators::from_parts(
1389            zx_graph,
1390            generators,
1391            vec![PauliString::try_from("_X").unwrap()],
1392        );
1393        assert!(matches!(
1394            with_auxiliary.validate_measurements_close_before_outputs_with_limits(
1395                ModuleCertificationLimits {
1396                    max_matrix_words: 100,
1397                    ..ModuleCertificationLimits::DEFAULT
1398                }
1399            ),
1400            Err(RuntimeBasisError::Stabilizer(
1401                StabilizerError::ResourceLimited {
1402                    phase: "dense matrix words",
1403                    observed: 147,
1404                    limit: 100,
1405                }
1406            ))
1407        ));
1408        with_auxiliary
1409            .validate_measurements_close_before_outputs()
1410            .unwrap();
1411    }
1412
1413    #[test]
1414    fn test_cnot() {
1415        let cnot = GalleryItem::CNOT
1416            .build()
1417            .materialize_root_graph()
1418            .expect("gallery flat projection");
1419        let zx = ZXGraph::try_from(&cnot).unwrap();
1420        assert_eq!(zx.nodes.len(), cnot.block_count());
1421        assert_eq!(zx.edges.len(), 2 * cnot.pipe_count());
1422        let stabilizers = zx.stabilizers().unwrap();
1423        assert_eq!(stabilizers.len(), 4);
1424    }
1425
1426    #[test]
1427    fn test_cz() {
1428        let cz = GalleryItem::CZSpatialH
1429            .build()
1430            .materialize_root_graph()
1431            .expect("gallery flat projection");
1432        let zx = ZXGraph::try_from(&cz).unwrap();
1433        let stabilizers = zx.stabilizers().unwrap();
1434        assert_eq!(stabilizers.len(), 4);
1435    }
1436
1437    #[test]
1438    fn signed_y_boundaries_track_choi_transposition() {
1439        for (blog, negative) in [
1440            (
1441                "BLOG 1.0\n\n  0: Y [0,0,0]\n  1: Port [0,0,1]\n  [0,0,0] -> +Z\n",
1442                false,
1443            ),
1444            (
1445                "BLOG 1.0\n\n  0: Port [0,0,0]\n  1: Y [0,0,1]\n  [0,0,0] -> +Z\n",
1446                true,
1447            ),
1448        ] {
1449            let stabilizers = BlockGraph::from_blog_text(blog)
1450                .unwrap()
1451                .stabilizers()
1452                .unwrap();
1453            assert_eq!(stabilizers.len(), 1);
1454            assert_eq!(stabilizers.generators[0].stabilizer.sign, negative);
1455        }
1456    }
1457
1458    #[test]
1459    fn test_closed_cnot() {
1460        let mut cnot = GalleryItem::CNOT
1461            .build()
1462            .materialize_root_graph()
1463            .expect("gallery flat projection");
1464        for pos in [
1465            ivec3(0, 0, 0),
1466            ivec3(1, 1, 0),
1467            ivec3(0, 0, 3),
1468            ivec3(1, 1, 3),
1469        ] {
1470            let block = cnot.get_block_mut(pos).unwrap();
1471            block.kind = BlockKind::Cube(crate::CubeKind::ZXZ);
1472        }
1473        let zx = ZXGraph::try_from(&cnot).unwrap();
1474        assert!(!zx.is_open());
1475        let stabilizers = zx.stabilizers().unwrap();
1476        assert_eq!(stabilizers.len(), 2);
1477    }
1478
1479    #[test]
1480    fn test_fill_ports_auto_covers_open_generators() {
1481        let graph = GalleryItem::CNOT
1482            .build()
1483            .materialize_root_graph()
1484            .expect("gallery flat projection");
1485        let ordered_ports = ordered_port_positions(&graph);
1486        let generators: HashSet<String> = graph
1487            .stabilizers()
1488            .unwrap()
1489            .generators
1490            .into_iter()
1491            .map(|stabilizer| stabilizer_external_string(&stabilizer.stabilizer, &ordered_ports))
1492            .collect();
1493
1494        let filled = graph.fill_ports_auto().unwrap();
1495        assert!(!filled.is_empty());
1496
1497        let mut covered = HashSet::new();
1498        for (filled_graph, stabilizers) in &filled {
1499            assert_eq!(filled_graph.port_count(), 0);
1500            filled_graph.validate().unwrap();
1501            let stabilizer_strings: Vec<String> = stabilizers
1502                .iter()
1503                .map(|generator| stabilizer_external_string(&generator.stabilizer, &ordered_ports))
1504                .collect();
1505            for stabilizer in &stabilizer_strings {
1506                covered.insert(stabilizer.clone());
1507            }
1508            for i in 0..stabilizer_strings.len() {
1509                for j in (i + 1)..stabilizer_strings.len() {
1510                    assert!(are_compatible_stabilizers(
1511                        &stabilizer_strings[i],
1512                        &stabilizer_strings[j]
1513                    ));
1514                }
1515            }
1516        }
1517
1518        assert_eq!(covered, generators);
1519    }
1520
1521    #[test]
1522    fn automatic_fill_honors_explicit_certification_limits() {
1523        let graph = GalleryItem::CNOT
1524            .build()
1525            .materialize_root_graph()
1526            .expect("gallery flat projection");
1527        let limited = ModuleCertificationLimits {
1528            max_local_columns: 0,
1529            ..ModuleCertificationLimits::DEFAULT
1530        };
1531        assert!(matches!(
1532            graph.filled_graphs_with_limits(limited),
1533            Err(BlockGraphError::Stabilizer(
1534                StabilizerError::ResourceLimited {
1535                    phase: "local ZX columns",
1536                    limit: 0,
1537                    ..
1538                }
1539            ))
1540        ));
1541        assert!(
1542            !graph
1543                .filled_graphs_with_limits(ModuleCertificationLimits {
1544                    max_local_columns: usize::MAX,
1545                    ..limited
1546                })
1547                .unwrap()
1548                .is_empty()
1549        );
1550    }
1551
1552    #[test]
1553    fn automatic_fill_keeps_only_selected_readouts_and_their_aliases() {
1554        for item in [GalleryItem::T, GalleryItem::TWithPreparedY] {
1555            let mut source = item
1556                .build()
1557                .materialize_root_graph()
1558                .expect("gallery flat projection");
1559            source
1560                .add_action(Action::Let {
1561                    name: "saved".into(),
1562                    expr: Expr::Var("mzz".into()),
1563                })
1564                .unwrap();
1565            let filled = source.fill_ports_auto().unwrap();
1566            assert!(!filled.is_empty());
1567            let mut retained = 0;
1568            let mut omitted = 0;
1569            for (graph, selected) in filled {
1570                let has_readout = selected
1571                    .iter()
1572                    .any(|generator| generator.measurement_name() == Some("mzz"));
1573                if has_readout {
1574                    retained += 1;
1575                } else {
1576                    omitted += 1;
1577                }
1578                assert_eq!(
1579                    graph.actions().iter().any(
1580                        |action| matches!(action, Action::Measure { name, .. } if name == "mzz")
1581                    ),
1582                    has_readout
1583                );
1584                assert_eq!(
1585                    graph.actions().iter().any(
1586                        |action| matches!(action, Action::Let { name, .. } if name == "saved")
1587                    ),
1588                    has_readout
1589                );
1590                assert_eq!(graph.selective_count(), 0);
1591                assert!(
1592                    !graph
1593                        .actions()
1594                        .iter()
1595                        .any(|action| matches!(action, Action::Resolve { .. }))
1596                );
1597                graph.validate().unwrap();
1598                for generator in selected {
1599                    for pos in ordered_port_positions(&source) {
1600                        let pauli = generator
1601                            .stabilizer
1602                            .interior_nodes
1603                            .get(&pos)
1604                            .copied()
1605                            .unwrap_or(Pauli::I);
1606                        if pauli == Pauli::I {
1607                            continue;
1608                        }
1609                        let fill = graph.get_block(pos).unwrap().kind();
1610                        let basis = match fill {
1611                            BlockKind::Y => Pauli::Y,
1612                            BlockKind::Cube(kind) => Pauli::from(kind.z()),
1613                            _ => panic!("unfilled {pos}"),
1614                        };
1615                        assert_eq!(basis, pauli);
1616                    }
1617                }
1618            }
1619            assert!(retained > 0 && omitted > 0);
1620            source
1621                .add_action(Action::DiscardIf(Expr::Var("saved".into())))
1622                .unwrap();
1623            assert!(matches!(
1624                source.fill_ports_auto(),
1625                Err(BlockGraphError::FillPorts(
1626                    FillPortsError::UnavailableControl { .. }
1627                ))
1628            ));
1629        }
1630    }
1631
1632    #[test]
1633    fn fill_ports_auto_removes_feedback_from_filled_variants() {
1634        let mut graph = BlockGraph::from_blog_text(
1635            "BLOG 1.0\n\n  0: ZXZ [0,0,0]\n  1: Port [0,0,1]\n  [0,0,0] -> +Z\n",
1636        )
1637        .unwrap();
1638        graph
1639            .add_action(Action::Feedback {
1640                targets: vec![FeedbackTarget {
1641                    pauli: PauliBasis::Z,
1642                    target: ivec3(0, 0, 1),
1643                    direction: None,
1644                }],
1645                condition: None,
1646            })
1647            .unwrap();
1648
1649        let filled = graph.fill_ports_auto().unwrap();
1650        assert!(!filled.is_empty());
1651
1652        for (filled_graph, _) in filled {
1653            assert!(
1654                filled_graph
1655                    .actions()
1656                    .iter()
1657                    .all(|action| !matches!(action, Action::Feedback { .. }))
1658            );
1659            filled_graph.validate().unwrap();
1660        }
1661    }
1662
1663    #[test]
1664    fn fill_ports_auto_uses_patch_rotation_pipe_face_basis() {
1665        let graph = BlockGraph::from_blog_text(
1666            "BLOG 1.0\n\n  0: rotate Z [0, 0, 0] -> [1, 0, 1]\n  1: Port [0, 0, -1]\n  2: Port [1, 0, 2]\n  [0, 0, 0] -> -Z\n  [1, 0, 1] -> +Z\n",
1667        )
1668        .expect("patch rotation graph should parse");
1669
1670        assert!(
1671            graph
1672                .fill_ports_auto()
1673                .unwrap()
1674                .into_iter()
1675                .all(|(filled_graph, _)| filled_graph.port_count() == 0
1676                    && filled_graph.validate().is_ok())
1677        );
1678    }
1679
1680    #[test]
1681    fn zx_stabilizers_tag_measurement_names_on_generators() {
1682        let graph = GalleryItem::T
1683            .build()
1684            .materialize_root_graph()
1685            .expect("gallery flat projection");
1686        let zx = ZXGraph::try_from(&graph).unwrap();
1687        let stabilizers = zx.stabilizers().unwrap();
1688
1689        assert_eq!(
1690            stabilizers
1691                .generators
1692                .iter()
1693                .filter_map(StabilizerGenerator::measurement_name)
1694                .collect::<Vec<_>>(),
1695            vec!["mzz"]
1696        );
1697    }
1698
1699    #[test]
1700    fn product_assumes_consistent_edge_orientation() {
1701        let a = ivec3(0, 0, 0);
1702        let b = ivec3(1, 0, 0);
1703        let with_edge = |edge: (IVec3, IVec3)| Stabilizer {
1704            paulis: PauliString::new(0),
1705            sign: false,
1706            port_stabilizer: Default::default(),
1707            interior_nodes: Default::default(),
1708            interior_edges: [(edge, Pauli::X)].into_iter().collect(),
1709        };
1710
1711        let cancelled = with_edge((a, b)).phase_free_product(&with_edge((a, b)));
1712        assert!(cancelled.interior_edges.is_empty());
1713
1714        let doubled = with_edge((a, b)).phase_free_product(&with_edge((b, a)));
1715        assert_eq!(doubled.interior_edges.len(), 2);
1716    }
1717
1718    #[test]
1719    fn in_place_product_updates_contracted_y_sign() {
1720        let edge = (ivec3(0, 0, 0), ivec3(1, 0, 0));
1721        let row = |pauli| Stabilizer {
1722            paulis: PauliString::new(0),
1723            sign: false,
1724            port_stabilizer: Default::default(),
1725            interior_nodes: Default::default(),
1726            interior_edges: [(edge, pauli)].into_iter().collect(),
1727        };
1728        let mut product = row(Pauli::X);
1729
1730        product.phase_free_mul_assign(&row(Pauli::Z));
1731
1732        assert_eq!(product.interior_edges[&edge], Pauli::Y);
1733        assert!(product.sign);
1734    }
1735
1736    #[test]
1737    fn test_ordered_port_positions_uses_block_order() {
1738        let mut graph = BlockGraph::new();
1739        graph.add_block(Block::new(ivec3(2, 0, 0), BlockKind::Port));
1740        graph.add_block(
1741            Block::new(ivec3(1, 0, 0), BlockKind::Port)
1742                .with_tag("b")
1743                .expect("valid tag"),
1744        );
1745        graph.add_block(
1746            Block::new(ivec3(0, 0, 0), BlockKind::Port)
1747                .with_tag("a")
1748                .expect("valid tag"),
1749        );
1750
1751        assert_eq!(
1752            ordered_port_positions(&graph),
1753            vec![ivec3(0, 0, 0), ivec3(1, 0, 0), ivec3(2, 0, 0)]
1754        );
1755    }
1756}