Skip to main content

quantrs2_circuit/
zx_calculus.rs

1//! ZX-calculus optimization for quantum circuits
2//!
3//! This module implements ZX-calculus, a powerful graphical language for
4//! reasoning about quantum computation that enables advanced optimizations
5//! through graph rewrite rules.
6
7use crate::builder::Circuit;
8use crate::dag::{circuit_to_dag, CircuitDag, DagNode};
9use quantrs2_core::{
10    error::{QuantRS2Error, QuantRS2Result},
11    gate::GateOp,
12    qubit::QubitId,
13};
14use serde::{Deserialize, Serialize};
15use std::collections::{HashMap, HashSet, VecDeque};
16use std::f64::consts::PI;
17use std::sync::Arc;
18
19/// A ZX-diagram node representing quantum operations
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21pub enum ZXNode {
22    /// Green spider (Z-spider) - represents Z-basis operations
23    ZSpider {
24        id: usize,
25        phase: f64,
26        /// Number of inputs/outputs
27        arity: usize,
28    },
29    /// Red spider (X-spider) - represents X-basis operations
30    XSpider {
31        id: usize,
32        phase: f64,
33        arity: usize,
34    },
35    /// Hadamard gate
36    Hadamard {
37        id: usize,
38    },
39    /// Input/Output boundaries
40    Input {
41        id: usize,
42        qubit: u32,
43    },
44    Output {
45        id: usize,
46        qubit: u32,
47    },
48}
49
50impl ZXNode {
51    #[must_use]
52    pub const fn id(&self) -> usize {
53        match self {
54            Self::ZSpider { id, .. } => *id,
55            Self::XSpider { id, .. } => *id,
56            Self::Hadamard { id } => *id,
57            Self::Input { id, .. } => *id,
58            Self::Output { id, .. } => *id,
59        }
60    }
61
62    #[must_use]
63    pub const fn phase(&self) -> f64 {
64        match self {
65            Self::ZSpider { phase, .. } | Self::XSpider { phase, .. } => *phase,
66            _ => 0.0,
67        }
68    }
69
70    pub const fn set_phase(&mut self, new_phase: f64) {
71        match self {
72            Self::ZSpider { phase, .. } | Self::XSpider { phase, .. } => *phase = new_phase,
73            _ => {}
74        }
75    }
76}
77
78/// Edge in ZX-diagram
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct ZXEdge {
81    pub source: usize,
82    pub target: usize,
83    /// Hadamard edges are represented as dashed lines in ZX-calculus
84    pub is_hadamard: bool,
85}
86
87/// ZX-diagram representation of a quantum circuit
88#[derive(Debug, Clone)]
89pub struct ZXDiagram {
90    /// Nodes in the diagram
91    pub nodes: HashMap<usize, ZXNode>,
92    /// Edges between nodes
93    pub edges: Vec<ZXEdge>,
94    /// Adjacency list for efficient traversal
95    pub adjacency: HashMap<usize, Vec<usize>>,
96    /// Input nodes for each qubit
97    pub inputs: HashMap<u32, usize>,
98    /// Output nodes for each qubit
99    pub outputs: HashMap<u32, usize>,
100    /// Next available node ID
101    next_id: usize,
102}
103
104impl Default for ZXDiagram {
105    fn default() -> Self {
106        Self::new()
107    }
108}
109
110impl ZXDiagram {
111    /// Create a new empty ZX diagram
112    #[must_use]
113    pub fn new() -> Self {
114        Self {
115            nodes: HashMap::new(),
116            edges: Vec::new(),
117            adjacency: HashMap::new(),
118            inputs: HashMap::new(),
119            outputs: HashMap::new(),
120            next_id: 0,
121        }
122    }
123
124    /// Add a node to the diagram
125    pub fn add_node(&mut self, node: ZXNode) -> usize {
126        let id = self.next_id;
127        self.next_id += 1;
128
129        let node_with_id = match node {
130            ZXNode::ZSpider { phase, arity, .. } => ZXNode::ZSpider { id, phase, arity },
131            ZXNode::XSpider { phase, arity, .. } => ZXNode::XSpider { id, phase, arity },
132            ZXNode::Hadamard { .. } => ZXNode::Hadamard { id },
133            ZXNode::Input { qubit, .. } => ZXNode::Input { id, qubit },
134            ZXNode::Output { qubit, .. } => ZXNode::Output { id, qubit },
135        };
136
137        self.nodes.insert(id, node_with_id);
138        self.adjacency.insert(id, Vec::new());
139        id
140    }
141
142    /// Add an edge between two nodes
143    pub fn add_edge(&mut self, source: usize, target: usize, is_hadamard: bool) {
144        let edge = ZXEdge {
145            source,
146            target,
147            is_hadamard,
148        };
149        self.edges.push(edge);
150
151        // Update adjacency lists
152        self.adjacency.entry(source).or_default().push(target);
153        self.adjacency.entry(target).or_default().push(source);
154    }
155
156    /// Initialize inputs and outputs for a given number of qubits
157    pub fn initialize_boundaries(&mut self, num_qubits: usize) {
158        for i in 0..num_qubits {
159            let qubit = i as u32;
160
161            let input_id = self.add_node(ZXNode::Input { id: 0, qubit });
162            let output_id = self.add_node(ZXNode::Output { id: 0, qubit });
163
164            self.inputs.insert(qubit, input_id);
165            self.outputs.insert(qubit, output_id);
166        }
167    }
168
169    /// Get neighbors of a node
170    #[must_use]
171    pub fn neighbors(&self, node_id: usize) -> &[usize] {
172        self.adjacency
173            .get(&node_id)
174            .map_or(&[], std::vec::Vec::as_slice)
175    }
176
177    /// Apply spider fusion rule
178    /// Two spiders of the same color connected by a plain edge can be fused
179    pub fn spider_fusion(&mut self) -> bool {
180        let mut changed = false;
181        let mut to_remove = Vec::new();
182        let mut to_update = Vec::new();
183
184        for edge in &self.edges {
185            if !edge.is_hadamard {
186                if let (Some(node1), Some(node2)) =
187                    (self.nodes.get(&edge.source), self.nodes.get(&edge.target))
188                {
189                    // Check if both are spiders of the same type
190                    match (node1, node2) {
191                        (
192                            ZXNode::ZSpider {
193                                id: id1,
194                                phase: phase1,
195                                ..
196                            },
197                            ZXNode::ZSpider {
198                                id: id2,
199                                phase: phase2,
200                                ..
201                            },
202                        )
203                        | (
204                            ZXNode::XSpider {
205                                id: id1,
206                                phase: phase1,
207                                ..
208                            },
209                            ZXNode::XSpider {
210                                id: id2,
211                                phase: phase2,
212                                ..
213                            },
214                        ) => {
215                            // Fuse the spiders: keep first, remove second
216                            let new_phase = (phase1 + phase2) % (2.0 * PI);
217                            to_update.push((*id1, new_phase));
218                            to_remove.push(*id2);
219                            changed = true;
220                        }
221                        _ => {}
222                    }
223                }
224            }
225        }
226
227        // Apply updates
228        for (id, new_phase) in to_update {
229            if let Some(node) = self.nodes.get_mut(&id) {
230                node.set_phase(new_phase);
231            }
232        }
233
234        // Remove fused nodes and update edges
235        for id in to_remove {
236            self.remove_node(id);
237        }
238
239        changed
240    }
241
242    /// Apply identity removal rule
243    /// A spider with phase 0 and arity 2 can be removed
244    pub fn identity_removal(&mut self) -> bool {
245        let mut changed = false;
246        let mut to_remove = Vec::new();
247
248        for (id, node) in &self.nodes {
249            match node {
250                ZXNode::ZSpider { phase, arity, .. } | ZXNode::XSpider { phase, arity, .. }
251                    if *arity == 2 && phase.abs() < 1e-10 =>
252                {
253                    to_remove.push(*id);
254                }
255                _ => {}
256            }
257        }
258
259        for id in to_remove {
260            // Connect the neighbors directly
261            let neighbors: Vec<_> = self.neighbors(id).to_vec();
262            if neighbors.len() == 2 {
263                self.add_edge(neighbors[0], neighbors[1], false);
264                changed = true;
265            }
266            self.remove_node(id);
267        }
268
269        changed
270    }
271
272    /// π-commutation (Pauli-push) rule: **not currently applied**.
273    ///
274    /// The π-commutation identity `Z(α)·X(π) = X(π)·Z(-α)` only preserves the
275    /// diagram's semantics if the π-spider is *relocated* to the other side of
276    /// the neighbouring spider — a graph edge-surgery, not a local phase tweak.
277    /// A correct, semantics-preserving implementation requires that relocation
278    /// (and, in the general entangled case, gflow-aware reasoning), which is not
279    /// yet implemented here.
280    ///
281    /// This method therefore deliberately performs **no rewrite** and returns
282    /// `false` (the honest "nothing changed" signal): it never reports a
283    /// simplification it did not make, and the other rules
284    /// ([`spider_fusion`](Self::spider_fusion),
285    /// [`identity_removal`](Self::identity_removal),
286    /// [`hadamard_cancellation`](Self::hadamard_cancellation)) already cover the
287    /// reductions that are sound on the diagrams this module produces.  It is
288    /// kept in the rule set so that adding the real rewrite later is a localized
289    /// change.
290    pub const fn pi_commutation(&self) -> bool {
291        false
292    }
293
294    /// Apply Hadamard cancellation
295    /// Two adjacent Hadamard gates cancel out
296    pub fn hadamard_cancellation(&mut self) -> bool {
297        let mut changed = false;
298        let mut to_remove = Vec::new();
299
300        // Find pairs of adjacent Hadamard nodes
301        for edge in &self.edges {
302            if let (Some(ZXNode::Hadamard { id: id1 }), Some(ZXNode::Hadamard { id: id2 })) =
303                (self.nodes.get(&edge.source), self.nodes.get(&edge.target))
304            {
305                // Two Hadamards connected - they cancel out
306                to_remove.push(*id1);
307                to_remove.push(*id2);
308                changed = true;
309            }
310        }
311
312        for id in to_remove {
313            self.remove_node(id);
314        }
315
316        changed
317    }
318
319    /// Remove a node and update the graph structure
320    fn remove_node(&mut self, node_id: usize) {
321        // Remove from nodes
322        self.nodes.remove(&node_id);
323
324        // Remove from adjacency
325        self.adjacency.remove(&node_id);
326
327        // Remove from other nodes' adjacency lists
328        for adj_list in self.adjacency.values_mut() {
329            adj_list.retain(|&id| id != node_id);
330        }
331
332        // Remove edges involving this node
333        self.edges
334            .retain(|edge| edge.source != node_id && edge.target != node_id);
335    }
336
337    /// Calculate the T-count (number of T gates) in the diagram
338    #[must_use]
339    pub fn t_count(&self) -> usize {
340        self.nodes
341            .values()
342            .filter(|node| {
343                let phase = node.phase();
344                (phase - PI / 4.0).abs() < 1e-10
345                    || (phase - 3.0 * PI / 4.0).abs() < 1e-10
346                    || (phase - 5.0 * PI / 4.0).abs() < 1e-10
347                    || (phase - 7.0 * PI / 4.0).abs() < 1e-10
348            })
349            .count()
350    }
351
352    /// Apply all optimization rules until convergence
353    pub fn optimize(&mut self) -> ZXOptimizationResult {
354        let initial_node_count = self.nodes.len();
355        let initial_t_count = self.t_count();
356
357        let mut iterations = 0;
358        let max_iterations = 100;
359
360        while iterations < max_iterations {
361            let mut changed = false;
362
363            // Apply rewrite rules
364            changed |= self.spider_fusion();
365            changed |= self.identity_removal();
366            changed |= self.hadamard_cancellation();
367            changed |= self.pi_commutation();
368
369            if !changed {
370                break;
371            }
372            iterations += 1;
373        }
374
375        let final_node_count = self.nodes.len();
376        let final_t_count = self.t_count();
377
378        ZXOptimizationResult {
379            iterations,
380            initial_node_count,
381            final_node_count,
382            initial_t_count,
383            final_t_count,
384            converged: iterations < max_iterations,
385        }
386    }
387}
388
389/// Result of ZX optimization
390#[derive(Debug, Clone)]
391pub struct ZXOptimizationResult {
392    pub iterations: usize,
393    pub initial_node_count: usize,
394    pub final_node_count: usize,
395    pub initial_t_count: usize,
396    pub final_t_count: usize,
397    pub converged: bool,
398}
399
400/// ZX-calculus optimizer
401pub struct ZXOptimizer {
402    /// Maximum number of optimization iterations
403    pub max_iterations: usize,
404    /// Enable specific optimization rules
405    pub enable_spider_fusion: bool,
406    pub enable_identity_removal: bool,
407    pub enable_pi_commutation: bool,
408    pub enable_hadamard_cancellation: bool,
409}
410
411impl Default for ZXOptimizer {
412    fn default() -> Self {
413        Self {
414            max_iterations: 100,
415            enable_spider_fusion: true,
416            enable_identity_removal: true,
417            enable_pi_commutation: true,
418            enable_hadamard_cancellation: true,
419        }
420    }
421}
422
423impl ZXOptimizer {
424    /// Create a new ZX optimizer
425    #[must_use]
426    pub fn new() -> Self {
427        Self::default()
428    }
429
430    /// Convert a quantum circuit to ZX diagram
431    pub fn circuit_to_zx<const N: usize>(&self, circuit: &Circuit<N>) -> QuantRS2Result<ZXDiagram> {
432        let mut diagram = ZXDiagram::new();
433        diagram.initialize_boundaries(N);
434
435        // Track the last node on each qubit wire
436        let mut qubit_wires = HashMap::new();
437        for i in 0..N {
438            let qubit = i as u32;
439            if let Some(&input_id) = diagram.inputs.get(&qubit) {
440                qubit_wires.insert(qubit, input_id);
441            }
442        }
443
444        // Convert each gate to ZX representation
445        for gate in circuit.gates() {
446            self.gate_to_zx(gate.as_ref(), &mut diagram, &mut qubit_wires)?;
447        }
448
449        // Connect to outputs
450        for i in 0..N {
451            let qubit = i as u32;
452            if let (Some(&last_node), Some(&output_id)) =
453                (qubit_wires.get(&qubit), diagram.outputs.get(&qubit))
454            {
455                diagram.add_edge(last_node, output_id, false);
456            }
457        }
458
459        Ok(diagram)
460    }
461
462    /// Convert a single gate to ZX representation
463    fn gate_to_zx(
464        &self,
465        gate: &dyn GateOp,
466        diagram: &mut ZXDiagram,
467        qubit_wires: &mut HashMap<u32, usize>,
468    ) -> QuantRS2Result<()> {
469        let gate_name = gate.name();
470        let qubits = gate.qubits();
471
472        match gate_name {
473            "H" => {
474                // Hadamard gate
475                let qubit = qubits[0].id();
476                let h_node = diagram.add_node(ZXNode::Hadamard { id: 0 });
477
478                if let Some(&prev_node) = qubit_wires.get(&qubit) {
479                    diagram.add_edge(prev_node, h_node, false);
480                }
481                qubit_wires.insert(qubit, h_node);
482            }
483            "X" => {
484                // Pauli-X = Z-spider with phase π
485                let qubit = qubits[0].id();
486                let x_node = diagram.add_node(ZXNode::ZSpider {
487                    id: 0,
488                    phase: PI,
489                    arity: 2,
490                });
491
492                if let Some(&prev_node) = qubit_wires.get(&qubit) {
493                    diagram.add_edge(prev_node, x_node, false);
494                }
495                qubit_wires.insert(qubit, x_node);
496            }
497            "Y" => {
498                // Pauli-Y = Z-spider with phase π followed by virtual Z
499                let qubit = qubits[0].id();
500                let y_node = diagram.add_node(ZXNode::ZSpider {
501                    id: 0,
502                    phase: PI,
503                    arity: 2,
504                });
505
506                if let Some(&prev_node) = qubit_wires.get(&qubit) {
507                    diagram.add_edge(prev_node, y_node, false);
508                }
509                qubit_wires.insert(qubit, y_node);
510            }
511            "Z" => {
512                // Pauli-Z = Z-spider with phase π
513                let qubit = qubits[0].id();
514                let z_node = diagram.add_node(ZXNode::ZSpider {
515                    id: 0,
516                    phase: PI,
517                    arity: 2,
518                });
519
520                if let Some(&prev_node) = qubit_wires.get(&qubit) {
521                    diagram.add_edge(prev_node, z_node, false);
522                }
523                qubit_wires.insert(qubit, z_node);
524            }
525            "RZ" => {
526                // Z-rotation = Z-spider with rotation angle
527                let qubit = qubits[0].id();
528
529                // Extract rotation angle from gate properties
530                let angle = self.extract_rotation_angle(gate);
531                let rz_node = diagram.add_node(ZXNode::ZSpider {
532                    id: 0,
533                    phase: angle,
534                    arity: 2,
535                });
536
537                if let Some(&prev_node) = qubit_wires.get(&qubit) {
538                    diagram.add_edge(prev_node, rz_node, false);
539                }
540                qubit_wires.insert(qubit, rz_node);
541            }
542            "CNOT" => {
543                // CNOT = Z-spider on control connected to X-spider on target
544                let control_qubit = qubits[0].id();
545                let target_qubit = qubits[1].id();
546
547                let control_spider = diagram.add_node(ZXNode::ZSpider {
548                    id: 0,
549                    phase: 0.0,
550                    arity: 3,
551                });
552                let target_spider = diagram.add_node(ZXNode::XSpider {
553                    id: 0,
554                    phase: 0.0,
555                    arity: 3,
556                });
557
558                // Connect control
559                if let Some(&prev_control) = qubit_wires.get(&control_qubit) {
560                    diagram.add_edge(prev_control, control_spider, false);
561                }
562
563                // Connect target
564                if let Some(&prev_target) = qubit_wires.get(&target_qubit) {
565                    diagram.add_edge(prev_target, target_spider, false);
566                }
567
568                // Connect control to target
569                diagram.add_edge(control_spider, target_spider, false);
570
571                qubit_wires.insert(control_qubit, control_spider);
572                qubit_wires.insert(target_qubit, target_spider);
573            }
574            _ => {
575                // For unsupported gates, add identity spiders
576                for qubit_id in qubits {
577                    let qubit = qubit_id.id();
578                    let identity_node = diagram.add_node(ZXNode::ZSpider {
579                        id: 0,
580                        phase: 0.0,
581                        arity: 2,
582                    });
583
584                    if let Some(&prev_node) = qubit_wires.get(&qubit) {
585                        diagram.add_edge(prev_node, identity_node, false);
586                    }
587                    qubit_wires.insert(qubit, identity_node);
588                }
589            }
590        }
591
592        Ok(())
593    }
594
595    /// Extract the rotation angle of a parameterized single-qubit gate.
596    ///
597    /// Downcasts the gate to the concrete `core` rotation/phase types and reads
598    /// the real angle.  Returns `0.0` (the identity phase) for gates that carry
599    /// no angle, so an unrecognized gate contributes a phase-0 spider rather
600    /// than a fabricated `π/4`.
601    fn extract_rotation_angle(&self, gate: &dyn GateOp) -> f64 {
602        use quantrs2_core::gate::single::{Phase, RotationX, RotationY, RotationZ};
603
604        let any = gate.as_any();
605        if let Some(g) = any.downcast_ref::<RotationZ>() {
606            g.theta
607        } else if let Some(g) = any.downcast_ref::<RotationX>() {
608            g.theta
609        } else if let Some(g) = any.downcast_ref::<RotationY>() {
610            g.theta
611        } else if any.downcast_ref::<Phase>().is_some() {
612            // S gate = Z-rotation by π/2 (up to global phase).
613            PI / 2.0
614        } else {
615            0.0
616        }
617    }
618
619    /// Optimize a circuit using ZX-calculus.
620    ///
621    /// The circuit is converted to a ZX diagram, simplified by the rewrite rules
622    /// to convergence, and extracted back to a circuit.  Because circuit
623    /// extraction from an arbitrary entangled diagram is out of scope (see
624    /// [`zx_to_circuit`](Self::zx_to_circuit)), the extraction step returns an
625    /// honest error for diagrams that retain entangling structure (e.g. those
626    /// containing CNOTs).  The `optimization_stats` on the returned result
627    /// always reflect the *real* diagram-level simplification (node/T-count
628    /// reductions) regardless of whether extraction succeeds.
629    pub fn optimize_circuit<const N: usize>(
630        &self,
631        circuit: &Circuit<N>,
632    ) -> QuantRS2Result<OptimizedZXResult<N>> {
633        // Convert to ZX diagram
634        let mut diagram = self.circuit_to_zx(circuit)?;
635
636        // Optimize the diagram
637        let optimization_result = diagram.optimize();
638
639        // Extract a circuit from the simplified diagram (honest error if the
640        // diagram is not extractable by the linear-wire extractor).
641        let optimized_circuit = self.zx_to_circuit(&diagram)?;
642
643        Ok(OptimizedZXResult {
644            original_circuit: circuit.clone(),
645            optimized_circuit,
646            diagram,
647            optimization_stats: optimization_result,
648        })
649    }
650
651    /// Extract a quantum circuit from a ZX diagram.
652    ///
653    /// General ZX-diagram extraction (recovering a circuit from an arbitrary,
654    /// entangled, post-optimization diagram) requires gflow-based synthesis and
655    /// is intentionally out of scope here.  This routine performs an **exact**
656    /// extraction for the class of diagrams that decompose into independent
657    /// per-qubit wires — i.e. circuits built only from single-qubit gates, plus
658    /// any diagram the rewrite rules reduce to that form.  Each wire is walked
659    /// from its `Input` to its `Output`, emitting one gate per degree-2 spider /
660    /// Hadamard encountered.
661    ///
662    /// If the diagram still contains entangling structure (a spider shared
663    /// between wires, e.g. a CNOT), this returns an honest
664    /// [`QuantRS2Error::UnsupportedOperation`] rather than silently dropping the
665    /// entangling gates and returning a circuit that is *not* equivalent.
666    fn zx_to_circuit<const N: usize>(&self, diagram: &ZXDiagram) -> QuantRS2Result<Circuit<N>> {
667        let mut circuit = Circuit::<N>::new();
668
669        for qubit in 0..N as u32 {
670            let Some(&input_id) = diagram.inputs.get(&qubit) else {
671                continue;
672            };
673            let Some(&output_id) = diagram.outputs.get(&qubit) else {
674                continue;
675            };
676
677            // Walk the wire from the input boundary to the output boundary.
678            let mut prev = input_id;
679            let mut current_neighbors = diagram.neighbors(input_id).to_vec();
680            // An input is degree-1 in a well-formed diagram; follow its single edge.
681            let mut current = match current_neighbors.as_slice() {
682                [next] => *next,
683                [] => continue, // disconnected boundary: nothing on this wire
684                _ => {
685                    return Err(QuantRS2Error::UnsupportedOperation(format!(
686                        "ZX extraction: input boundary for qubit {qubit} has degree \
687                         {} (expected 1); entangled diagrams are not supported",
688                        current_neighbors.len()
689                    )))
690                }
691            };
692
693            let mut guard = 0usize;
694            let node_budget = diagram.nodes.len() + 1;
695            while current != output_id {
696                guard += 1;
697                if guard > node_budget {
698                    return Err(QuantRS2Error::ComputationError(
699                        "ZX extraction: wire traversal did not terminate (cycle in diagram)"
700                            .to_string(),
701                    ));
702                }
703
704                let node = diagram.nodes.get(&current).ok_or_else(|| {
705                    QuantRS2Error::ComputationError(format!(
706                        "ZX extraction: dangling node reference {current}"
707                    ))
708                })?;
709                current_neighbors = diagram.neighbors(current).to_vec();
710
711                // Only degree-2 (pass-through) nodes can be extracted as a wire
712                // element; higher degree means the node entangles wires.
713                if current_neighbors.len() != 2 {
714                    return Err(QuantRS2Error::UnsupportedOperation(format!(
715                        "ZX extraction: node {current} on qubit {qubit} has degree {} \
716                         (expected 2); entangling structure cannot be extracted by the \
717                         linear-wire extractor",
718                        current_neighbors.len()
719                    )));
720                }
721
722                // Emit the gate corresponding to this node.
723                let target = QubitId(qubit);
724                match node {
725                    ZXNode::ZSpider { phase, .. } => {
726                        emit_phase_gate(&mut circuit, target, *phase, true)?;
727                    }
728                    ZXNode::XSpider { phase, .. } => {
729                        emit_phase_gate(&mut circuit, target, *phase, false)?;
730                    }
731                    ZXNode::Hadamard { .. } => {
732                        circuit.h(target)?;
733                    }
734                    ZXNode::Input { .. } | ZXNode::Output { .. } => {
735                        return Err(QuantRS2Error::ComputationError(format!(
736                            "ZX extraction: unexpected boundary node {current} in wire interior"
737                        )));
738                    }
739                }
740
741                // Step to the neighbor that is not where we came from.
742                let next = if current_neighbors[0] == prev {
743                    current_neighbors[1]
744                } else {
745                    current_neighbors[0]
746                };
747                prev = current;
748                current = next;
749            }
750        }
751
752        Ok(circuit)
753    }
754}
755
756/// Emit the single-qubit gate for a degree-2 spider of the given color.
757///
758/// A phase of (multiples of) `π` collapses to the corresponding Pauli; `π/2`
759/// Z-spiders become `S`; otherwise a parameterized rotation is emitted.  A
760/// phase-0 spider is the identity and emits nothing.
761fn emit_phase_gate<const N: usize>(
762    circuit: &mut Circuit<N>,
763    target: QubitId,
764    phase: f64,
765    is_z: bool,
766) -> QuantRS2Result<()> {
767    let two_pi = 2.0 * PI;
768    // Normalize the phase into [0, 2π).
769    let phase = phase.rem_euclid(two_pi);
770    if phase.abs() < 1e-10 || (phase - two_pi).abs() < 1e-10 {
771        return Ok(()); // identity spider
772    }
773
774    if (phase - PI).abs() < 1e-10 {
775        // Pauli.
776        if is_z {
777            circuit.z(target)?;
778        } else {
779            circuit.x(target)?;
780        }
781    } else if is_z {
782        circuit.rz(target, phase)?;
783    } else {
784        circuit.rx(target, phase)?;
785    }
786    Ok(())
787}
788
789/// Result of ZX optimization containing original and optimized circuits
790#[derive(Debug)]
791pub struct OptimizedZXResult<const N: usize> {
792    pub original_circuit: Circuit<N>,
793    pub optimized_circuit: Circuit<N>,
794    pub diagram: ZXDiagram,
795    pub optimization_stats: ZXOptimizationResult,
796}
797
798#[cfg(test)]
799mod tests {
800    use super::*;
801    use quantrs2_core::gate::multi::CNOT;
802    use quantrs2_core::gate::single::Hadamard;
803
804    #[test]
805    fn test_zx_diagram_creation() {
806        let mut diagram = ZXDiagram::new();
807        diagram.initialize_boundaries(2);
808
809        assert_eq!(diagram.inputs.len(), 2);
810        assert_eq!(diagram.outputs.len(), 2);
811    }
812
813    #[test]
814    fn test_spider_fusion() {
815        let mut diagram = ZXDiagram::new();
816
817        // Add two Z-spiders with phases π/4 and π/8
818        let spider1 = diagram.add_node(ZXNode::ZSpider {
819            id: 0,
820            phase: PI / 4.0,
821            arity: 2,
822        });
823        let spider2 = diagram.add_node(ZXNode::ZSpider {
824            id: 0,
825            phase: PI / 8.0,
826            arity: 2,
827        });
828
829        // Connect them
830        diagram.add_edge(spider1, spider2, false);
831
832        // Apply spider fusion
833        let changed = diagram.spider_fusion();
834        assert!(changed);
835
836        // One spider should be removed
837        assert_eq!(diagram.nodes.len(), 1);
838
839        // Remaining spider should have combined phase
840        let remaining_node = diagram
841            .nodes
842            .values()
843            .next()
844            .expect("Expected at least one remaining node after fusion");
845        assert!((remaining_node.phase() - (PI / 4.0 + PI / 8.0)).abs() < 1e-10);
846    }
847
848    #[test]
849    fn test_identity_removal() {
850        let mut diagram = ZXDiagram::new();
851
852        // Add identity spider (phase 0, arity 2)
853        let identity = diagram.add_node(ZXNode::ZSpider {
854            id: 0,
855            phase: 0.0,
856            arity: 2,
857        });
858
859        // Add two other nodes
860        let node1 = diagram.add_node(ZXNode::ZSpider {
861            id: 0,
862            phase: PI / 4.0,
863            arity: 2,
864        });
865        let node2 = diagram.add_node(ZXNode::ZSpider {
866            id: 0,
867            phase: PI / 2.0,
868            arity: 2,
869        });
870
871        // Connect through identity
872        diagram.add_edge(node1, identity, false);
873        diagram.add_edge(identity, node2, false);
874
875        let initial_count = diagram.nodes.len();
876        let changed = diagram.identity_removal();
877
878        assert!(changed);
879        assert_eq!(diagram.nodes.len(), initial_count - 1);
880    }
881
882    #[test]
883    fn test_circuit_to_zx_conversion() {
884        let optimizer = ZXOptimizer::new();
885
886        let mut circuit = Circuit::<2>::new();
887        circuit
888            .add_gate(Hadamard { target: QubitId(0) })
889            .expect("Failed to add Hadamard gate");
890        circuit
891            .add_gate(CNOT {
892                control: QubitId(0),
893                target: QubitId(1),
894            })
895            .expect("Failed to add CNOT gate");
896
897        let diagram = optimizer
898            .circuit_to_zx(&circuit)
899            .expect("Failed to convert circuit to ZX diagram");
900
901        // Should have input/output nodes plus gate nodes
902        assert!(diagram.nodes.len() >= 4); // 2 inputs + 2 outputs + gate nodes
903        assert!(!diagram.edges.is_empty());
904    }
905
906    #[test]
907    fn test_zx_optimization() {
908        let optimizer = ZXOptimizer::new();
909
910        let mut circuit = Circuit::<1>::new();
911        circuit
912            .add_gate(Hadamard { target: QubitId(0) })
913            .expect("Failed to add first Hadamard gate");
914        circuit
915            .add_gate(Hadamard { target: QubitId(0) })
916            .expect("Failed to add second Hadamard gate"); // Should cancel out
917
918        let result = optimizer
919            .optimize_circuit(&circuit)
920            .expect("Failed to optimize circuit");
921
922        assert!(
923            result.optimization_stats.final_node_count
924                <= result.optimization_stats.initial_node_count
925        );
926    }
927
928    /// `extract_rotation_angle` must read the real gate angle, not a hardcoded
929    /// `π/4`.
930    #[test]
931    fn test_extract_rotation_angle_reads_real_theta() {
932        use quantrs2_core::gate::single::{RotationX, RotationY, RotationZ};
933        let optimizer = ZXOptimizer::new();
934
935        let rz = RotationZ {
936            target: QubitId(0),
937            theta: 0.123,
938        };
939        assert!((optimizer.extract_rotation_angle(&rz) - 0.123).abs() < 1e-12);
940
941        let rx = RotationX {
942            target: QubitId(0),
943            theta: 1.75,
944        };
945        assert!((optimizer.extract_rotation_angle(&rx) - 1.75).abs() < 1e-12);
946
947        let ry = RotationY {
948            target: QubitId(0),
949            theta: -0.6,
950        };
951        assert!((optimizer.extract_rotation_angle(&ry) + 0.6).abs() < 1e-12);
952
953        // A non-rotation gate must NOT report the bogus π/4.
954        let h = Hadamard { target: QubitId(0) };
955        assert!(optimizer.extract_rotation_angle(&h).abs() < 1e-12);
956    }
957
958    /// A single-qubit gate chain must extract back to a non-empty circuit
959    /// carrying the real gates — not the former empty placeholder circuit.
960    ///
961    /// We extract directly from the converted diagram (without running the lossy
962    /// optimize pass) to isolate the extractor: H; RZ(0.4); Z on one wire must
963    /// come back as H, RZ(0.4), Z.  (`circuit_to_zx` encodes a Pauli-Z as a
964    /// phase-π Z-spider, which the extractor inverts back to a Z gate.)
965    #[test]
966    fn test_zx_to_circuit_extracts_single_qubit_chain() {
967        use quantrs2_core::gate::single::{PauliZ, RotationZ};
968        let optimizer = ZXOptimizer::new();
969
970        let mut circuit = Circuit::<1>::new();
971        circuit
972            .add_gate(Hadamard { target: QubitId(0) })
973            .expect("h");
974        circuit
975            .add_gate(RotationZ {
976                target: QubitId(0),
977                theta: 0.4,
978            })
979            .expect("rz");
980        circuit.add_gate(PauliZ { target: QubitId(0) }).expect("z");
981
982        let diagram = optimizer.circuit_to_zx(&circuit).expect("to zx");
983        let extracted: Circuit<1> = optimizer.zx_to_circuit(&diagram).expect("extract");
984
985        let names: Vec<&str> = extracted.gates().iter().map(|g| g.name()).collect();
986        // H stays H; RZ(0.4) stays RZ; phase-π Z-spider extracts back to Z.
987        assert_eq!(names, vec!["H", "RZ", "Z"], "got {names:?}");
988
989        // The RZ must carry the real angle (0.4), proving extract_rotation_angle
990        // and the phase round-trip are real (not a fabricated π/4).
991        let rz = extracted
992            .gates()
993            .iter()
994            .find(|g| g.name() == "RZ")
995            .expect("rz present");
996        let rz_concrete = rz
997            .as_any()
998            .downcast_ref::<RotationZ>()
999            .expect("downcast RZ");
1000        assert!(
1001            (rz_concrete.theta - 0.4).abs() < 1e-10,
1002            "RZ angle {}",
1003            rz_concrete.theta
1004        );
1005    }
1006
1007    /// Extracting a circuit that still contains entangling structure (a CNOT)
1008    /// must return an HONEST error rather than silently dropping the CNOT and
1009    /// returning a non-equivalent circuit.
1010    #[test]
1011    fn test_zx_to_circuit_errors_on_entangling_diagram() {
1012        let optimizer = ZXOptimizer::new();
1013
1014        let mut circuit = Circuit::<2>::new();
1015        circuit
1016            .add_gate(CNOT {
1017                control: QubitId(0),
1018                target: QubitId(1),
1019            })
1020            .expect("cnot");
1021
1022        let result = optimizer.optimize_circuit(&circuit);
1023        assert!(
1024            result.is_err(),
1025            "entangling diagram extraction must error, not fabricate an empty circuit"
1026        );
1027    }
1028
1029    /// An empty single-qubit circuit (or one that cancels to identity) extracts
1030    /// to an empty circuit successfully.
1031    #[test]
1032    fn test_zx_to_circuit_identity_is_empty() {
1033        let optimizer = ZXOptimizer::new();
1034
1035        let mut circuit = Circuit::<1>::new();
1036        circuit
1037            .add_gate(Hadamard { target: QubitId(0) })
1038            .expect("h1");
1039        circuit
1040            .add_gate(Hadamard { target: QubitId(0) })
1041            .expect("h2");
1042
1043        let result = optimizer
1044            .optimize_circuit(&circuit)
1045            .expect("optimize identity");
1046        assert_eq!(result.optimized_circuit.gates().len(), 0);
1047    }
1048}