1mod 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#[derive(Debug, Clone, Error)]
42#[non_exhaustive]
43pub enum FillPortsError {
44 #[error("there are no open ports to fill for minimal simulation")]
46 NoOpenPorts,
47 #[error("port at {pos} requires exactly one pipe to infer a fill, got {degree}")]
49 PortFillRequiresSinglePipe {
50 pos: IVec3,
52 degree: usize,
54 },
55 #[error("cannot infer a cube kind for filled port at {pos}")]
57 InvalidFilledPortCube {
58 pos: IVec3,
60 #[source]
62 source: crate::BlockError,
63 },
64 #[error("action {ordinal} depends on a measurement outside the selected static fill")]
66 UnavailableControl {
67 ordinal: usize,
69 },
70 #[error("invalid external stabilizer character {ch}")]
72 InvalidExternalStabilizerChar {
73 ch: char,
75 },
76 #[error("unsupported external stabilizer basis {basis} at port {pos}")]
78 UnsupportedPortBasis {
79 pos: IVec3,
81 basis: Pauli,
83 },
84}
85
86#[derive(Debug, Clone, Error)]
88#[non_exhaustive]
89pub enum StabilizerError {
90 #[error("{0}")]
92 Cancelled(#[from] crate::ComputationCancelled),
93 #[error(
95 "stabilizer derivation exceeded {phase} limit: {observed} > {limit}; {help}",
96 help = ModuleCertificationLimits::RESOURCE_LIMIT_HELP
97 )]
98 ResourceLimited {
99 phase: &'static str,
101 observed: usize,
103 limit: usize,
105 },
106 #[error("canonical stabilizer basis lost rank: expected {expected}, got {actual}")]
108 BasisRankDeficient {
109 expected: usize,
111 actual: usize,
113 },
114 #[error("selective support at {pos} for {kind} cannot be normalized")]
116 SelectiveSupportUnsatisfiable {
117 pos: IVec3,
119 kind: SelectiveKind,
121 },
122 #[error("control parity for mvar '{mvar}' is not available at deadline {deadline}")]
124 UnavailableControlParity {
125 mvar: String,
127 deadline: i64,
129 },
130 #[error("measurement surface for '{mvar}' is not available at any valid deadline")]
132 MeasurementSurfaceUnavailable {
133 mvar: String,
135 },
136 #[error("feedback target {target} has no wire on which to apply {pauli}")]
138 FeedbackTargetWithoutWire {
139 target: IVec3,
141 pauli: bloq_utils::PauliBasis,
143 },
144 #[error("measurement surface for '{name}' reaches output port {port}")]
146 MeasurementSurfaceTouchesOutputPort {
147 name: String,
149 port: IVec3,
151 },
152 #[error(
154 "measurement surface for '{name}' has interior support at z {max_z}, past its accepted deadline {deadline}"
155 )]
156 MeasurementSurfaceExtendsPastDeadline {
157 name: String,
159 max_z: i32,
161 deadline: i64,
163 },
164 #[error(
166 "selective fixing row for site {row:?} anticommutes with the flip axis of selective site {site:?}"
167 )]
168 SelectiveFixingRowsCoupled {
169 row: IVec3,
171 site: IVec3,
173 },
174 #[error("composed module certificate has no admissible deterministic presentation")]
176 ComposedPresentationUnsatisfied,
177}
178
179impl StabilizerError {
180 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 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 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 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#[derive(Debug, Clone, PartialEq)]
297pub struct Stabilizer {
298 pub paulis: PauliString,
300 pub sign: bool,
302 pub port_stabilizer: FxHashMap<IVec3, Pauli>,
304 pub interior_nodes: FxHashMap<IVec3, Pauli>,
306 pub interior_edges: FxHashMap<(IVec3, IVec3), Pauli>,
308}
309
310impl Stabilizer {
311 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 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 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 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 #[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 #[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#[derive(Debug, Clone, PartialEq, Eq)]
448pub struct SelectiveFixingTarget {
449 pub pos: IVec3,
451 pub forbidden: Pauli,
453}
454
455#[derive(Debug, Clone, Copy)]
457pub struct SelectiveFixing<'a> {
458 pub row: &'a Stabilizer,
460 pub forbidden: Pauli,
462}
463
464pub type SelectiveFixings<'a> = FxHashMap<IVec3, SelectiveFixing<'a>>;
466
467pub 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#[derive(Debug, Clone, PartialEq, Eq)]
486pub enum StabilizerRowKind {
487 Measurement {
489 name: String,
491 },
492 SelectiveFixing {
494 targets: Vec<SelectiveFixingTarget>,
496 },
497 Logical,
499}
500
501impl StabilizerRowKind {
502 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 pub fn is_readout(&self) -> bool {
522 matches!(
523 self,
524 StabilizerRowKind::Measurement { .. } | StabilizerRowKind::Logical
525 )
526 }
527}
528
529#[derive(Debug, Clone, PartialEq)]
534pub struct StabilizerGenerator {
535 pub stabilizer: Stabilizer,
537 pub kind: StabilizerRowKind,
539 readout_plan: Option<std::sync::Arc<super::ReadoutPlan>>,
540}
541
542impl StabilizerGenerator {
543 pub fn new(stabilizer: Stabilizer, kind: StabilizerRowKind) -> Self {
545 Self {
546 stabilizer,
547 kind,
548 readout_plan: None,
549 }
550 }
551
552 pub fn readout_plan(&self) -> Option<&super::ReadoutPlan> {
554 self.readout_plan.as_deref()
555 }
556
557 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 pub fn uses_source_outcomes(&self) -> bool {
568 self.readout_plan()
569 .is_some_and(|recipe| recipe.coordinates == super::ReadoutCoordinates::SourceOutcomes)
570 }
571
572 pub fn is_measurement(&self) -> bool {
574 matches!(self.kind, StabilizerRowKind::Measurement { .. })
575 }
576
577 pub fn measurement_name(&self) -> Option<&str> {
579 match &self.kind {
580 StabilizerRowKind::Measurement { name } => Some(name),
581 _ => None,
582 }
583 }
584}
585
586#[derive(Debug, Clone)]
589pub struct StabilizerGenerators {
590 pub zx_graph: ZXGraph,
592 pub generators: Vec<StabilizerGenerator>,
595 auxiliary_rows: Vec<PauliString>,
598}
599
600impl StabilizerGenerators {
601 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 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 pub(crate) fn basis_rank(&self) -> usize {
686 self.generators.len() + self.auxiliary_rows.len()
687 }
688
689 pub fn len(&self) -> usize {
691 self.generators.len()
692 }
693
694 pub fn is_empty(&self) -> bool {
696 self.generators.is_empty()
697 }
698
699 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 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 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#[cfg(test)]
1153pub(super) mod test_support {
1154 use super::super::stabilizer::basis::CoeffVec;
1155 use bloq_utils::PauliString;
1156
1157 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 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}