Skip to main content

bloq_graph/verify/
diagram.rs

1//! Logical-verifier construction, branch instantiation, and map comparison.
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::sync::{Arc, Mutex};
5
6use glam::IVec3;
7use quizx::fscalar::Zero;
8use quizx::graph::{EType, GraphLike, V, VType};
9use quizx::params::{Parity, Var};
10use quizx::phase::Phase;
11use quizx::tensor::{CompareTensors, TensorF, ToTensor};
12use rand::{Rng, RngExt, SeedableRng, rngs::StdRng};
13use rustc_hash::FxHashMap;
14
15use crate::{
16    Action, ActionNode, BinaryOp, BlockGraph, Expr, FeedbackTarget, NodeKind, Pauli, PauliBasis,
17    PortRole, SelectiveKind, ZXGraph,
18};
19
20use super::correction::{InternalCorrection, ResolvedCorrection, fresh_var};
21use super::{MeasurementKey, VerifyLogicalError, stabilizer_supports_measurement};
22use crate::zx::{CoeffVec, solve_coeff_combination};
23
24/// The concrete QuiZX graph type used for logical maps.
25pub type QuizxGraph = quizx::vec_graph::Graph;
26
27/// Ordered open boundaries of a logical map.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct BoundaryOrder {
30    inputs: Vec<IVec3>,
31    outputs: Vec<IVec3>,
32}
33
34impl BoundaryOrder {
35    /// Creates an explicit input/output order.
36    pub fn new(inputs: Vec<IVec3>, outputs: Vec<IVec3>) -> Self {
37        Self { inputs, outputs }
38    }
39
40    /// Ordered input ports.
41    pub fn inputs(&self) -> &[IVec3] {
42        &self.inputs
43    }
44
45    /// Ordered output ports.
46    pub fn outputs(&self) -> &[IVec3] {
47        &self.outputs
48    }
49
50    fn infer(source: &BlockGraph, zx: &ZXGraph) -> Self {
51        let mut inputs = Vec::new();
52        let mut outputs = Vec::new();
53        for node in zx.nodes().iter().filter(|node| node.kind == NodeKind::Port) {
54            let neighbors = zx.neighbors(node.id).unwrap_or_default();
55            let temporal_input = !neighbors.is_empty()
56                && neighbors
57                    .iter()
58                    .all(|&neighbor| zx.nodes()[neighbor].pos.z > node.pos.z);
59            let role = source
60                .get_block(node.pos)
61                .and_then(crate::Block::port_role)
62                .unwrap_or(PortRole::Auto);
63            if role.has_input_boundary() || (role == PortRole::Auto && temporal_input) {
64                inputs.push(node.pos);
65            }
66            if role.has_output_boundary() || (role == PortRole::Auto && !temporal_input) {
67                outputs.push(node.pos);
68            }
69        }
70        inputs.sort_by_key(IVec3::to_array);
71        outputs.sort_by_key(IVec3::to_array);
72        let input_keys = inputs
73            .iter()
74            .map(|&position| boundary_tag_key(source, position, true))
75            .collect::<Option<Vec<_>>>();
76        if let Some(mut keys) = input_keys {
77            let mut tagged_inputs = inputs.into_iter().zip(keys.drain(..)).collect::<Vec<_>>();
78            tagged_inputs.sort_by(|(lhs_pos, lhs), (rhs_pos, rhs)| {
79                lhs.cmp(rhs)
80                    .then_with(|| lhs_pos.to_array().cmp(&rhs_pos.to_array()))
81            });
82            inputs = tagged_inputs
83                .iter()
84                .map(|(position, _)| *position)
85                .collect();
86            let ranks = tagged_inputs
87                .into_iter()
88                .enumerate()
89                .map(|(rank, (_, key))| (key, rank))
90                .collect::<FxHashMap<_, _>>();
91            outputs.sort_by_key(|&position| {
92                let rank = boundary_tag_key(source, position, false)
93                    .and_then(|key| ranks.get(&key).copied())
94                    .unwrap_or(usize::MAX);
95                (rank, position.to_array())
96            });
97        }
98        Self { inputs, outputs }
99    }
100
101    fn validate(&self, zx: &ZXGraph) -> Result<(), VerifyLogicalError> {
102        let ports = zx
103            .nodes()
104            .iter()
105            .filter(|node| node.kind == NodeKind::Port)
106            .map(|node| node.pos.to_array())
107            .collect::<BTreeSet<_>>();
108        let collect = |positions: &[IVec3]| {
109            let mut seen = BTreeSet::new();
110            for &position in positions {
111                if !ports.contains(&position.to_array()) {
112                    return Err(VerifyLogicalError::InvalidBoundary(position));
113                }
114                if !seen.insert(position.to_array()) {
115                    return Err(VerifyLogicalError::DuplicateBoundary(position));
116                }
117            }
118            Ok(seen)
119        };
120        let inputs = collect(&self.inputs)?;
121        let outputs = collect(&self.outputs)?;
122        for position in inputs.intersection(&outputs) {
123            let pos = IVec3::from_array(*position);
124            if zx
125                .node_at(pos)
126                .is_none_or(|node| node.role != PortRole::Multiplex)
127            {
128                return Err(VerifyLogicalError::DuplicateBoundary(pos));
129            }
130        }
131        for node in zx
132            .nodes()
133            .iter()
134            .filter(|node| node.role == PortRole::Multiplex)
135        {
136            if !inputs.contains(&node.pos.to_array()) || !outputs.contains(&node.pos.to_array()) {
137                return Err(VerifyLogicalError::MissingBoundary(node.pos));
138            }
139        }
140        let seen = inputs.union(&outputs).copied().collect::<BTreeSet<_>>();
141        if let Some(position) = ports.difference(&seen).next() {
142            return Err(VerifyLogicalError::MissingBoundary(IVec3::from_array(
143                *position,
144            )));
145        }
146        Ok(())
147    }
148}
149
150fn boundary_tag_key(source: &BlockGraph, position: IVec3, input: bool) -> Option<String> {
151    let tag = source.get_block(position)?.tag()?.to_ascii_lowercase();
152    let prefixes: &[&str] = if input {
153        &["input", "in"]
154    } else {
155        &["output", "out"]
156    };
157    prefixes.iter().find_map(|prefix| {
158        tag.strip_prefix(prefix)
159            .map(|suffix| suffix.trim_start_matches('_').to_string())
160    })
161}
162
163/// Caller-chosen measurement outcomes and external Boolean inputs.
164pub type BranchAssignment = BTreeMap<String, bool>;
165
166/// Result category for one sampled assignment.
167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
168pub enum BranchStatus {
169    /// The action assignment produced a diagram, but no expected map was checked.
170    Instantiated,
171    /// A DiscardIf fired; no graph was contracted.
172    Rejected,
173    /// The concrete diagram contracted to zero, so the assignment is impossible.
174    Impossible,
175    /// A nonzero map matched the expected map projectively.
176    Verified,
177}
178
179/// Classical values and status for one branch.
180#[derive(Debug, Clone, PartialEq, Eq)]
181pub struct LogicalBranch {
182    status: BranchStatus,
183    classical_state: BTreeMap<String, bool>,
184}
185
186impl LogicalBranch {
187    /// Branch result.
188    pub const fn status(&self) -> BranchStatus {
189        self.status
190    }
191
192    /// External inputs, presampled measurement outcomes, and let bindings
193    /// evaluated before the branch was rejected or fully instantiated.
194    pub fn classical_state(&self) -> &BTreeMap<String, bool> {
195        &self.classical_state
196    }
197}
198
199/// Aggregate result of randomized branch verification.
200#[derive(Debug, Clone, PartialEq, Eq)]
201pub struct LogicalVerificationReport {
202    /// Number of random assignments attempted.
203    pub samples: usize,
204    /// Accepted, nonzero branches proven equal to the expected map.
205    pub verified: usize,
206    /// Assignments stopped by DiscardIf before contraction.
207    pub rejected: usize,
208    /// Assignments whose concrete ZX diagram was the zero map.
209    pub impossible: usize,
210}
211
212#[derive(Debug, Clone)]
213struct SelectiveSite {
214    vertex: V,
215    kind: SelectiveKind,
216    is_state: bool,
217}
218
219#[derive(Debug, Clone, Copy)]
220struct WireAnchor {
221    target: V,
222    next: V,
223    edge_type: EType,
224}
225
226#[derive(Debug, Clone, Copy, PartialEq, Eq)]
227enum InternalMeasurements {
228    Ignore,
229    Correct,
230}
231
232const MAX_CACHED_STRUCTURAL_PROJECTIONS: usize = 64;
233
234#[derive(Debug, Clone)]
235struct StructuralVerifier {
236    actions: Vec<Action>,
237    source: BlockGraph,
238    targets: Vec<IVec3>,
239    boundaries: BoundaryOrder,
240    internal_measurements: InternalMeasurements,
241    arms: Arc<Mutex<BTreeMap<Vec<bool>, Arc<LogicalVerifier>>>>,
242}
243
244impl StructuralVerifier {
245    fn arm(&self, values: &[bool]) -> Result<Arc<LogicalVerifier>, VerifyLogicalError> {
246        if let Some(arm) = self
247            .arms
248            .lock()
249            .expect("projection cache is not poisoned")
250            .get(values)
251        {
252            return Ok(Arc::clone(arm));
253        }
254        let projected = self
255            .source
256            .project_branches(self.targets.iter().copied().zip(values.iter().copied()))?;
257        let arm = Arc::new(LogicalVerifier::with_boundaries_from_zx(
258            projected.to_zx_graph()?,
259            self.boundaries.clone(),
260            self.internal_measurements,
261        )?);
262        let mut cache = self.arms.lock().expect("projection cache is not poisoned");
263        // ponytail: bounded cache, clear on mask churn; use LRU if profiling
264        // finds repeated rebuilds of hot projections significant.
265        if cache.len() >= MAX_CACHED_STRUCTURAL_PROJECTIONS {
266            cache.clear();
267        }
268        cache.insert(values.to_vec(), Arc::clone(&arm));
269        Ok(arm)
270    }
271
272    fn values_for(
273        &self,
274        assignment: &BranchAssignment,
275        inputs: &BTreeSet<String>,
276    ) -> Result<Vec<bool>, VerifyLogicalError> {
277        let mut state = assigned_inputs(inputs, assignment)?;
278        for action in &self.actions {
279            let Action::Measure { name, .. } = action else {
280                continue;
281            };
282            let value = assignment
283                .get(name)
284                .copied()
285                .ok_or_else(|| VerifyLogicalError::MissingMeasurementValue(name.clone()))?;
286            state.insert(name.clone(), value);
287        }
288
289        let mut values = Vec::new();
290        for action in &self.actions {
291            match action {
292                Action::Let { name, expr } => {
293                    state.insert(name.clone(), eval_expr(expr, &state));
294                }
295                Action::Branch { condition, .. } => {
296                    values.push(eval_expr(condition, &state));
297                }
298                _ => {}
299            }
300        }
301        Ok(values)
302    }
303}
304
305/// Reusable symbolic ZX verifier for one static graph or a bounded family of
306/// reachable terminal-branch projections.
307#[derive(Debug, Clone)]
308pub struct LogicalVerifier {
309    symbolic: QuizxGraph,
310    boundaries: BoundaryOrder,
311    actions: Vec<ActionNode>,
312    inputs: BTreeSet<String>,
313    measurement_vars: BTreeMap<String, Var>,
314    feedback_vars: FxHashMap<usize, Var>,
315    selectives: FxHashMap<IVec3, SelectiveSite>,
316    correction: Option<InternalCorrection>,
317    structural: Option<StructuralVerifier>,
318}
319
320struct ResolvedAssignment {
321    branch: LogicalBranch,
322    variable_values: FxHashMap<Var, bool>,
323    selective_values: FxHashMap<IVec3, PauliBasis>,
324    feedback_values: FxHashMap<usize, bool>,
325    correction: Option<ResolvedCorrection>,
326}
327
328impl LogicalVerifier {
329    /// The tensor boundary order used by this verifier.
330    pub fn boundaries(&self) -> &BoundaryOrder {
331        &self.boundaries
332    }
333    /// Builds a symbolic verifier from explicit roles or temporal geometry.
334    /// [`crate::CancellationToken::run`] enables cooperative cancellation during
335    /// named-measurement normalization.
336    ///
337    /// # Errors
338    ///
339    /// Returns an error if graph conversion, boundaries, actions, or symbolic setup are invalid.
340    pub fn new(source: &BlockGraph) -> Result<Self, VerifyLogicalError> {
341        Self::from_source(source, None, InternalMeasurements::Ignore)
342    }
343
344    /// Builds a verifier with caller-specified tensor-axis and boundary order.
345    ///
346    /// # Errors
347    ///
348    /// Returns an error if the graph or supplied boundary order is invalid.
349    pub fn with_boundaries(
350        source: &BlockGraph,
351        boundaries: BoundaryOrder,
352    ) -> Result<Self, VerifyLogicalError> {
353        Self::from_source(source, Some(boundaries), InternalMeasurements::Ignore)
354    }
355
356    /// Builds a verifier that samples unnamed boundary/joint measurements and
357    /// materializes their terminal output-frame correction.
358    /// Sampling and selective fills preserve the supplied named readout parities.
359    ///
360    /// # Errors
361    ///
362    /// Returns an error if the graph or its internal correction system is invalid.
363    pub fn with_internal_measurements(source: &BlockGraph) -> Result<Self, VerifyLogicalError> {
364        Self::from_source(source, None, InternalMeasurements::Correct)
365    }
366
367    /// Internal-measurement mode with caller-specified boundary order.
368    ///
369    /// # Errors
370    ///
371    /// Returns an error if the graph, boundaries, or internal correction system is invalid.
372    pub fn with_boundaries_and_internal_measurements(
373        source: &BlockGraph,
374        boundaries: BoundaryOrder,
375    ) -> Result<Self, VerifyLogicalError> {
376        Self::from_source(source, Some(boundaries), InternalMeasurements::Correct)
377    }
378
379    fn from_source(
380        source: &BlockGraph,
381        boundaries: Option<BoundaryOrder>,
382        internal_measurements: InternalMeasurements,
383    ) -> Result<Self, VerifyLogicalError> {
384        source.require_flat_hierarchy("logical verification")?;
385        let regions = source.branch_regions()?;
386        if regions.is_empty() {
387            let zx = source.to_zx_graph()?;
388            let boundaries = boundaries.unwrap_or_else(|| BoundaryOrder::infer(source, &zx));
389            return Self::with_boundaries_from_zx(zx, boundaries, internal_measurements);
390        }
391        let assignments = source
392            .branch_assignments_up_to(1)?
393            .pop()
394            .expect("Boolean branch conditions have a reachable assignment");
395        let projected = source.project_branches(assignments.iter().copied())?;
396        let zx = projected.to_zx_graph()?;
397        let boundaries = boundaries.unwrap_or_else(|| BoundaryOrder::infer(&projected, &zx));
398        let mut verifier =
399            Self::with_boundaries_from_zx(zx, boundaries.clone(), internal_measurements)?;
400        let values = assignments.iter().map(|(_, value)| *value).collect();
401        let arms = Arc::new(Mutex::new(BTreeMap::from([(
402            values,
403            Arc::new(verifier.clone()),
404        )])));
405        verifier.structural = Some(StructuralVerifier {
406            actions: source.actions(),
407            source: source.clone(),
408            targets: assignments.iter().map(|(target, _)| *target).collect(),
409            boundaries,
410            internal_measurements,
411            arms,
412        });
413        Ok(verifier)
414    }
415
416    fn with_boundaries_from_zx(
417        zx: ZXGraph,
418        boundaries: BoundaryOrder,
419        internal_measurements: InternalMeasurements,
420    ) -> Result<Self, VerifyLogicalError> {
421        zx.validate_for_program()?;
422        boundaries.validate(&zx)?;
423        let actions = zx
424            .action_graph()
425            .ordered_nodes()
426            .cloned()
427            .collect::<Vec<_>>();
428        let inputs = zx.action_graph().inputs().map(str::to_owned).collect();
429
430        let mut next_variable = 0;
431        let mut measurement_vars = BTreeMap::new();
432        let mut feedback_vars = FxHashMap::default();
433        for action in &actions {
434            match &action.action {
435                Action::Measure { name, .. } => {
436                    measurement_vars.insert(name.clone(), fresh_var(&mut next_variable)?);
437                }
438                Action::Feedback { targets, .. } if !targets.is_empty() => {
439                    feedback_vars.insert(action.ordinal, fresh_var(&mut next_variable)?);
440                }
441                _ => {}
442            }
443        }
444
445        let named_phases = named_measurement_phases(&zx, &actions, &measurement_vars)?;
446        let correction = match internal_measurements {
447            InternalMeasurements::Ignore => None,
448            InternalMeasurements::Correct => Some(InternalCorrection::new(
449                &zx,
450                &named_phases,
451                &measurement_vars,
452                &mut next_variable,
453            )?),
454        };
455        let phases = correction
456            .as_ref()
457            .map(InternalCorrection::measurement_vars)
458            .unwrap_or(&named_phases);
459        let (symbolic, selectives) =
460            build_symbolic_graph(&zx, &boundaries, &actions, phases, &feedback_vars)?;
461        Ok(Self {
462            symbolic,
463            boundaries,
464            actions,
465            inputs,
466            measurement_vars,
467            feedback_vars,
468            selectives,
469            correction,
470            structural: None,
471        })
472    }
473
474    /// Instantiates a deterministic assignment. Rejected branches have no diagram.
475    ///
476    /// # Errors
477    ///
478    /// Returns an error for missing values or an invalid reachable branch.
479    pub fn instantiate(
480        &self,
481        assignment: &BranchAssignment,
482    ) -> Result<(LogicalBranch, Option<QuizxGraph>), VerifyLogicalError> {
483        self.instantiate_with(assignment, |_| false)
484    }
485
486    /// Samples external inputs and measurements, then instantiates one branch.
487    ///
488    /// # Errors
489    ///
490    /// Returns an error if the sampled branch cannot be instantiated.
491    pub fn sample<R: Rng + ?Sized>(
492        &self,
493        rng: &mut R,
494    ) -> Result<(LogicalBranch, Option<QuizxGraph>), VerifyLogicalError> {
495        let assignment = self
496            .inputs
497            .iter()
498            .chain(self.measurement_vars.keys())
499            .map(|name| (name.clone(), rng.random()))
500            .collect::<BranchAssignment>();
501        self.instantiate_with(&assignment, |_| rng.random())
502    }
503
504    /// Samples one reproducible branch.
505    ///
506    /// # Errors
507    ///
508    /// Returns an error if the sampled branch cannot be instantiated.
509    pub fn sample_with_seed(
510        &self,
511        seed: u64,
512    ) -> Result<(LogicalBranch, Option<QuizxGraph>), VerifyLogicalError> {
513        self.sample(&mut StdRng::seed_from_u64(seed))
514    }
515
516    /// Verifies one caller-supplied assignment against expected.
517    ///
518    /// # Errors
519    ///
520    /// Returns an arity, assignment, zero-map, or logical-mismatch error.
521    pub fn verify_branch(
522        &self,
523        expected: &QuizxGraph,
524        assignment: &BranchAssignment,
525    ) -> Result<LogicalBranch, VerifyLogicalError> {
526        self.check_boundary_arity(expected)?;
527        let expected_tensor = contract(expected);
528        if !tensor_is_nonzero(&expected_tensor) {
529            return Err(VerifyLogicalError::ZeroExpectedMap);
530        }
531        let (branch, diagram) = self.instantiate(assignment)?;
532        compare_branch(branch, diagram, &expected_tensor)
533    }
534
535    /// Verifies random measurement assignments from a stable seed.
536    ///
537    /// # Errors
538    ///
539    /// Returns an arity, sampling, zero-map, or logical-mismatch error.
540    pub fn verify(
541        &self,
542        expected: &QuizxGraph,
543        samples: usize,
544        seed: u64,
545    ) -> Result<LogicalVerificationReport, VerifyLogicalError> {
546        self.check_boundary_arity(expected)?;
547        let expected_tensor = contract(expected);
548        if !tensor_is_nonzero(&expected_tensor) {
549            return Err(VerifyLogicalError::ZeroExpectedMap);
550        }
551        let mut rng = StdRng::seed_from_u64(seed);
552        let mut report = LogicalVerificationReport {
553            samples,
554            verified: 0,
555            rejected: 0,
556            impossible: 0,
557        };
558        for _ in 0..samples {
559            let (branch, diagram) = self.sample(&mut rng)?;
560            let branch = compare_branch(branch, diagram, &expected_tensor)?;
561            match branch.status {
562                BranchStatus::Instantiated => {
563                    unreachable!("map comparison always resolves an instantiated branch")
564                }
565                BranchStatus::Rejected => report.rejected += 1,
566                BranchStatus::Impossible => report.impossible += 1,
567                BranchStatus::Verified => report.verified += 1,
568            }
569        }
570        if report.verified == 0 {
571            return Err(VerifyLogicalError::NoVerifiedBranch { samples });
572        }
573        Ok(report)
574    }
575
576    fn check_boundary_arity(&self, expected: &QuizxGraph) -> Result<(), VerifyLogicalError> {
577        let actual_inputs = self.boundaries.inputs.len();
578        let actual_outputs = self.boundaries.outputs.len();
579        let expected_inputs = expected.inputs().len();
580        let expected_outputs = expected.outputs().len();
581        if (actual_inputs, actual_outputs) != (expected_inputs, expected_outputs) {
582            return Err(VerifyLogicalError::BoundaryArityMismatch {
583                actual_inputs,
584                actual_outputs,
585                expected_inputs,
586                expected_outputs,
587            });
588        }
589        Ok(())
590    }
591
592    fn instantiate_with(
593        &self,
594        assignment: &BranchAssignment,
595        internal_value: impl FnMut(Var) -> bool,
596    ) -> Result<(LogicalBranch, Option<QuizxGraph>), VerifyLogicalError> {
597        if let Some(structural) = &self.structural {
598            let values = structural.values_for(assignment, &self.inputs)?;
599            let arm = structural.arm(&values)?;
600            return arm.instantiate_with(assignment, internal_value);
601        }
602
603        let resolved = self.resolve_assignment(assignment)?;
604        self.instantiate_resolved(&resolved, internal_value)
605    }
606
607    /// One feasible representative of every corrected anonymous boundary class.
608    pub(super) fn branch_family(
609        &self,
610        assignment: &BranchAssignment,
611        limit: usize,
612    ) -> Result<(LogicalBranch, Vec<QuizxGraph>), super::feedback::FeedbackInferenceError> {
613        if let Some(structural) = &self.structural {
614            let values = structural.values_for(assignment, &self.inputs)?;
615            return structural.arm(&values)?.branch_family(assignment, limit);
616        }
617        let resolved = self.resolve_assignment(assignment)?;
618        if resolved.branch.status == BranchStatus::Rejected {
619            return Ok((resolved.branch, Vec::new()));
620        }
621        let representatives = match &self.correction {
622            Some(correction) => correction.representatives(
623                &self.boundaries.outputs,
624                &self.actions,
625                &resolved.variable_values,
626                &resolved.feedback_values,
627                resolved
628                    .correction
629                    .as_ref()
630                    .expect("resolved internal branch"),
631                limit,
632            )?,
633            None => vec![FxHashMap::default()],
634        };
635        let diagrams = representatives
636            .into_iter()
637            .map(|values| {
638                let (_, diagram) =
639                    self.instantiate_resolved(&resolved, |variable| values[&variable])?;
640                Ok(diagram.expect("resolved branch is not rejected"))
641            })
642            .collect::<Result<Vec<_>, VerifyLogicalError>>()?;
643        Ok((resolved.branch, diagrams))
644    }
645
646    fn resolve_assignment(
647        &self,
648        assignment: &BranchAssignment,
649    ) -> Result<ResolvedAssignment, VerifyLogicalError> {
650        let mut classical_state = assigned_inputs(&self.inputs, assignment)?;
651        let mut variable_values = FxHashMap::<Var, bool>::default();
652        let mut selective_values = FxHashMap::<IVec3, PauliBasis>::default();
653
654        // Measurements are branch inputs and may be referenced before their action.
655        for action in &self.actions {
656            let Action::Measure { name, .. } = &action.action else {
657                continue;
658            };
659            let value = assignment
660                .get(name)
661                .copied()
662                .ok_or_else(|| VerifyLogicalError::MissingMeasurementValue(name.clone()))?;
663            classical_state.insert(name.clone(), value);
664            variable_values.insert(self.measurement_vars[name], value);
665        }
666
667        for action in &self.actions {
668            match &action.action {
669                Action::Let { name, expr } => {
670                    classical_state.insert(name.clone(), eval_expr(expr, &classical_state));
671                }
672                Action::DiscardIf(expr) if eval_expr(expr, &classical_state) => {
673                    return Ok(ResolvedAssignment {
674                        branch: LogicalBranch {
675                            status: BranchStatus::Rejected,
676                            classical_state,
677                        },
678                        variable_values,
679                        selective_values,
680                        feedback_values: FxHashMap::default(),
681                        correction: None,
682                    });
683                }
684                Action::Resolve { target, condition } => {
685                    let site = self
686                        .selectives
687                        .get(target)
688                        .expect("validated resolve target has a selective site");
689                    let chosen = if eval_expr(condition, &classical_state) {
690                        site.kind.pauli_if_true()
691                    } else {
692                        site.kind.pauli_if_false()
693                    };
694                    selective_values.insert(*target, chosen);
695                }
696                Action::Feedback { condition, .. } => {
697                    if let Some(variable) = self.feedback_vars.get(&action.ordinal) {
698                        let value = condition
699                            .as_ref()
700                            .is_none_or(|expr| eval_expr(expr, &classical_state));
701                        variable_values.insert(*variable, value);
702                    }
703                }
704                // Conditional sources are projected to a static arm before this
705                // evaluator runs; the all-true construction pass has no action
706                // work left for the structural marker itself.
707                Action::Branch { .. } | Action::Measure { .. } | Action::DiscardIf(_) => {}
708            }
709        }
710
711        let feedback_values = self
712            .feedback_vars
713            .iter()
714            .map(|(&ordinal, &variable)| (ordinal, variable_values[&variable]))
715            .collect();
716        let correction = self
717            .correction
718            .as_ref()
719            .map(|correction| correction.resolve(&self.actions, &selective_values))
720            .transpose()?;
721        Ok(ResolvedAssignment {
722            branch: LogicalBranch {
723                status: BranchStatus::Instantiated,
724                classical_state,
725            },
726            variable_values,
727            selective_values,
728            feedback_values,
729            correction,
730        })
731    }
732
733    fn instantiate_resolved(
734        &self,
735        resolved: &ResolvedAssignment,
736        mut internal_value: impl FnMut(Var) -> bool,
737    ) -> Result<(LogicalBranch, Option<QuizxGraph>), VerifyLogicalError> {
738        let mut variable_values = resolved.variable_values.clone();
739        if let Some(correction) = &self.correction {
740            for variable in correction.sampled_vars() {
741                variable_values.insert(variable, internal_value(variable));
742            }
743        }
744        if resolved.branch.status == BranchStatus::Rejected {
745            return Ok((resolved.branch.clone(), None));
746        }
747        let selective_values = &resolved.selective_values;
748
749        let (mut diagram, sites) = if let Some(correction) = &self.correction {
750            build_symbolic_graph(
751                correction.zx_graph(),
752                &self.boundaries,
753                &self.actions,
754                &resolved
755                    .correction
756                    .as_ref()
757                    .expect("resolved internal branch")
758                    .phases,
759                &self.feedback_vars,
760            )?
761        } else {
762            (self.symbolic.clone(), self.selectives.clone())
763        };
764        for (&position, site) in &sites {
765            let chosen = selective_values[&position];
766            let (vertex_type, base_phase) = measurement_spider(chosen, site.is_state);
767            diagram.set_vertex_type(site.vertex, vertex_type);
768            diagram.set_phase(site.vertex, base_phase);
769        }
770        for vertex in diagram.vertex_vec() {
771            let odd = super::parity::phase_value(&diagram.vars(vertex), |variable| {
772                variable_values.get(&variable).copied().unwrap_or(false)
773            });
774            if odd {
775                diagram.add_to_phase(vertex, Phase::from(1_i64));
776            }
777            diagram.set_vars(vertex, Parity::default());
778        }
779
780        if let Some(correction) = &self.correction {
781            correction.apply(
782                &mut diagram,
783                &self.boundaries.outputs,
784                &variable_values,
785                resolved
786                    .correction
787                    .as_ref()
788                    .expect("resolved internal branch"),
789                &self.actions,
790                &resolved.feedback_values,
791            )?;
792        }
793
794        Ok((resolved.branch.clone(), Some(diagram)))
795    }
796}
797
798fn assigned_inputs(
799    inputs: &BTreeSet<String>,
800    assignment: &BranchAssignment,
801) -> Result<BTreeMap<String, bool>, VerifyLogicalError> {
802    inputs
803        .iter()
804        .map(|name| {
805            assignment
806                .get(name)
807                .copied()
808                .map(|value| (name.clone(), value))
809                .ok_or_else(|| VerifyLogicalError::MissingInputValue(name.clone()))
810        })
811        .collect()
812}
813
814/// Builds and fuzzes a verifier in one call.
815///
816/// # Errors
817///
818/// Returns verifier-construction or sampled logical-verification errors.
819pub fn verify_logical(
820    graph: &BlockGraph,
821    expected: &QuizxGraph,
822    samples: usize,
823    seed: u64,
824) -> Result<LogicalVerificationReport, VerifyLogicalError> {
825    LogicalVerifier::new(graph)?.verify(expected, samples, seed)
826}
827
828fn compare_branch(
829    mut branch: LogicalBranch,
830    diagram: Option<QuizxGraph>,
831    expected: &TensorF,
832) -> Result<LogicalBranch, VerifyLogicalError> {
833    let Some(diagram) = diagram else {
834        branch.status = BranchStatus::Rejected;
835        return Ok(branch);
836    };
837    let actual = contract(&diagram);
838    if !tensor_is_nonzero(&actual) {
839        branch.status = BranchStatus::Impossible;
840        return Ok(branch);
841    }
842    if !<TensorF as CompareTensors>::scalar_eq(&actual, expected) {
843        return Err(VerifyLogicalError::MapMismatch {
844            classical_state: branch.classical_state,
845        });
846    }
847    branch.status = BranchStatus::Verified;
848    Ok(branch)
849}
850
851fn tensor_is_nonzero(tensor: &TensorF) -> bool {
852    tensor.iter().any(|value| !value.is_zero())
853}
854
855fn contract(diagram: &QuizxGraph) -> TensorF {
856    let mut reduced = diagram.clone();
857    quizx::simplify::full_simp(&mut reduced);
858    reduced.to_tensorf()
859}
860
861pub(super) fn eval_expr(expr: &Expr, values: &BTreeMap<String, bool>) -> bool {
862    match expr {
863        Expr::Var(name) => values[name],
864        Expr::Not(inner) => !eval_expr(inner, values),
865        Expr::Binary(op, lhs, rhs) => {
866            let lhs = eval_expr(lhs, values);
867            let rhs = eval_expr(rhs, values);
868            match op {
869                BinaryOp::Xor => lhs ^ rhs,
870                BinaryOp::And => lhs & rhs,
871                BinaryOp::Or => lhs | rhs,
872            }
873        }
874    }
875}
876
877fn named_measurement_phases(
878    zx: &ZXGraph,
879    actions: &[ActionNode],
880    variables: &BTreeMap<String, Var>,
881) -> Result<BTreeMap<MeasurementKey, Parity>, VerifyLogicalError> {
882    let mut phases = BTreeMap::new();
883    let mut crossings = Vec::new();
884    for action in actions {
885        let Action::Measure { name, .. } = &action.action else {
886            continue;
887        };
888        let key = MeasurementKey::for_action(zx, action).expect("validated measurement has a key");
889        if let MeasurementKey::Node { pos, pauli } = key {
890            let node = zx.node_at(pos).expect("validated measurement node");
891            let native = match node.kind {
892                NodeKind::X => Pauli::Z,
893                NodeKind::Z => Pauli::X,
894                _ => pauli,
895            };
896            if native != pauli {
897                crossings.push((name.as_str(), node.id, pauli));
898            }
899            phases.insert(
900                MeasurementKey::Node { pos, pauli: native },
901                Parity::single(variables[name]),
902            );
903        } else {
904            phases.insert(key, Parity::single(variables[name]));
905        }
906    }
907    if crossings.is_empty() {
908        return Ok(phases);
909    }
910
911    let stabilizers = zx.stabilizers().map_err(|source| {
912        VerifyLogicalError::RuntimeBasisInitializationFailed {
913            source: source.into(),
914        }
915    })?;
916    let rows = actions
917        .iter()
918        .filter_map(|action| {
919            let Action::Measure { name, .. } = &action.action else {
920                return None;
921            };
922            let row = stabilizers
923                .generators
924                .iter()
925                .find(|generator| generator.measurement_name() == Some(name))
926                .expect("canonical table includes each named measurement");
927            Some((name.as_str(), &row.stabilizer))
928        })
929        .collect::<Vec<_>>();
930    for (name, node_id, pauli) in crossings {
931        let row = rows
932            .iter()
933            .find(|(measured, _)| *measured == name)
934            .expect("named row exists")
935            .1;
936        for &neighbor in zx.neighbors(node_id).expect("measurement node exists") {
937            let edge = zx
938                .edge_between(node_id, neighbor)
939                .expect("incident edge exists");
940            let (first, second) = sorted_pair(node_id, neighbor);
941            let key = MeasurementKey::Edge {
942                src: zx.nodes()[first].pos,
943                dst: zx.nodes()[second].pos,
944                pauli: if node_id == second && edge.hadamard {
945                    pauli.flip()
946                } else {
947                    pauli
948                },
949            };
950            if stabilizer_supports_measurement(row, &key) {
951                phases.entry(key).or_default();
952            }
953        }
954    }
955    // Native phases must toggle the selected named rows jointly: a crossing
956    // record can share a broadcast with another record.
957    let columns = phases
958        .keys()
959        .map(|key| {
960            let mut column = CoeffVec::zeros(rows.len());
961            for (index, (_, row)) in rows.iter().enumerate() {
962                column.set_bit(index, stabilizer_supports_measurement(row, key));
963            }
964            column
965        })
966        .collect::<Vec<_>>();
967    for parity in phases.values_mut() {
968        *parity = Parity::default();
969    }
970    for (index, &(name, _)) in rows.iter().enumerate() {
971        let solution =
972            solve_coeff_combination(&columns, &CoeffVec::singleton(index, rows.len()))
973                .ok_or_else(|| VerifyLogicalError::MeasurementPhaseUnavailable(name.to_owned()))?;
974        for (candidate, parity) in phases.values_mut().enumerate() {
975            if solution.bit(candidate) {
976                *parity = &*parity + &Parity::single(variables[name]);
977            }
978        }
979    }
980    phases.retain(|_, parity| !parity.is_empty());
981    Ok(phases)
982}
983
984fn build_symbolic_graph(
985    zx: &ZXGraph,
986    boundaries: &BoundaryOrder,
987    actions: &[ActionNode],
988    measurement_key_vars: &BTreeMap<MeasurementKey, Parity>,
989    feedback_vars: &FxHashMap<usize, Var>,
990) -> Result<(QuizxGraph, FxHashMap<IVec3, SelectiveSite>), VerifyLogicalError> {
991    let mut graph = QuizxGraph::new();
992    let mut node_vertices = Vec::with_capacity(zx.nodes().len());
993    let mut input_boundaries = FxHashMap::default();
994    let mut output_boundaries = FxHashMap::default();
995    let mut selectives = FxHashMap::default();
996    for node in zx.nodes() {
997        if node.kind == NodeKind::Port && node.role == PortRole::Multiplex {
998            let spider = graph.add_vertex_with_phase(VType::Z, Phase::from(0_i64));
999            let input = graph.add_vertex_with_phase(VType::B, Phase::from(0_i64));
1000            let output = graph.add_vertex_with_phase(VType::B, Phase::from(0_i64));
1001            graph.set_coord(spider, (f64::from(node.pos.z), f64::from(node.pos.x)));
1002            graph.set_coord(input, (f64::from(node.pos.z) - 0.25, f64::from(node.pos.x)));
1003            graph.set_coord(
1004                output,
1005                (f64::from(node.pos.z) + 0.25, f64::from(node.pos.x)),
1006            );
1007            graph.add_edge_with_type(input, spider, EType::N);
1008            graph.add_edge_with_type(spider, output, EType::N);
1009            input_boundaries.insert(node.pos, input);
1010            output_boundaries.insert(node.pos, output);
1011            node_vertices.push(spider);
1012            continue;
1013        }
1014        let is_state = is_state_boundary(zx, node.id);
1015        let (vertex_type, phase) = match node.kind {
1016            NodeKind::X => (VType::X, Phase::from(0_i64)),
1017            NodeKind::Z => (VType::Z, Phase::from(0_i64)),
1018            NodeKind::Y => measurement_spider(PauliBasis::Y, is_state),
1019            NodeKind::Port => (VType::B, Phase::from(0_i64)),
1020            NodeKind::T => (VType::Z, Phase::from((1_i64, 4_i64))),
1021            NodeKind::Selective(kind) => {
1022                let vertex = graph.add_vertex_with_phase(VType::Z, Phase::from(0_i64));
1023                graph.set_coord(vertex, (f64::from(node.pos.z), f64::from(node.pos.x)));
1024                node_vertices.push(vertex);
1025                selectives.insert(
1026                    node.pos,
1027                    SelectiveSite {
1028                        vertex,
1029                        kind,
1030                        is_state,
1031                    },
1032                );
1033                continue;
1034            }
1035        };
1036        let vertex = graph.add_vertex_with_phase(vertex_type, phase);
1037        graph.set_coord(vertex, (f64::from(node.pos.z), f64::from(node.pos.x)));
1038        if node.kind == NodeKind::Port {
1039            input_boundaries.insert(node.pos, vertex);
1040            output_boundaries.insert(node.pos, vertex);
1041        }
1042        node_vertices.push(vertex);
1043    }
1044
1045    let mut edge_measurements = FxHashMap::<(usize, usize), Vec<(PauliBasis, &Parity)>>::default();
1046    for (key, parity) in measurement_key_vars {
1047        match key {
1048            MeasurementKey::Node { pos, .. } => {
1049                let node = zx.node_at(*pos).expect("scheduled measurement node exists");
1050                graph.add_to_vars(node_vertices[node.id], parity);
1051            }
1052            MeasurementKey::Edge { src, dst, pauli } => {
1053                let lhs = zx.node_at(*src).expect("scheduled measurement source").id;
1054                let rhs = zx
1055                    .node_at(*dst)
1056                    .expect("scheduled measurement destination")
1057                    .id;
1058                let basis = match pauli {
1059                    Pauli::X => PauliBasis::X,
1060                    Pauli::Z => PauliBasis::Z,
1061                    Pauli::I | Pauli::Y => unreachable!("wire phases use X or Z components"),
1062                };
1063                edge_measurements
1064                    .entry(sorted_pair(lhs, rhs))
1065                    .or_default()
1066                    .push((basis, parity));
1067            }
1068        }
1069    }
1070    let mut anchors = FxHashMap::<(usize, usize), WireAnchor>::default();
1071    for edge in zx.edges().iter().filter(|edge| edge.n1 < edge.n2) {
1072        let lhs = node_vertices[edge.n1];
1073        let rhs = node_vertices[edge.n2];
1074        let edge_type = if edge.hadamard { EType::H } else { EType::N };
1075        let mut first = None;
1076        let mut last = lhs;
1077        for (observable, parity) in edge_measurements
1078            .get(&(edge.n1, edge.n2))
1079            .into_iter()
1080            .flatten()
1081        {
1082            let (vertex_type, _) = measurement_spider(*observable, false);
1083            let measurement = graph.add_vertex_with_phase(vertex_type, Phase::from(0_i64));
1084            graph.set_vars(measurement, (*parity).clone());
1085            graph.add_edge_with_type(last, measurement, EType::N);
1086            first.get_or_insert(measurement);
1087            last = measurement;
1088        }
1089        graph.add_edge_with_type(last, rhs, edge_type);
1090        anchors.insert(
1091            (edge.n1, edge.n2),
1092            WireAnchor {
1093                target: lhs,
1094                next: first.unwrap_or(rhs),
1095                edge_type: if first.is_some() { EType::N } else { edge_type },
1096            },
1097        );
1098        anchors.insert(
1099            (edge.n2, edge.n1),
1100            WireAnchor {
1101                target: rhs,
1102                next: last,
1103                edge_type,
1104            },
1105        );
1106    }
1107
1108    for action in actions {
1109        match &action.action {
1110            Action::Feedback { targets, .. } if !targets.is_empty() => {
1111                let variable = feedback_vars[&action.ordinal];
1112                for target in targets {
1113                    insert_feedback(
1114                        &mut graph,
1115                        zx,
1116                        &node_vertices,
1117                        &mut anchors,
1118                        target,
1119                        variable,
1120                    )?;
1121                }
1122            }
1123            _ => {}
1124        }
1125    }
1126
1127    graph.set_inputs(
1128        boundaries
1129            .inputs
1130            .iter()
1131            .map(|position| input_boundaries[position])
1132            .collect(),
1133    );
1134    graph.set_outputs(
1135        boundaries
1136            .outputs
1137            .iter()
1138            .map(|position| output_boundaries[position])
1139            .collect(),
1140    );
1141    Ok((graph, selectives))
1142}
1143
1144fn insert_feedback(
1145    graph: &mut QuizxGraph,
1146    zx: &ZXGraph,
1147    node_vertices: &[V],
1148    anchors: &mut FxHashMap<(usize, usize), WireAnchor>,
1149    target: &FeedbackTarget,
1150    variable: Var,
1151) -> Result<(), VerifyLogicalError> {
1152    let node = zx
1153        .node_at(target.target)
1154        .expect("validated feedback target exists");
1155    let neighbor = zx
1156        .feedback_edge(target)
1157        .ok_or(VerifyLogicalError::FeedbackTargetWithoutWire {
1158            target: target.target,
1159            pauli: target.pauli,
1160        })?
1161        .n2;
1162    let vertex_types = match target.pauli {
1163        PauliBasis::X => [Some(VType::X), None],
1164        PauliBasis::Y => [Some(VType::Z), Some(VType::X)],
1165        PauliBasis::Z => [Some(VType::Z), None],
1166    };
1167    for vertex_type in vertex_types.into_iter().flatten() {
1168        let anchor = *anchors
1169            .get(&(node.id, neighbor))
1170            .expect("every original edge has anchors in both directions");
1171        graph.remove_edge(anchor.target, anchor.next);
1172        let correction = graph.add_vertex_with_phase(vertex_type, Phase::from(0_i64));
1173        graph.set_vars(correction, Parity::single(variable));
1174        graph.add_edge_with_type(anchor.target, correction, EType::N);
1175        graph.add_edge_with_type(correction, anchor.next, anchor.edge_type);
1176        anchors.insert(
1177            (node.id, neighbor),
1178            WireAnchor {
1179                target: anchor.target,
1180                next: correction,
1181                edge_type: EType::N,
1182            },
1183        );
1184
1185        // If this was still a direct original edge, its reverse anchor also
1186        // starts at the vertex just replaced. Keep both endpoint frontiers on
1187        // the same subdivided wire. Once either side already has an inserted
1188        // spider, its first edge is unaffected by insertions from the other.
1189        let reverse = anchors
1190            .get_mut(&(neighbor, node.id))
1191            .expect("every original edge has a reverse anchor");
1192        if reverse.next == anchor.target {
1193            reverse.next = correction;
1194            reverse.edge_type = anchor.edge_type;
1195        }
1196    }
1197    debug_assert_eq!(node_vertices[node.id], anchors[&(node.id, neighbor)].target);
1198    Ok(())
1199}
1200
1201fn is_state_boundary(zx: &ZXGraph, node: usize) -> bool {
1202    let position = zx.nodes()[node].pos;
1203    let neighbors = zx.neighbors(node).unwrap_or_default();
1204    !neighbors.is_empty()
1205        && neighbors
1206            .iter()
1207            .all(|&neighbor| zx.nodes()[neighbor].pos.z > position.z)
1208}
1209
1210fn measurement_spider(pauli: PauliBasis, is_state: bool) -> (VType, Phase) {
1211    match pauli {
1212        PauliBasis::X => (VType::Z, Phase::from(0_i64)),
1213        PauliBasis::Z => (VType::X, Phase::from(0_i64)),
1214        PauliBasis::Y if is_state => (VType::Z, Phase::from((1_i64, 2_i64))),
1215        PauliBasis::Y => (VType::Z, Phase::from((-1_i64, 2_i64))),
1216    }
1217}
1218
1219fn sorted_pair(lhs: usize, rhs: usize) -> (usize, usize) {
1220    if lhs <= rhs { (lhs, rhs) } else { (rhs, lhs) }
1221}
1222
1223#[cfg(test)]
1224mod tests {
1225    use quizx::circuit::Circuit;
1226
1227    use super::*;
1228    use crate::{Basis, Block, BlockKind, BranchArm, CubeKind, Direction, MeasureTarget, Pipe};
1229
1230    fn circuit_graph(qubits: usize, gates: &[(&str, Vec<usize>)]) -> QuizxGraph {
1231        let mut circuit = Circuit::new(qubits);
1232        for (gate, operands) in gates {
1233            circuit.add_gate(gate, operands.clone());
1234        }
1235        circuit.to_graph()
1236    }
1237
1238    fn identity_source() -> BlockGraph {
1239        let mut source = BlockGraph::new();
1240        source.add_block(Block::new(IVec3::new(0, 0, 0), BlockKind::Port));
1241        source.add_block(Block::new(
1242            IVec3::new(0, 0, 1),
1243            BlockKind::Cube(CubeKind::ZXZ),
1244        ));
1245        source.add_block(Block::new(IVec3::new(0, 0, 2), BlockKind::Port));
1246        source.add_pipe(Pipe::new(IVec3::new(0, 0, 0), Direction::ZPLUS));
1247        source.add_pipe(Pipe::new(IVec3::new(0, 0, 1), Direction::ZPLUS));
1248        source
1249    }
1250
1251    fn add_test_branch(source: &mut BlockGraph, name: impl Into<String>, past: IVec3) -> IVec3 {
1252        let target = past + IVec3::Z;
1253        source.add_block(Block::new(past, BlockKind::Cube(CubeKind::ZXZ)));
1254        let arm = |kind| {
1255            BranchArm::new(
1256                vec![Block::new(target, kind)],
1257                vec![Pipe::new(past, Direction::ZPLUS)],
1258            )
1259        };
1260        source
1261            .try_add_branch_region(
1262                name,
1263                arm(BlockKind::Measurement(Basis::X)),
1264                arm(BlockKind::Cube(CubeKind::ZXZ)),
1265            )
1266            .expect("matching terminal arms")
1267    }
1268
1269    fn conditional_scalar_source() -> BlockGraph {
1270        let past = IVec3::new(0, 0, 0);
1271        let target = IVec3::new(0, 0, 1);
1272        let controller = IVec3::new(2, 0, 0);
1273        let mut source = BlockGraph::new();
1274        source.add_block(Block::new(controller, BlockKind::Cube(CubeKind::ZXZ)));
1275        assert_eq!(add_test_branch(&mut source, "b0", past), target);
1276        source
1277            .set_actions(vec![
1278                Action::Measure {
1279                    target: MeasureTarget::Node(controller),
1280                    name: "m".into(),
1281                },
1282                Action::Branch {
1283                    target,
1284                    condition: Expr::Var("m".into()),
1285                },
1286            ])
1287            .expect("conditional scalar family validates");
1288        source
1289    }
1290
1291    #[test]
1292    fn cancelled_named_measurement_normalization_keeps_its_typed_cause() {
1293        let source = crate::GalleryItem::ThreeBitAdder.build().flatten().unwrap();
1294        let token = crate::CancellationToken::new();
1295        let error = token.scope(|| {
1296            token.cancel();
1297            LogicalVerifier::new(&source).unwrap_err()
1298        });
1299        let mut cause: &(dyn std::error::Error + 'static) = &error;
1300        while !cause.is::<crate::ComputationCancelled>() {
1301            cause = cause
1302                .source()
1303                .expect("verifier preserves cancellation cause");
1304        }
1305        LogicalVerifier::new(&identity_source()).unwrap();
1306    }
1307
1308    #[test]
1309    fn identity_wire_matches_quizx_identity() {
1310        let source = identity_source();
1311        let verifier = LogicalVerifier::new(&source).unwrap();
1312        let (branch, diagram) = verifier.instantiate(&BranchAssignment::new()).unwrap();
1313        assert_eq!(branch.status(), BranchStatus::Instantiated);
1314        assert!(diagram.is_some());
1315        let expected = circuit_graph(1, &[]);
1316        let report = verifier.verify(&expected, 1, 0).unwrap();
1317
1318        assert_eq!(report.verified, 1);
1319    }
1320
1321    #[test]
1322    fn multiplex_port_is_a_three_leg_z_spider() {
1323        let mut source = BlockGraph::new();
1324        source.add_block(
1325            Block::new(IVec3::ZERO, BlockKind::Port)
1326                .with_port_role(PortRole::Multiplex)
1327                .unwrap(),
1328        );
1329        source.add_block(Block::new(IVec3::X, BlockKind::Cube(CubeKind::ZXZ)));
1330        source.add_pipe(Pipe::new(IVec3::ZERO, Direction::XPLUS));
1331
1332        let verifier = LogicalVerifier::new(&source).unwrap();
1333        assert_eq!(verifier.boundaries.inputs, [IVec3::ZERO]);
1334        assert_eq!(verifier.boundaries.outputs, [IVec3::ZERO]);
1335        let input = verifier.symbolic.inputs()[0];
1336        let output = verifier.symbolic.outputs()[0];
1337        assert_ne!(input, output);
1338        let spider = verifier.symbolic.neighbors(input).next().unwrap();
1339        assert_eq!(verifier.symbolic.vertex_type(spider), VType::Z);
1340        assert_eq!(verifier.symbolic.degree(spider), 3);
1341        assert!(verifier.symbolic.neighbors(spider).any(|v| v == output));
1342    }
1343
1344    #[test]
1345    fn conditional_source_selects_a_projected_static_verifier() {
1346        let verifier = LogicalVerifier::new(&conditional_scalar_source()).unwrap();
1347        let structural = verifier
1348            .structural
1349            .as_ref()
1350            .expect("structural source caches projected verifiers");
1351        assert_eq!(structural.arms.lock().unwrap().len(), 1);
1352
1353        let mut vertex_type_counts = Vec::new();
1354        for value in [false, true] {
1355            let assignment = BTreeMap::from([("m".to_string(), value)]);
1356            let (branch, diagram) = verifier.instantiate(&assignment).unwrap();
1357            assert_eq!(branch.status(), BranchStatus::Instantiated);
1358            assert_eq!(branch.classical_state().get("m"), Some(&value));
1359            let diagram = diagram.expect("selected static arm has a diagram");
1360            vertex_type_counts.push((
1361                diagram
1362                    .vertex_vec()
1363                    .iter()
1364                    .filter(|&&vertex| diagram.vertex_type(vertex) == VType::X)
1365                    .count(),
1366                diagram
1367                    .vertex_vec()
1368                    .iter()
1369                    .filter(|&&vertex| diagram.vertex_type(vertex) == VType::Z)
1370                    .count(),
1371            ));
1372        }
1373        assert_ne!(
1374            vertex_type_counts[0], vertex_type_counts[1],
1375            "authored false and true arms select different static diagrams"
1376        );
1377        assert_eq!(structural.arms.lock().unwrap().len(), 2);
1378    }
1379
1380    #[test]
1381    fn verifier_limit_counts_reachable_tuples_not_branch_sites() {
1382        let controller = IVec3::new(20, 0, 0);
1383        let mut source = BlockGraph::new();
1384        source.add_block(Block::new(controller, BlockKind::Cube(CubeKind::ZXZ)));
1385        let mut actions = vec![Action::Measure {
1386            target: MeasureTarget::Node(controller),
1387            name: "m".into(),
1388        }];
1389        for x in 0..9 {
1390            let past = IVec3::new(2 * x, 0, 0);
1391            let target = add_test_branch(&mut source, format!("b{x}"), past);
1392            actions.push(Action::Branch {
1393                target,
1394                condition: Expr::Var("m".into()),
1395            });
1396        }
1397        source.set_actions(actions).unwrap();
1398
1399        let verifier = LogicalVerifier::new(&source).unwrap();
1400        for value in [false, true] {
1401            verifier
1402                .instantiate(&BTreeMap::from([("m".to_owned(), value)]))
1403                .unwrap();
1404        }
1405        assert_eq!(verifier.structural.unwrap().arms.lock().unwrap().len(), 2);
1406    }
1407
1408    #[test]
1409    fn verifier_projection_cache_is_bounded_without_limiting_branch_count() {
1410        let mut source = BlockGraph::new();
1411        let mut actions = Vec::new();
1412        for x in 0..9 {
1413            let x = 3 * x;
1414            let past = IVec3::new(x, 0, 0);
1415            let controller = IVec3::new(x, 2, 0);
1416            source.add_block(Block::new(controller, BlockKind::Cube(CubeKind::ZXZ)));
1417            let target = add_test_branch(&mut source, format!("b{x}"), past);
1418            let name = format!("m{x}");
1419            actions.push(Action::Measure {
1420                target: MeasureTarget::Node(controller),
1421                name: name.clone(),
1422            });
1423            actions.push(Action::Branch {
1424                target,
1425                condition: Expr::Var(name),
1426            });
1427        }
1428        source.set_actions_lenient(actions).unwrap();
1429
1430        let verifier = LogicalVerifier::new(&source).unwrap();
1431        assert_eq!(
1432            verifier
1433                .structural
1434                .as_ref()
1435                .unwrap()
1436                .arms
1437                .lock()
1438                .unwrap()
1439                .len(),
1440            1
1441        );
1442        for mask in 0..70 {
1443            let assignment = (0..9)
1444                .map(|bit| (format!("m{}", 3 * bit), mask & (1 << bit) != 0))
1445                .collect();
1446            verifier.instantiate(&assignment).unwrap();
1447        }
1448        assert!(
1449            verifier
1450                .structural
1451                .as_ref()
1452                .unwrap()
1453                .arms
1454                .lock()
1455                .unwrap()
1456                .len()
1457                <= MAX_CACHED_STRUCTURAL_PROJECTIONS
1458        );
1459    }
1460
1461    #[test]
1462    fn named_selective_phases_survive_both_fills() {
1463        let source = crate::GalleryItem::TComparison
1464            .build()
1465            .materialize_root_graph()
1466            .expect("gallery flat projection");
1467        let verifier = LogicalVerifier::with_internal_measurements(&source).unwrap();
1468        let correction = verifier.correction.as_ref().unwrap();
1469        let site = IVec3::new(1, 0, 2);
1470        let sampled = correction.sampled_vars().collect::<BTreeSet<_>>();
1471        assert!(
1472            correction.measurement_vars().iter().any(|(key, parity)| {
1473                matches!(key, MeasurementKey::Node { pos, .. } if *pos == site)
1474                    && parity.iter().any(|variable| sampled.contains(&variable))
1475            }),
1476            "named selective must carry a nontrivial unnamed compensation"
1477        );
1478        for chosen in [PauliBasis::X, PauliBasis::Y] {
1479            let resolved = correction
1480                .resolve(&verifier.actions, &FxHashMap::from_iter([(site, chosen)]))
1481                .unwrap();
1482            let basis = resolved.basis;
1483            let phases = resolved.phases;
1484            for name in ["cmp", "parity"] {
1485                let row = basis
1486                    .derive_measurement_surface(name, i64::MAX)
1487                    .unwrap()
1488                    .stabilizer;
1489                let actual = phases
1490                    .iter()
1491                    .filter(|(key, _)| stabilizer_supports_measurement(&row, key))
1492                    .fold(Parity::default(), |parity, (_, term)| &parity + term);
1493                assert_eq!(
1494                    actual,
1495                    Parity::single(verifier.measurement_vars[name]),
1496                    "{chosen:?} {name}"
1497                );
1498            }
1499        }
1500    }
1501
1502    #[test]
1503    fn unnamed_phases_preserve_the_supplied_named_measurement_parities() {
1504        let source = crate::GalleryItem::CCZInjectedAnd
1505            .build()
1506            .materialize_root_graph()
1507            .expect("gallery flat projection");
1508        let zx = source.to_zx_graph().unwrap();
1509        let stabilizers = zx.stabilizers().unwrap();
1510        let verifier = LogicalVerifier::with_internal_measurements(&source).unwrap();
1511        let correction = verifier.correction.as_ref().unwrap();
1512        assert!(correction.sampled_vars().next().is_some());
1513        for generator in &stabilizers.generators {
1514            let Some(name) = generator.measurement_name() else {
1515                continue;
1516            };
1517            let actual = correction
1518                .measurement_vars()
1519                .iter()
1520                .filter(|(key, _)| stabilizer_supports_measurement(&generator.stabilizer, key))
1521                .fold(Parity::default(), |parity, (_, contribution)| {
1522                    &parity + contribution
1523                });
1524            assert_eq!(
1525                actual,
1526                Parity::single(verifier.measurement_vars[name]),
1527                "every unnamed phase assignment must preserve action input {name}"
1528            );
1529        }
1530    }
1531
1532    #[test]
1533    fn anonymous_class_weights_match_all_native_outcomes_with_an_odd_closed_relation() {
1534        use quizx::fscalar::FScalar;
1535        let mut source = BlockGraph::new();
1536        for x in [0, 1, 3] {
1537            source.add_block(Block::new(IVec3::new(x, 0, 0), BlockKind::Port));
1538            for z in [1, 2] {
1539                source.add_block(Block::new(
1540                    IVec3::new(x, 0, z),
1541                    BlockKind::Cube(CubeKind::XZX),
1542                ));
1543            }
1544            source.add_block(Block::new(
1545                IVec3::new(x, 0, 3),
1546                if x == 3 {
1547                    BlockKind::Port
1548                } else {
1549                    BlockKind::Measurement(Basis::X)
1550                },
1551            ));
1552            for z in 0..3 {
1553                source.add_pipe(Pipe::new(IVec3::new(x, 0, z), Direction::ZPLUS));
1554            }
1555        }
1556        for z in [1, 2] {
1557            source.add_pipe(Pipe::new(IVec3::new(0, 0, z), Direction::XPLUS));
1558        }
1559        source
1560            .set_actions(vec![
1561                Action::Measure {
1562                    target: MeasureTarget::Edge {
1563                        src: IVec3::Z,
1564                        dir: Direction::XPLUS,
1565                    },
1566                    name: "m".into(),
1567                },
1568                Action::Feedback {
1569                    targets: vec![FeedbackTarget {
1570                        pauli: PauliBasis::X,
1571                        target: IVec3::Z,
1572                        direction: None,
1573                    }],
1574                    condition: None,
1575                },
1576            ])
1577            .unwrap();
1578        // The second ZZ readout must disagree with the first: X lies between
1579        // the merges. Keep the first record's label, including that odd relation.
1580        let qasm = "OPENQASM 2.0; qreg q[5]; creg m[1]; creg again[1]; creg erased[2];
1581            reset q[3]; reset q[4]; cx q[0],q[3]; cx q[1],q[3]; measure q[3] -> m;
1582            x q[0]; cx q[0],q[4]; cx q[1],q[4]; measure q[4] -> again;
1583            h q[0]; h q[1]; measure q[0] -> erased[0]; measure q[1] -> erased[1];
1584            if(m==1) x q[2];";
1585        let options = crate::FeedbackOptions::default()
1586            .with_search_limit(0)
1587            .with_discarded_measurement("again[0]")
1588            .with_discarded_measurement("erased[0]")
1589            .with_discarded_measurement("erased[1]");
1590        let actions = crate::infer_feedback_with(qasm, &source, &options).unwrap();
1591        assert!(
1592            matches!(actions.as_slice(), [Action::Feedback { targets, condition: Some(Expr::Var(name)) }]
1593            if name == "m" && targets.len() == 1 && targets[0].pauli == PauliBasis::X && targets[0].target == IVec3::new(3,0,3))
1594        );
1595
1596        fn choi(maps: &[TensorF]) -> Vec<FScalar> {
1597            let width = maps[0].len();
1598            let mut result = vec![FScalar::zero(); width * width];
1599            for map in maps {
1600                for (i, a) in map.iter().enumerate() {
1601                    for (j, b) in map.iter().enumerate() {
1602                        result[i * width + j] += *a * b.conj();
1603                    }
1604                }
1605            }
1606            result
1607        }
1608        let verifier = LogicalVerifier::with_internal_measurements(&source).unwrap();
1609        let variables = verifier
1610            .correction
1611            .as_ref()
1612            .unwrap()
1613            .sampled_vars()
1614            .collect::<Vec<_>>();
1615        assert!(
1616            variables.len() <= 6,
1617            "keep exhaustive native enumeration small"
1618        );
1619        let mut resource = Circuit::new(3);
1620        for qubit in 0..3 {
1621            resource.add_gate("h", vec![qubit]);
1622        }
1623        resource.add_gate("ccz", vec![0, 1, 2]);
1624        let mut resource: QuizxGraph = resource.to_graph();
1625        resource.plug_inputs(&[quizx::graph::BasisElem::Z0; 3]);
1626        let mut saw_vanishing_class = false;
1627        for value in [false, true] {
1628            let assignment = BranchAssignment::from([("m".into(), value)]);
1629            let native = (0..1 << variables.len())
1630                .map(|mask| {
1631                    verifier
1632                        .instantiate_with(&assignment, |variable| {
1633                            mask & (1
1634                                << variables
1635                                    .iter()
1636                                    .position(|candidate| *candidate == variable)
1637                                    .unwrap())
1638                                != 0
1639                        })
1640                        .unwrap()
1641                        .1
1642                        .unwrap()
1643                })
1644                .collect::<Vec<_>>();
1645            let (_, representatives) = verifier.branch_family(&assignment, 64).unwrap();
1646            let open = native.iter().map(contract).collect::<Vec<_>>();
1647            let quotient = representatives.iter().map(contract).collect::<Vec<_>>();
1648            let nonzero = open.iter().filter(|map| tensor_is_nonzero(map)).count();
1649            assert!(
1650                nonzero > 0 && nonzero < native.len(),
1651                "odd closure must rule out native coordinates"
1652            );
1653            assert!(quotient.iter().all(tensor_is_nonzero));
1654            assert_eq!(nonzero % quotient.len(), 0);
1655            let multiplicity = FScalar::real((nonzero / quotient.len()) as f64);
1656            assert_eq!(
1657                choi(&open),
1658                choi(&quotient)
1659                    .into_iter()
1660                    .map(|value| value * multiplicity)
1661                    .collect::<Vec<_>>()
1662            );
1663            let prepared = |diagram: &QuizxGraph| {
1664                let mut prepared = resource.clone();
1665                prepared.plug(diagram);
1666                contract(&prepared)
1667            };
1668            let open = native.iter().map(prepared).collect::<Vec<_>>();
1669            let quotient = representatives.iter().map(prepared).collect::<Vec<_>>();
1670            saw_vanishing_class |= quotient.iter().any(|map| !tensor_is_nonzero(map));
1671            // Exactly the same multiplicity must work after non-Clifford
1672            // resource contraction; individual rays may vanish or change norm.
1673            assert_eq!(
1674                choi(&open),
1675                choi(&quotient)
1676                    .into_iter()
1677                    .map(|value| value * multiplicity)
1678                    .collect::<Vec<_>>()
1679            );
1680        }
1681        assert!(saw_vanishing_class);
1682    }
1683
1684    #[test]
1685    fn spatial_node_records_drive_joint_native_phase_parities() {
1686        let mut source = crate::GalleryItem::CZSpatialH
1687            .build()
1688            .materialize_root_graph()
1689            .expect("gallery flat projection");
1690        source
1691            .set_actions(vec![
1692                Action::Measure {
1693                    target: MeasureTarget::Node(IVec3::Z),
1694                    name: "a".into(),
1695                },
1696                Action::Measure {
1697                    target: MeasureTarget::Node(IVec3::X + IVec3::Z),
1698                    name: "b".into(),
1699                },
1700            ])
1701            .unwrap();
1702        assert_eq!(
1703            source
1704                .action_graph()
1705                .node_by_ordinal(1)
1706                .unwrap()
1707                .measurement,
1708            Some(crate::MeasurementObservable::Concrete(PauliBasis::Z))
1709        );
1710        let mut reversed = BlockGraph::new();
1711        for block in source
1712            .blocks()
1713            .cloned()
1714            .collect::<Vec<_>>()
1715            .into_iter()
1716            .rev()
1717        {
1718            reversed.add_block(block);
1719        }
1720        for pipe in source.pipes() {
1721            reversed.add_pipe(if pipe.is_hadamard() {
1722                Pipe::new(pipe.dst(), pipe.dir().negate()).with_hadamard()
1723            } else {
1724                pipe.clone()
1725            });
1726        }
1727        reversed.set_actions(source.actions()).unwrap();
1728        for mut source in [source, reversed] {
1729            // The X-readout spur is identity when unnamed outcomes are zero,
1730            // but its nonzero terminal outcome needs an output correction.
1731            let mut with_unnamed = source.clone();
1732            for z in [1, 2] {
1733                with_unnamed.add_block(Block::new(
1734                    IVec3::new(-1, 0, z),
1735                    BlockKind::Cube(CubeKind::XZX),
1736                ));
1737            }
1738            with_unnamed.add_pipe(Pipe::new(IVec3::new(-1, 0, 1), Direction::XPLUS));
1739            with_unnamed.add_pipe(Pipe::new(IVec3::new(-1, 0, 1), Direction::ZPLUS));
1740            for verifier in [
1741                LogicalVerifier::new(&source).unwrap(),
1742                LogicalVerifier::with_internal_measurements(&with_unnamed).unwrap(),
1743            ] {
1744                let internal_count = verifier
1745                    .correction
1746                    .as_ref()
1747                    .map_or(0, |correction| correction.sampled_vars().count());
1748                if verifier.correction.is_some() {
1749                    assert!(
1750                        (1..=8).contains(&internal_count),
1751                        "spur must supply bounded unnamed samples"
1752                    );
1753                }
1754                for a in [false, true] {
1755                    for b in [false, true] {
1756                        let mut gates = vec![("cz", vec![0, 1])];
1757                        if b {
1758                            gates.push(("z", vec![0]));
1759                        }
1760                        if a ^ b {
1761                            gates.push(("x", vec![1]));
1762                        }
1763                        let expected = circuit_graph(2, &gates);
1764                        let assignment = BTreeMap::from([("a".into(), a), ("b".into(), b)]);
1765                        let expected = contract(&expected);
1766                        let mut verified = false;
1767                        let mut verified_nonzero = false;
1768                        for mask in 0..(1usize << internal_count) {
1769                            let mut sample = 0;
1770                            let (branch, diagram) = verifier
1771                                .instantiate_with(&assignment, |_| {
1772                                    let value = mask & (1 << sample) != 0;
1773                                    sample += 1;
1774                                    value
1775                                })
1776                                .unwrap();
1777                            assert_eq!(sample, internal_count);
1778                            match compare_branch(branch, diagram, &expected).unwrap().status() {
1779                                BranchStatus::Verified => {
1780                                    verified = true;
1781                                    verified_nonzero |= mask != 0;
1782                                }
1783                                BranchStatus::Impossible => {}
1784                                status => panic!("unexpected branch status {status:?}"),
1785                            }
1786                        }
1787                        assert!(verified, "a={a}, b={b} has a nonzero branch");
1788                        if internal_count != 0 {
1789                            assert!(
1790                                verified_nonzero,
1791                                "a={a}, b={b} must verify with nonzero unnamed outcomes"
1792                            );
1793                        }
1794                    }
1795                }
1796            }
1797            source.add_block(Block::new(
1798                IVec3::new(5, 0, 0),
1799                BlockKind::Cube(CubeKind::ZXZ),
1800            ));
1801            source
1802                .add_action(Action::Measure {
1803                    target: MeasureTarget::Node(IVec3::new(5, 0, 0)),
1804                    name: "closed".into(),
1805                })
1806                .unwrap();
1807            let branch = LogicalVerifier::new(&source)
1808                .unwrap()
1809                .verify_branch(
1810                    &circuit_graph(2, &[("cz", vec![0, 1])]),
1811                    &BTreeMap::from([
1812                        ("a".into(), false),
1813                        ("b".into(), false),
1814                        ("closed".into(), true),
1815                    ]),
1816                )
1817                .unwrap();
1818            assert_eq!(branch.status(), BranchStatus::Impossible);
1819        }
1820    }
1821
1822    #[test]
1823    fn spatial_and_edge_records_compose_two_phases_on_one_wire() {
1824        let mut source = crate::GalleryItem::CZSpatialH
1825            .build()
1826            .materialize_root_graph()
1827            .expect("gallery flat projection");
1828        source
1829            .set_actions(vec![
1830                Action::Measure {
1831                    target: MeasureTarget::Node(IVec3::X + IVec3::Z),
1832                    name: "b".into(),
1833                },
1834                Action::Measure {
1835                    target: MeasureTarget::Edge {
1836                        src: IVec3::Z,
1837                        dir: Direction::XPLUS,
1838                    },
1839                    name: "m".into(),
1840                },
1841            ])
1842            .unwrap();
1843        let verifier = LogicalVerifier::new(&source).unwrap();
1844        for b in [false, true] {
1845            for m in [false, true] {
1846                let mut gates = vec![("cz", vec![0, 1])];
1847                if b {
1848                    gates.push(("z", vec![0]));
1849                }
1850                if m {
1851                    gates.push(("z", vec![1]));
1852                }
1853                let branch = verifier
1854                    .verify_branch(
1855                        &circuit_graph(2, &gates),
1856                        &BTreeMap::from([("b".into(), b), ("m".into(), m)]),
1857                    )
1858                    .unwrap();
1859                assert_eq!(branch.status(), BranchStatus::Verified);
1860            }
1861        }
1862    }
1863
1864    #[test]
1865    fn hadamard_edge_measurement_is_independent_of_storage_and_target_direction() {
1866        let original = crate::GalleryItem::CZSpatialH
1867            .build()
1868            .materialize_root_graph()
1869            .expect("gallery flat projection");
1870        let edge = original.pipes().find(|pipe| pipe.is_hadamard()).unwrap();
1871        for reverse_blocks in [false, true] {
1872            for reverse_pipe in [false, true] {
1873                for reverse_target in [false, true] {
1874                    let mut source = BlockGraph::new();
1875                    let mut blocks = original.blocks().cloned().collect::<Vec<_>>();
1876                    if reverse_blocks {
1877                        blocks.reverse();
1878                    }
1879                    for block in blocks {
1880                        source.add_block(block);
1881                    }
1882                    for pipe in original.pipes() {
1883                        source.add_pipe(if pipe.is_hadamard() && reverse_pipe {
1884                            crate::Pipe::new(pipe.dst(), pipe.dir().negate()).with_hadamard()
1885                        } else {
1886                            pipe.clone()
1887                        });
1888                    }
1889                    let (src, dir) = if reverse_target {
1890                        (edge.dst(), edge.dir().negate())
1891                    } else {
1892                        (edge.src(), edge.dir())
1893                    };
1894                    source
1895                        .set_actions(vec![Action::Measure {
1896                            target: MeasureTarget::Edge { src, dir },
1897                            name: "m".into(),
1898                        }])
1899                        .unwrap();
1900                    source.validate().unwrap();
1901                    for value in [false, true] {
1902                        let expected = if value {
1903                            circuit_graph(2, &[("cz", vec![0, 1]), ("z", vec![1])])
1904                        } else {
1905                            circuit_graph(2, &[("cz", vec![0, 1])])
1906                        };
1907                        let branch = LogicalVerifier::new(&source)
1908                            .unwrap()
1909                            .verify_branch(&expected, &BTreeMap::from([("m".into(), value)]))
1910                            .unwrap();
1911                        assert_eq!(branch.status(), BranchStatus::Verified);
1912                    }
1913                }
1914            }
1915        }
1916    }
1917
1918    #[test]
1919    fn basis_flipped_selective_matches_each_ordered_cap_map() {
1920        for spelling in ["XY", "YX", "XZ", "ZX", "YZ", "ZY"] {
1921            let source = crate::parse_inline_graph(&format!(
1922                "BLOG 1.0\nmodule main {{\nin control\nin q: data = 0\n0: Port [0,0,0]\n1: {spelling} [0,0,1]\n0 -> +Z\nresolve 1 if control\n}}\n"
1923            )).unwrap().materialize_flat_graph().unwrap();
1924            let flipped = source.flip_xz_basis().unwrap();
1925            assert_eq!(
1926                flipped.flip_xz_basis().unwrap().to_blog_text(),
1927                source.to_blog_text()
1928            );
1929            let verifier = LogicalVerifier::new(&flipped).unwrap();
1930            let choices = spelling.chars().collect::<Vec<_>>();
1931            for control in [false, true] {
1932                let before = choices[usize::from(!control)];
1933                let expected_source = BlockGraph::from_blog_text(&format!(
1934                    "BLOG 1.0\n0: Port [0,0,0]\n1: {before} [0,0,1]\n0 -> +Z\n"
1935                ))
1936                .unwrap();
1937                let (_, expected) = LogicalVerifier::new(&expected_source)
1938                    .unwrap()
1939                    .instantiate(&BTreeMap::new())
1940                    .unwrap();
1941                let mut dual = circuit_graph(1, &[("h", vec![0])]);
1942                dual.plug(&expected.unwrap());
1943                verifier
1944                    .verify_branch(&dual, &BTreeMap::from([("control".into(), control)]))
1945                    .unwrap();
1946            }
1947            let BlockKind::Selective(kind) = source.get_block(IVec3::Z).unwrap().kind() else {
1948                unreachable!()
1949            };
1950            let axis = match kind {
1951                crate::SelectiveKind::XY => Some(PauliBasis::X),
1952                crate::SelectiveKind::YZ => Some(PauliBasis::Z),
1953                crate::SelectiveKind::XZ => None,
1954            };
1955            if let Some(pauli) = axis {
1956                let mut corrected = source.clone();
1957                corrected
1958                    .add_action(Action::Feedback {
1959                        targets: vec![FeedbackTarget {
1960                            target: IVec3::Z,
1961                            pauli,
1962                            direction: None,
1963                        }],
1964                        condition: None,
1965                    })
1966                    .unwrap();
1967                let once = corrected.flip_xz_basis().unwrap();
1968                assert!(
1969                    !once
1970                        .actions()
1971                        .iter()
1972                        .any(|action| matches!(action, Action::Feedback { .. })),
1973                    "adjacent equal corrections cancel as a whole, without empty feedback"
1974                );
1975                assert_eq!(
1976                    once.flip_xz_basis().unwrap().to_blog_text(),
1977                    corrected.to_blog_text()
1978                );
1979            }
1980        }
1981    }
1982
1983    #[test]
1984    fn basis_flip_dualizes_fixed_resources_through_their_pipes() {
1985        for (kind, gate) in [(BlockKind::T, "t"), (BlockKind::Y, "s")] {
1986            for hadamard in [false, true] {
1987                for reversed in [false, true] {
1988                    let mut source = identity_source();
1989                    source.set_block_kind(IVec3::ZERO, kind).unwrap();
1990                    source.remove_pipe(IVec3::ZERO, IVec3::Z);
1991                    let mut pipe = Pipe::new(
1992                        if reversed { IVec3::Z } else { IVec3::ZERO },
1993                        if reversed {
1994                            Direction::ZMINUS
1995                        } else {
1996                            Direction::ZPLUS
1997                        },
1998                    )
1999                    .with_tag("resource")
2000                    .unwrap();
2001                    if hadamard {
2002                        pipe = pipe.with_hadamard();
2003                    }
2004                    source.add_pipe(pipe.clone());
2005                    let flipped = source.flip_xz_basis().unwrap();
2006                    let actual = flipped.get_pipe(IVec3::ZERO, IVec3::Z).unwrap();
2007                    assert_eq!(
2008                        (actual.src(), actual.dir(), actual.tag()),
2009                        (pipe.src(), pipe.dir(), pipe.tag())
2010                    );
2011                    assert_eq!(actual.is_hadamard(), !hadamard);
2012                    let mut gates = vec![("h", vec![0]), (gate, vec![0])];
2013                    if !hadamard {
2014                        gates.push(("h", vec![0]));
2015                    }
2016                    let mut expected = circuit_graph(1, &gates);
2017                    expected.plug_inputs(&[quizx::graph::BasisElem::Z0]);
2018                    LogicalVerifier::new(&flipped)
2019                        .unwrap()
2020                        .verify_branch(&expected, &BTreeMap::new())
2021                        .unwrap();
2022                    assert_eq!(
2023                        flipped.flip_xz_basis().unwrap().to_blog_text(),
2024                        source.to_blog_text()
2025                    );
2026                }
2027            }
2028        }
2029    }
2030
2031    #[test]
2032    fn basis_flip_preserves_native_t_comparison_postselection() {
2033        let source = crate::GalleryItem::TComparison
2034            .build()
2035            .materialize_root_graph()
2036            .expect("gallery flat projection");
2037        let flipped = source.flip_xz_basis().unwrap();
2038        assert!(
2039            flipped
2040                .actions()
2041                .iter()
2042                .any(|action| action == source.actions().last().unwrap())
2043        );
2044        for (index, mut graph) in [source, flipped].into_iter().enumerate() {
2045            let actions = graph
2046                .actions()
2047                .into_iter()
2048                .filter(|action| !matches!(action, Action::DiscardIf(_)))
2049                .collect();
2050            graph.set_actions(actions).unwrap();
2051            let verifier = LogicalVerifier::new(&graph).unwrap();
2052            for cmp in [false, true] {
2053                for parity in [false, true] {
2054                    let (_, branch) = verifier
2055                        .instantiate(&BTreeMap::from([
2056                            ("cmp".into(), cmp),
2057                            ("parity".into(), parity),
2058                        ]))
2059                        .unwrap();
2060                    assert_eq!(
2061                        tensor_is_nonzero(&contract(&branch.unwrap())),
2062                        !parity,
2063                        "variant={index} cmp={cmp} parity={parity}"
2064                    );
2065                }
2066            }
2067        }
2068    }
2069
2070    #[test]
2071    fn isolated_feedback_returns_missing_wire_error() {
2072        let mut source = BlockGraph::new();
2073        source.add_block(Block::new(IVec3::ZERO, BlockKind::Cube(CubeKind::ZXZ)));
2074        source.set_actions_unchecked(vec![Action::Feedback {
2075            targets: vec![FeedbackTarget {
2076                pauli: PauliBasis::X,
2077                target: IVec3::ZERO,
2078                direction: None,
2079            }],
2080            condition: None,
2081        }]);
2082        assert!(matches!(
2083            source.validate(),
2084            Err(crate::BlockGraphError::Stabilizer(
2085                crate::StabilizerError::FeedbackTargetWithoutWire { .. }
2086            ))
2087        ));
2088        assert!(matches!(LogicalVerifier::new(&source),
2089            Err(VerifyLogicalError::FeedbackTargetWithoutWire { target, pauli: PauliBasis::X })
2090                if target == IVec3::ZERO));
2091    }
2092
2093    #[test]
2094    fn unconditional_feedback_is_a_literal_pauli_on_the_wire() {
2095        let target = IVec3::new(0, 0, 1);
2096        let mut source = identity_source();
2097        source
2098            .set_actions(vec![Action::Feedback {
2099                targets: vec![FeedbackTarget {
2100                    pauli: PauliBasis::X,
2101                    target,
2102                    direction: None,
2103                }],
2104                condition: None,
2105            }])
2106            .unwrap();
2107
2108        let verifier = LogicalVerifier::new(&source).unwrap();
2109        verifier
2110            .verify(&circuit_graph(1, &[("x", vec![0])]), 1, 0)
2111            .unwrap();
2112    }
2113
2114    #[test]
2115    fn external_inputs_control_feedback_and_are_sampled() {
2116        let mut source = identity_source();
2117        source
2118            .set_actions_with_inputs(
2119                vec![Action::Feedback {
2120                    targets: vec![FeedbackTarget {
2121                        pauli: PauliBasis::X,
2122                        target: IVec3::Z,
2123                        direction: None,
2124                    }],
2125                    condition: Some(Expr::Var("enabled".into())),
2126                }],
2127                ["enabled".into()],
2128            )
2129            .unwrap();
2130        let verifier = LogicalVerifier::new(&source).unwrap();
2131        for enabled in [false, true] {
2132            let expected = if enabled {
2133                circuit_graph(1, &[("x", vec![0])])
2134            } else {
2135                circuit_graph(1, &[])
2136            };
2137            let branch = verifier
2138                .verify_branch(&expected, &BTreeMap::from([("enabled".into(), enabled)]))
2139                .unwrap();
2140            assert_eq!(branch.status(), BranchStatus::Verified);
2141            assert_eq!(branch.classical_state()["enabled"], enabled);
2142        }
2143        assert!(matches!(
2144            verifier.instantiate(&BTreeMap::new()),
2145            Err(VerifyLogicalError::MissingInputValue(name)) if name == "enabled"
2146        ));
2147        let sampled = (0..16)
2148            .map(|seed| verifier.sample_with_seed(seed).unwrap().0.classical_state()["enabled"])
2149            .collect::<BTreeSet<_>>();
2150        assert_eq!(sampled, BTreeSet::from([false, true]));
2151    }
2152
2153    #[test]
2154    fn external_inputs_select_structural_arms() {
2155        let mut source = BlockGraph::new();
2156        let target = add_test_branch(&mut source, "b0", IVec3::ZERO);
2157        source
2158            .set_actions_with_inputs(
2159                vec![Action::Branch {
2160                    target,
2161                    condition: Expr::Var("enabled".into()),
2162                }],
2163                ["enabled".into()],
2164            )
2165            .unwrap();
2166        let verifier = LogicalVerifier::new(&source).unwrap();
2167        for enabled in [false, true] {
2168            let (branch, diagram) = verifier
2169                .instantiate(&BTreeMap::from([("enabled".into(), enabled)]))
2170                .unwrap();
2171            assert_eq!(branch.classical_state()["enabled"], enabled);
2172            assert!(diagram.is_some());
2173        }
2174    }
2175
2176    #[test]
2177    fn repeated_t_parities_keep_the_named_branch_frame() {
2178        let expected = circuit_graph(1, &[("t", vec![0])]);
2179        let expected_tensor = contract(&expected);
2180        for layers in 1..=3 {
2181            let position = |x, z| IVec3::new(x, 0, z);
2182            let mut source = BlockGraph::new();
2183            source.add_block(Block::new(position(0, -1), BlockKind::T));
2184            source.add_block(Block::new(position(1, -1), BlockKind::Port));
2185            source.add_block(Block::new(
2186                position(0, layers),
2187                BlockKind::Selective(SelectiveKind::XY),
2188            ));
2189            source.add_block(Block::new(position(1, layers), BlockKind::Port));
2190            for layer in 0..layers {
2191                for x in 0..=1 {
2192                    source.add_block(Block::new(
2193                        position(x, layer),
2194                        BlockKind::Cube(CubeKind::ZXZ),
2195                    ));
2196                }
2197                source.add_pipe(Pipe::new(position(0, layer), crate::Direction::XPLUS));
2198            }
2199            for x in 0..=1 {
2200                for layer in -1..layers {
2201                    let pipe = Pipe::new(position(x, layer), Direction::ZPLUS);
2202                    source.add_pipe(if layer == -1 || layer == layers - 1 {
2203                        pipe.with_hadamard()
2204                    } else {
2205                        pipe
2206                    });
2207                }
2208            }
2209            source
2210                .set_actions(vec![
2211                    Action::Measure {
2212                        target: MeasureTarget::Edge {
2213                            src: IVec3::ZERO,
2214                            dir: Direction::XPLUS,
2215                        },
2216                        name: "m".into(),
2217                    },
2218                    Action::Resolve {
2219                        target: position(0, layers),
2220                        condition: Expr::Not(Box::new(Expr::Var("m".into()))),
2221                    },
2222                ])
2223                .unwrap();
2224
2225            // Pure ZX gives the independent reference: every repeated raw
2226            // parity agrees, and both all-zero and all-one branches implement T.
2227            let mut raw = source.clone();
2228            let mut actions = raw.actions();
2229            for layer in 1..layers {
2230                actions.push(Action::Measure {
2231                    target: MeasureTarget::Edge {
2232                        src: position(0, layer),
2233                        dir: Direction::XPLUS,
2234                    },
2235                    name: format!("raw{layer}"),
2236                });
2237            }
2238            raw.set_actions_deferred(actions).unwrap();
2239            let raw_verifier = LogicalVerifier::new(&raw).unwrap();
2240            for value in [false, true] {
2241                let mut assignment = BranchAssignment::from([("m".into(), value)]);
2242                assignment.extend((1..layers).map(|layer| (format!("raw{layer}"), value)));
2243                assert_eq!(
2244                    raw_verifier
2245                        .verify_branch(&expected, &assignment)
2246                        .unwrap()
2247                        .status(),
2248                    BranchStatus::Verified
2249                );
2250            }
2251
2252            let corrected = LogicalVerifier::with_internal_measurements(&source).unwrap();
2253            let mut verified = BTreeSet::new();
2254            for seed in 0..128 {
2255                let (branch, diagram) = corrected.sample_with_seed(seed).unwrap();
2256                let branch = compare_branch(branch, diagram, &expected_tensor).unwrap();
2257                if branch.status() == BranchStatus::Verified {
2258                    verified.insert(branch.classical_state()["m"]);
2259                }
2260            }
2261            assert_eq!(verified, BTreeSet::from([false, true]));
2262        }
2263    }
2264
2265    #[test]
2266    fn rejected_assignment_stops_without_contracting() {
2267        let source = crate::GalleryItem::TComparison
2268            .build()
2269            .materialize_root_graph()
2270            .expect("gallery flat projection");
2271        let verifier = LogicalVerifier::new(&source).unwrap();
2272        let assignment = BTreeMap::from([("cmp".to_string(), false), ("parity".to_string(), true)]);
2273
2274        let (branch, diagram) = verifier.instantiate(&assignment).unwrap();
2275
2276        assert_eq!(branch.status(), BranchStatus::Rejected);
2277        assert_eq!(branch.classical_state().get("cmp"), Some(&false));
2278        assert_eq!(branch.classical_state().get("parity"), Some(&true));
2279        assert!(diagram.is_none());
2280    }
2281
2282    #[test]
2283    fn resolve_can_read_a_presampled_future_measurement() {
2284        let selective = IVec3::new(0, 0, 0);
2285        let measured = IVec3::new(0, 0, 2);
2286        let mut source = BlockGraph::new();
2287        source.add_block(Block::new(
2288            selective,
2289            BlockKind::Selective(SelectiveKind::XZ),
2290        ));
2291        // The port makes both cap arms reachable.
2292        source.add_block(Block::new(IVec3::new(0, 0, -1), BlockKind::Port));
2293        source.add_block(Block::new(measured, BlockKind::Cube(CubeKind::ZXZ)));
2294        source.add_pipe(Pipe::new(IVec3::new(0, 0, -1), Direction::ZPLUS));
2295        source
2296            .set_actions(vec![
2297                Action::Measure {
2298                    target: MeasureTarget::Node(measured),
2299                    name: "future".to_string(),
2300                },
2301                Action::Resolve {
2302                    target: selective,
2303                    condition: Expr::Var("future".to_string()),
2304                },
2305            ])
2306            .unwrap();
2307        let verifier = LogicalVerifier::new(&source).unwrap();
2308
2309        let (branch, diagram) = verifier
2310            .instantiate(&BTreeMap::from([("future".to_string(), true)]))
2311            .unwrap();
2312
2313        assert_eq!(branch.status(), BranchStatus::Instantiated);
2314        assert_eq!(branch.classical_state().get("future"), Some(&true));
2315        assert!(diagram.is_some());
2316    }
2317}