Skip to main content

quantrs2_device/
transpiler_scirs2_graph.rs

1//! Enhanced Circuit Transpiler with SciRS2 Graph Optimization
2//!
3//! This module extends the basic transpiler with advanced graph-based circuit optimization
4//! leveraging SciRS2's graph algorithms for:
5//! - Gate dependency analysis
6//! - Circuit topology optimization
7//! - Optimal qubit routing
8//! - Gate commutation and reordering
9//! - Critical path analysis
10
11use crate::{DeviceError, DeviceResult};
12use quantrs2_circuit::prelude::Circuit;
13use quantrs2_core::prelude::*;
14use std::cmp::Reverse;
15use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque};
16
17// Graph structure for gate dependencies
18#[derive(Debug, Clone)]
19pub struct DirectedGraph<T> {
20    nodes: Vec<T>,
21    edges: HashMap<usize, Vec<usize>>,
22}
23
24/// Undirected weighted graph for hardware topology and routing
25#[derive(Debug, Clone)]
26pub struct UndirectedGraph {
27    num_nodes: usize,
28    /// Adjacency list: node -> Vec<(neighbor, weight)>
29    adjacency: HashMap<usize, Vec<(usize, f64)>>,
30}
31
32impl UndirectedGraph {
33    /// Create a new empty undirected graph with `num_nodes` nodes
34    pub fn new(num_nodes: usize) -> Self {
35        Self {
36            num_nodes,
37            adjacency: HashMap::new(),
38        }
39    }
40
41    /// Add an undirected edge between `u` and `v` with the given weight
42    pub fn add_edge(&mut self, u: usize, v: usize, weight: f64) {
43        self.adjacency
44            .entry(u)
45            .or_insert_with(Vec::new)
46            .push((v, weight));
47        self.adjacency
48            .entry(v)
49            .or_insert_with(Vec::new)
50            .push((u, weight));
51    }
52
53    /// Return the number of nodes in the graph
54    pub fn num_nodes(&self) -> usize {
55        self.num_nodes
56    }
57
58    /// BFS traversal from `start`.  Returns visited nodes in BFS order.
59    pub fn bfs(&self, start: usize) -> Vec<usize> {
60        let mut visited = vec![false; self.num_nodes];
61        let mut order = Vec::new();
62        let mut queue = VecDeque::new();
63
64        if start >= self.num_nodes {
65            return order;
66        }
67
68        visited[start] = true;
69        queue.push_back(start);
70
71        while let Some(node) = queue.pop_front() {
72            order.push(node);
73            if let Some(neighbors) = self.adjacency.get(&node) {
74                let mut sorted_neighbors = neighbors.clone();
75                sorted_neighbors.sort_by_key(|(n, _)| *n);
76                for (neighbor, _) in sorted_neighbors {
77                    if !visited[neighbor] {
78                        visited[neighbor] = true;
79                        queue.push_back(neighbor);
80                    }
81                }
82            }
83        }
84        order
85    }
86
87    /// DFS traversal from `start`.  Returns visited nodes in DFS order.
88    pub fn dfs(&self, start: usize) -> Vec<usize> {
89        let mut visited = vec![false; self.num_nodes];
90        let mut order = Vec::new();
91        self.dfs_recursive(start, &mut visited, &mut order);
92        order
93    }
94
95    fn dfs_recursive(&self, node: usize, visited: &mut Vec<bool>, order: &mut Vec<usize>) {
96        if node >= self.num_nodes || visited[node] {
97            return;
98        }
99        visited[node] = true;
100        order.push(node);
101        if let Some(neighbors) = self.adjacency.get(&node) {
102            let mut sorted_neighbors = neighbors.clone();
103            sorted_neighbors.sort_by_key(|(n, _)| *n);
104            for (neighbor, _) in sorted_neighbors {
105                self.dfs_recursive(neighbor, visited, order);
106            }
107        }
108    }
109
110    /// Dijkstra shortest path from `source` to all other nodes.
111    ///
112    /// Returns a `HashMap<usize, f64>` mapping node index to minimum distance.
113    /// Unreachable nodes are absent from the map.
114    ///
115    /// Weights are scaled to `u64` (multiplied by 1e9) for use in the binary
116    /// heap, which requires `Ord`.  This gives nanosecond precision and avoids
117    /// pulling in an `ordered_float` dependency.
118    pub fn dijkstra_distances(&self, source: usize) -> HashMap<usize, f64> {
119        const SCALE: f64 = 1_000_000_000.0;
120        // Min-heap stores (scaled_distance, node)
121        let mut heap: BinaryHeap<(Reverse<u64>, usize)> = BinaryHeap::new();
122        let mut dist: HashMap<usize, u64> = HashMap::new();
123
124        dist.insert(source, 0);
125        heap.push((Reverse(0), source));
126
127        while let Some((Reverse(d), u)) = heap.pop() {
128            if dist.get(&u).map_or(true, |&best| d > best) {
129                continue;
130            }
131            if let Some(neighbors) = self.adjacency.get(&u) {
132                for &(v, w) in neighbors {
133                    let w_scaled = (w * SCALE) as u64;
134                    let next_d = d.saturating_add(w_scaled);
135                    let better = dist.get(&v).map_or(true, |&cur| next_d < cur);
136                    if better {
137                        dist.insert(v, next_d);
138                        heap.push((Reverse(next_d), v));
139                    }
140                }
141            }
142        }
143        dist.into_iter()
144            .map(|(k, v)| (k, v as f64 / SCALE))
145            .collect()
146    }
147
148    /// Dijkstra shortest path from `source` to `target`.
149    ///
150    /// Returns `Some((distance, path))` or `None` if unreachable.
151    pub fn dijkstra_path(&self, source: usize, target: usize) -> Option<(f64, Vec<usize>)> {
152        const SCALE: f64 = 1_000_000_000.0;
153        let mut heap: BinaryHeap<(Reverse<u64>, usize)> = BinaryHeap::new();
154        let mut dist: HashMap<usize, u64> = HashMap::new();
155        let mut prev: HashMap<usize, usize> = HashMap::new();
156
157        dist.insert(source, 0);
158        heap.push((Reverse(0), source));
159
160        while let Some((Reverse(d), u)) = heap.pop() {
161            if u == target {
162                break;
163            }
164            if dist.get(&u).map_or(true, |&best| d > best) {
165                continue;
166            }
167            if let Some(neighbors) = self.adjacency.get(&u) {
168                for &(v, w) in neighbors {
169                    let w_scaled = (w * SCALE) as u64;
170                    let next_d = d.saturating_add(w_scaled);
171                    let better = dist.get(&v).map_or(true, |&cur| next_d < cur);
172                    if better {
173                        dist.insert(v, next_d);
174                        prev.insert(v, u);
175                        heap.push((Reverse(next_d), v));
176                    }
177                }
178            }
179        }
180
181        let total_scaled = *dist.get(&target)?;
182        let total = total_scaled as f64 / SCALE;
183
184        // Reconstruct path by walking `prev` backwards
185        let mut path = Vec::new();
186        let mut cur = target;
187        loop {
188            path.push(cur);
189            if cur == source {
190                break;
191            }
192            match prev.get(&cur) {
193                Some(&p) => cur = p,
194                None => return None, // disconnected
195            }
196        }
197        path.reverse();
198        Some((total, path))
199    }
200}
201
202/// A labeled pattern graph used for subgraph isomorphism matching.
203///
204/// Nodes carry a `String` label (e.g., gate type) and edges represent
205/// dependencies or connections between them.
206#[derive(Debug, Clone)]
207pub struct PatternGraph {
208    /// Node labels
209    labels: Vec<String>,
210    /// Adjacency set: (from, to) pairs
211    edges: HashSet<(usize, usize)>,
212}
213
214impl PatternGraph {
215    /// Create an empty pattern graph
216    pub fn new() -> Self {
217        Self {
218            labels: Vec::new(),
219            edges: HashSet::new(),
220        }
221    }
222
223    /// Add a labeled node; returns its index
224    pub fn add_node(&mut self, label: impl Into<String>) -> usize {
225        let idx = self.labels.len();
226        self.labels.push(label.into());
227        idx
228    }
229
230    /// Add a directed edge
231    pub fn add_edge(&mut self, from: usize, to: usize) {
232        self.edges.insert((from, to));
233    }
234
235    /// Number of nodes
236    pub fn num_nodes(&self) -> usize {
237        self.labels.len()
238    }
239
240    /// Label of node `i`
241    pub fn label(&self, i: usize) -> Option<&str> {
242        self.labels.get(i).map(String::as_str)
243    }
244
245    /// Whether there is an edge (from, to)
246    pub fn has_edge(&self, from: usize, to: usize) -> bool {
247        self.edges.contains(&(from, to))
248    }
249}
250
251/// Result of a subgraph isomorphism search.
252///
253/// Each entry is a mapping `pattern_node -> target_node`.
254#[derive(Debug, Clone)]
255pub struct IsomorphismMapping {
256    /// `mapping[p] = t` means pattern node `p` maps to target node `t`
257    pub mapping: Vec<usize>,
258}
259
260/// VF2-inspired subgraph isomorphism engine (simplified backtracking).
261///
262/// Checks whether `pattern` is isomorphic to a subgraph of `target_labels /
263/// target_edges`.
264pub struct SubgraphIsomorphism<'a> {
265    pattern: &'a PatternGraph,
266    target_labels: &'a [String],
267    target_edges: &'a HashSet<(usize, usize)>,
268}
269
270impl<'a> SubgraphIsomorphism<'a> {
271    /// Create a new searcher
272    pub fn new(
273        pattern: &'a PatternGraph,
274        target_labels: &'a [String],
275        target_edges: &'a HashSet<(usize, usize)>,
276    ) -> Self {
277        Self {
278            pattern,
279            target_labels,
280            target_edges,
281        }
282    }
283
284    /// Find all subgraph isomorphisms.  Returns a list of mappings.
285    pub fn find_all(&self) -> Vec<IsomorphismMapping> {
286        let n_p = self.pattern.num_nodes();
287        let n_t = self.target_labels.len();
288        if n_p == 0 || n_p > n_t {
289            return Vec::new();
290        }
291
292        let mut results = Vec::new();
293        // `partial[p]` = index of the target node assigned to pattern node `p`
294        let mut partial: Vec<Option<usize>> = vec![None; n_p];
295        // Reverse lookup: which target nodes are already used
296        let mut used = vec![false; n_t];
297
298        self.backtrack(0, &mut partial, &mut used, &mut results);
299        results
300    }
301
302    /// Recursive backtracking procedure (VF2-style)
303    fn backtrack(
304        &self,
305        depth: usize,
306        partial: &mut Vec<Option<usize>>,
307        used: &mut Vec<bool>,
308        results: &mut Vec<IsomorphismMapping>,
309    ) {
310        let n_p = self.pattern.num_nodes();
311        if depth == n_p {
312            // Full mapping found – record it
313            let mapping: Vec<usize> = partial.iter().filter_map(|x| *x).collect();
314            results.push(IsomorphismMapping { mapping });
315            return;
316        }
317
318        // Try to assign each unoccupied target node to pattern node `depth`
319        for t in 0..self.target_labels.len() {
320            if used[t] {
321                continue;
322            }
323            // Compatibility check 1: node labels must match
324            if !self.labels_compatible(depth, t) {
325                continue;
326            }
327            // Compatibility check 2: structural consistency with already-mapped nodes
328            if !self.structurally_consistent(depth, t, partial) {
329                continue;
330            }
331
332            // Extend mapping
333            partial[depth] = Some(t);
334            used[t] = true;
335
336            self.backtrack(depth + 1, partial, used, results);
337
338            // Undo
339            partial[depth] = None;
340            used[t] = false;
341        }
342    }
343
344    /// Check whether the label of pattern node `p` is compatible with target node `t`.
345    /// An empty pattern label acts as a wildcard.
346    fn labels_compatible(&self, p: usize, t: usize) -> bool {
347        let p_label = match self.pattern.label(p) {
348            Some(l) => l,
349            None => return false,
350        };
351        if p_label.is_empty() {
352            return true; // wildcard
353        }
354        match self.target_labels.get(t) {
355            Some(t_label) => p_label == t_label.as_str(),
356            None => false,
357        }
358    }
359
360    /// Check structural consistency: for every already-mapped pattern node `q < depth`,
361    /// ensure that edges between `q` and `depth` are preserved in the target graph.
362    fn structurally_consistent(&self, depth: usize, t: usize, partial: &[Option<usize>]) -> bool {
363        for q in 0..depth {
364            let mapped_q = match partial[q] {
365                Some(m) => m,
366                None => continue,
367            };
368            // If pattern has edge q→depth, target must have mapped_q→t
369            if self.pattern.has_edge(q, depth) && !self.target_edges.contains(&(mapped_q, t)) {
370                return false;
371            }
372            // If pattern has edge depth→q, target must have t→mapped_q
373            if self.pattern.has_edge(depth, q) && !self.target_edges.contains(&(t, mapped_q)) {
374                return false;
375            }
376        }
377        true
378    }
379}
380
381impl<T: Clone + PartialEq> DirectedGraph<T> {
382    pub fn new() -> Self {
383        Self {
384            nodes: Vec::new(),
385            edges: HashMap::new(),
386        }
387    }
388
389    pub fn add_node(&mut self, node: T) -> usize {
390        let idx = self.nodes.len();
391        self.nodes.push(node);
392        idx
393    }
394
395    pub fn add_edge(&mut self, from_idx: usize, to_idx: usize) {
396        self.edges
397            .entry(from_idx)
398            .or_insert_with(Vec::new)
399            .push(to_idx);
400    }
401
402    pub fn nodes(&self) -> &[T] {
403        &self.nodes
404    }
405
406    pub fn has_edge(&self, from: &T, to: &T) -> bool {
407        if let Some(from_idx) = self.nodes.iter().position(|n| n == from) {
408            if let Some(to_idx) = self.nodes.iter().position(|n| n == to) {
409                if let Some(neighbors) = self.edges.get(&from_idx) {
410                    return neighbors.contains(&to_idx);
411                }
412            }
413        }
414        false
415    }
416
417    pub fn num_nodes(&self) -> usize {
418        self.nodes.len()
419    }
420
421    pub fn num_edges(&self) -> usize {
422        self.edges.values().map(|v| v.len()).sum()
423    }
424}
425
426/// Gate dependency graph node
427#[derive(Debug, Clone, PartialEq, Eq, Hash)]
428pub struct GateNode {
429    /// Gate index in original circuit
430    pub gate_index: usize,
431    /// Gate type/name
432    pub gate_type: String,
433    /// Qubits this gate acts on
434    pub qubits: Vec<usize>,
435    /// Gate depth in the circuit
436    pub depth: usize,
437}
438
439/// Circuit topology representation for hardware mapping
440#[derive(Debug, Clone)]
441pub struct CircuitTopology {
442    /// Qubit connectivity graph
443    pub qubit_graph: DirectedGraph<usize>,
444    /// Gate dependency graph
445    pub gate_graph: DirectedGraph<GateNode>,
446    /// Critical path length
447    pub critical_path_length: usize,
448    /// Circuit depth
449    pub circuit_depth: usize,
450}
451
452/// Hardware topology constraints
453#[derive(Debug, Clone)]
454pub struct HardwareTopology {
455    /// Physical qubit connectivity (adjacency list)
456    pub qubit_connectivity: HashMap<usize, Vec<usize>>,
457    /// Number of physical qubits
458    pub num_physical_qubits: usize,
459    /// Gate error rates per qubit pair
460    pub error_rates: HashMap<(usize, usize), f64>,
461}
462
463impl Default for HardwareTopology {
464    fn default() -> Self {
465        Self {
466            qubit_connectivity: HashMap::new(),
467            num_physical_qubits: 0,
468            error_rates: HashMap::new(),
469        }
470    }
471}
472
473/// Configuration for graph-based transpilation
474#[derive(Debug, Clone)]
475pub struct SciRS2TranspilerConfig {
476    /// Enable gate commutation optimization
477    pub enable_commutation: bool,
478    /// Enable critical path optimization
479    pub enable_critical_path_opt: bool,
480    /// Enable qubit routing optimization
481    pub enable_routing_opt: bool,
482    /// Maximum optimization passes
483    pub max_optimization_passes: usize,
484    /// Target hardware topology
485    pub hardware_topology: Option<HardwareTopology>,
486}
487
488impl Default for SciRS2TranspilerConfig {
489    fn default() -> Self {
490        Self {
491            enable_commutation: true,
492            enable_critical_path_opt: true,
493            enable_routing_opt: true,
494            max_optimization_passes: 3,
495            hardware_topology: None,
496        }
497    }
498}
499
500/// Enhanced transpiler using SciRS2 graph algorithms
501pub struct SciRS2GraphTranspiler {
502    config: SciRS2TranspilerConfig,
503}
504
505impl SciRS2GraphTranspiler {
506    /// Create a new SciRS2 graph transpiler
507    pub fn new(config: SciRS2TranspilerConfig) -> Self {
508        Self { config }
509    }
510
511    /// Build gate dependency graph from circuit
512    pub fn build_dependency_graph<const N: usize>(
513        &self,
514        circuit: &Circuit<N>,
515    ) -> DeviceResult<DirectedGraph<GateNode>> {
516        let mut graph = DirectedGraph::new();
517        let mut qubit_last_gate: HashMap<usize, usize> = HashMap::new();
518
519        // Create nodes for each gate
520        for (idx, gate) in circuit.gates().iter().enumerate() {
521            let node = GateNode {
522                gate_index: idx,
523                gate_type: gate.name().to_string(),
524                qubits: gate.qubits().iter().map(|q| q.id() as usize).collect(),
525                depth: 0, // Will be computed later
526            };
527            let node_idx = graph.add_node(node);
528
529            // Add edges based on qubit dependencies
530            for qubit in gate.qubits() {
531                let q_id = qubit.id() as usize;
532
533                // If there's a previous gate on this qubit, add dependency edge
534                if let Some(&prev_idx) = qubit_last_gate.get(&q_id) {
535                    graph.add_edge(prev_idx, node_idx);
536                }
537
538                // Update last gate for this qubit
539                qubit_last_gate.insert(q_id, node_idx);
540            }
541        }
542
543        Ok(graph)
544    }
545
546    /// Analyze circuit topology using graph algorithms
547    pub fn analyze_topology<const N: usize>(
548        &self,
549        circuit: &Circuit<N>,
550    ) -> DeviceResult<CircuitTopology> {
551        // Build gate dependency graph
552        let gate_graph = self.build_dependency_graph(circuit)?;
553
554        // Build qubit connectivity graph
555        let mut qubit_graph = DirectedGraph::new();
556        let mut qubit_node_indices: HashMap<usize, usize> = HashMap::new();
557
558        for gate in circuit.gates() {
559            let qubits: Vec<usize> = gate.qubits().iter().map(|q| q.id() as usize).collect();
560
561            // Add qubit nodes if not already present
562            for &q in &qubits {
563                qubit_node_indices
564                    .entry(q)
565                    .or_insert_with(|| qubit_graph.add_node(q));
566            }
567
568            // For two-qubit gates, add connectivity
569            if qubits.len() == 2 {
570                let (q0, q1) = (qubits[0], qubits[1]);
571                if q0 != q1 {
572                    if let (Some(&idx0), Some(&idx1)) =
573                        (qubit_node_indices.get(&q0), qubit_node_indices.get(&q1))
574                    {
575                        qubit_graph.add_edge(idx0, idx1);
576                        qubit_graph.add_edge(idx1, idx0); // Bidirectional
577                    }
578                }
579            }
580        }
581
582        // Compute circuit depth using topological sort
583        let circuit_depth = self.compute_circuit_depth(&gate_graph)?;
584
585        // Compute critical path
586        let critical_path_length = self.compute_critical_path(&gate_graph)?;
587
588        Ok(CircuitTopology {
589            qubit_graph,
590            gate_graph,
591            critical_path_length,
592            circuit_depth,
593        })
594    }
595
596    /// Compute circuit depth using simple dependency analysis
597    fn compute_circuit_depth(&self, gate_graph: &DirectedGraph<GateNode>) -> DeviceResult<usize> {
598        // Simplified depth computation without topological sort
599        let mut depths: HashMap<usize, usize> = HashMap::new();
600        let mut max_depth = 0;
601
602        // Process all gates
603        for node in gate_graph.nodes() {
604            // Compute depth as 1 + max depth of predecessors
605            let mut gate_depth = 0;
606
607            // Find predecessors (gates that must execute before this one)
608            for potential_pred in gate_graph.nodes() {
609                if gate_graph.has_edge(potential_pred, node) {
610                    if let Some(&pred_depth) = depths.get(&potential_pred.gate_index) {
611                        gate_depth = gate_depth.max(pred_depth + 1);
612                    }
613                }
614            }
615
616            depths.insert(node.gate_index, gate_depth);
617            max_depth = max_depth.max(gate_depth);
618        }
619
620        Ok(max_depth + 1)
621    }
622
623    /// Compute critical path length (longest dependency chain)
624    fn compute_critical_path(&self, gate_graph: &DirectedGraph<GateNode>) -> DeviceResult<usize> {
625        // Critical path = longest path in DAG
626        // Simple dynamic programming approach
627
628        let mut longest_paths: HashMap<usize, usize> = HashMap::new();
629        let mut max_path_length = 0;
630
631        for node in gate_graph.nodes() {
632            let mut path_length = 0;
633
634            // Find the longest path to this gate
635            for potential_pred in gate_graph.nodes() {
636                if gate_graph.has_edge(potential_pred, node) {
637                    if let Some(&pred_path) = longest_paths.get(&potential_pred.gate_index) {
638                        path_length = path_length.max(pred_path + 1);
639                    }
640                }
641            }
642
643            longest_paths.insert(node.gate_index, path_length);
644            max_path_length = max_path_length.max(path_length);
645        }
646
647        Ok(max_path_length)
648    }
649
650    /// Optimize qubit routing using Dijkstra shortest paths on the hardware graph.
651    ///
652    /// For each logical qubit, we compute all-pairs shortest distances on the
653    /// hardware coupling map and assign logical qubits to physical qubits in
654    /// order of descending two-qubit gate frequency, choosing the physical qubit
655    /// with the smallest average distance to already-assigned qubits.
656    pub fn optimize_qubit_routing<const N: usize>(
657        &self,
658        circuit: &Circuit<N>,
659        hardware_topology: &HardwareTopology,
660    ) -> DeviceResult<HashMap<usize, usize>> {
661        // Build hardware graph for Dijkstra
662        let n_phys = hardware_topology.num_physical_qubits;
663        let mut hw_graph = UndirectedGraph::new(n_phys);
664        for (&phys_q, neighbors) in &hardware_topology.qubit_connectivity {
665            for &neighbor in neighbors {
666                if phys_q < neighbor {
667                    // Use error rate as edge weight, falling back to 1.0
668                    let weight = hardware_topology
669                        .error_rates
670                        .get(&(phys_q, neighbor))
671                        .copied()
672                        .unwrap_or(1.0);
673                    hw_graph.add_edge(phys_q, neighbor, weight);
674                }
675            }
676        }
677
678        // Precompute all-pairs shortest distances via Dijkstra for each physical qubit
679        let all_dists: Vec<HashMap<usize, f64>> = (0..n_phys)
680            .map(|src| hw_graph.dijkstra_distances(src))
681            .collect();
682
683        // Count how often each logical qubit pair appears in two-qubit gates
684        let mut interaction_freq: HashMap<(usize, usize), usize> = HashMap::new();
685        for gate in circuit.gates() {
686            let qubits: Vec<usize> = gate.qubits().iter().map(|q| q.id() as usize).collect();
687            if qubits.len() == 2 {
688                let (a, b) = (qubits[0].min(qubits[1]), qubits[0].max(qubits[1]));
689                *interaction_freq.entry((a, b)).or_insert(0) += 1;
690            }
691        }
692
693        // Build per-logical-qubit interaction count
694        let mut qubit_freq = vec![0usize; N];
695        for (&(a, b), &freq) in &interaction_freq {
696            if a < N {
697                qubit_freq[a] += freq;
698            }
699            if b < N {
700                qubit_freq[b] += freq;
701            }
702        }
703
704        // Sort logical qubits by descending interaction frequency
705        let mut sorted_logical: Vec<usize> = (0..N).collect();
706        sorted_logical.sort_by(|&x, &y| qubit_freq[y].cmp(&qubit_freq[x]));
707
708        // Greedy assignment: pick physical qubit minimizing average distance to
709        // already-assigned neighbours
710        let mut mapping: HashMap<usize, usize> = HashMap::new();
711        let mut assigned_phys: HashSet<usize> = HashSet::new();
712
713        for &logical in &sorted_logical {
714            // Collect logical neighbours that are already assigned
715            let assigned_neighbors: Vec<usize> = mapping.keys().copied().collect();
716
717            let best_phys = (0..n_phys)
718                .filter(|p| !assigned_phys.contains(p))
719                .min_by(|&p1, &p2| {
720                    let score = |p: usize| -> f64 {
721                        if assigned_neighbors.is_empty() {
722                            return p as f64; // tie-break by index
723                        }
724                        assigned_neighbors
725                            .iter()
726                            .map(|ln| {
727                                let phys_n = *mapping.get(ln).unwrap_or(&0);
728                                all_dists[p].get(&phys_n).copied().unwrap_or(f64::MAX)
729                            })
730                            .sum::<f64>()
731                            / assigned_neighbors.len() as f64
732                    };
733                    score(p1)
734                        .partial_cmp(&score(p2))
735                        .unwrap_or(std::cmp::Ordering::Equal)
736                })
737                .unwrap_or_else(|| logical % n_phys.max(1));
738
739            mapping.insert(logical, best_phys);
740            assigned_phys.insert(best_phys);
741        }
742
743        Ok(mapping)
744    }
745
746    /// Search for occurrences of a circuit pattern in a larger circuit using
747    /// the VF2 subgraph-isomorphism algorithm.
748    ///
749    /// The pattern is represented as a `PatternGraph` whose node labels are
750    /// gate type strings (empty = wildcard).  Returns all qubit mappings found.
751    pub fn find_circuit_pattern_matches<const N: usize>(
752        &self,
753        circuit: &Circuit<N>,
754        pattern: &PatternGraph,
755    ) -> DeviceResult<Vec<IsomorphismMapping>> {
756        // Build target node labels and edge set from the dependency graph
757        let dep_graph = self.build_dependency_graph(circuit)?;
758
759        let target_labels: Vec<String> = dep_graph
760            .nodes()
761            .iter()
762            .map(|n| n.gate_type.clone())
763            .collect();
764
765        let mut target_edges: HashSet<(usize, usize)> = HashSet::new();
766        for (from_idx, to_list) in &dep_graph.edges {
767            for &to_idx in to_list {
768                target_edges.insert((*from_idx, to_idx));
769            }
770        }
771
772        let iso = SubgraphIsomorphism::new(pattern, &target_labels, &target_edges);
773        Ok(iso.find_all())
774    }
775
776    /// Identify commuting gates using dependency analysis
777    pub fn find_commuting_gates<const N: usize>(
778        &self,
779        circuit: &Circuit<N>,
780    ) -> DeviceResult<Vec<(usize, usize)>> {
781        let mut commuting_pairs = Vec::new();
782        let gates = circuit.gates();
783
784        for i in 0..gates.len() {
785            for j in (i + 1)..gates.len() {
786                // Check if gates commute (act on disjoint qubits)
787                let qubits_i: HashSet<u32> = gates[i].qubits().iter().map(|q| q.id()).collect();
788                let qubits_j: HashSet<u32> = gates[j].qubits().iter().map(|q| q.id()).collect();
789
790                if qubits_i.is_disjoint(&qubits_j) {
791                    commuting_pairs.push((i, j));
792                }
793            }
794        }
795
796        Ok(commuting_pairs)
797    }
798
799    /// Optimize circuit using graph-based analysis.
800    ///
801    /// Uses the gate dependency graph from `analyze_topology` to perform
802    /// level-based topological scheduling.  Gates are grouped into levels
803    /// where each level contains only gates that have no unsatisfied
804    /// dependency on each other (commuting gates at the same depth).
805    /// Reordering gates within and across levels implements both gate
806    /// commutation reordering and parallel gate scheduling without
807    /// requiring SWAP-gate insertion.
808    pub fn optimize_circuit<const N: usize>(
809        &self,
810        circuit: &Circuit<N>,
811    ) -> DeviceResult<Circuit<N>> {
812        let topology = self.analyze_topology(circuit)?;
813        let gate_graph = &topology.gate_graph;
814        let original_gates = circuit.gates();
815        let num_gates = original_gates.len();
816
817        if num_gates == 0 {
818            return Ok(Circuit::<N>::new());
819        }
820
821        // Compute the level (earliest executable slot) for every gate using the
822        // dependency graph: level[i] = 1 + max(level[pred]) for all predecessors.
823        let mut levels: Vec<usize> = vec![0; num_gates];
824        let nodes = gate_graph.nodes();
825
826        // Iterate in gate-index order; for acyclic dependency graphs this gives
827        // correct results because dependencies always point from lower to higher
828        // indices (gates are added in circuit order).
829        for node in nodes {
830            let idx = node.gate_index;
831            let mut max_pred_level = 0usize;
832
833            for pred in nodes {
834                if gate_graph.has_edge(pred, node) {
835                    let pred_level = levels.get(pred.gate_index).copied().unwrap_or(0);
836                    if pred_level + 1 > max_pred_level {
837                        max_pred_level = pred_level + 1;
838                    }
839                }
840            }
841
842            if idx < levels.len() {
843                levels[idx] = max_pred_level;
844            }
845        }
846
847        // Bucket gate indices by level, then sort within each level by the
848        // smallest qubit id the gate touches (deterministic, reproducible ordering).
849        let max_level = levels.iter().copied().max().unwrap_or(0);
850        let mut buckets: Vec<Vec<usize>> = vec![Vec::new(); max_level + 1];
851        for (gate_idx, &level) in levels.iter().enumerate() {
852            buckets[level].push(gate_idx);
853        }
854
855        // Within each level, sort by smallest qubit id for deterministic output.
856        for bucket in &mut buckets {
857            bucket.sort_by_key(|&gate_idx| {
858                original_gates
859                    .get(gate_idx)
860                    .map(|g| g.qubits().iter().map(|q| q.id()).min().unwrap_or(u32::MAX))
861                    .unwrap_or(u32::MAX)
862            });
863        }
864
865        // Emit gates level-by-level into a fresh circuit.
866        let mut optimized = Circuit::<N>::new();
867        for bucket in &buckets {
868            for &gate_idx in bucket {
869                if let Some(gate_arc) = original_gates.get(gate_idx) {
870                    optimized.add_gate_arc(gate_arc.clone())?;
871                }
872            }
873        }
874
875        Ok(optimized)
876    }
877
878    /// Generate optimization report with graph analysis
879    pub fn generate_optimization_report<const N: usize>(
880        &self,
881        circuit: &Circuit<N>,
882    ) -> DeviceResult<String> {
883        let topology = self.analyze_topology(circuit)?;
884
885        let mut report = String::from("=== SciRS2 Graph Transpiler Analysis ===\n\n");
886        report.push_str(&format!("Circuit Depth: {}\n", topology.circuit_depth));
887        report.push_str(&format!(
888            "Critical Path Length: {}\n",
889            topology.critical_path_length
890        ));
891        report.push_str(&format!("Number of Gates: {}\n", circuit.gates().len()));
892        report.push_str(&format!("Number of Qubits: {}\n", N));
893
894        // Qubit connectivity statistics
895        let num_qubit_edges = topology.qubit_graph.num_edges();
896        report.push_str(&format!("Qubit Connections: {}\n", num_qubit_edges));
897
898        // Gate dependency statistics
899        let num_dependencies = topology.gate_graph.num_edges();
900        report.push_str(&format!("Gate Dependencies: {}\n", num_dependencies));
901
902        // Commuting gate analysis
903        if self.config.enable_commutation {
904            let commuting = self.find_commuting_gates(circuit)?;
905            report.push_str(&format!("Commuting Gate Pairs: {}\n", commuting.len()));
906        }
907
908        Ok(report)
909    }
910}
911
912#[cfg(test)]
913#[allow(clippy::pedantic, clippy::field_reassign_with_default)]
914mod tests {
915    use super::*;
916    use quantrs2_circuit::prelude::*;
917
918    #[test]
919    fn test_transpiler_creation() {
920        let config = SciRS2TranspilerConfig::default();
921        let transpiler = SciRS2GraphTranspiler::new(config);
922        assert!(transpiler.config.enable_commutation);
923    }
924
925    #[test]
926    fn test_dependency_graph_building() {
927        let mut circuit = Circuit::<2>::new();
928        let _ = circuit.h(0);
929        let _ = circuit.cnot(0, 1);
930        let _ = circuit.h(1);
931
932        let config = SciRS2TranspilerConfig::default();
933        let transpiler = SciRS2GraphTranspiler::new(config);
934
935        let graph = transpiler
936            .build_dependency_graph(&circuit)
937            .expect("Failed to build dependency graph");
938
939        assert_eq!(graph.num_nodes(), 3); // H, CNOT, H
940    }
941
942    #[test]
943    fn test_topology_analysis() {
944        let mut circuit = Circuit::<3>::new();
945        let _ = circuit.h(0);
946        let _ = circuit.h(1);
947        let _ = circuit.cnot(0, 1);
948        let _ = circuit.cnot(1, 2);
949
950        let config = SciRS2TranspilerConfig::default();
951        let transpiler = SciRS2GraphTranspiler::new(config);
952
953        let topology = transpiler
954            .analyze_topology(&circuit)
955            .expect("Failed to analyze topology");
956
957        assert!(topology.circuit_depth > 0);
958        assert!(topology.critical_path_length > 0);
959    }
960
961    #[test]
962    fn test_commuting_gates_detection() {
963        let mut circuit = Circuit::<4>::new();
964        let _ = circuit.h(0);
965        let _ = circuit.h(1); // Commutes with H(0)
966        let _ = circuit.x(2); // Commutes with both
967        let _ = circuit.cnot(0, 1); // Does not commute with H(0) or H(1)
968
969        let config = SciRS2TranspilerConfig::default();
970        let transpiler = SciRS2GraphTranspiler::new(config);
971
972        let commuting = transpiler
973            .find_commuting_gates(&circuit)
974            .expect("Failed to find commuting gates");
975
976        assert!(!commuting.is_empty());
977    }
978
979    #[test]
980    fn test_optimization_report() {
981        let mut circuit = Circuit::<2>::new();
982        let _ = circuit.h(0);
983        let _ = circuit.cnot(0, 1);
984        let _ = circuit.measure_all();
985
986        let config = SciRS2TranspilerConfig::default();
987        let transpiler = SciRS2GraphTranspiler::new(config);
988
989        let report = transpiler
990            .generate_optimization_report(&circuit)
991            .expect("Failed to generate report");
992
993        assert!(report.contains("Circuit Depth"));
994        assert!(report.contains("Critical Path"));
995    }
996
997    #[test]
998    fn test_hardware_topology_creation() {
999        let mut topology = HardwareTopology {
1000            num_physical_qubits: 5,
1001            ..Default::default()
1002        };
1003
1004        // Linear connectivity: 0-1-2-3-4
1005        topology.qubit_connectivity.insert(0, vec![1]);
1006        topology.qubit_connectivity.insert(1, vec![0, 2]);
1007        topology.qubit_connectivity.insert(2, vec![1, 3]);
1008        topology.qubit_connectivity.insert(3, vec![2, 4]);
1009        topology.qubit_connectivity.insert(4, vec![3]);
1010
1011        assert_eq!(topology.num_physical_qubits, 5);
1012        assert_eq!(topology.qubit_connectivity.len(), 5);
1013    }
1014
1015    #[test]
1016    fn test_qubit_routing_optimization() {
1017        let mut circuit = Circuit::<3>::new();
1018        let _ = circuit.cnot(0, 1);
1019        let _ = circuit.cnot(1, 2);
1020
1021        let mut hardware = HardwareTopology::default();
1022        hardware.num_physical_qubits = 5;
1023        hardware.qubit_connectivity.insert(0, vec![1]);
1024        hardware.qubit_connectivity.insert(1, vec![0, 2]);
1025        hardware.qubit_connectivity.insert(2, vec![1]);
1026
1027        let config = SciRS2TranspilerConfig {
1028            enable_routing_opt: true,
1029            ..Default::default()
1030        };
1031        let transpiler = SciRS2GraphTranspiler::new(config);
1032
1033        let mapping = transpiler
1034            .optimize_qubit_routing(&circuit, &hardware)
1035            .expect("Failed to optimize routing");
1036
1037        assert_eq!(mapping.len(), 3);
1038    }
1039
1040    // ── New algorithm tests ────────────────────────────────────────────────
1041
1042    #[test]
1043    fn test_undirected_graph_bfs() {
1044        // Linear graph: 0-1-2-3
1045        let mut g = UndirectedGraph::new(4);
1046        g.add_edge(0, 1, 1.0);
1047        g.add_edge(1, 2, 1.0);
1048        g.add_edge(2, 3, 1.0);
1049
1050        let order = g.bfs(0);
1051        assert_eq!(order, vec![0, 1, 2, 3]);
1052    }
1053
1054    #[test]
1055    fn test_undirected_graph_dfs() {
1056        // Linear graph: 0-1-2-3
1057        let mut g = UndirectedGraph::new(4);
1058        g.add_edge(0, 1, 1.0);
1059        g.add_edge(1, 2, 1.0);
1060        g.add_edge(2, 3, 1.0);
1061
1062        let order = g.dfs(0);
1063        assert_eq!(order, vec![0, 1, 2, 3]);
1064    }
1065
1066    #[test]
1067    fn test_dijkstra_linear_graph() {
1068        // 0 -1.0- 1 -2.0- 2 -1.0- 3
1069        let mut g = UndirectedGraph::new(4);
1070        g.add_edge(0, 1, 1.0);
1071        g.add_edge(1, 2, 2.0);
1072        g.add_edge(2, 3, 1.0);
1073
1074        let dists = g.dijkstra_distances(0);
1075        assert!((dists[&0] - 0.0).abs() < 1e-6);
1076        assert!((dists[&1] - 1.0).abs() < 1e-6);
1077        assert!((dists[&2] - 3.0).abs() < 1e-6);
1078        assert!((dists[&3] - 4.0).abs() < 1e-6);
1079    }
1080
1081    #[test]
1082    fn test_dijkstra_path() {
1083        let mut g = UndirectedGraph::new(5);
1084        g.add_edge(0, 1, 1.0);
1085        g.add_edge(1, 2, 1.0);
1086        g.add_edge(0, 3, 5.0);
1087        g.add_edge(3, 2, 1.0); // 0->3->2 costs 6, but 0->1->2 costs 2
1088
1089        let result = g.dijkstra_path(0, 2);
1090        assert!(result.is_some());
1091        let (dist, path) = result.expect("path should exist");
1092        assert!(
1093            (dist - 2.0).abs() < 1e-6,
1094            "expected distance 2.0, got {}",
1095            dist
1096        );
1097        assert_eq!(path, vec![0, 1, 2]);
1098    }
1099
1100    #[test]
1101    fn test_pattern_graph_construction() {
1102        let mut pattern = PatternGraph::new();
1103        let h = pattern.add_node("H");
1104        let cx = pattern.add_node("CNOT");
1105        pattern.add_edge(h, cx);
1106
1107        assert_eq!(pattern.num_nodes(), 2);
1108        assert!(pattern.has_edge(0, 1));
1109        assert!(!pattern.has_edge(1, 0));
1110    }
1111
1112    #[test]
1113    fn test_subgraph_isomorphism_simple() {
1114        // Pattern: H -> CNOT
1115        let mut pattern = PatternGraph::new();
1116        let ph = pattern.add_node("H");
1117        let pcx = pattern.add_node("CNOT");
1118        pattern.add_edge(ph, pcx);
1119
1120        // Target: H(q0) -> CNOT(q0,q1) -> H(q1)
1121        let target_labels = vec!["H".to_string(), "CNOT".to_string(), "H".to_string()];
1122        let mut target_edges: HashSet<(usize, usize)> = HashSet::new();
1123        target_edges.insert((0, 1)); // H -> CNOT
1124        target_edges.insert((1, 2)); // CNOT -> H
1125
1126        let iso = SubgraphIsomorphism::new(&pattern, &target_labels, &target_edges);
1127        let mappings = iso.find_all();
1128        // Pattern node 0 (H) -> target 0, pattern node 1 (CNOT) -> target 1
1129        assert!(!mappings.is_empty(), "Should find at least one isomorphism");
1130    }
1131
1132    #[test]
1133    fn test_find_circuit_pattern_matches() {
1134        let mut circuit = Circuit::<2>::new();
1135        let _ = circuit.h(0);
1136        let _ = circuit.cnot(0, 1);
1137        let _ = circuit.h(1);
1138
1139        // Build pattern: any gate -> CNOT
1140        let mut pattern = PatternGraph::new();
1141        let _p0 = pattern.add_node(""); // wildcard
1142        let p1 = pattern.add_node("CNOT");
1143        pattern.add_edge(0, 1);
1144
1145        let config = SciRS2TranspilerConfig::default();
1146        let transpiler = SciRS2GraphTranspiler::new(config);
1147
1148        let matches = transpiler
1149            .find_circuit_pattern_matches(&circuit, &pattern)
1150            .expect("Pattern match should succeed");
1151
1152        assert!(
1153            !matches.is_empty(),
1154            "Should find at least one pattern occurrence"
1155        );
1156    }
1157}