quantrs2_device/
topology.rs

1//! Hardware topology analysis using SciRS2 graph algorithms
2//!
3//! This module provides tools for analyzing quantum hardware topologies,
4//! finding optimal qubit mappings, and identifying connectivity constraints.
5
6use petgraph::algo::{connected_components, dijkstra, min_spanning_tree};
7use petgraph::data::FromElements;
8use petgraph::graph::{NodeIndex, UnGraph};
9use std::collections::{HashMap, HashSet};
10use thiserror::Error;
11
12/// Error type for topology operations
13#[derive(Error, Debug)]
14pub enum TopologyError {
15    #[error("Unsupported topology: {0}")]
16    UnsupportedTopology(String),
17
18    #[error("Invalid configuration: {0}")]
19    InvalidConfiguration(String),
20}
21
22/// Represents a quantum hardware topology
23#[derive(Debug, Clone)]
24pub struct HardwareTopology {
25    /// Number of physical qubits
26    pub num_qubits: usize,
27    /// Connectivity graph (undirected for most hardware)
28    pub connectivity: UnGraph<u32, f64>,
29    /// Qubit properties (T1, T2, gate errors, etc.)
30    pub qubit_properties: Vec<QubitProperties>,
31    /// Two-qubit gate properties (gate errors, gate times)
32    pub gate_properties: HashMap<(u32, u32), GateProperties>,
33}
34
35/// Properties of a physical qubit
36#[derive(Debug, Clone)]
37pub struct QubitProperties {
38    /// Qubit index
39    pub id: u32,
40    /// Qubit index (same as id, for compatibility)
41    pub index: u32,
42    /// T1 coherence time (microseconds)
43    pub t1: f64,
44    /// T2 coherence time (microseconds)
45    pub t2: f64,
46    /// Single-qubit gate error rate
47    pub single_qubit_gate_error: f64,
48    /// Single-qubit gate error rate (same as above, for compatibility)
49    pub gate_error_1q: f64,
50    /// Readout error rate
51    pub readout_error: f64,
52    /// Frequency (GHz)
53    pub frequency: f64,
54}
55
56/// Properties of a two-qubit gate between qubits
57#[derive(Debug, Clone)]
58pub struct GateProperties {
59    /// Gate error rate
60    pub error_rate: f64,
61    /// Gate duration (nanoseconds)
62    pub duration: f64,
63    /// Gate type (e.g., "CZ", "CNOT")
64    pub gate_type: String,
65}
66
67impl HardwareTopology {
68    /// Create a new hardware topology
69    pub fn new(num_qubits: usize) -> Self {
70        Self {
71            num_qubits,
72            connectivity: UnGraph::new_undirected(),
73            qubit_properties: Vec::new(),
74            gate_properties: HashMap::new(),
75        }
76    }
77
78    /// Add a qubit with properties
79    pub fn add_qubit(&mut self, properties: QubitProperties) -> NodeIndex {
80        let idx = self.connectivity.add_node(properties.index);
81        self.qubit_properties.push(properties);
82        idx
83    }
84
85    /// Add a connection between two qubits
86    pub fn add_connection(&mut self, q1: u32, q2: u32, properties: GateProperties) {
87        // Find node indices
88        let n1 = self
89            .connectivity
90            .node_indices()
91            .find(|&n| self.connectivity[n] == q1);
92        let n2 = self
93            .connectivity
94            .node_indices()
95            .find(|&n| self.connectivity[n] == q2);
96
97        if let (Some(n1), Some(n2)) = (n1, n2) {
98            // Use error rate as edge weight (lower is better)
99            self.connectivity.add_edge(n1, n2, properties.error_rate);
100            self.gate_properties.insert((q1, q2), properties.clone());
101            self.gate_properties.insert((q2, q1), properties);
102        }
103    }
104
105    /// Load standard hardware topologies
106    pub fn load_standard(topology_type: &str) -> Result<Self, TopologyError> {
107        match topology_type {
108            "linear" => Ok(Self::linear_topology(5)),
109            "grid_2x3" => Ok(Self::grid_topology(2, 3)),
110            "ibm_5q" => Ok(Self::ibm_topology()),
111            "google_sycamore" => Ok(Self::create_sycamore_subset()),
112            _ => Err(TopologyError::UnsupportedTopology(format!(
113                "Unknown topology: {}",
114                topology_type
115            ))),
116        }
117    }
118
119    /// Create a linear topology (chain of qubits)
120    pub fn linear_topology(n: usize) -> Self {
121        let mut topo = Self::new(n);
122
123        // Add qubits
124        for i in 0..n {
125            topo.add_qubit(QubitProperties {
126                id: i as u32,
127                index: i as u32,
128                t1: 50.0,
129                t2: 70.0,
130                single_qubit_gate_error: 0.001,
131                gate_error_1q: 0.001,
132                readout_error: 0.01,
133                frequency: 5.0 + 0.1 * i as f64,
134            });
135        }
136
137        // Add connections
138        for i in 0..n - 1 {
139            topo.add_connection(
140                i as u32,
141                (i + 1) as u32,
142                GateProperties {
143                    error_rate: 0.01,
144                    duration: 200.0,
145                    gate_type: "CZ".to_string(),
146                },
147            );
148        }
149
150        topo
151    }
152
153    /// Create a 2D grid topology
154    pub fn grid_topology(rows: usize, cols: usize) -> Self {
155        let n = rows * cols;
156        let mut topo = Self::new(n);
157
158        // Add qubits
159        for i in 0..n {
160            topo.add_qubit(QubitProperties {
161                id: i as u32,
162                index: i as u32,
163                t1: 45.0 + 5.0 * fastrand::f64(),
164                t2: 60.0 + 10.0 * fastrand::f64(),
165                single_qubit_gate_error: 0.001 * (1.0 + 0.2 * fastrand::f64()),
166                gate_error_1q: 0.001 * (1.0 + 0.2 * fastrand::f64()),
167                readout_error: 0.01 * (1.0 + 0.3 * fastrand::f64()),
168                frequency: 5.0 + 0.2 * fastrand::f64(),
169            });
170        }
171
172        // Add horizontal connections
173        for r in 0..rows {
174            for c in 0..cols - 1 {
175                let q1 = (r * cols + c) as u32;
176                let q2 = (r * cols + c + 1) as u32;
177                topo.add_connection(
178                    q1,
179                    q2,
180                    GateProperties {
181                        error_rate: 0.01 * (1.0 + 0.2 * fastrand::f64()),
182                        duration: 180.0 + 40.0 * fastrand::f64(),
183                        gate_type: "CZ".to_string(),
184                    },
185                );
186            }
187        }
188
189        // Add vertical connections
190        for r in 0..rows - 1 {
191            for c in 0..cols {
192                let q1 = (r * cols + c) as u32;
193                let q2 = ((r + 1) * cols + c) as u32;
194                topo.add_connection(
195                    q1,
196                    q2,
197                    GateProperties {
198                        error_rate: 0.01 * (1.0 + 0.2 * fastrand::f64()),
199                        duration: 180.0 + 40.0 * fastrand::f64(),
200                        gate_type: "CZ".to_string(),
201                    },
202                );
203            }
204        }
205
206        topo
207    }
208
209    /// Create IBM 5-qubit topology (bow-tie shape)
210    pub fn ibm_topology() -> Self {
211        let mut topo = Self::new(5);
212
213        // Add qubits with realistic properties
214        let qubit_props = vec![
215            (0, 52.3, 71.2, 0.0008, 0.015),
216            (1, 48.7, 68.5, 0.0011, 0.018),
217            (2, 55.1, 73.8, 0.0009, 0.012),
218            (3, 51.4, 69.9, 0.0010, 0.016),
219            (4, 49.8, 67.3, 0.0012, 0.020),
220        ];
221
222        for (i, (idx, t1, t2, err_1q, err_ro)) in qubit_props.into_iter().enumerate() {
223            topo.add_qubit(QubitProperties {
224                id: idx,
225                index: idx,
226                t1,
227                t2,
228                single_qubit_gate_error: err_1q,
229                gate_error_1q: err_1q,
230                readout_error: err_ro,
231                frequency: 5.0 + 0.05 * i as f64,
232            });
233        }
234
235        // Add connections (bow-tie pattern)
236        let connections = vec![(0, 1, 0.008), (1, 2, 0.012), (1, 3, 0.010), (3, 4, 0.009)];
237
238        for (q1, q2, err) in connections {
239            topo.add_connection(
240                q1,
241                q2,
242                GateProperties {
243                    error_rate: err,
244                    duration: 200.0 + 20.0 * fastrand::f64(),
245                    gate_type: "CNOT".to_string(),
246                },
247            );
248        }
249
250        topo
251    }
252
253    /// Create Google Sycamore-like topology (subset)
254    pub fn google_topology() -> Self {
255        // Create a 6-qubit subset of Sycamore topology
256        let mut topo = Self::new(6);
257
258        // Add qubits
259        for i in 0..6 {
260            topo.add_qubit(QubitProperties {
261                id: i as u32,
262                index: i as u32,
263                t1: 15.0 + 2.0 * fastrand::f64(),
264                t2: 20.0 + 3.0 * fastrand::f64(),
265                single_qubit_gate_error: 0.0015 * (1.0 + 0.1 * fastrand::f64()),
266                gate_error_1q: 0.0015 * (1.0 + 0.1 * fastrand::f64()),
267                readout_error: 0.008 * (1.0 + 0.2 * fastrand::f64()),
268                frequency: 5.5 + 0.1 * fastrand::f64(),
269            });
270        }
271
272        // Add connections in a 2x3 grid pattern with diagonals
273        let connections = vec![
274            (0, 1),
275            (1, 2), // Top row
276            (3, 4),
277            (4, 5), // Bottom row
278            (0, 3),
279            (1, 4),
280            (2, 5), // Vertical
281            (0, 4),
282            (1, 5), // Diagonals
283        ];
284
285        for (q1, q2) in connections {
286            topo.add_connection(
287                q1,
288                q2,
289                GateProperties {
290                    error_rate: 0.006 * (1.0 + 0.15 * fastrand::f64()),
291                    duration: 25.0 + 5.0 * fastrand::f64(),
292                    gate_type: "sqrt_ISWAP".to_string(),
293                },
294            );
295        }
296
297        topo
298    }
299
300    /// Create IBM Heavy-Hex topology
301    pub fn from_heavy_hex(num_qubits: usize) -> Self {
302        let mut topo = Self::new(num_qubits);
303
304        // Add qubits with IBM-like properties
305        for i in 0..num_qubits {
306            topo.add_qubit(QubitProperties {
307                id: i as u32,
308                index: i as u32,
309                t1: 50.0 + 10.0 * fastrand::f64(),
310                t2: 70.0 + 15.0 * fastrand::f64(),
311                single_qubit_gate_error: 0.001 * (1.0 + 0.2 * fastrand::f64()),
312                gate_error_1q: 0.001 * (1.0 + 0.2 * fastrand::f64()),
313                readout_error: 0.015 * (1.0 + 0.3 * fastrand::f64()),
314                frequency: 5.0 + 0.2 * fastrand::f64(),
315            });
316        }
317
318        // Heavy-hex pattern for IBM quantum computers
319        // This is a simplified version - real IBM topologies vary
320        if num_qubits >= 27 {
321            // Create a heavy-hex lattice pattern
322            // Row 0: 0, 1, 2, 3, 4
323            for i in 0..4 {
324                topo.add_connection(
325                    i,
326                    i + 1,
327                    GateProperties {
328                        error_rate: 0.01 * (1.0 + 0.2 * fastrand::f64()),
329                        duration: 200.0 + 40.0 * fastrand::f64(),
330                        gate_type: "CNOT".to_string(),
331                    },
332                );
333            }
334
335            // Row 1: 5, 6, 7, 8, 9
336            for i in 5..9 {
337                topo.add_connection(
338                    i,
339                    i + 1,
340                    GateProperties {
341                        error_rate: 0.01 * (1.0 + 0.2 * fastrand::f64()),
342                        duration: 200.0 + 40.0 * fastrand::f64(),
343                        gate_type: "CNOT".to_string(),
344                    },
345                );
346            }
347
348            // Row 2: 10, 11, 12, 13, 14
349            for i in 10..14 {
350                topo.add_connection(
351                    i,
352                    i + 1,
353                    GateProperties {
354                        error_rate: 0.01 * (1.0 + 0.2 * fastrand::f64()),
355                        duration: 200.0 + 40.0 * fastrand::f64(),
356                        gate_type: "CNOT".to_string(),
357                    },
358                );
359            }
360
361            // Row 3: 15, 16, 17, 18, 19
362            for i in 15..19 {
363                topo.add_connection(
364                    i,
365                    i + 1,
366                    GateProperties {
367                        error_rate: 0.01 * (1.0 + 0.2 * fastrand::f64()),
368                        duration: 200.0 + 40.0 * fastrand::f64(),
369                        gate_type: "CNOT".to_string(),
370                    },
371                );
372            }
373
374            // Row 4: 20, 21, 22, 23, 24, 25, 26
375            for i in 20..26 {
376                topo.add_connection(
377                    i,
378                    i + 1,
379                    GateProperties {
380                        error_rate: 0.01 * (1.0 + 0.2 * fastrand::f64()),
381                        duration: 200.0 + 40.0 * fastrand::f64(),
382                        gate_type: "CNOT".to_string(),
383                    },
384                );
385            }
386
387            // Vertical connections (heavy edges)
388            let vertical_connections = vec![
389                (0, 5),
390                (2, 7),
391                (4, 9),
392                (5, 10),
393                (7, 12),
394                (9, 14),
395                (10, 15),
396                (12, 17),
397                (14, 19),
398                (15, 20),
399                (16, 21),
400                (17, 22),
401                (18, 23),
402                (19, 24),
403            ];
404
405            for (a, b) in vertical_connections {
406                if a < num_qubits && b < num_qubits {
407                    topo.add_connection(
408                        a as u32,
409                        b as u32,
410                        GateProperties {
411                            error_rate: 0.01 * (1.0 + 0.2 * fastrand::f64()),
412                            duration: 200.0 + 40.0 * fastrand::f64(),
413                            gate_type: "CNOT".to_string(),
414                        },
415                    );
416                }
417            }
418
419            // Diagonal connections (hex pattern)
420            let diagonal_connections = vec![(1, 6), (3, 8), (6, 11), (8, 13), (11, 16), (13, 18)];
421
422            for (a, b) in diagonal_connections {
423                if a < num_qubits && b < num_qubits {
424                    topo.add_connection(
425                        a as u32,
426                        b as u32,
427                        GateProperties {
428                            error_rate: 0.01 * (1.0 + 0.2 * fastrand::f64()),
429                            duration: 200.0 + 40.0 * fastrand::f64(),
430                            gate_type: "CNOT".to_string(),
431                        },
432                    );
433                }
434            }
435        }
436
437        topo
438    }
439
440    /// Create Google Sycamore-like topology
441    pub fn from_sycamore(num_qubits: usize) -> Self {
442        let mut topo = Self::new(num_qubits);
443
444        // Add qubits with Google-like properties
445        for i in 0..num_qubits {
446            topo.add_qubit(QubitProperties {
447                id: i as u32,
448                index: i as u32,
449                t1: 15.0 + 3.0 * fastrand::f64(),
450                t2: 20.0 + 5.0 * fastrand::f64(),
451                single_qubit_gate_error: 0.0015 * (1.0 + 0.2 * fastrand::f64()),
452                gate_error_1q: 0.0015 * (1.0 + 0.2 * fastrand::f64()),
453                readout_error: 0.008 * (1.0 + 0.2 * fastrand::f64()),
454                frequency: 5.5 + 0.1 * fastrand::f64(),
455            });
456        }
457
458        // Sycamore has a 2D grid with couplers removed
459        // This is a simplified version
460        if num_qubits >= 20 {
461            // Create a 4x5 grid-like structure
462            let rows = 4;
463            let cols = 5;
464
465            // Horizontal connections
466            for row in 0..rows {
467                for col in 0..cols - 1 {
468                    let q1 = row * cols + col;
469                    let q2 = row * cols + col + 1;
470                    if q1 < num_qubits && q2 < num_qubits {
471                        topo.add_connection(
472                            q1 as u32,
473                            q2 as u32,
474                            GateProperties {
475                                error_rate: 0.006 * (1.0 + 0.15 * fastrand::f64()),
476                                duration: 12.0,
477                                gate_type: "sqrt_ISwap".to_string(),
478                            },
479                        );
480                    }
481                }
482            }
483
484            // Vertical connections (with some removed for Sycamore pattern)
485            for row in 0..rows - 1 {
486                for col in 0..cols {
487                    // Skip some connections to create Sycamore pattern
488                    if (row + col) % 2 == 0 {
489                        let q1 = row * cols + col;
490                        let q2 = (row + 1) * cols + col;
491                        if q1 < num_qubits && q2 < num_qubits {
492                            topo.add_connection(
493                                q1 as u32,
494                                q2 as u32,
495                                GateProperties {
496                                    error_rate: 0.006 * (1.0 + 0.15 * fastrand::f64()),
497                                    duration: 12.0,
498                                    gate_type: "sqrt_ISwap".to_string(),
499                                },
500                            );
501                        }
502                    }
503                }
504            }
505        }
506
507        topo
508    }
509
510    /// Create a subset of Google Sycamore topology (3x3 grid with diagonal connections)
511    fn create_sycamore_subset() -> Self {
512        let mut topo = Self::new(9);
513
514        // Add qubits
515        for i in 0..9 {
516            topo.add_qubit(QubitProperties {
517                id: i as u32,
518                index: i as u32,
519                t1: 15.0 + 3.0 * fastrand::f64(),
520                t2: 20.0 + 5.0 * fastrand::f64(),
521                single_qubit_gate_error: 0.0015 * (1.0 + 0.2 * fastrand::f64()),
522                gate_error_1q: 0.0015 * (1.0 + 0.2 * fastrand::f64()),
523                readout_error: 0.03 * (1.0 + 0.2 * fastrand::f64()),
524                frequency: 5.5 + 0.1 * fastrand::f64(),
525            });
526        }
527
528        // Add grid connections
529        let grid_conns = vec![
530            (0, 1),
531            (1, 2),
532            (3, 4),
533            (4, 5),
534            (6, 7),
535            (7, 8), // horizontal
536            (0, 3),
537            (3, 6),
538            (1, 4),
539            (4, 7),
540            (2, 5),
541            (5, 8), // vertical
542        ];
543
544        for (q1, q2) in grid_conns {
545            topo.add_connection(
546                q1,
547                q2,
548                GateProperties {
549                    error_rate: 0.006 * (1.0 + 0.3 * fastrand::f64()),
550                    duration: 12.0,
551                    gate_type: "sqrt_ISwap".to_string(),
552                },
553            );
554        }
555
556        // Add some diagonal connections (Sycamore feature)
557        let diag_conns = vec![(0, 4), (4, 8), (2, 4), (4, 6)];
558
559        for (q1, q2) in diag_conns {
560            topo.add_connection(
561                q1,
562                q2,
563                GateProperties {
564                    error_rate: 0.008 * (1.0 + 0.3 * fastrand::f64()),
565                    duration: 12.0,
566                    gate_type: "sqrt_ISwap".to_string(),
567                },
568            );
569        }
570
571        topo
572    }
573
574    /// Get the number of qubits
575    pub fn num_qubits(&self) -> usize {
576        self.num_qubits
577    }
578
579    /// Get all connected pairs of qubits
580    pub fn connectivity(&self) -> Vec<(usize, usize)> {
581        let mut connections = Vec::new();
582        for edge in self.connectivity.edge_indices() {
583            if let Some((a, b)) = self.connectivity.edge_endpoints(edge) {
584                let q1 = self.connectivity[a] as usize;
585                let q2 = self.connectivity[b] as usize;
586                connections.push((q1, q2));
587            }
588        }
589        connections
590    }
591
592    /// Check if two qubits are connected
593    pub fn are_connected(&self, q1: usize, q2: usize) -> bool {
594        // Find node indices for these qubits
595        let node1 = self
596            .connectivity
597            .node_indices()
598            .find(|&n| self.connectivity[n] == q1 as u32);
599        let node2 = self
600            .connectivity
601            .node_indices()
602            .find(|&n| self.connectivity[n] == q2 as u32);
603
604        if let (Some(n1), Some(n2)) = (node1, node2) {
605            self.connectivity.contains_edge(n1, n2)
606        } else {
607            false
608        }
609    }
610
611    /// Get shortest path distance between two qubits
612    pub fn shortest_path_distance(&self, q1: usize, q2: usize) -> Option<f64> {
613        // Find node indices for these qubits
614        let node1 = self
615            .connectivity
616            .node_indices()
617            .find(|&n| self.connectivity[n] == q1 as u32)?;
618        let node2 = self
619            .connectivity
620            .node_indices()
621            .find(|&n| self.connectivity[n] == q2 as u32)?;
622
623        // Use Dijkstra to find shortest path
624        let distances = dijkstra(&self.connectivity, node1, Some(node2), |e| *e.weight());
625        distances.get(&node2).cloned()
626    }
627
628    /// Simple connectivity analysis
629    pub fn analyze_connectivity(&self) -> ConnectivityAnalysis {
630        let num_edges = self.connectivity.edge_count();
631        let mut min_conn = usize::MAX;
632        let mut max_conn = 0;
633        let mut total_conn = 0;
634
635        for node in self.connectivity.node_indices() {
636            let degree = self.connectivity.edges(node).count();
637            min_conn = min_conn.min(degree);
638            max_conn = max_conn.max(degree);
639            total_conn += degree;
640        }
641
642        let avg_conn = if self.num_qubits > 0 {
643            total_conn as f64 / self.num_qubits as f64
644        } else {
645            0.0
646        };
647
648        ConnectivityAnalysis {
649            num_qubits: self.num_qubits,
650            num_edges,
651            average_connectivity: avg_conn,
652            max_connectivity: max_conn,
653            min_connectivity: min_conn,
654            connected_components: connected_components(&self.connectivity),
655        }
656    }
657
658    /// Analyze topology properties
659    pub fn analyze(&self) -> TopologyAnalysis {
660        // Calculate shortest paths between all pairs
661        let mut all_distances = vec![vec![None; self.num_qubits]; self.num_qubits];
662
663        for node in self.connectivity.node_indices() {
664            let distances = dijkstra(&self.connectivity, node, None, |e| *e.weight());
665            for (&target, &dist) in distances.iter() {
666                let src_idx = self.connectivity[node] as usize;
667                let tgt_idx = self.connectivity[target] as usize;
668                all_distances[src_idx][tgt_idx] = Some(dist);
669            }
670        }
671
672        // Calculate average distance
673        let mut total_dist = 0.0;
674        let mut count = 0;
675        for i in 0..self.num_qubits {
676            for j in i + 1..self.num_qubits {
677                if let Some(d) = all_distances[i][j] {
678                    total_dist += d;
679                    count += 1;
680                }
681            }
682        }
683        let avg_distance = if count > 0 {
684            total_dist / count as f64
685        } else {
686            0.0
687        };
688
689        // Calculate degree distribution
690        let mut degree_dist = HashMap::new();
691        for node in self.connectivity.node_indices() {
692            let degree = self.connectivity.edges(node).count();
693            *degree_dist.entry(degree).or_insert(0) += 1;
694        }
695
696        // Find most connected qubit
697        let most_connected = self
698            .connectivity
699            .node_indices()
700            .max_by_key(|&n| self.connectivity.edges(n).count())
701            .map(|n| self.connectivity[n]);
702
703        // Calculate clustering coefficient
704        let clustering_coeff = self.calculate_clustering_coefficient();
705
706        // Count connected components
707        let num_components = connected_components(&self.connectivity);
708
709        // Find minimum spanning tree weight
710        let mst = min_spanning_tree(&self.connectivity);
711        let mst_graph = UnGraph::<_, _>::from_elements(mst);
712        let mst_weight: f64 = mst_graph.edge_references().map(|e| *e.weight()).sum();
713
714        TopologyAnalysis {
715            num_qubits: self.num_qubits,
716            num_connections: self.connectivity.edge_count(),
717            average_distance: avg_distance,
718            max_distance: all_distances
719                .iter()
720                .flatten()
721                .filter_map(|&d| d)
722                .fold(0.0, f64::max),
723            degree_distribution: degree_dist,
724            most_connected_qubit: most_connected,
725            clustering_coefficient: clustering_coeff,
726            connected_components: num_components,
727            mst_weight,
728            avg_gate_error: self
729                .gate_properties
730                .values()
731                .map(|p| p.error_rate)
732                .sum::<f64>()
733                / self.gate_properties.len().max(1) as f64,
734            avg_coherence_time: self.qubit_properties.iter().map(|p| p.t2).sum::<f64>()
735                / self.qubit_properties.len().max(1) as f64,
736        }
737    }
738
739    /// Calculate clustering coefficient
740    fn calculate_clustering_coefficient(&self) -> f64 {
741        let mut total_coeff = 0.0;
742        let mut count = 0;
743
744        for node in self.connectivity.node_indices() {
745            let neighbors: Vec<_> = self.connectivity.neighbors(node).collect();
746            let k = neighbors.len();
747
748            if k >= 2 {
749                let mut triangles = 0;
750                for i in 0..k {
751                    for j in i + 1..k {
752                        if self.connectivity.contains_edge(neighbors[i], neighbors[j]) {
753                            triangles += 1;
754                        }
755                    }
756                }
757
758                let possible = k * (k - 1) / 2;
759                total_coeff += triangles as f64 / possible as f64;
760                count += 1;
761            }
762        }
763
764        if count > 0 {
765            total_coeff / count as f64
766        } else {
767            0.0
768        }
769    }
770
771    /// Find critical qubits (removal increases distances significantly)
772    pub fn find_critical_qubits(&self) -> Vec<u32> {
773        let base_analysis = self.analyze();
774        let mut critical = Vec::new();
775
776        for node in self.connectivity.node_indices() {
777            // Create topology without this qubit
778            let mut test_graph = self.connectivity.clone();
779            test_graph.remove_node(node);
780
781            // Check if it disconnects the graph
782            if connected_components(&test_graph) > base_analysis.connected_components {
783                critical.push(self.connectivity[node]);
784            }
785        }
786
787        critical
788    }
789
790    /// Find optimal qubit subset for a given circuit size
791    pub fn find_optimal_subset(&self, required_qubits: usize) -> Result<Vec<u32>, TopologyError> {
792        if required_qubits > self.num_qubits {
793            return Err(TopologyError::InvalidConfiguration(format!(
794                "Required {} qubits but hardware has only {}",
795                required_qubits, self.num_qubits
796            )));
797        }
798
799        // Score function: minimize average error and maximize connectivity
800        let score_subset = |subset: &[u32]| -> f64 {
801            let mut score = 0.0;
802            let mut connections = 0;
803
804            // Add qubit quality scores
805            for &q in subset {
806                if let Some(props) = self.qubit_properties.get(q as usize) {
807                    score += props.gate_error_1q + props.readout_error;
808                }
809            }
810
811            // Add connection scores
812            for i in 0..subset.len() {
813                for j in i + 1..subset.len() {
814                    if let Some(props) = self
815                        .gate_properties
816                        .get(&(subset[i], subset[j]))
817                        .or_else(|| self.gate_properties.get(&(subset[j], subset[i])))
818                    {
819                        score += props.error_rate;
820                        connections += 1;
821                    }
822                }
823            }
824
825            // Penalize low connectivity
826            if connections < required_qubits - 1 {
827                score *= 10.0;
828            }
829
830            score
831        };
832
833        // Use greedy algorithm to find good subset
834        let mut best_subset = Vec::new();
835        let mut best_score = f64::INFINITY;
836
837        // Try different starting points
838        for start in 0..self.num_qubits.min(5) {
839            let mut subset = vec![start as u32];
840            let mut remaining: HashSet<_> = (0..self.num_qubits as u32).collect();
841            remaining.remove(&(start as u32));
842
843            while subset.len() < required_qubits {
844                // Find best qubit to add - prefer connected qubits
845                let mut best_add = None;
846                let mut best_add_score = f64::INFINITY;
847
848                // First, try to add only connected qubits
849                let mut found_connected = false;
850                for &candidate in &remaining {
851                    // Check if candidate is connected to any qubit in subset
852                    let is_connected = subset.iter().any(|&q| {
853                        self.gate_properties.contains_key(&(q, candidate))
854                            || self.gate_properties.contains_key(&(candidate, q))
855                    });
856
857                    if is_connected {
858                        found_connected = true;
859                        let mut test_subset = subset.clone();
860                        test_subset.push(candidate);
861                        let score = score_subset(&test_subset);
862
863                        if score < best_add_score {
864                            best_add_score = score;
865                            best_add = Some(candidate);
866                        }
867                    }
868                }
869
870                // If no connected qubit found, consider all remaining qubits
871                if !found_connected {
872                    for &candidate in &remaining {
873                        let mut test_subset = subset.clone();
874                        test_subset.push(candidate);
875                        let score = score_subset(&test_subset);
876
877                        if score < best_add_score {
878                            best_add_score = score;
879                            best_add = Some(candidate);
880                        }
881                    }
882                }
883
884                if let Some(q) = best_add {
885                    subset.push(q);
886                    remaining.remove(&q);
887                } else {
888                    break; // No more qubits to add
889                }
890            }
891
892            let score = score_subset(&subset);
893            if score < best_score {
894                best_score = score;
895                best_subset = subset;
896            }
897        }
898
899        Ok(best_subset)
900    }
901}
902
903impl Default for HardwareTopology {
904    fn default() -> Self {
905        Self {
906            num_qubits: 0,
907            connectivity: UnGraph::new_undirected(),
908            qubit_properties: Vec::new(),
909            gate_properties: HashMap::new(),
910        }
911    }
912}
913
914/// Simple connectivity analysis results
915#[derive(Debug)]
916pub struct ConnectivityAnalysis {
917    pub num_qubits: usize,
918    pub num_edges: usize,
919    pub average_connectivity: f64,
920    pub max_connectivity: usize,
921    pub min_connectivity: usize,
922    pub connected_components: usize,
923}
924
925/// Analysis results for a hardware topology
926#[derive(Debug)]
927pub struct TopologyAnalysis {
928    pub num_qubits: usize,
929    pub num_connections: usize,
930    pub average_distance: f64,
931    pub max_distance: f64,
932    pub degree_distribution: HashMap<usize, usize>,
933    pub most_connected_qubit: Option<u32>,
934    pub clustering_coefficient: f64,
935    pub connected_components: usize,
936    pub mst_weight: f64,
937    pub avg_gate_error: f64,
938    pub avg_coherence_time: f64,
939}
940
941impl TopologyAnalysis {
942    /// Generate a report of the analysis
943    pub fn report(&self) -> String {
944        let mut report = String::new();
945
946        report.push_str("=== Hardware Topology Analysis ===\n");
947        report.push_str(&format!("Number of qubits: {}\n", self.num_qubits));
948        report.push_str(&format!(
949            "Number of connections: {}\n",
950            self.num_connections
951        ));
952        report.push_str(&format!(
953            "Connected components: {}\n",
954            self.connected_components
955        ));
956        report.push_str("\nDistance metrics:\n");
957        report.push_str(&format!(
958            "  Average distance: {:.2}\n",
959            self.average_distance
960        ));
961        report.push_str(&format!("  Maximum distance: {:.2}\n", self.max_distance));
962        report.push_str("\nConnectivity metrics:\n");
963        report.push_str(&format!(
964            "  Clustering coefficient: {:.3}\n",
965            self.clustering_coefficient
966        ));
967        report.push_str(&format!("  MST weight: {:.3}\n", self.mst_weight));
968
969        if let Some(q) = self.most_connected_qubit {
970            report.push_str(&format!("  Most connected qubit: {}\n", q));
971        }
972
973        report.push_str("\nQuality metrics:\n");
974        report.push_str(&format!(
975            "  Average gate error: {:.4}\n",
976            self.avg_gate_error
977        ));
978        report.push_str(&format!(
979            "  Average T2 time: {:.1} μs\n",
980            self.avg_coherence_time
981        ));
982
983        report.push_str(&format!("\nDegree distribution:\n"));
984        let mut degrees: Vec<_> = self.degree_distribution.iter().collect();
985        degrees.sort_by_key(|&(k, _)| k);
986        for (degree, count) in degrees {
987            report.push_str(&format!("  {} connections: {} qubits\n", degree, count));
988        }
989
990        report
991    }
992}
993
994#[cfg(test)]
995mod tests {
996    use super::*;
997
998    #[test]
999    fn test_linear_topology() {
1000        let topo = HardwareTopology::linear_topology(5);
1001        let analysis = topo.analyze();
1002
1003        assert_eq!(analysis.num_qubits, 5);
1004        assert_eq!(analysis.num_connections, 4);
1005        assert_eq!(analysis.connected_components, 1);
1006        assert_eq!(analysis.max_distance, 4.0 * 0.01); // 4 hops with error 0.01 each
1007    }
1008
1009    #[test]
1010    fn test_grid_topology() {
1011        let topo = HardwareTopology::grid_topology(3, 3);
1012        let analysis = topo.analyze();
1013
1014        assert_eq!(analysis.num_qubits, 9);
1015        assert_eq!(analysis.connected_components, 1);
1016        assert!(analysis.average_distance > 0.0);
1017    }
1018
1019    #[test]
1020    fn test_ibm_topology() {
1021        let topo = HardwareTopology::ibm_topology();
1022        let analysis = topo.analyze();
1023
1024        assert_eq!(analysis.num_qubits, 5);
1025        assert_eq!(analysis.num_connections, 4);
1026        assert_eq!(analysis.most_connected_qubit, Some(1)); // Qubit 1 is central
1027    }
1028
1029    #[test]
1030    fn test_find_optimal_subset() {
1031        let topo = HardwareTopology::grid_topology(3, 3);
1032        let subset = topo.find_optimal_subset(4).unwrap();
1033
1034        assert_eq!(subset.len(), 4);
1035        // Should pick connected qubits
1036        let mut connected = false;
1037        for i in 0..subset.len() {
1038            for j in i + 1..subset.len() {
1039                if topo.gate_properties.contains_key(&(subset[i], subset[j]))
1040                    || topo.gate_properties.contains_key(&(subset[j], subset[i]))
1041                {
1042                    connected = true;
1043                    break;
1044                }
1045            }
1046            if connected {
1047                break;
1048            }
1049        }
1050        assert!(connected, "Subset {:?} is not connected", subset);
1051    }
1052}