quantrs2_device/
routing.rs

1//! Qubit Routing Algorithms for Quantum Hardware
2//!
3//! This module implements advanced qubit routing algorithms using SciRS2
4//! optimization techniques to map logical qubits to physical qubits.
5
6use crate::topology::HardwareTopology;
7use crate::{DeviceError, DeviceResult};
8use petgraph::algo::astar;
9use petgraph::graph::{NodeIndex, UnGraph};
10use quantrs2_circuit::prelude::*;
11use std::collections::{HashMap, HashSet};
12
13/// Routing strategy for qubit mapping
14#[derive(Debug, Clone, Copy)]
15pub enum RoutingStrategy {
16    /// Basic nearest-neighbor mapping
17    NearestNeighbor,
18    /// Steiner tree based routing
19    SteinerTree,
20    /// Lookahead-based routing
21    Lookahead { depth: usize },
22    /// Stochastic routing with simulated annealing
23    StochasticAnnealing,
24}
25
26/// Result of qubit routing
27#[derive(Debug, Clone)]
28pub struct RoutingResult {
29    /// Initial qubit mapping (logical -> physical)
30    pub initial_mapping: HashMap<usize, usize>,
31    /// Final qubit mapping after all swaps
32    pub final_mapping: HashMap<usize, usize>,
33    /// List of SWAP gates to insert
34    pub swap_gates: Vec<SwapGate>,
35    /// Total routing cost (number of SWAPs)
36    pub cost: usize,
37    /// Circuit depth increase
38    pub depth_overhead: usize,
39}
40
41/// SWAP gate information
42#[derive(Debug, Clone)]
43pub struct SwapGate {
44    /// Physical qubit indices
45    pub qubit1: usize,
46    pub qubit2: usize,
47    /// Position in circuit where SWAP should be inserted
48    pub position: usize,
49}
50
51/// Qubit router using SciRS2 optimization
52pub struct QubitRouter {
53    /// Hardware topology
54    topology: HardwareTopology,
55    /// Routing strategy
56    strategy: RoutingStrategy,
57    /// Random seed for stochastic methods
58    seed: u64,
59}
60
61impl QubitRouter {
62    /// Create a new qubit router
63    pub const fn new(topology: HardwareTopology, strategy: RoutingStrategy) -> Self {
64        Self {
65            topology,
66            strategy,
67            seed: 42,
68        }
69    }
70
71    /// Route a quantum circuit to hardware topology
72    pub fn route_circuit<const N: usize>(
73        &self,
74        circuit: &Circuit<N>,
75    ) -> DeviceResult<RoutingResult> {
76        // Extract two-qubit gate interactions
77        let interactions = self.extract_interactions(circuit)?;
78
79        // Find initial mapping
80        let initial_mapping = self.find_initial_mapping(&interactions, N)?;
81
82        // Route based on strategy
83        match self.strategy {
84            RoutingStrategy::NearestNeighbor => {
85                self.route_nearest_neighbor(circuit, initial_mapping, interactions)
86            }
87            RoutingStrategy::SteinerTree => {
88                self.route_steiner_tree(circuit, initial_mapping, interactions)
89            }
90            RoutingStrategy::Lookahead { depth } => {
91                self.route_lookahead(circuit, initial_mapping, interactions, depth)
92            }
93            RoutingStrategy::StochasticAnnealing => {
94                self.route_simulated_annealing(circuit, initial_mapping, interactions)
95            }
96        }
97    }
98
99    /// Extract two-qubit interactions from circuit
100    fn extract_interactions<const N: usize>(
101        &self,
102        circuit: &Circuit<N>,
103    ) -> DeviceResult<Vec<(usize, usize)>> {
104        let mut interactions = Vec::new();
105
106        for gate in circuit.gates() {
107            if gate.qubits().len() == 2 {
108                let q0 = gate.qubits()[0].id() as usize;
109                let q1 = gate.qubits()[1].id() as usize;
110                interactions.push((q0, q1));
111            }
112        }
113
114        Ok(interactions)
115    }
116
117    /// Find initial qubit mapping using graph algorithms
118    fn find_initial_mapping(
119        &self,
120        interactions: &[(usize, usize)],
121        num_logical_qubits: usize,
122    ) -> DeviceResult<HashMap<usize, usize>> {
123        if num_logical_qubits > self.topology.num_qubits {
124            return Err(DeviceError::InsufficientQubits {
125                required: num_logical_qubits,
126                available: self.topology.num_qubits,
127            });
128        }
129
130        // Build interaction graph
131        let mut interaction_graph = UnGraph::<(), ()>::new_undirected();
132        let mut nodes = HashMap::new();
133
134        for i in 0..num_logical_qubits {
135            let node = interaction_graph.add_node(());
136            nodes.insert(i, node);
137        }
138
139        for &(q0, q1) in interactions {
140            if let (Some(&n0), Some(&n1)) = (nodes.get(&q0), nodes.get(&q1)) {
141                interaction_graph.add_edge(n0, n1, ());
142            }
143        }
144
145        // Use spectral graph partitioning for initial mapping
146        // For now, use a simple heuristic
147        let mut mapping = HashMap::new();
148        let _available_physical: Vec<usize> = (0..self.topology.num_qubits).collect();
149
150        // Map most connected logical qubits to most connected physical qubits
151        let mut logical_degrees: Vec<(usize, usize)> = nodes
152            .iter()
153            .map(|(&log_q, &node)| {
154                let degree = interaction_graph.edges(node).count();
155                (log_q, degree)
156            })
157            .collect();
158        logical_degrees.sort_by_key(|&(_, deg)| std::cmp::Reverse(deg));
159
160        let mut physical_degrees: Vec<(usize, usize)> = (0..self.topology.num_qubits)
161            .map(|p| {
162                let node = NodeIndex::new(p);
163                let degree = self.topology.connectivity.edges(node).count();
164                (p, degree)
165            })
166            .collect();
167        physical_degrees.sort_by_key(|&(_, deg)| std::cmp::Reverse(deg));
168
169        for (i, &(log_q, _)) in logical_degrees.iter().enumerate() {
170            if i < physical_degrees.len() {
171                mapping.insert(log_q, physical_degrees[i].0);
172            }
173        }
174
175        Ok(mapping)
176    }
177
178    /// Nearest-neighbor routing algorithm
179    fn route_nearest_neighbor<const N: usize>(
180        &self,
181        _circuit: &Circuit<N>,
182        initial_mapping: HashMap<usize, usize>,
183        interactions: Vec<(usize, usize)>,
184    ) -> DeviceResult<RoutingResult> {
185        let mut current_mapping = initial_mapping.clone();
186        let mut swap_gates = Vec::new();
187        let mut position = 0;
188
189        for (log_q0, log_q1) in interactions {
190            let phys_q0 = current_mapping[&log_q0];
191            let phys_q1 = current_mapping[&log_q1];
192
193            // Check if qubits are connected
194            if !self.are_connected(phys_q0, phys_q1)? {
195                // Find shortest path and insert SWAPs
196                let path = self.find_shortest_path(phys_q0, phys_q1)?;
197
198                // Insert SWAPs along the path
199                for i in 0..path.len() - 2 {
200                    swap_gates.push(SwapGate {
201                        qubit1: path[i],
202                        qubit2: path[i + 1],
203                        position,
204                    });
205
206                    // Update mapping
207                    self.apply_swap(&mut current_mapping, path[i], path[i + 1]);
208                }
209            }
210
211            position += 1;
212        }
213
214        Ok(RoutingResult {
215            initial_mapping,
216            final_mapping: current_mapping,
217            cost: swap_gates.len(),
218            depth_overhead: swap_gates.len() * 3, // Each SWAP is ~3 CNOTs
219            swap_gates,
220        })
221    }
222
223    /// Steiner tree based routing
224    fn route_steiner_tree<const N: usize>(
225        &self,
226        _circuit: &Circuit<N>,
227        initial_mapping: HashMap<usize, usize>,
228        interactions: Vec<(usize, usize)>,
229    ) -> DeviceResult<RoutingResult> {
230        // Implement Steiner tree approximation for multi-qubit gates
231        // For now, fallback to nearest neighbor
232        self.route_nearest_neighbor(_circuit, initial_mapping, interactions)
233    }
234
235    /// Lookahead routing with configurable depth
236    fn route_lookahead<const N: usize>(
237        &self,
238        circuit: &Circuit<N>,
239        initial_mapping: HashMap<usize, usize>,
240        interactions: Vec<(usize, usize)>,
241        lookahead_depth: usize,
242    ) -> DeviceResult<RoutingResult> {
243        let mut current_mapping = initial_mapping.clone();
244        let mut swap_gates = Vec::new();
245        let mut position = 0;
246
247        for i in 0..interactions.len() {
248            let (log_q0, log_q1) = interactions[i];
249            let phys_q0 = current_mapping[&log_q0];
250            let phys_q1 = current_mapping[&log_q1];
251
252            if !self.are_connected(phys_q0, phys_q1)? {
253                // Look ahead at future gates
254                let future_gates =
255                    &interactions[i..std::cmp::min(i + lookahead_depth, interactions.len())];
256
257                // Find best SWAP considering future gates
258                let best_swap = self.find_best_swap_lookahead(
259                    &current_mapping,
260                    phys_q0,
261                    phys_q1,
262                    future_gates,
263                )?;
264
265                if let Some((swap_q0, swap_q1)) = best_swap {
266                    swap_gates.push(SwapGate {
267                        qubit1: swap_q0,
268                        qubit2: swap_q1,
269                        position,
270                    });
271
272                    self.apply_swap(&mut current_mapping, swap_q0, swap_q1);
273                }
274            }
275
276            position += 1;
277        }
278
279        Ok(RoutingResult {
280            initial_mapping,
281            final_mapping: current_mapping,
282            cost: swap_gates.len(),
283            depth_overhead: swap_gates.len() * 3,
284            swap_gates,
285        })
286    }
287
288    /// Simulated annealing based routing
289    fn route_simulated_annealing<const N: usize>(
290        &self,
291        circuit: &Circuit<N>,
292        initial_mapping: HashMap<usize, usize>,
293        interactions: Vec<(usize, usize)>,
294    ) -> DeviceResult<RoutingResult> {
295        use fastrand::Rng;
296        let mut rng = Rng::with_seed(self.seed);
297
298        let mut best_mapping = initial_mapping;
299        let mut best_cost = self.evaluate_mapping(&best_mapping, &interactions)?;
300
301        let mut current_mapping = best_mapping.clone();
302        let mut current_cost = best_cost;
303
304        // Annealing parameters
305        let mut temperature = 100.0;
306        let cooling_rate = 0.95;
307        let min_temperature = 0.01;
308
309        while temperature > min_temperature {
310            // Generate neighbor by random swap
311            let mut neighbor_mapping = current_mapping.clone();
312            let logical_qubits: Vec<usize> = neighbor_mapping.keys().copied().collect();
313
314            if logical_qubits.len() >= 2 {
315                let idx1 = rng.usize(0..logical_qubits.len());
316                let idx2 = rng.usize(0..logical_qubits.len());
317
318                if idx1 != idx2 {
319                    let log_q1 = logical_qubits[idx1];
320                    let log_q2 = logical_qubits[idx2];
321
322                    let phys_q1 = neighbor_mapping[&log_q1];
323                    let phys_q2 = neighbor_mapping[&log_q2];
324
325                    neighbor_mapping.insert(log_q1, phys_q2);
326                    neighbor_mapping.insert(log_q2, phys_q1);
327                }
328            }
329
330            let neighbor_cost = self.evaluate_mapping(&neighbor_mapping, &interactions)?;
331            let delta = neighbor_cost as f64 - current_cost as f64;
332
333            // Accept or reject
334            if delta < 0.0 || rng.f64() < (-delta / temperature).exp() {
335                current_mapping = neighbor_mapping;
336                current_cost = neighbor_cost;
337
338                if current_cost < best_cost {
339                    best_mapping.clone_from(&current_mapping);
340                    best_cost = current_cost;
341                }
342            }
343
344            temperature *= cooling_rate;
345        }
346
347        // Now route with the best mapping found
348        self.route_nearest_neighbor(circuit, best_mapping, interactions)
349    }
350
351    /// Check if two physical qubits are connected
352    fn are_connected(&self, phys_q0: usize, phys_q1: usize) -> DeviceResult<bool> {
353        let n0 = NodeIndex::new(phys_q0);
354        let n1 = NodeIndex::new(phys_q1);
355        Ok(self.topology.connectivity.find_edge(n0, n1).is_some())
356    }
357
358    /// Find shortest path between two physical qubits
359    fn find_shortest_path(&self, start: usize, end: usize) -> DeviceResult<Vec<usize>> {
360        let start_node = NodeIndex::new(start);
361        let end_node = NodeIndex::new(end);
362
363        let result = astar(
364            &self.topology.connectivity,
365            start_node,
366            |n| n == end_node,
367            |e| *e.weight(),
368            |_| 0.0,
369        );
370
371        match result {
372            Some((_, path)) => Ok(path.into_iter().map(|n| n.index()).collect()),
373            None => Err(DeviceError::RoutingError(format!(
374                "No path found between qubits {start} and {end}"
375            ))),
376        }
377    }
378
379    /// Apply a SWAP to the mapping
380    fn apply_swap(&self, mapping: &mut HashMap<usize, usize>, phys_q0: usize, phys_q1: usize) {
381        // Find logical qubits mapped to these physical qubits
382        let mut log_q0 = None;
383        let mut log_q1 = None;
384
385        for (&log_q, &phys_q) in mapping.iter() {
386            if phys_q == phys_q0 {
387                log_q0 = Some(log_q);
388            } else if phys_q == phys_q1 {
389                log_q1 = Some(log_q);
390            }
391        }
392
393        // Swap the mappings
394        if let (Some(l0), Some(l1)) = (log_q0, log_q1) {
395            mapping.insert(l0, phys_q1);
396            mapping.insert(l1, phys_q0);
397        }
398    }
399
400    /// Find best swap considering future gates
401    fn find_best_swap_lookahead(
402        &self,
403        mapping: &HashMap<usize, usize>,
404        target_phys_q0: usize,
405        target_phys_q1: usize,
406        future_gates: &[(usize, usize)],
407    ) -> DeviceResult<Option<(usize, usize)>> {
408        let mut best_swap = None;
409        let mut best_score = f64::MAX;
410
411        // Try all possible swaps
412        for edge in self.topology.connectivity.edge_indices() {
413            if let Some((n0, n1)) = self.topology.connectivity.edge_endpoints(edge) {
414                let phys_q0 = n0.index();
415                let phys_q1 = n1.index();
416
417                // Simulate this swap
418                let mut test_mapping = mapping.clone();
419                self.apply_swap(&mut test_mapping, phys_q0, phys_q1);
420
421                // Evaluate the swap
422                let score = self.evaluate_swap_lookahead(
423                    &test_mapping,
424                    target_phys_q0,
425                    target_phys_q1,
426                    future_gates,
427                )?;
428
429                if score < best_score {
430                    best_score = score;
431                    best_swap = Some((phys_q0, phys_q1));
432                }
433            }
434        }
435
436        Ok(best_swap)
437    }
438
439    /// Evaluate a swap considering future gates
440    fn evaluate_swap_lookahead(
441        &self,
442        mapping: &HashMap<usize, usize>,
443        target_phys_q0: usize,
444        target_phys_q1: usize,
445        future_gates: &[(usize, usize)],
446    ) -> DeviceResult<f64> {
447        let mut score = 0.0;
448
449        // Check if target gate is now executable
450        if self.are_connected(target_phys_q0, target_phys_q1)? {
451            score -= 10.0; // Bonus for making target executable
452        }
453
454        // Evaluate future gates
455        for (i, &(log_q0, log_q1)) in future_gates.iter().enumerate() {
456            if let (Some(&phys_q0), Some(&phys_q1)) = (mapping.get(&log_q0), mapping.get(&log_q1)) {
457                if self.are_connected(phys_q0, phys_q1)? {
458                    score -= 1.0 / (i + 1) as f64; // Decreasing weight for future gates
459                } else {
460                    let path = self.find_shortest_path(phys_q0, phys_q1)?;
461                    score += path.len() as f64 / (i + 1) as f64;
462                }
463            }
464        }
465
466        Ok(score)
467    }
468
469    /// Evaluate total cost of a mapping
470    fn evaluate_mapping(
471        &self,
472        mapping: &HashMap<usize, usize>,
473        interactions: &[(usize, usize)],
474    ) -> DeviceResult<usize> {
475        let mut total_cost = 0;
476
477        for &(log_q0, log_q1) in interactions {
478            if let (Some(&phys_q0), Some(&phys_q1)) = (mapping.get(&log_q0), mapping.get(&log_q1)) {
479                if !self.are_connected(phys_q0, phys_q1)? {
480                    let path = self.find_shortest_path(phys_q0, phys_q1)?;
481                    total_cost += path.len() - 1; // Number of swaps needed
482                }
483            }
484        }
485
486        Ok(total_cost)
487    }
488}
489
490/// Layout synthesis for initial qubit placement
491pub struct LayoutSynthesis {
492    /// Hardware topology
493    topology: HardwareTopology,
494}
495
496impl LayoutSynthesis {
497    /// Create a new layout synthesizer
498    pub const fn new(topology: HardwareTopology) -> Self {
499        Self { topology }
500    }
501
502    /// Synthesize optimal initial layout using SciRS2 optimization
503    pub fn synthesize_layout(
504        &self,
505        interaction_graph: &UnGraph<(), f64>,
506    ) -> DeviceResult<HashMap<usize, usize>> {
507        // Use spectral placement algorithm
508        // For now, use degree-based heuristic
509        let mut mapping = HashMap::new();
510
511        // Sort logical qubits by degree
512        let mut logical_degrees: Vec<(usize, usize)> = interaction_graph
513            .node_indices()
514            .map(|n| {
515                let degree = interaction_graph.edges(n).count();
516                (n.index(), degree)
517            })
518            .collect();
519        logical_degrees.sort_by_key(|&(_, deg)| std::cmp::Reverse(deg));
520
521        // Sort physical qubits by connectivity
522        let mut physical_degrees: Vec<(usize, usize)> = (0..self.topology.num_qubits)
523            .map(|p| {
524                let node = NodeIndex::new(p);
525                let degree = self.topology.connectivity.edges(node).count();
526                (p, degree)
527            })
528            .collect();
529        physical_degrees.sort_by_key(|&(_, deg)| std::cmp::Reverse(deg));
530
531        // Map high-degree logical to high-degree physical
532        for (i, &(log_q, _)) in logical_degrees.iter().enumerate() {
533            if i < physical_degrees.len() {
534                mapping.insert(log_q, physical_degrees[i].0);
535            }
536        }
537
538        Ok(mapping)
539    }
540}
541
542#[cfg(test)]
543mod tests {
544    use super::*;
545    use quantrs2_core::prelude::QubitId;
546
547    #[test]
548    fn test_qubit_router_creation() {
549        let topology = HardwareTopology::linear_topology(5);
550        let router = QubitRouter::new(topology, RoutingStrategy::NearestNeighbor);
551        assert_eq!(router.topology.num_qubits, 5);
552    }
553
554    #[test]
555    fn test_routing_strategies() {
556        let topology = HardwareTopology::grid_topology(3, 3);
557
558        // Test different strategies
559        let strategies = vec![
560            RoutingStrategy::NearestNeighbor,
561            RoutingStrategy::SteinerTree,
562            RoutingStrategy::Lookahead { depth: 3 },
563            RoutingStrategy::StochasticAnnealing,
564        ];
565
566        for strategy in strategies {
567            let router = QubitRouter::new(topology.clone(), strategy);
568            assert_eq!(router.topology.num_qubits, 9);
569        }
570    }
571
572    #[test]
573    fn test_swap_application() {
574        let topology = HardwareTopology::linear_topology(4);
575        let router = QubitRouter::new(topology, RoutingStrategy::NearestNeighbor);
576
577        let mut mapping = HashMap::from([(0, 0), (1, 1), (2, 2), (3, 3)]);
578
579        router.apply_swap(&mut mapping, 1, 2);
580
581        assert_eq!(mapping[&1], 2);
582        assert_eq!(mapping[&2], 1);
583    }
584
585    #[test]
586    fn test_linear_topology_routing() {
587        // Create a linear topology: 0-1-2-3-4
588        let topology = HardwareTopology::linear_topology(5);
589        let router = QubitRouter::new(topology, RoutingStrategy::NearestNeighbor);
590
591        // Create a circuit that requires routing
592        let mut circuit = Circuit::<5>::new();
593        circuit.h(QubitId::new(0)).expect("Failed to add H gate");
594        circuit
595            .cnot(QubitId::new(0), QubitId::new(4))
596            .expect("Failed to add CNOT gate"); // This requires routing
597        circuit
598            .cnot(QubitId::new(1), QubitId::new(3))
599            .expect("Failed to add CNOT gate"); // This also requires routing
600
601        let result = router
602            .route_circuit(&circuit)
603            .expect("Failed to route circuit");
604
605        // Check that we have a valid result
606        // The initial mapping might be optimal, so swaps might not be needed
607        // Note: cost is usize, so it's always >= 0
608
609        // Verify initial and final mappings exist
610        assert_eq!(result.initial_mapping.len(), 5);
611        assert_eq!(result.final_mapping.len(), 5);
612    }
613
614    #[test]
615    fn test_grid_topology_routing() {
616        // Create a 3x3 grid topology
617        let topology = HardwareTopology::grid_topology(3, 3);
618        let router = QubitRouter::new(topology, RoutingStrategy::Lookahead { depth: 3 });
619
620        // Create a circuit with non-local interactions
621        let mut circuit = Circuit::<9>::new();
622        circuit.h(QubitId::new(0)).expect("Failed to add H gate");
623        circuit
624            .cnot(QubitId::new(0), QubitId::new(8))
625            .expect("Failed to add CNOT gate"); // Corner to corner
626        circuit
627            .cnot(QubitId::new(4), QubitId::new(2))
628            .expect("Failed to add CNOT gate"); // Center to edge
629
630        let result = router
631            .route_circuit(&circuit)
632            .expect("Failed to route circuit");
633
634        // Should require swaps for non-adjacent qubits
635        assert!(result.swap_gates.len() > 0);
636    }
637
638    #[test]
639    fn test_heavy_hex_routing() {
640        // Test on IBM's heavy-hex topology
641        let topology = HardwareTopology::from_heavy_hex(27);
642        let router = QubitRouter::new(topology, RoutingStrategy::StochasticAnnealing);
643
644        let mut circuit = Circuit::<10>::new();
645        // Create a random circuit
646        circuit.h(QubitId::new(0)).expect("Failed to add H gate");
647        circuit
648            .cnot(QubitId::new(0), QubitId::new(5))
649            .expect("Failed to add CNOT gate");
650        circuit
651            .cnot(QubitId::new(2), QubitId::new(7))
652            .expect("Failed to add CNOT gate");
653        circuit
654            .cnot(QubitId::new(3), QubitId::new(9))
655            .expect("Failed to add CNOT gate");
656
657        let result = router
658            .route_circuit(&circuit)
659            .expect("Failed to route circuit");
660
661        // Should successfully route
662        assert!(result.initial_mapping.len() <= 27);
663    }
664
665    #[test]
666    fn test_insufficient_qubits() {
667        let topology = HardwareTopology::linear_topology(3);
668        let router = QubitRouter::new(topology, RoutingStrategy::NearestNeighbor);
669
670        // Try to route a circuit with more logical qubits than physical
671        let circuit = Circuit::<5>::new();
672
673        let result = router.route_circuit(&circuit);
674        assert!(result.is_err());
675    }
676
677    #[test]
678    fn test_different_strategies_comparison() {
679        let topology = HardwareTopology::grid_topology(4, 4);
680
681        // Create a challenging circuit
682        let mut circuit = Circuit::<16>::new();
683        for i in 0..8 {
684            circuit
685                .h(QubitId::new(i as u32))
686                .expect("Failed to add H gate in loop");
687            circuit
688                .cnot(QubitId::new(i as u32), QubitId::new((i + 8) as u32))
689                .expect("Failed to add CNOT gate in loop");
690        }
691
692        // Test all strategies
693        let strategies = vec![
694            RoutingStrategy::NearestNeighbor,
695            RoutingStrategy::Lookahead { depth: 3 },
696            RoutingStrategy::Lookahead { depth: 5 },
697            RoutingStrategy::StochasticAnnealing,
698        ];
699
700        let mut results = Vec::new();
701        for strategy in strategies {
702            let router = QubitRouter::new(topology.clone(), strategy);
703            let result = router
704                .route_circuit(&circuit)
705                .expect("Failed to route with strategy");
706            results.push((strategy, result.cost));
707        }
708
709        // All strategies should produce valid results
710        // Note: cost is usize, so it's always >= 0
711    }
712
713    #[test]
714    fn test_already_connected_qubits() {
715        let topology = HardwareTopology::linear_topology(3);
716        let router = QubitRouter::new(topology, RoutingStrategy::NearestNeighbor);
717
718        // Circuit with already connected qubits
719        let mut circuit = Circuit::<3>::new();
720        circuit
721            .cnot(QubitId::new(0), QubitId::new(1))
722            .expect("Failed to add CNOT gate"); // Adjacent
723        circuit
724            .cnot(QubitId::new(1), QubitId::new(2))
725            .expect("Failed to add CNOT gate"); // Adjacent
726
727        let result = router
728            .route_circuit(&circuit)
729            .expect("Failed to route circuit");
730
731        // Should require no swaps
732        assert_eq!(result.swap_gates.len(), 0);
733        assert_eq!(result.cost, 0);
734    }
735
736    #[test]
737    fn test_layout_synthesis() {
738        let topology = HardwareTopology::grid_topology(3, 3);
739        let synthesizer = LayoutSynthesis::new(topology);
740
741        // Create interaction graph
742        let mut graph = UnGraph::<(), f64>::new_undirected();
743        let n0 = graph.add_node(());
744        let n1 = graph.add_node(());
745        let n2 = graph.add_node(());
746        let n3 = graph.add_node(());
747
748        graph.add_edge(n0, n1, 1.0);
749        graph.add_edge(n1, n2, 1.0);
750        graph.add_edge(n2, n3, 1.0);
751        graph.add_edge(n3, n0, 1.0); // Square
752
753        let layout = synthesizer
754            .synthesize_layout(&graph)
755            .expect("Failed to synthesize layout");
756
757        // Should map all 4 qubits
758        assert_eq!(layout.len(), 4);
759
760        // All mappings should be to different physical qubits
761        let physical_qubits: HashSet<usize> = layout.values().copied().collect();
762        assert_eq!(physical_qubits.len(), 4);
763    }
764
765    #[test]
766    fn test_path_finding() {
767        let topology = HardwareTopology::linear_topology(5);
768        let router = QubitRouter::new(topology, RoutingStrategy::NearestNeighbor);
769
770        // Test path from 0 to 4
771        let path = router
772            .find_shortest_path(0, 4)
773            .expect("Failed to find path 0->4");
774        assert_eq!(path, vec![0, 1, 2, 3, 4]);
775
776        // Test path from 2 to 0
777        let path = router
778            .find_shortest_path(2, 0)
779            .expect("Failed to find path 2->0");
780        assert_eq!(path, vec![2, 1, 0]);
781    }
782
783    #[test]
784    fn test_connectivity_check() {
785        let topology = HardwareTopology::grid_topology(3, 3);
786        let router = QubitRouter::new(topology, RoutingStrategy::NearestNeighbor);
787
788        // Adjacent qubits in grid should be connected
789        assert!(router
790            .are_connected(0, 1)
791            .expect("Connectivity check failed")); // Horizontal
792        assert!(router
793            .are_connected(0, 3)
794            .expect("Connectivity check failed")); // Vertical
795
796        // Diagonal qubits should not be connected
797        assert!(!router
798            .are_connected(0, 4)
799            .expect("Connectivity check failed"));
800
801        // Far qubits should not be connected
802        assert!(!router
803            .are_connected(0, 8)
804            .expect("Connectivity check failed"));
805    }
806}