1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
4use std::ops::RangeInclusive;
5use std::str::FromStr;
6use std::sync::Arc;
7
8use bloq_utils::{Basis, PauliBasis, UDirection};
9use glam::IVec3;
10use petgraph::stable_graph::{EdgeIndex, NodeIndex, StableUnGraph};
11use petgraph::visit::EdgeRef;
12use rand::{Rng, RngExt, SeedableRng, rngs::StdRng};
13use rustc_hash::FxBuildHasher;
14use smallvec::SmallVec;
15
16use crate::ZXGraph;
17#[cfg(feature = "gltf")]
18use crate::gltf::{
19 GltfData, GltfFaceSelector, POPPED_X_FACE_COLOR, POPPED_Y_FACE_COLOR, POPPED_Z_FACE_COLOR,
20 block_graph_as_gltf_data_with_modules, block_graph_as_gltf_data_with_popped_faces,
21 module_html_geometry, stabilizer_as_gltf_data,
22};
23use crate::validate::InvalidActionError;
24use crate::zx::{
25 Stabilizer, StabilizerGenerator, StabilizerGenerators, StabilizerRowKind,
26 fill_ports_auto as fill_ports_auto_impl,
27};
28use crate::{
29 Action, ActionDag, Block, BlockGraphError, BlockKind, BranchProjection, CubeHeight, CubeKind,
30 Direction, MeasureTarget, MeasurementObservable, PatchRotationKind, Pipe, WalkingBoundaryKind,
31 WalkingKind,
32};
33
34#[cfg(feature = "gltf")]
35const OUTER_FACE_OPACITY_SCALE: f32 = 0.55;
36#[cfg(feature = "gltf")]
37fn emphasize_popped_faces(mut data: GltfData) -> GltfData {
38 use bloq_utils::RGBA;
41
42 let mut triangles: BTreeMap<RGBA, Vec<[glam::Vec3; 3]>> = BTreeMap::new();
43 for (color, faces) in std::mem::take(&mut data.triangles) {
44 let color = match color {
45 RGBA::X_RED => POPPED_X_FACE_COLOR,
46 RGBA::Y_GREEN => POPPED_Y_FACE_COLOR,
47 RGBA::Z_BLUE => POPPED_Z_FACE_COLOR,
48 _ => RGBA { a: 255, ..color },
49 };
50 triangles.entry(color).or_default().extend(faces);
51 }
52 data.triangles = triangles;
53 data
54}
55
56#[derive(Debug, Clone, Copy)]
61pub struct BlockLayerView<'a> {
62 graph: &'a BlockGraph,
63 z: i32,
64}
65
66pub type AnalyzedBranchProjections = Vec<(BranchProjection, StabilizerGenerators)>;
69
70#[derive(Default)]
73pub(crate) struct LinkBranchOccupancy {
74 by_position: HashMap<IVec3, Vec<(IVec3, BlockKind)>>,
75}
76
77impl LinkBranchOccupancy {
78 pub(crate) fn add_region(
79 &mut self,
80 region: &crate::BranchRegion,
81 ) -> Result<(), BlockGraphError> {
82 for block in region.on_false().blocks().chain(region.on_true().blocks()) {
83 for position in block.checked_reserved_positions()? {
84 self.by_position
85 .entry(position)
86 .or_default()
87 .push((block.pos(), block.kind()));
88 }
89 }
90 Ok(())
91 }
92}
93
94#[derive(Debug, Clone)]
103pub struct BlockGraph {
104 inner: StableUnGraph<Block, Pipe>,
105 block_id: HashMap<IVec3, u32, FxBuildHasher>,
106 reserved_by_position: HashMap<IVec3, SmallVec<[IVec3; 1]>, FxBuildHasher>,
107 pub(crate) branches: Vec<crate::BranchRegion>,
108 action_graph: ActionDag,
109 action_inputs: BTreeSet<String>,
110 action_graph_error: Option<BlockGraphError>,
113 pub name: String,
115 pub interface: crate::ModuleInterface,
117 pub instances: Vec<crate::ModuleInstance>,
119 pub quantum_connections: Vec<crate::QuantumConnection>,
121 pub bit_bindings: Vec<crate::BitBinding>,
123 pub(crate) definitions: Vec<BlockGraph>,
124 pub(crate) interface_declared: bool,
125}
126
127impl Default for BlockGraph {
128 fn default() -> Self {
129 Self {
130 inner: StableUnGraph::default(),
131 block_id: HashMap::default(),
132 reserved_by_position: HashMap::default(),
133 branches: Vec::new(),
134 action_graph: ActionDag::default(),
135 action_inputs: BTreeSet::new(),
136 action_graph_error: None,
137 name: Self::ENTRY_MODULE.to_string(),
138 interface: crate::ModuleInterface::default(),
139 instances: Vec::new(),
140 quantum_connections: Vec::new(),
141 bit_bindings: Vec::new(),
142 definitions: Vec::new(),
143 interface_declared: false,
144 }
145 }
146}
147
148impl BlockGraph {
149 pub fn replace_local_body(&mut self, body: Self) {
151 self.inner = body.inner;
152 self.block_id = body.block_id;
153 self.reserved_by_position = body.reserved_by_position;
154 self.branches = body.branches;
155 self.action_graph = body.action_graph;
156 self.action_inputs = body.action_inputs;
157 self.action_graph_error = body.action_graph_error;
158 }
159
160 pub(crate) fn copy_local_geometry(&self) -> Self {
161 Self {
162 inner: self.inner.clone(),
163 block_id: self.block_id.clone(),
164 reserved_by_position: self.reserved_by_position.clone(),
165 branches: self.branches.clone(),
166 action_graph: self.action_graph.clone(),
167 action_inputs: self.action_inputs.clone(),
168 action_graph_error: self.action_graph_error.clone(),
169 ..Self::default()
170 }
171 }
172}
173
174impl<'a> BlockLayerView<'a> {
175 pub fn z(&self) -> i32 {
177 self.z
178 }
179
180 pub fn graph(&self) -> &BlockGraph {
182 self.graph
183 }
184
185 pub fn blocks(&self) -> impl Iterator<Item = &Block> + '_ {
187 self.graph
188 .blocks()
189 .filter(move |block| block.occupies_layer(self.z))
190 }
191
192 pub fn spacelike_pipes(&self) -> impl Iterator<Item = Pipe> + '_ {
195 self.graph.pipes().filter_map(move |pipe| {
196 let p1 = pipe.src;
197 let p2 = pipe.dst();
198 if p1.z == self.z && p2.z == self.z {
199 return Some(pipe.clone());
200 }
201 if !pipe.dir.is_spatial() {
202 return None;
203 }
204 let src_block = self.graph.get_endpoint_block(p1)?;
205 let dst_block = self.graph.get_endpoint_block(p2)?;
206 if src_block.occupies_layer(self.z) && dst_block.occupies_layer(self.z) {
207 let mut projected = pipe.clone();
208 projected.src.z = self.z;
209 Some(projected)
210 } else {
211 None
212 }
213 })
214 }
215
216 pub fn boundary_pipes(&self) -> impl Iterator<Item = &Pipe> + '_ {
218 self.graph.pipes().filter(move |pipe| {
219 let p1 = pipe.src;
220 let p2 = pipe.dst();
221 let (p1, p2) = if p1.z <= p2.z { (p1, p2) } else { (p2, p1) };
222 (p1.z == self.z && p2.z > self.z) || (p1.z < self.z && p2.z == self.z)
223 })
224 }
225
226 pub fn into_graph(self) -> BlockGraph {
234 let mut layer = BlockGraph::new();
235 for block in self.blocks() {
236 let mut layer_block = block.clone();
237 if layer_block.kind.is_cube() && layer_block.height_cells() != 1 {
238 layer_block = Block::new(IVec3::new(block.pos.x, block.pos.y, self.z), block.kind);
239 if let Some(tag) = block.tag() {
240 layer_block = layer_block
241 .with_tag(tag)
242 .expect("existing block tag is valid");
243 }
244 }
245 layer.add_block(layer_block);
246 }
247 for p in self.spacelike_pipes() {
248 layer.add_pipe(p);
249 }
250 for p in self.boundary_pipes() {
251 let p1 = p.src;
252 let p2 = p.dst();
253 let (p1, p2) = if p1.z <= p2.z { (p1, p2) } else { (p2, p1) };
254 if p1.z == self.z && p2.z > self.z {
255 layer.add_block(Block::new(p2, BlockKind::Port));
256 layer.add_pipe(p.clone());
257 } else if p1.z < self.z && p2.z == self.z {
258 layer.add_block(Block::new(p1, BlockKind::Port));
259 layer.add_pipe(p.clone());
260 }
261 }
262 layer.clear_actions();
263 layer
264 }
265}
266
267impl BlockGraph {
268 pub fn new() -> Self {
270 Self::default()
271 }
272
273 pub(crate) fn require_flat_hierarchy(
274 &self,
275 operation: &'static str,
276 ) -> Result<(), BlockGraphError> {
277 if self.instances.is_empty() {
278 Ok(())
279 } else {
280 Err(BlockGraphError::HierarchyRequiresFlatten { operation })
281 }
282 }
283
284 pub(crate) fn require_flat_source(
285 &self,
286 operation: &'static str,
287 ) -> Result<(), BlockGraphError> {
288 if self.has_module_structure() {
289 Err(BlockGraphError::HierarchyRequiresFlatten { operation })
290 } else {
291 Ok(())
292 }
293 }
294
295 pub fn infer_pipe_basis(&self, pipe: &Pipe) -> [Option<Basis>; 3] {
297 self.infer_pipe_basis_from_endpoint(pipe, pipe.src)
298 }
299
300 pub fn infer_pipe_basis_from_endpoint(
302 &self,
303 pipe: &Pipe,
304 endpoint: IVec3,
305 ) -> [Option<Basis>; 3] {
306 let Ok(dst) = pipe.try_dst() else {
307 return [None; 3];
308 };
309 if endpoint != pipe.src && endpoint != dst {
310 return [None; 3];
311 }
312
313 let mut canonical = if pipe.src.to_array() < dst.to_array() {
314 pipe.clone()
315 } else {
316 Pipe::new(dst, pipe.dir.negate())
317 };
318 if pipe.hadamard {
319 canonical = canonical.with_hadamard();
320 }
321 let mut bases = self.infer_pipe_basis_from_source(&canonical);
322 if endpoint != canonical.src && pipe.hadamard {
323 for basis in bases.iter_mut().flatten() {
324 *basis = basis.flip();
325 }
326 }
327 bases
328 }
329
330 pub fn infer_spatial_port_cube_kind(&self, position: IVec3) -> Option<crate::CubeKind> {
337 let block = self.get_block(position)?;
338 if !block.kind().is_port() {
339 return None;
340 }
341 let pipe = self
342 .inner
343 .edges(self.try_get_id(position).ok()?)
344 .filter(|edge| edge.weight().src() == position || edge.weight().dst() == position)
345 .min_by_key(|edge| edge.id().index())?
346 .weight();
347 if !pipe.dir().is_spatial() {
348 return None;
349 }
350 let mut bases = self.infer_pipe_basis_from_endpoint(pipe, position);
351 let temporal = bases[crate::UDirection::Z.index()]?;
352 bases[pipe.dir().as_udirection().index()] = Some(temporal);
353 let kind = crate::CubeKind::try_from([bases[0]?, bases[1]?, bases[2]?]).ok()?;
354 (!kind.is_spatial()).then_some(kind)
355 }
356
357 fn infer_pipe_basis_from_source(&self, pipe: &Pipe) -> [Option<Basis>; 3] {
358 let Ok(dst) = pipe.try_dst() else {
359 return [None; 3];
360 };
361 let mut bases = [None; 3];
362 let src_bases = self.infer_pipe_endpoint_face_bases(pipe, pipe.src);
363 let dst_bases = self.infer_pipe_endpoint_face_bases(pipe, dst);
364
365 for (i, dir) in UDirection::iter().enumerate() {
366 if dir == pipe.dir.as_udirection() {
367 continue;
368 }
369 if let Some(basis) = src_bases[i] {
370 bases[i] = Some(basis);
371 } else if let Some(b) = dst_bases[i] {
372 bases[i] = Some(if pipe.hadamard { b.flip() } else { b });
373 }
374 }
375 let pipe_dir_id = pipe.dir.as_udirection().index();
376 let none_count = bases.iter().filter(|b| b.is_none()).count();
379 if none_count == 3 {
380 bases[(pipe_dir_id + 1) % 3] = Some(Basis::X);
381 bases[(pipe_dir_id + 2) % 3] = Some(Basis::Z);
382 } else if none_count == 2 {
383 let Some(resolved_id) = bases.iter().position(Option::is_some) else {
384 return bases;
385 };
386 let other_id = 3 - pipe_dir_id - resolved_id;
387 if let Some(resolved) = bases[resolved_id] {
388 bases[other_id] = Some(resolved.flip());
389 }
390 }
391 bases
392 }
393
394 pub(crate) fn infer_pipe_endpoint_face_bases(
395 &self,
396 pipe: &Pipe,
397 endpoint: IVec3,
398 ) -> [Option<Basis>; 3] {
399 let Ok(pipe_dst) = pipe.try_dst() else {
400 return [None; 3];
401 };
402 if endpoint != pipe.src && endpoint != pipe_dst {
403 return [None; 3];
404 }
405 let Some(block) = self.get_endpoint_block(endpoint) else {
406 return [None; 3];
407 };
408 let endpoint_bases = match block.kind() {
409 BlockKind::PatchRotation(kind) => {
410 kind.pipe_face_bases_at_endpoint(block.pos(), endpoint)
411 }
412 _ => block.bases_at_endpoint(endpoint),
413 };
414 let Some(kind_bases) = endpoint_bases else {
415 return [None; 3];
416 };
417
418 let mut bases = [None; 3];
419 for (i, dir) in UDirection::iter().enumerate() {
420 if dir == pipe.dir.as_udirection() {
421 continue;
422 }
423 let offset = dir.to_ivec3();
424 let has_opposite_pair = crate::checked_add_position(endpoint, offset)
425 .is_ok_and(|neighbor| self.has_pipe_between(endpoint, neighbor))
426 && crate::checked_add_position(endpoint, -offset)
427 .is_ok_and(|neighbor| self.has_pipe_between(endpoint, neighbor));
428 if !has_opposite_pair {
429 bases[i] = Some(kind_bases[i]);
430 }
431 }
432 bases
433 }
434
435 pub fn layer(&self, z: i32) -> BlockLayerView<'_> {
437 BlockLayerView { graph: self, z }
438 }
439
440 pub fn cube_layer_components(&self) -> BTreeMap<i32, Vec<Vec<IVec3>>> {
449 let mut layers = BTreeMap::<i32, Vec<IVec3>>::new();
450 for block in self.blocks().filter(|block| block.kind().is_cube()) {
451 layers.entry(block.pos().z).or_default().push(block.pos());
452 }
453
454 layers
455 .into_iter()
456 .map(|(z, mut positions)| {
457 positions.sort_by_key(IVec3::to_array);
458 let index: HashMap<IVec3, usize> = positions
459 .iter()
460 .copied()
461 .enumerate()
462 .map(|(i, pos)| (pos, i))
463 .collect();
464 let mut union = petgraph::unionfind::UnionFind::new(positions.len());
465 for pipe in self.layer(z).spacelike_pipes() {
466 let (src, dst) = pipe.endpoints();
467 let (Some(src), Some(dst)) =
468 (self.get_endpoint_block(src), self.get_endpoint_block(dst))
469 else {
470 continue;
471 };
472 let (Some(&src), Some(&dst)) = (index.get(&src.pos()), index.get(&dst.pos()))
475 else {
476 continue;
477 };
478 union.union(src, dst);
479 }
480
481 let mut groups: Vec<Vec<IVec3>> = Vec::new();
482 let mut slot_of_root = HashMap::<usize, usize>::new();
483 for (i, &pos) in positions.iter().enumerate() {
484 let slot = *slot_of_root.entry(union.find(i)).or_insert_with(|| {
485 groups.push(Vec::new());
486 groups.len() - 1
487 });
488 groups[slot].push(pos);
489 }
490 (z, groups)
491 })
492 .collect()
493 }
494
495 pub fn action_graph(&self) -> &ActionDag {
500 &self.action_graph
501 }
502
503 pub fn action_graph_error(&self) -> Option<&BlockGraphError> {
507 self.action_graph_error.as_ref()
508 }
509
510 pub fn analyze_action_graph(&self) -> Result<ActionDag, BlockGraphError> {
524 self.analyze_action_graph_with_limits(crate::ModuleCertificationLimits::DEFAULT)
525 }
526
527 pub fn analyze_action_graph_with_limits(
534 &self,
535 limits: crate::ModuleCertificationLimits,
536 ) -> Result<ActionDag, BlockGraphError> {
537 if self.has_module_structure() {
538 return crate::action_dag::hierarchy::analyze(self, limits);
539 }
540 if !self.has_continuing_branches() {
541 let (analyzed, _) = self.clone().analyze_actions_with_limits(limits)?;
542 return Ok(analyzed.action_graph);
543 }
544 let topology = crate::GuardedTopology::new(self, limits)?;
545 let mut plan = crate::GuardedSurfaceSpace::new(topology, &Default::default(), limits)?
546 .plan_readouts()?;
547 let mut dag = self.build_action_graph(&self.actions())?;
548 dag.attach_guarded_dependencies(plan.action_dependencies()?)?;
549 Ok(dag)
550 }
551
552 pub fn actions(&self) -> Vec<Action> {
554 self.action_graph
555 .ordered_nodes()
556 .map(|node| node.action.clone())
557 .collect()
558 }
559
560 pub fn has_actions(&self) -> bool {
562 self.action_graph.ordered_nodes().next().is_some()
563 }
564
565 pub fn get_block_id(&self, pos: impl Into<IVec3>) -> Option<u32> {
567 self.block_id.get(&pos.into()).copied()
568 }
569
570 pub(crate) fn blog_block_ids(&self) -> Vec<(IVec3, u32)> {
571 let mut blocks = self
572 .block_id
573 .iter()
574 .map(|(&position, &id)| (position, id))
575 .collect::<Vec<_>>();
576 blocks.sort_by_key(|(_, id)| *id);
577 if self.branches.is_empty() {
578 return blocks;
579 }
580 let shown = self
581 .branches
582 .iter()
583 .flat_map(|region| region.shown_arm().blocks().map(Block::pos))
584 .collect::<HashSet<_>>();
585 blocks
586 .into_iter()
587 .filter(|(position, _)| !shown.contains(position))
588 .enumerate()
589 .map(|(id, (position, _))| {
590 (
591 position,
592 u32::try_from(id).expect("BlockGraph ids fit in u32"),
593 )
594 })
595 .collect()
596 }
597
598 pub(crate) fn get_endpoint_block_id(&self, pos: impl Into<IVec3>) -> Option<u32> {
604 self.try_get_endpoint_id(pos)
605 .ok()
606 .map(|id| id.index() as u32)
607 }
608
609 pub fn to_zx_graph(&self) -> Result<ZXGraph, BlockGraphError> {
619 self.require_flat_hierarchy("ZX conversion")?;
620 ZXGraph::try_from(self).map_err(Into::into)
621 }
622
623 pub fn stabilizers(&self) -> Result<StabilizerGenerators, BlockGraphError> {
633 self.stabilizers_with_limits(crate::ModuleCertificationLimits::DEFAULT)
634 }
635
636 pub fn stabilizers_with_limits(
642 &self,
643 limits: crate::ModuleCertificationLimits,
644 ) -> Result<StabilizerGenerators, BlockGraphError> {
645 self.require_flat_hierarchy("stabilizer analysis")?;
646 self.validate_resource_limits(limits)?;
647 if let Some(target) = self.action_graph.ordered_nodes().find_map(|node| {
648 if let Action::Branch { target, .. } = node.action {
649 Some(target)
650 } else {
651 None
652 }
653 }) {
654 return Err(crate::InvalidActionError::MissingBranchValue(target).into());
655 }
656 self.stabilizer_zx_graph(limits)?
657 .stabilizers_with_limits(limits)
658 .map_err(Into::into)
659 }
660
661 fn stabilizer_zx_graph(
662 &self,
663 limits: crate::ModuleCertificationLimits,
664 ) -> Result<ZXGraph, BlockGraphError> {
665 let columns = self
666 .pipe_count()
667 .checked_mul(2)
668 .and_then(|pipes| pipes.checked_add(self.block_count()));
669 if columns.is_none_or(|columns| columns > limits.max_local_columns) {
670 return Err(crate::StabilizerError::ResourceLimited {
671 phase: "local ZX columns",
672 observed: columns.unwrap_or(usize::MAX),
673 limit: limits.max_local_columns,
674 }
675 .into());
676 }
677 ZXGraph::from_block_graph_for_analysis(self).map_err(Into::into)
678 }
679
680 pub(crate) fn stabilizers_for_action_analysis_with_limits(
684 &self,
685 limits: crate::ModuleCertificationLimits,
686 ) -> Result<StabilizerGenerators, BlockGraphError> {
687 self.stabilizer_analysis_for_actions_impl(limits)
688 .map(|(baseline, _)| baseline)
689 }
690
691 fn stabilizer_analysis_for_actions(
692 &self,
693 ) -> Result<(StabilizerGenerators, AnalyzedBranchProjections), BlockGraphError> {
694 self.stabilizer_analysis_for_actions_impl(crate::ModuleCertificationLimits::DEFAULT)
695 }
696
697 fn stabilizer_analysis_for_actions_impl(
698 &self,
699 limits: crate::ModuleCertificationLimits,
700 ) -> Result<(StabilizerGenerators, AnalyzedBranchProjections), BlockGraphError> {
701 self.validate_resource_limits(limits)?;
702 if self.has_continuing_branches() {
703 return Err(InvalidActionError::GuardedAnalysisRequired.into());
704 }
705 let canonical = self
706 .branches
707 .iter()
708 .any(|region| !region.shown_true())
709 .then(|| self.canonical_true_branch_view())
710 .transpose()?;
711 let graph = canonical.as_ref().unwrap_or(self);
712
713 let zx = graph.stabilizer_zx_graph(limits)?;
716 let baseline = zx.stabilizers_with_limits(limits)?;
717 if !graph
718 .action_graph
719 .ordered_nodes()
720 .any(|node| matches!(node.action, Action::Branch { .. }))
721 {
722 return Ok((baseline, Vec::new()));
723 }
724 baseline.validate_selective_decoupling()?;
725 let regions = graph.branch_regions()?;
726 for (name, row) in named_measurement_rows(&baseline) {
727 if regions
728 .iter()
729 .any(|region| positioned_stabilizer_touches_region(row, region))
730 {
731 return Err(InvalidActionError::BranchDependentMeasurementSurface {
732 name: name.to_owned(),
733 }
734 .into());
735 }
736 }
737
738 let mut projections = Vec::new();
739 let measurements = baseline
740 .generators
741 .iter()
742 .filter(|generator| generator.is_measurement())
743 .collect::<Vec<_>>();
744 let static_projections = graph.branch_projections_for_analysis_up_to_with_limits(
745 limits.max_guarded_domain_size.saturating_add(1),
746 limits.boolean_limits(),
747 )?;
748 if static_projections.len() > limits.max_guarded_domain_size {
749 return Err(crate::StabilizerError::ResourceLimited {
750 phase: "action-analysis branch projections",
751 observed: static_projections.len(),
752 limit: limits.max_guarded_domain_size,
753 }
754 .into());
755 }
756 for projection in static_projections {
757 let zx = projection.graph().stabilizer_zx_graph(limits)?;
758 let projected = if let Some(reused) = zx
759 .stabilizers_reusing_measurements(&measurements, limits)?
760 .filter(|candidate| {
761 candidate.validate_selective_decoupling().is_ok()
762 && validate_shared_measurement_rows(&baseline, candidate).is_ok()
763 }) {
764 reused
765 } else {
766 let independent = zx.stabilizers_with_limits(limits)?;
767 independent.validate_selective_decoupling()?;
768 validate_shared_measurement_rows(&baseline, &independent)?;
769 independent
770 };
771 let projection = projection.with_analyzed_actions(&projected)?;
772 projections.push((projection, projected));
773 }
774
775 Ok((baseline, projections))
776 }
777
778 pub fn with_analyzed_action_graph(
787 mut self,
788 stabilizers: &StabilizerGenerators,
789 ) -> Result<Self, BlockGraphError> {
790 let actions = self.actions();
791 let mut dag = self.build_action_graph(&actions)?;
792 stabilizers.validate_selective_decoupling()?;
793 dag.attach_readout_dependencies(&stabilizers.generators, Some(&stabilizers.zx_graph))?;
794 self.install_action_graph(dag, None);
795 Ok(self)
796 }
797
798 pub fn analyze_actions(self) -> Result<(Self, StabilizerGenerators), BlockGraphError> {
810 self.analyze_actions_with_limits(crate::ModuleCertificationLimits::DEFAULT)
811 }
812
813 pub fn analyze_actions_with_limits(
819 self,
820 limits: crate::ModuleCertificationLimits,
821 ) -> Result<(Self, StabilizerGenerators), BlockGraphError> {
822 self.require_flat_hierarchy("action analysis")?;
823 let (stabilizers, _) = self.stabilizer_analysis_for_actions_impl(limits)?;
824 let graph = self.with_analyzed_action_graph(&stabilizers)?;
825 Ok((graph, stabilizers))
826 }
827
828 #[doc(hidden)]
829 pub fn analyze_actions_with_projections(
830 self,
831 ) -> Result<(Self, StabilizerGenerators, AnalyzedBranchProjections), BlockGraphError> {
832 self.require_flat_hierarchy("action analysis")?;
833 let (stabilizers, projections) = self.stabilizer_analysis_for_actions()?;
834 let graph = self.with_analyzed_action_graph(&stabilizers)?;
835 Ok((graph, stabilizers, projections))
836 }
837
838 pub(crate) fn validate_guarded_actions_with_limits(
840 &self,
841 limits: crate::ModuleCertificationLimits,
842 ) -> Result<(), BlockGraphError> {
843 let topology = crate::GuardedTopology::new(self, limits)?;
844 crate::GuardedSurfaceSpace::new(topology, &Default::default(), limits)?
845 .plan_readouts()
846 .map(drop)
847 }
848
849 fn analyze_installed_actions(
851 &mut self,
852 limits: crate::ModuleCertificationLimits,
853 ) -> Result<(), BlockGraphError> {
854 if self.has_continuing_branches() {
855 return self.validate_guarded_actions_with_limits(limits);
856 }
857 let (stabilizers, _) = self.stabilizer_analysis_for_actions_impl(limits)?;
858 stabilizers.validate_selective_decoupling()?;
859 self.action_graph
860 .attach_readout_dependencies(&stabilizers.generators, Some(&stabilizers.zx_graph))
861 }
862
863 pub fn fill_ports_auto(
873 &self,
874 ) -> Result<Vec<(BlockGraph, Vec<StabilizerGenerator>)>, BlockGraphError> {
875 self.fill_ports_auto_with_limits(crate::ModuleCertificationLimits::DEFAULT)
876 }
877
878 pub fn fill_ports_auto_with_limits(
885 &self,
886 limits: crate::ModuleCertificationLimits,
887 ) -> Result<Vec<(BlockGraph, Vec<StabilizerGenerator>)>, BlockGraphError> {
888 self.require_flat_source("boundary filling")?;
889 fill_ports_auto_impl(self, limits)
890 }
891
892 pub fn filled_graphs(&self) -> Result<Vec<BlockGraph>, BlockGraphError> {
899 self.filled_graphs_with_limits(crate::ModuleCertificationLimits::DEFAULT)
900 }
901
902 pub fn filled_graphs_with_limits(
909 &self,
910 limits: crate::ModuleCertificationLimits,
911 ) -> Result<Vec<BlockGraph>, BlockGraphError> {
912 Ok(self
913 .fill_ports_auto_with_limits(limits)?
914 .into_iter()
915 .map(|(graph, _)| graph)
916 .collect())
917 }
918
919 #[must_use]
922 pub fn spans(
923 &self,
924 ) -> Option<(
925 RangeInclusive<i32>,
926 RangeInclusive<i32>,
927 RangeInclusive<i32>,
928 )> {
929 let boundary_positions = |block: &Block| {
930 let offsets = if block.kind().is_cube() {
933 block.connectable_offsets()
934 } else {
935 block.reserved_offsets()
936 };
937 let position = block.pos();
938 offsets.into_iter().map(move |offset| position + offset)
939 };
940 let visible = self.blocks().flat_map(boundary_positions);
941 let branch_arms = self
942 .branches
943 .iter()
944 .flat_map(|region| region.on_false().blocks().chain(region.on_true().blocks()))
945 .flat_map(boundary_positions);
946 let mut positions = visible.chain(branch_arms);
947 let first_pos = positions.next()?;
948
949 let (mut min_x, mut max_x) = (first_pos.x, first_pos.x);
950 let (mut min_y, mut max_y) = (first_pos.y, first_pos.y);
951 let (mut min_z, mut max_z) = (first_pos.z, first_pos.z);
952
953 for pos in positions {
954 min_x = min_x.min(pos.x);
955 max_x = max_x.max(pos.x);
956 min_y = min_y.min(pos.y);
957 max_y = max_y.max(pos.y);
958 min_z = min_z.min(pos.z);
959 max_z = max_z.max(pos.z);
960 }
961
962 Some((min_x..=max_x, min_y..=max_y, min_z..=max_z))
963 }
964
965 pub fn randomly_resolve_selectives(
984 &self,
985 seed: u64,
986 ) -> Result<(BlockGraph, HashMap<Block, Block>), BlockGraphError> {
987 self.require_flat_source("selective resolution")?;
988 let mut rng = StdRng::seed_from_u64(seed);
989 let selective_blocks: Vec<Block> = self
990 .blocks()
991 .filter(|block| block.kind.is_selective())
992 .cloned()
993 .collect();
994 let mut resolved_graph = self.clone();
995 let mut replacements = HashMap::with_capacity(selective_blocks.len());
996 let has_selectives = !selective_blocks.is_empty();
997 let targets = selective_blocks
998 .iter()
999 .map(|block| block.pos)
1000 .collect::<Vec<_>>();
1001 let sampled_resolves = self.action_graph.sample_resolve_values(&targets, &mut rng);
1002
1003 for original in selective_blocks {
1004 let resolved_kind = sampled_resolves.get(&original.pos).map_or_else(
1005 || self.randomly_resolved_selective_kind(&original, &mut rng),
1006 |&value| {
1007 let BlockKind::Selective(kind) = original.kind else {
1008 unreachable!("selective_blocks contains only selectives")
1009 };
1010 let chosen = if value {
1011 kind.pauli_if_true()
1012 } else {
1013 kind.pauli_if_false()
1014 };
1015 self.resolved_selective_kind_for_basis(chosen)
1016 },
1017 );
1018 let resolved_block = {
1019 let block = resolved_graph
1020 .get_block_mut(original.pos)
1021 .expect("resolved graph cloned from source must still contain selective block");
1022 block.kind = resolved_kind;
1025 block.clone()
1026 };
1027 replacements.insert(original, resolved_block);
1028 }
1029
1030 if has_selectives {
1033 resolved_graph.clear_actions();
1034 }
1035
1036 Ok((resolved_graph, replacements))
1037 }
1038
1039 pub fn add_action(&mut self, action: Action) -> Result<(), BlockGraphError> {
1047 let mut ordered = self
1048 .action_graph
1049 .ordered_nodes()
1050 .map(|node| node.action.clone())
1051 .collect::<Vec<_>>();
1052 ordered.push(action);
1053 self.set_actions(ordered)
1054 }
1055
1056 pub(crate) fn resolve_measurement_observable(
1057 &self,
1058 target: &MeasureTarget,
1059 ) -> Result<MeasurementObservable, BlockGraphError> {
1060 match target {
1061 MeasureTarget::Node(pos) => match self
1062 .get_block(*pos)
1063 .ok_or(BlockGraphError::BlockNotFound(*pos))?
1064 .kind
1065 {
1066 BlockKind::Y => Ok(MeasurementObservable::Concrete(PauliBasis::Y)),
1067 BlockKind::Measurement(basis) => Ok(MeasurementObservable::Concrete(basis.into())),
1068 BlockKind::Selective(kind) => Ok(MeasurementObservable::Selective(kind)),
1069 BlockKind::Cube(kind) => Ok(MeasurementObservable::Concrete(kind.z().into())),
1070 BlockKind::Walking(kind) => {
1071 Ok(MeasurementObservable::Concrete(kind.boundary().z().into()))
1072 }
1073 BlockKind::PatchRotation(kind) => {
1074 Ok(MeasurementObservable::Concrete(kind.basis().into()))
1075 }
1076 BlockKind::Port | BlockKind::T => Err(BlockGraphError::InvalidAction(
1077 InvalidActionError::InvalidMeasurementNode(*pos),
1078 )),
1079 },
1080 MeasureTarget::Edge { src, dir } => {
1081 let dst = crate::checked_add_position(*src, dir.to_ivec3())?;
1082 let pipe = self
1083 .get_pipe(*src, dst)
1084 .ok_or(BlockGraphError::PipeNotFound(*src, dst))?;
1085 let bases = self.infer_pipe_basis(pipe);
1086 let basis =
1090 bases[2].ok_or(BlockGraphError::MeasurementEdgeMissingTimeBasis(*src, dst))?;
1091 Ok(MeasurementObservable::Concrete(basis.flip().into()))
1092 }
1093 }
1094 }
1095
1096 pub(crate) fn build_action_graph(
1097 &self,
1098 actions: &[Action],
1099 ) -> Result<ActionDag, BlockGraphError> {
1100 if actions.is_empty() {
1101 return Ok(self.new_action_graph(actions));
1102 }
1103 let mut dag = self.new_action_graph(actions);
1104 dag.validate(Some(self))?;
1105 self.attach_measurement_observables(&mut dag, actions.len(), true)?;
1106 Ok(dag)
1107 }
1108
1109 fn new_action_graph(&self, actions: &[Action]) -> ActionDag {
1110 ActionDag::from_actions_with_inputs(actions, self.action_inputs.iter().cloned())
1111 }
1112
1113 fn attach_measurement_observables(
1121 &self,
1122 dag: &mut ActionDag,
1123 len: usize,
1124 strict: bool,
1125 ) -> Result<(), BlockGraphError> {
1126 for ordinal in 0..len {
1127 let target = match dag.node_by_ordinal(ordinal).map(|node| &node.action) {
1128 Some(Action::Measure { target, .. }) => *target,
1129 _ => continue,
1130 };
1131 match self.resolve_measurement_observable(&target) {
1132 Ok(observable) => dag
1133 .set_measurement_observable(ordinal, observable)
1134 .expect("ordinals 0..len index the DAG built from these same actions"),
1135 Err(err) if strict => return Err(err),
1136 Err(_) => {}
1137 }
1138 }
1139 Ok(())
1140 }
1141
1142 fn install_action_graph(&mut self, dag: ActionDag, error: Option<BlockGraphError>) {
1145 self.action_graph = dag;
1146 self.action_graph_error = error;
1147 }
1148
1149 fn build_action_graph_best_effort(&self, actions: &[Action]) -> ActionDag {
1153 let mut dag = self.new_action_graph(actions);
1154 let _ = self.attach_measurement_observables(&mut dag, actions.len(), false);
1155 dag
1156 }
1157
1158 pub(crate) fn rebuild_action_graph_lenient(&mut self, actions: Vec<Action>) {
1161 match self.build_action_graph(&actions) {
1162 Ok(dag) => self.install_action_graph(dag, None),
1163 Err(err) => {
1164 let dag = self.build_action_graph_best_effort(&actions);
1165 self.install_action_graph(dag, Some(err));
1166 }
1167 }
1168 }
1169
1170 #[cfg(test)]
1171 pub(crate) fn set_actions_unchecked(&mut self, actions: Vec<Action>) {
1172 let dag = self.new_action_graph(&actions);
1173 self.install_action_graph(dag, None);
1174 }
1175
1176 pub fn set_actions(&mut self, actions: Vec<Action>) -> Result<(), BlockGraphError> {
1187 self.set_actions_with_limits(actions, crate::ModuleCertificationLimits::DEFAULT)
1188 }
1189
1190 pub(crate) fn set_actions_with_limits(
1191 &mut self,
1192 actions: Vec<Action>,
1193 limits: crate::ModuleCertificationLimits,
1194 ) -> Result<(), BlockGraphError> {
1195 let dag = self.build_action_graph(&actions)?;
1196 if actions.is_empty() || crate::validate::validate_structure(self).is_err() {
1197 self.install_action_graph(dag, None);
1200 return Ok(());
1201 }
1202
1203 let previous = std::mem::replace(&mut self.action_graph, dag);
1206 let previous_error = self.action_graph_error.take();
1207 if let Err(err) = self.analyze_installed_actions(limits) {
1208 self.install_action_graph(previous, previous_error);
1209 return Err(err);
1210 }
1211 Ok(())
1212 }
1213
1214 pub fn set_actions_with_inputs(
1224 &mut self,
1225 actions: Vec<Action>,
1226 inputs: impl IntoIterator<Item = String>,
1227 ) -> Result<(), BlockGraphError> {
1228 self.set_actions_with_inputs_and_limits(
1229 actions,
1230 inputs,
1231 crate::ModuleCertificationLimits::DEFAULT,
1232 )
1233 }
1234
1235 pub(crate) fn set_actions_with_inputs_and_limits(
1236 &mut self,
1237 actions: Vec<Action>,
1238 inputs: impl IntoIterator<Item = String>,
1239 limits: crate::ModuleCertificationLimits,
1240 ) -> Result<(), BlockGraphError> {
1241 self.set_actions_with_inputs_impl(actions, inputs, false, limits)
1242 }
1243
1244 pub(crate) fn set_actions_deferred_with_inputs(
1245 &mut self,
1246 actions: Vec<Action>,
1247 inputs: impl IntoIterator<Item = String>,
1248 ) -> Result<(), BlockGraphError> {
1249 self.set_actions_with_inputs_impl(
1250 actions,
1251 inputs,
1252 true,
1253 crate::ModuleCertificationLimits::DEFAULT,
1254 )
1255 }
1256
1257 fn set_actions_with_inputs_impl(
1258 &mut self,
1259 actions: Vec<Action>,
1260 inputs: impl IntoIterator<Item = String>,
1261 deferred: bool,
1262 limits: crate::ModuleCertificationLimits,
1263 ) -> Result<(), BlockGraphError> {
1264 let inputs = inputs.into_iter().collect::<BTreeSet<_>>();
1265 if let Some(name) = inputs
1266 .iter()
1267 .find(|name| !crate::parser::is_valid_identifier(name))
1268 {
1269 return Err(InvalidActionError::InvalidActionName(name.clone()).into());
1270 }
1271 let previous = std::mem::replace(&mut self.action_inputs, inputs);
1272 let result = if deferred {
1273 self.set_actions_deferred(actions)
1274 } else {
1275 self.set_actions_with_limits(actions, limits)
1276 };
1277 if let Err(err) = result {
1278 self.action_inputs = previous;
1279 return Err(err);
1280 }
1281 Ok(())
1282 }
1283
1284 pub fn set_actions_deferred(&mut self, actions: Vec<Action>) -> Result<(), BlockGraphError> {
1292 let dag = self.build_action_graph(&actions)?;
1293 self.install_action_graph(dag, None);
1294 Ok(())
1295 }
1296
1297 pub fn set_actions_lenient(&mut self, actions: Vec<Action>) -> Result<(), BlockGraphError> {
1312 for action in &actions {
1313 Self::validate_action_syntax(action)?;
1314 }
1315 self.rebuild_action_graph_lenient(actions);
1316 Ok(())
1317 }
1318
1319 pub(crate) fn validate_action_syntax(action: &Action) -> Result<(), BlockGraphError> {
1327 if matches!(action, Action::Feedback { targets, .. } if targets.is_empty()) {
1328 return Err(InvalidActionError::EmptyFeedbackTargets.into());
1329 }
1330 let name = match action {
1331 Action::Measure { name, .. } | Action::Let { name, .. } => name,
1332 _ => return Ok(()),
1333 };
1334 if crate::parser::is_valid_identifier(name) {
1335 Ok(())
1336 } else {
1337 Err(InvalidActionError::InvalidActionName(name.clone()).into())
1338 }
1339 }
1340
1341 pub fn clear_actions(&mut self) {
1343 self.action_inputs.clear();
1344 self.install_action_graph(ActionDag::default(), None);
1345 }
1346
1347 fn refresh_action_graph_after_mutation(&mut self) {
1348 let actions = self.actions();
1353 self.rebuild_action_graph_lenient(actions);
1354 }
1355
1356 pub fn shift_positions(&self, offset: IVec3) -> Result<Self, BlockGraphError> {
1364 if offset == IVec3::ZERO {
1365 return Ok(self.clone());
1366 }
1367 self.require_flat_source("position shifting")?;
1368 let mut g = Self::new();
1369 g.action_inputs = self.action_inputs.clone();
1370 for block in self.blocks() {
1371 g.try_add_block(block.try_with_shift(offset)?)?;
1372 }
1373 for pipe in self.pipes() {
1374 g.try_add_pipe(pipe.try_with_shift(offset)?)?;
1375 }
1376 g.branches =
1377 self.transformed_branches(|position| crate::checked_add_position(position, offset))?;
1378 let shifted_actions = self
1379 .action_graph
1380 .ordered_nodes()
1381 .map(|node| node.action.try_with_shift(offset))
1382 .collect::<Result<Vec<_>, _>>()?;
1383 g.rebuild_action_graph_lenient(shifted_actions);
1386 Ok(g)
1387 }
1388
1389 pub fn rotate_about_origin(
1398 &self,
1399 axis: UDirection,
1400 quarter_turns: i32,
1401 ) -> Result<Self, BlockGraphError> {
1402 let rotated = self.rotate_about_origin_lenient(axis, quarter_turns)?;
1403 rotated.validate()?;
1404 Ok(rotated)
1405 }
1406
1407 pub fn rotate_about_origin_lenient(
1413 &self,
1414 axis: UDirection,
1415 quarter_turns: i32,
1416 ) -> Result<Self, BlockGraphError> {
1417 self.with_orientation_lenient(crate::ModuleRotation::new(axis, quarter_turns).orientation())
1418 }
1419
1420 #[doc(hidden)]
1422 pub fn with_orientation_lenient(
1423 &self,
1424 orientation: crate::ModuleOrientation,
1425 ) -> Result<Self, BlockGraphError> {
1426 if orientation != crate::ModuleOrientation::IDENTITY {
1427 self.require_flat_source("rotation")?;
1428 }
1429 validate_block_orientation(
1430 self.blocks().chain(
1431 self.branches
1432 .iter()
1433 .flat_map(|branch| branch.on_false().blocks().chain(branch.on_true().blocks())),
1434 ),
1435 orientation,
1436 )?;
1437 if orientation == crate::ModuleOrientation::IDENTITY {
1438 return Ok(self.clone());
1439 }
1440 let mut rotated = Self::new();
1441 rotated.action_inputs = self.action_inputs.clone();
1442
1443 for block in self.blocks() {
1444 rotated.try_add_block(oriented_block(block, orientation)?)?;
1445 }
1446
1447 for pipe in self.pipes() {
1448 rotated.try_add_pipe(oriented_pipe(pipe, orientation)?)?;
1449 }
1450
1451 rotated.branches = self
1452 .branches
1453 .iter()
1454 .map(|branch| branch.try_with_orientation(orientation))
1455 .collect::<Result<_, _>>()?;
1456 let actions = self
1457 .action_graph
1458 .ordered_nodes()
1459 .map(|node| node.action.try_with_orientation(orientation))
1460 .collect::<Result<Vec<_>, _>>()?;
1461 rotated.rebuild_action_graph_lenient(actions);
1462 Ok(rotated)
1463 }
1464
1465 pub fn flip_xz_basis_lenient(&self) -> Result<Self, BlockGraphError> {
1479 self.require_flat_source("basis flipping")?;
1480 let mut flipped = self.clone();
1481 for block in flipped.inner.node_weights_mut() {
1482 block.kind = block.kind.flip_xz_basis();
1483 }
1484 for pipe in flipped.inner.edge_weights_mut() {
1485 flip_pipe_xz_basis(pipe, |pos| {
1486 self.get_block(pos)
1487 .is_some_and(|block| matches!(block.kind(), BlockKind::T | BlockKind::Y))
1488 });
1489 }
1490 flipped.branches = self
1491 .branches
1492 .iter()
1493 .map(|branch| branch.flip_xz_basis(self))
1494 .collect();
1495 let mut actions = self.actions();
1496 for action in &mut actions {
1497 if let Action::Resolve { condition, .. } = action {
1498 *condition = condition.clone().negated();
1499 }
1500 if let Action::Feedback { targets, .. } = action {
1501 for target in targets {
1502 target.pauli = match target.pauli {
1503 PauliBasis::X => PauliBasis::Z,
1504 PauliBasis::Y => PauliBasis::Y,
1505 PauliBasis::Z => PauliBasis::X,
1506 };
1507 }
1508 }
1509 }
1510 let mut y_signs = flipped
1511 .blocks()
1512 .filter_map(|block| {
1513 let pauli = match block.kind() {
1514 BlockKind::Selective(crate::SelectiveKind::XY) => PauliBasis::X,
1515 BlockKind::Selective(crate::SelectiveKind::YZ) => PauliBasis::Z,
1516 _ => return None,
1517 };
1518 Some(crate::FeedbackTarget {
1519 pauli,
1520 target: block.pos(),
1521 direction: None,
1522 })
1523 })
1524 .collect::<Vec<_>>();
1525 y_signs.sort_unstable_by_key(|target| target.target.to_array());
1526 if !y_signs.is_empty() {
1527 let correction = Action::Feedback {
1528 targets: y_signs,
1529 condition: None,
1530 };
1531 if actions.last() == Some(&correction) {
1534 actions.pop();
1535 } else {
1536 actions.push(correction);
1537 }
1538 }
1539 flipped.rebuild_action_graph_lenient(actions);
1540 Ok(flipped)
1541 }
1542
1543 pub fn flip_xz_basis(&self) -> Result<Self, BlockGraphError> {
1550 fn is_missing_resolve(err: &BlockGraphError) -> bool {
1551 matches!(
1552 err,
1553 BlockGraphError::InvalidAction(
1554 InvalidActionError::MissingResolveForSelective { .. }
1555 )
1556 )
1557 }
1558
1559 let mut flipped = self.flip_xz_basis_lenient()?;
1560 let actions = flipped.actions();
1561 match flipped
1562 .set_actions(actions)
1563 .and_then(|()| flipped.validate())
1564 {
1565 Ok(()) => Ok(flipped),
1566 Err(err) if is_missing_resolve(&err) => {
1567 flipped.clear_actions();
1568 flipped.validate()?;
1569 Ok(flipped)
1570 }
1571 Err(err) => Err(err),
1572 }
1573 }
1574
1575 pub fn with_zero_min_z(&self) -> Result<Self, BlockGraphError> {
1583 self.require_flat_source("time normalization")?;
1584 let Some((_, _, z_span)) = self.spans() else {
1585 return Ok(self.clone());
1586 };
1587 let min_z = *z_span.start();
1588 let max_z = *z_span.end();
1589 if min_z == 0 {
1590 return Ok(self.clone());
1591 }
1592 if i64::from(max_z) - i64::from(min_z) > i64::from(i32::MAX) {
1593 return Err(BlockGraphError::CoordinateNormalizationOverflow { min_z, max_z });
1594 }
1595
1596 let normalize = |position| normalize_z_position(position, min_z, max_z);
1597 let mut normalized = Self::new();
1598 normalized.action_inputs = self.action_inputs.clone();
1599 for block in self.blocks() {
1600 let mut block = block.clone();
1601 block.pos = normalize(block.pos)?;
1602 normalized.try_add_block(block)?;
1603 }
1604 for pipe in self.pipes() {
1605 let mut pipe = pipe.clone();
1606 pipe.src = normalize(pipe.src)?;
1607 normalized.try_add_pipe(pipe)?;
1608 }
1609 normalized.branches = self.transformed_branches(normalize)?;
1610 let actions = self
1611 .action_graph
1612 .ordered_nodes()
1613 .map(|node| normalize_action_z(&node.action, min_z, max_z))
1614 .collect::<Result<Vec<_>, _>>()?;
1615 normalized.rebuild_action_graph_lenient(actions);
1616 Ok(normalized)
1617 }
1618
1619 fn all_reachable_definitions(&self, predicate: impl Fn(&Self) -> bool) -> bool {
1620 let mut pending = vec![self];
1621 let mut seen = HashSet::new();
1622 while let Some(graph) = pending.pop() {
1623 if !seen.insert(graph.name.as_str()) {
1624 continue;
1625 }
1626 if !predicate(graph) {
1627 return false;
1628 }
1629 for instance in &graph.instances {
1630 let Some(child) = self.module(&instance.definition) else {
1631 return false;
1632 };
1633 pending.push(child);
1634 }
1635 }
1636 true
1637 }
1638
1639 fn local_blocks_with_branch_arms(&self) -> impl Iterator<Item = &Block> {
1640 self.blocks().chain(
1641 self.branches
1642 .iter()
1643 .flat_map(|branch| branch.on_false().blocks().chain(branch.on_true().blocks())),
1644 )
1645 }
1646
1647 pub fn is_empty(&self) -> bool {
1650 self.all_reachable_definitions(|graph| {
1651 graph.local_blocks_with_branch_arms().next().is_none()
1652 })
1653 }
1654
1655 pub fn block_count(&self) -> usize {
1657 self.inner.node_count()
1658 }
1659
1660 pub fn pipe_count(&self) -> usize {
1662 self.inner.edge_count()
1663 }
1664
1665 fn count_kind(&self, pred: impl Fn(&BlockKind) -> bool) -> usize {
1666 self.inner.node_weights().filter(|b| pred(&b.kind)).count()
1667 }
1668
1669 pub fn walking_count(&self) -> usize {
1671 self.count_kind(BlockKind::is_walking)
1672 }
1673
1674 pub fn patch_rotation_count(&self) -> usize {
1676 self.count_kind(BlockKind::is_patch_rotation)
1677 }
1678
1679 pub fn port_count(&self) -> usize {
1681 self.count_kind(BlockKind::is_port)
1682 }
1683
1684 pub fn y_count(&self) -> usize {
1686 self.count_kind(BlockKind::is_y)
1687 }
1688
1689 pub fn t_count(&self) -> usize {
1691 self.count_kind(BlockKind::is_t)
1692 }
1693
1694 pub fn selective_count(&self) -> usize {
1696 self.count_kind(BlockKind::is_selective)
1697 }
1698
1699 pub fn blocks(&self) -> impl Iterator<Item = &Block> {
1701 self.inner.node_weights()
1702 }
1703
1704 pub fn pipes(&self) -> impl Iterator<Item = &Pipe> {
1706 self.inner.edge_weights()
1707 }
1708
1709 pub fn pipes_at(&self, position: IVec3) -> impl Iterator<Item = &Pipe> {
1713 self.block_id
1714 .get(&position)
1715 .into_iter()
1716 .flat_map(|&id| self.inner.edges(NodeIndex::new(id as usize)))
1717 .map(|edge| edge.weight())
1718 }
1719
1720 pub fn positions(&self) -> impl Iterator<Item = IVec3> {
1722 self.inner.node_weights().map(|b| b.pos)
1723 }
1724
1725 pub fn is_clifford(&self) -> bool {
1728 self.all_reachable_definitions(|graph| {
1729 graph
1730 .local_blocks_with_branch_arms()
1731 .all(|block| !block.kind.is_t())
1732 })
1733 }
1734
1735 pub fn is_rigid(&self) -> bool {
1738 self.all_reachable_definitions(|graph| {
1739 graph
1740 .local_blocks_with_branch_arms()
1741 .all(|block| !block.kind.is_dynamic())
1742 })
1743 }
1744
1745 pub fn is_open(&self) -> bool {
1748 !self.interface.quantum_ports.is_empty()
1749 || self
1750 .local_blocks_with_branch_arms()
1751 .any(|block| block.kind.is_port())
1752 }
1753
1754 fn try_get_id(&self, pos: impl Into<IVec3>) -> Result<NodeIndex<u32>, BlockGraphError> {
1755 let pos = pos.into();
1756 self.block_id
1757 .get(&pos)
1758 .copied()
1759 .map(NodeIndex::from)
1760 .ok_or(BlockGraphError::BlockNotFound(pos))
1761 }
1762
1763 fn try_get_endpoint_id(
1764 &self,
1765 pos: impl Into<IVec3>,
1766 ) -> Result<NodeIndex<u32>, BlockGraphError> {
1767 let pos = pos.into();
1768 if let Some(id) = self.block_id.get(&pos).copied() {
1769 return Ok(NodeIndex::from(id));
1770 }
1771
1772 self.reserved_by_position
1773 .get(&pos)
1774 .into_iter()
1775 .flatten()
1776 .find_map(|anchor| {
1777 let id = self
1778 .block_id
1779 .get(anchor)
1780 .copied()
1781 .expect("reserved-position index references an existing block");
1782 let block = self
1783 .inner
1784 .node_weight(NodeIndex::from(id))
1785 .expect("block_id references an existing block");
1786 block
1787 .connectable_offsets()
1788 .into_iter()
1789 .filter(|offset| *offset != IVec3::ZERO)
1790 .any(|offset| block.pos + offset == pos)
1791 .then_some(id)
1792 })
1793 .map(NodeIndex::from)
1794 .ok_or(BlockGraphError::BlockNotFound(pos))
1795 }
1796
1797 pub fn get_block(&self, position: impl Into<IVec3>) -> Option<&Block> {
1799 self.try_get_id(position)
1800 .ok()
1801 .and_then(|id| self.inner.node_weight(id))
1802 }
1803
1804 pub fn get_endpoint_block(&self, position: impl Into<IVec3>) -> Option<&Block> {
1810 self.try_get_endpoint_id(position)
1811 .ok()
1812 .and_then(|id| self.inner.node_weight(id))
1813 }
1814
1815 pub fn has_block_at(&self, position: impl Into<IVec3>) -> bool {
1817 self.block_id.contains_key(&position.into())
1818 }
1819
1820 pub fn has_endpoint_at(&self, position: impl Into<IVec3>) -> bool {
1822 self.try_get_endpoint_id(position).is_ok()
1823 }
1824
1825 pub fn occupied_positions(&self) -> impl Iterator<Item = IVec3> + '_ {
1827 self.blocks().flat_map(|block| {
1828 block
1829 .reserved_offsets()
1830 .into_iter()
1831 .map(move |offset| block.pos + offset)
1832 })
1833 }
1834
1835 pub fn can_place_block(&self, block: &Block) -> Result<(), BlockGraphError> {
1842 self.can_place_block_except(block, None)
1843 }
1844
1845 fn can_place_block_except(
1846 &self,
1847 block: &Block,
1848 ignored_anchor: Option<IVec3>,
1849 ) -> Result<(), BlockGraphError> {
1850 let edited_region = ignored_anchor.and_then(|position| {
1851 self.branches
1852 .iter()
1853 .position(|region| region.shown_arm().contains_block(position))
1854 });
1855 for occupied in block.checked_reserved_positions()? {
1856 self.check_visible_occupancy(block, occupied, ignored_anchor)?;
1857 for (index, region) in self.branches.iter().enumerate() {
1858 if Some(index) == edited_region {
1859 continue;
1860 }
1861 for existing in region.on_false().blocks().chain(region.on_true().blocks()) {
1862 let overlaps = existing
1863 .reserved_offsets()
1864 .into_iter()
1865 .any(|offset| existing.pos() + offset == occupied);
1866 if overlaps
1867 && (edited_region.is_some()
1868 || !block.kind.allows_reserved_overlap(
1869 block.pos,
1870 existing.kind(),
1871 existing.pos(),
1872 occupied,
1873 ))
1874 {
1875 return Err(BlockGraphError::BlockPositionOccupied(occupied));
1876 }
1877 }
1878 }
1879 }
1880 Ok(())
1881 }
1882
1883 fn check_visible_occupancy(
1884 &self,
1885 block: &Block,
1886 occupied: IVec3,
1887 ignored_anchor: Option<IVec3>,
1888 ) -> Result<(), BlockGraphError> {
1889 for &anchor in self
1890 .reserved_by_position
1891 .get(&occupied)
1892 .into_iter()
1893 .flatten()
1894 .filter(|anchor| Some(**anchor) != ignored_anchor)
1895 {
1896 let existing = self
1897 .get_block(anchor)
1898 .expect("reserved-position index references an existing block");
1899 if !block
1900 .kind
1901 .allows_reserved_overlap(block.pos, existing.kind, existing.pos, occupied)
1902 {
1903 return Err(BlockGraphError::BlockPositionOccupied(occupied));
1904 }
1905 }
1906 Ok(())
1907 }
1908
1909 fn index_reserved_positions(&mut self, block: &Block) {
1910 for position in block
1911 .checked_reserved_positions()
1912 .expect("placed block footprint fits the coordinate range")
1913 {
1914 self.reserved_by_position
1915 .entry(position)
1916 .or_default()
1917 .push(block.pos);
1918 }
1919 }
1920
1921 fn unindex_reserved_positions(&mut self, block: &Block) {
1922 for position in block
1923 .checked_reserved_positions()
1924 .expect("placed block footprint fits the coordinate range")
1925 {
1926 let Some(anchors) = self.reserved_by_position.get_mut(&position) else {
1927 continue;
1928 };
1929 anchors.retain(|anchor| *anchor != block.pos);
1930 if anchors.is_empty() {
1931 self.reserved_by_position.remove(&position);
1932 }
1933 }
1934 }
1935
1936 pub fn set_cube_height(
1953 &mut self,
1954 position: IVec3,
1955 height: CubeHeight,
1956 ) -> Result<(), BlockGraphError> {
1957 let existing = self
1958 .get_block(position)
1959 .ok_or(BlockGraphError::BlockNotFound(position))?
1960 .clone();
1961 let candidate = existing.clone().with_height(height)?;
1962 self.can_place_block_except(&candidate, Some(position))?;
1963 self.check_preserved_endpoints(&candidate)?;
1964 self.unindex_reserved_positions(&existing);
1965 self.index_reserved_positions(&candidate);
1966 *self
1967 .get_block_mut(position)
1968 .expect("block existence checked above") = candidate;
1969 self.sync_shown_branch_arm_data();
1970 self.refresh_action_graph_after_mutation();
1971 Ok(())
1972 }
1973
1974 pub fn has_pipe_between(&self, u: impl Into<IVec3>, v: impl Into<IVec3>) -> bool {
1976 self.get_pipe(u, v).is_some()
1977 }
1978
1979 pub(crate) fn get_block_mut(&mut self, position: impl Into<IVec3>) -> Option<&mut Block> {
1980 self.try_get_id(position)
1981 .ok()
1982 .and_then(|index| self.inner.node_weight_mut(index))
1983 }
1984
1985 fn check_preserved_endpoints(&self, candidate: &Block) -> Result<(), BlockGraphError> {
1986 let position = candidate.pos();
1987 let endpoints = candidate.connectable_offsets();
1988 for pipe in self.inner.edges(self.try_get_id(position)?) {
1989 for endpoint in [pipe.weight().src(), pipe.weight().dst()] {
1990 if self
1991 .get_endpoint_block(endpoint)
1992 .is_some_and(|owner| owner.pos() == position)
1993 && !endpoints
1994 .iter()
1995 .any(|offset| position + *offset == endpoint)
1996 {
1997 return Err(BlockGraphError::BlockNotFound(endpoint));
1998 }
1999 }
2000 }
2001 Ok(())
2002 }
2003
2004 pub fn set_block_kind(
2014 &mut self,
2015 position: impl Into<IVec3>,
2016 kind: BlockKind,
2017 ) -> Result<(), BlockGraphError> {
2018 let position = position.into();
2019 if self.get_block(position).is_none() {
2020 return Err(BlockGraphError::BlockNotFound(position));
2021 }
2022 if let Some(region) = self.shown_branch_at(position)
2023 && (kind.is_dynamic() || kind.is_port())
2024 {
2025 return Err(InvalidActionError::InvalidBranchInterface {
2026 name: region.name.clone(),
2027 reason: format!("arms cannot contain {kind} blocks"),
2028 }
2029 .into());
2030 }
2031 let mut candidate = Block::new(position, kind);
2032 if kind.is_cube() {
2033 let height = self
2034 .get_block(position)
2035 .expect("block existence checked")
2036 .height();
2037 candidate = candidate
2038 .with_height(height)
2039 .expect("existing cube height is valid");
2040 }
2041 self.can_place_block_except(&candidate, Some(position))?;
2042 self.check_preserved_endpoints(&candidate)?;
2043 let existing = self
2044 .get_block(position)
2045 .expect("block existence checked")
2046 .clone();
2047 self.unindex_reserved_positions(&existing);
2048 let block = self
2049 .get_block_mut(position)
2050 .ok_or(BlockGraphError::BlockNotFound(position))?;
2051 block.set_kind(kind);
2052 let block = block.clone();
2053 self.index_reserved_positions(&block);
2054 self.sync_shown_branch_arm_data();
2055 self.refresh_action_graph_after_mutation();
2056 Ok(())
2057 }
2058
2059 pub fn set_block_tag(
2067 &mut self,
2068 position: impl Into<IVec3>,
2069 tag: impl Into<String>,
2070 ) -> Result<(), BlockGraphError> {
2071 let position = position.into();
2072 let block = self
2073 .get_block_mut(position)
2074 .ok_or(BlockGraphError::BlockNotFound(position))?;
2075 block.set_tag(tag)?;
2076 self.sync_shown_branch_arm_data();
2077 Ok(())
2078 }
2079
2080 pub fn set_port_color(
2089 &mut self,
2090 position: impl Into<IVec3>,
2091 rgb: [u8; 3],
2092 ) -> Result<(), BlockGraphError> {
2093 let position = position.into();
2094 self.get_block_mut(position)
2095 .ok_or(BlockGraphError::BlockNotFound(position))?
2096 .set_port_color_checked(rgb)?;
2097 Ok(())
2098 }
2099
2100 pub fn set_port_role(
2106 &mut self,
2107 position: impl Into<IVec3>,
2108 role: crate::PortRole,
2109 ) -> Result<(), BlockGraphError> {
2110 let position = position.into();
2111 self.get_block_mut(position)
2112 .ok_or(BlockGraphError::BlockNotFound(position))?
2113 .set_port_role_checked(role)?;
2114 Ok(())
2115 }
2116
2117 pub fn add_block(&mut self, block: Block) -> IVec3 {
2127 self.try_add_block(block)
2128 .unwrap_or_else(|err| panic!("Failed to add block: {err}"))
2129 }
2130
2131 pub fn try_add_block(&mut self, block: Block) -> Result<IVec3, BlockGraphError> {
2140 self.try_add_block_with_branch_occupancy(block, None)
2141 }
2142
2143 pub(crate) fn try_add_block_with_branch_occupancy(
2144 &mut self,
2145 block: Block,
2146 branch_occupancy: Option<&LinkBranchOccupancy>,
2147 ) -> Result<IVec3, BlockGraphError> {
2148 let pos = block.pos;
2149 if self.block_id.contains_key(&pos) {
2150 return Err(BlockGraphError::BlockExists(pos));
2151 }
2152 if let Some(index) = branch_occupancy {
2153 self.can_place_block_for_link(&block, index)?;
2154 } else {
2155 self.can_place_block(&block)?;
2156 }
2157 let indexed = block.clone();
2158 let index = self.inner.add_node(block);
2159 self.block_id.insert(pos, index.index() as u32);
2160 self.index_reserved_positions(&indexed);
2161 self.refresh_action_graph_after_mutation();
2162 Ok(pos)
2163 }
2164
2165 fn can_place_block_for_link(
2166 &self,
2167 block: &Block,
2168 branch_occupancy: &LinkBranchOccupancy,
2169 ) -> Result<(), BlockGraphError> {
2170 for occupied in block.checked_reserved_positions()? {
2171 self.check_visible_occupancy(block, occupied, None)?;
2172 for &(existing_pos, existing_kind) in branch_occupancy
2173 .by_position
2174 .get(&occupied)
2175 .into_iter()
2176 .flatten()
2177 {
2178 if !block.kind.allows_reserved_overlap(
2179 block.pos,
2180 existing_kind,
2181 existing_pos,
2182 occupied,
2183 ) {
2184 return Err(BlockGraphError::BlockPositionOccupied(occupied));
2185 }
2186 }
2187 }
2188 Ok(())
2189 }
2190
2191 pub fn remove_block(&mut self, pos: impl Into<IVec3>) -> Option<Block> {
2196 let id = self.block_id.remove(&pos.into())?;
2197 let block = self.inner.remove_node(id.into())?;
2198 self.unindex_reserved_positions(&block);
2199 self.sync_shown_branch_arm_data();
2200 self.refresh_action_graph_after_mutation();
2201 Some(block)
2202 }
2203
2204 pub fn add_pipe(&mut self, pipe: Pipe) {
2214 self.try_add_pipe(pipe)
2215 .unwrap_or_else(|err| panic!("Failed to add pipe: {err}"));
2216 }
2217
2218 pub fn try_add_pipe(&mut self, pipe: Pipe) -> Result<(), BlockGraphError> {
2227 self.try_add_pipe_with_branch_candidates(pipe, None)
2228 }
2229
2230 pub(crate) fn try_add_pipe_with_branch_candidates(
2233 &mut self,
2234 pipe: Pipe,
2235 branch_candidates: Option<&[usize]>,
2236 ) -> Result<(), BlockGraphError> {
2237 let (u, v) = pipe.try_endpoints()?;
2238 let u_index = self.try_get_endpoint_id(u)?;
2239 let v_index = self.try_get_endpoint_id(v)?;
2240 if self.has_pipe_between(u, v) {
2241 return Err(BlockGraphError::PipeExists(u, v));
2242 }
2243 if let Some(indices) = branch_candidates {
2244 self.record_added_branch_pipe_for(&pipe, indices.iter().copied());
2245 } else {
2246 self.record_added_branch_pipe(&pipe);
2247 }
2248 self.inner.add_edge(u_index, v_index, pipe);
2249 self.refresh_action_graph_after_mutation();
2250 Ok(())
2251 }
2252
2253 fn find_pipe_edge(&self, u: IVec3, v: IVec3) -> Option<EdgeIndex> {
2255 let u_index = self.try_get_endpoint_id(u).ok()?;
2256 let v_index = self.try_get_endpoint_id(v).ok()?;
2257 self.inner
2258 .edges_connecting(u_index, v_index)
2259 .find_map(|edge| {
2260 let pipe = edge.weight();
2261 let endpoints_match =
2262 (pipe.src() == u && pipe.dst() == v) || (pipe.src() == v && pipe.dst() == u);
2263 endpoints_match.then_some(edge.id())
2264 })
2265 }
2266
2267 pub fn remove_pipe(&mut self, u: impl Into<IVec3>, v: impl Into<IVec3>) -> Option<Pipe> {
2269 let removed = self
2270 .find_pipe_edge(u.into(), v.into())
2271 .and_then(|edge_index| self.inner.remove_edge(edge_index));
2272 if removed.is_some() {
2273 self.sync_shown_branch_arm_data();
2274 self.refresh_action_graph_after_mutation();
2275 }
2276 removed
2277 }
2278
2279 pub fn get_pipe(&self, u: impl Into<IVec3>, v: impl Into<IVec3>) -> Option<&Pipe> {
2281 let edge_index = self.find_pipe_edge(u.into(), v.into())?;
2282 self.inner.edge_weight(edge_index)
2283 }
2284
2285 pub fn get_pipe_between_blocks(
2291 &self,
2292 u: impl Into<IVec3>,
2293 v: impl Into<IVec3>,
2294 ) -> Option<&Pipe> {
2295 let u_index = self.try_get_id(u).ok()?;
2296 let v_index = self.try_get_id(v).ok()?;
2297 self.inner
2298 .edges_connecting(u_index, v_index)
2299 .min_by_key(EdgeRef::id)
2300 .map(|edge| edge.weight())
2301 }
2302
2303 pub(crate) fn get_pipe_mut(
2304 &mut self,
2305 u: impl Into<IVec3>,
2306 v: impl Into<IVec3>,
2307 ) -> Option<&mut Pipe> {
2308 let edge_index = self.find_pipe_edge(u.into(), v.into())?;
2309 self.inner.edge_weight_mut(edge_index)
2310 }
2311
2312 pub fn set_pipe_tag(
2320 &mut self,
2321 u: impl Into<IVec3>,
2322 v: impl Into<IVec3>,
2323 tag: impl Into<String>,
2324 ) -> Result<(), BlockGraphError> {
2325 let u = u.into();
2326 let v = v.into();
2327 let pipe = self
2328 .get_pipe_mut(u, v)
2329 .ok_or(BlockGraphError::PipeNotFound(u, v))?;
2330 pipe.set_tag(tag)?;
2331 self.sync_shown_branch_arm_data();
2332 Ok(())
2333 }
2334
2335 pub fn set_pipe_hadamard(
2341 &mut self,
2342 u: impl Into<IVec3>,
2343 v: impl Into<IVec3>,
2344 hadamard: bool,
2345 ) -> Result<(), BlockGraphError> {
2346 let u = u.into();
2347 let v = v.into();
2348 let key = {
2349 let pipe = self
2350 .get_pipe_mut(u, v)
2351 .ok_or(BlockGraphError::PipeNotFound(u, v))?;
2352 pipe.hadamard = hadamard;
2353 crate::branch::pipe_key(pipe)
2354 };
2355 for region in &mut self.branches {
2356 region.set_cut_hadamard(key, hadamard);
2357 }
2358 self.sync_shown_branch_arm_data();
2359 self.refresh_action_graph_after_mutation();
2360 Ok(())
2361 }
2362
2363 pub fn neighbors(&self, position: impl Into<IVec3>) -> Vec<&Block> {
2365 if let Ok(index) = self.try_get_endpoint_id(position.into()) {
2366 return self
2367 .inner
2368 .neighbors(index)
2369 .filter_map(|neighbor_index| self.inner.node_weight(neighbor_index))
2370 .collect();
2371 }
2372 vec![]
2373 }
2374
2375 pub fn neighbor_positions(&self, position: impl Into<IVec3>) -> Vec<IVec3> {
2378 let position = position.into();
2379 let Ok(index) = self.try_get_endpoint_id(position) else {
2380 return Vec::new();
2381 };
2382 self.inner
2383 .edges(index)
2384 .filter_map(|edge| {
2385 let pipe = edge.weight();
2386 let dst = pipe.dst();
2387 if pipe.src == position {
2388 Some(dst)
2389 } else if dst == position {
2390 Some(pipe.src)
2391 } else {
2392 None
2393 }
2394 })
2395 .collect()
2396 }
2397
2398 pub fn degree(&self, position: impl Into<IVec3>) -> usize {
2400 self.neighbor_positions(position).len()
2401 }
2402
2403 pub fn block_pairs_with_pipe(&self) -> impl Iterator<Item = (&Block, &Block, &Pipe)> {
2405 self.inner.edge_indices().filter_map(|edge_index| {
2406 let (u_index, v_index) = self.inner.edge_endpoints(edge_index)?;
2407 let u = self.inner.node_weight(u_index)?;
2408 let v = self.inner.node_weight(v_index)?;
2409 let pipe = self.inner.edge_weight(edge_index)?;
2410 Some((u, v, pipe))
2411 })
2412 }
2413
2414 pub fn pipe_endpoints_with_blocks(
2416 &self,
2417 ) -> impl Iterator<Item = (IVec3, IVec3, &Block, &Block, &Pipe)> {
2418 self.block_pairs_with_pipe().filter_map(|(u, v, pipe)| {
2419 let src_block = self.get_endpoint_block(pipe.src)?;
2420 let dst = pipe.dst();
2421 let dst_block = self.get_endpoint_block(dst)?;
2422
2423 if src_block.pos == u.pos && dst_block.pos == v.pos {
2424 Some((pipe.src, dst, u, v, pipe))
2425 } else if src_block.pos == v.pos && dst_block.pos == u.pos {
2426 Some((pipe.src, dst, v, u, pipe))
2427 } else {
2428 None
2429 }
2430 })
2431 }
2432
2433 pub fn validate(&self) -> Result<(), BlockGraphError> {
2441 if self.has_module_structure() {
2442 self.validate_source()?;
2443 if !self.instances.is_empty() {
2444 return self
2445 .summarize_root(crate::ModuleCertificationLimits::DEFAULT)
2446 .map(drop)
2447 .map_err(|source| {
2448 BlockGraphError::ModuleSource(Arc::new(
2449 crate::ModuleError::InvalidGeometry {
2450 module: self.name.clone(),
2451 source,
2452 span: None,
2453 },
2454 ))
2455 });
2456 }
2457 return crate::validate::validate(&self.copy_local_geometry().fix_shadowed_faces());
2458 }
2459 crate::validate::validate(self)
2460 }
2461
2462 pub fn validate_structure(&self) -> Result<(), BlockGraphError> {
2468 crate::validate::validate_structure(self)
2469 }
2470
2471 pub fn validate_source(&self) -> Result<(), BlockGraphError> {
2478 if self.has_module_structure() {
2479 self.validate_with_limits(crate::ModuleCertificationLimits::DEFAULT)
2480 .map_err(|error| BlockGraphError::ModuleSource(Arc::new(error)))?;
2481 for definition in self.modules() {
2482 let local = definition.copy_local_geometry().fix_shadowed_faces();
2483 if definition.instances.is_empty() {
2484 crate::validate::validate_source(&local)?;
2485 } else {
2486 let mut dag = local.build_action_graph(&local.actions())?;
2489 dag.validate(Some(&local))?;
2490 }
2491 }
2492 return Ok(());
2493 }
2494 crate::validate::validate_source(self).map(|_| ())
2495 }
2496
2497 pub fn structural_errors(&self) -> Vec<BlockGraphError> {
2506 crate::validate::structural_errors(self)
2507 }
2508}
2509
2510#[cfg(feature = "gltf")]
2511impl BlockGraph {
2512 fn build_export_gltf(
2514 &self,
2515 pipe_length: f32,
2516 stabilizer: Option<&StabilizerGenerator>,
2517 popped_faces: &[GltfFaceSelector],
2518 ) -> Result<GltfData, BlockGraphError> {
2519 self.require_flat_hierarchy("glTF export")?;
2520 let mut data = block_graph_as_gltf_data_with_popped_faces(self, pipe_length, popped_faces);
2521 if !popped_faces.is_empty() {
2522 data = emphasize_popped_faces(data);
2523 }
2524 let editor_world = |point: glam::Vec3| glam::vec3(point.x, point.z, -point.y);
2525 data = data.map_points(editor_world);
2526 if let Some(stabilizer) = stabilizer {
2527 if popped_faces.is_empty() {
2528 data = data.scale_face_opacity(OUTER_FACE_OPACITY_SCALE);
2529 }
2530 let stabilizer_data =
2531 stabilizer_as_gltf_data(stabilizer, self, pipe_length)?.map_points(editor_world);
2532 data.extend(stabilizer_data);
2533 }
2534 Ok(data)
2535 }
2536
2537 fn build_module_export_gltf(
2538 &self,
2539 pipe_length: f32,
2540 popped_faces: &[GltfFaceSelector],
2541 ) -> Result<(GltfData, Option<crate::ModuleView>), BlockGraphError> {
2542 let flat = self
2543 .flatten()
2544 .map_err(|error| BlockGraphError::ModuleMaterialization(Arc::new(error)))?;
2545 let modules = crate::ModuleView::from_graph(self, &flat);
2546 let data = block_graph_as_gltf_data_with_modules(
2547 &flat,
2548 pipe_length,
2549 popped_faces,
2550 modules.as_ref(),
2551 )
2552 .map_points(|point| glam::vec3(point.x, point.z, -point.y));
2553 Ok((data, modules))
2554 }
2555
2556 pub fn write_module_gltf_file(
2565 &self,
2566 pipe_length: f32,
2567 path: impl AsRef<std::path::Path>,
2568 popped_faces: &[GltfFaceSelector],
2569 ) -> Result<(), BlockGraphError> {
2570 self.build_module_export_gltf(pipe_length, popped_faces)?
2571 .0
2572 .to_file(path)
2573 }
2574
2575 pub fn write_module_html_viewer(
2584 &self,
2585 pipe_length: f32,
2586 path: impl AsRef<std::path::Path>,
2587 popped_faces: &[GltfFaceSelector],
2588 ) -> Result<(), BlockGraphError> {
2589 let flat = self
2590 .flatten()
2591 .map_err(|error| BlockGraphError::ModuleMaterialization(Arc::new(error)))?;
2592 let modules = crate::ModuleView::from_graph(self, &flat);
2593 let (data, ranges) =
2594 module_html_geometry(&flat, pipe_length, popped_faces, modules.as_ref());
2595 let data = data.map_points(|point| glam::vec3(point.x, point.z, -point.y));
2596 let mut legend = String::new();
2597 if let Some(modules) = modules {
2598 legend.push_str("<details class=\"module-legend\" open><summary>Modules</summary><ul>");
2599 for (index, module) in modules.modules().iter().enumerate() {
2600 let color = crate::module_color(index);
2601 let name = module
2602 .name
2603 .replace('&', "&")
2604 .replace('<', "<")
2605 .replace('>', ">")
2606 .replace('"', """);
2607 let instance_suffix = if module.instance_count == 1 { "" } else { "s" };
2608 let block_suffix = if module.block_count == 1 { "" } else { "s" };
2609 legend.push_str(&format!("<li><label><input type=\"checkbox\" data-module=\"{index}\" checked disabled aria-label=\"Highlight {name}\"><i style=\"background:#{:02x}{:02x}{:02x}\"></i>{name}<br>{} instance{instance_suffix} · {} block{block_suffix}</label></li>",
2610 color.r, color.g, color.b, module.instance_count, module.block_count));
2611 }
2612 legend.push_str("</ul></details>");
2613 }
2614 std::fs::write(&path, data.to_module_html_str(&legend, &ranges)?).map_err(|source| {
2615 BlockGraphError::Io {
2616 path: path.as_ref().to_path_buf(),
2617 source: Arc::new(source),
2618 }
2619 })
2620 }
2621
2622 pub fn write_to_gltf_file(
2630 &self,
2631 pipe_length: f32,
2632 path: impl AsRef<std::path::Path>,
2633 stabilizer: Option<&StabilizerGenerator>,
2634 popped_faces: &[GltfFaceSelector],
2635 ) -> Result<(), BlockGraphError> {
2636 self.build_export_gltf(pipe_length, stabilizer, popped_faces)?
2637 .to_file(path)
2638 }
2639
2640 pub fn write_to_gltf_html_viewer(
2649 &self,
2650 pipe_length: f32,
2651 path: impl AsRef<std::path::Path>,
2652 stabilizer: Option<&StabilizerGenerator>,
2653 popped_faces: &[GltfFaceSelector],
2654 ) -> Result<(), BlockGraphError> {
2655 let gltf_data = self.build_export_gltf(pipe_length, stabilizer, popped_faces)?;
2656 let html_str = gltf_data.to_html_str()?;
2657 std::fs::write(&path, html_str).map_err(|e| BlockGraphError::Io {
2658 path: path.as_ref().to_path_buf(),
2659 source: Arc::new(e),
2660 })
2661 }
2662}
2663
2664impl BlockGraph {
2665 pub fn to_program_blog_text(&self) -> Result<String, crate::ModuleError> {
2675 if self.has_module_structure() {
2676 return Ok(self.to_blog_text());
2677 }
2678 self.with_local_export_names()
2679 .with_inferred_interface()
2680 .map(|program| program.to_blog_text())
2681 }
2682
2683 fn with_local_export_names(&self) -> Self {
2684 fn rename_expr(expr: &mut crate::Expr, names: &HashMap<String, String>) {
2685 match expr {
2686 crate::Expr::Var(name) => {
2687 if let Some(local) = names.get(name) {
2688 *name = local.clone();
2689 }
2690 }
2691 crate::Expr::Not(inner) => rename_expr(inner, names),
2692 crate::Expr::Binary(_, left, right) => {
2693 rename_expr(left, names);
2694 rename_expr(right, names);
2695 }
2696 }
2697 }
2698
2699 let mut actions = self.actions();
2700 let names = self
2701 .action_inputs
2702 .iter()
2703 .chain(actions.iter().filter_map(|action| match action {
2704 Action::Let { name, .. } | Action::Measure { name, .. } => Some(name),
2705 _ => None,
2706 }))
2707 .chain(self.branches.iter().map(|branch| &branch.name))
2708 .cloned()
2709 .collect::<BTreeSet<_>>();
2710 let mut next = 0;
2711 let replacements = names
2712 .iter()
2713 .filter(|name| name.contains("__") || !crate::parser::is_valid_simple_identifier(name))
2714 .map(|name| {
2715 let local = loop {
2716 let candidate = format!("flat{next}");
2717 next += 1;
2718 if !names.contains(&candidate) {
2719 break candidate;
2720 }
2721 };
2722 (name.clone(), local)
2723 })
2724 .collect::<HashMap<_, _>>();
2725 let rename = |name: &String| replacements.get(name).unwrap_or(name).clone();
2726 let mut graph = self.clone();
2727 graph.action_inputs = self.action_inputs.iter().map(rename).collect();
2728 for branch in &mut graph.branches {
2729 branch.name = rename(&branch.name);
2730 }
2731 for action in &mut actions {
2732 let expr = match action {
2733 Action::Let { name, expr } => {
2734 *name = rename(name);
2735 Some(expr)
2736 }
2737 Action::Measure { name, .. } => {
2738 *name = rename(name);
2739 None
2740 }
2741 Action::DiscardIf(expr) => Some(expr),
2742 Action::Resolve { condition, .. } | Action::Branch { condition, .. } => {
2743 Some(condition)
2744 }
2745 Action::Feedback { condition, .. } => condition.as_mut(),
2746 };
2747 if let Some(expr) = expr {
2748 rename_expr(expr, &replacements);
2749 }
2750 }
2751 graph.rebuild_action_graph_lenient(actions);
2752 graph
2753 }
2754
2755 pub fn to_blog_body_text(&self) -> String {
2761 use std::fmt::Write as _;
2762
2763 if !self.branches.is_empty() {
2764 return self.to_blog_text_with_branches();
2765 }
2766
2767 let mut blog = String::from("BLOG 1.0\n\n");
2768
2769 let sorted_id_pos = self.blog_block_ids();
2770
2771 let has_data = !sorted_id_pos.is_empty() || self.pipes().next().is_some();
2772 if has_data {
2773 for (pos, id) in sorted_id_pos {
2774 let block = self.get_block(pos).expect("block exists in block_id");
2775 writeln!(blog, "{id}: {block}").expect("writing to String cannot fail");
2776 }
2777
2778 let mut sorted_pipes: Vec<Pipe> = self.pipes().cloned().collect();
2779 sorted_pipes.sort_by_key(|pipe| {
2780 let src_id = self
2781 .get_endpoint_block_id(pipe.src)
2782 .expect("pipe src has block_id");
2783 let dst_pos = pipe.src + pipe.dir.to_ivec3();
2784 let dst_id = self
2785 .get_endpoint_block_id(dst_pos)
2786 .expect("pipe dst has block_id");
2787 (src_id, dst_id)
2788 });
2789 for pipe in sorted_pipes {
2790 writeln!(
2791 blog,
2792 "{}",
2793 pipe.display_with_ids(|position| self.block_id.get(&position).copied())
2794 )
2795 .expect("writing to String cannot fail");
2796 }
2797 }
2798
2799 if self.has_actions() {
2800 if has_data {
2801 blog.push('\n');
2802 }
2803 for node in self.action_graph.ordered_nodes() {
2804 let action_str = node
2805 .action
2806 .display_with_ids(|position| self.block_id.get(&position).copied());
2807 writeln!(blog, "{action_str}").expect("writing to String cannot fail");
2808 }
2809 }
2810 blog
2811 }
2812
2813 fn to_blog_text_with_branches(&self) -> String {
2814 use std::fmt::Write as _;
2815
2816 let mut blog = String::from("BLOG 1.0\n\n");
2817 let shown_pipes = self
2818 .branches
2819 .iter()
2820 .flat_map(|region| region.shown_arm().pipes())
2821 .map(crate::branch::pipe_key)
2822 .collect::<HashSet<_>>();
2823 let shared_pipes = self
2824 .branches
2825 .iter()
2826 .flat_map(|region| {
2827 region
2828 .on_false()
2829 .pipes()
2830 .filter(|pipe| region.on_true().pipes().any(|candidate| candidate == *pipe))
2831 })
2832 .cloned()
2833 .collect::<HashSet<_>>();
2834
2835 let common = self.blog_block_ids();
2836 let mut action_ids = HashMap::new();
2837 let mut next_id = u32::try_from(common.len()).expect("BlockGraph ids fit in u32");
2838 for (position, id) in common {
2839 let block = self
2840 .get_block(position)
2841 .expect("common block exists in block_id");
2842 writeln!(blog, "{id}: {block}").expect("writing to String cannot fail");
2843 action_ids.insert(position, id);
2844 }
2845
2846 let mut common_pipes = self
2847 .pipes()
2848 .filter(|pipe| !shown_pipes.contains(&crate::branch::pipe_key(pipe)))
2849 .cloned()
2850 .collect::<Vec<_>>();
2851 common_pipes.extend(shared_pipes.iter().cloned());
2852 common_pipes.sort_by_key(|pipe| {
2853 let (src, dst) = pipe.endpoints();
2854 (src.to_array(), dst.to_array())
2855 });
2856 for pipe in common_pipes {
2857 writeln!(
2858 blog,
2859 "{}",
2860 pipe.display_with_ids(|position| action_ids.get(&position).copied())
2861 )
2862 .expect("writing to String cannot fail");
2863 }
2864
2865 for region in &self.branches {
2866 writeln!(blog, "branch {} {{", region.name).expect("writing to String cannot fail");
2867 for (label, arm) in [("false", region.on_false()), ("true", region.on_true())] {
2868 writeln!(blog, " {label} {{").expect("writing to String cannot fail");
2869 let mut arm_ids = HashMap::new();
2870 for block in arm.blocks() {
2871 writeln!(blog, " {next_id}: {block}")
2872 .expect("writing to String cannot fail");
2873 arm_ids.insert(block.pos(), next_id);
2874 next_id += 1;
2875 }
2876 for pipe in arm.pipes().filter(|pipe| !shared_pipes.contains(*pipe)) {
2877 writeln!(
2878 blog,
2879 " {}",
2880 pipe.display_with_ids(|position| {
2881 arm_ids
2882 .get(&position)
2883 .or_else(|| action_ids.get(&position))
2884 .copied()
2885 })
2886 )
2887 .expect("writing to String cannot fail");
2888 }
2889 writeln!(blog, " }}").expect("writing to String cannot fail");
2890 }
2891 writeln!(blog, "}}").expect("writing to String cannot fail");
2892 }
2893
2894 if self.has_actions() {
2895 blog.push('\n');
2896 for node in self.action_graph.ordered_nodes() {
2897 match &node.action {
2898 Action::Branch { target, condition } => {
2899 let name = &self
2900 .branch_by_target(*target)
2901 .expect("validated branch action has a named region")
2902 .name;
2903 writeln!(blog, "resolve {name} if {condition}")
2904 .expect("writing to String cannot fail");
2905 }
2906 action => {
2907 let action =
2908 action.display_with_ids(|position| action_ids.get(&position).copied());
2909 writeln!(blog, "{action}").expect("writing to String cannot fail");
2910 }
2911 }
2912 }
2913 }
2914 blog
2915 }
2916
2917 pub fn to_file(&self, path: impl AsRef<std::path::Path>) -> Result<(), BlockGraphError> {
2923 std::fs::write(&path, self.to_blog_text()).map_err(|e| BlockGraphError::Io {
2924 path: path.as_ref().to_path_buf(),
2925 source: Arc::new(e),
2926 })
2927 }
2928
2929 pub fn from_blog_text(blog_text: &str) -> Result<Self, BlockGraphError> {
2938 crate::parser::parse_blog_to_graph(blog_text)
2939 }
2940
2941 pub fn from_text(text: &str) -> Result<Self, BlockGraphError> {
2961 Self::from_text_with_limits(text, crate::ModuleCertificationLimits::DEFAULT)
2962 }
2963
2964 pub fn from_text_with_limits(
2972 text: &str,
2973 limits: crate::ModuleCertificationLimits,
2974 ) -> Result<Self, BlockGraphError> {
2975 crate::parser::parse_graph_input(text, limits)
2976 }
2977
2978 pub fn load(path: impl AsRef<std::path::Path>) -> Result<Self, BlockGraphError> {
2987 Self::load_with_limits(path, crate::ModuleCertificationLimits::DEFAULT)
2988 }
2989
2990 pub fn load_with_limits(
2998 path: impl AsRef<std::path::Path>,
2999 limits: crate::ModuleCertificationLimits,
3000 ) -> Result<Self, BlockGraphError> {
3001 crate::parser::load_graph_input(path.as_ref(), limits)
3002 }
3003
3004 #[must_use]
3006 pub fn fix_shadowed_faces(&self) -> Self {
3007 self.fix_shadowed_faces_with(&[])
3008 }
3009
3010 pub(crate) fn fix_shadowed_faces_with(&self, extra_pipes: &[(IVec3, UDirection)]) -> Self {
3011 let mut extras = HashMap::<IVec3, [usize; 3]>::new();
3012 for &(position, direction) in extra_pipes {
3013 extras.entry(position).or_default()[direction.index()] += 1;
3014 }
3015 let mut updates: Vec<(IVec3, CubeKind)> = Vec::new();
3016 for block in self.blocks() {
3017 let BlockKind::Cube(current_kind) = block.kind else {
3018 continue;
3019 };
3020 let mut counts = extras.get(&block.pos()).copied().unwrap_or_default();
3022 for edge in self.inner.edges(
3023 self.try_get_id(block.pos)
3024 .expect("block from self has valid id"),
3025 ) {
3026 counts[edge.weight().dir.as_udirection().index()] += 1;
3027 }
3028
3029 let new_kind = shadowed_cube_kind(current_kind, counts);
3030 if new_kind != current_kind {
3031 updates.push((block.pos, new_kind));
3032 }
3033 }
3034
3035 let mut new_graph = self.clone();
3036 let changed = !updates.is_empty();
3037 for (pos, kind) in updates {
3038 if let Some(block) = new_graph.get_block_mut(pos) {
3039 block.kind = BlockKind::Cube(kind);
3042 }
3043 }
3044 if changed {
3045 new_graph.sync_shown_branch_arm_data();
3046 new_graph.refresh_action_graph_after_mutation();
3047 }
3048 new_graph
3049 }
3050
3051 fn randomly_resolved_selective_kind(&self, block: &Block, rng: &mut impl Rng) -> BlockKind {
3052 let BlockKind::Selective(selective_kind) = block.kind else {
3053 return block.kind;
3054 };
3055 let chosen_basis = if rng.random::<bool>() {
3056 selective_kind.pauli_if_true()
3057 } else {
3058 selective_kind.pauli_if_false()
3059 };
3060 self.resolved_selective_kind_for_basis(chosen_basis)
3061 }
3062
3063 fn resolved_selective_kind_for_basis(&self, chosen_basis: PauliBasis) -> BlockKind {
3064 match chosen_basis {
3065 PauliBasis::X => BlockKind::Measurement(Basis::X),
3066 PauliBasis::Y => BlockKind::Y,
3067 PauliBasis::Z => BlockKind::Measurement(Basis::Z),
3068 }
3069 }
3070}
3071
3072fn named_measurement_rows(stabilizers: &StabilizerGenerators) -> BTreeMap<&str, &Stabilizer> {
3073 stabilizers
3074 .generators
3075 .iter()
3076 .filter_map(|generator| match &generator.kind {
3077 StabilizerRowKind::Measurement { name } => Some((name.as_str(), &generator.stabilizer)),
3078 _ => None,
3079 })
3080 .collect()
3081}
3082
3083fn validate_shared_measurement_rows(
3084 baseline: &StabilizerGenerators,
3085 projected: &StabilizerGenerators,
3086) -> Result<(), BlockGraphError> {
3087 let baseline_fixings = crate::selective_fixings(&baseline.generators);
3088 let projected_fixings = crate::selective_fixings(&projected.generators);
3089 let projected_rows = named_measurement_rows(projected);
3090 for (name, row) in named_measurement_rows(baseline) {
3091 let shared = projected_rows.get(name).is_some_and(|projected| {
3092 positioned_stabilizers_equal(row, projected)
3093 && shared_measurement_fixings(row, &baseline_fixings, &projected_fixings)
3094 });
3095 if !shared {
3096 return Err(InvalidActionError::BranchDependentMeasurementSurface {
3097 name: name.to_owned(),
3098 }
3099 .into());
3100 }
3101 }
3102 Ok(())
3103}
3104
3105fn positioned_stabilizers_equal(left: &Stabilizer, right: &Stabilizer) -> bool {
3106 left.sign == right.sign
3107 && left.port_stabilizer == right.port_stabilizer
3108 && left.interior_nodes == right.interior_nodes
3109 && left.interior_edges == right.interior_edges
3110}
3111
3112fn shared_measurement_fixings(
3113 row: &Stabilizer,
3114 baseline: &crate::SelectiveFixings<'_>,
3115 projected: &crate::SelectiveFixings<'_>,
3116) -> bool {
3117 let mut sites = row
3118 .interior_nodes
3119 .iter()
3120 .filter_map(|(&pos, &pauli)| {
3121 (pauli != crate::Pauli::I && baseline.contains_key(&pos)).then_some(pos)
3122 })
3123 .collect::<Vec<_>>();
3124 let mut seen = sites.iter().copied().collect::<HashSet<_>>();
3125 let mut scan = 0;
3126 while scan < sites.len() {
3127 let site = sites[scan];
3128 scan += 1;
3129 let base = baseline[&site];
3130 let Some(projected) = projected.get(&site) else {
3131 return false;
3132 };
3133 if base.forbidden != projected.forbidden
3134 || !positioned_stabilizers_equal(base.row, projected.row)
3135 {
3136 return false;
3137 }
3138 for (&pos, &pauli) in &base.row.interior_nodes {
3139 if pauli != crate::Pauli::I && baseline.contains_key(&pos) && seen.insert(pos) {
3140 sites.push(pos);
3141 }
3142 }
3143 }
3144 true
3145}
3146
3147fn positioned_stabilizer_touches_region(row: &Stabilizer, region: &crate::BranchRegion) -> bool {
3148 row.interior_nodes
3149 .iter()
3150 .any(|(&position, &pauli)| pauli != crate::Pauli::I && region.contains_block(position))
3151 || row.interior_edges.iter().any(|(&(first, second), &pauli)| {
3152 pauli != crate::Pauli::I
3153 && (region.contains_block(first) || region.contains_block(second))
3154 })
3155}
3156
3157impl std::fmt::Display for BlockGraph {
3158 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3159 f.write_str(&self.to_blog_text())
3160 }
3161}
3162
3163impl FromStr for BlockGraph {
3164 type Err = BlockGraphError;
3165
3166 fn from_str(s: &str) -> Result<Self, Self::Err> {
3167 Self::from_text(s)
3168 }
3169}
3170
3171pub(crate) fn flip_pipe_xz_basis(pipe: &mut Pipe, is_fixed_resource: impl Fn(IVec3) -> bool) {
3173 pipe.hadamard ^= is_fixed_resource(pipe.src()) ^ is_fixed_resource(pipe.dst());
3174}
3175
3176pub(crate) fn validate_block_orientation<'a>(
3177 blocks: impl IntoIterator<Item = &'a Block>,
3178 orientation: crate::ModuleOrientation,
3179) -> Result<(), BlockGraphError> {
3180 let (mut has_dynamic, mut has_walking_or_patch, mut has_custom_height, mut has_y) =
3181 (false, false, false, false);
3182 for block in blocks {
3183 has_dynamic |= block.kind.is_t() || block.kind.is_selective();
3184 has_walking_or_patch |= block.kind.is_walking() || block.kind.is_patch_rotation();
3185 has_custom_height |= !block.height().is_default();
3189 has_y |= block.kind.is_y();
3190 }
3191 if !orientation.preserves_time_direction() {
3192 if has_dynamic {
3193 return Err(BlockGraphError::RotationUnsupportedDynamicBlocks);
3194 }
3195 if has_walking_or_patch {
3196 return Err(BlockGraphError::RotationWalkingRequiresTimeAxis);
3197 }
3198 if has_custom_height {
3199 return Err(BlockGraphError::RotationCubeHeightRequiresTimeAxis);
3200 }
3201 }
3202 if has_y && !orientation.is_coordinate_half_turn() {
3203 return Err(BlockGraphError::RotationRequiresHalfTurns);
3204 }
3205 Ok(())
3206}
3207
3208pub(crate) fn shadowed_cube_kind(current_kind: CubeKind, counts: [usize; 3]) -> CubeKind {
3211 let mut new_kind = current_kind;
3212 match counts.iter().filter(|&&c| c > 0).count() {
3213 1 if current_kind.is_spatial() => {
3214 let active_dim = counts
3215 .iter()
3216 .position(|&c| c > 0)
3217 .expect("exactly one active dimension in this match arm");
3218 if active_dim != 2 && counts[active_dim] == 2 {
3220 let mut bases = new_kind.bases();
3221 bases[active_dim] = bases[active_dim].flip();
3222 new_kind =
3223 CubeKind::try_from(bases).expect("flipped basis produces valid CubeKind");
3224 }
3225 }
3226 2 => {
3227 let mut active_dims = (0..3).filter(|&i| counts[i] > 0).take(2);
3228 let dim1 = active_dims
3229 .next()
3230 .expect("two active dims in this match arm");
3231 let dim2 = active_dims
3232 .next()
3233 .expect("two active dims in this match arm");
3234 let mut bases = current_kind.bases();
3236 let normal_dim = counts
3237 .iter()
3238 .position(|c| *c == 0)
3239 .expect("one inactive dim in two-active match arm");
3240 let normal_basis = bases[normal_dim];
3241 let side_basis = normal_basis.flip();
3242 bases[dim1] = side_basis;
3243 bases[dim2] = side_basis;
3244 new_kind = CubeKind::try_from(bases).expect("valid bases produce valid CubeKind");
3245 }
3246 _ => {}
3247 }
3248 new_kind
3249}
3250
3251pub(crate) fn oriented_block(
3252 block: &Block,
3253 orientation: crate::ModuleOrientation,
3254) -> Result<Block, BlockGraphError> {
3255 let mut rotated = block.clone();
3256 rotated.pos = orientation.try_rotate_position(block.pos)?;
3257 rotated.kind = oriented_block_kind(block.kind, orientation);
3258 for offset in rotated.reserved_offsets() {
3259 crate::checked_add_position(rotated.pos, offset)?;
3260 }
3261 Ok(rotated)
3262}
3263
3264pub(crate) fn oriented_pipe(
3265 pipe: &Pipe,
3266 orientation: crate::ModuleOrientation,
3267) -> Result<Pipe, BlockGraphError> {
3268 let src = orientation.try_rotate_position(pipe.src)?;
3269 let dir = orientation.rotate_direction(pipe.dir);
3270 crate::checked_add_position(src, dir.to_ivec3())?;
3271 Ok(Pipe {
3272 src,
3273 dir,
3274 hadamard: pipe.hadamard,
3275 tag: pipe.tag.clone(),
3276 })
3277}
3278
3279fn oriented_block_kind(kind: BlockKind, orientation: crate::ModuleOrientation) -> BlockKind {
3280 match kind {
3281 BlockKind::Cube(kind) => BlockKind::Cube(
3282 CubeKind::try_from(orientation.rotate_axis_values(kind.bases()))
3283 .expect("a cubic orientation permutes valid cube bases"),
3284 ),
3285 BlockKind::Walking(kind) => {
3286 let movement = orientation
3287 .try_rotate_position(kind.movement_3d())
3288 .expect("small walking movement remains representable");
3289 let boundary =
3290 CubeKind::try_from(orientation.rotate_axis_values(kind.boundary().bases()))
3291 .and_then(WalkingBoundaryKind::try_from)
3292 .expect("a time-preserving orientation keeps walking boundaries temporal");
3293 BlockKind::Walking(
3294 WalkingKind::new(boundary, movement.truncate())
3295 .expect("a time-preserving orientation keeps walking movement valid"),
3296 )
3297 }
3298 BlockKind::PatchRotation(kind) => {
3299 let movement = orientation
3300 .try_rotate_position(kind.movement_3d())
3301 .expect("small patch rotation movement remains representable");
3302 let basis = if orientation
3303 .rotate_direction(Direction::XPLUS)
3304 .as_udirection()
3305 == UDirection::X
3306 {
3307 kind.basis()
3308 } else {
3309 kind.basis().flip()
3310 };
3311 BlockKind::PatchRotation(
3312 PatchRotationKind::new(basis, movement.truncate())
3313 .expect("a time-preserving orientation keeps patch movement valid"),
3314 )
3315 }
3316 _ => kind,
3317 }
3318}
3319
3320fn normalize_z_position(position: IVec3, min_z: i32, max_z: i32) -> Result<IVec3, BlockGraphError> {
3321 let z = i64::from(position.z) - i64::from(min_z);
3322 let z = i32::try_from(z)
3323 .map_err(|_| BlockGraphError::CoordinateNormalizationOverflow { min_z, max_z })?;
3324 Ok(IVec3::new(position.x, position.y, z))
3325}
3326
3327fn normalize_action_z(action: &Action, min_z: i32, max_z: i32) -> Result<Action, BlockGraphError> {
3328 let normalize = |position| normalize_z_position(position, min_z, max_z);
3329 Ok(match action {
3330 Action::Measure { target, name } => Action::Measure {
3331 target: match target {
3332 MeasureTarget::Node(position) => MeasureTarget::Node(normalize(*position)?),
3333 MeasureTarget::Edge { src, dir } => {
3334 let src = normalize(*src)?;
3335 crate::checked_add_position(src, dir.to_ivec3())?;
3336 MeasureTarget::Edge { src, dir: *dir }
3337 }
3338 },
3339 name: name.clone(),
3340 },
3341 Action::Resolve { target, condition } => Action::Resolve {
3342 target: normalize(*target)?,
3343 condition: condition.clone(),
3344 },
3345 Action::Branch { target, condition } => Action::Branch {
3346 target: normalize(*target)?,
3347 condition: condition.clone(),
3348 },
3349 Action::Feedback { targets, condition } => Action::Feedback {
3350 targets: targets
3351 .iter()
3352 .map(|target| {
3353 Ok(crate::FeedbackTarget {
3354 pauli: target.pauli,
3355 target: normalize(target.target)?,
3356 direction: target.direction,
3357 })
3358 })
3359 .collect::<Result<_, BlockGraphError>>()?,
3360 condition: condition.clone(),
3361 },
3362 Action::Let { .. } | Action::DiscardIf(..) => action.clone(),
3363 })
3364}
3365
3366#[cfg(test)]
3367mod tests {
3368 use super::*;
3369 use crate::{BranchArm, Expr, GalleryItem, SelectiveKind, StabilizerError};
3370 use bloq_utils::Pauli;
3371
3372 #[test]
3373 fn cube_height_edits_preserve_connected_endpoints() {
3374 let mut graph = BlockGraph::new();
3375 graph.add_block(
3376 Block::new(IVec3::ZERO, BlockKind::Cube(CubeKind::ZXZ))
3377 .with_height("2d".parse().unwrap())
3378 .unwrap(),
3379 );
3380 graph.add_block(Block::new(2 * IVec3::Z, BlockKind::Cube(CubeKind::ZXZ)));
3381 graph.add_pipe(Pipe::new(IVec3::Z, Direction::ZPLUS));
3382 graph.validate().unwrap();
3383 let before = graph.to_blog_text();
3384 assert!(matches!(
3385 graph.set_cube_height(IVec3::ZERO, CubeHeight::DEFAULT),
3386 Err(BlockGraphError::BlockNotFound(endpoint)) if endpoint == IVec3::Z
3387 ));
3388 assert_eq!(graph.to_blog_text(), before);
3389 graph.to_zx_graph().unwrap();
3390
3391 let existing = graph.get_block(IVec3::ZERO).unwrap().clone();
3394 graph.unindex_reserved_positions(&existing);
3395 let short = existing.with_height(CubeHeight::DEFAULT).unwrap();
3396 graph.index_reserved_positions(&short);
3397 *graph.get_block_mut(IVec3::ZERO).unwrap() = short;
3398 assert!(matches!(
3399 graph.validate_structure(),
3400 Err(BlockGraphError::BlockNotFound(endpoint)) if endpoint == IVec3::Z
3401 ));
3402 graph.to_zx_graph().unwrap_err();
3403 }
3404
3405 #[test]
3406 fn executable_flat_exports_round_trip_linked_symbols_without_collisions() {
3407 for source in [
3408 GalleryItem::PhaseGradientK4.build(),
3409 BlockGraph::from_text(bloq_test::ONE_BIT_ADDER_SOURCE).unwrap(),
3410 ] {
3411 let graph = source
3412 .materialize_root_graph()
3413 .expect("gallery flat projection");
3414 let original_actions = graph.actions();
3415 let text = graph.to_program_blog_text().unwrap();
3416 let reparsed = crate::parse_inline_graph(&text)
3417 .unwrap()
3418 .materialize_flat_graph()
3419 .unwrap();
3420 assert_eq!(reparsed.block_count(), graph.block_count());
3421 assert_eq!(reparsed.actions().len(), graph.actions().len());
3422 assert_eq!(reparsed.to_program_blog_text().unwrap(), text);
3423 assert_eq!(graph.actions(), original_actions);
3424 }
3425
3426 let mut graph = BlockGraph::new();
3427 graph
3428 .set_actions_with_inputs(
3429 vec![
3430 Action::Let {
3431 name: "flat0".into(),
3432 expr: crate::Expr::Var("child__input".into()),
3433 },
3434 Action::Let {
3435 name: "child__value".into(),
3436 expr: crate::Expr::Not(Box::new(crate::Expr::Var("flat0".into()))),
3437 },
3438 Action::DiscardIf(crate::Expr::Var("child__value".into())),
3439 ],
3440 ["child__input".into()],
3441 )
3442 .unwrap();
3443 let text = graph.to_program_blog_text().unwrap();
3444 let program = crate::parse_inline_graph(&text).unwrap();
3445 assert!(text.contains("flat0 = flat1"), "{text}");
3446 assert!(text.contains("flat2 = !flat0"), "{text}");
3447 assert_eq!(program.root().interface.bit_inputs, ["flat1"]);
3448 }
3449
3450 #[test]
3451 fn executable_exports_keep_boolean_and_quantum_names_disjoint() {
3452 for input in ["q0", "source/value", "module"] {
3453 let mut graph = GalleryItem::CNOT
3454 .build()
3455 .materialize_root_graph()
3456 .expect("gallery flat projection");
3457 graph
3458 .set_actions_with_inputs(
3459 vec![Action::Let {
3460 name: "readout".into(),
3461 expr: crate::Expr::Var(input.into()),
3462 }],
3463 [input.into()],
3464 )
3465 .unwrap();
3466 let exported = graph.to_program_blog_text().unwrap();
3467 let program = crate::parse_inline_graph(&exported).unwrap();
3468 let [name] = program.root().interface.bit_inputs.as_slice() else {
3469 panic!("one external bit survives export");
3470 };
3471 assert!(
3472 program
3473 .root()
3474 .interface
3475 .quantum_ports
3476 .iter()
3477 .all(|port| port.name != *name)
3478 );
3479 assert_eq!(
3480 program.root().local_body().actions(),
3481 vec![Action::Let {
3482 name: "readout".into(),
3483 expr: crate::Expr::Var(name.clone()),
3484 }]
3485 );
3486 assert_eq!(graph.action_graph().inputs().collect::<Vec<_>>(), [input]);
3487 }
3488 }
3489
3490 #[test]
3496 fn lenient_action_install_carries_an_incomplete_list_then_clears_it() {
3497 let mut graph = BlockGraph::default();
3498 graph.add_block(Block::new(IVec3::ZERO, BlockKind::Cube(CubeKind::ZXZ)));
3499 graph.add_block(Block::new(
3500 IVec3::X,
3501 BlockKind::Selective(SelectiveKind::XY),
3502 ));
3503 graph.add_pipe(Pipe::new(IVec3::ZERO, Direction::XPLUS));
3504 let measure = Action::Measure {
3505 target: MeasureTarget::Node(IVec3::ZERO),
3506 name: "m0".into(),
3507 };
3508 let resolve = Action::Resolve {
3509 target: IVec3::X,
3510 condition: crate::Expr::Var("m0".into()),
3511 };
3512
3513 assert!(
3514 graph.set_actions(vec![measure.clone()]).is_err(),
3515 "the strict install is what forces the lenient one to exist"
3516 );
3517
3518 graph
3519 .set_actions_lenient(vec![measure.clone()])
3520 .expect("a valid identifier installs even while the list is incomplete");
3521 assert_eq!(graph.actions(), vec![measure.clone()]);
3522 assert!(
3523 graph.action_graph_error().is_some(),
3524 "the unmet constraint is carried, not dropped"
3525 );
3526
3527 graph
3528 .set_actions_lenient(vec![measure, resolve])
3529 .expect("the completed list installs");
3530 assert!(
3531 graph.action_graph_error().is_none(),
3532 "completing the list clears the carried violation"
3533 );
3534 }
3535
3536 #[test]
3539 fn lenient_action_install_still_rejects_an_unroundtrippable_name() {
3540 let mut graph = BlockGraph::default();
3541 graph.add_block(Block::new(IVec3::ZERO, BlockKind::Cube(CubeKind::ZXZ)));
3542
3543 let rejected = graph.set_actions_lenient(vec![Action::Measure {
3544 target: MeasureTarget::Node(IVec3::ZERO),
3545 name: "not an identifier".into(),
3546 }]);
3547
3548 assert!(rejected.is_err());
3549 assert!(graph.actions().is_empty());
3550 }
3551
3552 #[cfg(feature = "gltf")]
3553 #[test]
3554 fn export_geometry_uses_editor_orientation_without_reflecting_faces() {
3555 let mut graph = BlockGraph::new();
3556 let position = glam::ivec3(1, 2, 3);
3557 graph.add_block(Block::new(position, BlockKind::Cube(CubeKind::ZXZ)));
3558 let source = block_graph_as_gltf_data_with_popped_faces(&graph, 2.0, &[]);
3559 let exported = graph.build_export_gltf(2.0, None, &[]).unwrap();
3560 let editor_world = |point: glam::Vec3| glam::vec3(point.x, point.z, -point.y);
3561 for (color, triangles) in source.triangles {
3562 let expected: Vec<_> = triangles
3563 .into_iter()
3564 .map(|triangle| triangle.map(editor_world))
3565 .collect();
3566 assert_eq!(exported.triangles[&color], expected);
3567 }
3568 }
3569
3570 #[cfg(feature = "gltf")]
3571 #[test]
3572 fn popped_stabilizer_view_emphasizes_remaining_faces() {
3573 let graph = GalleryItem::CNOT
3574 .build()
3575 .materialize_root_graph()
3576 .expect("gallery flat projection");
3577 let stabilizers = graph.stabilizers().expect("CNOT stabilizers");
3578 let stabilizer = &stabilizers.generators[0];
3579
3580 let transparent = graph
3581 .build_export_gltf(2.0, Some(stabilizer), &[])
3582 .expect("transparent stabilizer view");
3583 let popped = graph
3584 .build_export_gltf(
3585 2.0,
3586 Some(stabilizer),
3587 &[GltfFaceSelector::All(Direction::YPLUS)],
3588 )
3589 .expect("popped stabilizer view");
3590
3591 assert!(transparent.triangles.keys().any(|color| color.a < 255));
3592 assert!(popped.triangles.keys().all(|color| color.a == 255));
3593 assert!(popped.triangles.contains_key(&POPPED_X_FACE_COLOR));
3594 assert!(popped.triangles.contains_key(&POPPED_Z_FACE_COLOR));
3595 }
3596
3597 #[test]
3598 fn to_blog_text_serializes_action_dag_in_ordinal_order() {
3599 let mut graph = BlockGraph::new();
3600 graph.add_block(Block::new(
3601 glam::ivec3(0, 0, 0),
3602 BlockKind::Cube(CubeKind::ZXZ),
3603 ));
3604 graph
3605 .set_actions_lenient(vec![Action::Measure {
3606 target: MeasureTarget::Node(glam::ivec3(0, 0, 0)),
3607 name: "m0".into(),
3608 }])
3609 .unwrap();
3610
3611 let text = graph.to_blog_text();
3612 assert!(text.starts_with("BLOG 1.0\n"));
3613 assert!(text.contains("m0 = measure 0\n"));
3614 assert!(!text.contains("debug"));
3615 assert!(!text.contains("set_time"));
3616 }
3617
3618 #[test]
3619 fn branch_blog_pipes_use_common_and_arm_local_ids() {
3620 let past = IVec3::ZERO;
3621 let inside = IVec3::Z;
3622 let mut graph = BlockGraph::new();
3623 graph.add_block(Block::new(past, BlockKind::Cube(CubeKind::ZXZ)));
3624 graph.add_block(Block::new(
3625 IVec3::new(2, 0, 0),
3626 BlockKind::Cube(CubeKind::ZXZ),
3627 ));
3628 let arm = |tag| {
3629 BranchArm::new(
3630 vec![
3631 Block::new(inside, BlockKind::Cube(CubeKind::ZXZ)),
3632 Block::new(2 * inside, BlockKind::Cube(CubeKind::ZXZ)),
3633 ],
3634 vec![
3635 Pipe::new(past, Direction::ZPLUS).with_tag(tag).unwrap(),
3636 Pipe::new(inside, Direction::ZPLUS).with_tag(tag).unwrap(),
3637 ],
3638 )
3639 };
3640 let target = graph
3641 .try_add_branch_region("b", arm("false"), arm("true"))
3642 .unwrap();
3643 graph
3644 .set_actions_deferred(vec![
3645 Action::Measure {
3646 target: MeasureTarget::Node(IVec3::new(2, 0, 0)),
3647 name: "m".into(),
3648 },
3649 Action::Branch {
3650 target,
3651 condition: Expr::Var("m".into()),
3652 },
3653 ])
3654 .unwrap();
3655
3656 let text = graph.to_blog_body_text();
3657 assert!(text.contains(" false {\n 2: ZXZ [0, 0, 1]\n 3: ZXZ [0, 0, 2]\n 0 -> +Z <false>\n 2 -> +Z <false>\n }"));
3658 assert!(text.contains(" true {\n 4: ZXZ [0, 0, 1]\n 5: ZXZ [0, 0, 2]\n 0 -> +Z <true>\n 4 -> +Z <true>\n }"));
3659 assert_eq!(
3660 BlockGraph::from_blog_text(&text)
3661 .unwrap()
3662 .to_blog_body_text(),
3663 text
3664 );
3665 let full = graph.to_blog_text();
3666 assert!(full.contains("module main {"));
3667 assert_eq!(BlockGraph::from_text(&full).unwrap().to_blog_text(), full);
3668 }
3669
3670 #[test]
3671 fn to_blog_text_serializes_walking_blocks_with_explicit_end_position() {
3672 let mut graph = BlockGraph::new();
3673 graph.add_block(Block::new(
3674 glam::ivec3(1, 0, 3),
3675 BlockKind::Walking(
3676 WalkingKind::new(WalkingBoundaryKind::ZXZ, glam::ivec2(0, 1)).unwrap(),
3677 ),
3678 ));
3679 graph.add_block(Block::new(glam::ivec3(1, 1, 5), BlockKind::Port));
3680 graph.add_pipe(Pipe::new(glam::ivec3(1, 1, 4), Direction::ZPLUS));
3681
3682 let text = graph.to_blog_text();
3683
3684 assert!(text.contains("0: walk ZXZ [1, 0, 3] -> [1, 1, 4]"));
3685 assert!(text.contains("[1, 1, 4] -> +Z"));
3686 assert!(!text.contains("WZXZ"));
3687 }
3688
3689 #[test]
3690 fn tags_survive_blog_text_round_trip() {
3691 let source = concat!(
3692 "BLOG 1.0\n\n",
3693 "0: ZXZ [0, 0, 0] <if>\n",
3694 "1: ZXZ [1, 0, 0] <R_Z(π/8)^†>\n",
3695 "0 -> +X <(X⊗X)^(1/2)>\n",
3696 );
3697 let graph = BlockGraph::from_blog_text(source).unwrap();
3698 assert_eq!(graph.to_blog_body_text(), source);
3699 let full = graph.to_blog_text();
3700 let restored = BlockGraph::from_text(&full).unwrap();
3701 assert_eq!(restored.to_blog_body_text(), source);
3702 assert_eq!(restored.to_blog_text(), full);
3703 }
3704
3705 #[test]
3706 fn tag_edits_preserve_analyzed_actions() {
3707 let mut graph = GalleryItem::T
3708 .build()
3709 .materialize_root_graph()
3710 .expect("gallery flat projection");
3711 let snapshot = |graph: &BlockGraph| {
3712 (
3713 graph.action_graph().is_analyzed(),
3714 graph
3715 .action_graph()
3716 .ordered_nodes()
3717 .cloned()
3718 .collect::<Vec<_>>(),
3719 graph.action_graph().dependencies().collect::<Vec<_>>(),
3720 )
3721 };
3722 let actions = snapshot(&graph);
3723 let block = graph
3724 .blocks()
3725 .next()
3726 .expect("gallery graph has blocks")
3727 .pos();
3728 let (src, dst) = graph
3729 .pipes()
3730 .next()
3731 .expect("gallery graph has pipes")
3732 .endpoints();
3733
3734 graph.set_block_tag(block, "block-tag").unwrap();
3735 graph.set_pipe_tag(src, dst, "pipe-tag").unwrap();
3736
3737 assert_eq!(snapshot(&graph), actions);
3738 }
3739
3740 #[test]
3741 fn shift_positions_preserves_action_dag_contents() {
3742 let mut graph = BlockGraph::new();
3743 graph.add_block(Block::new(
3744 glam::ivec3(0, 0, 0),
3745 BlockKind::Cube(CubeKind::ZXZ),
3746 ));
3747 graph
3748 .add_action(Action::Measure {
3749 target: MeasureTarget::Node(glam::ivec3(0, 0, 0)),
3750 name: "m0".into(),
3751 })
3752 .unwrap();
3753
3754 let shifted = graph
3755 .shift_positions(glam::ivec3(3, 0, 1))
3756 .expect("shift fits");
3757 let ordered = shifted
3758 .action_graph()
3759 .ordered_nodes()
3760 .map(|node| node.action.clone())
3761 .collect::<Vec<_>>();
3762 assert_eq!(
3763 ordered,
3764 vec![Action::Measure {
3765 target: MeasureTarget::Node(glam::ivec3(3, 0, 1)),
3766 name: "m0".into(),
3767 }]
3768 );
3769 }
3770
3771 #[test]
3772 fn position_transforms_preserve_external_action_inputs() {
3773 let mut graph = BlockGraph::new();
3774 graph.add_block(Block::new(
3775 glam::ivec3(0, 0, 3),
3776 BlockKind::Cube(CubeKind::ZXZ),
3777 ));
3778 graph
3779 .set_actions_with_inputs(
3780 vec![Action::DiscardIf(crate::Expr::Var("enabled".into()))],
3781 ["enabled".to_string()],
3782 )
3783 .unwrap();
3784
3785 for transformed in [
3786 graph.shift_positions(glam::ivec3(1, 0, 0)).unwrap(),
3787 graph.with_zero_min_z().unwrap(),
3788 ] {
3789 assert!(transformed.action_graph_error().is_none());
3790 assert_eq!(
3791 transformed.action_graph().inputs().collect::<Vec<_>>(),
3792 ["enabled"]
3793 );
3794 transformed.validate().unwrap();
3795 }
3796 }
3797
3798 #[test]
3799 fn set_actions_with_inputs_replaces_declared_inputs() {
3800 let mut graph = BlockGraph::new();
3801 graph
3802 .set_actions_with_inputs(Vec::new(), ["old".to_string()])
3803 .unwrap();
3804
3805 assert!(matches!(
3806 graph.set_actions_with_inputs(
3807 vec![Action::DiscardIf(crate::Expr::Var("old".into()))],
3808 ["new".to_string()],
3809 ),
3810 Err(BlockGraphError::InvalidAction(
3811 InvalidActionError::UndefinedVariable(name)
3812 )) if name == "old"
3813 ));
3814 }
3815
3816 #[test]
3817 fn limited_action_install_restores_actions_and_inputs_after_exhaustion() {
3818 let continuing = crate::parse_inline_graph(include_str!(
3819 "../../docs/fixtures/conditional_cz_strip.blog"
3820 ))
3821 .unwrap()
3822 .flatten()
3823 .unwrap();
3824 assert!(continuing.has_continuing_branches());
3825 for mut graph in [
3826 GalleryItem::CNOT
3827 .build()
3828 .materialize_root_graph()
3829 .expect("gallery flat projection"),
3830 continuing,
3831 ] {
3832 let previous_actions = graph.actions();
3833 let previous_nodes = graph
3834 .action_graph()
3835 .ordered_nodes()
3836 .cloned()
3837 .collect::<Vec<_>>();
3838 let previous_inputs = graph.action_inputs.clone();
3839 let mut actions = previous_actions.clone();
3840 actions.push(Action::Let {
3841 name: "new_value".into(),
3842 expr: crate::Expr::Var("new_input".into()),
3843 });
3844 let mut inputs = previous_inputs.clone();
3845 inputs.insert("new_input".into());
3846 let limits = crate::ModuleCertificationLimits {
3847 max_local_columns: 0,
3848 max_boolean_steps: 0,
3849 ..crate::ModuleCertificationLimits::DEFAULT
3850 };
3851 let error = graph
3852 .set_actions_with_inputs_and_limits(actions.clone(), inputs.clone(), limits)
3853 .unwrap_err();
3854 assert!(matches!(
3855 error,
3856 BlockGraphError::Stabilizer(crate::StabilizerError::ResourceLimited { .. })
3857 ));
3858 assert_eq!(graph.actions(), previous_actions);
3859 assert_eq!(graph.action_inputs, previous_inputs);
3860 assert_eq!(
3861 graph
3862 .action_graph()
3863 .ordered_nodes()
3864 .cloned()
3865 .collect::<Vec<_>>(),
3866 previous_nodes
3867 );
3868 assert!(graph.action_graph_error().is_none());
3869 graph
3870 .set_actions_with_inputs_and_limits(
3871 actions.clone(),
3872 inputs.clone(),
3873 crate::ModuleCertificationLimits::DEFAULT,
3874 )
3875 .unwrap();
3876 assert_eq!(graph.actions(), actions);
3877 assert_eq!(graph.action_inputs, inputs);
3878 }
3879 }
3880
3881 #[test]
3882 fn block_graph_set_actions_enriches_measurements_before_conversion() {
3883 use crate::{MeasureTarget, MeasurementObservable};
3884 use glam::ivec3;
3885
3886 let mut graph = BlockGraph::new();
3887 graph.add_block(Block::new(ivec3(0, 0, 0), BlockKind::Cube(CubeKind::ZXZ)));
3888 graph
3889 .set_actions(vec![Action::Measure {
3890 target: MeasureTarget::Node(ivec3(0, 0, 0)),
3891 name: "m0".into(),
3892 }])
3893 .unwrap();
3894
3895 assert_eq!(
3896 graph.action_graph().node_by_ordinal(0).unwrap().measurement,
3897 Some(MeasurementObservable::Concrete(crate::PauliBasis::Z))
3898 );
3899 assert!(graph.action_graph().is_analyzed());
3900 assert!(
3901 graph
3902 .action_graph()
3903 .node_by_ordinal(0)
3904 .unwrap()
3905 .measurement_stabilizer
3906 .is_some()
3907 );
3908 }
3909
3910 #[test]
3911 fn deferred_actions_wait_for_physical_analysis() {
3912 let mut graph = GalleryItem::T
3913 .build()
3914 .materialize_root_graph()
3915 .expect("gallery flat projection");
3916 let actions = graph.actions();
3917 graph.clear_actions();
3918
3919 graph.set_actions_deferred(actions).unwrap();
3920 graph.validate_source().unwrap();
3921 assert!(!graph.action_graph().is_analyzed());
3922
3923 let (graph, _) = graph.analyze_actions().unwrap();
3924 assert!(graph.action_graph().is_analyzed());
3925 }
3926
3927 #[test]
3928 fn projected_measurement_must_match_shared_decode() {
3929 let graph = GalleryItem::CCZGateTeleport
3930 .build()
3931 .materialize_root_graph()
3932 .expect("gallery flat projection");
3933 let (_, baseline, projections) = graph.analyze_actions_with_projections().unwrap();
3934 let mut projected = projections[0].1.clone();
3935 let row = projected
3936 .generators
3937 .iter_mut()
3938 .find(|row| row.is_measurement())
3939 .unwrap();
3940 let name = row.measurement_name().unwrap().to_owned();
3941 row.stabilizer.sign ^= true;
3942
3943 assert!(matches!(
3944 validate_shared_measurement_rows(&baseline, &projected),
3945 Err(BlockGraphError::InvalidAction(
3946 InvalidActionError::BranchDependentMeasurementSurface { name: rejected }
3947 )) if rejected == name
3948 ));
3949 }
3950
3951 #[test]
3952 fn projected_measurement_fixings_must_match_shared_decode() {
3953 let (_, baseline) = GalleryItem::PhaseGradientK4
3954 .build()
3955 .materialize_root_graph()
3956 .expect("gallery flat projection")
3957 .analyze_actions()
3958 .unwrap();
3959 let mut projected = baseline.clone();
3960 projected
3961 .generators
3962 .iter_mut()
3963 .find(|row| matches!(row.kind, StabilizerRowKind::SelectiveFixing { .. }))
3964 .unwrap()
3965 .stabilizer
3966 .sign ^= true;
3967
3968 assert!(matches!(
3969 validate_shared_measurement_rows(&baseline, &projected),
3970 Err(BlockGraphError::InvalidAction(
3971 InvalidActionError::BranchDependentMeasurementSurface { .. }
3972 ))
3973 ));
3974 }
3975
3976 #[test]
3977 fn strict_action_install_keeps_previous_list_when_surface_is_impossible() {
3978 let mut graph = GalleryItem::T
3979 .build()
3980 .materialize_root_graph()
3981 .expect("gallery flat projection");
3982 let actions = graph.actions();
3983 for pos in [glam::ivec3(0, 0, 0), glam::ivec3(0, 0, 2)] {
3984 graph.get_block_mut(pos).unwrap().kind = BlockKind::Cube(CubeKind::XZX);
3985 }
3986 graph.clear_actions();
3987
3988 let err = graph.set_actions(actions).unwrap_err();
3989
3990 assert!(matches!(
3991 err,
3992 BlockGraphError::Stabilizer(StabilizerError::MeasurementSurfaceUnavailable {
3993 ref mvar
3994 }) if mvar == "mzz"
3995 ));
3996 assert!(graph.actions().is_empty());
3997 }
3998
3999 #[test]
4000 fn complete_surface_fallback_backtracks_across_cz_records() {
4001 let mut graph = GalleryItem::CZSpatialH
4002 .build()
4003 .materialize_root_graph()
4004 .expect("gallery flat projection");
4005 graph.clear_actions();
4006 let actions = vec![
4007 Action::Measure {
4008 target: MeasureTarget::Node(glam::ivec3(0, 0, 1)),
4009 name: "a".into(),
4010 },
4011 Action::Measure {
4012 target: MeasureTarget::Node(glam::ivec3(1, 0, 1)),
4013 name: "b".into(),
4014 },
4015 ];
4016 let mut greedy_probe = graph.clone();
4017 greedy_probe.set_actions_lenient(actions.clone()).unwrap();
4018 let zx = ZXGraph::from_block_graph_for_analysis(&greedy_probe).unwrap();
4019 assert!(matches!(
4020 zx.to_stabilizer_table(),
4021 Err(StabilizerError::MeasurementSurfaceUnavailable { ref mvar }) if mvar == "b"
4022 ));
4023
4024 graph.set_actions(actions).unwrap();
4025 for (ordinal, pos) in [glam::ivec3(0, 0, 1), glam::ivec3(1, 0, 1)]
4026 .into_iter()
4027 .enumerate()
4028 {
4029 let node = graph.action_graph().node_by_ordinal(ordinal).unwrap();
4030 let Some(MeasurementObservable::Concrete(basis)) = node.measurement else {
4031 panic!("CZ node measurement has a concrete basis");
4032 };
4033 assert_eq!(
4034 node.measurement_stabilizer
4035 .as_ref()
4036 .unwrap()
4037 .interior_nodes
4038 .get(&pos),
4039 Some(&Pauli::from(basis))
4040 );
4041 }
4042 }
4043
4044 #[test]
4045 fn failed_refresh_after_mutation_records_action_graph_error() {
4046 use glam::ivec3;
4047
4048 let mut graph = BlockGraph::new();
4049 graph.add_block(Block::new(ivec3(0, 0, 0), BlockKind::Cube(CubeKind::ZXZ)));
4050 graph
4051 .set_actions(vec![Action::Measure {
4052 target: MeasureTarget::Node(ivec3(0, 0, 0)),
4053 name: "m0".into(),
4054 }])
4055 .unwrap();
4056 assert!(graph.action_graph_error().is_none());
4057
4058 graph.remove_block(ivec3(0, 0, 0));
4061 assert!(graph.action_graph_error().is_some());
4062 assert!(graph.has_actions());
4063
4064 graph.clear_actions();
4065 assert!(graph.action_graph_error().is_none());
4066 }
4067
4068 #[test]
4069 fn shift_positions_carries_degradation_instead_of_panicking() {
4070 use glam::ivec3;
4071
4072 let mut graph = BlockGraph::new();
4073 graph.add_block(Block::new(ivec3(0, 0, 0), BlockKind::Cube(CubeKind::ZXZ)));
4074 graph.add_block(Block::new(ivec3(5, 0, 0), BlockKind::Cube(CubeKind::ZXZ)));
4075 graph
4076 .set_actions(vec![Action::Measure {
4077 target: MeasureTarget::Node(ivec3(0, 0, 0)),
4078 name: "m0".into(),
4079 }])
4080 .unwrap();
4081 graph.remove_block(ivec3(0, 0, 0));
4082 assert!(graph.action_graph_error().is_some());
4083
4084 let shifted = graph.shift_positions(ivec3(1, 0, 0)).expect("shift fits");
4085 assert!(shifted.action_graph_error().is_some());
4086 assert!(shifted.has_actions());
4087 }
4088
4089 #[test]
4090 fn shift_positions_translates_actions_in_an_empty_degraded_graph() {
4091 use glam::ivec3;
4092
4093 let mut graph = BlockGraph::new();
4094 graph.add_block(Block::new(ivec3(0, 0, 0), BlockKind::Cube(CubeKind::ZXZ)));
4095 graph
4096 .set_actions(vec![Action::Measure {
4097 target: MeasureTarget::Node(ivec3(0, 0, 0)),
4098 name: "m0".into(),
4099 }])
4100 .unwrap();
4101 graph.remove_block(ivec3(0, 0, 0));
4102 assert!(graph.is_empty());
4103
4104 let shifted = graph.shift_positions(ivec3(4, 0, 0)).expect("shift fits");
4105 assert_eq!(
4106 shifted.actions(),
4107 [Action::Measure {
4108 target: MeasureTarget::Node(ivec3(4, 0, 0)),
4109 name: "m0".into(),
4110 }]
4111 );
4112 assert!(shifted.action_graph_error().is_some());
4113 }
4114
4115 #[test]
4116 fn shift_positions_keeps_lookups_valid_after_removing_lower_block() {
4117 use glam::ivec3;
4118
4119 let mut graph = BlockGraph::new();
4123 graph.add_block(Block::new(ivec3(0, 0, 0), BlockKind::Cube(CubeKind::ZXZ)));
4124 graph.add_block(Block::new(ivec3(1, 0, 0), BlockKind::Cube(CubeKind::ZXZ)));
4125 graph.add_block(Block::new(ivec3(2, 0, 0), BlockKind::Cube(CubeKind::ZXZ)));
4126 graph.remove_block(ivec3(0, 0, 0));
4127
4128 let mut shifted = graph.shift_positions(ivec3(10, 0, 0)).expect("shift fits");
4129
4130 assert_eq!(
4131 shifted.get_block(ivec3(11, 0, 0)).unwrap().pos,
4132 ivec3(11, 0, 0)
4133 );
4134 assert_eq!(
4135 shifted.get_block(ivec3(12, 0, 0)).unwrap().pos,
4136 ivec3(12, 0, 0)
4137 );
4138 assert!(shifted.get_block(ivec3(13, 0, 0)).is_none());
4139
4140 shifted
4141 .try_add_pipe(Pipe::new(ivec3(11, 0, 0), Direction::XPLUS))
4142 .unwrap();
4143 assert!(shifted.has_pipe_between(ivec3(11, 0, 0), ivec3(12, 0, 0)));
4144 }
4145
4146 #[test]
4147 fn validate_and_to_zx_graph_rebuild_measurements_after_kind_mutation() {
4148 use glam::ivec3;
4149
4150 let mut graph = BlockGraph::new();
4151 graph.add_block(Block::new(ivec3(0, 0, 0), BlockKind::Cube(CubeKind::ZXZ)));
4152 graph
4153 .set_actions(vec![Action::Measure {
4154 target: MeasureTarget::Node(ivec3(0, 0, 0)),
4155 name: "m0".into(),
4156 }])
4157 .unwrap();
4158
4159 graph
4160 .set_block_kind(ivec3(0, 0, 0), BlockKind::Cube(CubeKind::XZX))
4161 .unwrap();
4162
4163 assert_eq!(
4164 graph.action_graph().node_by_ordinal(0).unwrap().measurement,
4165 Some(MeasurementObservable::Concrete(crate::PauliBasis::X))
4166 );
4167 graph.validate().unwrap();
4168 let zx = graph.to_zx_graph().unwrap();
4169
4170 assert_eq!(
4171 zx.action_graph().node_by_ordinal(0).unwrap().measurement,
4172 Some(MeasurementObservable::Concrete(crate::PauliBasis::X))
4173 );
4174 }
4175
4176 #[test]
4177 fn validate_structure_ignores_action_semantic_errors() {
4178 use glam::ivec3;
4179
4180 let measured = ivec3(0, 0, 0);
4181 let src = ivec3(2, 0, 0);
4182 let dst = ivec3(3, 0, 0);
4183 let mut graph = BlockGraph::new();
4184 graph.add_block(Block::new(measured, BlockKind::Cube(CubeKind::ZXZ)));
4185 graph.add_block(Block::new(src, BlockKind::Cube(CubeKind::ZXZ)));
4186 graph.add_block(Block::new(dst, BlockKind::Cube(CubeKind::ZXZ)));
4187 graph.add_pipe(Pipe::new(src, Direction::XPLUS));
4188 graph
4189 .set_actions(vec![Action::Measure {
4190 target: MeasureTarget::Node(measured),
4191 name: "m0".into(),
4192 }])
4193 .unwrap();
4194
4195 graph.remove_block(measured);
4196
4197 graph.validate_structure().unwrap();
4198 assert!(graph.validate().is_err());
4199 }
4200
4201 #[test]
4202 fn set_pipe_hadamard_refreshes_cached_edge_measurement_observable() {
4203 let mut graph = BlockGraph::new();
4204 graph.add_block(Block::new(
4205 IVec3::new(0, 0, -1),
4206 BlockKind::Cube(CubeKind::ZXZ),
4207 ));
4208 graph.add_block(Block::new(
4209 IVec3::new(0, 0, 0),
4210 BlockKind::Cube(CubeKind::ZXZ),
4211 ));
4212 graph.add_block(Block::new(
4213 IVec3::new(0, 0, 1),
4214 BlockKind::Cube(CubeKind::ZXZ),
4215 ));
4216 graph.add_block(Block::new(
4217 IVec3::new(1, 0, 0),
4218 BlockKind::Cube(CubeKind::ZXZ),
4219 ));
4220 graph.add_pipe(Pipe::new(IVec3::new(0, 0, -1), Direction::ZPLUS));
4221 graph.add_pipe(Pipe::new(IVec3::new(0, 0, 0), Direction::ZPLUS));
4222 graph.add_pipe(Pipe::new(IVec3::new(0, 0, 0), Direction::XPLUS));
4223 graph
4224 .set_actions_lenient(vec![Action::Measure {
4225 target: MeasureTarget::Edge {
4226 src: IVec3::new(0, 0, 0),
4227 dir: Direction::XPLUS,
4228 },
4229 name: "mx".into(),
4230 }])
4231 .unwrap();
4232
4233 assert_eq!(
4234 graph.action_graph().node_by_ordinal(0).unwrap().measurement,
4235 Some(MeasurementObservable::Concrete(crate::PauliBasis::X))
4236 );
4237
4238 graph
4239 .set_pipe_hadamard(IVec3::new(0, 0, 0), IVec3::new(1, 0, 0), true)
4240 .expect("pipe exists");
4241
4242 assert_eq!(
4243 graph.action_graph().node_by_ordinal(0).unwrap().measurement,
4244 Some(MeasurementObservable::Concrete(crate::PauliBasis::Z))
4245 );
4246 }
4247
4248 #[test]
4249 fn test_randomly_resolve_selectives_returns_replacement_map() {
4250 let graph = GalleryItem::CCZFactoryWithTels
4251 .build()
4252 .materialize_root_graph()
4253 .expect("gallery flat projection");
4254 let (resolved, replacements) = graph.randomly_resolve_selectives(rand::random()).unwrap();
4255
4256 assert!(resolved.blocks().all(|block| !block.kind.is_selective()));
4257 assert!(!resolved.has_actions());
4258 assert!(!replacements.is_empty());
4259 for (original, current) in replacements {
4260 assert!(original.kind.is_selective());
4261 assert!(!current.kind.is_selective());
4262 assert_eq!(original.pos, current.pos);
4263 assert_eq!(original.tag, current.tag);
4264 assert_eq!(resolved.get_block(current.pos), Some(¤t));
4265 }
4266 }
4267
4268 #[test]
4269 fn randomly_resolve_selectives_samples_joint_controls_once() {
4270 let graph = GalleryItem::ToffoliFromAndDelayedCZ
4271 .build()
4272 .materialize_root_graph()
4273 .expect("gallery flat projection");
4274 let joint = [IVec3::new(0, 1, 5), IVec3::new(1, 0, 5)];
4275 let mut seen = HashSet::new();
4276
4277 let limit = 2usize
4278 .saturating_pow(u32::try_from(joint.len()).unwrap_or(u32::MAX))
4279 .min(crate::ModuleCertificationLimits::DEFAULT.max_guarded_domain_size);
4280 let domain = graph
4281 .action_graph()
4282 .resolve_value_domain_bounded(&joint, limit)
4283 .expect("two joint sites have at most four resolve tuples");
4284 assert_eq!(domain.values(), &[vec![false, false], vec![true, true]]);
4285
4286 for seed in 0..64 {
4287 let (_, replacements) = graph.randomly_resolve_selectives(seed).unwrap();
4288 let choices = joint.map(|pos| {
4289 let original = graph.get_block(pos).expect("joint selective exists");
4290 let resolved = &replacements[original];
4291 let BlockKind::Selective(kind) = original.kind else {
4292 panic!("joint site is selective");
4293 };
4294 resolved.kind == graph.resolved_selective_kind_for_basis(kind.pauli_if_true())
4295 });
4296 assert_eq!(
4297 choices[0], choices[1],
4298 "seed {seed} split sites controlled by the same mxy value",
4299 );
4300 seen.insert(choices[0]);
4301 }
4302
4303 assert_eq!(seen, HashSet::from([false, true]));
4304 }
4305
4306 #[test]
4307 fn layer_graph_discards_actions() {
4308 let mut graph = BlockGraph::new();
4309 graph.add_block(Block::new(
4310 IVec3::new(0, 0, 0),
4311 BlockKind::Cube(CubeKind::ZXZ),
4312 ));
4313 graph.add_block(Block::new(
4314 IVec3::new(0, 0, 1),
4315 BlockKind::Cube(CubeKind::ZXZ),
4316 ));
4317 graph.add_pipe(Pipe::new(IVec3::new(0, 0, 0), Direction::ZPLUS));
4318 graph
4319 .add_action(Action::Measure {
4320 target: MeasureTarget::Node(IVec3::new(0, 0, 1)),
4321 name: "m1".into(),
4322 })
4323 .unwrap();
4324
4325 let layer = graph.layer(0).into_graph();
4326 assert!(!layer.has_actions());
4327 assert!(layer.block_count() > 0);
4328 }
4329
4330 #[test]
4331 fn block_layer_view_materializes_boundary_ports() {
4332 let graph = hand_built_layer_graph();
4333 let view_graph = graph.layer(0).into_graph();
4334
4335 let expected_blocks = vec![
4336 (IVec3::new(0, 0, -1), BlockKind::Port),
4337 (IVec3::new(0, 0, 0), BlockKind::Cube(CubeKind::ZXZ)),
4338 (IVec3::new(1, 0, 0), BlockKind::Cube(CubeKind::ZXZ)),
4339 (IVec3::new(1, 0, 1), BlockKind::Port),
4340 ];
4341 let expected_pipes = vec![
4342 (IVec3::new(0, 0, -1), IVec3::new(0, 0, 0), false),
4343 (IVec3::new(0, 0, 0), IVec3::new(1, 0, 0), false),
4344 (IVec3::new(1, 0, 0), IVec3::new(1, 0, 1), false),
4345 ];
4346
4347 assert_eq!(view_graph.block_count(), 4);
4348 assert_eq!(view_graph.pipe_count(), 3);
4349 assert_eq!(block_signatures(&view_graph), expected_blocks);
4350 assert_eq!(pipe_signatures(&view_graph), expected_pipes);
4351 }
4352
4353 #[test]
4354 fn scaled_cube_occupies_multiple_time_cells_and_exposes_top_endpoint() {
4355 let mut graph = BlockGraph::new();
4356 let block = Block::new(IVec3::ZERO, BlockKind::Cube(CubeKind::ZXZ))
4357 .with_height("3d".parse().expect("valid height"))
4358 .unwrap();
4359 graph.add_block(block);
4360
4361 assert_eq!(
4362 graph.occupied_positions().collect::<Vec<_>>(),
4363 vec![
4364 IVec3::new(0, 0, 0),
4365 IVec3::new(0, 0, 1),
4366 IVec3::new(0, 0, 2)
4367 ]
4368 );
4369 assert!(graph.has_endpoint_at(IVec3::new(0, 0, 2)));
4370 assert_eq!(
4371 graph
4372 .get_block(IVec3::ZERO)
4373 .expect("scaled cube")
4374 .endpoint_for_direction(Direction::ZPLUS),
4375 IVec3::new(0, 0, 2)
4376 );
4377 assert_eq!(graph.spans().expect("non-empty graph has spans").2, 0..=2);
4378 assert_eq!(graph.layer(1).blocks().count(), 1);
4379 }
4380
4381 #[test]
4382 fn scaled_cube_footprint_blocks_overlapping_placement() {
4383 let mut graph = BlockGraph::new();
4384 graph.add_block(
4385 Block::new(IVec3::ZERO, BlockKind::Cube(CubeKind::ZXZ))
4386 .with_height("2d".parse().expect("valid height"))
4387 .unwrap(),
4388 );
4389
4390 assert!(matches!(
4391 graph.try_add_block(Block::new(
4392 IVec3::new(0, 0, 1),
4393 BlockKind::Cube(CubeKind::ZXZ)
4394 )),
4395 Err(BlockGraphError::BlockPositionOccupied(pos))
4396 if pos == IVec3::new(0, 0, 1)
4397 ));
4398 }
4399
4400 #[test]
4401 fn max_height_cube_placement_is_linear_in_footprint_size() {
4402 let mut graph = BlockGraph::new();
4403 graph
4404 .try_add_block(
4405 Block::new(IVec3::ZERO, BlockKind::Cube(CubeKind::ZXZ))
4406 .with_height(CubeHeight::new(crate::MAX_CUBE_HEIGHT_CELLS, 1, 0).unwrap())
4407 .unwrap(),
4408 )
4409 .unwrap();
4410
4411 graph
4412 .try_add_block(
4413 Block::new(IVec3::X, BlockKind::Cube(CubeKind::ZXZ))
4414 .with_height(CubeHeight::new(crate::MAX_CUBE_HEIGHT_CELLS, 1, 0).unwrap())
4415 .unwrap(),
4416 )
4417 .expect("disjoint maximum-height footprints place without a quadratic scan");
4418 }
4419
4420 #[test]
4421 fn graph_ingress_rejects_coordinate_overflow() {
4422 let walking = WalkingKind::new(WalkingBoundaryKind::ZXZ, glam::ivec2(1, 0)).unwrap();
4423 let mut graph = BlockGraph::new();
4424 let position = IVec3::new(0, 0, i32::MAX);
4425 assert!(matches!(
4426 graph.try_add_block(Block::new(position, BlockKind::Walking(walking))),
4427 Err(BlockGraphError::CoordinateOverflow { position: p, .. }) if p == position
4428 ));
4429
4430 let pipe = Pipe::new(IVec3::new(i32::MAX, 0, 0), Direction::XPLUS);
4431 assert!(matches!(
4432 graph.try_add_pipe(pipe),
4433 Err(BlockGraphError::CoordinateOverflow { .. })
4434 ));
4435
4436 assert!(matches!(
4437 graph.add_action(Action::Measure {
4438 target: MeasureTarget::Edge {
4439 src: IVec3::new(i32::MAX, 0, 0),
4440 dir: Direction::XPLUS,
4441 },
4442 name: "overflow".into(),
4443 }),
4444 Err(BlockGraphError::CoordinateOverflow { .. })
4445 ));
4446 }
4447
4448 #[test]
4449 fn graph_transforms_report_coordinate_overflow() {
4450 let mut graph = BlockGraph::new();
4451 graph
4452 .try_add_block(Block::new(IVec3::new(i32::MAX, 0, 0), BlockKind::Port))
4453 .unwrap();
4454 assert!(matches!(
4455 graph.shift_positions(IVec3::X),
4456 Err(BlockGraphError::CoordinateOverflow { .. })
4457 ));
4458
4459 let mut rotation = BlockGraph::new();
4460 rotation
4461 .try_add_block(Block::new(IVec3::new(i32::MIN, 0, 0), BlockKind::Port))
4462 .unwrap();
4463 assert!(matches!(
4464 rotation.rotate_about_origin(UDirection::Y, 1),
4465 Err(BlockGraphError::CoordinateRotationOverflow { .. })
4466 ));
4467 }
4468
4469 #[test]
4470 fn zero_min_z_normalizes_i32_min_and_preserves_graph_content() {
4471 let mut at_min = BlockGraph::new();
4472 at_min
4473 .try_add_block(Block::new(IVec3::new(0, 0, i32::MIN), BlockKind::Port))
4474 .unwrap();
4475 at_min
4476 .try_add_block(Block::new(
4477 IVec3::new(0, 0, i32::MIN + 1),
4478 BlockKind::Cube(CubeKind::ZXZ),
4479 ))
4480 .unwrap();
4481 at_min
4482 .try_add_pipe(Pipe::new(IVec3::new(0, 0, i32::MIN), Direction::ZPLUS))
4483 .unwrap();
4484 at_min
4485 .add_action(Action::Measure {
4486 target: MeasureTarget::Node(IVec3::new(0, 0, i32::MIN + 1)),
4487 name: "m".into(),
4488 })
4489 .unwrap();
4490
4491 let normalized = at_min.with_zero_min_z().expect("two-layer span fits");
4492 assert_eq!(
4493 normalized.positions().collect::<HashSet<_>>(),
4494 HashSet::from([IVec3::ZERO, IVec3::Z])
4495 );
4496 assert!(normalized.has_pipe_between(IVec3::ZERO, IVec3::Z));
4497 assert_eq!(
4498 normalized.actions(),
4499 [Action::Measure {
4500 target: MeasureTarget::Node(IVec3::Z),
4501 name: "m".into(),
4502 }]
4503 );
4504 }
4505
4506 #[test]
4507 fn zero_min_z_rejects_unrepresentable_span() {
4508 let mut too_wide = BlockGraph::new();
4509 too_wide
4510 .try_add_block(Block::new(
4511 IVec3::new(0, 0, -2_000_000_000),
4512 BlockKind::Cube(CubeKind::ZXZ),
4513 ))
4514 .unwrap();
4515 too_wide
4516 .try_add_block(Block::new(
4517 IVec3::new(0, 0, 2_000_000_000),
4518 BlockKind::Cube(CubeKind::ZXZ),
4519 ))
4520 .unwrap();
4521 assert!(matches!(
4522 too_wide.with_zero_min_z(),
4523 Err(BlockGraphError::CoordinateNormalizationOverflow {
4524 min_z: -2_000_000_000,
4525 max_z: 2_000_000_000,
4526 })
4527 ));
4528 }
4529
4530 #[test]
4531 fn three_quarter_turns_do_not_require_unrepresentable_intermediate_turns() {
4532 let rotate = |axis, position| {
4533 crate::ModuleRotation::new(axis, 3)
4534 .try_rotate_position(position)
4535 .unwrap()
4536 };
4537 assert_eq!(
4538 rotate(UDirection::X, IVec3::new(0, 0, i32::MIN)),
4539 IVec3::new(0, i32::MIN, 0)
4540 );
4541 assert_eq!(
4542 rotate(UDirection::Y, IVec3::new(i32::MIN, 0, 0)),
4543 IVec3::new(0, 0, i32::MIN)
4544 );
4545 assert_eq!(
4546 rotate(UDirection::Z, IVec3::new(0, i32::MIN, 0)),
4547 IVec3::new(i32::MIN, 0, 0)
4548 );
4549 }
4550
4551 #[test]
4552 fn layer_graph_materializes_scaled_cube_slice_on_requested_layer() {
4553 let mut graph = BlockGraph::new();
4554 graph.add_block(
4555 Block::new(IVec3::ZERO, BlockKind::Cube(CubeKind::ZXZ))
4556 .with_height("3d".parse().expect("valid height"))
4557 .unwrap(),
4558 );
4559 graph.add_block(Block::new(IVec3::new(0, 0, 3), BlockKind::Port));
4560 graph.add_pipe(Pipe::new(IVec3::new(0, 0, 2), Direction::ZPLUS));
4561
4562 let layer = graph.layer(2).into_graph();
4563 let block = layer
4564 .get_block(IVec3::new(0, 0, 2))
4565 .expect("scaled cube slice appears on occupied layer");
4566
4567 assert_eq!(block.height_cells(), 1);
4568 assert_eq!(
4569 layer.occupied_positions().collect::<Vec<_>>(),
4570 vec![IVec3::new(0, 0, 2), IVec3::new(0, 0, 3)]
4571 );
4572 assert!(layer.has_pipe_between(IVec3::new(0, 0, 2), IVec3::new(0, 0, 3)));
4573 }
4574
4575 #[test]
4576 fn extreme_layer_queries_do_not_overflow() {
4577 let mut graph = BlockGraph::new();
4578 graph.add_block(Block::new(IVec3::ZERO, BlockKind::Port));
4579 graph.add_block(Block::new(IVec3::Z, BlockKind::Port));
4580 graph.add_pipe(Pipe::new(IVec3::ZERO, Direction::ZPLUS));
4581
4582 assert_eq!(graph.layer(i32::MIN).boundary_pipes().count(), 0);
4583 assert_eq!(graph.layer(i32::MAX).boundary_pipes().count(), 0);
4584 }
4585
4586 #[test]
4587 fn boundary_pipe_basis_inference_does_not_wrap_perpendicular_neighbors() {
4588 let lower = IVec3::new(i32::MAX, 0, 0);
4589 let upper = IVec3::new(i32::MAX, 0, 1);
4590 let mut graph = BlockGraph::new();
4591 graph
4592 .try_add_block(Block::new(lower, BlockKind::Port))
4593 .unwrap();
4594 graph
4595 .try_add_block(Block::new(upper, BlockKind::Cube(CubeKind::ZXZ)))
4596 .unwrap();
4597 graph
4598 .try_add_pipe(Pipe::new(lower, Direction::ZPLUS))
4599 .unwrap();
4600
4601 graph.validate().expect("boundary pipe remains valid");
4602 let pipe = graph.pipes().next().unwrap();
4603 assert!(
4604 graph
4605 .infer_pipe_endpoint_face_bases(pipe, upper)
4606 .into_iter()
4607 .any(|basis| basis.is_some())
4608 );
4609
4610 let invalid = Pipe::new(lower, Direction::XPLUS);
4611 assert_eq!(graph.infer_pipe_basis(&invalid), [None; 3]);
4612 assert_eq!(
4613 graph.infer_pipe_basis_from_endpoint(&invalid, lower),
4614 [None; 3]
4615 );
4616 }
4617
4618 #[test]
4619 fn incident_pipes_follow_extended_block_ownership() {
4620 let mut graph = hand_built_layer_graph();
4621 let anchor = IVec3::new(4, 0, 0);
4622 graph.add_block(
4623 Block::new(anchor, BlockKind::Cube(CubeKind::ZXZ))
4624 .with_height("3d".parse().unwrap())
4625 .unwrap(),
4626 );
4627 graph.add_block(Block::new(
4628 anchor + 3 * IVec3::Z,
4629 BlockKind::Cube(CubeKind::ZXZ),
4630 ));
4631 graph.add_pipe(Pipe::new(anchor + 2 * IVec3::Z, Direction::ZPLUS));
4632 for position in graph.positions().chain([IVec3::new(99, 0, 0)]) {
4633 let mut expected: Vec<_> = graph
4634 .pipes()
4635 .filter(|pipe| {
4636 [pipe.src(), pipe.dst()].into_iter().any(|endpoint| {
4637 graph
4638 .get_endpoint_block(endpoint)
4639 .is_some_and(|block| block.pos() == position)
4640 })
4641 })
4642 .map(|pipe| (pipe.src().to_array(), pipe.dst().to_array()))
4643 .collect();
4644 let mut actual: Vec<_> = graph
4645 .pipes_at(position)
4646 .map(|pipe| (pipe.src().to_array(), pipe.dst().to_array()))
4647 .collect();
4648 expected.sort_unstable();
4649 actual.sort_unstable();
4650 assert_eq!(actual, expected);
4651 }
4652 }
4653
4654 #[test]
4655 fn spatial_port_inference_matches_first_live_pipe_scan() {
4656 let mut graph = BlockGraph::new();
4657 let port = IVec3::ZERO;
4658 graph.add_block(Block::new(port, BlockKind::Port));
4659 for position in [IVec3::X, -IVec3::X, IVec3::Z, 10 * IVec3::X, 11 * IVec3::X] {
4660 graph.add_block(Block::new(position, BlockKind::Cube(CubeKind::ZXZ)));
4661 }
4662 graph.add_block(Block::new(IVec3::Y, BlockKind::Cube(CubeKind::XZZ)));
4663 graph.add_pipe(Pipe::new(10 * IVec3::X, Direction::XPLUS));
4664 graph.add_pipe(Pipe::new(port, Direction::XPLUS).with_hadamard());
4665 graph.add_pipe(Pipe::new(port, Direction::ZPLUS));
4666 graph.add_pipe(Pipe::new(-IVec3::X, Direction::XPLUS));
4667 graph.add_pipe(Pipe::new(port, Direction::YPLUS));
4668
4669 let check = |graph: &BlockGraph, expected| {
4670 assert_eq!(graph.infer_spatial_port_cube_kind(port), expected);
4671 };
4672
4673 check(&graph, Some(CubeKind::XZX));
4674 graph.remove_pipe(port, IVec3::X).unwrap();
4675 check(&graph, None);
4676 graph.remove_pipe(port, IVec3::Z).unwrap();
4677 check(&graph, Some(CubeKind::ZXZ));
4678 graph.remove_pipe(port, -IVec3::X).unwrap();
4679 check(&graph, Some(CubeKind::XZZ));
4680 graph.remove_pipe(port, IVec3::Y).unwrap();
4681 check(&graph, None);
4682 }
4683
4684 #[test]
4685 fn layer_graph_projects_scaled_cube_spatial_pipe_to_requested_layer() {
4686 let mut graph = BlockGraph::new();
4687 graph.add_block(
4688 Block::new(IVec3::ZERO, BlockKind::Cube(CubeKind::ZXZ))
4689 .with_height("3d".parse().expect("valid height"))
4690 .unwrap(),
4691 );
4692 graph.add_block(
4693 Block::new(IVec3::X, BlockKind::Cube(CubeKind::ZXZ))
4694 .with_height("3d".parse().expect("valid height"))
4695 .unwrap(),
4696 );
4697 graph.add_pipe(Pipe::new(IVec3::ZERO, Direction::XPLUS));
4698
4699 let layer = graph.layer(2).into_graph();
4700
4701 assert!(layer.has_pipe_between(IVec3::new(0, 0, 2), IVec3::new(1, 0, 2)));
4702 }
4703
4704 fn hand_built_layer_graph() -> BlockGraph {
4705 let mut graph = BlockGraph::new();
4706 graph.add_block(Block::new(
4707 IVec3::new(0, 0, -1),
4708 BlockKind::Cube(CubeKind::ZXZ),
4709 ));
4710 graph.add_block(Block::new(
4711 IVec3::new(0, 0, 0),
4712 BlockKind::Cube(CubeKind::ZXZ),
4713 ));
4714 graph.add_block(Block::new(
4715 IVec3::new(1, 0, 0),
4716 BlockKind::Cube(CubeKind::ZXZ),
4717 ));
4718 graph.add_block(Block::new(
4719 IVec3::new(1, 0, 1),
4720 BlockKind::Cube(CubeKind::ZXZ),
4721 ));
4722 graph.add_pipe(Pipe::new(IVec3::new(0, 0, -1), Direction::ZPLUS));
4723 graph.add_pipe(Pipe::new(IVec3::new(0, 0, 0), Direction::XPLUS));
4724 graph.add_pipe(Pipe::new(IVec3::new(1, 0, 0), Direction::ZPLUS));
4725 graph
4726 }
4727
4728 fn block_signatures(graph: &BlockGraph) -> Vec<(IVec3, BlockKind)> {
4729 let mut blocks = graph
4730 .blocks()
4731 .map(|block| (block.pos, block.kind))
4732 .collect::<Vec<_>>();
4733 blocks.sort_by_key(|(pos, kind)| (pos.to_array(), *kind));
4734 blocks
4735 }
4736
4737 fn pipe_signatures(graph: &BlockGraph) -> Vec<(IVec3, IVec3, bool)> {
4738 let mut pipes = graph
4739 .block_pairs_with_pipe()
4740 .map(|(_, _, pipe)| {
4741 let (src, dst) = pipe.endpoints();
4742 let (src, dst) = if src.to_array() <= dst.to_array() {
4743 (src, dst)
4744 } else {
4745 (dst, src)
4746 };
4747 (src, dst, pipe.hadamard)
4748 })
4749 .collect::<Vec<_>>();
4750 pipes.sort_by_key(|(src, dst, hadamard)| (src.to_array(), dst.to_array(), *hadamard));
4751 pipes
4752 }
4753
4754 #[test]
4755 fn test_selective_resolution_uses_selected_basis() {
4756 let graph = BlockGraph::new();
4757 for (basis, expected) in [
4758 (PauliBasis::X, BlockKind::Measurement(Basis::X)),
4759 (PauliBasis::Y, BlockKind::Y),
4760 (PauliBasis::Z, BlockKind::Measurement(Basis::Z)),
4761 ] {
4762 assert_eq!(graph.resolved_selective_kind_for_basis(basis), expected);
4763 }
4764 }
4765
4766 #[test]
4767 fn fixed_measurement_actions_use_the_declared_basis() {
4768 for basis in [Basis::X, Basis::Z] {
4769 let mut graph = BlockGraph::new();
4770 graph.add_block(Block::new(IVec3::ZERO, BlockKind::Measurement(basis)));
4771
4772 assert_eq!(
4773 graph
4774 .resolve_measurement_observable(&MeasureTarget::Node(IVec3::ZERO))
4775 .unwrap(),
4776 MeasurementObservable::Concrete(basis.into()),
4777 );
4778 }
4779 }
4780
4781 #[test]
4782 fn rotate_about_origin_lenient_rotates_graph_and_actions() {
4783 let mut graph = BlockGraph::from_blog_text(
4784 "BLOG 1.0\n\n 0: ZXZ [0, 0, 0]\n 1: ZXZ [0, 0, 1]\n [0, 0, 0] -> +Z\n",
4785 )
4786 .expect("inline test graph is valid");
4787 graph
4788 .set_actions_lenient(vec![Action::Measure {
4789 target: MeasureTarget::Node(IVec3::Z),
4790 name: "m0".to_string(),
4791 }])
4792 .unwrap();
4793
4794 let rotated = graph
4795 .rotate_about_origin_lenient(UDirection::X, 1)
4796 .expect("rotation succeeds");
4797
4798 assert!(rotated.has_block_at(IVec3::new(0, 0, 0)));
4799 assert!(rotated.has_block_at(IVec3::new(0, -1, 0)));
4800 assert!(rotated.has_pipe_between(IVec3::new(0, 0, 0), IVec3::new(0, -1, 0)));
4801 assert_eq!(
4802 rotated
4803 .get_block(IVec3::new(0, 0, 0))
4804 .map(|block| block.kind),
4805 Some(BlockKind::Cube(CubeKind::ZZX))
4806 );
4807 assert_eq!(
4808 rotated
4809 .get_block(IVec3::new(0, -1, 0))
4810 .map(|block| block.kind),
4811 Some(BlockKind::Cube(CubeKind::ZZX))
4812 );
4813 assert_eq!(
4814 rotated.actions(),
4815 &[Action::Measure {
4816 target: MeasureTarget::Node(IVec3::NEG_Y),
4817 name: "m0".to_string(),
4818 }]
4819 );
4820 }
4821
4822 #[test]
4823 fn rotate_about_origin_rotates_walking_boundary_around_time_axis() {
4824 let graph = GalleryItem::GHZSlideThenGlide
4825 .build()
4826 .materialize_root_graph()
4827 .expect("gallery flat projection");
4828
4829 let rotated = graph
4830 .rotate_about_origin(UDirection::Z, 1)
4831 .expect("walking gallery should rotate around time axis");
4832
4833 assert_eq!(
4834 rotated
4835 .get_block(IVec3::new(0, 0, 1))
4836 .map(|block| block.kind),
4837 Some(BlockKind::Walking(
4838 WalkingKind::new(WalkingBoundaryKind::XZZ, glam::ivec2(0, 1)).unwrap()
4839 ))
4840 );
4841 assert!(rotated.has_pipe_between(IVec3::new(0, 0, 0), IVec3::new(0, 0, 1)));
4842 }
4843
4844 #[test]
4845 fn rotate_about_origin_rotates_patch_rotation_construction_basis() {
4846 let kind = PatchRotationKind::new(Basis::X, glam::ivec2(0, 1)).unwrap();
4847 let mut graph = BlockGraph::new();
4848 graph.add_block(Block::new(IVec3::ZERO, BlockKind::PatchRotation(kind)));
4849 graph.add_block(Block::new(IVec3::NEG_Z, BlockKind::Port));
4850 graph.add_block(Block::new(IVec3::new(0, 1, 2), BlockKind::Port));
4851 graph.add_pipe(Pipe::new(IVec3::ZERO, Direction::ZMINUS));
4852 graph.add_pipe(Pipe::new(kind.end_position(IVec3::ZERO), Direction::ZPLUS));
4853
4854 let rotated = graph
4855 .rotate_about_origin(UDirection::Z, 1)
4856 .expect("patch rotation should rotate around time axis");
4857
4858 let rotated_kind = match rotated
4859 .get_block(IVec3::ZERO)
4860 .expect("rotated patch rotation remains at origin")
4861 .kind()
4862 {
4863 BlockKind::PatchRotation(rotated_kind) => rotated_kind,
4864 other => panic!("expected patch rotation, got {other:?}"),
4865 };
4866 assert_eq!(rotated_kind.movement(), glam::ivec2(-1, 0));
4867 assert_eq!(rotated_kind.basis(), kind.basis().flip());
4868 }
4869
4870 #[test]
4871 fn rotate_about_origin_requires_half_turns_for_y_graphs() {
4872 let graph = GalleryItem::YMemory
4873 .build()
4874 .materialize_root_graph()
4875 .expect("gallery flat projection");
4876
4877 assert!(matches!(
4878 graph.rotate_about_origin(UDirection::X, 1),
4879 Err(BlockGraphError::RotationRequiresHalfTurns)
4880 ));
4881 graph.rotate_about_origin(UDirection::X, 2).unwrap();
4882 }
4883
4884 #[test]
4885 fn rotate_about_origin_only_rotates_dynamic_graphs_around_time() {
4886 let graph = GalleryItem::T
4887 .build()
4888 .materialize_root_graph()
4889 .expect("gallery flat projection");
4890
4891 graph.rotate_about_origin(UDirection::Z, 1).unwrap();
4892 assert!(matches!(
4893 graph.rotate_about_origin(UDirection::X, 1),
4894 Err(BlockGraphError::RotationUnsupportedDynamicBlocks)
4895 ));
4896 }
4897
4898 #[test]
4900 fn flip_xz_basis_preserves_metadata_and_flips_selective_basis() {
4901 let graph = BlockGraph::from_blog_text(
4902 "BLOG 1.0\n\n 0: Port [0, 0, 0] <source>\n\
4903 1: XY [0, 0, 1] <selective>\n\
4904 2: ZXZ [2, 0, 0]\n\
4905 3: ZXZ [2, 0, 1]\n\
4906 [0, 0, 0] -H> +Z <link>\n\
4907 [2, 0, 0] -> +Z\n\n m0 = measure 3\n resolve 1 if m0\n",
4908 )
4909 .unwrap();
4910
4911 let flipped = graph.flip_xz_basis().expect("basis flip succeeds");
4912
4913 assert_eq!(
4914 flipped
4915 .get_block(IVec3::new(0, 0, 0))
4916 .map(|block| (&block.kind, block.tag.as_str(),)),
4917 Some((&BlockKind::Port, "source"))
4918 );
4919 assert_eq!(
4920 flipped
4921 .get_block(IVec3::new(0, 0, 1))
4922 .map(|block| (&block.kind, block.tag.as_str(),)),
4923 Some((&BlockKind::Selective(SelectiveKind::YZ), "selective",))
4924 );
4925 assert_eq!(
4926 flipped
4927 .get_pipe(IVec3::new(0, 0, 0), IVec3::new(0, 0, 1))
4928 .map(|pipe| (pipe.hadamard, pipe.tag.as_str())),
4929 Some((true, "link"))
4930 );
4931 let mut expected_actions = graph.actions();
4932 if let Action::Resolve { condition, .. } = &mut expected_actions[1] {
4933 *condition = condition.clone().negated();
4934 }
4935 expected_actions.push(Action::Feedback {
4936 targets: vec![crate::FeedbackTarget {
4937 pauli: PauliBasis::Z,
4938 target: IVec3::new(0, 0, 1),
4939 direction: None,
4940 }],
4941 condition: None,
4942 });
4943 assert_eq!(flipped.actions(), expected_actions);
4944
4945 let roundtrip = flipped.flip_xz_basis().expect("second flip succeeds");
4946 assert_eq!(roundtrip.actions(), graph.actions());
4947 assert_eq!(
4948 roundtrip
4949 .get_block(IVec3::new(0, 0, 1))
4950 .map(|block| block.kind),
4951 Some(BlockKind::Selective(SelectiveKind::XY))
4952 );
4953 }
4954
4955 #[test]
4956 fn basis_flip_swaps_both_selective_choices_but_keeps_branch_conditions() {
4957 use crate::Expr;
4958 for kind in [SelectiveKind::XY, SelectiveKind::XZ, SelectiveKind::YZ] {
4959 let mut graph = BlockGraph::new();
4960 graph.add_block(Block::new(IVec3::ZERO, BlockKind::Selective(kind)));
4961 let actions = vec![
4962 Action::Resolve {
4963 target: IVec3::ZERO,
4964 condition: Expr::Var("control".into()),
4965 },
4966 Action::Branch {
4967 target: IVec3::X,
4968 condition: Expr::Var("control".into()),
4969 },
4970 ];
4971 graph.set_actions_lenient(actions.clone()).unwrap();
4972 let flipped = graph.flip_xz_basis_lenient().unwrap();
4973 assert_eq!(flipped.actions()[1], actions[1]);
4974 assert_eq!(
4975 flipped.actions()[0],
4976 Action::Resolve {
4977 target: IVec3::ZERO,
4978 condition: Expr::Not(Box::new(Expr::Var("control".into())))
4979 }
4980 );
4981 let BlockKind::Selective(flipped_kind) = flipped.get_block(IVec3::ZERO).unwrap().kind
4982 else {
4983 unreachable!()
4984 };
4985 for control in [false, true] {
4986 let before = if control {
4987 kind.pauli_if_true()
4988 } else {
4989 kind.pauli_if_false()
4990 };
4991 let after = if !control {
4992 flipped_kind.pauli_if_true()
4993 } else {
4994 flipped_kind.pauli_if_false()
4995 };
4996 assert_eq!(
4997 after,
4998 match before {
4999 PauliBasis::X => PauliBasis::Z,
5000 PauliBasis::Y => PauliBasis::Y,
5001 PauliBasis::Z => PauliBasis::X,
5002 }
5003 );
5004 }
5005 assert_eq!(flipped.flip_xz_basis_lenient().unwrap().actions(), actions);
5006 }
5007 }
5008
5009 #[test]
5010 fn lenient_basis_flip_defers_action_analysis() {
5011 let graph = GalleryItem::T
5012 .build()
5013 .materialize_root_graph()
5014 .expect("gallery flat projection");
5015 let flipped = graph.flip_xz_basis_lenient().unwrap();
5016
5017 assert_eq!(
5018 flipped.flip_xz_basis_lenient().unwrap().actions(),
5019 graph.actions()
5020 );
5021 assert!(!flipped.action_graph().is_analyzed());
5022 }
5023
5024 #[test]
5025 fn flip_xz_basis_rebuilds_measurement_metadata_from_flipped_structure() {
5026 use glam::ivec3;
5027
5028 let graph = BlockGraph::from_blog_text(
5029 "BLOG 1.0\n\n 0: ZXZ [0, 0, 0]\n 1: ZXZ [0, 0, 1]\n [0, 0, 0] -> +Z\n\n m0 = measure 1\n",
5030 )
5031 .unwrap();
5032
5033 let flipped = graph.flip_xz_basis().expect("basis flip succeeds");
5034
5035 assert_eq!(
5036 flipped.get_block(ivec3(0, 0, 0)).map(|block| block.kind),
5037 Some(BlockKind::Cube(CubeKind::XZX))
5038 );
5039 assert_eq!(
5040 flipped
5041 .action_graph()
5042 .node_by_ordinal(0)
5043 .unwrap()
5044 .measurement,
5045 Some(MeasurementObservable::Concrete(crate::PauliBasis::X))
5046 );
5047 }
5048
5049 #[test]
5050 fn flip_xz_basis_handles_walking_end_pipe_without_zx_roundtrip_adjacency_error() {
5051 let graph = BlockGraph::from_blog_text(
5052 "BLOG 1.0\n\n\
5053 0: XZZ [0, 0, 0]\n\
5054 1: walk XZX [0, 0, 1] -> [1, 0, 2]\n\
5055 2: Port [1, 0, 3]\n\
5056 [0, 0, 0] -> +Z\n\
5057 [1, 0, 2] -> +Z\n",
5058 )
5059 .expect("walking graph should parse");
5060
5061 let flipped = graph.flip_xz_basis().expect("basis flip succeeds");
5062
5063 assert_eq!(
5064 flipped
5065 .get_block(IVec3::new(0, 0, 1))
5066 .map(|block| block.kind),
5067 Some(BlockKind::Walking(
5068 WalkingKind::new(WalkingBoundaryKind::ZXZ, glam::ivec2(1, 0)).unwrap()
5069 ))
5070 );
5071 assert!(flipped.has_pipe_between(IVec3::new(1, 0, 2), IVec3::new(1, 0, 3)));
5072 }
5073
5074 #[test]
5075 fn spans_cover_each_axis_inclusively() {
5076 let mut graph = BlockGraph::new();
5077 graph.add_block(Block::new(IVec3::new(-2, 3, 5), BlockKind::Port));
5078 graph.add_block(Block::new(
5079 IVec3::new(1, -1, 8),
5080 BlockKind::Cube(CubeKind::ZXZ),
5081 ));
5082 graph.add_block(Block::new(
5083 IVec3::new(0, 2, 4),
5084 BlockKind::Cube(CubeKind::XZX),
5085 ));
5086
5087 let (x_span, y_span, z_span) = graph.spans().expect("non-empty graph has spans");
5088
5089 assert_eq!(x_span, -2..=1);
5090 assert_eq!(y_span, -1..=3);
5091 assert_eq!(z_span, 4..=8);
5092
5093 graph
5094 .set_cube_height(IVec3::new(1, -1, 8), "65535d".parse().unwrap())
5095 .unwrap();
5096 let spans = graph.spans().unwrap();
5097 assert_eq!(spans, (-2..=1, -1..=3, 4..=65_542));
5098 }
5099}