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    pub fn optimize_circuit<const N: usize>(
801        &self,
802        circuit: &Circuit<N>,
803    ) -> DeviceResult<Circuit<N>> {
804        // Analyze circuit topology
805        let _topology = self.analyze_topology(circuit)?;
806
807        // TODO: Implement optimization transformations
808        // - Use graph analysis for gate commutation reordering
809        // - Implement parallel gate scheduling
810        // - Add SWAP gate insertion for routing
811
812        // For now, return the original circuit
813        Ok(circuit.clone())
814    }
815
816    /// Generate optimization report with graph analysis
817    pub fn generate_optimization_report<const N: usize>(
818        &self,
819        circuit: &Circuit<N>,
820    ) -> DeviceResult<String> {
821        let topology = self.analyze_topology(circuit)?;
822
823        let mut report = String::from("=== SciRS2 Graph Transpiler Analysis ===\n\n");
824        report.push_str(&format!("Circuit Depth: {}\n", topology.circuit_depth));
825        report.push_str(&format!(
826            "Critical Path Length: {}\n",
827            topology.critical_path_length
828        ));
829        report.push_str(&format!("Number of Gates: {}\n", circuit.gates().len()));
830        report.push_str(&format!("Number of Qubits: {}\n", N));
831
832        // Qubit connectivity statistics
833        let num_qubit_edges = topology.qubit_graph.num_edges();
834        report.push_str(&format!("Qubit Connections: {}\n", num_qubit_edges));
835
836        // Gate dependency statistics
837        let num_dependencies = topology.gate_graph.num_edges();
838        report.push_str(&format!("Gate Dependencies: {}\n", num_dependencies));
839
840        // Commuting gate analysis
841        if self.config.enable_commutation {
842            let commuting = self.find_commuting_gates(circuit)?;
843            report.push_str(&format!("Commuting Gate Pairs: {}\n", commuting.len()));
844        }
845
846        Ok(report)
847    }
848}
849
850#[cfg(test)]
851#[allow(clippy::pedantic, clippy::field_reassign_with_default)]
852mod tests {
853    use super::*;
854    use quantrs2_circuit::prelude::*;
855
856    #[test]
857    fn test_transpiler_creation() {
858        let config = SciRS2TranspilerConfig::default();
859        let transpiler = SciRS2GraphTranspiler::new(config);
860        assert!(transpiler.config.enable_commutation);
861    }
862
863    #[test]
864    fn test_dependency_graph_building() {
865        let mut circuit = Circuit::<2>::new();
866        let _ = circuit.h(0);
867        let _ = circuit.cnot(0, 1);
868        let _ = circuit.h(1);
869
870        let config = SciRS2TranspilerConfig::default();
871        let transpiler = SciRS2GraphTranspiler::new(config);
872
873        let graph = transpiler
874            .build_dependency_graph(&circuit)
875            .expect("Failed to build dependency graph");
876
877        assert_eq!(graph.num_nodes(), 3); // H, CNOT, H
878    }
879
880    #[test]
881    fn test_topology_analysis() {
882        let mut circuit = Circuit::<3>::new();
883        let _ = circuit.h(0);
884        let _ = circuit.h(1);
885        let _ = circuit.cnot(0, 1);
886        let _ = circuit.cnot(1, 2);
887
888        let config = SciRS2TranspilerConfig::default();
889        let transpiler = SciRS2GraphTranspiler::new(config);
890
891        let topology = transpiler
892            .analyze_topology(&circuit)
893            .expect("Failed to analyze topology");
894
895        assert!(topology.circuit_depth > 0);
896        assert!(topology.critical_path_length > 0);
897    }
898
899    #[test]
900    fn test_commuting_gates_detection() {
901        let mut circuit = Circuit::<4>::new();
902        let _ = circuit.h(0);
903        let _ = circuit.h(1); // Commutes with H(0)
904        let _ = circuit.x(2); // Commutes with both
905        let _ = circuit.cnot(0, 1); // Does not commute with H(0) or H(1)
906
907        let config = SciRS2TranspilerConfig::default();
908        let transpiler = SciRS2GraphTranspiler::new(config);
909
910        let commuting = transpiler
911            .find_commuting_gates(&circuit)
912            .expect("Failed to find commuting gates");
913
914        assert!(!commuting.is_empty());
915    }
916
917    #[test]
918    fn test_optimization_report() {
919        let mut circuit = Circuit::<2>::new();
920        let _ = circuit.h(0);
921        let _ = circuit.cnot(0, 1);
922        let _ = circuit.measure_all();
923
924        let config = SciRS2TranspilerConfig::default();
925        let transpiler = SciRS2GraphTranspiler::new(config);
926
927        let report = transpiler
928            .generate_optimization_report(&circuit)
929            .expect("Failed to generate report");
930
931        assert!(report.contains("Circuit Depth"));
932        assert!(report.contains("Critical Path"));
933    }
934
935    #[test]
936    fn test_hardware_topology_creation() {
937        let mut topology = HardwareTopology {
938            num_physical_qubits: 5,
939            ..Default::default()
940        };
941
942        // Linear connectivity: 0-1-2-3-4
943        topology.qubit_connectivity.insert(0, vec![1]);
944        topology.qubit_connectivity.insert(1, vec![0, 2]);
945        topology.qubit_connectivity.insert(2, vec![1, 3]);
946        topology.qubit_connectivity.insert(3, vec![2, 4]);
947        topology.qubit_connectivity.insert(4, vec![3]);
948
949        assert_eq!(topology.num_physical_qubits, 5);
950        assert_eq!(topology.qubit_connectivity.len(), 5);
951    }
952
953    #[test]
954    fn test_qubit_routing_optimization() {
955        let mut circuit = Circuit::<3>::new();
956        let _ = circuit.cnot(0, 1);
957        let _ = circuit.cnot(1, 2);
958
959        let mut hardware = HardwareTopology::default();
960        hardware.num_physical_qubits = 5;
961        hardware.qubit_connectivity.insert(0, vec![1]);
962        hardware.qubit_connectivity.insert(1, vec![0, 2]);
963        hardware.qubit_connectivity.insert(2, vec![1]);
964
965        let config = SciRS2TranspilerConfig {
966            enable_routing_opt: true,
967            ..Default::default()
968        };
969        let transpiler = SciRS2GraphTranspiler::new(config);
970
971        let mapping = transpiler
972            .optimize_qubit_routing(&circuit, &hardware)
973            .expect("Failed to optimize routing");
974
975        assert_eq!(mapping.len(), 3);
976    }
977
978    // ── New algorithm tests ────────────────────────────────────────────────
979
980    #[test]
981    fn test_undirected_graph_bfs() {
982        // Linear graph: 0-1-2-3
983        let mut g = UndirectedGraph::new(4);
984        g.add_edge(0, 1, 1.0);
985        g.add_edge(1, 2, 1.0);
986        g.add_edge(2, 3, 1.0);
987
988        let order = g.bfs(0);
989        assert_eq!(order, vec![0, 1, 2, 3]);
990    }
991
992    #[test]
993    fn test_undirected_graph_dfs() {
994        // Linear graph: 0-1-2-3
995        let mut g = UndirectedGraph::new(4);
996        g.add_edge(0, 1, 1.0);
997        g.add_edge(1, 2, 1.0);
998        g.add_edge(2, 3, 1.0);
999
1000        let order = g.dfs(0);
1001        assert_eq!(order, vec![0, 1, 2, 3]);
1002    }
1003
1004    #[test]
1005    fn test_dijkstra_linear_graph() {
1006        // 0 -1.0- 1 -2.0- 2 -1.0- 3
1007        let mut g = UndirectedGraph::new(4);
1008        g.add_edge(0, 1, 1.0);
1009        g.add_edge(1, 2, 2.0);
1010        g.add_edge(2, 3, 1.0);
1011
1012        let dists = g.dijkstra_distances(0);
1013        assert!((dists[&0] - 0.0).abs() < 1e-6);
1014        assert!((dists[&1] - 1.0).abs() < 1e-6);
1015        assert!((dists[&2] - 3.0).abs() < 1e-6);
1016        assert!((dists[&3] - 4.0).abs() < 1e-6);
1017    }
1018
1019    #[test]
1020    fn test_dijkstra_path() {
1021        let mut g = UndirectedGraph::new(5);
1022        g.add_edge(0, 1, 1.0);
1023        g.add_edge(1, 2, 1.0);
1024        g.add_edge(0, 3, 5.0);
1025        g.add_edge(3, 2, 1.0); // 0->3->2 costs 6, but 0->1->2 costs 2
1026
1027        let result = g.dijkstra_path(0, 2);
1028        assert!(result.is_some());
1029        let (dist, path) = result.expect("path should exist");
1030        assert!(
1031            (dist - 2.0).abs() < 1e-6,
1032            "expected distance 2.0, got {}",
1033            dist
1034        );
1035        assert_eq!(path, vec![0, 1, 2]);
1036    }
1037
1038    #[test]
1039    fn test_pattern_graph_construction() {
1040        let mut pattern = PatternGraph::new();
1041        let h = pattern.add_node("H");
1042        let cx = pattern.add_node("CNOT");
1043        pattern.add_edge(h, cx);
1044
1045        assert_eq!(pattern.num_nodes(), 2);
1046        assert!(pattern.has_edge(0, 1));
1047        assert!(!pattern.has_edge(1, 0));
1048    }
1049
1050    #[test]
1051    fn test_subgraph_isomorphism_simple() {
1052        // Pattern: H -> CNOT
1053        let mut pattern = PatternGraph::new();
1054        let ph = pattern.add_node("H");
1055        let pcx = pattern.add_node("CNOT");
1056        pattern.add_edge(ph, pcx);
1057
1058        // Target: H(q0) -> CNOT(q0,q1) -> H(q1)
1059        let target_labels = vec!["H".to_string(), "CNOT".to_string(), "H".to_string()];
1060        let mut target_edges: HashSet<(usize, usize)> = HashSet::new();
1061        target_edges.insert((0, 1)); // H -> CNOT
1062        target_edges.insert((1, 2)); // CNOT -> H
1063
1064        let iso = SubgraphIsomorphism::new(&pattern, &target_labels, &target_edges);
1065        let mappings = iso.find_all();
1066        // Pattern node 0 (H) -> target 0, pattern node 1 (CNOT) -> target 1
1067        assert!(!mappings.is_empty(), "Should find at least one isomorphism");
1068    }
1069
1070    #[test]
1071    fn test_find_circuit_pattern_matches() {
1072        let mut circuit = Circuit::<2>::new();
1073        let _ = circuit.h(0);
1074        let _ = circuit.cnot(0, 1);
1075        let _ = circuit.h(1);
1076
1077        // Build pattern: any gate -> CNOT
1078        let mut pattern = PatternGraph::new();
1079        let _p0 = pattern.add_node(""); // wildcard
1080        let p1 = pattern.add_node("CNOT");
1081        pattern.add_edge(0, 1);
1082
1083        let config = SciRS2TranspilerConfig::default();
1084        let transpiler = SciRS2GraphTranspiler::new(config);
1085
1086        let matches = transpiler
1087            .find_circuit_pattern_matches(&circuit, &pattern)
1088            .expect("Pattern match should succeed");
1089
1090        assert!(
1091            !matches.is_empty(),
1092            "Should find at least one pattern occurrence"
1093        );
1094    }
1095}