1pub(crate) mod hierarchy;
4mod svg;
5
6use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
7
8use bloq_utils::PauliBasis;
9use glam::IVec3;
10use petgraph::algo::toposort;
11use petgraph::stable_graph::{NodeIndex, StableDiGraph};
12use petgraph::visit::{EdgeRef, IntoEdgeReferences};
13use rand::{Rng, RngExt};
14
15use crate::validate::InvalidActionError;
16use crate::{
17 Action, BlockGraph, BlockGraphError, BlockKind, BranchRegion, Expr, FeedbackTarget,
18 MeasureTarget, SelectiveKind, Stabilizer, StabilizerGenerator, selective_fixings,
19};
20
21#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct ResolveValueDomain {
29 values: Vec<Vec<bool>>,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
34#[non_exhaustive]
35pub enum ResolveDomainError {
36 #[error("{0}")]
38 Resource(#[from] BooleanResourceError),
39 #[error("resolve domain contains at least {observed} values")]
41 TooManyValues {
42 observed: usize,
44 },
45}
46
47impl ResolveDomainError {
48 pub(crate) fn into_stabilizer(
50 self,
51 phase: &'static str,
52 limit: usize,
53 ) -> crate::StabilizerError {
54 match self {
55 Self::Resource(error) => error.into(),
56 Self::TooManyValues { observed } => crate::StabilizerError::ResourceLimited {
57 phase,
58 observed,
59 limit,
60 },
61 }
62 }
63}
64
65impl ResolveValueDomain {
66 pub fn values(&self) -> &[Vec<bool>] {
68 &self.values
69 }
70
71 pub fn contains(&self, values: &[bool]) -> bool {
73 self.values
74 .binary_search_by(|candidate| candidate.as_slice().cmp(values))
75 .is_ok()
76 }
77}
78
79use bloq_utils::boolean::{BooleanDecisionDiagram, BooleanLimits, BooleanOp, BooleanResourceError};
80pub(crate) use bloq_utils::boolean::{DECISION_FALSE, DECISION_TRUE, DecisionId};
81
82#[derive(Debug, Default)]
83pub(crate) struct ResolveDecisionDiagram {
84 pub(crate) diagram: BooleanDecisionDiagram,
85 pub(crate) variable_indices: HashMap<String, usize>,
86 alias_cache: HashMap<String, DecisionId>,
87 pub(crate) roots: Vec<DecisionId>,
88}
89
90impl ResolveDecisionDiagram {
91 pub(crate) fn build(
92 exprs: &[&Expr],
93 aliases: &HashMap<&str, &Expr>,
94 limits: BooleanLimits,
95 ) -> Result<Option<Self>, BooleanResourceError> {
96 let mut diagram = Self {
97 diagram: BooleanDecisionDiagram::with_limits(limits),
98 ..Self::default()
99 };
100 for expr in exprs {
101 let Some(root) = diagram.expression(expr, aliases)? else {
102 return Ok(None);
103 };
104 diagram.roots.push(root);
105 }
106 Ok(Some(diagram))
107 }
108
109 fn expression(
110 &mut self,
111 expr: &Expr,
112 aliases: &HashMap<&str, &Expr>,
113 ) -> Result<Option<DecisionId>, BooleanResourceError> {
114 enum Step<'a> {
115 Expr(&'a Expr),
116 Alias(&'a str),
117 Not,
118 Binary(crate::BinaryOp),
119 }
120 let mut pending = vec![Step::Expr(expr)];
121 let mut values = Vec::new();
122 let mut visiting = HashSet::new();
123 while let Some(step) = pending.pop() {
124 self.diagram.charge(1)?;
125 match step {
126 Step::Expr(Expr::Var(name)) => {
127 if let Some(&root) = self.alias_cache.get(name) {
128 values.push(root);
129 } else if let Some(&resolved) = aliases.get(name.as_str()) {
130 if !visiting.insert(name.as_str()) {
131 return Ok(None);
132 }
133 pending.extend([Step::Alias(name), Step::Expr(resolved)]);
134 } else {
135 let next = self.variable_indices.len();
138 let variable = *self.variable_indices.entry(name.clone()).or_insert(next);
139 values.push(self.make_node(variable, DECISION_FALSE, DECISION_TRUE)?);
140 }
141 }
142 Step::Expr(Expr::Not(inner)) => {
143 pending.extend([Step::Not, Step::Expr(inner)]);
144 }
145 Step::Expr(Expr::Binary(operator, left, right)) => {
146 pending.extend([Step::Binary(*operator), Step::Expr(right), Step::Expr(left)]);
147 }
148 Step::Alias(name) => {
149 visiting.remove(name);
150 self.alias_cache.insert(
151 name.to_owned(),
152 *values
153 .last()
154 .expect("alias expression is evaluated before caching"),
155 );
156 }
157 Step::Not => {
158 let value = values.pop().expect("operand is evaluated before negation");
159 values.push(self.negate(value)?);
160 }
161 Step::Binary(operator) => {
162 let right = values
163 .pop()
164 .expect("right operand is evaluated before its operator");
165 let left = values
166 .pop()
167 .expect("left operand is evaluated before its operator");
168 values.push(self.apply(operator, left, right)?);
169 }
170 }
171 }
172 Ok(values.pop())
173 }
174
175 pub(crate) fn make_node(
176 &mut self,
177 variable: usize,
178 low: DecisionId,
179 high: DecisionId,
180 ) -> Result<DecisionId, BooleanResourceError> {
181 self.diagram.make_node(variable, low, high)
182 }
183 pub(crate) fn negate(&mut self, id: DecisionId) -> Result<DecisionId, BooleanResourceError> {
184 self.diagram.negate(id)
185 }
186 pub(crate) fn apply(
187 &mut self,
188 op: crate::BinaryOp,
189 left: DecisionId,
190 right: DecisionId,
191 ) -> Result<DecisionId, BooleanResourceError> {
192 let op = match op {
193 crate::BinaryOp::Xor => BooleanOp::Xor,
194 crate::BinaryOp::And => BooleanOp::And,
195 crate::BinaryOp::Or => BooleanOp::Or,
196 };
197 self.diagram.apply(op, left, right)
198 }
199 fn values(&mut self) -> Result<Vec<Vec<bool>>, BooleanResourceError> {
200 self.values_up_to(usize::MAX)
201 }
202 pub(crate) fn values_up_to(
203 &mut self,
204 limit: usize,
205 ) -> Result<Vec<Vec<bool>>, BooleanResourceError> {
206 self.diagram.values_up_to(&self.roots, limit)
207 }
208 fn contains(&mut self, values: &[bool]) -> Result<bool, BooleanResourceError> {
209 if values.len() != self.roots.len() {
210 return Ok(false);
211 }
212 let mut conjunction = DECISION_TRUE;
213 for (index, &value) in values.iter().enumerate() {
214 let root = self.roots[index];
215 let required = if value { root } else { self.negate(root)? };
216 conjunction = self.apply(crate::BinaryOp::And, conjunction, required)?;
217 if conjunction == DECISION_FALSE {
218 return Ok(false);
219 }
220 }
221 Ok(true)
222 }
223}
224#[derive(Debug, Clone, Copy, PartialEq, Eq)]
226pub enum MeasurementObservable {
227 Concrete(PauliBasis),
229 Selective(SelectiveKind),
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
235pub enum ActionDependency {
236 Classical,
238 SelectiveSupport,
240 BranchSupport,
243 FeedbackAnticommutation,
245 ReadoutParity,
247}
248
249impl ActionDependency {
250 pub const fn is_implicit(self) -> bool {
252 !matches!(self, Self::Classical)
253 }
254}
255
256#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
258#[non_exhaustive]
259pub(crate) enum SetMeasurementObservableError {
260 #[error("action ordinal {ordinal} is out of range for {len} actions")]
262 OrdinalOutOfRange {
263 ordinal: usize,
265 len: usize,
267 },
268 #[error("action at ordinal {ordinal} is not a measurement node")]
270 NotMeasurementNode {
271 ordinal: usize,
273 },
274}
275
276#[derive(Debug, Clone, PartialEq, Eq)]
278pub struct ActionOwner {
279 pub definition: String,
281 pub instance_path: String,
283}
284
285#[derive(Debug, Clone, PartialEq)]
287pub struct ActionNode {
288 pub ordinal: usize,
290 pub action: Action,
292 pub owner: Option<ActionOwner>,
296 pub measurement: Option<MeasurementObservable>,
300 pub measurement_stabilizer: Option<Stabilizer>,
302}
303
304#[derive(Debug, Clone, Default)]
311pub struct ActionDag {
312 graph: StableDiGraph<ActionNode, ActionDependency>,
313 ordered: Vec<NodeIndex<u32>>,
314 inputs: BTreeSet<String>,
315 branch_regions: Vec<BranchRegion>,
316 analyzed: bool,
317}
318
319impl ActionDag {
320 pub fn resolve_sites_for_measurement(&self, name: &str) -> Option<Vec<IVec3>> {
323 let consumer = self
324 .ordered_nodes()
325 .find(|node| {
326 matches!(&node.action, Action::Measure { name: measured, .. } if measured == name)
327 })?
328 .ordinal;
329 let mut sites = self
330 .dependencies()
331 .filter_map(|(predecessor, target, dependency)| {
332 if target != consumer || dependency != ActionDependency::SelectiveSupport {
333 return None;
334 }
335 match self.node_by_ordinal(predecessor)?.action {
336 Action::Resolve { target, .. } => Some(target),
337 _ => None,
338 }
339 })
340 .collect::<Vec<_>>();
341 sites.sort_unstable_by_key(IVec3::to_array);
342 sites.dedup();
343 Some(sites)
344 }
345
346 pub fn from_actions(actions: &[Action]) -> Self {
349 Self::from_actions_with_inputs(actions, std::iter::empty())
350 }
351
352 pub(crate) fn from_actions_with_inputs(
353 actions: &[Action],
354 inputs: impl IntoIterator<Item = String>,
355 ) -> Self {
356 let mut dag = ActionDag::default();
357 dag.inputs.extend(inputs);
358 let mut defs = HashMap::<&str, NodeIndex<u32>>::new();
359 let mut resolve_targets = HashMap::<IVec3, NodeIndex<u32>>::new();
360
361 for (ordinal, action) in actions.iter().enumerate() {
362 let node = dag.graph.add_node(ActionNode {
363 ordinal,
364 action: action.clone(),
365 owner: None,
366 measurement: None,
367 measurement_stabilizer: None,
368 });
369 dag.ordered.push(node);
370
371 if let Some(name) = defines_name(action) {
372 defs.entry(name).or_insert(node);
373 }
374
375 if let Action::Resolve { target, .. } = action {
376 resolve_targets.entry(*target).or_insert(node);
377 }
378 }
379
380 for (ordinal, action) in actions.iter().enumerate() {
381 let node = dag.ordered[ordinal];
382 for used in action.referenced_names() {
383 let Some(&pred) = defs.get(used) else {
384 continue;
385 };
386 dag.add_dependency(pred, node, ActionDependency::Classical);
387 }
388
389 if let Action::Measure {
390 target: MeasureTarget::Node(pos),
391 ..
392 } = action
393 && let Some(&pred) = resolve_targets.get(pos)
394 {
395 dag.add_dependency(pred, node, ActionDependency::SelectiveSupport);
396 }
397 }
398
399 dag
400 }
401
402 pub fn inputs(&self) -> impl Iterator<Item = &str> {
404 self.inputs.iter().map(String::as_str)
405 }
406
407 pub fn ordered_nodes(&self) -> impl Iterator<Item = &ActionNode> {
409 self.ordered.iter().map(|&idx| &self.graph[idx])
410 }
411
412 pub(crate) fn branch_region(&self, target: IVec3) -> Option<&BranchRegion> {
415 self.branch_regions
416 .iter()
417 .find(|region| region.target == target)
418 }
419
420 pub fn dependencies(&self) -> impl Iterator<Item = (usize, usize, ActionDependency)> + '_ {
422 self.graph.edge_references().map(|edge| {
423 (
424 self.graph[edge.source()].ordinal,
425 self.graph[edge.target()].ordinal,
426 *edge.weight(),
427 )
428 })
429 }
430
431 pub const fn is_analyzed(&self) -> bool {
434 self.analyzed || self.ordered.is_empty()
435 }
436
437 pub(crate) fn attach_guarded_dependencies(
438 &mut self,
439 edges: impl IntoIterator<Item = (usize, usize, ActionDependency)>,
440 ) -> Result<(), BlockGraphError> {
441 for (from, to, reason) in edges {
442 self.add_dependency(self.ordered[from], self.ordered[to], reason);
443 }
444 self.validate_semantics()?;
445 self.analyzed = true;
446 Ok(())
447 }
448
449 pub fn node_by_ordinal(&self, ordinal: usize) -> Option<&ActionNode> {
451 self.ordered.get(ordinal).map(|&idx| &self.graph[idx])
452 }
453
454 pub(crate) fn set_measurement_observable(
461 &mut self,
462 ordinal: usize,
463 measurement: MeasurementObservable,
464 ) -> Result<(), SetMeasurementObservableError> {
465 let Some(&idx) = self.ordered.get(ordinal) else {
466 return Err(SetMeasurementObservableError::OrdinalOutOfRange {
467 ordinal,
468 len: self.ordered.len(),
469 });
470 };
471 if !matches!(self.graph[idx].action, Action::Measure { .. }) {
472 return Err(SetMeasurementObservableError::NotMeasurementNode { ordinal });
473 }
474 self.graph[idx].measurement = Some(measurement);
475 Ok(())
476 }
477
478 pub fn depends_on(&self, node: &ActionNode, pred_ordinal: usize) -> bool {
481 let Some(&node_idx) = self.ordered.get(node.ordinal) else {
482 return false;
483 };
484 let Some(&pred_idx) = self.ordered.get(pred_ordinal) else {
485 return false;
486 };
487 self.graph.find_edge(pred_idx, node_idx).is_some()
488 }
489
490 pub(crate) fn attach_readout_dependencies(
492 &mut self,
493 generators: &[StabilizerGenerator],
494 zx: Option<&crate::ZXGraph>,
495 ) -> Result<(), BlockGraphError> {
496 let fixings = selective_fixings(generators);
497 let no_fixings = Default::default();
498 let measurement_nodes = self
499 .ordered
500 .iter()
501 .filter_map(|&node| match &self.graph[node].action {
502 Action::Measure { name, .. } => Some((name.clone(), node)),
503 _ => None,
504 })
505 .collect::<BTreeMap<_, _>>();
506 let resolve_nodes = self
507 .ordered
508 .iter()
509 .filter_map(|&node| match self.graph[node].action {
510 Action::Resolve { target, .. } => Some((target, node)),
511 _ => None,
512 })
513 .collect::<HashMap<_, _>>();
514 let branch_nodes = self
515 .ordered
516 .iter()
517 .filter_map(|&node| match self.graph[node].action {
518 Action::Branch { target, .. } => Some((target, node)),
519 _ => None,
520 })
521 .collect::<HashMap<_, _>>();
522 let branch_regions = self
523 .branch_regions
524 .iter()
525 .filter_map(|region| {
526 branch_nodes
527 .get(®ion.target)
528 .copied()
529 .map(|node| (node, region.clone()))
530 })
531 .collect::<Vec<_>>();
532 let feedback_nodes = self
533 .ordered
534 .iter()
535 .filter_map(|&node| match &self.graph[node].action {
536 Action::Feedback { targets, .. } => Some((node, targets.clone())),
537 _ => None,
538 })
539 .collect::<Vec<_>>();
540
541 let surfaces = generators
542 .iter()
543 .filter_map(|generator| Some((generator, generator.named_readout()?)))
544 .flat_map(|(generator, recipe)| {
545 recipe.surfaces().map(move |surface| (generator, surface))
546 });
547 for (generator, surface) in surfaces {
548 let name = generator.measurement_name().expect("named readout");
549 let fixings = if generator.readout_plan().is_some() {
550 &no_fixings
551 } else {
552 &fixings
553 };
554 let Some(&measurement_node) = measurement_nodes.get(name) else {
555 continue;
556 };
557 self.graph[measurement_node].measurement_stabilizer = Some(surface.clone());
558
559 let selective_sites = reachable_selective_sites(surface, fixings);
560 for site in &selective_sites {
561 if let Some(&resolve_node) = resolve_nodes.get(site) {
562 self.add_dependency(
563 resolve_node,
564 measurement_node,
565 ActionDependency::SelectiveSupport,
566 );
567 }
568 }
569 for (branch_node, region) in &branch_regions {
570 if measurement_touches_branch_region(surface, region, &selective_sites, fixings) {
571 self.add_dependency(
572 *branch_node,
573 measurement_node,
574 ActionDependency::BranchSupport,
575 );
576 }
577 }
578 for (feedback_node, targets) in &feedback_nodes {
579 if feedback_perturbs_measurement(targets, surface, &selective_sites, fixings, zx) {
580 self.add_dependency(
581 *feedback_node,
582 measurement_node,
583 ActionDependency::FeedbackAnticommutation,
584 );
585 }
586 }
587 }
588
589 for generator in generators {
590 if let Some(recipe) = generator.readout_plan() {
591 let name = generator.measurement_name().expect("named readout");
592 let measurement_node = measurement_nodes[name];
593 if generator.uses_source_outcomes() {
594 for ordinal in recipe
595 .branches
596 .iter()
597 .flat_map(|(_, branch)| &branch.readout_ordinals)
598 {
599 let predecessor = generators[*ordinal]
600 .measurement_name()
601 .expect("readout recipe references a named row");
602 self.add_dependency(
603 measurement_nodes[predecessor],
604 measurement_node,
605 ActionDependency::ReadoutParity,
606 );
607 }
608 }
609 self.graph[measurement_node].measurement_stabilizer =
610 Some(recipe.branches[0].1.stabilizer.clone());
611 for site in &recipe.sites {
612 if let Some(&resolve_node) = resolve_nodes.get(site) {
613 self.add_dependency(
614 resolve_node,
615 measurement_node,
616 ActionDependency::SelectiveSupport,
617 );
618 }
619 }
620 }
621 }
622
623 for (name, &node) in &measurement_nodes {
624 if self.graph[node].measurement_stabilizer.is_none() {
625 return Err(crate::StabilizerError::MeasurementSurfaceUnavailable {
626 mvar: name.clone(),
627 }
628 .into());
629 }
630 }
631
632 self.validate_semantics()?;
633 self.analyzed = true;
634 Ok(())
635 }
636
637 fn add_dependency(
638 &mut self,
639 predecessor: NodeIndex<u32>,
640 consumer: NodeIndex<u32>,
641 dependency: ActionDependency,
642 ) {
643 if self
644 .graph
645 .edges_connecting(predecessor, consumer)
646 .any(|edge| *edge.weight() == dependency)
647 {
648 return;
649 }
650 self.graph.add_edge(predecessor, consumer, dependency);
651 }
652
653 fn validate_semantics(&self) -> Result<(), BlockGraphError> {
654 let mut defined_vars = self.inputs.iter().cloned().collect::<HashSet<_>>();
655 let mut resolve_targets = HashSet::new();
656 let mut branch_targets = HashSet::new();
657 let mut measurement_targets = HashSet::new();
658 let bucket_measurements = self
659 .ordered_nodes()
660 .filter_map(|node| match &node.action {
661 Action::Measure { name, .. } => Some(name.clone()),
662 _ => None,
663 })
664 .collect::<HashSet<_>>();
665
666 for action in self.ordered_nodes().map(|node| &node.action) {
667 BlockGraph::validate_action_syntax(action)?;
668 match action {
669 Action::Measure { target, name } => {
670 if defined_vars.contains(name) {
671 return Err(InvalidActionError::VariableRedefinition(name.clone()).into());
672 }
673 let site_key = measurement_site_key(target)?;
674 if !measurement_targets.insert(site_key) {
675 return Err(InvalidActionError::DuplicateMeasurementTarget(*target).into());
676 }
677 defined_vars.insert(name.clone());
678 }
679 Action::Let { name, expr } => {
680 if defined_vars.contains(name) {
681 return Err(InvalidActionError::VariableRedefinition(name.clone()).into());
682 }
683 check_expr_vars(expr, &defined_vars, &bucket_measurements)?;
684 defined_vars.insert(name.clone());
685 }
686 Action::DiscardIf(expr) => {
687 check_expr_vars(expr, &defined_vars, &bucket_measurements)?;
688 }
689 Action::Resolve { target, condition } => {
690 check_expr_vars(condition, &defined_vars, &bucket_measurements)?;
691 if !resolve_targets.insert(*target) {
692 return Err(InvalidActionError::DuplicateResolveTarget(*target).into());
693 }
694 }
695 Action::Branch { target, condition } => {
696 check_expr_vars(condition, &defined_vars, &bucket_measurements)?;
697 if !branch_targets.insert(*target) {
698 return Err(InvalidActionError::DuplicateBranchTarget(*target).into());
699 }
700 }
701 Action::Feedback { condition, .. } => {
702 if let Some(condition) = condition {
703 check_expr_vars(condition, &defined_vars, &bucket_measurements)?;
704 }
705 }
706 }
707 }
708
709 toposort(&self.graph, None).map_err(|cycle| {
710 let ordinal = self.graph[cycle.node_id()].ordinal;
711 BlockGraphError::InvalidAction(InvalidActionError::DependencyCycle { ordinal })
712 })?;
713
714 Ok(())
715 }
716
717 fn validate_graph_constraints(&mut self, graph: &BlockGraph) -> Result<(), BlockGraphError> {
718 let mut selective_positions = graph
719 .blocks()
720 .filter(|block| block.kind.is_selective())
721 .map(|block| block.pos)
722 .collect::<HashSet<_>>();
723
724 for action in self.ordered_nodes().map(|node| &node.action) {
725 match action {
726 Action::Measure { target, .. } => {
727 ensure_measure_target_is_valid(graph, target)?;
728 }
729 Action::Resolve { target, .. } => {
730 match graph.get_block(*target) {
731 Some(b) if b.kind.is_selective() => {}
732 _ => {
733 return Err(InvalidActionError::InvalidResolveTarget(*target).into());
734 }
735 }
736 selective_positions.remove(target);
737 }
738 Action::Branch { target, .. } => {
739 if graph.branch_by_target(*target).is_none() {
740 return Err(InvalidActionError::InvalidBranchTarget(*target).into());
741 }
742 }
743 Action::Feedback { targets, .. } => {
744 for feedback in targets {
745 ensure_feedback_target_exists(graph, feedback.target)?;
746 if let Some(dir) = feedback.direction {
747 let dst = crate::checked_add_position(feedback.target, dir.to_ivec3())?;
748 if graph.get_pipe(feedback.target, dst).is_none() {
749 return Err(InvalidActionError::InvalidFeedbackTarget(
750 feedback.target,
751 )
752 .into());
753 }
754 }
755 }
756 }
757 Action::Let { .. } | Action::DiscardIf(..) => {}
758 }
759 }
760
761 if let Some(&missing) = selective_positions.iter().min_by_key(|pos| pos.to_array()) {
762 return Err(InvalidActionError::MissingResolveForSelective { target: missing }.into());
763 }
764
765 let actions = self
766 .ordered_nodes()
767 .map(|node| node.action.clone())
768 .collect::<Vec<_>>();
769 self.branch_regions = crate::branch::validate_regions_for_actions(graph, &actions)?;
770
771 Ok(())
772 }
773
774 pub(crate) fn validate(
775 &mut self,
776 source_graph: Option<&BlockGraph>,
777 ) -> Result<(), BlockGraphError> {
778 self.validate_semantics()?;
779
780 if let Some(graph) = source_graph {
781 self.validate_graph_constraints(graph)?;
782 }
783
784 Ok(())
785 }
786
787 pub fn resolve_value_domain(
797 &self,
798 targets: &[IVec3],
799 ) -> Result<ResolveValueDomain, BooleanResourceError> {
800 let Some(mut diagram) = self.resolve_decision_diagram(targets)? else {
801 return Ok(ResolveValueDomain { values: Vec::new() });
802 };
803 Ok(ResolveValueDomain {
804 values: diagram.values()?,
805 })
806 }
807
808 pub fn resolve_value_domain_bounded(
816 &self,
817 targets: &[IVec3],
818 limit: usize,
819 ) -> Result<ResolveValueDomain, ResolveDomainError> {
820 self.resolve_value_domain_bounded_with_limits(targets, limit, BooleanLimits::DEFAULT)
821 }
822
823 pub fn resolve_value_domain_bounded_with_limits(
829 &self,
830 targets: &[IVec3],
831 limit: usize,
832 boolean_limits: BooleanLimits,
833 ) -> Result<ResolveValueDomain, ResolveDomainError> {
834 let Some(mut diagram) =
835 self.resolve_decision_diagram_with_limits(targets, boolean_limits)?
836 else {
837 return Ok(ResolveValueDomain { values: Vec::new() });
838 };
839 let values = diagram.values_up_to(limit.saturating_add(1))?;
840 if values.len() > limit {
841 Err(ResolveDomainError::TooManyValues {
842 observed: values.len(),
843 })
844 } else {
845 Ok(ResolveValueDomain { values })
846 }
847 }
848
849 pub(crate) fn first_resolve_values_with_limits(
850 &self,
851 targets: &[IVec3],
852 limits: BooleanLimits,
853 ) -> Result<Option<Vec<bool>>, BooleanResourceError> {
854 let Some(mut diagram) = self.resolve_decision_diagram_with_limits(targets, limits)? else {
855 return Ok(None);
856 };
857 Ok(diagram.values_up_to(1)?.pop())
858 }
859
860 pub(crate) fn complete_resolve_values_with_limits(
861 &self,
862 targets: &[IVec3],
863 fixed: &[(IVec3, bool)],
864 limits: BooleanLimits,
865 ) -> Result<Option<Vec<bool>>, BooleanResourceError> {
866 let Some(mut diagram) = self.resolve_decision_diagram_with_limits(targets, limits)? else {
867 return Ok(None);
868 };
869 let mut conjunction = DECISION_TRUE;
870 for &(site, value) in fixed {
871 let Some(index) = targets.iter().position(|&target| target == site) else {
872 return Ok(None);
873 };
874 let root = diagram.roots[index];
875 let required = if value { root } else { diagram.negate(root)? };
876 conjunction = diagram.apply(crate::BinaryOp::And, conjunction, required)?;
877 }
878 if conjunction == DECISION_FALSE {
879 return Ok(None);
880 }
881 let mut values = Vec::new();
882 for index in 0..targets.len() {
883 let root = diagram.roots[index];
884 let false_root = diagram.negate(root)?;
885 let when_false = diagram.apply(crate::BinaryOp::And, conjunction, false_root)?;
886 let value = when_false == DECISION_FALSE;
887 conjunction = if value {
888 diagram.apply(crate::BinaryOp::And, conjunction, root)?
889 } else {
890 when_false
891 };
892 values.push(value);
893 }
894 Ok(Some(values))
895 }
896
897 pub fn resolve_values_are_reachable(
905 &self,
906 targets: &[IVec3],
907 values: &[bool],
908 ) -> Result<bool, BooleanResourceError> {
909 let Some(mut diagram) = self.resolve_decision_diagram(targets)? else {
910 return Ok(false);
911 };
912 diagram.contains(values)
913 }
914
915 pub(crate) fn resolve_xor_is_with_limits(
916 &self,
917 targets: &[IVec3],
918 expected: bool,
919 limits: BooleanLimits,
920 ) -> Result<bool, BooleanResourceError> {
921 let Some(mut diagram) = self.resolve_decision_diagram_with_limits(targets, limits)? else {
922 return Ok(false);
923 };
924 let roots = std::mem::take(&mut diagram.roots);
925 let parity = roots.into_iter().try_fold(DECISION_FALSE, |parity, root| {
926 diagram.apply(crate::BinaryOp::Xor, parity, root)
927 })?;
928 Ok(parity
929 == if expected {
930 DECISION_TRUE
931 } else {
932 DECISION_FALSE
933 })
934 }
935
936 pub(crate) fn branch_values_up_to(
937 &self,
938 targets: &[IVec3],
939 limit: usize,
940 ) -> Result<Vec<Vec<bool>>, BooleanResourceError> {
941 self.branch_values_up_to_with_limits(targets, limit, BooleanLimits::DEFAULT)
942 }
943
944 pub(crate) fn branch_values_up_to_with_limits(
945 &self,
946 targets: &[IVec3],
947 limit: usize,
948 limits: BooleanLimits,
949 ) -> Result<Vec<Vec<bool>>, BooleanResourceError> {
950 let Some(mut diagram) = self.branch_decision_diagram(targets, limits)? else {
951 return Ok(Vec::new());
952 };
953 diagram.values_up_to(limit)
954 }
955
956 pub(crate) fn resolve_decision_diagram(
957 &self,
958 targets: &[IVec3],
959 ) -> Result<Option<ResolveDecisionDiagram>, BooleanResourceError> {
960 self.resolve_decision_diagram_with_limits(targets, BooleanLimits::DEFAULT)
961 }
962
963 pub(crate) fn resolve_decision_diagram_with_limits(
964 &self,
965 targets: &[IVec3],
966 limits: BooleanLimits,
967 ) -> Result<Option<ResolveDecisionDiagram>, BooleanResourceError> {
968 let exprs = targets
969 .iter()
970 .map(|&target| self.resolve_expr(target))
971 .collect::<Option<Vec<_>>>();
972 match exprs {
973 Some(exprs) => ResolveDecisionDiagram::build(&exprs, &self.let_bindings(), limits),
974 None => Ok(None),
975 }
976 }
977
978 fn branch_decision_diagram(
979 &self,
980 targets: &[IVec3],
981 limits: BooleanLimits,
982 ) -> Result<Option<ResolveDecisionDiagram>, BooleanResourceError> {
983 let exprs = targets
984 .iter()
985 .map(|&target| self.branch_expr(target))
986 .collect::<Option<Vec<_>>>();
987 match exprs {
988 Some(exprs) => ResolveDecisionDiagram::build(&exprs, &self.let_bindings(), limits),
989 None => Ok(None),
990 }
991 }
992
993 pub(crate) fn sample_resolve_values(
998 &self,
999 targets: &[IVec3],
1000 rng: &mut impl Rng,
1001 ) -> HashMap<IVec3, bool> {
1002 let exprs = targets
1003 .iter()
1004 .filter_map(|&target| {
1005 self.ordered_nodes()
1006 .find_map(|node| match &node.action {
1007 Action::Resolve {
1008 target: candidate,
1009 condition,
1010 } if *candidate == target => Some(condition),
1011 _ => None,
1012 })
1013 .map(|expr| (target, expr))
1014 })
1015 .collect::<Vec<_>>();
1016 let aliases = self.let_bindings();
1017 let mut variables = BTreeSet::new();
1018 let mut visited = HashSet::new();
1019 for (_, expr) in &exprs {
1020 expand_expr_vars_to_measurements(expr, &aliases, &mut variables, &mut visited);
1021 }
1022 let values = variables
1023 .into_iter()
1024 .map(|name| (name, rng.random()))
1025 .collect::<HashMap<_, _>>();
1026
1027 let mut alias_values = HashMap::new();
1028 exprs
1029 .into_iter()
1030 .map(|(target, expr)| {
1031 (
1032 target,
1033 evaluate_expr_with(expr, &aliases, &|name| values[name], &mut alias_values),
1034 )
1035 })
1036 .collect()
1037 }
1038
1039 fn resolve_expr(&self, target: IVec3) -> Option<&Expr> {
1040 self.ordered_nodes().find_map(|node| match &node.action {
1041 Action::Resolve {
1042 target: candidate,
1043 condition,
1044 } if *candidate == target => Some(condition),
1045 _ => None,
1046 })
1047 }
1048
1049 fn branch_expr(&self, target: IVec3) -> Option<&Expr> {
1050 self.ordered_nodes().find_map(|node| match &node.action {
1051 Action::Branch {
1052 target: candidate,
1053 condition,
1054 } if *candidate == target => Some(condition),
1055 _ => None,
1056 })
1057 }
1058
1059 fn let_bindings(&self) -> HashMap<&str, &Expr> {
1060 self.ordered_nodes()
1061 .filter_map(|node| match &node.action {
1062 Action::Let { name, expr } => Some((name.as_str(), expr)),
1063 _ => None,
1064 })
1065 .collect()
1066 }
1067}
1068
1069fn reachable_selective_sites(
1070 row: &Stabilizer,
1071 fixings: &crate::SelectiveFixings<'_>,
1072) -> Vec<IVec3> {
1073 let mut sites = fixings
1074 .keys()
1075 .copied()
1076 .filter(|site| {
1077 row.interior_nodes
1078 .get(site)
1079 .is_some_and(|pauli| *pauli != crate::Pauli::I)
1080 })
1081 .collect::<HashSet<_>>();
1082 let mut pending = sites.iter().copied().collect::<Vec<_>>();
1083 while let Some(site) = pending.pop() {
1084 for (&pos, &pauli) in &fixings[&site].row.interior_nodes {
1085 if pauli != crate::Pauli::I && fixings.contains_key(&pos) && sites.insert(pos) {
1086 pending.push(pos);
1087 }
1088 }
1089 }
1090 let mut sites = sites.into_iter().collect::<Vec<_>>();
1091 sites.sort_unstable_by_key(|pos| (pos.x, pos.y, pos.z));
1092 sites
1093}
1094
1095fn measurement_touches_branch_region(
1096 row: &Stabilizer,
1097 region: &BranchRegion,
1098 selective_sites: &[IVec3],
1099 fixings: &crate::SelectiveFixings<'_>,
1100) -> bool {
1101 stabilizer_touches_branch_region(row, region)
1102 || selective_sites
1103 .iter()
1104 .any(|site| stabilizer_touches_branch_region(fixings[site].row, region))
1105}
1106
1107fn stabilizer_touches_branch_region(row: &Stabilizer, region: &BranchRegion) -> bool {
1108 row.interior_nodes
1111 .iter()
1112 .any(|(&position, &pauli)| pauli != crate::Pauli::I && region.contains_block(position))
1113 || row.interior_edges.iter().any(|(&(first, second), &pauli)| {
1114 pauli != crate::Pauli::I
1115 && (region.contains_block(first) || region.contains_block(second))
1116 })
1117}
1118
1119fn feedback_perturbs_measurement(
1120 targets: &[FeedbackTarget],
1121 row: &Stabilizer,
1122 selective_sites: &[IVec3],
1123 fixings: &crate::SelectiveFixings<'_>,
1124 zx: Option<&crate::ZXGraph>,
1125) -> bool {
1126 row.odd_anticommutes_feedback(targets, zx)
1127 || selective_sites
1128 .iter()
1129 .any(|site| fixings[site].row.odd_anticommutes_feedback(targets, zx))
1130}
1131
1132fn evaluate_expr_with(
1133 expr: &Expr,
1134 aliases: &HashMap<&str, &Expr>,
1135 value_of: &impl Fn(&str) -> bool,
1136 alias_values: &mut HashMap<String, bool>,
1137) -> bool {
1138 match expr {
1139 Expr::Var(name) => {
1140 if let Some(&value) = alias_values.get(name) {
1141 return value;
1142 }
1143 let Some(expr) = aliases.get(name.as_str()) else {
1144 return value_of(name);
1145 };
1146 let value = evaluate_expr_with(expr, aliases, value_of, alias_values);
1147 alias_values.insert(name.clone(), value);
1148 value
1149 }
1150 Expr::Not(expr) => !evaluate_expr_with(expr, aliases, value_of, alias_values),
1151 Expr::Binary(op, left, right) => {
1152 let left = evaluate_expr_with(left, aliases, value_of, alias_values);
1153 let right = evaluate_expr_with(right, aliases, value_of, alias_values);
1154 match op {
1155 crate::BinaryOp::Xor => left ^ right,
1156 crate::BinaryOp::And => left & right,
1157 crate::BinaryOp::Or => left | right,
1158 }
1159 }
1160 }
1161}
1162
1163fn defines_name(action: &Action) -> Option<&str> {
1164 match action {
1165 Action::Let { name, .. } | Action::Measure { name, .. } => Some(name),
1166 Action::DiscardIf(_)
1167 | Action::Resolve { .. }
1168 | Action::Branch { .. }
1169 | Action::Feedback { .. } => None,
1170 }
1171}
1172
1173fn expand_expr_vars_to_measurements(
1174 expr: &Expr,
1175 aliases: &HashMap<&str, &Expr>,
1176 out: &mut BTreeSet<String>,
1177 visited: &mut HashSet<String>,
1178) {
1179 match expr {
1180 Expr::Var(name) => {
1181 if let Some(resolved) = aliases.get(name.as_str()) {
1182 if !visited.insert(name.clone()) {
1183 return;
1184 }
1185 expand_expr_vars_to_measurements(resolved, aliases, out, visited);
1186 } else {
1187 out.insert(name.clone());
1188 }
1189 }
1190 Expr::Not(expr) => expand_expr_vars_to_measurements(expr, aliases, out, visited),
1191 Expr::Binary(_, lhs, rhs) => {
1192 expand_expr_vars_to_measurements(lhs, aliases, out, visited);
1193 expand_expr_vars_to_measurements(rhs, aliases, out, visited);
1194 }
1195 }
1196}
1197
1198fn check_expr_vars(
1199 expr: &Expr,
1200 defined: &HashSet<String>,
1201 bucket_measurements: &HashSet<String>,
1202) -> Result<(), InvalidActionError> {
1203 match expr {
1204 Expr::Var(name) => {
1205 if !defined.contains(name) && !bucket_measurements.contains(name) {
1206 return Err(InvalidActionError::UndefinedVariable(name.clone()));
1207 }
1208 }
1209 Expr::Not(expr) => check_expr_vars(expr, defined, bucket_measurements)?,
1210 Expr::Binary(_, lhs, rhs) => {
1211 check_expr_vars(lhs, defined, bucket_measurements)?;
1212 check_expr_vars(rhs, defined, bucket_measurements)?;
1213 }
1214 }
1215 Ok(())
1216}
1217
1218#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1219enum MeasurementSiteKey {
1220 Node(IVec3),
1221 Edge { a: IVec3, b: IVec3 },
1222}
1223
1224fn measurement_site_key(target: &MeasureTarget) -> Result<MeasurementSiteKey, BlockGraphError> {
1225 Ok(match target {
1226 MeasureTarget::Node(pos) => MeasurementSiteKey::Node(*pos),
1227 MeasureTarget::Edge { src, dir } => {
1228 let dst = crate::checked_add_position(*src, dir.to_ivec3())?;
1229 if src.to_array() <= dst.to_array() {
1230 MeasurementSiteKey::Edge { a: *src, b: dst }
1231 } else {
1232 MeasurementSiteKey::Edge { a: dst, b: *src }
1233 }
1234 }
1235 })
1236}
1237
1238fn ensure_measure_target_is_valid(
1239 graph: &BlockGraph,
1240 target: &MeasureTarget,
1241) -> Result<(), InvalidActionError> {
1242 match target {
1243 MeasureTarget::Node(pos) => match graph.get_block(*pos).map(|block| block.kind) {
1244 Some(
1245 BlockKind::Cube(_)
1246 | BlockKind::Y
1247 | BlockKind::Measurement(_)
1248 | BlockKind::Selective(_),
1249 ) => Ok(()),
1250 _ => Err(InvalidActionError::InvalidMeasurementNode(*pos)),
1251 },
1252 MeasureTarget::Edge { src, dir } if dir.as_udirection() == crate::UDirection::Z => {
1253 Err(InvalidActionError::TimeLikeMeasurementEdge(*src, *dir))
1254 }
1255 MeasureTarget::Edge { .. } => Ok(()),
1256 }
1257}
1258
1259fn ensure_feedback_target_exists(graph: &BlockGraph, pos: IVec3) -> Result<(), InvalidActionError> {
1260 if graph.get_block(pos).is_none() {
1261 return Err(InvalidActionError::InvalidFeedbackTarget(pos));
1262 }
1263 Ok(())
1264}
1265
1266#[cfg(test)]
1267mod tests {
1268 use super::{ResolveDomainError, SetMeasurementObservableError};
1269 use bloq_utils::boolean::{BooleanLimits, BooleanResourceError};
1270 use glam::ivec3;
1271
1272 use crate::validate::InvalidActionError;
1273 use crate::{
1274 Action, Block, BlockGraph, BlockKind, CubeKind, Direction, Expr, MeasureTarget, Pipe,
1275 };
1276 use crate::{
1277 ActionDag, ActionDependency, BlockGraphError, MeasurementObservable, PauliBasis,
1278 SelectiveKind,
1279 };
1280
1281 #[test]
1282 fn shared_aliases_stay_bounded_for_domain_and_sampling() {
1283 let target = ivec3(0, 0, 1);
1284 let mut actions = vec![Action::Measure {
1285 target: MeasureTarget::Node(ivec3(0, 0, 0)),
1286 name: "m".into(),
1287 }];
1288 let mut name = "m".to_owned();
1289 for index in 0..64 {
1290 let next = format!("a{index}");
1291 actions.push(Action::Let {
1292 name: next.clone(),
1293 expr: Expr::Binary(
1294 crate::BinaryOp::Xor,
1295 Box::new(Expr::Var(name.clone())),
1296 Box::new(Expr::Var(name)),
1297 ),
1298 });
1299 name = next;
1300 }
1301 actions.push(Action::Resolve {
1302 target,
1303 condition: Expr::Var(name),
1304 });
1305 let dag = ActionDag::from_actions(&actions);
1306 assert_eq!(
1307 dag.resolve_value_domain(&[target]).unwrap().values(),
1308 &[vec![false]]
1309 );
1310 assert!(!dag.sample_resolve_values(&[target], &mut rand::rng())[&target]);
1311 }
1312
1313 #[test]
1314 fn resolve_domain_preserves_shared_control_correlation() {
1315 let first = ivec3(0, 0, 1);
1316 let second = ivec3(1, 0, 1);
1317 let dag = ActionDag::from_actions(&[
1318 Action::Measure {
1319 target: MeasureTarget::Node(ivec3(0, 0, 0)),
1320 name: "m".into(),
1321 },
1322 Action::Let {
1323 name: "not_m".into(),
1324 expr: Expr::Not(Box::new(Expr::Var("m".into()))),
1325 },
1326 Action::Resolve {
1327 target: first,
1328 condition: Expr::Var("m".into()),
1329 },
1330 Action::Resolve {
1331 target: second,
1332 condition: Expr::Var("not_m".into()),
1333 },
1334 ]);
1335
1336 let domain = dag.resolve_value_domain(&[first, second]).unwrap();
1337 assert_eq!(domain.values(), &[vec![false, true], vec![true, false]]);
1338 assert!(
1339 dag.resolve_xor_is_with_limits(&[first, second], true, BooleanLimits::DEFAULT)
1340 .unwrap()
1341 );
1342 }
1343
1344 #[test]
1345 fn branch_domain_preserves_shared_control_correlation() {
1346 let first = ivec3(0, 0, 1);
1347 let second = ivec3(1, 0, 1);
1348 let dag = ActionDag::from_actions(&[
1349 Action::Measure {
1350 target: MeasureTarget::Node(ivec3(0, 0, 0)),
1351 name: "m".into(),
1352 },
1353 Action::Let {
1354 name: "not_m".into(),
1355 expr: Expr::Not(Box::new(Expr::Var("m".into()))),
1356 },
1357 Action::Branch {
1358 target: first,
1359 condition: Expr::Var("m".into()),
1360 },
1361 Action::Branch {
1362 target: second,
1363 condition: Expr::Var("not_m".into()),
1364 },
1365 ]);
1366
1367 let domain = dag
1370 .branch_values_up_to(&[first, second], usize::MAX)
1371 .unwrap();
1372 assert_eq!(domain, [vec![false, true], vec![true, false]]);
1373 }
1374
1375 #[test]
1376 fn symbolic_domain_matches_a_small_truth_table() {
1377 let targets = [ivec3(0, 0, 1), ivec3(1, 0, 1), ivec3(2, 0, 1)];
1378 let actions = [
1379 Action::Let {
1380 name: "parity".into(),
1381 expr: Expr::Binary(
1382 crate::BinaryOp::Xor,
1383 Box::new(Expr::Var("a".into())),
1384 Box::new(Expr::Var("b".into())),
1385 ),
1386 },
1387 Action::Resolve {
1388 target: targets[0],
1389 condition: Expr::Var("parity".into()),
1390 },
1391 Action::Resolve {
1392 target: targets[1],
1393 condition: Expr::Binary(
1394 crate::BinaryOp::And,
1395 Box::new(Expr::Var("parity".into())),
1396 Box::new(Expr::Var("c".into())),
1397 ),
1398 },
1399 Action::Resolve {
1400 target: targets[2],
1401 condition: Expr::Binary(
1402 crate::BinaryOp::Or,
1403 Box::new(Expr::Not(Box::new(Expr::Var("a".into())))),
1404 Box::new(Expr::Var("c".into())),
1405 ),
1406 },
1407 ];
1408 let dag = ActionDag::from_actions(&actions);
1409 let aliases = dag.let_bindings();
1410 let exprs = targets.map(|target| dag.resolve_expr(target).unwrap());
1411 let mut expected = std::collections::BTreeSet::new();
1412 for assignment in 0..8 {
1413 expected.insert(
1414 exprs
1415 .iter()
1416 .map(|expr| {
1417 super::evaluate_expr_with(
1418 expr,
1419 &aliases,
1420 &|name| {
1421 let index = match name {
1422 "a" => 0,
1423 "b" => 1,
1424 "c" => 2,
1425 _ => unreachable!("all input variables are listed"),
1426 };
1427 assignment >> index & 1 == 1
1428 },
1429 &mut std::collections::HashMap::new(),
1430 )
1431 })
1432 .collect::<Vec<_>>(),
1433 );
1434 }
1435
1436 let expected = expected.into_iter().collect::<Vec<_>>();
1437 let domain = dag.resolve_value_domain(&targets).unwrap();
1438 assert_eq!(domain.values(), expected);
1439 let bounded = dag
1440 .resolve_value_domain_bounded(&targets, expected.len())
1441 .expect("the exact domain fits its measured size");
1442 assert_eq!(bounded.values(), expected);
1443 assert_eq!(
1444 dag.resolve_value_domain_bounded(&targets, expected.len() - 1),
1445 Err(ResolveDomainError::TooManyValues {
1446 observed: expected.len()
1447 })
1448 );
1449 for assignment in 0..8 {
1450 let values = (0..3)
1451 .map(|index| assignment >> index & 1 == 1)
1452 .collect::<Vec<_>>();
1453 assert_eq!(
1454 dag.resolve_values_are_reachable(&targets, &values).unwrap(),
1455 domain.contains(&values)
1456 );
1457 }
1458 }
1459
1460 #[test]
1461 fn domain_limits_distinguish_alias_work_from_value_count() {
1462 let target = ivec3(0, 0, 1);
1463 let mut actions = (0..256)
1464 .map(|index| Action::Let {
1465 name: format!("a{index}"),
1466 expr: Expr::Var(if index == 0 {
1467 "input".into()
1468 } else {
1469 format!("a{}", index - 1)
1470 }),
1471 })
1472 .collect::<Vec<_>>();
1473 actions.push(Action::Resolve {
1474 target,
1475 condition: Expr::Var("a255".into()),
1476 });
1477 let dag = ActionDag::from_actions(&actions);
1478 let query = |values, limits| {
1479 dag.resolve_value_domain_bounded_with_limits(&[target], values, limits)
1480 };
1481 assert!(matches!(
1482 query(
1483 2,
1484 BooleanLimits {
1485 max_steps: 8,
1486 ..BooleanLimits::DEFAULT
1487 }
1488 ),
1489 Err(ResolveDomainError::Resource(BooleanResourceError {
1490 resource: "Boolean work steps",
1491 observed: 9,
1492 limit: 8,
1493 }))
1494 ));
1495 assert!(matches!(
1496 query(
1497 2,
1498 BooleanLimits {
1499 max_nodes: 0,
1500 ..BooleanLimits::DEFAULT
1501 }
1502 ),
1503 Err(ResolveDomainError::Resource(BooleanResourceError {
1504 resource: "Boolean nodes",
1505 observed: 1,
1506 limit: 0,
1507 }))
1508 ));
1509 assert_eq!(
1510 query(1, BooleanLimits::DEFAULT),
1511 Err(ResolveDomainError::TooManyValues { observed: 2 })
1512 );
1513 let domain = query(2, BooleanLimits::DEFAULT).unwrap();
1514 assert_eq!(domain.values(), &[vec![false], vec![true]]);
1515 assert_eq!(domain, query(2, BooleanLimits::UNLIMITED).unwrap());
1516 assert_eq!(
1517 dag.complete_resolve_values_with_limits(
1518 &[target],
1519 &[(target, true)],
1520 BooleanLimits::DEFAULT
1521 )
1522 .unwrap(),
1523 Some(vec![true])
1524 );
1525 }
1526
1527 #[test]
1528 fn symbolic_reachability_handles_wide_independent_controls() {
1529 let count = usize::BITS as usize + 1;
1530 let targets = (0..count)
1531 .map(|index| ivec3(index as i32, 0, 0))
1532 .collect::<Vec<_>>();
1533 let actions = targets
1534 .iter()
1535 .enumerate()
1536 .map(|(index, &target)| Action::Resolve {
1537 target,
1538 condition: Expr::Var(format!("m{index}")),
1539 })
1540 .collect::<Vec<_>>();
1541 let dag = ActionDag::from_actions(&actions);
1542
1543 assert!(
1544 dag.resolve_values_are_reachable(&targets, &vec![false; count])
1545 .unwrap()
1546 );
1547 }
1548
1549 #[test]
1550 fn measure_target_with_shift_updates_node_and_edge_geometry() {
1551 use crate::{Action, Direction, MeasureTarget};
1552 use glam::ivec3;
1553
1554 let shifted_node = Action::Measure {
1555 target: MeasureTarget::Node(ivec3(0, 0, 0)),
1556 name: "m_node".into(),
1557 }
1558 .with_shift(ivec3(2, 3, 4));
1559 assert_eq!(
1560 shifted_node,
1561 Action::Measure {
1562 target: MeasureTarget::Node(ivec3(2, 3, 4)),
1563 name: "m_node".into(),
1564 }
1565 );
1566
1567 let shifted_edge = Action::Measure {
1568 target: MeasureTarget::Edge {
1569 src: ivec3(1, 0, 0),
1570 dir: Direction::XPLUS,
1571 },
1572 name: "m_edge".into(),
1573 }
1574 .with_shift(ivec3(0, 5, -1));
1575 assert_eq!(
1576 shifted_edge,
1577 Action::Measure {
1578 target: MeasureTarget::Edge {
1579 src: ivec3(1, 5, -1),
1580 dir: Direction::XPLUS,
1581 },
1582 name: "m_edge".into(),
1583 }
1584 );
1585 }
1586
1587 #[test]
1588 fn measurement_metadata_requires_an_existing_measurement() {
1589 use crate::{Action, ActionDag, MeasureTarget};
1590 use glam::ivec3;
1591
1592 let mut dag = ActionDag::from_actions(&[Action::Measure {
1593 target: MeasureTarget::Node(ivec3(0, 0, 0)),
1594 name: "m0".into(),
1595 }]);
1596 let observable = MeasurementObservable::Selective(SelectiveKind::XY);
1597 dag.set_measurement_observable(0, observable).unwrap();
1598 assert_eq!(
1599 dag.node_by_ordinal(0).unwrap().measurement,
1600 Some(observable)
1601 );
1602 assert!(matches!(
1603 dag.set_measurement_observable(1, observable),
1604 Err(SetMeasurementObservableError::OrdinalOutOfRange { ordinal: 1, .. })
1605 ));
1606
1607 let mut dag = ActionDag::from_actions(&[Action::Let {
1608 name: "a".into(),
1609 expr: Expr::Var("m0".into()),
1610 }]);
1611 assert!(matches!(
1612 dag.set_measurement_observable(0, MeasurementObservable::Concrete(PauliBasis::Z)),
1613 Err(SetMeasurementObservableError::NotMeasurementNode { ordinal: 0 })
1614 ));
1615 }
1616
1617 #[test]
1618 fn action_dag_adds_dependency_edges_for_variable_uses() {
1619 let mut graph = BlockGraph::new();
1620 graph.add_block(Block::new(
1621 glam::ivec3(0, 0, 0),
1622 BlockKind::Cube(CubeKind::ZXZ),
1623 ));
1624 graph
1625 .add_action(Action::Measure {
1626 target: MeasureTarget::Node(glam::ivec3(0, 0, 0)),
1627 name: "m0".into(),
1628 })
1629 .unwrap();
1630 graph
1631 .add_action(Action::Let {
1632 name: "alias".into(),
1633 expr: Expr::Var("m0".into()),
1634 })
1635 .unwrap();
1636 graph
1637 .add_action(Action::DiscardIf(Expr::Var("alias".into())))
1638 .unwrap();
1639
1640 let dag = graph.action_graph();
1641 let ordinals = dag
1642 .ordered_nodes()
1643 .map(|node| node.ordinal)
1644 .collect::<Vec<_>>();
1645 assert_eq!(ordinals, vec![0, 1, 2]);
1646
1647 let alias_node = dag.node_by_ordinal(1).unwrap();
1648 let discard_node = dag.node_by_ordinal(2).unwrap();
1649 assert!(dag.depends_on(alias_node, 0));
1650 assert!(dag.depends_on(discard_node, 1));
1651 }
1652
1653 #[test]
1654 fn action_dag_requires_resolve_before_selective_measurement() {
1655 let graph = BlockGraph::from_blog_text(
1656 "BLOG 1.0\n\n 0: Port [0,0,0]\n 1: XZX [0,0,1]\n 2: XY [0,0,2]\n 3: ZXZ [1,0,1]\n [0,0,0] -> +Z\n [0,0,1] -> +Z\n [0,0,1] -> +X\n\n resolve 2 if m0\n m_sel = measure 2\n m0 = measure 1 -> +X\n",
1657 )
1658 .unwrap();
1659
1660 let dag = graph.action_graph();
1661 let resolve = dag
1662 .ordered_nodes()
1663 .find(|node| matches!(node.action, Action::Resolve { target, .. } if target == glam::ivec3(0, 0, 2)))
1664 .unwrap();
1665 let measure = dag
1666 .ordered_nodes()
1667 .find(|node| matches!(&node.action, Action::Measure { name, .. } if name == "m_sel"))
1668 .unwrap();
1669 assert!(dag.depends_on(measure, resolve.ordinal));
1670 }
1671
1672 #[test]
1673 fn action_dag_rejects_variable_redefinition() {
1674 let actions = vec![
1675 Action::Measure {
1676 target: MeasureTarget::Node(glam::ivec3(0, 0, 0)),
1677 name: "m0".into(),
1678 },
1679 Action::Let {
1680 name: "a".into(),
1681 expr: Expr::Var("m0".into()),
1682 },
1683 Action::Let {
1684 name: "a".into(),
1685 expr: Expr::Var("m0".into()),
1686 },
1687 ];
1688
1689 let mut dag = ActionDag::from_actions(&actions);
1690 let err = dag.validate(None).unwrap_err();
1691 assert!(
1692 matches!(
1693 err,
1694 BlockGraphError::InvalidAction(InvalidActionError::VariableRedefinition(ref n))
1695 if n == "a"
1696 ),
1697 "expected VariableRedefinition, got: {err:?}",
1698 );
1699 }
1700
1701 #[test]
1702 fn action_dag_rejects_duplicate_resolve_target() {
1703 let actions = vec![
1704 Action::Measure {
1705 target: MeasureTarget::Node(glam::ivec3(0, 0, -1)),
1706 name: "m0".into(),
1707 },
1708 Action::Resolve {
1709 target: glam::ivec3(0, 0, 0),
1710 condition: Expr::Var("m0".into()),
1711 },
1712 Action::Resolve {
1713 target: glam::ivec3(0, 0, 0),
1714 condition: Expr::Var("m0".into()),
1715 },
1716 ];
1717
1718 let mut dag = ActionDag::from_actions(&actions);
1719 let err = dag.validate(None).unwrap_err();
1720 assert!(
1721 matches!(
1722 err,
1723 BlockGraphError::InvalidAction(InvalidActionError::DuplicateResolveTarget(target))
1724 if target == glam::ivec3(0, 0, 0)
1725 ),
1726 "expected DuplicateResolveTarget, got: {err:?}",
1727 );
1728 }
1729
1730 #[test]
1731 fn action_dag_rejects_duplicate_measurement_target() {
1732 let actions = vec![
1733 Action::Measure {
1734 target: MeasureTarget::Node(glam::ivec3(0, 0, 0)),
1735 name: "m0".into(),
1736 },
1737 Action::Measure {
1738 target: MeasureTarget::Node(glam::ivec3(0, 0, 0)),
1739 name: "m1".into(),
1740 },
1741 ];
1742
1743 let mut dag = ActionDag::from_actions(&actions);
1744 let err = dag.validate(None).unwrap_err();
1745 assert!(
1746 matches!(
1747 err,
1748 BlockGraphError::InvalidAction(InvalidActionError::DuplicateMeasurementTarget(..))
1749 ),
1750 "expected DuplicateMeasurementTarget, got: {err:?}",
1751 );
1752 }
1753
1754 #[test]
1755 fn action_dag_rejects_missing_resolve_for_selective_block() {
1756 let mut graph = BlockGraph::new();
1757 graph.add_block(Block::new(
1758 glam::ivec3(0, 0, 0),
1759 BlockKind::Selective(SelectiveKind::XY),
1760 ));
1761 graph.add_block(Block::new(
1762 glam::ivec3(0, 0, -1),
1763 BlockKind::Cube(CubeKind::ZXZ),
1764 ));
1765 graph.add_pipe(Pipe::new(glam::ivec3(0, 0, -1), Direction::ZPLUS));
1766
1767 let err = ActionDag::from_actions(&[])
1768 .validate(Some(&graph))
1769 .unwrap_err();
1770 assert!(
1771 matches!(
1772 err,
1773 BlockGraphError::InvalidAction(
1774 InvalidActionError::MissingResolveForSelective { target }
1775 ) if target == glam::ivec3(0, 0, 0)
1776 ),
1777 "expected MissingResolveForSelective, got: {err:?}",
1778 );
1779 }
1780
1781 #[test]
1782 fn action_dag_rejects_dependency_cycle() {
1783 let actions = vec![
1784 Action::Measure {
1785 target: MeasureTarget::Node(glam::ivec3(0, 0, 0)),
1786 name: "m".into(),
1787 },
1788 Action::Resolve {
1789 target: glam::ivec3(0, 0, 0),
1790 condition: Expr::Var("m".into()),
1791 },
1792 ];
1793
1794 let mut dag = ActionDag::from_actions(&actions);
1795 let err = dag.validate(None).unwrap_err();
1796 match err {
1797 BlockGraphError::InvalidAction(InvalidActionError::DependencyCycle { ordinal }) => {
1798 assert!(ordinal == 0 || ordinal == 1, "got ordinal: {ordinal}");
1799 }
1800 other => panic!("expected DependencyCycle, got: {other:?}"),
1801 }
1802 }
1803
1804 #[test]
1805 fn stabilizer_analysis_adds_all_implicit_dependencies_and_surface() {
1806 use crate::{
1807 FeedbackTarget, SelectiveFixingTarget, Stabilizer, StabilizerGenerator,
1808 StabilizerRowKind,
1809 };
1810
1811 let selective_a = ivec3(1, 0, 0);
1812 let selective_b = ivec3(2, 0, 0);
1813 let feedback_target = ivec3(3, 0, 0);
1814 let actions = vec![
1815 Action::Measure {
1816 target: MeasureTarget::Node(ivec3(0, 0, 0)),
1817 name: "control".into(),
1818 },
1819 Action::Resolve {
1820 target: selective_a,
1821 condition: Expr::Var("control".into()),
1822 },
1823 Action::Resolve {
1824 target: selective_b,
1825 condition: Expr::Var("control".into()),
1826 },
1827 Action::Feedback {
1828 targets: vec![FeedbackTarget {
1829 pauli: PauliBasis::X,
1830 target: feedback_target,
1831 direction: None,
1832 }],
1833 condition: None,
1834 },
1835 Action::Measure {
1836 target: MeasureTarget::Node(ivec3(4, 0, 0)),
1837 name: "m".into(),
1838 },
1839 ];
1840 let measurement = Stabilizer::from_interior_nodes([
1841 (selective_a, crate::Pauli::X),
1842 (feedback_target, crate::Pauli::Z),
1843 ]);
1844 let fixing_a = Stabilizer::from_interior_nodes([
1845 (selective_a, crate::Pauli::Y),
1846 (selective_b, crate::Pauli::Z),
1847 ]);
1848 let fixing_b = Stabilizer::from_interior_nodes([(selective_b, crate::Pauli::Y)]);
1849 let generators = vec![
1850 StabilizerGenerator::new(
1851 Stabilizer::from_interior_nodes([(ivec3(0, 0, 0), crate::Pauli::X)]),
1852 StabilizerRowKind::Measurement {
1853 name: "control".into(),
1854 },
1855 ),
1856 StabilizerGenerator::new(
1857 measurement.clone(),
1858 StabilizerRowKind::Measurement { name: "m".into() },
1859 ),
1860 StabilizerGenerator::new(
1861 fixing_a,
1862 StabilizerRowKind::SelectiveFixing {
1863 targets: vec![SelectiveFixingTarget {
1864 pos: selective_a,
1865 forbidden: crate::Pauli::Y,
1866 }],
1867 },
1868 ),
1869 StabilizerGenerator::new(
1870 fixing_b,
1871 StabilizerRowKind::SelectiveFixing {
1872 targets: vec![SelectiveFixingTarget {
1873 pos: selective_b,
1874 forbidden: crate::Pauli::Y,
1875 }],
1876 },
1877 ),
1878 ];
1879
1880 let mut dag = ActionDag::from_actions(&actions);
1881 dag.attach_readout_dependencies(&generators, None).unwrap();
1882
1883 let dependencies = dag.dependencies().collect::<Vec<_>>();
1884 assert!(dependencies.contains(&(1, 4, ActionDependency::SelectiveSupport)));
1885 assert!(dependencies.contains(&(2, 4, ActionDependency::SelectiveSupport)));
1886 assert!(dependencies.contains(&(3, 4, ActionDependency::FeedbackAnticommutation)));
1887 assert_eq!(
1888 dag.node_by_ordinal(4).unwrap().measurement_stabilizer,
1889 Some(measurement)
1890 );
1891 assert!(dag.is_analyzed());
1892 }
1893
1894 #[test]
1895 fn implicit_feedback_dependency_rejects_a_cycle() {
1896 use crate::{FeedbackTarget, Stabilizer, StabilizerGenerator, StabilizerRowKind};
1897
1898 let target = ivec3(0, 0, 0);
1899 let actions = vec![
1900 Action::Measure {
1901 target: MeasureTarget::Node(target),
1902 name: "m".into(),
1903 },
1904 Action::Feedback {
1905 targets: vec![FeedbackTarget {
1906 pauli: PauliBasis::X,
1907 target,
1908 direction: None,
1909 }],
1910 condition: Some(Expr::Var("m".into())),
1911 },
1912 ];
1913 let generators = vec![StabilizerGenerator::new(
1914 Stabilizer::from_interior_nodes([(target, crate::Pauli::Z)]),
1915 StabilizerRowKind::Measurement { name: "m".into() },
1916 )];
1917
1918 let err = ActionDag::from_actions(&actions)
1919 .attach_readout_dependencies(&generators, None)
1920 .unwrap_err();
1921 assert!(matches!(
1922 err,
1923 BlockGraphError::InvalidAction(InvalidActionError::DependencyCycle { .. })
1924 ));
1925 }
1926
1927 #[test]
1928 fn stabilizer_analysis_adds_terminal_branch_support_dependency() {
1929 use crate::{BranchRegion, Stabilizer, StabilizerGenerator, StabilizerRowKind};
1930
1931 let controller = ivec3(0, 0, 0);
1932 let branch_target = ivec3(0, 0, 1);
1933 let actions = [
1934 Action::Measure {
1935 target: MeasureTarget::Node(controller),
1936 name: "control".into(),
1937 },
1938 Action::Branch {
1939 target: branch_target,
1940 condition: Expr::Var("control".into()),
1941 },
1942 Action::Measure {
1943 target: MeasureTarget::Node(ivec3(1, 0, 0)),
1944 name: "m".into(),
1945 },
1946 ];
1947 let generators = [
1948 StabilizerGenerator::new(
1949 Stabilizer::from_interior_nodes([(controller, crate::Pauli::X)]),
1950 StabilizerRowKind::Measurement {
1951 name: "control".into(),
1952 },
1953 ),
1954 StabilizerGenerator::new(
1955 Stabilizer::from_interior_nodes([])
1956 .with_interior_edges([((controller, branch_target), crate::Pauli::Z)]),
1957 StabilizerRowKind::Measurement { name: "m".into() },
1958 ),
1959 ];
1960
1961 let mut dag = ActionDag::from_actions(&actions);
1962 dag.branch_regions.push(BranchRegion::test_region(
1963 branch_target,
1964 vec![branch_target],
1965 Vec::new(),
1966 ));
1967 dag.attach_readout_dependencies(&generators, None).unwrap();
1968
1969 assert!(
1970 dag.dependencies()
1971 .any(|dependency| dependency == (1, 2, ActionDependency::BranchSupport))
1972 );
1973 }
1974
1975 #[test]
1976 fn terminal_branch_support_rejects_self_control_cycle() {
1977 use crate::{BranchRegion, Stabilizer, StabilizerGenerator, StabilizerRowKind};
1978
1979 let controller = ivec3(0, 0, 0);
1980 let branch_target = ivec3(0, 0, 1);
1981 let actions = [
1982 Action::Measure {
1983 target: MeasureTarget::Node(controller),
1984 name: "control".into(),
1985 },
1986 Action::Branch {
1987 target: branch_target,
1988 condition: Expr::Var("control".into()),
1989 },
1990 ];
1991 let generators = [StabilizerGenerator::new(
1992 Stabilizer::from_interior_nodes([(branch_target, crate::Pauli::X)]),
1993 StabilizerRowKind::Measurement {
1994 name: "control".into(),
1995 },
1996 )];
1997
1998 let mut dag = ActionDag::from_actions(&actions);
1999 dag.branch_regions.push(BranchRegion::test_region(
2000 branch_target,
2001 vec![branch_target],
2002 Vec::new(),
2003 ));
2004 let err = dag
2005 .attach_readout_dependencies(&generators, None)
2006 .unwrap_err();
2007
2008 assert!(matches!(
2009 err,
2010 BlockGraphError::InvalidAction(InvalidActionError::DependencyCycle { .. })
2011 ));
2012 }
2013}