quantrs2_circuit/
graph_optimizer.rs

1//! Graph-based circuit optimizer using `SciRS2` algorithms
2//!
3//! This module implements advanced circuit optimization using graph representations
4//! and algorithms from `SciRS2` for optimal gate scheduling and optimization.
5
6use crate::builder::Circuit;
7use quantrs2_core::error::QuantRS2Error;
8use quantrs2_core::qubit::QubitId;
9use scirs2_core::Complex64;
10use std::collections::{HashMap, HashSet, VecDeque};
11
12// SciRS2 integration for graph algorithms when available
13#[cfg(feature = "scirs")]
14use scirs2_core::sparse::{CsrMatrix, SparseMatrix};
15#[cfg(feature = "scirs")]
16use scirs2_optimize::graph::{
17    find_critical_path, graph_coloring, minimum_feedback_arc_set, topological_sort_weighted,
18};
19
20/// Helper function to multiply two 2x2 matrices
21fn matrix_multiply_2x2(a: &[Vec<Complex64>], b: &[Vec<Complex64>]) -> Vec<Vec<Complex64>> {
22    vec![
23        vec![
24            a[0][0] * b[0][0] + a[0][1] * b[1][0],
25            a[0][0] * b[0][1] + a[0][1] * b[1][1],
26        ],
27        vec![
28            a[1][0] * b[0][0] + a[1][1] * b[1][0],
29            a[1][0] * b[0][1] + a[1][1] * b[1][1],
30        ],
31    ]
32}
33
34/// Represents a gate in the circuit graph
35#[derive(Debug, Clone, PartialEq)]
36pub struct GraphGate {
37    pub id: usize,
38    pub gate_type: String,
39    pub qubits: Vec<QubitId>,
40    pub params: Vec<f64>,
41    pub matrix: Option<Vec<Vec<Complex64>>>,
42}
43
44/// Edge types in the circuit graph
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum EdgeType {
47    /// Data dependency (same qubit)
48    DataDependency,
49    /// Commutation constraint
50    NonCommuting,
51    /// Can be parallelized
52    Parallelizable,
53}
54
55/// Circuit DAG (Directed Acyclic Graph) representation
56pub struct CircuitDAG {
57    nodes: Vec<GraphGate>,
58    edges: HashMap<(usize, usize), EdgeType>,
59    qubit_chains: HashMap<u32, Vec<usize>>, // qubit_id -> [gate_ids in order]
60}
61
62impl CircuitDAG {
63    /// Create a new empty DAG
64    #[must_use]
65    pub fn new() -> Self {
66        Self {
67            nodes: Vec::new(),
68            edges: HashMap::new(),
69            qubit_chains: HashMap::new(),
70        }
71    }
72
73    /// Add a gate to the DAG
74    pub fn add_gate(&mut self, gate: GraphGate) -> usize {
75        let gate_id = self.nodes.len();
76
77        // Update qubit chains
78        for qubit in &gate.qubits {
79            self.qubit_chains
80                .entry(qubit.id())
81                .or_default()
82                .push(gate_id);
83        }
84
85        // Add edges for data dependencies
86        for qubit in &gate.qubits {
87            if let Some(chain) = self.qubit_chains.get(&qubit.id()) {
88                if chain.len() > 1 {
89                    let prev_gate = chain[chain.len() - 2];
90                    self.edges
91                        .insert((prev_gate, gate_id), EdgeType::DataDependency);
92                }
93            }
94        }
95
96        self.nodes.push(gate);
97        gate_id
98    }
99
100    /// Check if two gates commute
101    fn gates_commute(&self, g1: &GraphGate, g2: &GraphGate) -> bool {
102        // Gates on different qubits always commute
103        let qubits1: HashSet<_> = g1.qubits.iter().map(quantrs2_core::QubitId::id).collect();
104        let qubits2: HashSet<_> = g2.qubits.iter().map(quantrs2_core::QubitId::id).collect();
105
106        if qubits1.is_disjoint(&qubits2) {
107            return true;
108        }
109
110        // Special cases for common gates
111        match (g1.gate_type.as_str(), g2.gate_type.as_str()) {
112            // Z gates always commute with each other
113            ("z" | "rz", "z" | "rz") => true,
114            // CNOT gates commute if they share only control or only target
115            ("cnot", "cnot") => {
116                if g1.qubits.len() == 2 && g2.qubits.len() == 2 {
117                    let same_control = g1.qubits[0] == g2.qubits[0];
118                    let same_target = g1.qubits[1] == g2.qubits[1];
119                    same_control && same_target // Only if identical
120                } else {
121                    false
122                }
123            }
124            _ => false,
125        }
126    }
127
128    /// Compute commutation graph
129    pub fn compute_commutation_edges(&mut self) {
130        #[cfg(feature = "scirs")]
131        {
132            if self.scirs_compute_commutation_edges() {
133                return;
134            }
135        }
136
137        self.standard_compute_commutation_edges();
138    }
139
140    #[cfg(feature = "scirs")]
141    /// SciRS2-powered commutation edge computation
142    fn scirs_compute_commutation_edges(&mut self) -> bool {
143        let n = self.nodes.len();
144        if n == 0 {
145            return true;
146        }
147
148        // Build interference graph for gates
149        let mut interference = vec![vec![false; n]; n];
150
151        for i in 0..n {
152            for j in i + 1..n {
153                let g1 = &self.nodes[i];
154                let g2 = &self.nodes[j];
155
156                // Check if gates interfere (can't be scheduled in parallel)
157                if !self.gates_commute(g1, g2) && !self.has_path(i, j) && !self.has_path(j, i) {
158                    interference[i][j] = true;
159                    interference[j][i] = true;
160                }
161            }
162        }
163
164        // Use graph coloring to find maximum parallelism
165        if let Ok(coloring) = graph_coloring(&interference) {
166            // Use coloring information to add parallelization hints
167            for i in 0..n {
168                for j in i + 1..n {
169                    if coloring[i] == coloring[j] && !interference[i][j] {
170                        // Same color = can be scheduled in parallel
171                        self.edges.insert((i, j), EdgeType::Parallelizable);
172                    }
173                }
174            }
175            return true;
176        }
177
178        false
179    }
180
181    /// Standard commutation edge computation
182    fn standard_compute_commutation_edges(&mut self) {
183        let n = self.nodes.len();
184
185        for i in 0..n {
186            for j in i + 1..n {
187                let g1 = &self.nodes[i];
188                let g2 = &self.nodes[j];
189
190                // Skip if already connected
191                if self.edges.contains_key(&(i, j)) || self.edges.contains_key(&(j, i)) {
192                    continue;
193                }
194
195                // Check commutation
196                if !self.gates_commute(g1, g2) {
197                    // Check if they could be reordered (no data dependency path)
198                    if !self.has_path(i, j) && !self.has_path(j, i) {
199                        self.edges.insert((i, j), EdgeType::NonCommuting);
200                    }
201                } else if g1.qubits.iter().any(|q| g2.qubits.contains(q)) {
202                    // Gates commute but share qubits
203                    self.edges.insert((i, j), EdgeType::Parallelizable);
204                }
205            }
206        }
207    }
208
209    #[cfg(feature = "scirs")]
210    /// Find optimal gate reordering using minimum feedback arc set
211    pub fn optimize_with_feedback_arc_set(&self) -> Option<Vec<usize>> {
212        let n = self.nodes.len();
213        if n == 0 {
214            return Some(vec![]);
215        }
216
217        // Build directed graph of non-commuting edges
218        let mut edges = Vec::new();
219        let mut weights = Vec::new();
220
221        for ((u, v), edge_type) in &self.edges {
222            if *edge_type == EdgeType::NonCommuting {
223                edges.push((*u, *v));
224                // Weight by impact on parallelism
225                let weight = self.gate_weight(*u) * self.gate_weight(*v);
226                weights.push(weight);
227            }
228        }
229
230        // Find minimum feedback arc set to break cycles
231        if let Ok(feedback_arcs) = minimum_feedback_arc_set(&edges, &weights) {
232            // Remove feedback arcs and compute new ordering
233            let mut filtered_edges = edges.clone();
234            for &arc_idx in &feedback_arcs {
235                filtered_edges[arc_idx] = (n, n); // Mark for removal
236            }
237            filtered_edges.retain(|&(u, v)| u < n && v < n);
238
239            // Build new DAG and compute topological sort
240            let mut new_dag = CircuitDAG::new();
241            new_dag.nodes = self.nodes.clone();
242            for (u, v) in filtered_edges {
243                new_dag.edges.insert((u, v), EdgeType::DataDependency);
244            }
245
246            return Some(new_dag.optimized_topological_sort());
247        }
248
249        None
250    }
251
252    /// Check if there's a path from src to dst
253    fn has_path(&self, src: usize, dst: usize) -> bool {
254        let mut visited = vec![false; self.nodes.len()];
255        let mut queue = VecDeque::new();
256
257        queue.push_back(src);
258        visited[src] = true;
259
260        while let Some(node) = queue.pop_front() {
261            if node == dst {
262                return true;
263            }
264
265            for ((u, v), edge_type) in &self.edges {
266                if *u == node && !visited[*v] && *edge_type == EdgeType::DataDependency {
267                    visited[*v] = true;
268                    queue.push_back(*v);
269                }
270            }
271        }
272
273        false
274    }
275
276    /// Topological sort with optimization
277    #[must_use]
278    pub fn optimized_topological_sort(&self) -> Vec<usize> {
279        #[cfg(feature = "scirs")]
280        {
281            // Use SciRS2's optimized topological sort when available
282            if let Some(order) = self.scirs_topological_sort() {
283                return order;
284            }
285        }
286
287        // Fallback to standard implementation
288        self.standard_topological_sort()
289    }
290
291    #[cfg(feature = "scirs")]
292    /// SciRS2-powered topological sort with advanced optimization
293    fn scirs_topological_sort(&self) -> Option<Vec<usize>> {
294        let n = self.nodes.len();
295        if n == 0 {
296            return Some(vec![]);
297        }
298
299        // Convert to sparse adjacency matrix for SciRS2
300        let mut row_indices = Vec::new();
301        let mut col_indices = Vec::new();
302        let mut values = Vec::new();
303
304        for ((u, v), edge_type) in &self.edges {
305            if *edge_type == EdgeType::DataDependency {
306                row_indices.push(*u);
307                col_indices.push(*v);
308                // Weight edges by gate complexity
309                let weight = self.gate_weight(*u) + self.gate_weight(*v);
310                values.push(weight);
311            }
312        }
313
314        // Create sparse matrix
315        let matrix = CsrMatrix::from_triplets(n, n, &row_indices, &col_indices, &values);
316
317        // Use weighted topological sort for optimal scheduling
318        if let Ok(order) = topological_sort_weighted(&matrix, |i| self.gate_priority(i)) {
319            // Find critical path for depth optimization
320            if let Ok(critical) = find_critical_path(&matrix, &order) {
321                // Reorder based on critical path analysis
322                return Some(self.optimize_order_by_critical_path(order, critical));
323            }
324            return Some(order);
325        }
326
327        None
328    }
329
330    /// Calculate gate weight for scheduling priority
331    fn gate_weight(&self, gate_id: usize) -> f64 {
332        let gate = &self.nodes[gate_id];
333        match gate.gate_type.as_str() {
334            // Two-qubit gates are more expensive
335            "cnot" | "cz" | "swap" => 10.0,
336            "rzz" | "rxx" | "ryy" => 15.0,
337            // Rotation gates
338            "rx" | "ry" | "rz" => 2.0,
339            // Clifford gates are cheap
340            "h" | "s" | "t" | "x" | "y" | "z" => 1.0,
341            // Unknown gates
342            _ => 5.0,
343        }
344    }
345
346    /// Calculate gate priority for scheduling
347    fn gate_priority(&self, gate_id: usize) -> f64 {
348        // Prioritize gates that enable more parallelism
349        let parallelism_score = self.count_parallel_successors(gate_id) as f64;
350        // Factor in gate weight
351        let weight = self.gate_weight(gate_id);
352        // Higher priority for gates that unlock more parallelism
353        parallelism_score / weight
354    }
355
356    /// Count how many gates can be executed in parallel after this gate
357    fn count_parallel_successors(&self, gate_id: usize) -> usize {
358        let mut count = 0;
359        for ((u, _v), edge_type) in &self.edges {
360            if *u == gate_id && *edge_type == EdgeType::Parallelizable {
361                count += 1;
362            }
363        }
364        count
365    }
366
367    #[cfg(feature = "scirs")]
368    /// Optimize gate order based on critical path analysis
369    fn optimize_order_by_critical_path(
370        &self,
371        order: Vec<usize>,
372        critical: Vec<usize>,
373    ) -> Vec<usize> {
374        let mut optimized = Vec::new();
375        let mut scheduled = HashSet::new();
376        let critical_set: HashSet<_> = critical.into_iter().collect();
377
378        // Schedule critical path gates first
379        for &gate_id in &order {
380            if critical_set.contains(&gate_id) && !scheduled.contains(&gate_id) {
381                optimized.push(gate_id);
382                scheduled.insert(gate_id);
383            }
384        }
385
386        // Then schedule remaining gates
387        for &gate_id in &order {
388            if !scheduled.contains(&gate_id) {
389                optimized.push(gate_id);
390                scheduled.insert(gate_id);
391            }
392        }
393
394        optimized
395    }
396
397    /// Standard topological sort implementation (fallback)
398    fn standard_topological_sort(&self) -> Vec<usize> {
399        let n = self.nodes.len();
400        let mut in_degree = vec![0; n];
401        let mut adj_list: HashMap<usize, Vec<usize>> = HashMap::new();
402
403        // Build adjacency list and compute in-degrees
404        for ((u, v), edge_type) in &self.edges {
405            if *edge_type == EdgeType::DataDependency {
406                adj_list.entry(*u).or_default().push(*v);
407                in_degree[*v] += 1;
408            }
409        }
410
411        // Priority queue for selecting next gate (minimize circuit depth)
412        let mut ready: Vec<usize> = Vec::new();
413        for (i, &degree) in in_degree.iter().enumerate() {
414            if degree == 0 {
415                ready.push(i);
416            }
417        }
418
419        let mut result = Vec::new();
420        let mut layer_qubits: HashSet<u32> = HashSet::new();
421
422        while !ready.is_empty() {
423            // Sort ready gates by number of qubits (prefer single-qubit gates)
424            ready.sort_by_key(|&i| self.nodes[i].qubits.len());
425
426            // Try to pack gates that don't conflict
427            let mut next_layer = Vec::new();
428            let mut used = vec![false; ready.len()];
429
430            for (idx, &gate_id) in ready.iter().enumerate() {
431                if used[idx] {
432                    continue;
433                }
434
435                let gate = &self.nodes[gate_id];
436                let gate_qubits: HashSet<_> =
437                    gate.qubits.iter().map(quantrs2_core::QubitId::id).collect();
438
439                // Check if this gate conflicts with current layer
440                if gate_qubits.is_disjoint(&layer_qubits) {
441                    next_layer.push(gate_id);
442                    layer_qubits.extend(&gate_qubits);
443                    used[idx] = true;
444                }
445            }
446
447            // If no gates selected, pick the first one
448            if next_layer.is_empty() && !ready.is_empty() {
449                next_layer.push(ready[0]);
450                used[0] = true;
451            }
452
453            // Remove selected gates from ready
454            ready.retain(|&g| !next_layer.contains(&g));
455
456            // Add to result and update graph
457            for &gate_id in &next_layer {
458                result.push(gate_id);
459
460                if let Some(neighbors) = adj_list.get(&gate_id) {
461                    for &neighbor in neighbors {
462                        in_degree[neighbor] -= 1;
463                        if in_degree[neighbor] == 0 {
464                            ready.push(neighbor);
465                        }
466                    }
467                }
468            }
469
470            layer_qubits.clear();
471        }
472
473        result
474    }
475}
476
477impl Default for CircuitDAG {
478    fn default() -> Self {
479        Self::new()
480    }
481}
482
483/// Graph-based circuit optimizer
484pub struct GraphOptimizer {
485    merge_threshold: f64,
486    #[allow(dead_code)]
487    max_lookahead: usize,
488}
489
490impl GraphOptimizer {
491    /// Create a new graph optimizer
492    #[must_use]
493    pub const fn new() -> Self {
494        Self {
495            merge_threshold: 1e-6,
496            max_lookahead: 10,
497        }
498    }
499
500    /// Convert circuit to DAG representation
501    pub fn circuit_to_dag<const N: usize>(
502        &self,
503        circuit: &Circuit<N>,
504    ) -> Result<CircuitDAG, QuantRS2Error> {
505        let mut dag = CircuitDAG::new();
506
507        // Extract gates from circuit and convert to GraphGates
508        for (gate_id, gate) in circuit.gates().iter().enumerate() {
509            let graph_gate = GraphGate {
510                id: gate_id,
511                gate_type: gate.name().to_string(),
512                qubits: gate.qubits(),
513                params: if gate.is_parameterized() {
514                    // Extract parameters from specific gate types
515                    match gate.name() {
516                        "RX" | "RY" | "RZ" => {
517                            // Try to extract rotation parameters using downcast
518                            if let Some(rx_gate) =
519                                gate.as_any()
520                                    .downcast_ref::<quantrs2_core::gate::single::RotationX>()
521                            {
522                                vec![rx_gate.theta]
523                            } else if let Some(ry_gate) =
524                                gate.as_any()
525                                    .downcast_ref::<quantrs2_core::gate::single::RotationY>()
526                            {
527                                vec![ry_gate.theta]
528                            } else if let Some(rz_gate) =
529                                gate.as_any()
530                                    .downcast_ref::<quantrs2_core::gate::single::RotationZ>()
531                            {
532                                vec![rz_gate.theta]
533                            } else {
534                                vec![] // Default for unknown parameterized gates
535                            }
536                        }
537                        "CRX" | "CRY" | "CRZ" => {
538                            // Try to extract controlled rotation parameters
539                            if let Some(crx_gate) = gate
540                                .as_any()
541                                .downcast_ref::<quantrs2_core::gate::multi::CRX>()
542                            {
543                                vec![crx_gate.theta]
544                            } else if let Some(cry_gate) =
545                                gate.as_any()
546                                    .downcast_ref::<quantrs2_core::gate::multi::CRY>()
547                            {
548                                vec![cry_gate.theta]
549                            } else if let Some(crz_gate) =
550                                gate.as_any()
551                                    .downcast_ref::<quantrs2_core::gate::multi::CRZ>()
552                            {
553                                vec![crz_gate.theta]
554                            } else {
555                                vec![]
556                            }
557                        }
558                        _ => vec![], // Other parameterized gates
559                    }
560                } else {
561                    vec![] // Non-parameterized gates have no parameters
562                },
563                matrix: None, // Compute on demand if needed
564            };
565
566            dag.add_gate(graph_gate);
567        }
568
569        Ok(dag)
570    }
571
572    /// Optimize gate sequence using commutation rules
573    #[must_use]
574    pub fn optimize_gate_sequence(&self, gates: Vec<GraphGate>) -> Vec<GraphGate> {
575        let mut dag = CircuitDAG::new();
576
577        // Add gates to DAG
578        for gate in gates {
579            dag.add_gate(gate);
580        }
581
582        // Compute commutation relationships
583        dag.compute_commutation_edges();
584
585        // Try advanced optimization methods if available
586        #[cfg(feature = "scirs")]
587        {
588            // Try feedback arc set optimization for complex circuits
589            if dag.nodes.len() > 10 {
590                if let Some(optimized_order) = dag.optimize_with_feedback_arc_set() {
591                    return optimized_order
592                        .iter()
593                        .map(|&i| dag.nodes[i].clone())
594                        .collect();
595                }
596            }
597        }
598
599        // Get optimized ordering
600        let order = dag.optimized_topological_sort();
601
602        // Apply gate merging optimization
603        self.merge_gates_in_sequence(order.iter().map(|&i| dag.nodes[i].clone()).collect())
604    }
605
606    /// Apply gate merging to an ordered sequence
607    fn merge_gates_in_sequence(&self, gates: Vec<GraphGate>) -> Vec<GraphGate> {
608        if gates.is_empty() {
609            return gates;
610        }
611
612        let mut merged = Vec::new();
613        let mut i = 0;
614
615        while i < gates.len() {
616            if i + 1 < gates.len() {
617                // Try to merge consecutive gates
618                if let Some(merged_gate) = self.try_merge_gates(&gates[i], &gates[i + 1]) {
619                    merged.push(merged_gate);
620                    i += 2; // Skip both gates
621                    continue;
622                }
623            }
624            merged.push(gates[i].clone());
625            i += 1;
626        }
627
628        merged
629    }
630
631    /// Try to merge two gates if possible
632    fn try_merge_gates(&self, g1: &GraphGate, g2: &GraphGate) -> Option<GraphGate> {
633        // Only merge single-qubit gates on the same qubit
634        if g1.qubits.len() == 1 && g2.qubits.len() == 1 && g1.qubits[0] == g2.qubits[0] {
635            self.merge_single_qubit_gates(g1, g2)
636        } else {
637            None
638        }
639    }
640
641    /// Merge consecutive single-qubit gates
642    #[must_use]
643    pub fn merge_single_qubit_gates(&self, g1: &GraphGate, g2: &GraphGate) -> Option<GraphGate> {
644        // Check if both are single-qubit gates on the same qubit
645        if g1.qubits.len() != 1 || g2.qubits.len() != 1 || g1.qubits[0] != g2.qubits[0] {
646            return None;
647        }
648
649        // Get matrices
650        let m1 = g1.matrix.as_ref()?;
651        let m2 = g2.matrix.as_ref()?;
652
653        // Multiply matrices (2x2)
654        let combined = matrix_multiply_2x2(m2, m1);
655
656        // Check if it's close to a known gate
657        if let Some((gate_type, params)) = self.identify_gate(&combined) {
658            Some(GraphGate {
659                id: g1.id, // Use first gate's ID
660                gate_type,
661                qubits: g1.qubits.clone(),
662                params,
663                matrix: Some(combined),
664            })
665        } else {
666            // Return as generic unitary
667            Some(GraphGate {
668                id: g1.id,
669                gate_type: "u".to_string(),
670                qubits: g1.qubits.clone(),
671                params: vec![],
672                matrix: Some(combined),
673            })
674        }
675    }
676
677    /// Identify a gate from its matrix
678    fn identify_gate(&self, matrix: &[Vec<Complex64>]) -> Option<(String, Vec<f64>)> {
679        let tolerance = self.merge_threshold;
680
681        // Check for Pauli gates
682        if self.is_pauli_x(matrix, tolerance) {
683            return Some(("x".to_string(), vec![]));
684        }
685        if self.is_pauli_y(matrix, tolerance) {
686            return Some(("y".to_string(), vec![]));
687        }
688        if self.is_pauli_z(matrix, tolerance) {
689            return Some(("z".to_string(), vec![]));
690        }
691
692        // Check for Hadamard
693        if self.is_hadamard(matrix, tolerance) {
694            return Some(("h".to_string(), vec![]));
695        }
696
697        // Check for rotation gates
698        if let Some(angle) = self.is_rz(matrix, tolerance) {
699            return Some(("rz".to_string(), vec![angle]));
700        }
701
702        None
703    }
704
705    fn is_pauli_x(&self, matrix: &[Vec<Complex64>], tol: f64) -> bool {
706        matrix.len() == 2
707            && matrix[0].len() == 2
708            && (matrix[0][0].norm() < tol)
709            && (matrix[0][1] - Complex64::new(1.0, 0.0)).norm() < tol
710            && (matrix[1][0] - Complex64::new(1.0, 0.0)).norm() < tol
711            && (matrix[1][1].norm() < tol)
712    }
713
714    fn is_pauli_y(&self, matrix: &[Vec<Complex64>], tol: f64) -> bool {
715        matrix.len() == 2
716            && matrix[0].len() == 2
717            && (matrix[0][0].norm() < tol)
718            && (matrix[0][1] - Complex64::new(0.0, -1.0)).norm() < tol
719            && (matrix[1][0] - Complex64::new(0.0, 1.0)).norm() < tol
720            && (matrix[1][1].norm() < tol)
721    }
722
723    fn is_pauli_z(&self, matrix: &[Vec<Complex64>], tol: f64) -> bool {
724        matrix.len() == 2
725            && matrix[0].len() == 2
726            && (matrix[0][0] - Complex64::new(1.0, 0.0)).norm() < tol
727            && (matrix[0][1].norm() < tol)
728            && (matrix[1][0].norm() < tol)
729            && (matrix[1][1] - Complex64::new(-1.0, 0.0)).norm() < tol
730    }
731
732    fn is_hadamard(&self, matrix: &[Vec<Complex64>], tol: f64) -> bool {
733        let h_val = 1.0 / 2.0_f64.sqrt();
734        matrix.len() == 2
735            && matrix[0].len() == 2
736            && (matrix[0][0] - Complex64::new(h_val, 0.0)).norm() < tol
737            && (matrix[0][1] - Complex64::new(h_val, 0.0)).norm() < tol
738            && (matrix[1][0] - Complex64::new(h_val, 0.0)).norm() < tol
739            && (matrix[1][1] - Complex64::new(-h_val, 0.0)).norm() < tol
740    }
741
742    fn is_rz(&self, matrix: &[Vec<Complex64>], tol: f64) -> Option<f64> {
743        if matrix.len() != 2
744            || matrix[0].len() != 2
745            || matrix[0][1].norm() > tol
746            || matrix[1][0].norm() > tol
747        {
748            return None;
749        }
750
751        let phase1 = matrix[0][0].arg();
752        let phase2 = matrix[1][1].arg();
753
754        if (matrix[0][0].norm() - 1.0).abs() < tol && (matrix[1][1].norm() - 1.0).abs() < tol {
755            let angle = phase2 - phase1;
756            Some(angle)
757        } else {
758            None
759        }
760    }
761}
762
763impl Default for GraphOptimizer {
764    fn default() -> Self {
765        Self::new()
766    }
767}
768
769/// Optimization statistics
770#[derive(Debug, Clone)]
771pub struct OptimizationStats {
772    pub original_gate_count: usize,
773    pub optimized_gate_count: usize,
774    pub original_depth: usize,
775    pub optimized_depth: usize,
776    pub gates_removed: usize,
777    pub gates_merged: usize,
778}
779
780impl OptimizationStats {
781    #[must_use]
782    pub fn improvement_percentage(&self) -> f64 {
783        if self.original_gate_count == 0 {
784            0.0
785        } else {
786            100.0 * (self.original_gate_count - self.optimized_gate_count) as f64
787                / self.original_gate_count as f64
788        }
789    }
790}
791
792#[cfg(test)]
793mod tests {
794    use super::*;
795
796    #[test]
797    fn test_dag_construction() {
798        let mut dag = CircuitDAG::new();
799
800        let g1 = GraphGate {
801            id: 0,
802            gate_type: "h".to_string(),
803            qubits: vec![QubitId::new(0)],
804            params: vec![],
805            matrix: None,
806        };
807
808        let g2 = GraphGate {
809            id: 1,
810            gate_type: "cnot".to_string(),
811            qubits: vec![QubitId::new(0), QubitId::new(1)],
812            params: vec![],
813            matrix: None,
814        };
815
816        dag.add_gate(g1);
817        dag.add_gate(g2);
818
819        assert_eq!(dag.nodes.len(), 2);
820        assert!(dag.edges.contains_key(&(0, 1)));
821    }
822
823    #[test]
824    fn test_commutation_detection() {
825        let _optimizer = GraphOptimizer::new();
826
827        let g1 = GraphGate {
828            id: 0,
829            gate_type: "z".to_string(),
830            qubits: vec![QubitId::new(0)],
831            params: vec![],
832            matrix: None,
833        };
834
835        let g2 = GraphGate {
836            id: 1,
837            gate_type: "z".to_string(),
838            qubits: vec![QubitId::new(0)],
839            params: vec![],
840            matrix: None,
841        };
842
843        let dag = CircuitDAG::new();
844        assert!(dag.gates_commute(&g1, &g2));
845    }
846
847    #[test]
848    fn test_gate_identification() {
849        let optimizer = GraphOptimizer::new();
850
851        // Pauli X matrix
852        let x_matrix = vec![
853            vec![Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0)],
854            vec![Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)],
855        ];
856
857        if let Some((gate_type, _)) = optimizer.identify_gate(&x_matrix) {
858            assert_eq!(gate_type, "x");
859        } else {
860            panic!("Failed to identify Pauli X gate");
861        }
862    }
863}