bloq_graph/zx/symbolic_basis.rs
1//! Branch-parametric (symbolic) stabilizer basis.
2//!
3//! Filling a selective node at site `c` acts on the basis through
4//! [`StabilizerBasis::apply_tagged_selective_fill`](super::stabilizer::StabilizerBasis::apply_tagged_selective_fill),
5//! which is the pointwise ground truth this module models. That transition uses
6//! the site's diagonalized fixing row `F_c` as the pivot: it XORs `F_c` into
7//! every row whose support at the site column *anticommutes* with the chosen
8//! fill axis, then removes `F_c` from the basis.
9//!
10//! # The affine model
11//!
12//! Two properties of the canonical basis make the whole evolution under *any*
13//! fill combination affine in the fill choices, with per-`(row, site)` behavior
14//! fixed at compile time:
15//!
16//! * **Fixing rows are pairwise diagonalized** — `F_c` carries the forbidden
17//! Pauli on its own selective column and identity on every *other* selective
18//! column. So filling site `d` never changes any row's support at column `c`,
19//! and never disturbs `F_c` itself (which is consumed only when site `c` is
20//! filled). A row's trigger status at site `c` therefore depends only on its
21//! *initial* support at column `c`, independent of the fill order.
22//!
23//! * Consequently every surviving (non-fixing) row evolves as
24//!
25//! ```text
26//! concrete(row) = base ⊕ ⊕ { F_c : site c's chosen axis triggers this row }
27//! ```
28//!
29//! an XOR of a compile-time-fixed `base` and a subset of the fixing rows
30//! selected by the fill assignment.
31//!
32//! # Arm polarity
33//!
34//! The tagged fill XORs `F_c` into a row iff the row's support `p` at the site
35//! column satisfies `p != I && p != chosen_axis` (see
36//! `apply_tagged_selective_fill`). Because fixing rows carry the forbidden Pauli
37//! and tracking rows are pivoted free of it during canonicalization, a surviving
38//! row's support at a site is normally one of `{I, axis0, axis1}` (the two
39//! allowed axes of the site's [`SelectiveKind`]):
40//!
41//! * support `I` → never triggered;
42//! * support `axis0` → triggered only by choosing `axis1`;
43//! * support `axis1` → triggered only by choosing `axis0`.
44//!
45//! We store the support Pauli itself, so evaluation mirrors the engine's
46//! condition verbatim (`support != chosen`). This also handles the degenerate
47//! case where a row carries the forbidden Pauli (`support == forbidden`): it then
48//! triggers for *both* axes — still affine, just unconditional.
49//!
50//! # Eligibility
51//!
52//! The model covers exactly the tagged-fill path. A graph is eligible iff every
53//! selective site owns a fixing row that (a) carries the forbidden Pauli on its
54//! own column and (b) is diagonalized against every other selective column, with
55//! a one-to-one correspondence between fixing rows and sites. Any violation
56//! yields a [`SymbolicBasisError`] naming the offending site — the
57//! representational-fallback signal, not a panic. At runtime such a graph takes
58//! the generic Gaussian path
59//! ([`apply_generic_selective_fill`](super::stabilizer::StabilizerBasis::apply_generic_selective_fill)),
60//! which this module deliberately does not represent.
61//!
62//! Partial evaluation (holding some sites symbolic while fixing others) is a
63//! trivial extension of the same structure but is intentionally not built here:
64//! no current consumer needs it.
65
66use std::collections::BTreeSet;
67#[cfg(test)]
68use std::collections::HashMap;
69
70use glam::IVec3;
71
72#[cfg(test)]
73use bloq_utils::PauliBasis;
74use bloq_utils::boolean::{BooleanLimits, BooleanResourceError};
75use bloq_utils::{Pauli, PauliString};
76
77#[cfg(test)]
78use super::stabilizer::StabilizerBasis;
79use super::stabilizer::{StabilizerRowKind, collect_selective_constraints};
80use super::{StabilizerGenerators, ZXGraph};
81use crate::SelectiveKind;
82
83/// A selective site's raw descriptor: where it is, its kind, and the column and
84/// forbidden Pauli its fixing row must own. The construction input, before a
85/// pivot is resolved.
86struct SiteSpec {
87 pos: IVec3,
88 kind: SelectiveKind,
89 col: usize,
90 forbidden: Pauli,
91}
92
93/// One selective site and the fixing row that resolves it.
94#[derive(Debug, Clone)]
95pub struct SymbolicSite {
96 /// Position of the selective node.
97 pub pos: IVec3,
98 /// The node's kind, defining its two allowed fill axes.
99 pub kind: SelectiveKind,
100 /// Column (node id) the site occupies in the Pauli strings.
101 pub(crate) col: usize,
102 /// The fixing row `F_c`, XORed into a row when this site triggers it.
103 pub(crate) fixing_row: PauliString,
104 /// Ordinal of `F_c` in the initial basis, for provenance composition.
105 #[cfg(test)]
106 pub(crate) fixing_ordinal: usize,
107}
108
109/// A gated contribution of one site's fixing row to one surviving row.
110#[derive(Debug, Clone)]
111pub(crate) struct SymbolicDelta {
112 /// Index of the triggering site in [`SymbolicStabilizerBasis::sites`].
113 pub(crate) site: usize,
114 /// The surviving row's support Pauli at the site column (never `I`). The
115 /// delta triggers for a chosen axis `a` iff `support != Pauli::from(a)`.
116 pub(crate) support: Pauli,
117}
118
119/// One surviving (non-fixing) basis row as an affine function of the fills.
120#[derive(Debug, Clone)]
121pub(crate) struct SymbolicRow {
122 /// The row's value when no site triggers it.
123 pub(crate) base: PauliString,
124 /// The role the row plays, preserved from the initial basis.
125 pub(crate) kind: StabilizerRowKind,
126 /// Ordinal of this row in the initial basis, seeding its provenance.
127 #[cfg(test)]
128 pub(crate) base_ordinal: usize,
129 /// Per-site gated fixing-row contributions (only sites the row touches).
130 pub(crate) deltas: Vec<SymbolicDelta>,
131}
132
133/// The surviving basis represented as an affine function of the fill assignment.
134#[derive(Debug, Clone)]
135pub struct SymbolicStabilizerBasis {
136 sites: Vec<SymbolicSite>,
137 rows: Vec<SymbolicRow>,
138}
139
140/// The concrete basis for one fill assignment, plus per-row provenance.
141#[cfg(test)]
142#[derive(Debug, Clone)]
143pub(crate) struct SymbolicEvaluation {
144 /// The surviving rows and their kinds, in survivor order.
145 pub(crate) basis: StabilizerBasis,
146 /// For each surviving row, the initial-basis ordinals whose product it is.
147 pub(crate) combinations: Vec<BTreeSet<usize>>,
148}
149
150/// A symbolic basis cannot represent a selective site's fixing structure.
151/// This reports an unsupported symbolic model, not an invalid source graph.
152/// Callers can fall back to runtime-basis replay.
153#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
154#[non_exhaustive]
155pub enum SymbolicBasisError {
156 /// A selective site has no symbolic fixing representation.
157 #[error("selective site {0} is unsupported by the symbolic basis model")]
158 UnsupportedSite(IVec3),
159}
160
161impl SymbolicStabilizerBasis {
162 /// The selective sites, in the order fills are keyed against.
163 pub fn sites(&self) -> &[SymbolicSite] {
164 &self.sites
165 }
166
167 pub(crate) fn measurements_close_before_outputs(
168 &self,
169 zx: &ZXGraph,
170 limits: BooleanLimits,
171 ) -> Result<bool, BooleanResourceError> {
172 let components = zx
173 .nodes()
174 .iter()
175 .filter(|node| node.is_output_port(zx))
176 .flat_map(|node| {
177 let axes: &[_] = if node.role == crate::PortRole::Multiplex {
178 &[Pauli::X]
179 } else {
180 &[Pauli::X, Pauli::Z]
181 };
182 axes.iter().map(move |&axis| (node.id, axis))
183 });
184 let components = components.collect::<Vec<_>>();
185
186 for row in self
187 .rows
188 .iter()
189 .filter(|row| matches!(row.kind, StabilizerRowKind::Measurement { .. }))
190 {
191 for &(col, axis) in &components {
192 if !self.component_closes(row, zx.action_graph(), col, axis, limits)? {
193 return Ok(false);
194 }
195 }
196 }
197 Ok(true)
198 }
199
200 fn component_closes(
201 &self,
202 row: &SymbolicRow,
203 actions: &crate::ActionDag,
204 col: usize,
205 axis: Pauli,
206 limits: BooleanLimits,
207 ) -> Result<bool, BooleanResourceError> {
208 let mut expected = row.base.get(col) & axis;
209 let mut targets = Vec::new();
210 for delta in &row.deltas {
211 let site = &self.sites[delta.site];
212 if !(site.fixing_row.get(col) & axis) {
213 continue;
214 }
215 if delta.support == Pauli::from(site.kind.pauli_if_true()) {
216 expected ^= true;
217 targets.push(site.pos);
218 } else if delta.support == Pauli::from(site.kind.pauli_if_false()) {
219 targets.push(site.pos);
220 } else {
221 expected ^= true;
222 }
223 }
224 actions.resolve_xor_is_with_limits(&targets, expected, limits)
225 }
226
227 /// Builds the symbolic basis from already-canonicalized generators, reusing
228 /// their rows and kinds verbatim so ordinals stay identity-stable with a
229 /// [`RuntimeStabilizerBasis::from_generators`](super::runtime_basis::RuntimeStabilizerBasis::from_generators)
230 /// replay.
231 ///
232 /// # Errors
233 ///
234 /// Returns a [`SymbolicBasisError`] naming the first site whose fixing
235 /// structure does not fit the affine model (see the module docs).
236 pub fn from_generators(stabilizers: &StabilizerGenerators) -> Result<Self, SymbolicBasisError> {
237 let rows: Vec<PauliString> = stabilizers
238 .generators
239 .iter()
240 .map(|generator| generator.stabilizer.paulis.clone())
241 .collect();
242 let kinds: Vec<StabilizerRowKind> = stabilizers
243 .generators
244 .iter()
245 .map(|generator| generator.kind.clone())
246 .collect();
247
248 // Site descriptors carry the column and forbidden Pauli the fixing row
249 // must own; `collect_selective_constraints` orders them by node scan
250 // order, which we adopt as the canonical site order.
251 let specs: Vec<SiteSpec> = collect_selective_constraints(&stabilizers.zx_graph)
252 .into_iter()
253 .map(|constraint| SiteSpec {
254 pos: constraint.pos,
255 kind: constraint.kind,
256 col: constraint.col,
257 forbidden: constraint.forbidden,
258 })
259 .collect();
260
261 Self::from_raw(rows, kinds, &specs)
262 }
263
264 /// Core construction from raw rows, kinds, and site descriptors. Split out
265 /// of [`from_generators`](Self::from_generators) so the eligibility rules can
266 /// be exercised on hand-built bases without a backing [`ZXGraph`].
267 fn from_raw(
268 rows: Vec<PauliString>,
269 kinds: Vec<StabilizerRowKind>,
270 specs: &[SiteSpec],
271 ) -> Result<Self, SymbolicBasisError> {
272 // Resolve each site to its pivot the same way the engine does — the
273 // first row that fixes the site — and validate the fixing structure.
274 let mut sites = Vec::with_capacity(specs.len());
275 let mut pivot_indices = Vec::with_capacity(specs.len());
276 for spec in specs {
277 let pivot = kinds
278 .iter()
279 .position(|kind| kind.fixes_selective(spec.pos))
280 .ok_or(SymbolicBasisError::UnsupportedSite(spec.pos))?;
281
282 // The pivot must carry the forbidden Pauli on its own column;
283 // otherwise the tagged fill rejects the axis equal to that support.
284 if rows[pivot].get(spec.col) != spec.forbidden {
285 return Err(SymbolicBasisError::UnsupportedSite(spec.pos));
286 }
287
288 // Diagonalization: identity on every other selective column, so
289 // filling this site cannot alter another site's trigger status.
290 for other in specs {
291 if other.pos != spec.pos && rows[pivot].get(other.col) != Pauli::I {
292 return Err(SymbolicBasisError::UnsupportedSite(spec.pos));
293 }
294 }
295
296 sites.push(SymbolicSite {
297 pos: spec.pos,
298 kind: spec.kind,
299 col: spec.col,
300 fixing_row: rows[pivot].clone(),
301 #[cfg(test)]
302 fixing_ordinal: pivot,
303 });
304 pivot_indices.push(pivot);
305 }
306
307 // Every fixing row must be exactly one site's pivot: distinct pivots and
308 // no leftover fixing rows. A leftover fixing row would survive our
309 // filter but never be removed by a replay, diverging the two.
310 let pivot_set: BTreeSet<usize> = pivot_indices.iter().copied().collect();
311 if pivot_set.len() != pivot_indices.len() {
312 // Two sites share a pivot; blame the second occurrence's site.
313 let mut seen = BTreeSet::new();
314 let doubled = pivot_indices
315 .iter()
316 .position(|pivot| !seen.insert(*pivot))
317 .expect("a duplicate pivot exists when the set is smaller");
318 return Err(SymbolicBasisError::UnsupportedSite(sites[doubled].pos));
319 }
320 for (index, kind) in kinds.iter().enumerate() {
321 if kind.is_selective_fixing() && !pivot_set.contains(&index) {
322 // A fixing row nobody pivots on; attribute it to its first target.
323 let pos = kind
324 .selective_fixing_targets()
325 .first()
326 .map(|target| target.pos)
327 .unwrap_or_default();
328 return Err(SymbolicBasisError::UnsupportedSite(pos));
329 }
330 }
331
332 // Survivors are the non-fixing rows in their original relative order,
333 // matching the in-place removals of a sequential replay.
334 let col_by_site: Vec<usize> = sites.iter().map(|site| site.col).collect();
335 let symbolic_rows = kinds
336 .iter()
337 .enumerate()
338 .filter(|(_, kind)| kind.is_readout())
339 .map(|(ordinal, kind)| {
340 let row = &rows[ordinal];
341 let deltas = col_by_site
342 .iter()
343 .enumerate()
344 .filter_map(|(site, &col)| {
345 let support = row.get(col);
346 (support != Pauli::I).then_some(SymbolicDelta { site, support })
347 })
348 .collect();
349 SymbolicRow {
350 base: row.clone(),
351 kind: kind.clone(),
352 #[cfg(test)]
353 base_ordinal: ordinal,
354 deltas,
355 }
356 })
357 .collect();
358
359 Ok(Self {
360 sites,
361 rows: symbolic_rows,
362 })
363 }
364
365 /// Evaluates the concrete basis for a full fill assignment (one chosen axis
366 /// per site), plus each surviving row's initial-ordinal provenance.
367 ///
368 /// # Panics
369 ///
370 /// Panics if `fills` omits a site of this basis: the model represents full
371 /// assignments only.
372 #[cfg(test)]
373 pub(crate) fn evaluate(&self, fills: &[(IVec3, PauliBasis)]) -> SymbolicEvaluation {
374 let chosen: HashMap<IVec3, PauliBasis> = fills.iter().copied().collect();
375 let axes = self
376 .sites
377 .iter()
378 .map(|site| {
379 *chosen
380 .get(&site.pos)
381 .expect("fill assignment covers every selective site")
382 })
383 .collect::<Vec<_>>();
384
385 let mut out_rows = Vec::with_capacity(self.rows.len());
386 let mut out_kinds = Vec::with_capacity(self.rows.len());
387 let mut combinations = Vec::with_capacity(self.rows.len());
388
389 for row in &self.rows {
390 let mut value = row.base.clone();
391 let mut combination = BTreeSet::from([row.base_ordinal]);
392 for delta in &row.deltas {
393 let site = &self.sites[delta.site];
394 let axis = axes[delta.site];
395 // Mirror the engine's trigger: XOR the fixing row iff the row's
396 // support anticommutes with the chosen axis (support is non-I).
397 if delta.support != Pauli::from(axis) {
398 value ^= &site.fixing_row;
399 xor_insert(&mut combination, site.fixing_ordinal);
400 }
401 }
402 out_rows.push(value);
403 out_kinds.push(row.kind.clone());
404 combinations.push(combination);
405 }
406
407 SymbolicEvaluation {
408 basis: StabilizerBasis::new(out_rows, out_kinds),
409 combinations,
410 }
411 }
412}
413
414/// XOR-toggle `value` into `set`: present ⇒ remove, absent ⇒ insert.
415#[cfg(test)]
416fn xor_insert(set: &mut BTreeSet<usize>, value: usize) {
417 if !set.remove(&value) {
418 set.insert(value);
419 }
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425
426 use super::super::stabilizer::{SelectiveFixingTarget, coeffs_to_sparse};
427 use crate::GalleryItem;
428
429 /// Site count below which we enumerate every fill assignment (`2^n`). Above
430 /// it, exhaustive enumeration is infeasible — the gallery's `PhaseGradientK4`
431 /// has 22 selective sites — so we fall back to a structured pairwise sample.
432 const EXHAUSTIVE_SITE_LIMIT: usize = 12;
433
434 /// Rows, kinds, and `(pos, kind, col)` site descriptors of a canonical basis.
435 type BasisParts = (
436 Vec<PauliString>,
437 Vec<StabilizerRowKind>,
438 Vec<(IVec3, SelectiveKind, usize)>,
439 );
440
441 /// One fill assignment: a chosen axis per selective site.
442 type Assignment = Vec<(IVec3, PauliBasis)>;
443
444 /// The result of an engine replay: surviving rows, their kinds, and each
445 /// row's composed initial-ordinal provenance.
446 type ReplayResult = (
447 Vec<PauliString>,
448 Vec<StabilizerRowKind>,
449 Vec<BTreeSet<usize>>,
450 );
451
452 /// Extracts the raw basis parts from canonicalized generators, mirroring what
453 /// [`SymbolicStabilizerBasis::from_generators`] and the engine replay consume.
454 fn basis_parts(stabilizers: &crate::StabilizerGenerators) -> BasisParts {
455 let rows = stabilizers
456 .generators
457 .iter()
458 .map(|generator| generator.stabilizer.paulis.clone())
459 .collect();
460 let kinds = stabilizers
461 .generators
462 .iter()
463 .map(|generator| generator.kind.clone())
464 .collect();
465 let sites = collect_selective_constraints(&stabilizers.zx_graph)
466 .into_iter()
467 .map(|constraint| (constraint.pos, constraint.kind, constraint.col))
468 .collect();
469 (rows, kinds, sites)
470 }
471
472 /// The assignment for a bitmask: bit `i` set ⇒ site `i` takes its `if_true`
473 /// axis, else `if_false`.
474 fn assignment_from_mask(sites: &[(IVec3, SelectiveKind, usize)], mask: u64) -> Assignment {
475 sites
476 .iter()
477 .enumerate()
478 .map(|(index, &(pos, kind, _))| {
479 let axis = if (mask >> index) & 1 == 1 {
480 kind.pauli_if_true()
481 } else {
482 kind.pauli_if_false()
483 };
484 (pos, axis)
485 })
486 .collect()
487 }
488
489 /// Fill assignments exercising `sites`. Up to [`EXHAUSTIVE_SITE_LIMIT`] sites
490 /// we enumerate the full `2^n` product. Beyond it we sample structurally:
491 /// all-false, all-true, every single site toggled from each pole, and every
492 /// pair. Pairwise coverage is sufficient here because the model is affine
493 /// (each site contributes an independent XOR), so any cross-site coupling
494 /// would already surface in some two-site assignment.
495 fn sampled_assignments(sites: &[(IVec3, SelectiveKind, usize)]) -> Vec<Assignment> {
496 let n = sites.len();
497 if n <= EXHAUSTIVE_SITE_LIMIT {
498 return (0..(1u64 << n))
499 .map(|mask| assignment_from_mask(sites, mask))
500 .collect();
501 }
502
503 let full = (1u64 << n) - 1;
504 let mut masks = BTreeSet::from([0, full]);
505 for i in 0..n {
506 masks.insert(1u64 << i);
507 masks.insert(full ^ (1u64 << i));
508 for j in (i + 1)..n {
509 masks.insert((1u64 << i) | (1u64 << j));
510 }
511 }
512 masks
513 .into_iter()
514 .map(|mask| assignment_from_mask(sites, mask))
515 .collect()
516 }
517
518 /// Replays one full assignment through the engine's tagged fill — the
519 /// pointwise ground truth — composing each step's combinations down to
520 /// initial ordinals. Returns `None` if any site rejects the tagged path.
521 fn replay_tagged(
522 rows: &[PauliString],
523 kinds: &[StabilizerRowKind],
524 sites: &[(IVec3, SelectiveKind, usize)],
525 assignment: &[(IVec3, PauliBasis)],
526 ) -> Option<ReplayResult> {
527 let chosen: HashMap<IVec3, PauliBasis> = assignment.iter().copied().collect();
528 let mut basis = StabilizerBasis::new(rows.to_vec(), kinds.to_vec());
529 let mut composed: Vec<BTreeSet<usize>> = (0..rows.len())
530 .map(|index| BTreeSet::from([index]))
531 .collect();
532
533 for &(pos, kind, col) in sites {
534 let axis = chosen[&pos];
535 let target = Pauli::from(axis);
536 let (next, coeffs) = basis
537 .apply_tagged_selective_fill(&[(col, target)], pos, kind)
538 .ok()?;
539 composed = coeffs_to_sparse(&coeffs)
540 .iter()
541 .map(|combination| {
542 let mut set = BTreeSet::new();
543 for &index in combination {
544 for &ordinal in &composed[index] {
545 xor_insert(&mut set, ordinal);
546 }
547 }
548 set
549 })
550 .collect();
551 basis = next;
552 }
553 Some((basis.rows, basis.kinds, composed))
554 }
555
556 fn fixing(pos: IVec3, forbidden: Pauli) -> StabilizerRowKind {
557 StabilizerRowKind::SelectiveFixing {
558 targets: vec![SelectiveFixingTarget { pos, forbidden }],
559 }
560 }
561
562 /// Full-combo equivalence across the gallery: for every eligible graph with
563 /// selective sites, every fill assignment must reproduce the engine replay's
564 /// rows, kinds, and composed provenance exactly.
565 #[test]
566 fn eligibility_and_fill_order_match_tagged_replay_across_gallery() {
567 let mut eligible_graphs = 0;
568 let mut total_assignments = 0;
569 let mut multi_site_exercised = false;
570
571 for entry in GalleryItem::iter().filter(|entry| {
572 !entry.in_category(crate::GalleryCategory::AnalysisOnly)
573 && entry.build().root().instances.is_empty()
574 }) {
575 let graph = entry
576 .build()
577 .materialize_root_graph()
578 .expect("gallery flat projection");
579 // This root replays selective fills on fixed topology. Structural
580 // choices require explicit projections, checked by guarded suites.
581 if !graph.branch_definitions().is_empty() {
582 continue;
583 }
584 let zx = crate::ZXGraph::try_from(&graph)
585 .unwrap_or_else(|error| panic!("gallery {entry:?}: ZX conversion failed: {error}"));
586 let stabilizers = zx
587 .stabilizers()
588 .unwrap_or_else(|error| panic!("gallery {entry:?}: stabilizers failed: {error}"));
589 let (rows, kinds, sites) = basis_parts(&stabilizers);
590 if sites.is_empty() {
591 continue;
592 }
593 let symbolic = match SymbolicStabilizerBasis::from_generators(&stabilizers) {
594 Ok(symbolic) => symbolic,
595 Err(SymbolicBasisError::UnsupportedSite(_)) => {
596 assert!(
597 sampled_assignments(&sites).iter().any(|assignment| {
598 replay_tagged(&rows, &kinds, &sites, assignment).is_none()
599 }),
600 "gallery {entry:?}: symbolic model rejected a fully tagged basis"
601 );
602 continue;
603 }
604 };
605 eligible_graphs += 1;
606
607 for assignment in sampled_assignments(&sites) {
608 let evaluated = symbolic.evaluate(&assignment);
609 let (replay_rows, replay_kinds, replay_combos) =
610 replay_tagged(&rows, &kinds, &sites, &assignment)
611 .expect("eligible graph takes the tagged path for every assignment");
612 if sites.len() >= 2 {
613 multi_site_exercised = true;
614 let mut reversed = sites.clone();
615 reversed.reverse();
616 let backward = replay_tagged(&rows, &kinds, &reversed, &assignment)
617 .expect("eligible graph takes the reversed tagged path");
618 assert_eq!(
619 (&replay_rows, &replay_kinds, &replay_combos),
620 (&backward.0, &backward.1, &backward.2),
621 "gallery {entry:?}: fill order matters"
622 );
623 }
624
625 assert_eq!(
626 evaluated.basis.rows, replay_rows,
627 "gallery {entry:?}: rows differ for {assignment:?}"
628 );
629 assert_eq!(
630 evaluated.basis.kinds, replay_kinds,
631 "gallery {entry:?}: kinds differ for {assignment:?}"
632 );
633 assert_eq!(
634 evaluated.combinations, replay_combos,
635 "gallery {entry:?}: combinations differ for {assignment:?}"
636 );
637 total_assignments += 1;
638 }
639 }
640
641 assert!(eligible_graphs > 0, "no eligible gallery graph exercised");
642 assert!(total_assignments > 1, "too few assignments exercised");
643 assert!(multi_site_exercised, "no multi-site fill order exercised");
644 }
645
646 /// Coupled detection: a fixing row that is not diagonalized against another
647 /// site's column is ineligible, blaming the coupled site pair. The gallery
648 /// has no such graph, so this pins the rule on a hand-built basis.
649 #[test]
650 fn non_diagonalized_fixing_rows_are_ineligible() {
651 let site_a = IVec3::new(0, 0, 0);
652 let site_b = IVec3::new(1, 0, 0);
653 // Fixing row for A carries Z at both columns: forbidden on its own (col 0)
654 // but non-identity on B's column (col 1), so the two fills are coupled.
655 let rows = vec![
656 PauliString::try_from("ZZ").unwrap(),
657 PauliString::try_from("_Z").unwrap(),
658 ];
659 let kinds = vec![fixing(site_a, Pauli::Z), fixing(site_b, Pauli::Z)];
660 let specs = [
661 SiteSpec {
662 pos: site_a,
663 kind: SelectiveKind::XY,
664 col: 0,
665 forbidden: Pauli::Z,
666 },
667 SiteSpec {
668 pos: site_b,
669 kind: SelectiveKind::XY,
670 col: 1,
671 forbidden: Pauli::Z,
672 },
673 ];
674
675 let error = SymbolicStabilizerBasis::from_raw(rows, kinds, &specs)
676 .expect_err("non-diagonalized fixing rows must be ineligible");
677 assert_eq!(error, SymbolicBasisError::UnsupportedSite(site_a));
678 }
679
680 /// A fixing row that does not carry the forbidden Pauli on its own column is
681 /// ineligible: the tagged fill would reject the axis equal to that support.
682 #[test]
683 fn fixing_row_without_forbidden_support_is_ineligible() {
684 let site = IVec3::new(0, 0, 0);
685 let rows = vec![PauliString::try_from("X").unwrap()];
686 let kinds = vec![fixing(site, Pauli::Z)];
687 let specs = [SiteSpec {
688 pos: site,
689 kind: SelectiveKind::XY,
690 col: 0,
691 forbidden: Pauli::Z,
692 }];
693
694 let error = SymbolicStabilizerBasis::from_raw(rows, kinds, &specs)
695 .expect_err("a fixing row lacking forbidden support must be ineligible");
696 assert_eq!(error, SymbolicBasisError::UnsupportedSite(site));
697 }
698}