Skip to main content

bloq_graph/zx/
convert.rs

1//! Conversion between block graphs and their local ZX representation.
2
3use 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    /// Converts a structurally/action-syntactically valid source graph while
43    /// physical action analysis itself is still deriving stabilizers.
44    #[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            // Quantum actions need their complete classical dependencies.
70            graph
71                .build_action_graph(&actions)
72                .map_err(|error| ZXError::Graph(Box::new(error)))?
73        } else {
74            // Pure classical work does not couple independent quantum components.
75            crate::ActionDag::default()
76        };
77        Self::from_block_graph_parts(graph, action_graph, seams)
78    }
79
80    /// Build the center's tensor/incidence view from a validated projection
81    /// whose shadowed faces have already been fixed. Neighbor kinds are copied
82    /// from that complete projection: normalizing a truncated graph could change
83    /// their spider colors. Only the center has its complete neighborhood here.
84    ///
85    /// Sorted anchor positions preserve the full converter's relative node
86    /// order and thus its directed Hadamard frames. Extended block endpoints
87    /// still resolve to their owning anchors. The view carries no source actions
88    /// and must not be used for whole-program stabilizer or interface analysis.
89    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    /// Split a connector's independent quantum components, preserving node order
153    /// so directed Hadamard frames and incoming Choi transposes do not change.
154    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        // Stable columns keep measurement witnesses independent of source storage order.
260        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        // Source records use the authored pipe source; both ZX edge columns use
273        // the smaller node ID's frame, irrespective of the action's direction.
274        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        // node_at needs only block centers, so key the lookup by `node.pos`
349        // rather than reusing the endpoint-offset map above.
350        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    /// Converts the ZX graph back into a [`BlockGraph`], inferring cube kinds
410    /// from spider structure. `TryFrom<&ZXGraph> for BlockGraph` is the public
411    /// entry point.
412    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            // The native caller receives this normalized, validated projection.
839            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}