quantrs2_core/
zx_calculus.rs

1//! ZX-calculus primitives for quantum circuit optimization
2//!
3//! This module implements the ZX-calculus, a graphical language for quantum computing
4//! that enables powerful circuit optimization through graph rewriting rules.
5//!
6//! The ZX-calculus represents quantum computations as graphs with:
7//! - Green nodes (Z-spiders): Phase rotations in the Z basis
8//! - Red nodes (X-spiders): Phase rotations in the X basis
9//! - Edges: Quantum entanglement
10//! - Hadamard edges: Basis changes
11
12use crate::{
13    error::{QuantRS2Error, QuantRS2Result},
14    gate::{single::*, GateOp},
15    qubit::QubitId,
16};
17use rustc_hash::FxHashMap;
18use std::collections::{HashSet, VecDeque};
19use std::f64::consts::PI;
20
21use std::fmt::Write;
22/// Type of spider in the ZX-diagram
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub enum SpiderType {
25    /// Z-spider (green node) - computational basis
26    Z,
27    /// X-spider (red node) - Hadamard basis
28    X,
29    /// Boundary node (input/output)
30    Boundary,
31}
32
33/// Represents a spider (node) in a ZX-diagram
34#[derive(Debug, Clone)]
35pub struct Spider {
36    /// Unique identifier for the spider
37    pub id: usize,
38    /// Type of spider
39    pub spider_type: SpiderType,
40    /// Phase angle (in radians)
41    pub phase: f64,
42    /// Qubit this spider is associated with (for boundary spiders)
43    pub qubit: Option<QubitId>,
44}
45
46impl Spider {
47    /// Create a new spider
48    pub const fn new(id: usize, spider_type: SpiderType, phase: f64) -> Self {
49        Self {
50            id,
51            spider_type,
52            phase,
53            qubit: None,
54        }
55    }
56
57    /// Create a boundary spider
58    pub const fn boundary(id: usize, qubit: QubitId) -> Self {
59        Self {
60            id,
61            spider_type: SpiderType::Boundary,
62            phase: 0.0,
63            qubit: Some(qubit),
64        }
65    }
66
67    /// Check if this is a Clifford spider (phase is multiple of π/2)
68    pub fn is_clifford(&self, tolerance: f64) -> bool {
69        let normalized_phase = self.phase % (2.0 * PI);
70        let multiples_of_pi_2 = [0.0, PI / 2.0, PI, 3.0 * PI / 2.0];
71
72        multiples_of_pi_2.iter().any(|&p| {
73            (normalized_phase - p).abs() < tolerance
74                || 2.0f64.mul_add(PI, normalized_phase - p).abs() < tolerance
75        })
76    }
77
78    /// Check if this is a Pauli spider (phase is 0 or π)
79    pub fn is_pauli(&self, tolerance: f64) -> bool {
80        let normalized_phase = self.phase % (2.0 * PI);
81        normalized_phase.abs() < tolerance
82            || (normalized_phase - PI).abs() < tolerance
83            || 2.0f64.mul_add(-PI, normalized_phase).abs() < tolerance
84    }
85}
86
87/// Type of edge in the ZX-diagram
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub enum EdgeType {
90    /// Regular edge (identity)
91    Regular,
92    /// Hadamard edge
93    Hadamard,
94}
95
96/// Represents an edge in a ZX-diagram
97#[derive(Debug, Clone)]
98pub struct Edge {
99    /// Source spider ID
100    pub source: usize,
101    /// Target spider ID
102    pub target: usize,
103    /// Type of edge
104    pub edge_type: EdgeType,
105}
106
107/// ZX-diagram representation
108#[derive(Debug, Clone)]
109pub struct ZXDiagram {
110    /// Spiders (nodes) in the diagram
111    pub spiders: FxHashMap<usize, Spider>,
112    /// Adjacency list representation of edges
113    pub adjacency: FxHashMap<usize, Vec<(usize, EdgeType)>>,
114    /// Input boundary spiders (ordered by qubit)
115    pub inputs: Vec<usize>,
116    /// Output boundary spiders (ordered by qubit)
117    pub outputs: Vec<usize>,
118    /// Next available spider ID
119    next_id: usize,
120}
121
122impl ZXDiagram {
123    /// Create a new empty ZX-diagram
124    pub fn new() -> Self {
125        Self {
126            spiders: FxHashMap::default(),
127            adjacency: FxHashMap::default(),
128            inputs: Vec::new(),
129            outputs: Vec::new(),
130            next_id: 0,
131        }
132    }
133
134    /// Add a spider to the diagram
135    pub fn add_spider(&mut self, spider_type: SpiderType, phase: f64) -> usize {
136        let id = self.next_id;
137        self.next_id += 1;
138
139        let spider = Spider::new(id, spider_type, phase);
140        self.spiders.insert(id, spider);
141        self.adjacency.insert(id, Vec::new());
142
143        id
144    }
145
146    /// Add a boundary spider
147    pub fn add_boundary(&mut self, qubit: QubitId, is_input: bool) -> usize {
148        let id = self.next_id;
149        self.next_id += 1;
150
151        let spider = Spider::boundary(id, qubit);
152        self.spiders.insert(id, spider);
153        self.adjacency.insert(id, Vec::new());
154
155        if is_input {
156            self.inputs.push(id);
157        } else {
158            self.outputs.push(id);
159        }
160
161        id
162    }
163
164    /// Add an edge between two spiders
165    pub fn add_edge(&mut self, source: usize, target: usize, edge_type: EdgeType) {
166        if let Some(adj) = self.adjacency.get_mut(&source) {
167            adj.push((target, edge_type));
168        }
169        if let Some(adj) = self.adjacency.get_mut(&target) {
170            adj.push((source, edge_type));
171        }
172    }
173
174    /// Remove an edge between two spiders
175    pub fn remove_edge(&mut self, source: usize, target: usize) {
176        if let Some(adj) = self.adjacency.get_mut(&source) {
177            adj.retain(|(t, _)| *t != target);
178        }
179        if let Some(adj) = self.adjacency.get_mut(&target) {
180            adj.retain(|(s, _)| *s != source);
181        }
182    }
183
184    /// Get the neighbors of a spider
185    pub fn neighbors(&self, spider_id: usize) -> Vec<(usize, EdgeType)> {
186        self.adjacency.get(&spider_id).cloned().unwrap_or_default()
187    }
188
189    /// Remove a spider and its edges
190    pub fn remove_spider(&mut self, spider_id: usize) {
191        // Remove from spider list
192        self.spiders.remove(&spider_id);
193
194        // Remove all edges connected to this spider
195        let neighbors = self.neighbors(spider_id);
196        for (neighbor, _) in neighbors {
197            self.remove_edge(spider_id, neighbor);
198        }
199
200        // Remove from adjacency list
201        self.adjacency.remove(&spider_id);
202
203        // Remove from inputs/outputs if present
204        self.inputs.retain(|&id| id != spider_id);
205        self.outputs.retain(|&id| id != spider_id);
206    }
207
208    /// Get the degree (number of connections) of a spider
209    pub fn degree(&self, spider_id: usize) -> usize {
210        self.adjacency.get(&spider_id).map_or(0, |adj| adj.len())
211    }
212
213    /// Apply spider fusion rule: two adjacent spiders of the same color merge
214    pub fn spider_fusion(&mut self, spider1: usize, spider2: usize) -> QuantRS2Result<()> {
215        let s1 = self
216            .spiders
217            .get(&spider1)
218            .ok_or_else(|| QuantRS2Error::InvalidInput("Spider 1 not found".to_string()))?;
219        let s2 = self
220            .spiders
221            .get(&spider2)
222            .ok_or_else(|| QuantRS2Error::InvalidInput("Spider 2 not found".to_string()))?;
223
224        // Check if spiders are the same type
225        if s1.spider_type != s2.spider_type || s1.spider_type == SpiderType::Boundary {
226            return Err(QuantRS2Error::InvalidInput(
227                "Can only fuse spiders of the same non-boundary type".to_string(),
228            ));
229        }
230
231        // Check if spiders are connected
232        let connected = self.neighbors(spider1).iter().any(|(id, _)| *id == spider2);
233
234        if !connected {
235            return Err(QuantRS2Error::InvalidInput(
236                "Spiders must be connected".to_string(),
237            ));
238        }
239
240        // Combine phases
241        let new_phase = s1.phase + s2.phase;
242
243        // Update spider1 with combined phase
244        if let Some(s1_mut) = self.spiders.get_mut(&spider1) {
245            s1_mut.phase = new_phase;
246        }
247
248        // Transfer all edges from spider2 to spider1
249        let spider2_neighbors = self.neighbors(spider2);
250        for (neighbor, edge_type) in spider2_neighbors {
251            if neighbor != spider1 {
252                self.remove_edge(spider2, neighbor);
253                self.add_edge(spider1, neighbor, edge_type);
254            }
255        }
256
257        // Remove spider2
258        self.remove_spider(spider2);
259
260        Ok(())
261    }
262
263    /// Apply identity removal: remove spiders with phase 0 and degree 2
264    pub fn remove_identities(&mut self) -> usize {
265        let mut removed = 0;
266        let mut to_remove = Vec::new();
267
268        for (&id, spider) in &self.spiders {
269            if spider.spider_type != SpiderType::Boundary
270                && spider.phase.abs() < 1e-10
271                && self.degree(id) == 2
272            {
273                to_remove.push(id);
274            }
275        }
276
277        for id in to_remove {
278            let neighbors = self.neighbors(id);
279            if neighbors.len() == 2 {
280                let (n1, e1) = neighbors[0];
281                let (n2, e2) = neighbors[1];
282
283                // Connect the two neighbors
284                let new_edge_type = match (e1, e2) {
285                    (EdgeType::Regular, EdgeType::Regular)
286                    | (EdgeType::Hadamard, EdgeType::Hadamard) => EdgeType::Regular,
287                    _ => EdgeType::Hadamard,
288                };
289
290                self.remove_spider(id);
291                self.add_edge(n1, n2, new_edge_type);
292                removed += 1;
293            }
294        }
295
296        removed
297    }
298
299    /// Apply color change rule: X and Z spiders connected by Hadamard become the same color
300    pub fn color_change(&mut self, spider_id: usize) -> QuantRS2Result<()> {
301        let spider = self
302            .spiders
303            .get(&spider_id)
304            .ok_or_else(|| QuantRS2Error::InvalidInput("Spider not found".to_string()))?
305            .clone();
306
307        if spider.spider_type == SpiderType::Boundary {
308            return Err(QuantRS2Error::InvalidInput(
309                "Cannot change color of boundary spider".to_string(),
310            ));
311        }
312
313        // Change spider color
314        let new_type = match spider.spider_type {
315            SpiderType::Z => SpiderType::X,
316            SpiderType::X => SpiderType::Z,
317            SpiderType::Boundary => return Ok(()),
318        };
319
320        if let Some(s) = self.spiders.get_mut(&spider_id) {
321            s.spider_type = new_type;
322        }
323
324        // Flip all edge types
325        let neighbors = self.neighbors(spider_id);
326        for (neighbor, edge_type) in neighbors {
327            self.remove_edge(spider_id, neighbor);
328            let new_edge_type = match edge_type {
329                EdgeType::Regular => EdgeType::Hadamard,
330                EdgeType::Hadamard => EdgeType::Regular,
331            };
332            self.add_edge(spider_id, neighbor, new_edge_type);
333        }
334
335        Ok(())
336    }
337
338    /// Apply pi-copy rule: Pauli spider can be copied through
339    pub fn pi_copy(&mut self, spider_id: usize) -> QuantRS2Result<Vec<usize>> {
340        let spider = self
341            .spiders
342            .get(&spider_id)
343            .ok_or_else(|| QuantRS2Error::InvalidInput("Spider not found".to_string()))?
344            .clone();
345
346        if !spider.is_pauli(1e-10) {
347            return Err(QuantRS2Error::InvalidInput(
348                "Spider must be Pauli (phase 0 or π)".to_string(),
349            ));
350        }
351
352        let neighbors = self.neighbors(spider_id);
353        let mut new_spiders = Vec::new();
354
355        // Create a copy for each neighbor
356        for (neighbor, edge_type) in &neighbors[1..] {
357            let new_id = self.add_spider(spider.spider_type, spider.phase);
358            new_spiders.push(new_id);
359
360            // Connect new spider to the neighbor
361            self.remove_edge(spider_id, *neighbor);
362            self.add_edge(new_id, *neighbor, *edge_type);
363
364            // Connect new spider to the first neighbor
365            if let Some((first_neighbor, first_edge_type)) = neighbors.first() {
366                self.add_edge(new_id, *first_neighbor, *first_edge_type);
367            }
368        }
369
370        Ok(new_spiders)
371    }
372
373    /// Apply bialgebra rule
374    pub fn bialgebra(&mut self, z_spider: usize, x_spider: usize) -> QuantRS2Result<()> {
375        let z = self
376            .spiders
377            .get(&z_spider)
378            .ok_or_else(|| QuantRS2Error::InvalidInput("Z-spider not found".to_string()))?;
379        let x = self
380            .spiders
381            .get(&x_spider)
382            .ok_or_else(|| QuantRS2Error::InvalidInput("X-spider not found".to_string()))?;
383
384        // Check spider types
385        if z.spider_type != SpiderType::Z || x.spider_type != SpiderType::X {
386            return Err(QuantRS2Error::InvalidInput(
387                "Need one Z-spider and one X-spider".to_string(),
388            ));
389        }
390
391        // Check if connected
392        let connected = self
393            .neighbors(z_spider)
394            .iter()
395            .any(|(id, _)| *id == x_spider);
396
397        if !connected {
398            return Err(QuantRS2Error::InvalidInput(
399                "Spiders must be connected".to_string(),
400            ));
401        }
402
403        // Get all neighbors
404        let z_neighbors: Vec<_> = self
405            .neighbors(z_spider)
406            .into_iter()
407            .filter(|(id, _)| *id != x_spider)
408            .collect();
409        let x_neighbors: Vec<_> = self
410            .neighbors(x_spider)
411            .into_iter()
412            .filter(|(id, _)| *id != z_spider)
413            .collect();
414
415        // Remove original spiders
416        self.remove_spider(z_spider);
417        self.remove_spider(x_spider);
418
419        // Create new connections
420        for (z_n, z_edge) in &z_neighbors {
421            for (x_n, x_edge) in &x_neighbors {
422                // Determine edge type based on the rule
423                let edge_type = match (z_edge, x_edge) {
424                    (EdgeType::Regular, EdgeType::Regular)
425                    | (EdgeType::Hadamard, EdgeType::Hadamard) => EdgeType::Regular,
426                    _ => EdgeType::Hadamard,
427                };
428                self.add_edge(*z_n, *x_n, edge_type);
429            }
430        }
431
432        Ok(())
433    }
434
435    /// Simplify the diagram by applying rewrite rules
436    pub fn simplify(&mut self, max_iterations: usize) -> usize {
437        let mut total_rewrites = 0;
438
439        for _ in 0..max_iterations {
440            let mut rewrites = 0;
441
442            // Phase 1: Basic cleanup
443            rewrites += self.remove_identities();
444
445            // Phase 2: Spider fusion
446            rewrites += self.apply_spider_fusion();
447
448            // Phase 3: Advanced optimizations
449            rewrites += self.apply_pivot_rules();
450            rewrites += self.apply_local_complementation();
451            rewrites += self.apply_stabilizer_decomposition();
452
453            // Phase 4: Clifford circuit extraction and optimization
454            rewrites += self.optimize_clifford_subcircuits();
455
456            total_rewrites += rewrites;
457            if rewrites == 0 {
458                break;
459            }
460        }
461
462        total_rewrites
463    }
464
465    /// Apply spider fusion systematically
466    fn apply_spider_fusion(&mut self) -> usize {
467        let mut rewrites = 0;
468        let spider_ids: Vec<_> = self.spiders.keys().copied().collect();
469
470        for i in 0..spider_ids.len() {
471            for j in i + 1..spider_ids.len() {
472                let id1 = spider_ids[i];
473                let id2 = spider_ids[j];
474
475                // Check if spiders still exist and can be fused
476                if self.spiders.contains_key(&id1)
477                    && self.spiders.contains_key(&id2)
478                    && self.spider_fusion(id1, id2) == Ok(())
479                {
480                    rewrites += 1;
481                    break;
482                }
483            }
484            if rewrites > 0 {
485                break;
486            }
487        }
488
489        rewrites
490    }
491
492    /// Apply pivot rules for graph state optimization
493    fn apply_pivot_rules(&mut self) -> usize {
494        let mut rewrites = 0;
495        let spider_ids: Vec<_> = self.spiders.keys().copied().collect();
496
497        for &spider_id in &spider_ids {
498            if !self.spiders.contains_key(&spider_id) {
499                continue;
500            }
501
502            let spider = self.spiders[&spider_id].clone();
503
504            // Apply pivot if this is a green spider (Z-spider) with even phase
505            if spider.spider_type == SpiderType::Z
506                && self.is_even_multiple_of_pi(spider.phase, 1e-10)
507                && self.degree(spider_id) >= 2
508                && self.pivot_around_spider(spider_id) == Ok(())
509            {
510                rewrites += 1;
511                break;
512            }
513        }
514
515        rewrites
516    }
517
518    /// Apply local complementation rules
519    fn apply_local_complementation(&mut self) -> usize {
520        let mut rewrites = 0;
521        let spider_ids: Vec<_> = self.spiders.keys().copied().collect();
522
523        for &spider_id in &spider_ids {
524            if !self.spiders.contains_key(&spider_id) {
525                continue;
526            }
527
528            let spider = self.spiders[&spider_id].clone();
529
530            // Apply local complementation for certain patterns
531            if spider.spider_type == SpiderType::X
532                && self.is_odd_multiple_of_pi(spider.phase, 1e-10)
533                && self.degree(spider_id) >= 3
534                && self.local_complement_around_spider(spider_id) == Ok(())
535            {
536                rewrites += 1;
537                break;
538            }
539        }
540
541        rewrites
542    }
543
544    /// Apply stabilizer decomposition for Clifford subcircuits
545    fn apply_stabilizer_decomposition(&mut self) -> usize {
546        let mut rewrites = 0;
547
548        // Find connected Clifford components
549        let clifford_components = self.find_clifford_components();
550
551        for component in clifford_components {
552            if component.len() > 2 && self.decompose_clifford_component(&component) == Ok(()) {
553                rewrites += 1;
554            }
555        }
556
557        rewrites
558    }
559
560    /// Optimize Clifford subcircuits using tableau methods
561    fn optimize_clifford_subcircuits(&self) -> usize {
562        let mut rewrites = 0;
563
564        // Extract Clifford parts and optimize using tableau representation
565        let clifford_regions = self.identify_clifford_regions();
566
567        for region in clifford_regions {
568            let optimized_size = self.optimize_clifford_region(&region);
569            if optimized_size < region.len() {
570                rewrites += region.len() - optimized_size;
571            }
572        }
573
574        rewrites
575    }
576
577    /// Check if phase is an even multiple of π
578    fn is_even_multiple_of_pi(&self, phase: f64, tolerance: f64) -> bool {
579        let normalized = (phase / PI) % 2.0;
580        normalized.abs() < tolerance || (normalized - 2.0).abs() < tolerance
581    }
582
583    /// Check if phase is an odd multiple of π
584    fn is_odd_multiple_of_pi(&self, phase: f64, tolerance: f64) -> bool {
585        let normalized = (phase / PI) % 2.0;
586        (normalized - 1.0).abs() < tolerance
587    }
588
589    /// Pivot around a spider (local complementation + color change)
590    fn pivot_around_spider(&mut self, spider_id: usize) -> QuantRS2Result<()> {
591        let neighbors = self.neighbors(spider_id);
592
593        // Add edges between all pairs of neighbors
594        for i in 0..neighbors.len() {
595            for j in i + 1..neighbors.len() {
596                let (n1, _) = neighbors[i];
597                let (n2, _) = neighbors[j];
598
599                // Check if edge already exists
600                let existing_edge = self.neighbors(n1).iter().any(|(id, _)| *id == n2);
601
602                if existing_edge {
603                    // Remove edge if it exists
604                    self.remove_edge(n1, n2);
605                } else {
606                    // Add edge if it doesn't exist
607                    self.add_edge(n1, n2, EdgeType::Regular);
608                }
609            }
610        }
611
612        // Remove the pivot spider
613        self.remove_spider(spider_id);
614
615        Ok(())
616    }
617
618    /// Apply local complementation around a spider
619    fn local_complement_around_spider(&mut self, spider_id: usize) -> QuantRS2Result<()> {
620        let neighbors = self.neighbors(spider_id);
621
622        // Toggle edges between neighbors
623        for i in 0..neighbors.len() {
624            for j in i + 1..neighbors.len() {
625                let (n1, _) = neighbors[i];
626                let (n2, _) = neighbors[j];
627
628                let existing_edge = self
629                    .neighbors(n1)
630                    .iter()
631                    .find(|(id, _edge_type)| *id == n2)
632                    .map(|(_, edge_type)| *edge_type);
633
634                match existing_edge {
635                    Some(EdgeType::Regular) => {
636                        self.remove_edge(n1, n2);
637                        self.add_edge(n1, n2, EdgeType::Hadamard);
638                    }
639                    Some(EdgeType::Hadamard) => {
640                        self.remove_edge(n1, n2);
641                        self.add_edge(n1, n2, EdgeType::Regular);
642                    }
643                    None => {
644                        self.add_edge(n1, n2, EdgeType::Regular);
645                    }
646                }
647            }
648        }
649
650        // Flip the phase of the central spider
651        if let Some(spider) = self.spiders.get_mut(&spider_id) {
652            spider.phase += PI;
653        }
654
655        Ok(())
656    }
657
658    /// Find connected components of Clifford spiders
659    fn find_clifford_components(&self) -> Vec<Vec<usize>> {
660        let mut visited = HashSet::new();
661        let mut components = Vec::new();
662
663        for &spider_id in self.spiders.keys() {
664            if !visited.contains(&spider_id) {
665                let spider = &self.spiders[&spider_id];
666
667                if spider.spider_type != SpiderType::Boundary && spider.is_clifford(1e-10) {
668                    let component = self.dfs_clifford_component(spider_id, &mut visited);
669                    if component.len() > 1 {
670                        components.push(component);
671                    }
672                }
673            }
674        }
675
676        components
677    }
678
679    /// DFS to find Clifford component
680    fn dfs_clifford_component(&self, start: usize, visited: &mut HashSet<usize>) -> Vec<usize> {
681        let mut component = Vec::new();
682        let mut stack = vec![start];
683
684        while let Some(spider_id) = stack.pop() {
685            if visited.contains(&spider_id) {
686                continue;
687            }
688
689            visited.insert(spider_id);
690
691            if let Some(spider) = self.spiders.get(&spider_id) {
692                if spider.spider_type != SpiderType::Boundary && spider.is_clifford(1e-10) {
693                    component.push(spider_id);
694
695                    // Add neighbors to stack
696                    for (neighbor, _) in self.neighbors(spider_id) {
697                        if !visited.contains(&neighbor) {
698                            if let Some(neighbor_spider) = self.spiders.get(&neighbor) {
699                                if neighbor_spider.spider_type != SpiderType::Boundary
700                                    && neighbor_spider.is_clifford(1e-10)
701                                {
702                                    stack.push(neighbor);
703                                }
704                            }
705                        }
706                    }
707                }
708            }
709        }
710
711        component
712    }
713
714    /// Decompose a Clifford component using stabilizer tableau
715    fn decompose_clifford_component(&mut self, component: &[usize]) -> QuantRS2Result<()> {
716        // This is a placeholder for advanced Clifford optimization
717        // In a full implementation, this would:
718        // 1. Extract the Clifford subcircuit
719        // 2. Convert to stabilizer tableau representation
720        // 3. Optimize using Gaussian elimination
721        // 4. Convert back to ZX-diagram
722
723        // For now, just remove redundant identity spiders in the component
724        for &spider_id in component {
725            if let Some(spider) = self.spiders.get(&spider_id) {
726                if spider.phase.abs() < 1e-10 && self.degree(spider_id) == 2 {
727                    let neighbors = self.neighbors(spider_id);
728                    if neighbors.len() == 2 {
729                        let (n1, e1) = neighbors[0];
730                        let (n2, e2) = neighbors[1];
731
732                        let new_edge_type = match (e1, e2) {
733                            (EdgeType::Regular, EdgeType::Regular)
734                            | (EdgeType::Hadamard, EdgeType::Hadamard) => EdgeType::Regular,
735                            _ => EdgeType::Hadamard,
736                        };
737
738                        self.remove_spider(spider_id);
739                        self.add_edge(n1, n2, new_edge_type);
740                        return Ok(());
741                    }
742                }
743            }
744        }
745
746        Ok(())
747    }
748
749    /// Identify regions of Clifford gates for optimization
750    fn identify_clifford_regions(&self) -> Vec<Vec<usize>> {
751        let mut regions = Vec::new();
752        let mut visited = HashSet::new();
753
754        for &spider_id in self.spiders.keys() {
755            if !visited.contains(&spider_id) {
756                if let Some(spider) = self.spiders.get(&spider_id) {
757                    if spider.spider_type != SpiderType::Boundary && spider.is_clifford(1e-10) {
758                        let region = self.expand_clifford_region(spider_id, &mut visited);
759                        if region.len() >= 2 {
760                            regions.push(region);
761                        }
762                    }
763                }
764            }
765        }
766
767        regions
768    }
769
770    /// Expand a Clifford region using BFS
771    fn expand_clifford_region(&self, start: usize, visited: &mut HashSet<usize>) -> Vec<usize> {
772        let mut region = Vec::new();
773        let mut queue = VecDeque::new();
774
775        queue.push_back(start);
776        visited.insert(start);
777
778        while let Some(spider_id) = queue.pop_front() {
779            if let Some(spider) = self.spiders.get(&spider_id) {
780                if spider.spider_type != SpiderType::Boundary && spider.is_clifford(1e-10) {
781                    region.push(spider_id);
782
783                    // Add Clifford neighbors to queue
784                    for (neighbor, _) in self.neighbors(spider_id) {
785                        if let Some(neighbor_spider) = self.spiders.get(&neighbor) {
786                            if neighbor_spider.spider_type != SpiderType::Boundary
787                                && neighbor_spider.is_clifford(1e-10)
788                                && visited.insert(neighbor)
789                            {
790                                queue.push_back(neighbor);
791                            }
792                        }
793                    }
794                }
795            }
796        }
797
798        region
799    }
800
801    /// Optimize a Clifford region using tableau methods
802    const fn optimize_clifford_region(&self, region: &[usize]) -> usize {
803        // Placeholder for tableau-based Clifford optimization
804        // This would implement:
805        // 1. Conversion to stabilizer tableau
806        // 2. Gaussian elimination on the tableau
807        // 3. Synthesis of optimized Clifford circuit
808        // 4. Conversion back to ZX-diagram
809
810        // For now, return the original size
811        region.len()
812    }
813
814    /// Convert to GraphViz DOT format for visualization
815    pub fn to_dot(&self) -> String {
816        let mut dot = String::from("graph ZX {\n");
817        dot.push_str("  rankdir=LR;\n");
818
819        // Add spiders
820        for (id, spider) in &self.spiders {
821            let color = match spider.spider_type {
822                SpiderType::Z => "green",
823                SpiderType::X => "red",
824                SpiderType::Boundary => "black",
825            };
826            let shape = match spider.spider_type {
827                SpiderType::Boundary => "square",
828                _ => "circle",
829            };
830            let label = match spider.spider_type {
831                SpiderType::Boundary => {
832                    if let Some(qubit) = spider.qubit {
833                        format!("q{}", qubit.0)
834                    } else {
835                        String::new()
836                    }
837                }
838                _ => {
839                    if spider.phase.abs() < 1e-10 {
840                        String::new()
841                    } else {
842                        format!("{:.2}", spider.phase / PI)
843                    }
844                }
845            };
846
847            let _ = writeln!(
848                dot,
849                "  {id} [label=\"{label}\", color={color}, shape={shape}];"
850            );
851        }
852
853        // Add edges (avoid duplicates)
854        let mut processed = HashSet::new();
855        for (&source, neighbors) in &self.adjacency {
856            for &(target, edge_type) in neighbors {
857                let key = if source < target {
858                    (source, target)
859                } else {
860                    (target, source)
861                };
862
863                if processed.insert(key) {
864                    let style = match edge_type {
865                        EdgeType::Regular => "solid",
866                        EdgeType::Hadamard => "dashed",
867                    };
868
869                    let _ = writeln!(dot, "  {source} -- {target} [style={style}];");
870                }
871            }
872        }
873
874        dot.push_str("}\n");
875        dot
876    }
877}
878
879impl Default for ZXDiagram {
880    fn default() -> Self {
881        Self::new()
882    }
883}
884
885/// Advanced ZX-calculus optimizer with state-of-the-art optimization passes
886#[derive(Debug, Clone)]
887pub struct ZXOptimizer {
888    /// Maximum number of iterations per optimization pass
889    max_iterations: usize,
890    /// Whether to enable advanced optimization passes
891    enable_advanced: bool,
892    /// Whether to enable verbose output
893    verbose: bool,
894    /// Tolerance for numerical comparisons
895    tolerance: f64,
896}
897
898impl Default for ZXOptimizer {
899    fn default() -> Self {
900        Self {
901            max_iterations: 100,
902            enable_advanced: true,
903            verbose: false,
904            tolerance: 1e-10,
905        }
906    }
907}
908
909impl ZXOptimizer {
910    /// Create a new ZX optimizer
911    pub const fn new() -> Self {
912        Self {
913            max_iterations: 100,
914            enable_advanced: true,
915            verbose: false,
916            tolerance: 1e-10,
917        }
918    }
919
920    /// Set the maximum iterations
921    #[must_use]
922    pub const fn with_max_iterations(mut self, max_iterations: usize) -> Self {
923        self.max_iterations = max_iterations;
924        self
925    }
926
927    /// Enable or disable advanced optimization passes
928    #[must_use]
929    pub const fn with_advanced(mut self, enable_advanced: bool) -> Self {
930        self.enable_advanced = enable_advanced;
931        self
932    }
933
934    /// Enable verbose output
935    #[must_use]
936    pub const fn with_verbose(mut self, verbose: bool) -> Self {
937        self.verbose = verbose;
938        self
939    }
940
941    /// Set numerical tolerance
942    #[must_use]
943    pub const fn with_tolerance(mut self, tolerance: f64) -> Self {
944        self.tolerance = tolerance;
945        self
946    }
947
948    /// Apply comprehensive optimization to a ZX-diagram
949    pub fn optimize(&self, diagram: &mut ZXDiagram) -> usize {
950        let initial_size = diagram.spiders.len();
951
952        if self.verbose {
953            println!("ZX Optimizer: Starting with {initial_size} spiders");
954        }
955
956        let mut total_rewrites = 0;
957        let mut iteration = 0;
958
959        while iteration < self.max_iterations {
960            let iteration_rewrites = if self.enable_advanced {
961                self.apply_advanced_optimization(diagram)
962            } else {
963                diagram.simplify(1)
964            };
965
966            total_rewrites += iteration_rewrites;
967            iteration += 1;
968
969            if iteration_rewrites == 0 {
970                if self.verbose {
971                    println!("ZX Optimizer: Converged after {iteration} iterations");
972                }
973                break;
974            }
975
976            if self.verbose && iteration % 10 == 0 {
977                println!(
978                    "ZX Optimizer: Iteration {}, {} spiders remaining",
979                    iteration,
980                    diagram.spiders.len()
981                );
982            }
983        }
984
985        let final_size = diagram.spiders.len();
986        let reduction = if initial_size > 0 {
987            ((initial_size - final_size) * 100) / initial_size
988        } else {
989            0
990        };
991
992        if self.verbose {
993            println!(
994                "ZX Optimizer: Reduced from {initial_size} to {final_size} spiders ({reduction}% reduction, {total_rewrites} total rewrites)"
995            );
996        }
997
998        total_rewrites
999    }
1000
1001    /// Apply advanced optimization passes
1002    fn apply_advanced_optimization(&self, diagram: &mut ZXDiagram) -> usize {
1003        let mut rewrites = 0;
1004
1005        // Phase 1: Basic simplification
1006        rewrites += diagram.remove_identities();
1007        rewrites += diagram.apply_spider_fusion();
1008
1009        // Phase 2: Graph state optimization
1010        rewrites += self.apply_graph_state_optimization(diagram);
1011
1012        // Phase 3: Clifford optimization
1013        rewrites += self.apply_clifford_optimization(diagram);
1014
1015        // Phase 4: Phase polynomial optimization
1016        rewrites += self.apply_phase_polynomial_optimization(diagram);
1017
1018        // Phase 5: Routing optimization
1019        rewrites += self.apply_routing_optimization(diagram);
1020
1021        rewrites
1022    }
1023
1024    /// Apply graph state optimization techniques
1025    fn apply_graph_state_optimization(&self, diagram: &mut ZXDiagram) -> usize {
1026        let mut rewrites = 0;
1027
1028        // Apply pivot rules
1029        rewrites += diagram.apply_pivot_rules();
1030
1031        // Apply local complementation
1032        rewrites += diagram.apply_local_complementation();
1033
1034        // Apply graph state decomposition
1035        rewrites += self.apply_graph_state_decomposition(diagram);
1036
1037        rewrites
1038    }
1039
1040    /// Apply Clifford optimization techniques
1041    fn apply_clifford_optimization(&self, diagram: &mut ZXDiagram) -> usize {
1042        let mut rewrites = 0;
1043
1044        // Stabilizer decomposition
1045        rewrites += diagram.apply_stabilizer_decomposition();
1046
1047        // Clifford subcircuit optimization
1048        rewrites += diagram.optimize_clifford_subcircuits();
1049
1050        // Apply Clifford tableau reduction
1051        rewrites += self.apply_tableau_reduction(diagram);
1052
1053        rewrites
1054    }
1055
1056    /// Apply phase polynomial optimization
1057    fn apply_phase_polynomial_optimization(&self, diagram: &mut ZXDiagram) -> usize {
1058        let mut rewrites = 0;
1059
1060        // Extract phase polynomials
1061        let phase_polynomials = self.extract_phase_polynomials(diagram);
1062
1063        // Optimize each polynomial
1064        for polynomial in phase_polynomials {
1065            rewrites += self.optimize_phase_polynomial(diagram, &polynomial);
1066        }
1067
1068        rewrites
1069    }
1070
1071    /// Apply routing optimization for connectivity
1072    const fn apply_routing_optimization(&self, diagram: &mut ZXDiagram) -> usize {
1073        let mut rewrites = 0;
1074
1075        // Minimize CNOT count through routing optimization
1076        rewrites += self.optimize_cnot_routing(diagram);
1077
1078        // Apply commutation-based optimization
1079        rewrites += self.apply_commutation_optimization(diagram);
1080
1081        rewrites
1082    }
1083
1084    /// Apply graph state decomposition
1085    const fn apply_graph_state_decomposition(&self, _diagram: &mut ZXDiagram) -> usize {
1086        // Placeholder for graph state decomposition
1087        // This would implement decomposition of large graph states
1088        // into smaller, more efficient representations
1089        0
1090    }
1091
1092    /// Apply Clifford tableau reduction
1093    const fn apply_tableau_reduction(&self, _diagram: &mut ZXDiagram) -> usize {
1094        // Placeholder for tableau-based Clifford reduction
1095        // This would implement:
1096        // 1. Convert Clifford subcircuits to stabilizer tableaux
1097        // 2. Apply Gaussian elimination
1098        // 3. Synthesize optimized circuits
1099        0
1100    }
1101
1102    /// Extract phase polynomials from the diagram
1103    fn extract_phase_polynomials(&self, diagram: &ZXDiagram) -> Vec<Vec<usize>> {
1104        let mut polynomials = Vec::new();
1105        let mut visited = HashSet::new();
1106
1107        for &spider_id in diagram.spiders.keys() {
1108            if !visited.contains(&spider_id) {
1109                if let Some(spider) = diagram.spiders.get(&spider_id) {
1110                    if spider.spider_type != SpiderType::Boundary
1111                        && !spider.is_clifford(self.tolerance)
1112                    {
1113                        let polynomial =
1114                            self.extract_connected_polynomial(diagram, spider_id, &mut visited);
1115                        if polynomial.len() > 1 {
1116                            polynomials.push(polynomial);
1117                        }
1118                    }
1119                }
1120            }
1121        }
1122
1123        polynomials
1124    }
1125
1126    /// Extract a connected phase polynomial component
1127    fn extract_connected_polynomial(
1128        &self,
1129        diagram: &ZXDiagram,
1130        start: usize,
1131        visited: &mut HashSet<usize>,
1132    ) -> Vec<usize> {
1133        let mut polynomial = Vec::new();
1134        let mut queue = VecDeque::new();
1135
1136        queue.push_back(start);
1137        visited.insert(start);
1138
1139        while let Some(spider_id) = queue.pop_front() {
1140            if let Some(spider) = diagram.spiders.get(&spider_id) {
1141                if spider.spider_type != SpiderType::Boundary {
1142                    polynomial.push(spider_id);
1143
1144                    // Add non-Clifford neighbors
1145                    for (neighbor, _) in diagram.neighbors(spider_id) {
1146                        if let Some(neighbor_spider) = diagram.spiders.get(&neighbor) {
1147                            if neighbor_spider.spider_type != SpiderType::Boundary
1148                                && !neighbor_spider.is_clifford(self.tolerance)
1149                                && visited.insert(neighbor)
1150                            {
1151                                queue.push_back(neighbor);
1152                            }
1153                        }
1154                    }
1155                }
1156            }
1157        }
1158
1159        polynomial
1160    }
1161
1162    /// Optimize a phase polynomial
1163    const fn optimize_phase_polynomial(
1164        &self,
1165        _diagram: &mut ZXDiagram,
1166        _polynomial: &[usize],
1167    ) -> usize {
1168        // Placeholder for phase polynomial optimization
1169        // This would implement:
1170        // 1. Convert to phase polynomial representation
1171        // 2. Apply algebraic simplification
1172        // 3. Convert back to ZX-diagram
1173        0
1174    }
1175
1176    /// Optimize CNOT routing
1177    const fn optimize_cnot_routing(&self, _diagram: &mut ZXDiagram) -> usize {
1178        // Placeholder for CNOT routing optimization
1179        // This would implement connectivity-aware gate routing
1180        0
1181    }
1182
1183    /// Apply commutation-based optimization
1184    const fn apply_commutation_optimization(&self, _diagram: &mut ZXDiagram) -> usize {
1185        // Placeholder for commutation-based optimization
1186        // This would implement gate commutation and reordering
1187        0
1188    }
1189
1190    /// Count the number of T-gates equivalent in the diagram
1191    pub fn count_t_gates(&self, diagram: &ZXDiagram) -> usize {
1192        diagram
1193            .spiders
1194            .values()
1195            .filter(|spider| {
1196                spider.spider_type != SpiderType::Boundary && !spider.is_clifford(self.tolerance)
1197            })
1198            .count()
1199    }
1200
1201    /// Estimate the circuit depth of the diagram
1202    pub fn estimate_depth(&self, diagram: &ZXDiagram) -> usize {
1203        // Simplified depth estimation based on graph diameter
1204        let inputs = &diagram.inputs;
1205        let outputs = &diagram.outputs;
1206
1207        if inputs.is_empty() || outputs.is_empty() {
1208            return 0;
1209        }
1210
1211        // Find the longest path from any input to any output
1212        let mut max_depth = 0;
1213
1214        for &input in inputs {
1215            for &output in outputs {
1216                let depth = self.shortest_path_length(diagram, input, output);
1217                max_depth = max_depth.max(depth);
1218            }
1219        }
1220
1221        max_depth
1222    }
1223
1224    /// Find shortest path length between two spiders
1225    fn shortest_path_length(&self, diagram: &ZXDiagram, start: usize, end: usize) -> usize {
1226        if start == end {
1227            return 0;
1228        }
1229
1230        let mut visited = HashSet::new();
1231        let mut queue = VecDeque::new();
1232
1233        queue.push_back((start, 0));
1234        visited.insert(start);
1235
1236        while let Some((current, depth)) = queue.pop_front() {
1237            if current == end {
1238                return depth;
1239            }
1240
1241            for (neighbor, _) in diagram.neighbors(current) {
1242                if visited.insert(neighbor) {
1243                    queue.push_back((neighbor, depth + 1));
1244                }
1245            }
1246        }
1247
1248        usize::MAX // Not reachable
1249    }
1250
1251    /// Optimize a quantum circuit using ZX-calculus
1252    pub fn optimize_circuit(
1253        &self,
1254        gates: &[Box<dyn GateOp>],
1255    ) -> QuantRS2Result<Vec<Box<dyn GateOp>>> {
1256        // Find number of qubits
1257        let num_qubits = gates
1258            .iter()
1259            .flat_map(|g| g.qubits())
1260            .map(|q| q.0 + 1)
1261            .max()
1262            .unwrap_or(0);
1263
1264        // Convert to ZX-diagram
1265        let mut converter = CircuitToZX::new(num_qubits as usize);
1266        for gate in gates {
1267            converter.add_gate(gate.as_ref())?;
1268        }
1269
1270        let mut diagram = converter.into_diagram();
1271
1272        // Optimize the diagram
1273        self.optimize(&mut diagram);
1274
1275        // Convert back to circuit (TODO: implement extraction)
1276        // For now, return the original circuit
1277        Ok(gates.to_vec())
1278    }
1279}
1280
1281/// Convert a quantum circuit to a ZX-diagram
1282pub struct CircuitToZX {
1283    diagram: ZXDiagram,
1284    qubit_wires: FxHashMap<QubitId, (usize, usize)>, // (current_start, current_end)
1285}
1286
1287impl CircuitToZX {
1288    /// Create a new converter
1289    pub fn new(num_qubits: usize) -> Self {
1290        let mut diagram = ZXDiagram::new();
1291        let mut qubit_wires = FxHashMap::default();
1292
1293        // Create input and output boundaries
1294        for i in 0..num_qubits {
1295            let qubit = QubitId(i as u32);
1296            let input = diagram.add_boundary(qubit, true);
1297            let output = diagram.add_boundary(qubit, false);
1298            qubit_wires.insert(qubit, (input, output));
1299        }
1300
1301        Self {
1302            diagram,
1303            qubit_wires,
1304        }
1305    }
1306
1307    /// Add a gate to the diagram
1308    pub fn add_gate(&mut self, gate: &dyn GateOp) -> QuantRS2Result<()> {
1309        let qubits = gate.qubits();
1310
1311        match gate.name() {
1312            "H" => self.add_hadamard(qubits[0]),
1313            "X" => self.add_pauli_x(qubits[0]),
1314            "Y" => self.add_pauli_y(qubits[0]),
1315            "Z" => self.add_pauli_z(qubits[0]),
1316            "S" => self.add_phase(qubits[0], PI / 2.0),
1317            "T" => self.add_phase(qubits[0], PI / 4.0),
1318            "RX" => {
1319                // Extract angle from parametric gate
1320                if let Some(rx) = gate.as_any().downcast_ref::<RotationX>() {
1321                    self.add_rotation_x(qubits[0], rx.theta)
1322                } else {
1323                    Err(QuantRS2Error::InvalidInput("Invalid RX gate".to_string()))
1324                }
1325            }
1326            "RY" => {
1327                if let Some(ry) = gate.as_any().downcast_ref::<RotationY>() {
1328                    self.add_rotation_y(qubits[0], ry.theta)
1329                } else {
1330                    Err(QuantRS2Error::InvalidInput("Invalid RY gate".to_string()))
1331                }
1332            }
1333            "RZ" => {
1334                if let Some(rz) = gate.as_any().downcast_ref::<RotationZ>() {
1335                    self.add_rotation_z(qubits[0], rz.theta)
1336                } else {
1337                    Err(QuantRS2Error::InvalidInput("Invalid RZ gate".to_string()))
1338                }
1339            }
1340            "CNOT" => self.add_cnot(qubits[0], qubits[1]),
1341            "CZ" => self.add_cz(qubits[0], qubits[1]),
1342            _ => Err(QuantRS2Error::UnsupportedOperation(format!(
1343                "Gate {} not yet supported in ZX-calculus",
1344                gate.name()
1345            ))),
1346        }
1347    }
1348
1349    /// Add a Hadamard gate
1350    fn add_hadamard(&mut self, qubit: QubitId) -> QuantRS2Result<()> {
1351        let (start, end) = *self
1352            .qubit_wires
1353            .get(&qubit)
1354            .ok_or_else(|| QuantRS2Error::InvalidInput("Qubit not found".to_string()))?;
1355
1356        // Add Hadamard edge
1357        self.diagram.remove_edge(start, end);
1358        self.diagram.add_edge(start, end, EdgeType::Hadamard);
1359
1360        Ok(())
1361    }
1362
1363    /// Add a Pauli X gate
1364    fn add_pauli_x(&mut self, qubit: QubitId) -> QuantRS2Result<()> {
1365        self.add_x_spider(qubit, PI)
1366    }
1367
1368    /// Add a Pauli Y gate
1369    fn add_pauli_y(&mut self, qubit: QubitId) -> QuantRS2Result<()> {
1370        // Y = iXZ, so we add both X(π) and Z(π) spiders
1371        self.add_x_spider(qubit, PI)?;
1372        self.add_z_spider(qubit, PI)
1373    }
1374
1375    /// Add a Pauli Z gate
1376    fn add_pauli_z(&mut self, qubit: QubitId) -> QuantRS2Result<()> {
1377        self.add_z_spider(qubit, PI)
1378    }
1379
1380    /// Add a phase gate
1381    fn add_phase(&mut self, qubit: QubitId, angle: f64) -> QuantRS2Result<()> {
1382        self.add_z_spider(qubit, angle)
1383    }
1384
1385    /// Add an X rotation
1386    fn add_rotation_x(&mut self, qubit: QubitId, angle: f64) -> QuantRS2Result<()> {
1387        self.add_x_spider(qubit, angle)
1388    }
1389
1390    /// Add a Y rotation
1391    fn add_rotation_y(&mut self, qubit: QubitId, angle: f64) -> QuantRS2Result<()> {
1392        // RY(θ) = e^(-iθY/2) = RZ(-π/2)RX(θ)RZ(π/2)
1393        self.add_z_spider(qubit, -PI / 2.0)?;
1394        self.add_x_spider(qubit, angle)?;
1395        self.add_z_spider(qubit, PI / 2.0)
1396    }
1397
1398    /// Add a Z rotation
1399    fn add_rotation_z(&mut self, qubit: QubitId, angle: f64) -> QuantRS2Result<()> {
1400        self.add_z_spider(qubit, angle)
1401    }
1402
1403    /// Add a CNOT gate
1404    fn add_cnot(&mut self, control: QubitId, target: QubitId) -> QuantRS2Result<()> {
1405        let (c_start, c_end) = *self
1406            .qubit_wires
1407            .get(&control)
1408            .ok_or_else(|| QuantRS2Error::InvalidInput("Control qubit not found".to_string()))?;
1409        let (t_start, t_end) = *self
1410            .qubit_wires
1411            .get(&target)
1412            .ok_or_else(|| QuantRS2Error::InvalidInput("Target qubit not found".to_string()))?;
1413
1414        // CNOT is represented as a Z-spider on control connected to X-spider on target
1415        let z_spider = self.diagram.add_spider(SpiderType::Z, 0.0);
1416        let x_spider = self.diagram.add_spider(SpiderType::X, 0.0);
1417
1418        // Break existing connections
1419        self.diagram.remove_edge(c_start, c_end);
1420        self.diagram.remove_edge(t_start, t_end);
1421
1422        // Connect control
1423        self.diagram.add_edge(c_start, z_spider, EdgeType::Regular);
1424        self.diagram.add_edge(z_spider, c_end, EdgeType::Regular);
1425
1426        // Connect target
1427        self.diagram.add_edge(t_start, x_spider, EdgeType::Regular);
1428        self.diagram.add_edge(x_spider, t_end, EdgeType::Regular);
1429
1430        // Connect control to target
1431        self.diagram.add_edge(z_spider, x_spider, EdgeType::Regular);
1432
1433        Ok(())
1434    }
1435
1436    /// Add a CZ gate
1437    fn add_cz(&mut self, qubit1: QubitId, qubit2: QubitId) -> QuantRS2Result<()> {
1438        let (q1_start, q1_end) = *self
1439            .qubit_wires
1440            .get(&qubit1)
1441            .ok_or_else(|| QuantRS2Error::InvalidInput("Qubit 1 not found".to_string()))?;
1442        let (q2_start, q2_end) = *self
1443            .qubit_wires
1444            .get(&qubit2)
1445            .ok_or_else(|| QuantRS2Error::InvalidInput("Qubit 2 not found".to_string()))?;
1446
1447        // CZ is represented as two connected Z-spiders
1448        let z1_spider = self.diagram.add_spider(SpiderType::Z, 0.0);
1449        let z2_spider = self.diagram.add_spider(SpiderType::Z, 0.0);
1450
1451        // Break existing connections
1452        self.diagram.remove_edge(q1_start, q1_end);
1453        self.diagram.remove_edge(q2_start, q2_end);
1454
1455        // Connect qubit 1
1456        self.diagram
1457            .add_edge(q1_start, z1_spider, EdgeType::Regular);
1458        self.diagram.add_edge(z1_spider, q1_end, EdgeType::Regular);
1459
1460        // Connect qubit 2
1461        self.diagram
1462            .add_edge(q2_start, z2_spider, EdgeType::Regular);
1463        self.diagram.add_edge(z2_spider, q2_end, EdgeType::Regular);
1464
1465        // Connect the two spiders with Hadamard edge
1466        self.diagram
1467            .add_edge(z1_spider, z2_spider, EdgeType::Hadamard);
1468
1469        Ok(())
1470    }
1471
1472    /// Add a Z-spider to a qubit wire
1473    fn add_z_spider(&mut self, qubit: QubitId, phase: f64) -> QuantRS2Result<()> {
1474        let (start, end) = *self
1475            .qubit_wires
1476            .get(&qubit)
1477            .ok_or_else(|| QuantRS2Error::InvalidInput("Qubit not found".to_string()))?;
1478
1479        // Create Z-spider
1480        let spider = self.diagram.add_spider(SpiderType::Z, phase);
1481
1482        // Insert into wire
1483        self.diagram.remove_edge(start, end);
1484        self.diagram.add_edge(start, spider, EdgeType::Regular);
1485        self.diagram.add_edge(spider, end, EdgeType::Regular);
1486
1487        // Update wire tracking
1488        self.qubit_wires.insert(qubit, (start, end));
1489
1490        Ok(())
1491    }
1492
1493    /// Add an X-spider to a qubit wire
1494    fn add_x_spider(&mut self, qubit: QubitId, phase: f64) -> QuantRS2Result<()> {
1495        let (start, end) = *self
1496            .qubit_wires
1497            .get(&qubit)
1498            .ok_or_else(|| QuantRS2Error::InvalidInput("Qubit not found".to_string()))?;
1499
1500        // Create X-spider
1501        let spider = self.diagram.add_spider(SpiderType::X, phase);
1502
1503        // Insert into wire
1504        self.diagram.remove_edge(start, end);
1505        self.diagram.add_edge(start, spider, EdgeType::Regular);
1506        self.diagram.add_edge(spider, end, EdgeType::Regular);
1507
1508        // Update wire tracking
1509        self.qubit_wires.insert(qubit, (start, end));
1510
1511        Ok(())
1512    }
1513
1514    /// Get the resulting ZX-diagram
1515    pub fn into_diagram(self) -> ZXDiagram {
1516        self.diagram
1517    }
1518}
1519
1520#[cfg(test)]
1521mod tests {
1522    use super::*;
1523    use crate::gate::multi::CNOT;
1524
1525    #[test]
1526    fn test_spider_creation() {
1527        let spider = Spider::new(0, SpiderType::Z, PI / 2.0);
1528        assert_eq!(spider.id, 0);
1529        assert_eq!(spider.spider_type, SpiderType::Z);
1530        assert!((spider.phase - PI / 2.0).abs() < 1e-10);
1531        assert!(spider.is_clifford(1e-10));
1532        assert!(!spider.is_pauli(1e-10));
1533    }
1534
1535    #[test]
1536    fn test_diagram_creation() {
1537        let mut diagram = ZXDiagram::new();
1538
1539        let z_id = diagram.add_spider(SpiderType::Z, 0.0);
1540        let x_id = diagram.add_spider(SpiderType::X, PI);
1541
1542        diagram.add_edge(z_id, x_id, EdgeType::Regular);
1543
1544        assert_eq!(diagram.degree(z_id), 1);
1545        assert_eq!(diagram.degree(x_id), 1);
1546    }
1547
1548    #[test]
1549    fn test_spider_fusion() {
1550        let mut diagram = ZXDiagram::new();
1551
1552        let z1 = diagram.add_spider(SpiderType::Z, PI / 4.0);
1553        let z2 = diagram.add_spider(SpiderType::Z, PI / 4.0);
1554        let boundary = diagram.add_boundary(QubitId(0), true);
1555
1556        diagram.add_edge(z1, z2, EdgeType::Regular);
1557        diagram.add_edge(z2, boundary, EdgeType::Regular);
1558
1559        assert!(diagram.spider_fusion(z1, z2).is_ok());
1560
1561        // Check that z2 is removed and z1 has combined phase
1562        assert!(!diagram.spiders.contains_key(&z2));
1563        assert_eq!(diagram.spiders[&z1].phase, PI / 2.0);
1564        assert_eq!(diagram.degree(z1), 1); // Connected to boundary
1565    }
1566
1567    #[test]
1568    fn test_identity_removal() {
1569        let mut diagram = ZXDiagram::new();
1570
1571        let b1 = diagram.add_boundary(QubitId(0), true);
1572        let id_spider = diagram.add_spider(SpiderType::Z, 0.0);
1573        let b2 = diagram.add_boundary(QubitId(0), false);
1574
1575        diagram.add_edge(b1, id_spider, EdgeType::Regular);
1576        diagram.add_edge(id_spider, b2, EdgeType::Regular);
1577
1578        let removed = diagram.remove_identities();
1579        assert_eq!(removed, 1);
1580        assert!(!diagram.spiders.contains_key(&id_spider));
1581
1582        // Check that boundaries are now connected
1583        assert!(diagram.neighbors(b1).iter().any(|(id, _)| *id == b2));
1584    }
1585
1586    #[test]
1587    fn test_circuit_to_zx_hadamard() {
1588        let mut converter = CircuitToZX::new(1);
1589        let h_gate = Hadamard { target: QubitId(0) };
1590
1591        assert!(converter.add_gate(&h_gate).is_ok());
1592
1593        let diagram = converter.into_diagram();
1594
1595        // Check that there's a Hadamard edge between boundaries
1596        let has_hadamard = diagram.adjacency.values().any(|neighbors| {
1597            neighbors
1598                .iter()
1599                .any(|(_, edge_type)| *edge_type == EdgeType::Hadamard)
1600        });
1601        assert!(has_hadamard);
1602    }
1603
1604    #[test]
1605    fn test_circuit_to_zx_cnot() {
1606        let mut converter = CircuitToZX::new(2);
1607        let cnot = CNOT {
1608            control: QubitId(0),
1609            target: QubitId(1),
1610        };
1611
1612        assert!(converter.add_gate(&cnot).is_ok());
1613
1614        let diagram = converter.into_diagram();
1615
1616        // Should have 4 boundaries + 2 spiders (Z and X)
1617        assert_eq!(diagram.spiders.len(), 6);
1618
1619        // Count Z and X spiders
1620        let z_count = diagram
1621            .spiders
1622            .values()
1623            .filter(|s| s.spider_type == SpiderType::Z && s.phase.abs() < 1e-10)
1624            .count();
1625        let x_count = diagram
1626            .spiders
1627            .values()
1628            .filter(|s| s.spider_type == SpiderType::X && s.phase.abs() < 1e-10)
1629            .count();
1630
1631        assert_eq!(z_count, 1);
1632        assert_eq!(x_count, 1);
1633    }
1634
1635    #[test]
1636    fn test_zx_optimizer() {
1637        let gates: Vec<Box<dyn GateOp>> = vec![
1638            Box::new(Hadamard { target: QubitId(0) }),
1639            Box::new(PauliZ { target: QubitId(0) }),
1640            Box::new(Hadamard { target: QubitId(0) }),
1641        ];
1642
1643        let optimizer = ZXOptimizer::new();
1644        let result = optimizer.optimize_circuit(&gates);
1645
1646        assert!(result.is_ok());
1647        // HZH = X, so optimized circuit should be simpler
1648    }
1649
1650    #[test]
1651    fn test_dot_generation() {
1652        let mut diagram = ZXDiagram::new();
1653
1654        let input = diagram.add_boundary(QubitId(0), true);
1655        let z = diagram.add_spider(SpiderType::Z, PI / 2.0);
1656        let x = diagram.add_spider(SpiderType::X, 0.0);
1657        let output = diagram.add_boundary(QubitId(0), false);
1658
1659        diagram.add_edge(input, z, EdgeType::Regular);
1660        diagram.add_edge(z, x, EdgeType::Hadamard);
1661        diagram.add_edge(x, output, EdgeType::Regular);
1662
1663        let dot = diagram.to_dot();
1664        assert!(dot.contains("graph ZX"));
1665        assert!(dot.contains("color=green")); // Z spider
1666        assert!(dot.contains("color=red")); // X spider
1667        assert!(dot.contains("style=dashed")); // Hadamard edge
1668    }
1669}