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