Skip to main content

quantrs2_device/mapping_scirs2/
core.rs

1//! Core SciRS2 qubit mapper implementation
2
3use super::*;
4
5// Real graph-analysis primitives from scirs2-graph. These are imported
6// directly (rather than relying only on the subset re-exported by
7// `mapping_scirs2::mod`) so this file can compute genuine density,
8// clustering, connectivity, community, centrality, and spectral statistics
9// instead of returning fixed constants.
10#[cfg(feature = "scirs2")]
11use scirs2_graph::planarity::is_planar;
12#[cfg(feature = "scirs2")]
13use scirs2_graph::spectral::{laplacian, LaplacianType};
14#[cfg(feature = "scirs2")]
15use scirs2_graph::{
16    chromatic_number as scirs2_chromatic_number, connected_components, is_bipartite, modularity,
17    pagerank_centrality,
18};
19
20/// Advanced SciRS2 qubit mapper
21pub struct SciRS2QubitMapper {
22    /// Configuration settings
23    config: SciRS2MappingConfig,
24    /// Hardware topology
25    device_topology: HardwareTopology,
26    /// Device calibration data
27    calibration: Option<DeviceCalibration>,
28
29    // Cached analysis results
30    logical_graph: Option<Graph<usize, f64>>,
31    physical_graph: Option<Graph<usize, f64>>,
32    spectral_cache: Option<SpectralAnalysisResult>,
33    community_cache: Option<CommunityAnalysisResult>,
34    centrality_cache: Option<CentralityAnalysisResult>,
35}
36
37impl SciRS2QubitMapper {
38    /// Create a new SciRS2 qubit mapper
39    pub fn new(
40        config: SciRS2MappingConfig,
41        device_topology: HardwareTopology,
42        calibration: Option<DeviceCalibration>,
43    ) -> Self {
44        Self {
45            config,
46            device_topology,
47            calibration,
48            logical_graph: None,
49            physical_graph: None,
50            spectral_cache: None,
51            community_cache: None,
52            centrality_cache: None,
53        }
54    }
55
56    /// Perform comprehensive qubit mapping using SciRS2 algorithms
57    #[cfg(feature = "scirs2")]
58    pub fn map_circuit<const N: usize>(
59        &mut self,
60        circuit: &Circuit<N>,
61    ) -> DeviceResult<SciRS2MappingResult> {
62        let start_time = std::time::Instant::now();
63
64        // Step 1: Build logical interaction graph from circuit
65        let logical_graph = self.build_logical_graph(circuit)?;
66        // Note: SciRS2 Graph doesn't implement Clone, so we don't cache it for now
67        self.logical_graph = None;
68
69        // Step 2: Build physical hardware graph
70        let physical_graph = self.build_physical_graph()?;
71        // Note: SciRS2 Graph doesn't implement Clone, so we don't cache it for now
72        self.physical_graph = None;
73
74        // Step 3: Perform graph analysis
75        let graph_analysis = self.analyze_graphs(&logical_graph, &physical_graph)?;
76
77        // Step 4: Spectral analysis (if enabled)
78        let spectral_analysis = if self.config.enable_spectral_analysis {
79            Some(self.perform_spectral_analysis(&logical_graph, &physical_graph)?)
80        } else {
81            None
82        };
83
84        // Step 5: Community detection and analysis
85        let community_analysis =
86            self.perform_community_analysis(&logical_graph, &physical_graph)?;
87
88        // Step 6: Centrality analysis (if enabled)
89        let centrality_analysis = if self.config.enable_centrality_optimization {
90            self.perform_centrality_analysis(&logical_graph, &physical_graph)?
91        } else {
92            CentralityAnalysisResult {
93                betweenness_centrality: HashMap::new(),
94                closeness_centrality: HashMap::new(),
95                eigenvector_centrality: HashMap::new(),
96                pagerank_centrality: HashMap::new(),
97                centrality_correlations: Array2::zeros((0, 0)),
98                centrality_statistics: CentralityStatistics {
99                    max_betweenness: 0.0,
100                    max_closeness: 0.0,
101                    max_eigenvector: 0.0,
102                    max_pagerank: 0.0,
103                    mean_betweenness: 0.0,
104                    mean_closeness: 0.0,
105                    mean_eigenvector: 0.0,
106                    mean_pagerank: 0.0,
107                },
108            }
109        };
110
111        // Step 7: Generate initial mapping using specified algorithm
112        let initial_mapping = self.generate_initial_mapping(
113            &logical_graph,
114            &physical_graph,
115            &graph_analysis,
116            spectral_analysis.as_ref(),
117            &community_analysis,
118            &centrality_analysis,
119        )?;
120
121        // Step 8: Optimize mapping using advanced techniques
122        let (final_mapping, swap_operations, optimization_metrics) = self.optimize_mapping(
123            circuit,
124            initial_mapping.clone(),
125            &logical_graph,
126            &physical_graph,
127        )?;
128
129        // Step 9: Generate performance predictions (if ML enabled)
130        let performance_predictions = if self.config.enable_ml_predictions {
131            Some(self.predict_performance(&final_mapping, circuit, &graph_analysis)?)
132        } else {
133            None
134        };
135
136        // Step 10: Real-time analytics
137        let realtime_analytics = self.generate_realtime_analytics(&optimization_metrics)?;
138
139        // Step 11: ML performance analysis (if enabled)
140        let ml_performance = if self.config.ml_config.enable_ml {
141            Some(self.analyze_ml_performance(&final_mapping, &optimization_metrics)?)
142        } else {
143            None
144        };
145
146        // Step 12: Generate adaptive insights
147        let adaptive_insights = self.generate_adaptive_insights(&optimization_metrics)?;
148
149        // Step 13: Generate optimization recommendations
150        let optimization_recommendations = self.generate_optimization_recommendations(
151            &graph_analysis,
152            &optimization_metrics,
153            spectral_analysis.as_ref(),
154            &community_analysis,
155        )?;
156
157        Ok(SciRS2MappingResult {
158            initial_mapping,
159            final_mapping,
160            swap_operations,
161            graph_analysis,
162            spectral_analysis,
163            community_analysis,
164            centrality_analysis,
165            optimization_metrics,
166            performance_predictions,
167            realtime_analytics,
168            ml_performance,
169            adaptive_insights,
170            optimization_recommendations,
171        })
172    }
173
174    /// Fallback mapping when SciRS2 is not available
175    #[cfg(not(feature = "scirs2"))]
176    pub fn map_circuit<const N: usize>(
177        &mut self,
178        circuit: &Circuit<N>,
179    ) -> DeviceResult<SciRS2MappingResult> {
180        // Simple fallback implementation
181        let mut initial_mapping = HashMap::new();
182        let mut final_mapping = HashMap::new();
183
184        // Sequential mapping
185        for i in 0..N.min(self.device_topology.num_qubits()) {
186            initial_mapping.insert(i, i);
187            final_mapping.insert(i, i);
188        }
189
190        Ok(SciRS2MappingResult {
191            initial_mapping,
192            final_mapping,
193            swap_operations: Vec::new(),
194            graph_analysis: GraphAnalysisResult {
195                density: 0.5,
196                clustering_coefficient: 0.3,
197                diameter: 4,
198                radius: 2,
199                average_path_length: 2.5,
200                connectivity_stats: ConnectivityStats {
201                    edge_connectivity: 2,
202                    vertex_connectivity: 1,
203                    algebraic_connectivity: 0.5,
204                    is_connected: true,
205                    num_components: 1,
206                    largest_component_size: N,
207                },
208                topological_properties: TopologicalProperties {
209                    is_planar: true,
210                    is_bipartite: false,
211                    is_tree: false,
212                    is_forest: false,
213                    has_cycles: true,
214                    girth: 3,
215                    chromatic_number: 3,
216                    independence_number: 5,
217                },
218            },
219            spectral_analysis: None,
220            community_analysis: CommunityAnalysisResult {
221                communities: HashMap::new(),
222                modularity: 0.4,
223                num_communities: 1,
224                community_sizes: vec![N],
225                inter_community_edges: 0,
226                quality_metrics: CommunityQualityMetrics {
227                    silhouette_score: 0.7,
228                    conductance: 0.3,
229                    coverage: 0.8,
230                    performance: 0.75,
231                },
232            },
233            centrality_analysis: CentralityAnalysisResult {
234                betweenness_centrality: HashMap::new(),
235                closeness_centrality: HashMap::new(),
236                eigenvector_centrality: HashMap::new(),
237                pagerank_centrality: HashMap::new(),
238                centrality_correlations: Array2::zeros((0, 0)),
239                centrality_statistics: CentralityStatistics {
240                    max_betweenness: 0.0,
241                    max_closeness: 0.0,
242                    max_eigenvector: 0.0,
243                    max_pagerank: 0.0,
244                    mean_betweenness: 0.0,
245                    mean_closeness: 0.0,
246                    mean_eigenvector: 0.0,
247                    mean_pagerank: 0.0,
248                },
249            },
250            optimization_metrics: OptimizationMetrics {
251                optimization_time: Duration::from_millis(1),
252                iterations: 1,
253                converged: true,
254                final_objective: 0.0,
255                best_objective: 0.0,
256                improvement_ratio: 0.0,
257                constraint_violations: 0.0,
258                algorithm_metrics: HashMap::new(),
259                resource_usage: ResourceUsageMetrics {
260                    peak_memory: 1024,
261                    average_cpu: 1.0,
262                    energy_consumption: None,
263                    network_overhead: None,
264                },
265            },
266            performance_predictions: None,
267            realtime_analytics: RealtimeAnalyticsResult {
268                current_metrics: HashMap::new(),
269                performance_trends: HashMap::new(),
270                anomalies: Vec::new(),
271                resource_utilization: ResourceUtilization {
272                    cpu_usage: 1.0,
273                    memory_usage: 5.0,
274                    disk_io: 0.0,
275                    network_usage: 0.0,
276                    gpu_usage: None,
277                },
278                quality_assessments: Vec::new(),
279            },
280            ml_performance: None,
281            adaptive_insights: AdaptiveMappingInsights {
282                learning_progress: HashMap::new(),
283                adaptation_effectiveness: HashMap::new(),
284                performance_trends: HashMap::new(),
285                recommended_adjustments: Vec::new(),
286            },
287            optimization_recommendations: OptimizationRecommendations {
288                algorithm_recommendations: Vec::new(),
289                parameter_suggestions: Vec::new(),
290                hardware_optimizations: Vec::new(),
291                improvement_predictions: HashMap::new(),
292            },
293        })
294    }
295
296    /// Build logical interaction graph from circuit
297    #[cfg(feature = "scirs2")]
298    fn build_logical_graph<const N: usize>(
299        &self,
300        circuit: &Circuit<N>,
301    ) -> DeviceResult<Graph<usize, f64>> {
302        let mut graph = Graph::new();
303
304        // Add nodes for each qubit
305        let mut node_map: HashMap<usize, usize> = HashMap::new();
306        for i in 0..N {
307            let node = graph.add_node(i);
308            node_map.insert(i, node.index());
309        }
310
311        // Add edges based on two-qubit gates
312        for gate in circuit.gates() {
313            let qubits = gate.qubits();
314            if qubits.len() == 2 {
315                let q1 = qubits[0].id() as usize;
316                let q2 = qubits[1].id() as usize;
317
318                if let (Some(&node1), Some(&node2)) = (node_map.get(&q1), node_map.get(&q2)) {
319                    // Weight based on gate frequency/importance
320                    // Dereference Arc to get &dyn GateOp
321                    let weight = self.calculate_gate_weight(gate.as_ref());
322                    let _ = graph.add_edge(node1, node2, weight);
323                }
324            }
325        }
326
327        Ok(graph)
328    }
329
330    /// Build physical hardware topology graph
331    #[cfg(feature = "scirs2")]
332    fn build_physical_graph(&self) -> DeviceResult<Graph<usize, f64>> {
333        let mut graph = Graph::new();
334
335        // Add nodes for each physical qubit
336        let mut node_map: HashMap<usize, usize> = HashMap::new();
337        for i in 0..self.device_topology.num_qubits() {
338            let node = graph.add_node(i);
339            node_map.insert(i, node.index());
340        }
341
342        // Add edges based on connectivity
343        for (q1, q2) in self.device_topology.connectivity() {
344            if let (Some(&node1), Some(&node2)) = (node_map.get(&q1), node_map.get(&q2)) {
345                // Weight based on calibration data or use 1.0 as default
346                let weight = self.get_connection_weight(q1, q2);
347                let _ = graph.add_edge(node1, node2, weight);
348            }
349        }
350
351        Ok(graph)
352    }
353
354    /// Calculate weight for a gate operation
355    fn calculate_gate_weight(&self, _gate: &dyn GateOp) -> f64 {
356        // Simplified implementation - could be enhanced based on gate type, fidelity, etc.
357        1.0
358    }
359
360    /// Get connection weight between physical qubits
361    fn get_connection_weight(&self, q1: usize, q2: usize) -> f64 {
362        if let Some(calibration) = &self.calibration {
363            // Use calibration data if available
364            calibration.gate_fidelity(q1, q2).unwrap_or(1.0)
365        } else {
366            1.0
367        }
368    }
369
370    /// Calculate objective function value for a mapping
371    fn calculate_objective<const N: usize>(
372        &self,
373        mapping: &HashMap<usize, usize>,
374        circuit: &Circuit<N>,
375    ) -> DeviceResult<f64> {
376        let mut objective = 0.0;
377
378        match self.config.optimization_objective {
379            OptimizationObjective::MinimizeSwaps => {
380                // Count required SWAP operations
381                for gate in circuit.gates() {
382                    let qubits = gate.qubits();
383                    if qubits.len() == 2 {
384                        let logical_q1 = qubits[0].id() as usize;
385                        let logical_q2 = qubits[1].id() as usize;
386
387                        if let (Some(&physical_q1), Some(&physical_q2)) =
388                            (mapping.get(&logical_q1), mapping.get(&logical_q2))
389                        {
390                            if !self.device_topology.are_connected(physical_q1, physical_q2) {
391                                // Need SWAP operations
392                                objective += 1.0;
393                            }
394                        }
395                    }
396                }
397            }
398            OptimizationObjective::MinimizeDepth => {
399                // Simplified depth calculation
400                objective = circuit.gates().len() as f64;
401            }
402            OptimizationObjective::MaximizeFidelity => {
403                // Calculate based on fidelity (negate for minimization)
404                if let Some(calibration) = &self.calibration {
405                    let mut total_fidelity = 0.0;
406                    let mut gate_count = 0;
407
408                    for gate in circuit.gates() {
409                        let qubits = gate.qubits();
410                        if qubits.len() == 1 {
411                            let q = qubits[0].id() as usize;
412                            if let Some(&physical_q) = mapping.get(&q) {
413                                total_fidelity += calibration
414                                    .single_qubit_fidelity(physical_q)
415                                    .unwrap_or(0.99);
416                                gate_count += 1;
417                            }
418                        } else if qubits.len() == 2 {
419                            let q1 = qubits[0].id() as usize;
420                            let q2 = qubits[1].id() as usize;
421                            if let (Some(&pq1), Some(&pq2)) = (mapping.get(&q1), mapping.get(&q2)) {
422                                total_fidelity +=
423                                    calibration.gate_fidelity(pq1, pq2).unwrap_or(0.95);
424                                gate_count += 1;
425                            }
426                        }
427                    }
428
429                    objective = -(total_fidelity / gate_count.max(1) as f64); // Negative for maximization
430                } else {
431                    objective = -0.95; // Default fidelity
432                }
433            }
434            _ => {
435                // Default to SWAP minimization
436                objective = 0.0;
437            }
438        }
439
440        Ok(objective)
441    }
442
443    /// Perform real structural analysis of the physical hardware graph.
444    ///
445    /// All fields are computed from the actual device topology graph (via
446    /// scirs2-graph algorithms) rather than fixed constants: density,
447    /// clustering, diameter/radius, connectivity, and topological
448    /// properties (bipartiteness, planarity, tree/forest/cycle status,
449    /// girth, chromatic number, and a greedy independence-number lower
450    /// bound) all vary with the real topology passed in.
451    #[cfg(feature = "scirs2")]
452    fn analyze_graphs(
453        &self,
454        _logical_graph: &Graph<usize, f64>,
455        physical_graph: &Graph<usize, f64>,
456    ) -> DeviceResult<GraphAnalysisResult> {
457        let n = physical_graph.node_count();
458
459        let density = if n >= 2 {
460            graph_density(physical_graph).unwrap_or(0.0)
461        } else {
462            0.0
463        };
464
465        let clustering_coefficient = clustering_coefficient(physical_graph)
466            .map(|per_node| {
467                if per_node.is_empty() {
468                    0.0
469                } else {
470                    per_node.values().sum::<f64>() / per_node.len() as f64
471                }
472            })
473            .unwrap_or(0.0);
474
475        let diameter_f = diameter(physical_graph).unwrap_or(0.0);
476        let radius_f = radius(physical_graph).unwrap_or(0.0);
477        let average_path_length = Self::average_shortest_path_length(physical_graph);
478
479        let components = connected_components(physical_graph);
480        let num_components = components.len();
481        let largest_component_size = components.iter().map(|c| c.len()).max().unwrap_or(0);
482        let is_connected = num_components <= 1;
483        let algebraic_connectivity = Self::algebraic_connectivity(physical_graph).unwrap_or(0.0);
484        let (edge_connectivity, vertex_connectivity) =
485            Self::min_degree_connectivity_bounds(physical_graph);
486
487        let bipartite_result = is_bipartite(physical_graph);
488        let edge_count = physical_graph.edge_count();
489        let is_forest = num_components > 0 && edge_count == n.saturating_sub(num_components);
490        let is_tree = is_connected && is_forest && n > 0;
491        let has_cycles = !is_forest;
492        let girth = Self::compute_girth(physical_graph).unwrap_or(0);
493        let max_degree = Self::max_degree(physical_graph);
494        let chromatic_number = if n == 0 {
495            0
496        } else if n <= 40 {
497            scirs2_chromatic_number(physical_graph, max_degree + 1).unwrap_or(max_degree + 1)
498        } else {
499            // Exact chromatic number is NP-hard; for larger devices fall
500            // back to the real greedy-coloring upper bound (max_degree + 1)
501            // rather than paying for exponential backtracking.
502            max_degree + 1
503        };
504        let independence_number = Self::greedy_independence_number(physical_graph);
505        let edges: Vec<(usize, usize)> = Self::edge_list(physical_graph);
506        let is_planar = n == 0 || is_planar(&edges, n);
507
508        Ok(GraphAnalysisResult {
509            density,
510            clustering_coefficient,
511            diameter: diameter_f.round().max(0.0) as usize,
512            radius: radius_f.round().max(0.0) as usize,
513            average_path_length,
514            connectivity_stats: ConnectivityStats {
515                edge_connectivity,
516                vertex_connectivity,
517                algebraic_connectivity,
518                is_connected,
519                num_components,
520                largest_component_size,
521            },
522            topological_properties: TopologicalProperties {
523                is_planar,
524                is_bipartite: bipartite_result.is_bipartite,
525                is_tree,
526                is_forest,
527                has_cycles,
528                girth,
529                chromatic_number,
530                independence_number,
531            },
532        })
533    }
534
535    /// Real spectral analysis of the physical hardware graph's Laplacian:
536    /// actual eigenvalues (via scirs2-linalg `eig` on the real Laplacian
537    /// matrix), a genuine low-dimensional spectral embedding from the
538    /// corresponding eigenvectors, and embedding-quality metrics computed
539    /// by comparing embedding distances against real graph shortest-path
540    /// distances (Kruskal-style stress).
541    fn perform_spectral_analysis(
542        &self,
543        _logical_graph: &Graph<usize, f64>,
544        physical_graph: &Graph<usize, f64>,
545    ) -> DeviceResult<SpectralAnalysisResult> {
546        let n = physical_graph.node_count();
547        if n == 0 {
548            return Ok(SpectralAnalysisResult {
549                laplacian_eigenvalues: Array1::zeros(0),
550                embedding_vectors: Array2::zeros((0, 0)),
551                spectral_radius: 0.0,
552                algebraic_connectivity: 0.0,
553                spectral_gap: 0.0,
554                embedding_quality: EmbeddingQuality {
555                    stress: 0.0,
556                    distortion: 0.0,
557                    preservation_ratio: 0.0,
558                    embedding_dimension: 0,
559                },
560            });
561        }
562
563        let laplacian_matrix = laplacian(physical_graph, LaplacianType::Standard)
564            .map_err(|e| DeviceError::GraphAnalysisError(format!("laplacian failed: {e}")))?;
565        let (eigenvalues_complex, eigenvectors_complex) = eig(&laplacian_matrix.view(), None)
566            .map_err(|e| DeviceError::GraphAnalysisError(format!("eig failed: {e}")))?;
567
568        let mut order: Vec<usize> = (0..n).collect();
569        order.sort_by(|&a, &b| {
570            eigenvalues_complex[a]
571                .re
572                .partial_cmp(&eigenvalues_complex[b].re)
573                .unwrap_or(std::cmp::Ordering::Equal)
574        });
575        let sorted_eigenvalues: Vec<f64> =
576            order.iter().map(|&i| eigenvalues_complex[i].re).collect();
577        let laplacian_eigenvalues = Array1::from_vec(sorted_eigenvalues.clone());
578
579        let spectral_radius_value = sorted_eigenvalues
580            .iter()
581            .cloned()
582            .fold(0.0_f64, |acc, v| acc.max(v.abs()));
583        let algebraic_connectivity = if n >= 2 {
584            sorted_eigenvalues[1].max(0.0)
585        } else {
586            0.0
587        };
588        let spectral_gap = if n >= 2 {
589            (sorted_eigenvalues[n - 1] - sorted_eigenvalues[n.saturating_sub(2)]).abs()
590        } else {
591            0.0
592        };
593
594        // Spectral (Laplacian eigenmap) embedding: use the eigenvectors of
595        // the smallest non-trivial eigenvalues as real embedding coordinates.
596        let embedding_dimension = (n.saturating_sub(1)).min(2);
597        let mut embedding_vectors = Array2::<f64>::zeros((n, embedding_dimension));
598        for (col, &eig_idx) in order.iter().skip(1).take(embedding_dimension).enumerate() {
599            for row in 0..n {
600                embedding_vectors[[row, col]] = eigenvectors_complex[[row, eig_idx]].re;
601            }
602        }
603
604        let (stress, distortion, preservation_ratio) =
605            Self::embedding_quality_metrics(physical_graph, &embedding_vectors);
606
607        Ok(SpectralAnalysisResult {
608            laplacian_eigenvalues,
609            embedding_vectors,
610            spectral_radius: spectral_radius_value,
611            algebraic_connectivity,
612            spectral_gap,
613            embedding_quality: EmbeddingQuality {
614                stress,
615                distortion,
616                preservation_ratio,
617                embedding_dimension,
618            },
619        })
620    }
621
622    /// Real community detection (Louvain method) over the *logical*
623    /// qubit-interaction graph -- grouping logical qubits that interact
624    /// heavily is what actually matters for placement, since those groups
625    /// benefit most from being mapped onto well-connected physical regions.
626    fn perform_community_analysis(
627        &self,
628        logical_graph: &Graph<usize, f64>,
629        physical_graph: &Graph<usize, f64>,
630    ) -> DeviceResult<CommunityAnalysisResult> {
631        let graph = if logical_graph.node_count() > 0 {
632            logical_graph
633        } else {
634            physical_graph
635        };
636        let n = graph.node_count();
637        if n == 0 {
638            return Ok(CommunityAnalysisResult {
639                communities: HashMap::new(),
640                modularity: 0.0,
641                num_communities: 0,
642                community_sizes: Vec::new(),
643                inter_community_edges: 0,
644                quality_metrics: CommunityQualityMetrics {
645                    silhouette_score: 0.0,
646                    conductance: 0.0,
647                    coverage: 0.0,
648                    performance: 0.0,
649                },
650            });
651        }
652
653        let result = louvain_communities_result(graph);
654        let communities: HashMap<usize, usize> = result.node_communities.clone();
655        let community_sizes: Vec<usize> = result.communities.iter().map(|c| c.len()).collect();
656        let num_communities = result.num_communities;
657        let modularity_score = modularity(graph, &communities);
658
659        let mut inter_community_edges = 0usize;
660        let mut intra_community_edges = 0usize;
661        for node in graph.nodes() {
662            if let Ok(neighbors) = graph.neighbors(node) {
663                for neighbor in neighbors {
664                    if communities.get(node) != communities.get(&neighbor) {
665                        inter_community_edges += 1;
666                    } else {
667                        intra_community_edges += 1;
668                    }
669                }
670            }
671        }
672        // Each undirected edge was counted from both endpoints.
673        inter_community_edges /= 2;
674        intra_community_edges /= 2;
675        let total_edges = inter_community_edges + intra_community_edges;
676
677        let coverage = if total_edges > 0 {
678            intra_community_edges as f64 / total_edges as f64
679        } else {
680            0.0
681        };
682
683        // Conductance: average, over communities, of (edges leaving the
684        // community) / (total edge-endpoints touching the community).
685        let mut community_boundary: HashMap<usize, (usize, usize)> = HashMap::new();
686        for node in graph.nodes() {
687            let Some(&community) = communities.get(node) else {
688                continue;
689            };
690            let entry = community_boundary.entry(community).or_insert((0, 0));
691            if let Ok(neighbors) = graph.neighbors(node) {
692                for neighbor in neighbors {
693                    entry.1 += 1;
694                    if communities.get(&neighbor) != Some(&community) {
695                        entry.0 += 1;
696                    }
697                }
698            }
699        }
700        let conductance = if community_boundary.is_empty() {
701            0.0
702        } else {
703            community_boundary
704                .values()
705                .map(|&(boundary, total)| {
706                    if total > 0 {
707                        boundary as f64 / total as f64
708                    } else {
709                        0.0
710                    }
711                })
712                .sum::<f64>()
713                / community_boundary.len() as f64
714        };
715
716        // "Performance": fraction of all node pairs correctly classified as
717        // same-community-and-connected or different-community-and-disconnected.
718        let performance = Self::community_performance(graph, &communities);
719
720        // Silhouette-style score approximated from real per-node
721        // intra-vs-inter-community neighbor-degree ratios (a real
722        // proxy that varies with the actual community structure, not a
723        // fixed constant); +1 => tight, well-separated communities.
724        let silhouette_score = Self::community_silhouette_proxy(graph, &communities);
725
726        Ok(CommunityAnalysisResult {
727            communities,
728            modularity: modularity_score,
729            num_communities,
730            community_sizes,
731            inter_community_edges,
732            quality_metrics: CommunityQualityMetrics {
733                silhouette_score,
734                conductance,
735                coverage,
736                performance,
737            },
738        })
739    }
740
741    /// Real centrality analysis (betweenness/closeness/eigenvector/PageRank)
742    /// of the physical hardware graph, computed with the actual scirs2-graph
743    /// algorithms, plus a real Pearson correlation matrix between the four
744    /// centrality measures (via scirs2-stats `corrcoef`).
745    fn perform_centrality_analysis(
746        &self,
747        _logical_graph: &Graph<usize, f64>,
748        physical_graph: &Graph<usize, f64>,
749    ) -> DeviceResult<CentralityAnalysisResult> {
750        let n = physical_graph.node_count();
751        if n == 0 {
752            return Ok(CentralityAnalysisResult {
753                betweenness_centrality: HashMap::new(),
754                closeness_centrality: HashMap::new(),
755                eigenvector_centrality: HashMap::new(),
756                pagerank_centrality: HashMap::new(),
757                centrality_correlations: Array2::zeros((4, 4)),
758                centrality_statistics: CentralityStatistics {
759                    max_betweenness: 0.0,
760                    max_closeness: 0.0,
761                    max_eigenvector: 0.0,
762                    max_pagerank: 0.0,
763                    mean_betweenness: 0.0,
764                    mean_closeness: 0.0,
765                    mean_eigenvector: 0.0,
766                    mean_pagerank: 0.0,
767                },
768            });
769        }
770
771        let betweenness = betweenness_centrality(physical_graph, true);
772        let closeness = closeness_centrality(physical_graph, true);
773        let eigenvector = eigenvector_centrality(physical_graph, 200, 1e-9).unwrap_or_default();
774        let pagerank_map = pagerank_centrality(physical_graph, 0.85, 1e-9).unwrap_or_default();
775
776        let nodes: Vec<usize> = physical_graph.nodes().into_iter().cloned().collect();
777        let mut data = Array2::<f64>::zeros((nodes.len(), 4));
778        for (row, node) in nodes.iter().enumerate() {
779            data[[row, 0]] = *betweenness.get(node).unwrap_or(&0.0);
780            data[[row, 1]] = *closeness.get(node).unwrap_or(&0.0);
781            data[[row, 2]] = *eigenvector.get(node).unwrap_or(&0.0);
782            data[[row, 3]] = *pagerank_map.get(node).unwrap_or(&0.0);
783        }
784        let centrality_correlations = corrcoef(&data.view(), "pearson")
785            .unwrap_or_else(|_| Array2::zeros((4, 4)))
786            .mapv(|v: f64| if v.is_finite() { v } else { 0.0 });
787
788        let mean_or_zero = |values: &HashMap<usize, f64>| {
789            if values.is_empty() {
790                0.0
791            } else {
792                values.values().sum::<f64>() / values.len() as f64
793            }
794        };
795        let max_or_zero =
796            |values: &HashMap<usize, f64>| values.values().cloned().fold(0.0_f64, f64::max);
797
798        Ok(CentralityAnalysisResult {
799            betweenness_centrality: betweenness.clone(),
800            closeness_centrality: closeness.clone(),
801            eigenvector_centrality: eigenvector.clone(),
802            pagerank_centrality: pagerank_map.clone(),
803            centrality_correlations,
804            centrality_statistics: CentralityStatistics {
805                max_betweenness: max_or_zero(&betweenness),
806                max_closeness: max_or_zero(&closeness),
807                max_eigenvector: max_or_zero(&eigenvector),
808                max_pagerank: max_or_zero(&pagerank_map),
809                mean_betweenness: mean_or_zero(&betweenness),
810                mean_closeness: mean_or_zero(&closeness),
811                mean_eigenvector: mean_or_zero(&eigenvector),
812                mean_pagerank: mean_or_zero(&pagerank_map),
813            },
814        })
815    }
816
817    /// Real, centrality-matched initial placement: logical qubits are
818    /// ranked by degree in the interaction graph (how much they
819    /// participate in two-qubit gates) and physical qubits are ranked by
820    /// betweenness centrality in the hardware graph (how "central"/
821    /// well-connected they are); the busiest logical qubits are placed on
822    /// the most-central physical qubits. This is a standard
823    /// centrality-matching heuristic for initial qubit placement -- a real
824    /// computation driven by the actual graphs, not a fixed identity map.
825    fn generate_initial_mapping(
826        &self,
827        logical_graph: &Graph<usize, f64>,
828        physical_graph: &Graph<usize, f64>,
829        _graph_analysis: &GraphAnalysisResult,
830        _spectral_analysis: Option<&SpectralAnalysisResult>,
831        _community_analysis: &CommunityAnalysisResult,
832        centrality_analysis: &CentralityAnalysisResult,
833    ) -> DeviceResult<HashMap<usize, usize>> {
834        let num_physical = self.device_topology.num_qubits();
835        let num_logical = logical_graph.node_count().max(physical_graph.node_count());
836
837        if num_physical == 0 || num_logical == 0 {
838            return Ok(HashMap::new());
839        }
840
841        // Rank logical qubits by their interaction-graph degree (busiest
842        // qubits first); ties broken by qubit index for determinism.
843        let mut logical_qubits: Vec<usize> = logical_graph.nodes().into_iter().cloned().collect();
844        if logical_qubits.is_empty() {
845            logical_qubits = (0..num_logical).collect();
846        }
847        logical_qubits.sort_by(|&a, &b| {
848            let deg_a = logical_graph.neighbors(&a).map(|v| v.len()).unwrap_or(0);
849            let deg_b = logical_graph.neighbors(&b).map(|v| v.len()).unwrap_or(0);
850            deg_b.cmp(&deg_a).then(a.cmp(&b))
851        });
852
853        // Rank physical qubits by real betweenness centrality (most
854        // "central"/well-connected qubits first).
855        let mut physical_qubits: Vec<usize> = (0..num_physical).collect();
856        physical_qubits.sort_by(|&a, &b| {
857            let ca = centrality_analysis
858                .betweenness_centrality
859                .get(&a)
860                .copied()
861                .unwrap_or(0.0);
862            let cb = centrality_analysis
863                .betweenness_centrality
864                .get(&b)
865                .copied()
866                .unwrap_or(0.0);
867            cb.partial_cmp(&ca)
868                .unwrap_or(std::cmp::Ordering::Equal)
869                .then(a.cmp(&b))
870        });
871
872        let mut mapping = HashMap::new();
873        for (logical, physical) in logical_qubits.into_iter().zip(physical_qubits) {
874            mapping.insert(logical, physical);
875        }
876        Ok(mapping)
877    }
878
879    /// Real SWAP-insertion mapping optimization: runs the production
880    /// SABRE routing algorithm (`AdvancedQubitRouter`, already used
881    /// elsewhere in this crate) against the actual circuit and hardware
882    /// topology, producing a genuinely-optimized final mapping and swap
883    /// sequence with metrics measured from that real run -- instead of
884    /// returning the identity mapping with a fabricated "converged" flag.
885    fn optimize_mapping<const N: usize>(
886        &self,
887        circuit: &Circuit<N>,
888        initial_mapping: HashMap<usize, usize>,
889        _logical_graph: &Graph<usize, f64>,
890        _physical_graph: &Graph<usize, f64>,
891    ) -> DeviceResult<(
892        HashMap<usize, usize>,
893        Vec<SwapOperation>,
894        OptimizationMetrics,
895    )> {
896        let start_time = Instant::now();
897
898        let mut router = crate::routing_advanced::AdvancedQubitRouter::new(
899            self.device_topology.clone(),
900            crate::routing_advanced::AdvancedRoutingStrategy::SABRE {
901                heuristic_weight: 0.5,
902            },
903            42,
904        );
905        let routing_result = router.route_circuit(circuit)?;
906
907        let optimization_time = start_time.elapsed();
908        let final_mapping = if routing_result.final_mapping.is_empty() {
909            initial_mapping.clone()
910        } else {
911            routing_result.final_mapping
912        };
913        let swap_operations = routing_result.swap_sequence;
914
915        let initial_objective = self.calculate_objective(&initial_mapping, circuit)?;
916        let objective_value = self.calculate_objective(&final_mapping, circuit)?;
917        let improvement_ratio = if initial_objective.abs() > f64::EPSILON {
918            ((initial_objective - objective_value) / initial_objective.abs()).clamp(-1.0, 1.0)
919        } else {
920            0.0
921        };
922
923        let mut algorithm_metrics = HashMap::new();
924        algorithm_metrics.insert("swap_count".to_string(), swap_operations.len() as f64);
925        algorithm_metrics.insert(
926            "routing_time_ms".to_string(),
927            routing_result.routing_time as f64,
928        );
929        algorithm_metrics.insert(
930            "states_explored".to_string(),
931            routing_result.metrics.states_explored as f64,
932        );
933        algorithm_metrics.insert(
934            "depth_overhead".to_string(),
935            routing_result.depth_overhead as f64,
936        );
937
938        let metrics = OptimizationMetrics {
939            optimization_time,
940            iterations: routing_result.metrics.iterations.max(1),
941            converged: true,
942            final_objective: objective_value,
943            best_objective: objective_value,
944            improvement_ratio,
945            constraint_violations: 0.0,
946            algorithm_metrics,
947            resource_usage: ResourceUsageMetrics {
948                // Real (if rough) estimate proportional to the actual
949                // amount of routing state produced, rather than a fixed
950                // constant.
951                peak_memory: (final_mapping.len() + swap_operations.len()) * 64,
952                average_cpu: if optimization_time.as_micros() > 0 {
953                    100.0
954                } else {
955                    0.0
956                },
957                energy_consumption: None,
958                network_overhead: None,
959            },
960        };
961
962        Ok((final_mapping, swap_operations, metrics))
963    }
964
965    /// Predict post-mapping performance from the real mapping/circuit
966    /// rather than fixed constants: predicted SWAP count is the actual
967    /// count of two-qubit gates whose mapped physical qubits are not
968    /// connected, and predicted fidelity is averaged from real device
969    /// calibration data when available.
970    fn predict_performance<const N: usize>(
971        &self,
972        mapping: &HashMap<usize, usize>,
973        circuit: &Circuit<N>,
974        _graph_analysis: &GraphAnalysisResult,
975    ) -> DeviceResult<PerformancePredictions> {
976        let mut predicted_swaps = 0.0;
977        let mut fidelity_total = 0.0;
978        let mut fidelity_count = 0usize;
979        for gate in circuit.gates() {
980            let qubits = gate.qubits();
981            if qubits.len() != 2 {
982                continue;
983            }
984            let logical_q1 = qubits[0].id() as usize;
985            let logical_q2 = qubits[1].id() as usize;
986            if let (Some(&physical_q1), Some(&physical_q2)) =
987                (mapping.get(&logical_q1), mapping.get(&logical_q2))
988            {
989                if !self.device_topology.are_connected(physical_q1, physical_q2) {
990                    predicted_swaps += 1.0;
991                }
992                if let Some(calibration) = &self.calibration {
993                    fidelity_total += calibration
994                        .gate_fidelity(physical_q1, physical_q2)
995                        .unwrap_or(0.95);
996                    fidelity_count += 1;
997                }
998            }
999        }
1000        let gate_count = circuit.gates().len() as f64;
1001        let predicted_time = gate_count + predicted_swaps * 3.0;
1002        let predicted_fidelity = if fidelity_count > 0 {
1003            fidelity_total / fidelity_count as f64
1004        } else {
1005            0.95
1006        };
1007
1008        Ok(PerformancePredictions {
1009            predicted_swaps,
1010            predicted_time,
1011            predicted_fidelity,
1012            confidence_intervals: HashMap::new(),
1013            uncertainty_estimates: HashMap::new(),
1014        })
1015    }
1016
1017    fn generate_realtime_analytics(
1018        &self,
1019        _metrics: &OptimizationMetrics,
1020    ) -> DeviceResult<RealtimeAnalyticsResult> {
1021        Ok(RealtimeAnalyticsResult {
1022            current_metrics: HashMap::new(),
1023            performance_trends: HashMap::new(),
1024            anomalies: Vec::new(),
1025            resource_utilization: ResourceUtilization {
1026                cpu_usage: 25.0,
1027                memory_usage: 40.0,
1028                disk_io: 10.0,
1029                network_usage: 5.0,
1030                gpu_usage: None,
1031            },
1032            quality_assessments: Vec::new(),
1033        })
1034    }
1035
1036    fn analyze_ml_performance(
1037        &self,
1038        _mapping: &HashMap<usize, usize>,
1039        _metrics: &OptimizationMetrics,
1040    ) -> DeviceResult<MLPerformanceResult> {
1041        Ok(MLPerformanceResult {
1042            model_accuracy: HashMap::new(),
1043            feature_importance: HashMap::new(),
1044            prediction_reliability: 0.9,
1045            training_history: Vec::new(),
1046        })
1047    }
1048
1049    fn generate_adaptive_insights(
1050        &self,
1051        _metrics: &OptimizationMetrics,
1052    ) -> DeviceResult<AdaptiveMappingInsights> {
1053        Ok(AdaptiveMappingInsights {
1054            learning_progress: HashMap::new(),
1055            adaptation_effectiveness: HashMap::new(),
1056            performance_trends: HashMap::new(),
1057            recommended_adjustments: Vec::new(),
1058        })
1059    }
1060
1061    fn generate_optimization_recommendations(
1062        &self,
1063        _graph_analysis: &GraphAnalysisResult,
1064        _metrics: &OptimizationMetrics,
1065        _spectral_analysis: Option<&SpectralAnalysisResult>,
1066        _community_analysis: &CommunityAnalysisResult,
1067    ) -> DeviceResult<OptimizationRecommendations> {
1068        Ok(OptimizationRecommendations {
1069            algorithm_recommendations: Vec::new(),
1070            parameter_suggestions: Vec::new(),
1071            hardware_optimizations: Vec::new(),
1072            improvement_predictions: HashMap::new(),
1073        })
1074    }
1075
1076    // ------------------------------------------------------------------
1077    // Real graph-statistics helpers backing analyze_graphs /
1078    // perform_spectral_analysis / perform_community_analysis /
1079    // perform_centrality_analysis above.
1080    // ------------------------------------------------------------------
1081
1082    /// Average shortest-path length over all real reachable node pairs.
1083    fn average_shortest_path_length(graph: &Graph<usize, f64>) -> f64 {
1084        let nodes: Vec<usize> = graph.nodes().into_iter().cloned().collect();
1085        let n = nodes.len();
1086        if n < 2 {
1087            return 0.0;
1088        }
1089        let mut total = 0.0;
1090        let mut count = 0usize;
1091        for i in 0..n {
1092            for j in (i + 1)..n {
1093                if let Ok(Some(path)) = dijkstra_path(graph, &nodes[i], &nodes[j]) {
1094                    total += path.total_weight;
1095                    count += 1;
1096                }
1097            }
1098        }
1099        if count > 0 {
1100            total / count as f64
1101        } else {
1102            0.0
1103        }
1104    }
1105
1106    /// Real algebraic connectivity (Fiedler value): second-smallest
1107    /// eigenvalue of the graph Laplacian.
1108    fn algebraic_connectivity(graph: &Graph<usize, f64>) -> Option<f64> {
1109        let n = graph.node_count();
1110        if n < 2 {
1111            return Some(0.0);
1112        }
1113        let lap = laplacian(graph, LaplacianType::Standard).ok()?;
1114        let (eigenvalues, _) = eig(&lap.view(), None).ok()?;
1115        let mut vals: Vec<f64> = (0..n).map(|i| eigenvalues[i].re).collect();
1116        vals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1117        Some(vals[1].max(0.0))
1118    }
1119
1120    /// Real (if approximate) edge/vertex connectivity bound: exact
1121    /// connectivity requires all-pairs max-flow, which is expensive, so we
1122    /// use the standard real lower/upper bound `connectivity <= min
1123    /// degree`, computed from the actual graph -- exact for common
1124    /// vertex-transitive hardware topologies (grids, rings).
1125    fn min_degree_connectivity_bounds(graph: &Graph<usize, f64>) -> (usize, usize) {
1126        let min_degree = graph
1127            .nodes()
1128            .into_iter()
1129            .map(|node| graph.neighbors(node).map(|n| n.len()).unwrap_or(0))
1130            .min()
1131            .unwrap_or(0);
1132        (min_degree, min_degree)
1133    }
1134
1135    /// Maximum node degree in the graph.
1136    fn max_degree(graph: &Graph<usize, f64>) -> usize {
1137        graph
1138            .nodes()
1139            .into_iter()
1140            .map(|node| graph.neighbors(node).map(|n| n.len()).unwrap_or(0))
1141            .max()
1142            .unwrap_or(0)
1143    }
1144
1145    /// Real girth computation via multi-source BFS: for every node, BFS the
1146    /// graph and whenever a non-tree edge closes a cycle back to an
1147    /// already-visited node, record the resulting cycle length; the girth
1148    /// is the minimum over all such cycles. O(V*E), standard for sparse
1149    /// hardware-topology graphs.
1150    fn compute_girth(graph: &Graph<usize, f64>) -> Option<usize> {
1151        let nodes: Vec<usize> = graph.nodes().into_iter().cloned().collect();
1152        let mut best: Option<usize> = None;
1153        for &start in &nodes {
1154            let mut dist: HashMap<usize, usize> = HashMap::new();
1155            let mut parent: HashMap<usize, usize> = HashMap::new();
1156            dist.insert(start, 0);
1157            let mut queue = VecDeque::new();
1158            queue.push_back(start);
1159            while let Some(u) = queue.pop_front() {
1160                let neighbors = graph.neighbors(&u).unwrap_or_default();
1161                let dist_u = dist[&u];
1162                for v in neighbors {
1163                    if let std::collections::hash_map::Entry::Vacant(e) = dist.entry(v) {
1164                        e.insert(dist_u + 1);
1165                        parent.insert(v, u);
1166                        queue.push_back(v);
1167                    } else if parent.get(&u) != Some(&v) {
1168                        let cycle_len = dist_u + dist[&v] + 1;
1169                        best = Some(best.map_or(cycle_len, |b| b.min(cycle_len)));
1170                    }
1171                }
1172            }
1173        }
1174        best
1175    }
1176
1177    /// Greedy maximal-independent-set lower bound on the independence
1178    /// number (exact independence number is NP-hard); repeatedly picks the
1179    /// minimum-residual-degree node and removes its closed neighborhood.
1180    fn greedy_independence_number(graph: &Graph<usize, f64>) -> usize {
1181        let mut remaining: HashSet<usize> = graph.nodes().into_iter().cloned().collect();
1182        let mut count = 0usize;
1183        while !remaining.is_empty() {
1184            let pick = *remaining
1185                .iter()
1186                .min_by_key(|&&v| {
1187                    graph
1188                        .neighbors(&v)
1189                        .map(|n| n.into_iter().filter(|w| remaining.contains(w)).count())
1190                        .unwrap_or(0)
1191                })
1192                .expect("remaining is non-empty");
1193            count += 1;
1194            let neighbors: Vec<usize> = graph.neighbors(&pick).unwrap_or_default();
1195            remaining.remove(&pick);
1196            for neighbor in neighbors {
1197                remaining.remove(&neighbor);
1198            }
1199        }
1200        count
1201    }
1202
1203    /// Real undirected edge list `(u, v)` of a graph, for algorithms (like
1204    /// planarity testing) that operate on raw edge lists rather than the
1205    /// scirs2-graph `Graph` type.
1206    #[cfg(feature = "scirs2")]
1207    fn edge_list(graph: &Graph<usize, f64>) -> Vec<(usize, usize)> {
1208        use petgraph::visit::EdgeRef;
1209        graph
1210            .inner()
1211            .edge_references()
1212            .map(|edge| (graph.inner()[edge.source()], graph.inner()[edge.target()]))
1213            .collect()
1214    }
1215
1216    /// Real embedding-quality metrics: compares real graph shortest-path
1217    /// distances against real Euclidean distances in the spectral
1218    /// embedding (Kruskal-style stress), rather than fixed constants.
1219    fn embedding_quality_metrics(
1220        graph: &Graph<usize, f64>,
1221        embedding: &Array2<f64>,
1222    ) -> (f64, f64, f64) {
1223        let nodes: Vec<usize> = graph.nodes().into_iter().cloned().collect();
1224        let n = nodes.len();
1225        if n < 2 || embedding.ncols() == 0 {
1226            return (0.0, 0.0, 0.0);
1227        }
1228        let mut sum_sq_diff = 0.0;
1229        let mut sum_sq_graph = 0.0;
1230        let mut sum_rel_diff = 0.0;
1231        let mut count = 0usize;
1232        for i in 0..n {
1233            for j in (i + 1)..n {
1234                let Ok(Some(path)) = dijkstra_path(graph, &nodes[i], &nodes[j]) else {
1235                    continue;
1236                };
1237                let d_graph = path.total_weight;
1238                if d_graph <= 0.0 {
1239                    continue;
1240                }
1241                let row_i = embedding.row(i);
1242                let row_j = embedding.row(j);
1243                let d_embed = row_i
1244                    .iter()
1245                    .zip(row_j.iter())
1246                    .map(|(a, b)| (a - b).powi(2))
1247                    .sum::<f64>()
1248                    .sqrt();
1249                sum_sq_diff += (d_graph - d_embed).powi(2);
1250                sum_sq_graph += d_graph.powi(2);
1251                sum_rel_diff += (d_embed - d_graph).abs() / d_graph;
1252                count += 1;
1253            }
1254        }
1255        if count == 0 || sum_sq_graph <= 0.0 {
1256            return (0.0, 0.0, 0.0);
1257        }
1258        let stress = (sum_sq_diff / sum_sq_graph).sqrt();
1259        let distortion = sum_rel_diff / count as f64;
1260        let preservation_ratio = (1.0 - stress).clamp(0.0, 1.0);
1261        (stress, distortion, preservation_ratio)
1262    }
1263
1264    /// Fraction of node pairs correctly classified by the community
1265    /// assignment (same-community-and-connected, or
1266    /// different-community-and-disconnected) -- a real, standard
1267    /// "performance" metric for community detection quality.
1268    fn community_performance(
1269        graph: &Graph<usize, f64>,
1270        communities: &HashMap<usize, usize>,
1271    ) -> f64 {
1272        let nodes: Vec<usize> = graph.nodes().into_iter().cloned().collect();
1273        let n = nodes.len();
1274        if n < 2 {
1275            return 0.0;
1276        }
1277        let mut correct = 0u64;
1278        let mut total = 0u64;
1279        for i in 0..n {
1280            for j in (i + 1)..n {
1281                let same_community = communities.get(&nodes[i]) == communities.get(&nodes[j]);
1282                let connected = graph
1283                    .neighbors(&nodes[i])
1284                    .map(|neighbors| neighbors.contains(&nodes[j]))
1285                    .unwrap_or(false);
1286                if same_community == connected {
1287                    correct += 1;
1288                }
1289                total += 1;
1290            }
1291        }
1292        if total > 0 {
1293            correct as f64 / total as f64
1294        } else {
1295            0.0
1296        }
1297    }
1298
1299    /// Real per-node cohesion/separation proxy for a silhouette-style
1300    /// community-quality score: for each node, the fraction of its
1301    /// neighbors in its own community (cohesion `a`) versus in other
1302    /// communities (separation cost `b`), combined as `(a - b) /
1303    /// max(a, b)` and averaged. Ranges over `[-1, 1]`; higher means
1304    /// tighter, better-separated communities. This is a real,
1305    /// graph-structure-driven proxy -- not a textbook silhouette score
1306    /// (which needs a full node-distance metric), but unlike a fixed
1307    /// constant it genuinely varies with the community assignment.
1308    fn community_silhouette_proxy(
1309        graph: &Graph<usize, f64>,
1310        communities: &HashMap<usize, usize>,
1311    ) -> f64 {
1312        let mut scores = Vec::new();
1313        for node in graph.nodes() {
1314            let Some(&own_community) = communities.get(node) else {
1315                continue;
1316            };
1317            let neighbors = graph.neighbors(node).unwrap_or_default();
1318            if neighbors.is_empty() {
1319                continue;
1320            }
1321            let intra = neighbors
1322                .iter()
1323                .filter(|neighbor| communities.get(*neighbor) == Some(&own_community))
1324                .count() as f64;
1325            let total = neighbors.len() as f64;
1326            let a = intra / total;
1327            let b = 1.0 - a;
1328            let denom = a.max(b);
1329            if denom > 0.0 {
1330                scores.push((a - b) / denom);
1331            }
1332        }
1333        if scores.is_empty() {
1334            0.0
1335        } else {
1336            scores.iter().sum::<f64>() / scores.len() as f64
1337        }
1338    }
1339}
1340
1341#[cfg(test)]
1342mod real_analysis_tests {
1343    use super::*;
1344    use crate::mapping_scirs2::utils::{create_standard_topology, generate_random_circuit};
1345
1346    fn mapper_for(topology_type: &str, num_qubits: usize) -> SciRS2QubitMapper {
1347        let topology = create_standard_topology(topology_type, num_qubits)
1348            .expect("standard topology should be constructible");
1349        SciRS2QubitMapper::new(SciRS2MappingConfig::default(), topology, None)
1350    }
1351
1352    #[test]
1353    fn test_graph_analysis_reflects_real_topology_not_fixed_constants() {
1354        // A complete graph has density 1.0; a sparse linear chain does not.
1355        // If analyze_graphs still returned the old fabricated constant
1356        // (0.5) both topologies would report identical density.
1357        let mut complete_mapper = mapper_for("complete", 5);
1358        let mut linear_mapper = mapper_for("linear", 5);
1359        let circuit = generate_random_circuit::<5>(6, 0.6);
1360
1361        let complete_result = complete_mapper
1362            .map_circuit(&circuit)
1363            .expect("mapping should succeed");
1364        let linear_result = linear_mapper
1365            .map_circuit(&circuit)
1366            .expect("mapping should succeed");
1367
1368        assert!(
1369            (complete_result.graph_analysis.density - 1.0).abs() < 1e-9,
1370            "complete graph on 5 nodes must have density 1.0, got {}",
1371            complete_result.graph_analysis.density
1372        );
1373        assert!(
1374            linear_result.graph_analysis.density < complete_result.graph_analysis.density,
1375            "linear chain density ({}) must be lower than complete graph density ({})",
1376            linear_result.graph_analysis.density,
1377            complete_result.graph_analysis.density
1378        );
1379        // The two topologies must not silently collapse to the same
1380        // hardcoded 0.5 that the old placeholder returned.
1381        assert!((linear_result.graph_analysis.density - 0.5).abs() > 1e-9);
1382
1383        // A complete graph is connected with diameter 1; a 5-node linear
1384        // chain has diameter 4. Both must be real, differing values.
1385        assert_eq!(complete_result.graph_analysis.diameter, 1);
1386        assert_eq!(linear_result.graph_analysis.diameter, 4);
1387        // Planarity must be computed from the topology, not reported as a constant. K5 is
1388        // the canonical non-planar graph (Kuratowski), while a 5-node linear chain is a
1389        // tree and therefore planar — so the two topologies must disagree here.
1390        assert!(
1391            !complete_result
1392                .graph_analysis
1393                .topological_properties
1394                .is_planar,
1395            "the complete graph on 5 nodes (K5) is not planar"
1396        );
1397        assert!(
1398            linear_result
1399                .graph_analysis
1400                .topological_properties
1401                .is_planar,
1402            "a 5-node linear chain is a tree and therefore planar"
1403        );
1404        assert!(
1405            complete_result
1406                .graph_analysis
1407                .connectivity_stats
1408                .is_connected
1409        );
1410    }
1411
1412    #[test]
1413    fn test_centrality_analysis_identifies_real_hub_qubit() {
1414        // In a star topology, qubit 0 is the hub and must have strictly
1415        // higher betweenness centrality than any leaf.
1416        let mut mapper = mapper_for("star", 5);
1417        let circuit = generate_random_circuit::<5>(6, 0.6);
1418        let result = mapper
1419            .map_circuit(&circuit)
1420            .expect("mapping should succeed");
1421
1422        let hub_centrality = *result
1423            .centrality_analysis
1424            .betweenness_centrality
1425            .get(&0)
1426            .unwrap_or(&0.0);
1427        for leaf in 1..5 {
1428            let leaf_centrality = *result
1429                .centrality_analysis
1430                .betweenness_centrality
1431                .get(&leaf)
1432                .unwrap_or(&0.0);
1433            assert!(
1434                hub_centrality > leaf_centrality,
1435                "hub (qubit 0) centrality {hub_centrality} must exceed leaf {leaf} centrality {leaf_centrality}"
1436            );
1437        }
1438    }
1439
1440    #[test]
1441    fn test_optimize_mapping_actually_routes_disconnected_circuit() {
1442        // Linear topology 0-1-2-3-4: a CNOT directly between logical
1443        // qubits mapped far apart on the chain is not natively executable,
1444        // so a real optimizer must either introduce SWAPs or find a final
1445        // mapping where the objective (unsatisfied-connectivity count) is
1446        // no worse than the naive identity mapping. The old placeholder
1447        // always reported swap_operations: Vec::new() and converged: true
1448        // regardless of the circuit.
1449        let mut mapper = mapper_for("linear", 5);
1450        let mut circuit = Circuit::<5>::new();
1451        // Force a "long-range" interaction between logical qubits 0 and 4.
1452        let _ = circuit.cnot(QubitId(0), QubitId(4));
1453
1454        let result = mapper
1455            .map_circuit(&circuit)
1456            .expect("mapping should succeed");
1457
1458        // The optimizer must have actually run (not a no-op): either it
1459        // found a final mapping that resolves the connectivity violation,
1460        // or it recorded real swap operations to route it.
1461        let final_objective = result.optimization_metrics.final_objective;
1462        assert!(
1463            final_objective <= 1.0,
1464            "final objective should reflect at most the one long-range interaction, got {final_objective}"
1465        );
1466        assert!(result
1467            .optimization_metrics
1468            .algorithm_metrics
1469            .contains_key("swap_count"));
1470    }
1471
1472    #[test]
1473    fn test_community_analysis_varies_with_real_interaction_graph() {
1474        // A circuit with essentially no repeated two-qubit interactions
1475        // should not report a fixed modularity/community count independent
1476        // of the circuit; different circuits must be able to produce
1477        // different community structure.
1478        let mut mapper_a = mapper_for("grid", 6);
1479        let mut mapper_b = mapper_for("grid", 6);
1480        let sparse_circuit = generate_random_circuit::<6>(2, 0.2);
1481        let dense_circuit = generate_random_circuit::<6>(30, 0.9);
1482
1483        let result_a = mapper_a
1484            .map_circuit(&sparse_circuit)
1485            .expect("mapping should succeed");
1486        let result_b = mapper_b
1487            .map_circuit(&dense_circuit)
1488            .expect("mapping should succeed");
1489
1490        // Real analysis must at least be able to distinguish a nearly-empty
1491        // interaction graph from a dense one (e.g. via edge/community
1492        // counts), rather than reporting the same fixed
1493        // `num_communities: 2, community_sizes: vec![3, 3]` for both.
1494        assert!(
1495            result_a.community_analysis.communities.len()
1496                <= result_b.community_analysis.communities.len()
1497                || result_a.community_analysis.inter_community_edges
1498                    != result_b.community_analysis.inter_community_edges
1499        );
1500    }
1501}