Skip to main content

bloq_graph/zx/
runtime_basis.rs

1//! Runtime-selectable stabilizer bases and derived readout surfaces.
2
3use std::collections::HashMap;
4
5use glam::IVec3;
6use thiserror::Error;
7
8use super::graph::filled_node_kind;
9use super::output_correction::{OutputCorrectionRow, pivot_output_correction_rows};
10use super::stabilizer::{
11    CoeffVec, DeadlineProjection, SelectiveConstraint, StabilizerBasis,
12    collect_selective_constraints, deadline_projection_with_initial_coeffs,
13    normalize_selective_support, reduce_to_basis, solve_coeff_combination,
14};
15use super::{
16    NodeKind, Stabilizer, StabilizerError, StabilizerGenerator, StabilizerGenerators,
17    StabilizerRowKind, ZXError, ZXGraph,
18};
19use crate::{BlockGraph, ModuleCertificationLimits, Pauli, PauliBasis, PauliString, SelectiveKind};
20
21/// An error from deriving or updating a [`RuntimeStabilizerBasis`].
22#[derive(Debug, Clone, Error)]
23#[non_exhaustive]
24pub enum RuntimeBasisError {
25    /// A selective site is absent from the live basis.
26    #[error("selective node at {0} is not part of the live basis")]
27    UnknownSelective(IVec3),
28    /// A selective site occurs twice in one assignment.
29    #[error("selective node at {0} appears more than once in one assignment")]
30    DuplicateSelective(IVec3),
31    /// A chosen basis is not allowed at the selective site.
32    #[error("basis {chosen} is not allowed at selective {pos} of kind {kind}")]
33    InvalidSelectiveBasis {
34        /// Selective position.
35        pos: IVec3,
36        /// Selective kind.
37        kind: SelectiveKind,
38        /// Chosen basis.
39        chosen: PauliBasis,
40    },
41    /// A measurement variable is absent from the live graph.
42    #[error("measurement variable {0:?} is not present in the live ZX graph")]
43    UnknownMeasurement(String),
44    /// Filling non-Clifford computation is unsupported.
45    #[error("selective fill is only supported for Clifford-only computation")]
46    NonCliffordFillUnsupported,
47    /// An output is absent from the live graph.
48    #[error("output port {0} is not present in the live ZX graph")]
49    UnknownOutput(IVec3),
50    /// No closed frame surface exists for an output Pauli.
51    #[error("no closed {pauli} frame surface for output {output}")]
52    OutputSurfaceUnavailable {
53        /// Output position.
54        output: IVec3,
55        /// Requested Pauli basis.
56        pauli: PauliBasis,
57    },
58    /// ZX graph update failed.
59    #[error("{0}")]
60    ZX(#[from] ZXError),
61    /// Stabilizer-basis derivation failed.
62    #[error("{0}")]
63    Stabilizer(#[from] StabilizerError),
64}
65
66/// The live stabilizer basis of a ZX graph, tracking the generators and the
67/// selective nodes whose runtime basis choice can still update it.
68///
69/// This is the state that dynamic (selective) fills mutate as each selective is
70/// resolved, and the source of derived measurement and output surfaces.
71#[derive(Debug, Clone)]
72pub struct RuntimeStabilizerBasis {
73    zx_graph: ZXGraph,
74    // Filling treats native T states as ports, but does not turn prepared
75    // resources into arbitrary inputs of the authored map.
76    input_columns: Vec<usize>,
77    prepared_columns: Vec<usize>,
78    basis: StabilizerBasis,
79    /// Readable-value provenance of every live row, expressed in the original
80    /// generator ordinals. Internal and fixing directions carry the zero vector.
81    /// Row operations update these coefficients with Pauli support, so
82    /// readability never depends on a mutable row-role label.
83    readout_coeffs: Vec<CoeffVec>,
84    measurement_ordinals: HashMap<String, usize>,
85    selective_kinds: HashMap<IVec3, SelectiveKind>,
86}
87
88/// A materialized stabilizer surface with readable-value provenance.
89#[derive(Debug, Clone, PartialEq)]
90pub struct DerivedSurface {
91    /// Original public generator ordinals whose readable values XOR to this
92    /// surface. Internal and fixing directions carry no readable ordinal.
93    /// A frozen [`super::ReadoutPlan`] declares whether these are public-row
94    /// coordinates or earlier corrected source outcomes. In source coordinates,
95    /// the name being defined is supplied by the recipe's owner.
96    pub readout_ordinals: Vec<usize>,
97    /// The surface materialized as a [`Stabilizer`].
98    pub stabilizer: Stabilizer,
99}
100
101impl RuntimeStabilizerBasis {
102    /// The live ZX graph backing this basis.
103    pub fn zx_graph(&self) -> &ZXGraph {
104        &self.zx_graph
105    }
106
107    /// The Pauli-string generators of this basis, in row order.
108    pub fn generators(&self) -> &[PauliString] {
109        &self.basis.rows
110    }
111
112    pub(crate) fn output_free_readout_failure(
113        &self,
114        name: &str,
115        ordinal: usize,
116        readout_width: usize,
117        outputs: &[IVec3],
118    ) -> Result<Option<IVec3>, RuntimeBasisError> {
119        let mut components = Vec::new();
120        for &output in outputs {
121            let node = self
122                .zx_graph
123                .node_at(output)
124                .filter(|node| node.is_output_port(&self.zx_graph))
125                .ok_or(RuntimeBasisError::UnknownOutput(output))?;
126            components.push((output, node.id, Pauli::X));
127            if node.role != crate::PortRole::Multiplex {
128                components.push((output, node.id, Pauli::Z));
129            }
130        }
131
132        let width = readout_width + components.len();
133        let vectors = self
134            .basis
135            .rows
136            .iter()
137            .zip(&self.readout_coeffs)
138            .map(|(row, readout)| {
139                let mut projected = CoeffVec::zeros(width);
140                for bit in readout.iter_ones() {
141                    projected.set_bit(bit, true);
142                }
143                for (offset, (_, col, axis)) in components.iter().enumerate() {
144                    projected.set_bit(readout_width + offset, row.get(*col) & *axis);
145                }
146                projected
147            })
148            .collect::<Vec<_>>();
149        if solve_coeff_combination(&vectors, &CoeffVec::singleton(ordinal, width)).is_some() {
150            return Ok(None);
151        }
152
153        let witness = solve_coeff_combination(
154            &self.readout_coeffs,
155            &CoeffVec::singleton(ordinal, readout_width),
156        )
157        .ok_or_else(|| RuntimeBasisError::UnknownMeasurement(name.into()))?;
158        let mut row = PauliString::new(self.zx_graph.total_ids());
159        for index in witness.iter_ones() {
160            row ^= &self.basis.rows[index];
161        }
162        for (output, col, axis) in components {
163            if row.get(col) & axis {
164                return Ok(Some(output));
165            }
166        }
167        unreachable!("an output-free witness would satisfy the projected solve")
168    }
169
170    /// Demotes every [`T`](NodeKind::T) node to a plain [`Port`](NodeKind::Port).
171    /// Used when selective filling treats prepared T states as open inputs.
172    pub fn with_t_nodes_as_ports(mut self) -> Self {
173        // T and Port have the same local flow rows and cross-center behavior,
174        // so their signed phase basis survives this relabeling.
175        for node in &mut self.zx_graph.nodes {
176            if node.kind == NodeKind::T {
177                node.kind = NodeKind::Port;
178            }
179        }
180        self
181    }
182
183    fn selective_port_map(
184        &self,
185        fills: &[(IVec3, PauliBasis)],
186    ) -> Result<HashMap<IVec3, NodeKind>, RuntimeBasisError> {
187        let mut port_map = HashMap::with_capacity(fills.len());
188        for &(pos, chosen) in fills {
189            let kind = self
190                .selective_kinds
191                .get(&pos)
192                .copied()
193                .ok_or(RuntimeBasisError::UnknownSelective(pos))?;
194            if ![kind.pauli_if_true(), kind.pauli_if_false()].contains(&chosen) {
195                return Err(RuntimeBasisError::InvalidSelectiveBasis { pos, kind, chosen });
196            }
197            if port_map.insert(pos, filled_node_kind(chosen)).is_some() {
198                return Err(RuntimeBasisError::DuplicateSelective(pos));
199            }
200        }
201        Ok(port_map)
202    }
203
204    /// Consumes this live basis and materializes its current rows against its
205    /// current ZX graph.
206    pub fn into_generators(self) -> StabilizerGenerators {
207        let Self {
208            zx_graph,
209            mut basis,
210            ..
211        } = self;
212        // Selective pivots XOR rows; cross centers must follow the resulting
213        // edge support before those rows become physical observable requests.
214        zx_graph.reconstruct_cross_center(&mut basis.rows);
215        let auxiliary_rows = basis.rows.split_off(basis.public_len);
216        basis.kinds.truncate(basis.public_len);
217        let row_phases = zx_graph.stabilizer_row_phases(&basis.rows);
218        let generators = basis
219            .rows
220            .into_iter()
221            .zip(basis.kinds)
222            .zip(row_phases)
223            .map(|((row, kind), row_phase)| {
224                StabilizerGenerator::new(
225                    zx_graph.pauli_string_to_stabilizer_with_row_phase(row, row_phase),
226                    kind,
227                )
228            })
229            .collect();
230        StabilizerGenerators::from_parts(zx_graph, generators, auxiliary_rows)
231    }
232
233    /// Builds the runtime basis by converting `graph` to a ZX graph first.
234    ///
235    /// # Errors
236    ///
237    /// Returns a [`RuntimeBasisError`] if conversion or basis derivation fails.
238    pub fn from_block_graph(graph: &BlockGraph) -> Result<Self, RuntimeBasisError> {
239        let zx_graph = ZXGraph::try_from(graph)?;
240        Self::from_zx_graph(zx_graph)
241    }
242
243    /// Builds the runtime basis from an existing ZX graph.
244    ///
245    /// # Errors
246    ///
247    /// Returns a [`RuntimeBasisError`] if the stabilizer basis cannot be derived.
248    pub fn from_zx_graph(zx_graph: ZXGraph) -> Result<Self, RuntimeBasisError> {
249        Self::from_zx_graph_with_limits(zx_graph, ModuleCertificationLimits::DEFAULT)
250    }
251
252    /// Builds a runtime basis under explicit stabilizer-search limits.
253    ///
254    /// # Errors
255    ///
256    /// Returns a basis-derivation or resource-limit error.
257    pub fn from_zx_graph_with_limits(
258        zx_graph: ZXGraph,
259        limits: ModuleCertificationLimits,
260    ) -> Result<Self, RuntimeBasisError> {
261        match zx_graph.stabilizers_with_limits(limits) {
262            Ok(stabilizers) => return Ok(Self::from_generators(&stabilizers)),
263            Err(
264                StabilizerError::SelectiveSupportUnsatisfiable { .. }
265                | StabilizerError::MeasurementSurfaceUnavailable { .. },
266            ) => {}
267            Err(error) => return Err(error.into()),
268        }
269
270        // Runtime filling can still use a private all-logical basis when no
271        // uniform readable presentation exists before the fills.
272        let raw_external = zx_graph.to_external_generator_table();
273        let constraints = collect_selective_constraints(&zx_graph);
274        let raw_basis = reduce_to_basis(&raw_external, zx_graph.total_ids);
275        let generators = reduce_to_basis(
276            &normalize_selective_support(&raw_basis, &constraints)?,
277            zx_graph.total_ids,
278        );
279        let basis = StabilizerBasis::all_logical(generators);
280        let selective_kinds = selective_kinds_of(&zx_graph);
281        let readout_coeffs = initial_readout_coeffs(&basis.kinds, basis.public_len);
282        let (input_columns, prepared_columns) = authored_boundary_columns(&zx_graph);
283        Ok(Self {
284            input_columns,
285            prepared_columns,
286            zx_graph,
287            basis,
288            readout_coeffs,
289            measurement_ordinals: HashMap::new(),
290            selective_kinds,
291        })
292    }
293
294    /// Builds the runtime basis directly from already-canonicalized
295    /// [`StabilizerGenerators`], reusing their rows and row kinds verbatim
296    /// instead of re-deriving the canonical stabilizer table from the ZX graph.
297    ///
298    /// The *n*th runtime generator is the *n*th input generator, so the row
299    /// ordinals downstream code keys on stay identity-stable. This exists so a
300    /// caller that already holds the graph's `StabilizerGenerators` avoids
301    /// paying for another canonicalization pass, which is otherwise what
302    /// [`from_zx_graph`](Self::from_zx_graph) would re-run.
303    pub fn from_generators(stabilizers: &StabilizerGenerators) -> Self {
304        let zx_graph = stabilizers.zx_graph.clone();
305        let rows = stabilizers
306            .generators
307            .iter()
308            .map(|generator| generator.stabilizer.paulis.clone())
309            .chain(stabilizers.auxiliary_rows().iter().cloned())
310            .collect();
311        let kinds: Vec<_> = stabilizers
312            .generators
313            .iter()
314            .map(|generator| generator.kind.clone())
315            .chain(
316                stabilizers
317                    .auxiliary_rows()
318                    .iter()
319                    .map(|_| StabilizerRowKind::Logical),
320            )
321            .collect();
322        let selective_kinds = selective_kinds_of(&zx_graph);
323        let measurement_ordinals = stabilizers
324            .generators
325            .iter()
326            .enumerate()
327            .filter_map(|(ordinal, generator)| {
328                generator
329                    .measurement_name()
330                    .map(|name| (name.to_owned(), ordinal))
331            })
332            .collect();
333        let public_len = stabilizers.generators.len();
334        let readout_coeffs = initial_readout_coeffs(&kinds, public_len);
335        let (input_columns, prepared_columns) = authored_boundary_columns(&zx_graph);
336        Self {
337            input_columns,
338            prepared_columns,
339            zx_graph,
340            basis: StabilizerBasis::with_public_len(rows, kinds, public_len),
341            readout_coeffs,
342            measurement_ordinals,
343            selective_kinds,
344        }
345    }
346
347    /// Builds linear X/Z source-outcome coordinates on the complete signed basis.
348    ///
349    /// Public row roles do not define these coordinates: a named anchor can also
350    /// occur in a logical or private completion row. This basis is used to freeze
351    /// causal readouts and to remove those same named components from frames.
352    /// Crossing-node and Y readouts require the ordinary readout convention.
353    ///
354    /// # Errors
355    ///
356    /// Returns an error if a named source measurement lacks a concrete readable row.
357    pub fn for_source_readouts(
358        stabilizers: &StabilizerGenerators,
359    ) -> Result<Self, RuntimeBasisError> {
360        let mut basis = Self::from_generators(stabilizers).with_t_nodes_as_ports();
361        let width = stabilizers.generators.len();
362        basis.readout_coeffs.fill(CoeffVec::zeros(width));
363        for node in basis.zx_graph.action_graph().ordered_nodes() {
364            let crate::Action::Measure { name, target } = &node.action else {
365                continue;
366            };
367            let axis = match node.measurement {
368                Some(crate::MeasurementObservable::Concrete(PauliBasis::X)) => Pauli::X,
369                Some(crate::MeasurementObservable::Concrete(PauliBasis::Z)) => Pauli::Z,
370                _ => {
371                    return Err(StabilizerError::MeasurementSurfaceUnavailable {
372                        mvar: name.clone(),
373                    }
374                    .into());
375                }
376            };
377            let col = basis
378                .zx_graph
379                .measurement_column(target)
380                .ok_or_else(|| RuntimeBasisError::UnknownMeasurement(name.clone()))?;
381            if basis.zx_graph.nodes().get(col).is_some_and(|node| {
382                matches!(node.kind, NodeKind::X | NodeKind::Z) && node.kind.cross_pauli() == axis
383            }) {
384                return Err(
385                    StabilizerError::MeasurementSurfaceUnavailable { mvar: name.clone() }.into(),
386                );
387            }
388            let ordinal = basis.measurement_ordinals[name];
389            for (row, coeff) in basis.basis.rows.iter().zip(&mut basis.readout_coeffs) {
390                coeff.set_bit(ordinal, row.get(col) & axis);
391            }
392        }
393        Ok(basis)
394    }
395
396    /// The composed relation restricted to zero source named-outcome coordinates.
397    /// Closed named readouts can remove those coordinates without changing any
398    /// output operator, so this kernel retains the full terminal frame space.
399    ///
400    /// Test-only since the symbolic-frame planner was superseded by
401    /// `GuardedSurfaceSpace`: its remaining caller is the terminal-boundary
402    /// oracle that cross-checks the live guarded planner.
403    #[cfg(test)]
404    pub(super) fn for_output_frames(
405        stabilizers: &StabilizerGenerators,
406    ) -> Result<Self, RuntimeBasisError> {
407        let mut basis = Self::for_source_readouts(stabilizers)?;
408        let constraints = basis
409            .zx_graph
410            .action_graph()
411            .ordered_nodes()
412            .filter_map(|node| {
413                let crate::Action::Measure { target, .. } = &node.action else {
414                    return None;
415                };
416                let Some(crate::MeasurementObservable::Concrete(axis)) = node.measurement else {
417                    unreachable!("source readout measurements have concrete observables")
418                };
419                Some((
420                    basis
421                        .zx_graph
422                        .measurement_column(target)
423                        .expect("named coordinate"),
424                    Pauli::from(axis),
425                ))
426            })
427            .collect::<Vec<_>>();
428        basis.restrict_to_zero(&constraints);
429        Ok(basis)
430    }
431
432    #[cfg(test)]
433    fn restrict_to_zero(&mut self, constraints: &[(usize, Pauli)]) {
434        let rank = super::stabilizer::gaussian_elimination_with_tracking(
435            &mut self.basis.rows,
436            &mut self.readout_coeffs,
437            constraints.iter().copied(),
438            |row, &(col, axis)| row.get(col) & axis,
439            None,
440        );
441        self.basis.rows.drain(..rank);
442        self.readout_coeffs.drain(..rank);
443        self.basis = StabilizerBasis::all_logical(std::mem::take(&mut self.basis.rows));
444    }
445
446    /// Resolves the selective node at `pos` to the `chosen` basis and returns
447    /// the evolved basis. Readable provenance stays internal to that basis.
448    ///
449    /// # Errors
450    ///
451    /// Returns a [`RuntimeBasisError`] if `pos` is not a live selective, the
452    /// basis is not allowed for its kind, or the computation is not Clifford.
453    pub fn apply_selective_fill(
454        &self,
455        pos: IVec3,
456        chosen: PauliBasis,
457    ) -> Result<RuntimeStabilizerBasis, RuntimeBasisError> {
458        self.apply_selective_fills(&[(pos, chosen)])
459    }
460
461    /// Resolves one known assignment with the usual ordered basis transitions,
462    /// but fills the graph only once.
463    ///
464    /// # Errors
465    ///
466    /// Returns [`RuntimeBasisError`] when a requested fill is invalid.
467    ///
468    /// # Panics
469    ///
470    /// Panics if validated selective metadata is internally inconsistent.
471    pub fn apply_selective_fills(
472        &self,
473        fills: &[(IVec3, PauliBasis)],
474    ) -> Result<RuntimeStabilizerBasis, RuntimeBasisError> {
475        if fills.is_empty() {
476            return Ok(self.clone());
477        }
478        if !self.zx_graph.is_clifford_computation() {
479            return Err(RuntimeBasisError::NonCliffordFillUnsupported);
480        }
481
482        let port_map = self.selective_port_map(fills)?;
483
484        let mut basis = self.basis.clone();
485        let mut readout_coeffs = self.readout_coeffs.clone();
486        let mut remaining_constraints = collect_selective_constraints(&self.zx_graph);
487        for &(pos, chosen) in fills {
488            let kind = self.selective_kinds[&pos];
489            let col = self
490                .zx_graph
491                .node_at(pos)
492                .expect("validated selective has a ZX node")
493                .id;
494            remaining_constraints.retain(|constraint| constraint.pos != pos);
495            let constraints = [(col, Pauli::from(chosen))];
496            (basis, readout_coeffs) = evolve_selective_basis(
497                &basis,
498                &readout_coeffs,
499                &constraints,
500                &remaining_constraints,
501                pos,
502                kind,
503            )?;
504        }
505
506        let filled_zx = self.zx_graph.fill_ports(&port_map)?;
507        Ok(RuntimeStabilizerBasis {
508            input_columns: self.input_columns.clone(),
509            prepared_columns: self.prepared_columns.clone(),
510            zx_graph: filled_zx,
511            basis,
512            readout_coeffs,
513            measurement_ordinals: self.measurement_ordinals.clone(),
514            selective_kinds: remaining_constraints
515                .into_iter()
516                .map(|constraint| (constraint.pos, constraint.kind))
517                .collect(),
518        })
519    }
520
521    /// Resolves several assignments while sharing their common fill prefixes.
522    /// Assignments with different site order use the ordinary independent path.
523    ///
524    /// # Errors
525    ///
526    /// Returns the same validation or fill errors as [`Self::apply_selective_fills`].
527    ///
528    /// # Panics
529    ///
530    /// Panics if validated selective metadata is internally inconsistent.
531    pub fn apply_selective_fill_assignments(
532        &self,
533        assignments: &[Vec<(IVec3, PauliBasis)>],
534    ) -> Result<Vec<RuntimeStabilizerBasis>, RuntimeBasisError> {
535        let independently = || {
536            assignments
537                .iter()
538                .map(|fills| self.apply_selective_fills(fills))
539                .collect()
540        };
541        let Some(first) = assignments.first() else {
542            return Ok(Vec::new());
543        };
544        if assignments.iter().any(|assignment| {
545            assignment.len() != first.len()
546                || assignment
547                    .iter()
548                    .zip(first)
549                    .any(|((pos, _), (first, _))| pos != first)
550        }) {
551            return independently();
552        }
553        if first.is_empty() {
554            return Ok(vec![self.clone(); assignments.len()]);
555        }
556        if !self.zx_graph.is_clifford_computation() {
557            return Err(RuntimeBasisError::NonCliffordFillUnsupported);
558        }
559        let Ok(port_maps) = assignments
560            .iter()
561            .map(|fills| self.selective_port_map(fills))
562            .collect::<Result<Vec<_>, _>>()
563        else {
564            return independently();
565        };
566        let mut states = vec![None; assignments.len()];
567        let remaining = collect_selective_constraints(&self.zx_graph);
568        if fill_prefix_states(self, assignments, remaining.clone(), &mut states).is_none() {
569            return independently();
570        }
571        let remaining = first.iter().fold(remaining, |mut remaining, (pos, _)| {
572            remaining.retain(|constraint| constraint.pos != *pos);
573            remaining
574        });
575        states
576            .into_iter()
577            .zip(port_maps)
578            .map(|(state, ports)| {
579                let (basis, readout_coeffs) = state.expect("every assignment reached a trie leaf");
580                Ok(RuntimeStabilizerBasis {
581                    input_columns: self.input_columns.clone(),
582                    prepared_columns: self.prepared_columns.clone(),
583                    zx_graph: self.zx_graph.fill_ports(&ports)?,
584                    basis,
585                    readout_coeffs,
586                    measurement_ordinals: self.measurement_ordinals.clone(),
587                    selective_kinds: remaining
588                        .iter()
589                        .map(|constraint| (constraint.pos, constraint.kind))
590                        .collect(),
591                })
592            })
593            .collect()
594    }
595
596    /// Derives the stabilizer surface that witnesses measurement variable
597    /// `mvar`, projected to what is decidable by time layer `deadline`.
598    /// Preserves the evolved readout identity after selective fills and prefers
599    /// an output-safe representative when one exists.
600    ///
601    /// # Errors
602    ///
603    /// Returns [`RuntimeBasisError::UnknownMeasurement`] if `mvar` is not a live
604    /// measurement variable.
605    pub fn derive_measurement_surface(
606        &self,
607        mvar: &str,
608        deadline: i64,
609    ) -> Result<DerivedSurface, RuntimeBasisError> {
610        self.derive_measurement_surface_with_constraints(mvar, deadline, &[], &[], false, &[])
611    }
612
613    /// Selects an output-free readout using only resolved selective choices.
614    ///
615    /// # Errors
616    ///
617    /// Returns an error if `mvar` is unknown or no causal representative exists.
618    pub fn derive_causal_measurement_surface(
619        &self,
620        mvar: &str,
621        deadline: i64,
622    ) -> Result<DerivedSurface, RuntimeBasisError> {
623        self.derive_readout_from_known(mvar, deadline, &[], &[])
624    }
625
626    pub(crate) fn derive_readout_from_known(
627        &self,
628        mvar: &str,
629        deadline: i64,
630        unavailable: &[Vec<(usize, Pauli)>],
631        known: &[usize],
632    ) -> Result<DerivedSurface, RuntimeBasisError> {
633        let absent = self
634            .selective_kinds
635            .keys()
636            .flat_map(|&pos| {
637                let col = self.zx_graph.node_at(pos).expect("live selective node").id;
638                [(col, Pauli::X), (col, Pauli::Z)]
639            })
640            .collect::<Vec<_>>();
641        self.derive_measurement_surface_with_constraints(
642            mvar,
643            deadline,
644            &absent,
645            unavailable,
646            true,
647            known,
648        )
649    }
650
651    fn derive_measurement_surface_with_constraints(
652        &self,
653        mvar: &str,
654        deadline: i64,
655        absent: &[(usize, Pauli)],
656        unavailable_feedback: &[Vec<(usize, Pauli)>],
657        require_output_free: bool,
658        known: &[usize],
659    ) -> Result<DerivedSurface, RuntimeBasisError> {
660        let (derivation_rows, derivation_coeffs) = if require_output_free {
661            (self.basis.rows.clone(), self.readout_coeffs.clone())
662        } else {
663            self.derivation_rows_and_coeffs()
664        };
665        let DeadlineProjection {
666            consumed,
667            rows: projected_rows,
668            coeffs: projected_coeffs,
669        } = deadline_projection_with_initial_coeffs(
670            &derivation_rows,
671            &derivation_coeffs,
672            &self.zx_graph,
673            deadline,
674        );
675        let ordinal = *self
676            .measurement_ordinals
677            .get(mvar)
678            .ok_or_else(|| RuntimeBasisError::UnknownMeasurement(mvar.to_owned()))?;
679        let readout_width = self.readout_coeffs.first().map_or(0, CoeffVec::len);
680        let rows = &projected_rows[consumed..];
681        let outputs = self.zx_graph.output_ports();
682        let output_components = outputs
683            .iter()
684            .flat_map(|&pos| {
685                let node = self.zx_graph.node_at(pos).expect("output port is a node");
686                [Pauli::X, Pauli::Z]
687                    .into_iter()
688                    .filter(move |&axis| {
689                        node.role != crate::PortRole::Multiplex || axis == Pauli::X
690                    })
691                    .map(move |axis| (node.id, axis))
692            })
693            .collect::<Vec<_>>();
694        // Selective fills can move a named parity off its original anchor.
695        // Preserve its readable identity, not its pre-fill local Pauli.
696        // Prefer C0-safe representatives; retain the analysis-only fallback.
697        for output_free in [true, false] {
698            if !output_free && require_output_free {
699                break;
700            }
701            let components = absent
702                .iter()
703                .chain(output_components.iter().filter(|_| output_free))
704                .copied()
705                .collect::<Vec<_>>();
706            let width = readout_width + components.len() + unavailable_feedback.len();
707            let constraints = rows
708                .iter()
709                .zip(&projected_coeffs[consumed..])
710                .map(|(row, coeff)| {
711                    let mut value = CoeffVec::zeros(width);
712                    for bit in coeff.iter_ones().filter(|bit| !known.contains(bit)) {
713                        value.set_bit(bit, true);
714                    }
715                    for (offset, &(col, axis)) in components.iter().enumerate() {
716                        value.set_bit(readout_width + offset, row.get(col) & axis);
717                    }
718                    for (offset, targets) in unavailable_feedback.iter().enumerate() {
719                        let odd = targets.iter().fold(false, |odd, &(column, pauli)| {
720                            odd ^ row.get(column).anticommutes(pauli)
721                        });
722                        value.set_bit(readout_width + components.len() + offset, odd);
723                    }
724                    value
725                })
726                .collect::<Vec<_>>();
727            let target = CoeffVec::singleton(ordinal, width);
728            if let Some(combination) = solve_coeff_combination(&constraints, &target) {
729                let mut row = PauliString::new(self.zx_graph.total_ids());
730                let mut coeff = CoeffVec::zeros(readout_width);
731                for index in combination.iter_ones() {
732                    row ^= &rows[index];
733                    coeff.xor_assign(&projected_coeffs[consumed + index]);
734                }
735                return Ok(materialize_derived_surface(&self.zx_graph, row, coeff));
736            }
737        }
738        Err(StabilizerError::UnavailableControlParity {
739            mvar: mvar.to_owned(),
740            deadline,
741        }
742        .into())
743    }
744
745    /// Derives the compact terminal output-correction rows consumed by
746    /// [`crate::solve_output_correction_symbolic`].
747    ///
748    /// # Errors
749    ///
750    /// Returns a [`RuntimeBasisError`] if an output is unknown or cannot be
751    /// supported.
752    pub fn derive_output_correction_rows(
753        &self,
754        outputs: &[IVec3],
755    ) -> Result<Vec<OutputCorrectionRow>, RuntimeBasisError> {
756        Ok(self
757            .pivoted_output_correction_rows(outputs)?
758            .into_iter()
759            .map(|(correction, _)| correction)
760            .collect())
761    }
762
763    /// Derives compact output rows together with their full stabilizer
764    /// surfaces. Consumers that must account for effects outside readable
765    /// generator provenance, such as feedback on auxiliary directions, use
766    /// the materialized surface.
767    ///
768    /// # Errors
769    ///
770    /// Returns an error if an output is unknown or cannot be supported.
771    pub fn derive_output_correction_surfaces(
772        &self,
773        outputs: &[IVec3],
774    ) -> Result<Vec<(OutputCorrectionRow, Stabilizer)>, RuntimeBasisError> {
775        Ok(self
776            .pivoted_output_correction_rows(outputs)?
777            .into_iter()
778            .map(|(correction, row)| (correction, self.zx_graph.materialize_stabilizer(row)))
779            .collect())
780    }
781
782    /// Closed Clifford surfaces constrain internal outcome assignments without
783    /// depending on the state at any open input or output (including T ports).
784    #[cfg(feature = "verify")]
785    pub(crate) fn closed_stabilizer_surfaces(&self) -> Vec<Stabilizer> {
786        let mut rows = self.derivation_rows_and_coeffs().0;
787        let constraints = self
788            .zx_graph
789            .nodes()
790            .iter()
791            .filter(|node| matches!(node.kind, NodeKind::Port | NodeKind::T))
792            .flat_map(|node| [(node.id, Pauli::X), (node.id, Pauli::Z)]);
793        let consumed = super::stabilizer::gaussian_elimination_with_tracking(
794            &mut rows,
795            &mut [],
796            constraints,
797            |row, &(column, axis)| row.get(column) & axis,
798            None,
799        );
800        rows.into_iter()
801            .skip(consumed)
802            .map(|row| self.zx_graph.materialize_stabilizer(row))
803            .collect()
804    }
805
806    /// All resolved rows, including zero-output and private directions.
807    #[cfg(feature = "verify")]
808    pub(crate) fn resolved_surfaces(&self) -> Vec<Stabilizer> {
809        self.derivation_rows_and_coeffs()
810            .0
811            .into_iter()
812            .map(|row| self.zx_graph.materialize_stabilizer(row))
813            .collect()
814    }
815
816    /// Consumes this basis while deriving output corrections, moving its dense
817    /// rows and provenance into the solve instead of cloning them.
818    ///
819    /// # Errors
820    ///
821    /// Returns an error if an output is unknown or cannot be supported.
822    pub fn into_output_correction_surfaces(
823        self,
824        outputs: &[IVec3],
825    ) -> Result<Vec<(OutputCorrectionRow, Stabilizer)>, RuntimeBasisError> {
826        let Self {
827            zx_graph,
828            input_columns,
829            prepared_columns,
830            basis,
831            readout_coeffs,
832            ..
833        } = self;
834        let (rows, coeffs) = basis
835            .rows
836            .into_iter()
837            .zip(basis.kinds)
838            .zip(readout_coeffs)
839            .filter(|((_, kind), _)| !kind.is_selective_fixing())
840            .map(|((row, _), coeff)| (row, coeff))
841            .unzip();
842        let output_cols = requested_output_columns(&zx_graph, outputs)?;
843        let protected = protected_boundary_columns(&zx_graph, &prepared_columns, &output_cols);
844        Ok(pivot_output_correction_rows(
845            rows,
846            coeffs,
847            outputs,
848            &output_cols,
849            &input_columns,
850            &protected,
851        )
852        .into_iter()
853        .map(|(correction, row)| (correction, zx_graph.materialize_stabilizer(row)))
854        .collect())
855    }
856
857    fn pivoted_output_correction_rows(
858        &self,
859        outputs: &[IVec3],
860    ) -> Result<Vec<(OutputCorrectionRow, PauliString)>, RuntimeBasisError> {
861        let projected = self.projected_rows_for_outputs(outputs)?;
862        let protected = protected_boundary_columns(
863            &self.zx_graph,
864            &self.prepared_columns,
865            &projected.output_cols,
866        );
867        Ok(pivot_output_correction_rows(
868            projected.rows,
869            projected.coeffs,
870            outputs,
871            &projected.output_cols,
872            &self.input_columns,
873            &protected,
874        ))
875    }
876
877    /// Projects the derivation rows past the last layer of the graph and
878    /// resolves the requested outputs to their live port nodes.
879    fn projected_rows_for_outputs(
880        &self,
881        outputs: &[IVec3],
882    ) -> Result<ProjectedOutputRows, RuntimeBasisError> {
883        let (projected_rows, projected_coeffs) = self.derivation_rows_and_coeffs();
884        let output_cols = requested_output_columns(&self.zx_graph, outputs)?;
885        Ok(ProjectedOutputRows {
886            rows: projected_rows,
887            coeffs: projected_coeffs,
888            output_cols,
889        })
890    }
891    fn derivation_rows_and_coeffs(&self) -> (Vec<PauliString>, Vec<CoeffVec>) {
892        self.basis
893            .rows
894            .iter()
895            .zip(&self.basis.kinds)
896            .zip(&self.readout_coeffs)
897            .filter(|((_, kind), _)| !kind.is_selective_fixing())
898            .map(|((row, _), coeff)| (row.clone(), coeff.clone()))
899            .unzip()
900    }
901}
902
903/// Capture boundary ownership before native resources are relaxed for fills.
904pub(super) fn authored_boundary_columns(zx: &ZXGraph) -> (Vec<usize>, Vec<usize>) {
905    let mut nodes = zx
906        .nodes()
907        .iter()
908        .filter(|node| node.kind == NodeKind::T || node.is_input_port(zx))
909        .collect::<Vec<_>>();
910    nodes.sort_unstable_by_key(|node| node.pos.to_array());
911    let inputs = nodes
912        .iter()
913        .filter(|node| node.is_input_port(zx))
914        .map(|node| node.id)
915        .collect();
916    let prepared = nodes
917        .iter()
918        .filter(|node| node.kind == NodeKind::T)
919        .map(|node| node.id)
920        .collect();
921    (inputs, prepared)
922}
923
924/// A partial-output query must also preserve every other live output.
925fn protected_boundary_columns(zx: &ZXGraph, prepared: &[usize], outputs: &[usize]) -> Vec<usize> {
926    let mut columns = prepared
927        .iter()
928        .copied()
929        .chain(
930            zx.nodes()
931                .iter()
932                .filter(|node| node.is_output_port(zx) && !outputs.contains(&node.id))
933                .map(|node| node.id),
934        )
935        .collect::<Vec<_>>();
936    columns.sort_unstable_by_key(|&column| zx.nodes()[column].pos.to_array());
937    columns.dedup();
938    columns
939}
940
941struct FillWork {
942    indices: Vec<usize>,
943    depth: usize,
944    basis: StabilizerBasis,
945    readout_coeffs: Vec<CoeffVec>,
946    remaining: Vec<SelectiveConstraint>,
947}
948
949fn fill_prefix_states(
950    source: &RuntimeStabilizerBasis,
951    assignments: &[Vec<(IVec3, PauliBasis)>],
952    remaining: Vec<SelectiveConstraint>,
953    states: &mut [Option<(StabilizerBasis, Vec<CoeffVec>)>],
954) -> Option<()> {
955    let mut stack = vec![FillWork {
956        indices: (0..assignments.len()).collect(),
957        depth: 0,
958        basis: source.basis.clone(),
959        readout_coeffs: source.readout_coeffs.clone(),
960        remaining,
961    }];
962    while let Some(mut work) = stack.pop() {
963        loop {
964            if work.depth == assignments[work.indices[0]].len() {
965                let last = work.indices.pop()?;
966                for &index in &work.indices {
967                    states[index] = Some((work.basis.clone(), work.readout_coeffs.clone()));
968                }
969                states[last] = Some((work.basis, work.readout_coeffs));
970                break;
971            }
972            let pos = assignments[work.indices[0]][work.depth].0;
973            let kind = source.selective_kinds[&pos];
974            let col = source
975                .zx_graph
976                .node_at(pos)
977                .expect("validated selective has a ZX node")
978                .id;
979            work.remaining.retain(|constraint| constraint.pos != pos);
980            let mut groups: Vec<(PauliBasis, Vec<usize>)> = Vec::new();
981            for &index in &work.indices {
982                let chosen = assignments[index][work.depth].1;
983                if let Some((_, group)) = groups.iter_mut().find(|(value, _)| *value == chosen) {
984                    group.push(index);
985                } else {
986                    groups.push((chosen, vec![index]));
987                }
988            }
989            if groups.len() == 1 {
990                let (chosen, indices) = groups.pop()?;
991                (work.basis, work.readout_coeffs) = evolve_selective_basis(
992                    &work.basis,
993                    &work.readout_coeffs,
994                    &[(col, Pauli::from(chosen))],
995                    &work.remaining,
996                    pos,
997                    kind,
998                )
999                .ok()?;
1000                work.indices = indices;
1001                work.depth += 1;
1002                continue;
1003            }
1004            for (chosen, indices) in groups {
1005                let (basis, readout_coeffs) = evolve_selective_basis(
1006                    &work.basis,
1007                    &work.readout_coeffs,
1008                    &[(col, Pauli::from(chosen))],
1009                    &work.remaining,
1010                    pos,
1011                    kind,
1012                )
1013                .ok()?;
1014                stack.push(FillWork {
1015                    indices,
1016                    depth: work.depth + 1,
1017                    basis,
1018                    readout_coeffs,
1019                    remaining: work.remaining.clone(),
1020                });
1021            }
1022            break;
1023        }
1024    }
1025    Some(())
1026}
1027
1028fn evolve_selective_basis(
1029    basis: &StabilizerBasis,
1030    readout_coeffs: &[CoeffVec],
1031    constraints: &[(usize, Pauli)],
1032    remaining_constraints: &[SelectiveConstraint],
1033    pos: IVec3,
1034    kind: SelectiveKind,
1035) -> Result<(StabilizerBasis, Vec<CoeffVec>), RuntimeBasisError> {
1036    let (basis, coeffs) = if basis
1037        .kinds
1038        .iter()
1039        .any(|row_kind| row_kind.fixes_selective(pos))
1040    {
1041        match basis.apply_tagged_selective_fill(constraints, pos, kind) {
1042            Ok(transition) => transition,
1043            Err(StabilizerError::SelectiveSupportUnsatisfiable { .. }) => {
1044                basis.apply_generic_selective_fill(constraints, remaining_constraints)?
1045            }
1046            Err(err) => return Err(err.into()),
1047        }
1048    } else {
1049        basis.apply_generic_selective_fill(constraints, remaining_constraints)?
1050    };
1051    Ok((basis, compose_coeffs(&coeffs, readout_coeffs)))
1052}
1053
1054fn initial_readout_coeffs(kinds: &[StabilizerRowKind], public_len: usize) -> Vec<CoeffVec> {
1055    let width = kinds.len();
1056    kinds
1057        .iter()
1058        .enumerate()
1059        .map(|(index, kind)| {
1060            if index < public_len && kind.is_readout() {
1061                CoeffVec::singleton(index, width)
1062            } else {
1063                CoeffVec::zeros(width)
1064            }
1065        })
1066        .collect()
1067}
1068
1069fn compose_coeffs(transform: &[CoeffVec], source: &[CoeffVec]) -> Vec<CoeffVec> {
1070    let width = source.first().map_or(0, CoeffVec::len);
1071    transform
1072        .iter()
1073        .map(|combination| {
1074            let mut composed = CoeffVec::zeros(width);
1075            for source_index in combination.iter_ones() {
1076                composed.xor_assign(&source[source_index]);
1077            }
1078            composed
1079        })
1080        .collect()
1081}
1082
1083fn selective_kinds_of(zx_graph: &ZXGraph) -> HashMap<IVec3, SelectiveKind> {
1084    collect_selective_constraints(zx_graph)
1085        .into_iter()
1086        .map(|constraint| (constraint.pos, constraint.kind))
1087        .collect()
1088}
1089
1090/// Derivation rows projected past the graph's last layer, plus requested output
1091/// columns in caller order.
1092struct ProjectedOutputRows {
1093    rows: Vec<PauliString>,
1094    coeffs: Vec<CoeffVec>,
1095    output_cols: Vec<usize>,
1096}
1097
1098fn materialize_derived_surface(
1099    zx_graph: &ZXGraph,
1100    row: PauliString,
1101    coeff: CoeffVec,
1102) -> DerivedSurface {
1103    DerivedSurface {
1104        readout_ordinals: coeff.to_indices(),
1105        stabilizer: zx_graph.materialize_stabilizer(row),
1106    }
1107}
1108
1109fn requested_output_columns(
1110    zx_graph: &ZXGraph,
1111    outputs: &[IVec3],
1112) -> Result<Vec<usize>, RuntimeBasisError> {
1113    outputs
1114        .iter()
1115        .copied()
1116        .map(|output| {
1117            let node_id = zx_graph
1118                .node_at(output)
1119                .filter(|node| node.is_output_port(zx_graph))
1120                .map(|node| node.id)
1121                .ok_or(RuntimeBasisError::UnknownOutput(output))?;
1122            Ok(node_id)
1123        })
1124        .collect()
1125}
1126
1127#[cfg(test)]
1128mod tests {
1129    use glam::ivec3;
1130
1131    use crate::MeasurementObservable;
1132    use crate::{BlockGraph, GalleryItem, Pauli, PauliBasis, PauliString};
1133
1134    use super::{CoeffVec, RuntimeBasisError, RuntimeStabilizerBasis};
1135
1136    #[test]
1137    fn source_readouts_preserve_the_input_instrument_under_a_public_rebase() {
1138        let mut generators = GalleryItem::CCZInjectedAnd
1139            .build()
1140            .materialize_root_graph()
1141            .expect("gallery flat projection")
1142            .stabilizers()
1143            .unwrap();
1144        let source = RuntimeStabilizerBasis::for_source_readouts(&generators).unwrap();
1145        let expected = source
1146            .derive_causal_measurement_surface("m1", i64::MAX)
1147            .unwrap();
1148        let named = generators
1149            .generators
1150            .iter()
1151            .position(|g| g.measurement_name() == Some("m1"))
1152            .unwrap();
1153        let logical = generators
1154            .generators
1155            .iter()
1156            .position(|g| matches!(g.kind, crate::StabilizerRowKind::Logical))
1157            .unwrap();
1158        let mixed = &generators.generators[named].stabilizer.paulis
1159            ^ &generators.generators[logical].stabilizer.paulis;
1160        generators.generators[named].stabilizer = generators.zx_graph.materialize_stabilizer(mixed);
1161        let rebased = RuntimeStabilizerBasis::for_source_readouts(&generators)
1162            .unwrap()
1163            .derive_causal_measurement_surface("m1", i64::MAX)
1164            .unwrap();
1165        assert_eq!(
1166            rebased.stabilizer.port_stabilizer,
1167            expected.stabilizer.port_stabilizer
1168        );
1169        assert_eq!(rebased.stabilizer.sign, expected.stabilizer.sign);
1170    }
1171
1172    #[test]
1173    fn joint_fill_provenance_never_names_internal_rows() {
1174        let graph = GalleryItem::ToffoliFromAndDelayedCZ
1175            .build()
1176            .materialize_root_graph()
1177            .expect("gallery flat projection");
1178        let stabilizers = graph.stabilizers().unwrap();
1179        let public_rows = stabilizers.generators.len();
1180        assert!(!stabilizers.auxiliary_rows().is_empty());
1181
1182        let mut basis =
1183            RuntimeStabilizerBasis::from_generators(&stabilizers).with_t_nodes_as_ports();
1184        assert!(
1185            basis.readout_coeffs[public_rows..]
1186                .iter()
1187                .all(CoeffVec::is_zero)
1188        );
1189
1190        let resolves = graph
1191            .actions()
1192            .iter()
1193            .filter_map(|action| match action {
1194                crate::Action::Resolve { target, .. } => Some(*target),
1195                _ => None,
1196            })
1197            .collect::<Vec<_>>();
1198        for pos in resolves {
1199            let kind = basis.selective_kinds[&pos];
1200            basis = basis
1201                .apply_selective_fill(pos, kind.pauli_if_false())
1202                .unwrap();
1203        }
1204
1205        let outputs = basis.zx_graph.output_ports();
1206        for correction in basis.derive_output_correction_rows(&outputs).unwrap() {
1207            assert!(
1208                correction
1209                    .readout_ordinals
1210                    .iter()
1211                    .all(|&ordinal| ordinal < public_rows),
1212                "derived readout provenance exposed an internal row"
1213            );
1214        }
1215    }
1216
1217    #[test]
1218    fn batched_fills_match_sequential_output_surfaces() {
1219        for entry in [
1220            GalleryItem::CCZInjectedAnd,
1221            GalleryItem::CCZInjectedMaj,
1222            GalleryItem::ToffoliFromAndDelayedCZ,
1223            GalleryItem::T,
1224        ] {
1225            let graph = entry
1226                .build()
1227                .materialize_root_graph()
1228                .expect("gallery flat projection");
1229            let stabilizers = graph.stabilizers().unwrap();
1230            let base =
1231                RuntimeStabilizerBasis::from_generators(&stabilizers).with_t_nodes_as_ports();
1232            assert!(base.input_columns.iter().all(|&column| {
1233                stabilizers.zx_graph.nodes()[column].is_input_port(&stabilizers.zx_graph)
1234            }));
1235            assert!(base.prepared_columns.iter().all(|&column| {
1236                stabilizers.zx_graph.nodes()[column].kind == crate::NodeKind::T
1237                    && base.zx_graph.nodes()[column].kind == crate::NodeKind::Port
1238                    && !base.input_columns.contains(&column)
1239            }));
1240            let sites = graph
1241                .actions()
1242                .iter()
1243                .filter_map(|action| match action {
1244                    crate::Action::Resolve { target, .. } => Some(*target),
1245                    _ => None,
1246                })
1247                .collect::<Vec<_>>();
1248            let outputs = stabilizers.zx_graph.output_ports();
1249
1250            let assignments = stabilizers
1251                .zx_graph
1252                .action_graph()
1253                .resolve_value_domain(&sites)
1254                .unwrap()
1255                .values()
1256                .iter()
1257                .map(|bits| {
1258                    sites
1259                        .iter()
1260                        .zip(bits)
1261                        .map(|(&pos, &bit)| {
1262                            let kind = base.selective_kinds[&pos];
1263                            (
1264                                pos,
1265                                if bit {
1266                                    kind.pauli_if_true()
1267                                } else {
1268                                    kind.pauli_if_false()
1269                                },
1270                            )
1271                        })
1272                        .collect::<Vec<_>>()
1273                })
1274                .collect::<Vec<_>>();
1275            let forked = base.apply_selective_fill_assignments(&assignments).unwrap();
1276            for (fills, forked) in assignments.iter().zip(forked) {
1277                let batched = base.apply_selective_fills(fills).unwrap();
1278                let sequential = fills.iter().try_fold(base.clone(), |basis, &(pos, axis)| {
1279                    basis.apply_selective_fill(pos, axis)
1280                });
1281
1282                let sequential = sequential
1283                    .unwrap()
1284                    .derive_output_correction_surfaces(&outputs)
1285                    .unwrap();
1286                assert_eq!(
1287                    batched.derive_output_correction_surfaces(&outputs).unwrap(),
1288                    sequential,
1289                    "borrowed {entry:?} assignment {fills:?}"
1290                );
1291                assert_eq!(
1292                    batched.into_output_correction_surfaces(&outputs).unwrap(),
1293                    sequential,
1294                    "consuming {entry:?} assignment {fills:?}"
1295                );
1296                assert_eq!(
1297                    forked.into_output_correction_surfaces(&outputs).unwrap(),
1298                    sequential,
1299                    "forked {entry:?} assignment {fills:?}"
1300                );
1301            }
1302
1303            let mut reordered = assignments[..2].to_vec();
1304            reordered[0].reverse();
1305            let fallback = base.apply_selective_fill_assignments(&reordered).unwrap();
1306            for (fills, fallback) in reordered.iter().zip(fallback) {
1307                let expected = base
1308                    .apply_selective_fills(fills)
1309                    .unwrap()
1310                    .into_output_correction_surfaces(&outputs)
1311                    .unwrap();
1312                assert_eq!(
1313                    fallback.into_output_correction_surfaces(&outputs).unwrap(),
1314                    expected,
1315                    "fallback {entry:?} assignment {fills:?}"
1316                );
1317            }
1318
1319            let repeated = vec![assignments[0].clone(); 3];
1320            let expected = base
1321                .apply_selective_fills(&repeated[0])
1322                .unwrap()
1323                .into_output_correction_surfaces(&outputs)
1324                .unwrap();
1325            for filled in base.apply_selective_fill_assignments(&repeated).unwrap() {
1326                assert_eq!(
1327                    filled.into_output_correction_surfaces(&outputs).unwrap(),
1328                    expected
1329                );
1330            }
1331        }
1332    }
1333
1334    #[test]
1335    fn uma_readout_prefers_output_free_surface_after_z_fills() {
1336        let stabilizers = GalleryItem::UMA
1337            .build()
1338            .materialize_root_graph()
1339            .expect("gallery flat projection")
1340            .stabilizers()
1341            .unwrap();
1342        stabilizers
1343            .validate_measurements_close_before_outputs()
1344            .unwrap();
1345        let basis = RuntimeStabilizerBasis::from_generators(&stabilizers)
1346            .apply_selective_fills(&[
1347                (ivec3(2, 1, 2), PauliBasis::Z),
1348                (ivec3(2, 2, 2), PauliBasis::Z),
1349            ])
1350            .unwrap();
1351        let derived = basis.derive_measurement_surface("m_ikprime", 2).unwrap();
1352        assert!(
1353            !derived
1354                .stabilizer
1355                .port_stabilizer
1356                .contains_key(&ivec3(2, 0, 1))
1357        );
1358        let ordinal = stabilizers
1359            .generators
1360            .iter()
1361            .position(|row| row.measurement_name() == Some("m_ikprime"))
1362            .unwrap();
1363        assert_eq!(derived.readout_ordinals, vec![ordinal]);
1364    }
1365
1366    #[test]
1367    fn measurement_derivation_preserves_observable_and_readout_identity() {
1368        for (kind, expected) in [("XZX", Pauli::X), ("ZXZ", Pauli::Z)] {
1369            let graph = BlockGraph::from_blog_text(&format!(
1370                "BLOG 1.0\n0: Port [0,0,-1]\n1: {kind} [0,0,0]\n2: Port [0,0,1]\n0 -> +Z\n1 -> +Z\nm = measure 1\n"
1371            )).unwrap();
1372            let stabilizers = graph.stabilizers().unwrap();
1373            let ordinal = stabilizers
1374                .generators
1375                .iter()
1376                .position(|generator| generator.measurement_name() == Some("m"))
1377                .unwrap();
1378            let basis = RuntimeStabilizerBasis::from_generators(&stabilizers);
1379            let derived = basis.derive_measurement_surface("m", 2).unwrap();
1380            assert_eq!(derived.stabilizer.interior_nodes[&ivec3(0, 0, 0)], expected);
1381            assert_eq!(derived.readout_ordinals, vec![ordinal]);
1382        }
1383    }
1384
1385    #[test]
1386    fn runtime_basis_uses_measurement_actions() {
1387        let graph = GalleryItem::T
1388            .build()
1389            .materialize_root_graph()
1390            .expect("gallery flat projection");
1391        let zx = crate::ZXGraph::try_from(&graph).unwrap();
1392
1393        let basis = RuntimeStabilizerBasis::from_zx_graph(zx).unwrap();
1394        let derived = basis.derive_measurement_surface("mzz", 2).unwrap();
1395
1396        assert!(!derived.readout_ordinals.is_empty());
1397    }
1398
1399    #[test]
1400    fn apply_selective_fill_refreshes_filled_selective_measurement_metadata() {
1401        let mut graph = BlockGraph::from_blog_text(
1402            "BLOG 1.0\n\n  0: Port [0,0,0]\n  1: XZ [0,0,1]\n  2: ZXZ [1,0,0]\n  [0,0,0] -> +Z\n",
1403        )
1404        .unwrap();
1405        graph
1406            .set_actions_lenient(vec![
1407                crate::Action::Measure {
1408                    target: crate::MeasureTarget::Node(ivec3(0, 0, 1)),
1409                    name: "m".into(),
1410                },
1411                crate::Action::Measure {
1412                    target: crate::MeasureTarget::Node(ivec3(1, 0, 0)),
1413                    name: "m2".into(),
1414                },
1415                crate::Action::Resolve {
1416                    target: ivec3(0, 0, 1),
1417                    condition: crate::Expr::Var("m2".into()),
1418                },
1419            ])
1420            .unwrap();
1421        let zx = crate::ZXGraph::from_block_graph_for_analysis(&graph).unwrap();
1422        let basis = RuntimeStabilizerBasis::from_zx_graph(zx).unwrap();
1423
1424        let filled = basis
1425            .apply_selective_fill(ivec3(0, 0, 1), PauliBasis::Z)
1426            .unwrap();
1427        let zx = &filled.zx_graph;
1428
1429        assert_eq!(
1430            zx.action_graph().node_by_ordinal(0).unwrap().measurement,
1431            Some(MeasurementObservable::Concrete(PauliBasis::Z))
1432        );
1433        let ordinal = basis.measurement_ordinals["m"];
1434        let derived = filled.derive_measurement_surface("m", 2).unwrap();
1435        assert_eq!(derived.stabilizer.interior_nodes[&ivec3(0, 0, 1)], Pauli::Z);
1436        assert_eq!(derived.readout_ordinals, vec![ordinal]);
1437    }
1438
1439    #[test]
1440    fn apply_selective_fill_rejects_non_clifford_graphs() {
1441        let x_basis_graph = GalleryItem::T
1442            .build()
1443            .materialize_root_graph()
1444            .expect("gallery flat projection");
1445        let x_basis = RuntimeStabilizerBasis::from_block_graph(&x_basis_graph).unwrap();
1446        let x_err = x_basis
1447            .apply_selective_fill(ivec3(1, 0, 2), crate::PauliBasis::X)
1448            .unwrap_err();
1449        assert!(matches!(
1450            x_err,
1451            RuntimeBasisError::NonCliffordFillUnsupported
1452        ));
1453
1454        let z_basis_graph = GalleryItem::TWithPreparedY
1455            .build()
1456            .materialize_root_graph()
1457            .expect("gallery flat projection");
1458        let z_basis = RuntimeStabilizerBasis::from_block_graph(&z_basis_graph).unwrap();
1459        let z_err = z_basis
1460            .apply_selective_fill(ivec3(2, 1, 2), crate::PauliBasis::Z)
1461            .unwrap_err();
1462        assert!(matches!(
1463            z_err,
1464            RuntimeBasisError::NonCliffordFillUnsupported
1465        ));
1466    }
1467
1468    #[test]
1469    fn selective_future_pipe_topology_is_rejected() {
1470        let graph = BlockGraph::from_blog_text(
1471            "BLOG 1.0\n\n  0: XZ [0,0,1]\n  1: ZXZ [0,0,2]\n  2: Port [0,0,3]\n  [0,0,1] -> +Z\n  [0,0,2] -> +Z\n",
1472        )
1473        .unwrap();
1474        RuntimeStabilizerBasis::from_block_graph(&graph).unwrap_err();
1475    }
1476
1477    #[test]
1478    fn tagged_selective_fill_preserves_remaining_selective_fixing_rows() {
1479        let filled_pos = ivec3(0, 0, 0);
1480        let remaining_pos = ivec3(1, 0, 0);
1481        let remaining_col = 1;
1482        let remaining_forbidden = Pauli::Z;
1483        let generators = vec![
1484            PauliString::try_from("Z_").unwrap(),
1485            PauliString::try_from("_Z").unwrap(),
1486            PauliString::try_from("Y_").unwrap(),
1487            PauliString::try_from("_X").unwrap(),
1488        ];
1489        let row_kinds = vec![
1490            super::super::stabilizer::StabilizerRowKind::SelectiveFixing {
1491                targets: vec![super::super::stabilizer::SelectiveFixingTarget {
1492                    pos: filled_pos,
1493                    forbidden: Pauli::Z,
1494                }],
1495            },
1496            super::super::stabilizer::StabilizerRowKind::SelectiveFixing {
1497                targets: vec![super::super::stabilizer::SelectiveFixingTarget {
1498                    pos: remaining_pos,
1499                    forbidden: remaining_forbidden,
1500                }],
1501            },
1502            super::super::stabilizer::StabilizerRowKind::Logical,
1503            super::super::stabilizer::StabilizerRowKind::Logical,
1504        ];
1505
1506        let basis = super::super::stabilizer::StabilizerBasis::new(generators, row_kinds);
1507        let (result, _) = basis
1508            .apply_tagged_selective_fill(&[(0, Pauli::X)], filled_pos, crate::SelectiveKind::XY)
1509            .unwrap();
1510
1511        assert!(
1512            result
1513                .kinds
1514                .iter()
1515                .all(|kind| !kind.fixes_selective(filled_pos))
1516        );
1517        assert!(result.rows.iter().zip(&result.kinds).any(|(row, kind)| {
1518            kind.fixes_selective(remaining_pos) && row.get(remaining_col) == remaining_forbidden
1519        }));
1520    }
1521
1522    #[test]
1523    fn output_correction_rows_cover_requested_outputs() {
1524        let graph = GalleryItem::CNOT
1525            .build()
1526            .materialize_root_graph()
1527            .expect("gallery flat projection");
1528        let basis = RuntimeStabilizerBasis::from_block_graph(&graph).unwrap();
1529        let outputs = [ivec3(0, 0, 3), ivec3(1, 1, 3)];
1530        let joints = basis.derive_output_correction_rows(&outputs).unwrap();
1531
1532        assert!(!joints.is_empty());
1533        for output in outputs {
1534            assert!(joints.iter().any(|joint| {
1535                joint
1536                    .output_support
1537                    .iter()
1538                    .any(|&(supported, _)| supported == output)
1539            }));
1540        }
1541        assert!(
1542            joints
1543                .iter()
1544                .all(|joint| !joint.readout_ordinals.is_empty())
1545        );
1546    }
1547
1548    #[test]
1549    fn output_projection_past_i32_max_layer_is_representable() {
1550        let graph = GalleryItem::CNOT
1551            .build()
1552            .materialize_root_graph()
1553            .expect("gallery flat projection");
1554        let max_z = *graph.spans().unwrap().2.end();
1555        let shifted = graph
1556            .shift_positions(ivec3(0, 0, i32::MAX - max_z))
1557            .unwrap();
1558        let zx = crate::ZXGraph::try_from(&shifted).unwrap();
1559        assert_eq!(
1560            zx.nodes().iter().map(|node| node.pos.z).max(),
1561            Some(i32::MAX)
1562        );
1563        let outputs = zx.output_ports();
1564        let basis = RuntimeStabilizerBasis::from_zx_graph(zx).unwrap();
1565
1566        assert!(
1567            !basis
1568                .derive_output_correction_rows(&outputs)
1569                .unwrap()
1570                .is_empty()
1571        );
1572    }
1573
1574    #[test]
1575    fn output_correction_rows_report_unknown_output() {
1576        let graph = GalleryItem::T
1577            .build()
1578            .materialize_root_graph()
1579            .expect("gallery flat projection");
1580        let basis = RuntimeStabilizerBasis::from_block_graph(&graph).unwrap();
1581        let err = basis
1582            .derive_output_correction_rows(&[ivec3(99, 99, 99)])
1583            .expect_err("missing output should error");
1584
1585        assert!(matches!(err, RuntimeBasisError::UnknownOutput(pos) if pos == ivec3(99, 99, 99)));
1586    }
1587
1588    #[test]
1589    fn ccz_factory_runtime_basis_derives_mx1357_surface() {
1590        let graph = GalleryItem::CCZFactoryWithTels
1591            .build()
1592            .materialize_root_graph()
1593            .expect("gallery flat projection");
1594        let basis = RuntimeStabilizerBasis::from_block_graph(&graph).unwrap();
1595        let derived = basis.derive_measurement_surface("mx1357", 7).unwrap();
1596
1597        assert!(!derived.readout_ordinals.is_empty());
1598        assert!(
1599            derived
1600                .stabilizer
1601                .interior_nodes
1602                .contains_key(&ivec3(2, 0, 6))
1603        );
1604    }
1605}