1use super::graph::{CsrAdjacency, ZXEdge, ZXNode};
4use super::{NodeKind, ZXError, ZXGraph};
5use crate::{Block, BlockGraph, BlockKind, CubeKind, Direction, Pipe};
6use bloq_utils::{Basis, UDirection};
7use glam::IVec3;
8use std::collections::{HashMap, HashSet};
9
10#[derive(Debug, Clone, Copy)]
11pub(crate) struct ModuleSeam {
12 pub block: IVec3,
13 pub direction: Direction,
14}
15
16impl TryFrom<&BlockGraph> for ZXGraph {
17 type Error = ZXError;
18
19 fn try_from(graph: &BlockGraph) -> Result<Self, Self::Error> {
20 graph
21 .require_flat_hierarchy("ZX conversion")
22 .map_err(|error| ZXError::Graph(Box::new(error)))?;
23 let graph = graph.copy_local_geometry().fix_shadowed_faces();
24 if let Some(target) = graph.action_graph().ordered_nodes().find_map(|node| {
25 if let crate::Action::Branch { target, .. } = node.action {
26 Some(target)
27 } else {
28 None
29 }
30 }) {
31 return Err(ZXError::Graph(Box::new(
32 crate::InvalidActionError::MissingBranchValue(target).into(),
33 )));
34 }
35 let actions = crate::validate::validate_source(&graph)
36 .map_err(|error| ZXError::Graph(Box::new(error)))?;
37 Self::from_block_graph_parts(&graph, actions, &[]).map(|(graph, _)| graph)
38 }
39}
40
41impl ZXGraph {
42 #[doc(hidden)]
45 pub fn from_block_graph_for_analysis(graph: &BlockGraph) -> Result<Self, ZXError> {
46 graph
47 .require_flat_hierarchy("ZX conversion")
48 .map_err(|error| ZXError::Graph(Box::new(error)))?;
49 let graph = graph.copy_local_geometry().fix_shadowed_faces();
50 crate::validate::validate_structure(&graph)
51 .map_err(|error| ZXError::Graph(Box::new(error)))?;
52 let action_graph = graph
53 .build_action_graph(&graph.actions())
54 .map_err(|error| ZXError::Graph(Box::new(error)))?;
55 Self::from_block_graph_parts(&graph, action_graph, &[]).map(|(graph, _)| graph)
56 }
57
58 pub(crate) fn from_module_body(
59 graph: &BlockGraph,
60 seams: &[ModuleSeam],
61 ) -> Result<(Self, Vec<usize>), ZXError> {
62 let actions = graph.actions();
63 let action_graph = if actions.iter().any(|action| {
64 !matches!(
65 action,
66 crate::Action::Let { .. } | crate::Action::DiscardIf(_)
67 )
68 }) {
69 graph
71 .build_action_graph(&actions)
72 .map_err(|error| ZXError::Graph(Box::new(error)))?
73 } else {
74 crate::ActionDag::default()
76 };
77 Self::from_block_graph_parts(graph, action_graph, seams)
78 }
79
80 pub(crate) fn from_local_block_neighborhood(
90 graph: &BlockGraph,
91 center: IVec3,
92 ) -> Result<Self, ZXError> {
93 let center = graph.get_block(center).ok_or_else(|| {
94 ZXError::Graph(Box::new(crate::BlockGraphError::BlockNotFound(center)))
95 })?;
96 let mut blocks = graph.neighbors(center.pos);
97 blocks.push(center);
98 blocks.sort_unstable_by_key(|block| block.pos.to_array());
99 blocks.dedup_by_key(|block| block.pos);
100 let nodes = blocks
101 .iter()
102 .enumerate()
103 .map(|(id, block)| {
104 ZXNode::new(id, block.pos, block_to_node_kind(block))
105 .with_role(block.port_role().unwrap_or_default())
106 })
107 .collect::<Vec<_>>();
108 let pos_to_node = nodes
109 .iter()
110 .map(|node| (node.pos, node.id))
111 .collect::<crate::FxHashMap<_, _>>();
112 let mut pipes = graph
113 .pipes_at(center.pos)
114 .map(|pipe| {
115 let source = graph
116 .get_endpoint_block(pipe.src)
117 .expect("validated pipe source has an owner");
118 let target = graph
119 .get_endpoint_block(pipe.dst())
120 .expect("validated pipe target has an owner");
121 let (a, b) = (pos_to_node[&source.pos], pos_to_node[&target.pos]);
122 (a.min(b), a.max(b), pipe.hadamard)
123 })
124 .collect::<Vec<_>>();
125 pipes.sort_unstable_by_key(|&(a, b, _)| (a, b));
126 let mut total_ids = nodes.len();
127 let mut edges = Vec::with_capacity(2 * pipes.len());
128 let mut directed_edges = Vec::with_capacity(2 * pipes.len());
129 for (a, b, hadamard) in pipes {
130 push_edge_pair(
131 a,
132 b,
133 hadamard,
134 &mut total_ids,
135 &mut edges,
136 &mut directed_edges,
137 );
138 }
139 let adjacency = CsrAdjacency::from_edges(nodes.len(), &directed_edges);
140 Ok(Self {
141 nodes,
142 edges,
143 adjacency,
144 pos_to_node,
145 action_graph: crate::ActionDag::default(),
146 total_ids,
147 cross_incident: Default::default(),
148 stabilizer_phase_basis: Default::default(),
149 })
150 }
151
152 pub(crate) fn into_connector_components(self) -> Vec<(Self, Vec<usize>)> {
155 debug_assert_eq!(self.action_graph.ordered_nodes().count(), 0);
156 let mut owners = vec![usize::MAX; self.nodes.len()];
157 let mut components = Vec::<Vec<usize>>::new();
158 for start in 0..self.nodes.len() {
159 if owners[start] != usize::MAX {
160 continue;
161 }
162 let owner = components.len();
163 let mut nodes = vec![start];
164 owners[start] = owner;
165 let mut scan = 0;
166 while scan < nodes.len() {
167 for &neighbor in self.neighbors(nodes[scan]).unwrap_or_default() {
168 if owners[neighbor] == usize::MAX {
169 owners[neighbor] = owner;
170 nodes.push(neighbor);
171 }
172 }
173 scan += 1;
174 }
175 nodes.sort_unstable();
176 components.push(nodes);
177 }
178 if components.len() <= 1 {
179 return components
180 .pop()
181 .map(|nodes| vec![(self, nodes)])
182 .unwrap_or_default();
183 }
184 let mut edge_groups = vec![Vec::new(); components.len()];
185 for edge in self.edges {
186 edge_groups[owners[edge.n1]].push(edge);
187 }
188 let mut local_ids = vec![0; self.nodes.len()];
189 components
190 .into_iter()
191 .zip(edge_groups)
192 .map(|(original_ids, edges)| {
193 let nodes = original_ids
194 .iter()
195 .enumerate()
196 .map(|(id, &original)| {
197 local_ids[original] = id;
198 ZXNode {
199 id,
200 ..self.nodes[original]
201 }
202 })
203 .collect::<Vec<_>>();
204 let edges = edges
205 .into_iter()
206 .enumerate()
207 .map(|(id, edge)| ZXEdge {
208 id: nodes.len() + id,
209 n1: local_ids[edge.n1],
210 n2: local_ids[edge.n2],
211 ..edge
212 })
213 .collect::<Vec<_>>();
214 let adjacency = CsrAdjacency::from_edges(
215 nodes.len(),
216 &edges.iter().map(|e| (e.n1, e.n2, e.id)).collect::<Vec<_>>(),
217 );
218 let pos_to_node = original_ids
219 .iter()
220 .filter_map(|&original| {
221 let pos = self.nodes[original].pos;
222 (self.pos_to_node.get(&pos) == Some(&original))
223 .then_some((pos, local_ids[original]))
224 })
225 .collect();
226 let total_ids = nodes.len() + edges.len();
227 (
228 Self {
229 nodes,
230 edges,
231 adjacency,
232 pos_to_node,
233 action_graph: crate::ActionDag::default(),
234 total_ids,
235 cross_incident: Default::default(),
236 stabilizer_phase_basis: Default::default(),
237 },
238 original_ids,
239 )
240 })
241 .collect()
242 }
243
244 fn from_block_graph_parts(
245 graph: &BlockGraph,
246 mut action_graph: crate::ActionDag,
247 seams: &[ModuleSeam],
248 ) -> Result<(Self, Vec<usize>), ZXError> {
249 graph
250 .require_flat_hierarchy("ZX conversion")
251 .map_err(|error| ZXError::Graph(Box::new(error)))?;
252 let mut nodes = Vec::with_capacity(graph.block_count() + seams.len());
253 let mut edges = Vec::with_capacity(2 * (graph.pipe_count() + seams.len()));
254 let mut directed_edges = Vec::with_capacity(2 * (graph.pipe_count() + seams.len()));
255 let mut endpoint_to_node: HashMap<IVec3, usize> =
256 HashMap::with_capacity(graph.block_count());
257 let mut id = 0;
258
259 let mut blocks = graph.blocks().collect::<Vec<_>>();
261 blocks.sort_unstable_by_key(|block| block.pos.to_array());
262 for block in blocks {
263 let node = ZXNode::new(id, block.pos, block_to_node_kind(block))
264 .with_role(block.port_role().unwrap_or_default());
265 for endpoint in block.connectable_offsets() {
266 endpoint_to_node.insert(block.pos + endpoint, id);
267 }
268 nodes.push(node);
269 id += 1;
270 }
271
272 for ordinal in 0..action_graph.ordered_nodes().count() {
275 let action = action_graph
276 .node_by_ordinal(ordinal)
277 .expect("ordered action exists");
278 let crate::Action::Measure {
279 target: crate::MeasureTarget::Edge { src, dir },
280 ..
281 } = action.action
282 else {
283 continue;
284 };
285 let pipe = graph
286 .get_pipe(src, src + dir.to_ivec3())
287 .expect("validated measurement pipe");
288 if pipe.hadamard && endpoint_to_node[&pipe.src] > endpoint_to_node[&pipe.dst()] {
289 let Some(crate::MeasurementObservable::Concrete(basis)) = action.measurement else {
290 continue;
291 };
292 action_graph
293 .set_measurement_observable(
294 ordinal,
295 crate::MeasurementObservable::Concrete(match basis {
296 crate::PauliBasis::X => crate::PauliBasis::Z,
297 crate::PauliBasis::Z => crate::PauliBasis::X,
298 crate::PauliBasis::Y => crate::PauliBasis::Y,
299 }),
300 )
301 .expect("measurement ordinal exists");
302 }
303 }
304
305 let original_node_count = nodes.len();
306 let mut seam_nodes = Vec::with_capacity(seams.len());
307 let mut seam_edges = Vec::with_capacity(seams.len());
308 for seam in seams {
309 let position = crate::checked_add_position(seam.block, seam.direction.to_ivec3())
310 .map_err(|error| ZXError::Graph(Box::new(error)))?;
311 let target = endpoint_to_node.get(&seam.block).copied().ok_or_else(|| {
312 ZXError::Graph(Box::new(crate::BlockGraphError::BlockNotFound(seam.block)))
313 })?;
314 nodes.push(ZXNode::new(id, position, NodeKind::Port));
315 seam_nodes.push(id);
316 seam_edges.push((target, id));
317 id += 1;
318 }
319
320 let mut pipes = graph.pipes().collect::<Vec<_>>();
321 pipes.sort_unstable_by_key(|pipe| {
322 let first = endpoint_to_node[&pipe.src];
323 let second = endpoint_to_node[&pipe.dst()];
324 (first.min(second), first.max(second))
325 });
326 for pipe in pipes {
327 push_edge_pair(
328 endpoint_to_node[&pipe.src],
329 endpoint_to_node[&pipe.dst()],
330 pipe.hadamard,
331 &mut id,
332 &mut edges,
333 &mut directed_edges,
334 );
335 }
336 for (target, boundary) in seam_edges {
337 push_edge_pair(
338 target,
339 boundary,
340 false,
341 &mut id,
342 &mut edges,
343 &mut directed_edges,
344 );
345 }
346 let adjacency = CsrAdjacency::from_edges(nodes.len(), &directed_edges);
347
348 let pos_to_node = nodes[..original_node_count]
351 .iter()
352 .map(|node| (node.pos, node.id))
353 .collect();
354
355 Ok((
356 ZXGraph {
357 nodes,
358 edges,
359 adjacency,
360 pos_to_node,
361 action_graph,
362 total_ids: id,
363 cross_incident: Default::default(),
364 stabilizer_phase_basis: Default::default(),
365 },
366 seam_nodes,
367 ))
368 }
369}
370
371fn push_edge_pair(
372 mut n1: usize,
373 mut n2: usize,
374 hadamard: bool,
375 id: &mut usize,
376 edges: &mut Vec<ZXEdge>,
377 directed_edges: &mut Vec<(usize, usize, usize)>,
378) {
379 if n1 > n2 {
380 std::mem::swap(&mut n1, &mut n2);
381 }
382 edges.push(ZXEdge {
383 n1,
384 n2,
385 id: *id,
386 hadamard,
387 });
388 directed_edges.push((n1, n2, *id));
389 *id += 1;
390 edges.push(ZXEdge {
391 n1: n2,
392 n2: n1,
393 id: *id,
394 hadamard,
395 });
396 directed_edges.push((n2, n1, *id));
397 *id += 1;
398}
399
400impl TryFrom<&ZXGraph> for BlockGraph {
401 type Error = ZXError;
402
403 fn try_from(value: &ZXGraph) -> Result<Self, Self::Error> {
404 value.to_block_graph()
405 }
406}
407
408impl ZXGraph {
409 fn to_block_graph(&self) -> Result<BlockGraph, ZXError> {
413 self.validate_positioned_graph()?;
414
415 let mut nodes_to_handle: HashSet<usize> = self.nodes.iter().map(|node| node.id).collect();
416 let mut edges_to_handle: HashSet<(usize, usize)> =
417 self.sorted_undirected_edges().into_iter().collect();
418 let mut graph = BlockGraph::new();
419
420 self.handle_corner_nodes(&mut graph, &mut nodes_to_handle);
421 self.handle_special_nodes_and_pipes(
422 &mut graph,
423 &mut nodes_to_handle,
424 &mut edges_to_handle,
425 )?;
426 self.greedily_construct_blocks(&mut graph, &mut nodes_to_handle, &mut edges_to_handle)?;
427 self.handle_leftover_nodes(&mut graph, &mut nodes_to_handle, &mut edges_to_handle)?;
428
429 graph
430 .set_actions(self.actions())
431 .map_err(|error| ZXError::Graph(Box::new(error)))?;
432 graph
433 .validate()
434 .map_err(|error| ZXError::Graph(Box::new(error)))?;
435 Ok(graph)
436 }
437
438 fn validate_positioned_graph(&self) -> Result<(), ZXError> {
439 for (n1, n2) in self.sorted_undirected_edges() {
440 self.direction_between(n1, n2)?;
441 }
442
443 for node in &self.nodes {
444 let degree = self.neighbor_ids(node.id).len();
445 if self.axis_directions(node.id).len() == 3 {
446 return Err(ZXError::ThreeDimensionalCorner { pos: node.pos });
447 }
448 match node.kind {
449 NodeKind::Y | NodeKind::T | NodeKind::Selective(_) => {
450 if degree > 1 {
451 return Err(ZXError::SpecialNodeNotDangling {
452 pos: node.pos,
453 kind: node.kind,
454 degree,
455 });
456 }
457 if let Some(&neighbor) = self.neighbor_ids(node.id).first() {
458 let direction = self.direction_between(node.id, neighbor)?;
459 if direction.as_udirection() != UDirection::Z {
460 return Err(ZXError::SpecialNodeNotTimeLike {
461 pos: node.pos,
462 kind: node.kind,
463 });
464 }
465 if node.kind == NodeKind::T && direction != Direction::ZPLUS {
466 return Err(ZXError::TNodeNotFutureDirected { pos: node.pos });
467 }
468 }
469 }
470 NodeKind::Port => {
471 if degree > 1 {
472 return Err(ZXError::SpecialNodeNotDangling {
473 pos: node.pos,
474 kind: node.kind,
475 degree,
476 });
477 }
478 }
479 NodeKind::X | NodeKind::Z => {}
480 }
481 }
482 Ok(())
483 }
484
485 fn handle_corner_nodes(&self, graph: &mut BlockGraph, nodes_to_handle: &mut HashSet<usize>) {
486 for node in &self.nodes {
487 if !is_zx_spider(node.kind) || !nodes_to_handle.contains(&node.id) {
488 continue;
489 }
490 let directions = self.axis_directions(node.id);
491 if directions.len() != 2 {
492 continue;
493 }
494 let normal_direction = UDirection::iter()
495 .find(|direction| !directions.contains(direction))
496 .expect("one axis is always missing when exactly two are present");
497 let normal_basis = spider_normal_basis(node.kind);
498 let mut bases = [normal_basis.flip(); 3];
499 bases[normal_direction.index()] = normal_basis;
500 let cube_kind =
501 CubeKind::try_from(bases).expect("corner inference always produces a valid cube");
502 graph.add_block(self.cube_block(node.id, cube_kind));
503 nodes_to_handle.remove(&node.id);
504 }
505 }
506
507 fn handle_special_nodes_and_pipes(
508 &self,
509 graph: &mut BlockGraph,
510 nodes_to_handle: &mut HashSet<usize>,
511 edges_to_handle: &mut HashSet<(usize, usize)>,
512 ) -> Result<(), ZXError> {
513 for node in &self.nodes {
514 if is_zx_spider(node.kind) || !nodes_to_handle.contains(&node.id) {
515 continue;
516 }
517 if self.neighbor_ids(node.id).is_empty() {
518 graph.add_block(self.special_block(node.id));
519 nodes_to_handle.remove(&node.id);
520 }
521 }
522
523 for (n1, n2) in self.sorted_undirected_edges() {
524 if !edges_to_handle.contains(&(n1, n2)) {
525 continue;
526 }
527 let node1 = self.nodes[n1];
528 let node2 = self.nodes[n2];
529 if is_zx_spider(node1.kind) || is_zx_spider(node2.kind) {
530 continue;
531 }
532 if nodes_to_handle.remove(&n1) {
533 graph.add_block(self.special_block(n1));
534 }
535 if nodes_to_handle.remove(&n2) {
536 graph.add_block(self.special_block(n2));
537 }
538 self.add_pipe_between_nodes(graph, n1, n2)?;
539 edges_to_handle.remove(&(n1, n2));
540 }
541 Ok(())
542 }
543
544 fn greedily_construct_blocks(
545 &self,
546 graph: &mut BlockGraph,
547 nodes_to_handle: &mut HashSet<usize>,
548 edges_to_handle: &mut HashSet<(usize, usize)>,
549 ) -> Result<(), ZXError> {
550 let mut previous_nodes_left = nodes_to_handle.len() + 1;
551 while nodes_to_handle.len() < previous_nodes_left {
552 previous_nodes_left = nodes_to_handle.len();
553 self.try_handle_edges(graph, nodes_to_handle, edges_to_handle)?;
554 }
555 Ok(())
556 }
557
558 fn try_handle_edges(
559 &self,
560 graph: &mut BlockGraph,
561 nodes_to_handle: &mut HashSet<usize>,
562 edges_to_handle: &mut HashSet<(usize, usize)>,
563 ) -> Result<(), ZXError> {
564 for (u, v) in self.sorted_undirected_edges() {
565 if !edges_to_handle.contains(&(u, v)) {
566 continue;
567 }
568 let can_infer_from_u =
569 !nodes_to_handle.contains(&u) && is_zx_spider(self.nodes[u].kind);
570 let can_infer_from_v =
571 !nodes_to_handle.contains(&v) && is_zx_spider(self.nodes[v].kind);
572 if !can_infer_from_u && !can_infer_from_v {
573 continue;
574 }
575
576 let (infer_from, other_node) = if can_infer_from_u { (u, v) } else { (v, u) };
577 let infer_cube_kind = match graph
578 .get_block(self.nodes[infer_from].pos)
579 .map(|block| block.kind)
580 {
581 Some(BlockKind::Cube(kind)) => kind,
582 _ => continue,
583 };
584
585 if is_zx_spider(self.nodes[other_node].kind) {
586 let inferred_kind =
587 self.infer_cube_kind_from_neighbor(infer_from, other_node, infer_cube_kind)?;
588 if nodes_to_handle.remove(&other_node) {
589 graph.add_block(self.cube_block(other_node, inferred_kind));
590 } else if let Some(Block {
591 kind: BlockKind::Cube(existing_kind),
592 ..
593 }) = graph.get_block(self.nodes[other_node].pos)
594 && *existing_kind != inferred_kind
595 {
596 return Err(ZXError::ConflictingCubeKinds {
597 pos: self.nodes[other_node].pos,
598 existing: *existing_kind,
599 inferred: inferred_kind,
600 });
601 }
602 } else if nodes_to_handle.remove(&other_node) {
603 graph.add_block(self.special_block(other_node));
604 }
605
606 self.add_pipe_between_nodes(graph, infer_from, other_node)?;
607 edges_to_handle.remove(&(u, v));
608 }
609 Ok(())
610 }
611
612 fn handle_leftover_nodes(
613 &self,
614 graph: &mut BlockGraph,
615 nodes_to_handle: &mut HashSet<usize>,
616 edges_to_handle: &mut HashSet<(usize, usize)>,
617 ) -> Result<(), ZXError> {
618 while nodes_to_handle
619 .iter()
620 .any(|node_id| is_zx_spider(self.nodes[*node_id].kind))
621 {
622 self.fix_kind_for_one_node(graph, nodes_to_handle)?;
623 self.greedily_construct_blocks(graph, nodes_to_handle, edges_to_handle)?;
624 }
625
626 for node_id in nodes_to_handle.drain() {
627 graph.add_block(self.special_block(node_id));
628 }
629 Ok(())
630 }
631
632 fn fix_kind_for_one_node(
633 &self,
634 graph: &mut BlockGraph,
635 nodes_to_handle: &mut HashSet<usize>,
636 ) -> Result<(), ZXError> {
637 let fix_node = nodes_to_handle
638 .iter()
639 .filter(|node_id| is_zx_spider(self.nodes[**node_id].kind))
640 .min_by_key(|node_id| self.nodes[**node_id].pos.to_array())
641 .copied()
642 .expect("caller guarantees there is an unresolved X/Z node");
643 let fix_kind = if self.neighbor_ids(fix_node).is_empty() {
644 match self.nodes[fix_node].kind {
645 NodeKind::X => CubeKind::ZXZ,
646 NodeKind::Z => CubeKind::ZXX,
647 _ => unreachable!("only X/Z nodes reach the arbitrary fix path"),
648 }
649 } else {
650 let neighbor = self.neighbor_ids(fix_node)[0];
651 let direction = self
652 .direction_between(fix_node, neighbor)?
653 .as_udirection()
654 .index();
655 let mut spare = [Basis::X, Basis::Z].into_iter();
656 let bases: [Basis; 3] = std::array::from_fn(|axis| {
657 if axis == direction {
658 spider_incident_basis(self.nodes[fix_node].kind)
659 } else {
660 spare
661 .next()
662 .expect("exactly two axes differ from the edge direction")
663 }
664 });
665 CubeKind::try_from(bases).expect("arbitrary fixing always yields a valid cube")
666 };
667 graph.add_block(self.cube_block(fix_node, fix_kind));
668 nodes_to_handle.remove(&fix_node);
669 Ok(())
670 }
671
672 fn infer_cube_kind_from_neighbor(
673 &self,
674 infer_from: usize,
675 other_node: usize,
676 infer_cube_kind: CubeKind,
677 ) -> Result<CubeKind, ZXError> {
678 let edge_direction = self
679 .direction_between(infer_from, other_node)?
680 .as_udirection()
681 .index();
682 let edge = self.get_edge(infer_from, other_node);
683 let mut bases = infer_cube_kind.bases();
684 for (index, basis) in bases.iter_mut().enumerate() {
685 if index != edge_direction && edge.hadamard {
686 *basis = basis.flip();
687 }
688 }
689 bases[edge_direction] = spider_incident_basis(self.nodes[other_node].kind);
690 CubeKind::try_from(bases).map_err(|e| ZXError::InvalidInferredCubeKind {
691 pos: self.nodes[other_node].pos,
692 source: e,
693 })
694 }
695
696 fn cube_block(&self, node_id: usize, kind: CubeKind) -> Block {
697 Block::new(self.nodes[node_id].pos, BlockKind::Cube(kind))
698 }
699
700 fn special_block(&self, node_id: usize) -> Block {
701 let node = self.nodes[node_id];
702 let kind = match node.kind {
703 NodeKind::Y => BlockKind::Y,
704 NodeKind::T => BlockKind::T,
705 NodeKind::Port => BlockKind::Port,
706 NodeKind::Selective(k) => BlockKind::Selective(k),
707 NodeKind::X | NodeKind::Z => unreachable!("X/Z nodes are synthesized as cubes"),
708 };
709 let block = Block::new(node.pos, kind);
710 if kind.is_port() {
711 block
712 .with_port_role(node.role)
713 .expect("Port nodes accept Port roles")
714 } else {
715 block
716 }
717 }
718
719 fn add_pipe_between_nodes(
720 &self,
721 graph: &mut BlockGraph,
722 src_node: usize,
723 dst_node: usize,
724 ) -> Result<(), ZXError> {
725 let direction = self.direction_between(src_node, dst_node)?;
726 let edge = self.get_edge(src_node, dst_node);
727 let pipe = if edge.hadamard {
728 Pipe::new(self.nodes[src_node].pos, direction).with_hadamard()
729 } else {
730 Pipe::new(self.nodes[src_node].pos, direction)
731 };
732 graph.add_pipe(pipe);
733 Ok(())
734 }
735
736 fn sorted_undirected_edges(&self) -> Vec<(usize, usize)> {
737 let mut edges: Vec<(usize, usize)> = self
738 .edges
739 .iter()
740 .filter(|edge| edge.n1 < edge.n2)
741 .map(|edge| (edge.n1, edge.n2))
742 .collect();
743 edges.sort_by_key(|&(n1, n2)| {
744 (self.nodes[n1].pos.to_array(), self.nodes[n2].pos.to_array())
745 });
746 edges
747 }
748
749 fn axis_directions(&self, node_id: usize) -> HashSet<UDirection> {
750 self.neighbor_ids(node_id)
751 .iter()
752 .copied()
753 .filter_map(|neighbor| self.direction_between(node_id, neighbor).ok())
754 .map(Direction::as_udirection)
755 .collect()
756 }
757
758 fn direction_between(&self, src_node: usize, dst_node: usize) -> Result<Direction, ZXError> {
759 let src = self.nodes[src_node].pos;
760 let dst = self.nodes[dst_node].pos;
761 let delta = dst - src;
762 Direction::try_from(delta).map_err(|_| ZXError::NonNeighboringZXEdge { src, dst })
763 }
764}
765
766fn is_zx_spider(kind: NodeKind) -> bool {
767 matches!(kind, NodeKind::X | NodeKind::Z)
768}
769
770fn spider_normal_basis(kind: NodeKind) -> Basis {
771 match kind {
772 NodeKind::X => Basis::X,
773 NodeKind::Z => Basis::Z,
774 _ => unreachable!("only X/Z spiders have a spider normal basis"),
775 }
776}
777
778fn spider_incident_basis(kind: NodeKind) -> Basis {
779 spider_normal_basis(kind).flip()
780}
781
782fn block_to_node_kind(block: &Block) -> NodeKind {
783 match block.kind {
784 BlockKind::Cube(kind) => {
785 if kind.is_x_type() {
786 NodeKind::X
787 } else {
788 NodeKind::Z
789 }
790 }
791 BlockKind::Walking(kind) => {
792 let cube_kind = CubeKind::from(kind.boundary());
793 if cube_kind.is_x_type() {
794 NodeKind::X
795 } else {
796 NodeKind::Z
797 }
798 }
799 BlockKind::PatchRotation(_) | BlockKind::Measurement(Basis::X) => NodeKind::Z,
800 BlockKind::Measurement(Basis::Z) => NodeKind::X,
801 BlockKind::Selective(kind) => NodeKind::Selective(kind),
802 BlockKind::Y => NodeKind::Y,
803 BlockKind::T => NodeKind::T,
804 BlockKind::Port => NodeKind::Port,
805 }
806}
807
808#[cfg(test)]
809mod tests {
810 use std::collections::HashMap;
811
812 use glam::ivec3;
813 use rustc_hash::FxHashMap;
814
815 use crate::{
816 Action, ActionDag, Block, BlockGraph, BlockKind, CubeKind, Expr, GalleryItem,
817 MeasureTarget, PatchRotationKind, SelectiveKind, WalkingBoundaryKind, WalkingKind,
818 };
819 use crate::{Basis, Direction, Pipe};
820
821 use super::{CsrAdjacency, NodeKind, ZXEdge, ZXError, ZXGraph, ZXNode, block_to_node_kind};
822
823 #[test]
824 fn local_neighborhoods_match_full_tensors_and_crossing_reconstruction() {
825 use crate::{Pauli, PauliString};
826
827 for gallery in [
828 GalleryItem::CNOT,
829 GalleryItem::CZSpatialH,
830 GalleryItem::CZTemporalH,
831 GalleryItem::S,
832 GalleryItem::THTH,
833 GalleryItem::GHZSlideThenGlide,
834 GalleryItem::GHZPatchRotations,
835 GalleryItem::CCZInjectedAnd,
836 GalleryItem::Stability,
837 ] {
838 let source = gallery
840 .build()
841 .materialize_root_graph()
842 .expect("gallery flat projection")
843 .project_branches_deferred([])
844 .unwrap();
845 let full = ZXGraph::from_block_graph_for_analysis(&source).unwrap();
846 for center in full.nodes() {
847 let local = ZXGraph::from_local_block_neighborhood(&source, center.pos).unwrap();
848 let local_center = local.node_at(center.pos).unwrap();
849 assert_eq!(local_center.kind, center.kind);
850 assert_eq!(local_center.role, center.role);
851 assert_eq!(
852 local_center.is_input_port(&local),
853 center.is_input_port(&full)
854 );
855 assert_eq!(
856 local_center.is_output_port(&local),
857 center.is_output_port(&full)
858 );
859 assert_eq!(
860 local.edges().len(),
861 full.neighbor_edges(center.id).count() * 2
862 );
863 assert!(local.nodes().len() <= local.edges().len() / 2 + 1);
864 assert_eq!(local.action_graph().ordered_nodes().count(), 0);
865 let full_column = |column: usize| {
866 if let Some(node) = local.nodes().get(column) {
867 let global = full.node_at(node.pos).unwrap();
868 assert_eq!(node.kind, global.kind);
869 assert_eq!(node.role, global.role);
870 global.id
871 } else {
872 let edge = local.edge_by_id(column);
873 let a = full.node_at(local.nodes()[edge.n1].pos).unwrap().id;
874 let b = full.node_at(local.nodes()[edge.n2].pos).unwrap().id;
875 assert_eq!(edge.n1 > edge.n2, a > b);
876 let global = full.edge_between(a, b).unwrap();
877 assert_eq!(edge.hadamard, global.hadamard);
878 global.id
879 }
880 };
881 for direction in std::iter::once(None).chain(Direction::iter().map(Some)) {
882 for pauli in [
883 crate::PauliBasis::X,
884 crate::PauliBasis::Y,
885 crate::PauliBasis::Z,
886 ] {
887 let target = crate::FeedbackTarget {
888 target: center.pos,
889 pauli,
890 direction,
891 };
892 assert_eq!(
893 local
894 .feedback_column(&target)
895 .map(|(column, pauli)| (full_column(column), pauli)),
896 full.feedback_column(&target),
897 );
898 }
899 }
900 for direction in [
901 Direction::XMINUS,
902 Direction::XPLUS,
903 Direction::YMINUS,
904 Direction::YPLUS,
905 Direction::ZMINUS,
906 Direction::ZPLUS,
907 ] {
908 let target = MeasureTarget::Edge {
909 src: center.pos,
910 dir: direction,
911 };
912 assert_eq!(
913 local.measurement_column(&target).map(full_column),
914 full.measurement_column(&target)
915 );
916 }
917 let terms = local
918 .local_stabilizer_flow_supports_for_node_kind(
919 local_center.id,
920 local_center.kind,
921 )
922 .into_iter()
923 .map(|row| {
924 row.into_iter()
925 .map(|(column, pauli)| (full_column(column), pauli))
926 .collect::<Vec<_>>()
927 })
928 .collect::<Vec<_>>();
929 assert_eq!(
930 terms,
931 full.local_stabilizer_flow_supports_for_node_kind(center.id, center.kind)
932 );
933 for seed in 0..16 {
934 let axes = [Pauli::I, Pauli::X, Pauli::Z, Pauli::Y];
935 let mut local_row = PauliString::new(local.total_ids());
936 local_row.set(local_center.id, axes[seed % 4]);
937 for (index, edge) in local
938 .edges()
939 .iter()
940 .filter(|edge| edge.n1 < edge.n2)
941 .enumerate()
942 {
943 let pauli = axes[(seed / 4 + index * seed) % 4];
944 local_row.set(edge.id, pauli);
945 local_row.set(local.edge_id(edge.n2, edge.n1).unwrap(), pauli);
946 }
947 let mut full_row = PauliString::new(full.total_ids());
948 for (column, pauli) in local_row.iter_support() {
949 full_row.set(full_column(column), pauli);
950 }
951 let actual = local.materialize_stabilizer_with_sign(local_row, false);
952 let expected = full.materialize_stabilizer_with_sign(full_row, false);
953 assert_eq!(actual.interior_nodes, expected.interior_nodes);
954 assert_eq!(actual.port_stabilizer, expected.port_stabilizer);
955 assert_eq!(actual.interior_edges, expected.interior_edges);
956 }
957 }
958 }
959 }
960
961 #[test]
962 fn local_neighborhood_ignores_unrelated_blocks_and_keeps_extended_anchors() {
963 let mut graph = BlockGraph::new();
964 let kind = PatchRotationKind::new(Basis::X, glam::IVec2::X).unwrap();
965 let start = ivec3(0, 0, 0);
966 graph.add_block(Block::new(start, BlockKind::PatchRotation(kind)));
967 graph.add_block(Block::new(ivec3(0, 0, -1), BlockKind::Port));
968 graph.add_block(Block::new(ivec3(1, 0, 2), BlockKind::Port));
969 graph.add_pipe(Pipe::new(start, Direction::ZMINUS));
970 graph.add_pipe(Pipe::new(kind.end_position(start), Direction::ZPLUS));
971 for x in 10..1010 {
972 graph.add_block(Block::new(ivec3(x, 0, 0), BlockKind::Cube(CubeKind::ZXZ)));
973 }
974 let graph = graph.project_branches_deferred([]).unwrap();
975 let local = ZXGraph::from_local_block_neighborhood(&graph, start).unwrap();
976 assert_eq!(local.nodes().len(), 3);
977 assert_eq!(local.total_ids(), 7);
978 assert!(local.node_at(kind.end_position(start)).is_none());
979 let center = local.node_at(start).unwrap().id;
980 let before = local.node_at(ivec3(0, 0, -1)).unwrap().id;
981 let after = local.node_at(ivec3(1, 0, 2)).unwrap().id;
982 assert!(!local.edge_between(center, before).unwrap().hadamard);
983 assert!(local.edge_between(center, after).is_some());
984 assert!(before < center && center < after);
985 let target = crate::FeedbackTarget {
986 target: start,
987 pauli: crate::PauliBasis::X,
988 direction: None,
989 };
990 assert_eq!(local.feedback_edge(&target).unwrap().n2, after);
991 }
992
993 #[test]
994 fn fixed_measurements_lower_to_concrete_zx_caps() {
995 assert_eq!(
996 block_to_node_kind(&Block::new(
997 ivec3(0, 0, 0),
998 BlockKind::Measurement(Basis::X),
999 )),
1000 NodeKind::Z,
1001 );
1002 assert_eq!(
1003 block_to_node_kind(&Block::new(
1004 ivec3(0, 0, 0),
1005 BlockKind::Measurement(Basis::Z),
1006 )),
1007 NodeKind::X,
1008 );
1009 }
1010
1011 fn restore_selectives(graph: &mut BlockGraph, originals: &HashMap<Block, Block>) {
1012 for (original, resolved) in originals {
1013 graph
1014 .get_block_mut(resolved.pos)
1015 .expect("resolved block must exist in graph")
1016 .kind = original.kind;
1017 }
1018 }
1019
1020 #[test]
1021 fn patch_rotation_lowers_to_single_z_node() {
1022 let mut graph = BlockGraph::new();
1023 let kind = PatchRotationKind::new(Basis::X, glam::IVec2::X).unwrap();
1024 graph.add_block(Block::new(ivec3(0, 0, 0), BlockKind::PatchRotation(kind)));
1025 graph.add_block(Block::new(ivec3(0, 0, -1), BlockKind::Port));
1026 graph.add_block(Block::new(ivec3(1, 0, 2), BlockKind::Port));
1027 graph.add_pipe(Pipe::new(ivec3(0, 0, 0), Direction::ZMINUS));
1028 graph.add_pipe(Pipe::new(
1029 kind.end_position(ivec3(0, 0, 0)),
1030 Direction::ZPLUS,
1031 ));
1032
1033 let zx = ZXGraph::try_from(&graph).expect("patch rotation graph converts to ZX");
1034 let node = zx
1035 .nodes
1036 .iter()
1037 .find(|node| node.pos == ivec3(0, 0, 0))
1038 .expect("patch rotation node exists");
1039
1040 assert_eq!(node.kind, NodeKind::Z);
1041 }
1042
1043 fn assert_roundtrip_matches(graph: &BlockGraph) {
1044 let canonical = graph.fix_shadowed_faces();
1045 let (concretized, selective_blocks) = canonical
1046 .randomly_resolve_selectives(rand::random())
1047 .unwrap();
1048 let zx = ZXGraph::try_from(&concretized).expect("concretized graph converts to ZX");
1049 let mut rebuilt = BlockGraph::try_from(&zx).expect("ZX graph converts back to BlockGraph");
1050 restore_selectives(&mut rebuilt, &selective_blocks);
1051
1052 assert_eq!(rebuilt.block_count(), canonical.block_count());
1053 assert_eq!(rebuilt.pipe_count(), canonical.pipe_count());
1054 for block in canonical.blocks() {
1055 let rebuilt_block = rebuilt
1056 .get_block(block.pos)
1057 .expect("rebuilt graph must contain all original blocks");
1058 assert_eq!(
1059 rebuilt_block.kind, block.kind,
1060 "block kind mismatch at {}",
1061 block.pos
1062 );
1063 }
1064 for pipe in canonical.pipes() {
1065 let rebuilt_pipe = rebuilt
1066 .get_pipe(pipe.src, pipe.dst())
1067 .expect("rebuilt graph must contain all original pipes");
1068 assert_eq!(
1069 rebuilt_pipe.hadamard,
1070 pipe.hadamard,
1071 "pipe hadamard mismatch at {} -> {}",
1072 pipe.src,
1073 pipe.dst()
1074 );
1075 }
1076 }
1077
1078 #[test]
1079 fn to_zx_graph_preserves_action_dag_metadata() {
1080 let graph = crate::GalleryItem::T
1081 .build()
1082 .materialize_root_graph()
1083 .expect("gallery flat projection");
1084 let zx = graph.to_zx_graph().unwrap();
1085
1086 let block_measurements = graph
1087 .action_graph()
1088 .ordered_nodes()
1089 .map(|node| node.measurement)
1090 .collect::<Vec<_>>();
1091 let zx_measurements = zx
1092 .action_graph()
1093 .ordered_nodes()
1094 .map(|node| node.measurement)
1095 .collect::<Vec<_>>();
1096
1097 assert_eq!(zx_measurements, block_measurements);
1098 }
1099
1100 #[test]
1101 fn block_graph_roundtrip_preserves_non_measure_actions() {
1102 use glam::ivec3;
1103
1104 let mut graph = BlockGraph::new();
1105 graph.add_block(Block::new(ivec3(0, 0, 0), BlockKind::Cube(CubeKind::ZXZ)));
1106 graph
1107 .set_actions(vec![
1108 Action::Measure {
1109 target: MeasureTarget::Node(ivec3(0, 0, 0)),
1110 name: "m0".into(),
1111 },
1112 Action::Let {
1113 name: "alias".into(),
1114 expr: Expr::Var("m0".into()),
1115 },
1116 Action::DiscardIf(Expr::Var("alias".into())),
1117 ])
1118 .unwrap();
1119
1120 let zx = graph.to_zx_graph().unwrap();
1121 assert_eq!(zx.actions(), graph.actions());
1122
1123 let rebuilt = BlockGraph::try_from(&zx).unwrap();
1124 assert_eq!(rebuilt.actions(), graph.actions());
1125 }
1126
1127 #[test]
1128 fn walking_block_converts_to_single_zx_node_from_boundary_kind() {
1129 let mut graph = BlockGraph::new();
1130 graph.add_block(Block::new(
1131 ivec3(0, 0, 0),
1132 BlockKind::Walking(
1133 WalkingKind::new(WalkingBoundaryKind::XZZ, glam::IVec2::new(1, 1)).unwrap(),
1134 ),
1135 ));
1136
1137 let zx = graph.to_zx_graph().unwrap();
1138
1139 assert_eq!(zx.nodes().len(), 1);
1140 assert_eq!(zx.nodes()[0].kind, NodeKind::X);
1141 assert_eq!(zx.nodes()[0].pos, ivec3(0, 0, 0));
1142 }
1143
1144 #[test]
1145 fn test_zx_to_block_graph_roundtrip_for_all_galleries() {
1146 for gallery in [
1147 GalleryItem::CNOT,
1148 GalleryItem::CZSpatialH,
1149 GalleryItem::CZTemporalH,
1150 GalleryItem::S,
1151 GalleryItem::TWithPreparedY,
1152 GalleryItem::CCZFactoryWithTels,
1153 GalleryItem::THTH,
1154 ] {
1155 let graph = gallery
1156 .build()
1157 .materialize_root_graph()
1158 .expect("gallery flat projection");
1159 assert_roundtrip_matches(&graph);
1160 }
1161 }
1162
1163 #[test]
1164 fn test_selective_boundary_nodes_must_be_timelike() {
1165 let zx = ZXGraph {
1166 nodes: vec![
1167 ZXNode::new(0, ivec3(0, 0, 0), NodeKind::Selective(SelectiveKind::XZ)),
1168 ZXNode::new(1, ivec3(1, 0, 0), NodeKind::X),
1169 ],
1170 edges: vec![
1171 ZXEdge {
1172 n1: 0,
1173 n2: 1,
1174 id: 2,
1175 hadamard: false,
1176 },
1177 ZXEdge {
1178 n1: 1,
1179 n2: 0,
1180 id: 3,
1181 hadamard: false,
1182 },
1183 ],
1184 adjacency: CsrAdjacency::from_edges(2, &[(0, 1, 2), (1, 0, 3)]),
1185 pos_to_node: FxHashMap::from_iter([(ivec3(0, 0, 0), 0), (ivec3(1, 0, 0), 1)]),
1186 action_graph: crate::ActionDag::default(),
1187 total_ids: 4,
1188 cross_incident: Default::default(),
1189 stabilizer_phase_basis: Default::default(),
1190 };
1191
1192 let err = zx
1193 .to_block_graph()
1194 .expect_err("spacelike selective should be rejected");
1195 assert!(matches!(
1196 err,
1197 ZXError::SpecialNodeNotTimeLike {
1198 pos,
1199 kind: NodeKind::Selective(_)
1200 } if pos == ivec3(0, 0, 0)
1201 ));
1202 }
1203
1204 #[test]
1205 fn validate_for_program_rejects_non_selective_resolve_targets() {
1206 let zx = ZXGraph {
1207 nodes: vec![ZXNode::new(0, ivec3(0, 0, 0), NodeKind::X)],
1208 edges: vec![],
1209 adjacency: CsrAdjacency::from_edges(1, &[]),
1210 pos_to_node: FxHashMap::from_iter([(ivec3(0, 0, 0), 0)]),
1211 action_graph: ActionDag::from_actions(&[Action::Resolve {
1212 target: ivec3(0, 0, 0),
1213 condition: Expr::Var("m0".into()),
1214 }]),
1215 total_ids: 1,
1216 cross_incident: Default::default(),
1217 stabilizer_phase_basis: Default::default(),
1218 };
1219
1220 let err = zx
1221 .validate_for_program()
1222 .expect_err("non-selective resolve target should be rejected");
1223 assert!(matches!(
1224 err,
1225 ZXError::InvalidResolveTarget { pos } if pos == ivec3(0, 0, 0)
1226 ));
1227 }
1228}