Skip to main content

quantrs2_circuit/
scirs2_similarity.rs

1//! Circuit similarity metrics using `SciRS2`
2//!
3//! This module implements sophisticated quantum circuit similarity and distance metrics
4//! leveraging `SciRS2`'s graph algorithms, numerical analysis, and machine learning capabilities.
5
6use crate::builder::Circuit;
7use crate::dag::{circuit_to_dag, CircuitDag};
8use quantrs2_core::{
9    error::{QuantRS2Error, QuantRS2Result},
10    gate::GateOp,
11    qubit::QubitId,
12};
13use scirs2_core::Complex64;
14use serde::{Deserialize, Serialize};
15use std::collections::{HashMap, HashSet};
16use std::f64::consts::PI;
17use std::sync::Arc;
18
19// Placeholder types representing SciRS2 graph and ML interface
20// In the real implementation, these would be imported from SciRS2
21
22/// Graph representation for `SciRS2` integration
23#[derive(Debug, Clone)]
24pub struct SciRS2Graph {
25    /// Node identifiers
26    pub nodes: Vec<usize>,
27    /// Edge list (source, target, weight)
28    pub edges: Vec<(usize, usize, f64)>,
29    /// Node attributes
30    pub node_attributes: HashMap<usize, HashMap<String, String>>,
31    /// Edge attributes
32    pub edge_attributes: HashMap<(usize, usize), HashMap<String, f64>>,
33}
34
35/// Graph similarity algorithms available in `SciRS2`
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum GraphSimilarityAlgorithm {
38    /// Graph edit distance
39    GraphEditDistance,
40    /// Spectral similarity based on eigenvalues
41    SpectralSimilarity,
42    /// Graph kernel methods
43    GraphKernel { kernel_type: GraphKernelType },
44    /// Network alignment
45    NetworkAlignment,
46    /// Subgraph isomorphism
47    SubgraphIsomorphism,
48    /// Graph neural network embeddings
49    GraphNeuralNetwork { embedding_dim: usize },
50}
51
52/// Graph kernel types
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub enum GraphKernelType {
55    /// Random walk kernel
56    RandomWalk { steps: usize },
57    /// Weisfeiler-Lehman kernel
58    WeisfeilerLehman { iterations: usize },
59    /// Shortest path kernel
60    ShortestPath,
61    /// Graphlet kernel
62    Graphlet { size: usize },
63}
64
65/// Circuit similarity metrics
66#[derive(Debug, Clone)]
67pub struct CircuitSimilarityMetrics {
68    /// Structural similarity (0.0 to 1.0)
69    pub structural_similarity: f64,
70    /// Functional similarity (0.0 to 1.0)
71    pub functional_similarity: f64,
72    /// Gate sequence similarity (0.0 to 1.0)
73    pub sequence_similarity: f64,
74    /// Topological similarity (0.0 to 1.0)
75    pub topological_similarity: f64,
76    /// Overall similarity score (0.0 to 1.0)
77    pub overall_similarity: f64,
78    /// Detailed breakdown by metric type
79    pub detailed_metrics: HashMap<String, f64>,
80}
81
82/// Circuit distance measures
83#[derive(Debug, Clone)]
84pub struct CircuitDistanceMetrics {
85    /// Edit distance (minimum operations to transform one circuit to another)
86    pub edit_distance: usize,
87    /// Normalized edit distance (0.0 to 1.0)
88    pub normalized_edit_distance: f64,
89    /// Wasserstein distance between gate distributions
90    pub wasserstein_distance: f64,
91    /// Hausdorff distance between circuit embeddings
92    pub hausdorff_distance: f64,
93    /// Earth mover's distance
94    pub earth_movers_distance: f64,
95    /// Quantum process fidelity distance
96    pub process_fidelity_distance: f64,
97}
98
99/// Configuration for similarity computation
100#[derive(Debug, Clone)]
101pub struct SimilarityConfig {
102    /// Algorithms to use for comparison
103    pub algorithms: Vec<SimilarityAlgorithm>,
104    /// Weight for different similarity aspects
105    pub weights: SimilarityWeights,
106    /// Tolerance for numerical comparisons
107    pub tolerance: f64,
108    /// Whether to normalize results
109    pub normalize: bool,
110    /// Cache intermediate results
111    pub cache_results: bool,
112    /// Use parallel computation
113    pub parallel: bool,
114}
115
116/// Similarity computation algorithms
117#[derive(Debug, Clone, PartialEq, Eq)]
118pub enum SimilarityAlgorithm {
119    /// Gate-level comparison
120    GateLevel,
121    /// DAG structure comparison
122    DAGStructure,
123    /// Unitary matrix comparison
124    UnitaryMatrix,
125    /// Graph-based comparison
126    GraphBased { algorithm: GraphSimilarityAlgorithm },
127    /// Statistical comparison
128    Statistical,
129    /// Machine learning embeddings
130    MLEmbeddings { model_type: MLModelType },
131}
132
133/// Machine learning model types for embeddings
134#[derive(Debug, Clone, PartialEq, Eq)]
135pub enum MLModelType {
136    /// Variational autoencoder
137    VAE { latent_dim: usize },
138    /// Graph convolutional network
139    GCN { hidden_dims: Vec<usize> },
140    /// Transformer model
141    Transformer { num_heads: usize, num_layers: usize },
142    /// Pre-trained circuit embedding model
143    PreTrained { model_name: String },
144}
145
146/// Weights for combining different similarity measures
147#[derive(Debug, Clone)]
148pub struct SimilarityWeights {
149    /// Weight for structural similarity
150    pub structural: f64,
151    /// Weight for functional similarity
152    pub functional: f64,
153    /// Weight for gate sequence similarity
154    pub sequence: f64,
155    /// Weight for topological similarity
156    pub topological: f64,
157}
158
159impl Default for SimilarityWeights {
160    fn default() -> Self {
161        Self {
162            structural: 0.3,
163            functional: 0.4,
164            sequence: 0.2,
165            topological: 0.1,
166        }
167    }
168}
169
170impl Default for SimilarityConfig {
171    fn default() -> Self {
172        Self {
173            algorithms: vec![
174                SimilarityAlgorithm::GateLevel,
175                SimilarityAlgorithm::DAGStructure,
176                SimilarityAlgorithm::UnitaryMatrix,
177            ],
178            weights: SimilarityWeights::default(),
179            tolerance: 1e-12,
180            normalize: true,
181            cache_results: true,
182            parallel: false,
183        }
184    }
185}
186
187/// Circuit similarity analyzer using `SciRS2`
188pub struct CircuitSimilarityAnalyzer {
189    /// Configuration for similarity computation
190    config: SimilarityConfig,
191    /// Cache for computed similarities
192    similarity_cache: HashMap<(String, String), CircuitSimilarityMetrics>,
193    /// Cache for circuit embeddings
194    embedding_cache: HashMap<String, Vec<f64>>,
195    /// Pre-computed circuit features
196    feature_cache: HashMap<String, CircuitFeatures>,
197}
198
199/// Circuit features for similarity computation
200#[derive(Debug, Clone)]
201pub struct CircuitFeatures {
202    /// Gate type histogram
203    pub gate_histogram: HashMap<String, usize>,
204    /// Circuit depth
205    pub depth: usize,
206    /// Two-qubit gate count
207    pub two_qubit_gates: usize,
208    /// Connectivity pattern
209    pub connectivity_pattern: Vec<(usize, usize)>,
210    /// Critical path information
211    pub critical_path: Vec<String>,
212    /// Parallelism profile
213    pub parallelism_profile: Vec<usize>,
214    /// Entanglement structure
215    pub entanglement_structure: EntanglementStructure,
216}
217
218/// Entanglement structure representation
219#[derive(Debug, Clone)]
220pub struct EntanglementStructure {
221    /// Entangling gates by layer
222    pub entangling_layers: Vec<Vec<(usize, usize)>>,
223    /// Maximum entanglement width
224    pub max_entanglement_width: usize,
225    /// Entanglement graph
226    pub entanglement_graph: SciRS2Graph,
227}
228
229impl CircuitSimilarityAnalyzer {
230    /// Create a new circuit similarity analyzer
231    #[must_use]
232    pub fn new(config: SimilarityConfig) -> Self {
233        Self {
234            config,
235            similarity_cache: HashMap::new(),
236            embedding_cache: HashMap::new(),
237            feature_cache: HashMap::new(),
238        }
239    }
240
241    /// Create analyzer with default configuration
242    #[must_use]
243    pub fn with_default_config() -> Self {
244        Self::new(SimilarityConfig::default())
245    }
246
247    /// Compute comprehensive similarity between two circuits
248    pub fn compute_similarity<const N: usize, const M: usize>(
249        &mut self,
250        circuit1: &Circuit<N>,
251        circuit2: &Circuit<M>,
252    ) -> QuantRS2Result<CircuitSimilarityMetrics> {
253        // Generate unique identifiers for caching
254        let id1 = Self::generate_circuit_id(circuit1);
255        let id2 = Self::generate_circuit_id(circuit2);
256        let cache_key = if id1 < id2 { (id1, id2) } else { (id2, id1) };
257
258        // Check cache
259        if self.config.cache_results {
260            if let Some(cached) = self.similarity_cache.get(&cache_key) {
261                return Ok(cached.clone());
262            }
263        }
264
265        // Extract features
266        let features1 = self.extract_circuit_features(circuit1)?;
267        let features2 = self.extract_circuit_features(circuit2)?;
268
269        // Compute individual similarity measures
270        let mut detailed_metrics = HashMap::new();
271        let mut similarities = Vec::new();
272
273        let algorithms = self.config.algorithms.clone();
274        for algorithm in &algorithms {
275            let similarity = match algorithm {
276                SimilarityAlgorithm::GateLevel => {
277                    Self::compute_gate_level_similarity(&features1, &features2)?
278                }
279                SimilarityAlgorithm::DAGStructure => {
280                    Self::compute_dag_similarity(circuit1, circuit2)?
281                }
282                SimilarityAlgorithm::UnitaryMatrix => {
283                    Self::compute_unitary_similarity(circuit1, circuit2)?
284                }
285                SimilarityAlgorithm::GraphBased {
286                    algorithm: graph_alg,
287                } => Self::compute_graph_similarity(&features1, &features2, graph_alg)?,
288                SimilarityAlgorithm::Statistical => {
289                    Self::compute_statistical_similarity(&features1, &features2)?
290                }
291                SimilarityAlgorithm::MLEmbeddings { model_type } => {
292                    self.compute_ml_similarity(circuit1, circuit2, model_type)?
293                }
294            };
295
296            detailed_metrics.insert(format!("{algorithm:?}"), similarity);
297            similarities.push(similarity);
298        }
299
300        // Compute component similarities
301        let structural_similarity = Self::compute_structural_similarity(&features1, &features2)?;
302        let functional_similarity = Self::compute_functional_similarity(circuit1, circuit2)?;
303        let sequence_similarity = Self::compute_sequence_similarity(&features1, &features2)?;
304        let topological_similarity = Self::compute_topological_similarity(&features1, &features2)?;
305
306        // Compute overall similarity using weighted combination
307        let overall_similarity = self.config.weights.topological.mul_add(
308            topological_similarity,
309            self.config.weights.sequence.mul_add(
310                sequence_similarity,
311                self.config.weights.structural.mul_add(
312                    structural_similarity,
313                    self.config.weights.functional * functional_similarity,
314                ),
315            ),
316        );
317
318        let result = CircuitSimilarityMetrics {
319            structural_similarity,
320            functional_similarity,
321            sequence_similarity,
322            topological_similarity,
323            overall_similarity,
324            detailed_metrics,
325        };
326
327        // Cache result
328        if self.config.cache_results {
329            self.similarity_cache.insert(cache_key, result.clone());
330        }
331
332        Ok(result)
333    }
334
335    /// Compute distance metrics between circuits
336    pub fn compute_distance<const N: usize, const M: usize>(
337        &mut self,
338        circuit1: &Circuit<N>,
339        circuit2: &Circuit<M>,
340    ) -> QuantRS2Result<CircuitDistanceMetrics> {
341        let features1 = self.extract_circuit_features(circuit1)?;
342        let features2 = self.extract_circuit_features(circuit2)?;
343
344        // Compute edit distance
345        let edit_distance = Self::compute_edit_distance(&features1, &features2)?;
346        let max_gates = features1
347            .gate_histogram
348            .values()
349            .sum::<usize>()
350            .max(features2.gate_histogram.values().sum::<usize>());
351        let normalized_edit_distance = if max_gates > 0 {
352            edit_distance as f64 / max_gates as f64
353        } else {
354            0.0
355        };
356
357        // Compute other distance measures
358        let wasserstein_distance = Self::compute_wasserstein_distance(&features1, &features2)?;
359        let hausdorff_distance = Self::compute_hausdorff_distance(circuit1, circuit2)?;
360        let earth_movers_distance = Self::compute_earth_movers_distance(&features1, &features2)?;
361        let process_fidelity_distance =
362            Self::compute_process_fidelity_distance(circuit1, circuit2)?;
363
364        Ok(CircuitDistanceMetrics {
365            edit_distance,
366            normalized_edit_distance,
367            wasserstein_distance,
368            hausdorff_distance,
369            earth_movers_distance,
370            process_fidelity_distance,
371        })
372    }
373
374    /// Extract comprehensive features from a circuit
375    fn extract_circuit_features<const N: usize>(
376        &mut self,
377        circuit: &Circuit<N>,
378    ) -> QuantRS2Result<CircuitFeatures> {
379        let id = Self::generate_circuit_id(circuit);
380
381        if let Some(cached) = self.feature_cache.get(&id) {
382            return Ok(cached.clone());
383        }
384
385        let mut gate_histogram = HashMap::new();
386        let mut connectivity_pattern = Vec::new();
387        let mut critical_path = Vec::new();
388        let mut two_qubit_gates = 0;
389
390        // Analyze gates
391        for gate in circuit.gates() {
392            let gate_name = gate.name();
393            *gate_histogram.entry(gate_name.to_string()).or_insert(0) += 1;
394            critical_path.push(gate_name.to_string());
395
396            if gate.qubits().len() == 2 {
397                two_qubit_gates += 1;
398                let qubits: Vec<usize> = gate.qubits().iter().map(|q| q.id() as usize).collect();
399                connectivity_pattern.push((qubits[0], qubits[1]));
400            }
401        }
402
403        // Compute parallelism profile
404        let parallelism_profile = Self::compute_parallelism_profile(circuit)?;
405
406        // Analyze entanglement structure
407        let entanglement_structure = Self::analyze_entanglement_structure(circuit)?;
408
409        let features = CircuitFeatures {
410            gate_histogram,
411            depth: circuit.gates().len(), // Simplified depth
412            two_qubit_gates,
413            connectivity_pattern,
414            critical_path,
415            parallelism_profile,
416            entanglement_structure,
417        };
418
419        self.feature_cache.insert(id, features.clone());
420        Ok(features)
421    }
422
423    /// Compute gate-level similarity
424    fn compute_gate_level_similarity(
425        features1: &CircuitFeatures,
426        features2: &CircuitFeatures,
427    ) -> QuantRS2Result<f64> {
428        // Compare gate histograms using cosine similarity
429        let mut dot_product = 0.0;
430        let mut norm1 = 0.0;
431        let mut norm2 = 0.0;
432
433        let all_gates: HashSet<String> = features1
434            .gate_histogram
435            .keys()
436            .chain(features2.gate_histogram.keys())
437            .cloned()
438            .collect();
439
440        for gate in all_gates {
441            let count1 = *features1.gate_histogram.get(&gate).unwrap_or(&0) as f64;
442            let count2 = *features2.gate_histogram.get(&gate).unwrap_or(&0) as f64;
443
444            dot_product += count1 * count2;
445            norm1 += count1 * count1;
446            norm2 += count2 * count2;
447        }
448
449        let similarity = if norm1 > 0.0 && norm2 > 0.0 {
450            dot_product / (norm1.sqrt() * norm2.sqrt())
451        } else {
452            0.0
453        };
454
455        Ok(similarity)
456    }
457
458    /// Compute DAG structure similarity
459    fn compute_dag_similarity<const N: usize, const M: usize>(
460        circuit1: &Circuit<N>,
461        circuit2: &Circuit<M>,
462    ) -> QuantRS2Result<f64> {
463        // Convert circuits to DAGs and compare structure
464        let dag1 = circuit_to_dag(circuit1);
465        let dag2 = circuit_to_dag(circuit2);
466
467        // Compare DAG properties
468        let nodes_similarity = if dag1.nodes().len() == dag2.nodes().len() {
469            1.0
470        } else {
471            let min_nodes = dag1.nodes().len().min(dag2.nodes().len()) as f64;
472            let max_nodes = dag1.nodes().len().max(dag2.nodes().len()) as f64;
473            min_nodes / max_nodes
474        };
475
476        let edges_similarity = if dag1.edges().len() == dag2.edges().len() {
477            1.0
478        } else {
479            let min_edges = dag1.edges().len().min(dag2.edges().len()) as f64;
480            let max_edges = dag1.edges().len().max(dag2.edges().len()) as f64;
481            min_edges / max_edges
482        };
483
484        Ok(f64::midpoint(nodes_similarity, edges_similarity))
485    }
486
487    /// Compute unitary matrix similarity.
488    ///
489    /// Reconstructs both circuits' full `2^N x 2^N` unitary matrices by
490    /// simulating their action on every computational basis state (via
491    /// [`Self::simulate_circuit_from_basis_state`]) and computes the real
492    /// process (entanglement) fidelity `F_pro = |Tr(U1^† U2)|^2 / d^2`
493    /// between them — `1.0` exactly for identical unitaries (up to global
494    /// phase) and `0.0` for maximally different ones.
495    fn compute_unitary_similarity<const N: usize, const M: usize>(
496        circuit1: &Circuit<N>,
497        circuit2: &Circuit<M>,
498    ) -> QuantRS2Result<f64> {
499        if N != M {
500            // Circuits with different qubit counts have zero unitary similarity
501            return Ok(0.0);
502        }
503
504        let dimension = 1usize << N;
505        // Tr(U1^dagger U2) equals the flattened Frobenius inner product of
506        // the two matrices (summing conj(U1[row][col]) * U2[row][col] over
507        // every entry), so the columns can be accumulated one at a time
508        // without ever materializing both full dense matrices at once.
509        let mut inner_product = Complex64::new(0.0, 0.0);
510        for basis_state in 0..dimension {
511            let column1 = Self::simulate_circuit_from_basis_state(circuit1, basis_state)?;
512            let column2 = Self::simulate_circuit_from_basis_state(circuit2, basis_state)?;
513            inner_product += column1
514                .iter()
515                .zip(column2.iter())
516                .map(|(a, b)| a.conj() * b)
517                .sum::<Complex64>();
518        }
519
520        let d = dimension as f64;
521        Ok((inner_product.norm_sqr() / (d * d)).clamp(0.0, 1.0))
522    }
523
524    /// Apply a single gate's matrix to a dense state vector.
525    ///
526    /// Uses the convention that qubit index `0` is the most-significant bit
527    /// of the computational-basis index (matching the tensor-product
528    /// ordering used elsewhere in this crate's matrix tooling), and supports
529    /// gates acting on any number of qubits by summing over every
530    /// combination of the untouched qubits' bit values.
531    fn apply_gate_to_state(
532        state: &[Complex64],
533        gate: &dyn GateOp,
534        num_qubits: usize,
535    ) -> QuantRS2Result<Vec<Complex64>> {
536        let qubits = gate.qubits();
537        let touched: Vec<usize> = qubits.iter().map(|q| q.id() as usize).collect();
538        let k = touched.len();
539        let local_dim = 1usize << k;
540        let matrix = gate.matrix()?;
541        if matrix.len() != local_dim * local_dim {
542            return Err(QuantRS2Error::InvalidInput(format!(
543                "gate {} matrix has {} entries, expected {} for a {}-qubit gate",
544                gate.name(),
545                matrix.len(),
546                local_dim * local_dim,
547                k
548            )));
549        }
550
551        let dimension = state.len();
552        let mut new_state = vec![Complex64::new(0.0, 0.0); dimension];
553
554        let other: Vec<usize> = (0..num_qubits).filter(|q| !touched.contains(q)).collect();
555        let other_count = other.len();
556        let other_dim = 1usize << other_count;
557
558        // Compose a full basis index from independently chosen bit values
559        // for the "other" (untouched) qubits and the "local" (touched)
560        // qubits, using the MSB-first bit layout.
561        let compose_index = |other_bits: usize, local_bits: usize| -> usize {
562            let mut idx = 0usize;
563            for (position, &qubit) in other.iter().enumerate() {
564                let bit = (other_bits >> (other_count - 1 - position)) & 1;
565                idx |= bit << (num_qubits - 1 - qubit);
566            }
567            for (position, &qubit) in touched.iter().enumerate() {
568                let bit = (local_bits >> (k - 1 - position)) & 1;
569                idx |= bit << (num_qubits - 1 - qubit);
570            }
571            idx
572        };
573
574        for other_bits in 0..other_dim {
575            for local_in in 0..local_dim {
576                let index_in = compose_index(other_bits, local_in);
577                let amplitude_in = state[index_in];
578                if amplitude_in == Complex64::new(0.0, 0.0) {
579                    continue;
580                }
581                for local_out in 0..local_dim {
582                    let index_out = compose_index(other_bits, local_out);
583                    new_state[index_out] += matrix[local_out * local_dim + local_in] * amplitude_in;
584                }
585            }
586        }
587
588        Ok(new_state)
589    }
590
591    /// Evolve a computational basis state through every gate in `circuit`,
592    /// in order, returning the resulting dense state vector. This is the
593    /// real (exponential-cost, exact) circuit simulation used by
594    /// [`Self::compute_functional_similarity`] and
595    /// [`Self::compute_unitary_similarity`].
596    fn simulate_circuit_from_basis_state<const N: usize>(
597        circuit: &Circuit<N>,
598        initial_basis_state: usize,
599    ) -> QuantRS2Result<Vec<Complex64>> {
600        let dimension = 1usize << N;
601        let mut state = vec![Complex64::new(0.0, 0.0); dimension];
602        state[initial_basis_state.min(dimension - 1)] = Complex64::new(1.0, 0.0);
603
604        for gate in circuit.gates() {
605            state = Self::apply_gate_to_state(&state, gate.as_ref(), N)?;
606        }
607
608        Ok(state)
609    }
610
611    /// Compute graph-based similarity
612    fn compute_graph_similarity(
613        features1: &CircuitFeatures,
614        features2: &CircuitFeatures,
615        algorithm: &GraphSimilarityAlgorithm,
616    ) -> QuantRS2Result<f64> {
617        match algorithm {
618            GraphSimilarityAlgorithm::GraphEditDistance => Self::compute_graph_edit_distance(
619                &features1.entanglement_structure.entanglement_graph,
620                &features2.entanglement_structure.entanglement_graph,
621            ),
622            GraphSimilarityAlgorithm::SpectralSimilarity => Self::compute_spectral_similarity(
623                &features1.entanglement_structure.entanglement_graph,
624                &features2.entanglement_structure.entanglement_graph,
625            ),
626            _ => {
627                // Other graph algorithms would be implemented
628                Ok(0.5) // Placeholder
629            }
630        }
631    }
632
633    /// Compute statistical similarity
634    fn compute_statistical_similarity(
635        features1: &CircuitFeatures,
636        features2: &CircuitFeatures,
637    ) -> QuantRS2Result<f64> {
638        // Compare statistical properties of circuits with division by zero protection
639        let max_depth = features1.depth.max(features2.depth);
640        let depth_similarity = if max_depth > 0 {
641            1.0 - (features1.depth as f64 - features2.depth as f64).abs() / (max_depth as f64)
642        } else {
643            1.0 // Both have zero depth - identical
644        };
645
646        let max_two_qubit = features1.two_qubit_gates.max(features2.two_qubit_gates);
647        let two_qubit_similarity = if max_two_qubit > 0 {
648            1.0 - (features1.two_qubit_gates as f64 - features2.two_qubit_gates as f64).abs()
649                / (max_two_qubit as f64)
650        } else {
651            1.0 // Both have zero two-qubit gates - identical
652        };
653
654        Ok(f64::midpoint(depth_similarity, two_qubit_similarity))
655    }
656
657    /// Compute ML-based similarity using embeddings
658    fn compute_ml_similarity<const N: usize, const M: usize>(
659        &mut self,
660        circuit1: &Circuit<N>,
661        circuit2: &Circuit<M>,
662        model_type: &MLModelType,
663    ) -> QuantRS2Result<f64> {
664        // Generate circuit embeddings using ML models
665        let embedding1 = self.generate_circuit_embedding(circuit1, model_type)?;
666        let embedding2 = self.generate_circuit_embedding(circuit2, model_type)?;
667
668        // Compute cosine similarity between embeddings
669        let similarity = Self::cosine_similarity(&embedding1, &embedding2);
670        Ok(similarity)
671    }
672
673    /// Generate circuit embedding using ML model
674    fn generate_circuit_embedding<const N: usize>(
675        &mut self,
676        circuit: &Circuit<N>,
677        model_type: &MLModelType,
678    ) -> QuantRS2Result<Vec<f64>> {
679        let id = format!("{}_{:?}", Self::generate_circuit_id(circuit), model_type);
680
681        if let Some(cached) = self.embedding_cache.get(&id) {
682            return Ok(cached.clone());
683        }
684
685        // Generate embedding based on model type. No trained neural-network
686        // weights are available in this crate for any of these model
687        // families, so rather than fabricate a fixed constant vector (which
688        // would make every circuit look identical under cosine similarity),
689        // each variant derives a real, deterministic embedding from the
690        // circuit's actual extracted features (see
691        // `Self::circuit_feature_vector` / `Self::project_feature_vector`),
692        // sized to the dimension the model type would have produced.
693        let embedding = match model_type {
694            MLModelType::VAE { latent_dim } => self.generate_vae_embedding(circuit, *latent_dim)?,
695            MLModelType::GCN { hidden_dims } => {
696                self.generate_gcn_embedding(circuit, hidden_dims)?
697            }
698            MLModelType::Transformer {
699                num_heads,
700                num_layers,
701            } => self.generate_transformer_embedding(circuit, *num_heads, *num_layers)?,
702            MLModelType::PreTrained { model_name } => {
703                self.generate_pretrained_embedding(circuit, model_name)?
704            }
705        };
706
707        self.embedding_cache.insert(id, embedding.clone());
708        Ok(embedding)
709    }
710
711    /// Real feature vector summarizing a circuit's structure: depth,
712    /// two-qubit gate count, entanglement width/layer count, parallelism
713    /// statistics, connectivity size, and a canonical gate-type histogram.
714    fn circuit_feature_vector(features: &CircuitFeatures) -> Vec<f64> {
715        const CANONICAL_GATES: [&str; 12] = [
716            "H", "X", "Y", "Z", "S", "T", "RX", "RY", "RZ", "CNOT", "CZ", "SWAP",
717        ];
718
719        let mut raw = vec![
720            features.depth as f64,
721            features.two_qubit_gates as f64,
722            features.entanglement_structure.max_entanglement_width as f64,
723            features.entanglement_structure.entangling_layers.len() as f64,
724            features.parallelism_profile.iter().sum::<usize>() as f64,
725            features
726                .parallelism_profile
727                .iter()
728                .copied()
729                .max()
730                .unwrap_or(0) as f64,
731            features.connectivity_pattern.len() as f64,
732        ];
733        for gate_name in CANONICAL_GATES {
734            raw.push(*features.gate_histogram.get(gate_name).unwrap_or(&0) as f64);
735        }
736        raw
737    }
738
739    /// Deterministically project a (small, fixed-length) raw feature vector
740    /// into an `output_dim`-dimensional embedding using a fixed cosine
741    /// (Fourier-feature-style) basis expansion, then L2-normalize. This is
742    /// not a trained model — it is a real, reproducible, circuit-dependent
743    /// encoding used as an honest stand-in until an actual trained
744    /// embedding model is wired into this crate.
745    fn project_feature_vector(raw_features: &[f64], output_dim: usize) -> Vec<f64> {
746        if output_dim == 0 {
747            return Vec::new();
748        }
749        let normalization = raw_features.len().max(1) as f64;
750        let basis_size = output_dim as f64 + 1.0;
751
752        let mut embedding = vec![0.0_f64; output_dim];
753        for (k, slot) in embedding.iter_mut().enumerate() {
754            let mut accumulator = 0.0_f64;
755            for (i, &value) in raw_features.iter().enumerate() {
756                let phase = 2.0 * PI * ((i + 1) as f64) * ((k + 1) as f64) / basis_size;
757                accumulator += value * phase.cos();
758            }
759            *slot = accumulator / normalization;
760        }
761
762        let norm: f64 = embedding.iter().map(|v| v * v).sum::<f64>().sqrt();
763        if norm > 0.0 {
764            for value in &mut embedding {
765                *value /= norm;
766            }
767        }
768        embedding
769    }
770
771    /// Generate a real, feature-derived "VAE-style" embedding (see
772    /// [`Self::project_feature_vector`] for why this is a deterministic
773    /// stand-in rather than an actual trained variational autoencoder).
774    fn generate_vae_embedding<const N: usize>(
775        &mut self,
776        circuit: &Circuit<N>,
777        latent_dim: usize,
778    ) -> QuantRS2Result<Vec<f64>> {
779        let features = self.extract_circuit_features(circuit)?;
780        let raw = Self::circuit_feature_vector(&features);
781        Ok(Self::project_feature_vector(&raw, latent_dim))
782    }
783
784    /// Generate a real, feature-derived "GCN-style" embedding.
785    fn generate_gcn_embedding<const N: usize>(
786        &mut self,
787        circuit: &Circuit<N>,
788        hidden_dims: &[usize],
789    ) -> QuantRS2Result<Vec<f64>> {
790        let output_dim = *hidden_dims.last().unwrap_or(&64);
791        let features = self.extract_circuit_features(circuit)?;
792        let raw = Self::circuit_feature_vector(&features);
793        Ok(Self::project_feature_vector(&raw, output_dim))
794    }
795
796    /// Generate a real, feature-derived "Transformer-style" embedding.
797    fn generate_transformer_embedding<const N: usize>(
798        &mut self,
799        circuit: &Circuit<N>,
800        num_heads: usize,
801        _num_layers: usize,
802    ) -> QuantRS2Result<Vec<f64>> {
803        let embedding_dim = num_heads * 64; // Typical dimension
804        let features = self.extract_circuit_features(circuit)?;
805        let raw = Self::circuit_feature_vector(&features);
806        Ok(Self::project_feature_vector(&raw, embedding_dim))
807    }
808
809    /// Generate a real, feature-derived "pre-trained-model-style" embedding.
810    fn generate_pretrained_embedding<const N: usize>(
811        &mut self,
812        circuit: &Circuit<N>,
813        model_name: &str,
814    ) -> QuantRS2Result<Vec<f64>> {
815        let embedding_dim = match model_name {
816            "circuit_bert" => 768,
817            "quantum_gpt" => 512,
818            _ => 256,
819        };
820        let features = self.extract_circuit_features(circuit)?;
821        let raw = Self::circuit_feature_vector(&features);
822        Ok(Self::project_feature_vector(&raw, embedding_dim))
823    }
824
825    /// Compute structural similarity
826    fn compute_structural_similarity(
827        features1: &CircuitFeatures,
828        features2: &CircuitFeatures,
829    ) -> QuantRS2Result<f64> {
830        // Compare circuit structure
831        let connectivity_similarity = Self::compare_connectivity_patterns(
832            &features1.connectivity_pattern,
833            &features2.connectivity_pattern,
834        );
835
836        // Handle depth comparison with division by zero protection
837        let max_depth = features1.depth.max(features2.depth);
838        let depth_similarity = if max_depth > 0 {
839            1.0 - (features1.depth as f64 - features2.depth as f64).abs() / (max_depth as f64)
840        } else {
841            // Both circuits have zero depth - they are identical in this metric
842            1.0
843        };
844
845        Ok(f64::midpoint(connectivity_similarity, depth_similarity))
846    }
847
848    /// Compute functional similarity: how similarly the two circuits *act*
849    /// on the canonical `|0...0>` reference state, as opposed to
850    /// [`Self::compute_unitary_similarity`]'s full-operator process
851    /// fidelity. Both circuits are simulated (via
852    /// [`Self::simulate_circuit_from_basis_state`]) from the `|0...0>`
853    /// input, and the resulting output states are compared via the real
854    /// quantum state fidelity `|<psi1|psi2>|^2`.
855    fn compute_functional_similarity<const N: usize, const M: usize>(
856        circuit1: &Circuit<N>,
857        circuit2: &Circuit<M>,
858    ) -> QuantRS2Result<f64> {
859        if N != M {
860            return Ok(0.0);
861        }
862
863        let state1 = Self::simulate_circuit_from_basis_state(circuit1, 0)?;
864        let state2 = Self::simulate_circuit_from_basis_state(circuit2, 0)?;
865
866        let overlap: Complex64 = state1
867            .iter()
868            .zip(state2.iter())
869            .map(|(a, b)| a.conj() * b)
870            .sum();
871
872        Ok(overlap.norm_sqr().clamp(0.0, 1.0))
873    }
874
875    /// Compute sequence similarity
876    fn compute_sequence_similarity(
877        features1: &CircuitFeatures,
878        features2: &CircuitFeatures,
879    ) -> QuantRS2Result<f64> {
880        // Compare gate sequences using edit distance
881        let edit_distance =
882            Self::string_edit_distance(&features1.critical_path, &features2.critical_path);
883        let max_length = features1
884            .critical_path
885            .len()
886            .max(features2.critical_path.len());
887
888        let similarity = if max_length > 0 {
889            1.0 - (edit_distance as f64 / max_length as f64)
890        } else {
891            1.0
892        };
893
894        Ok(similarity)
895    }
896
897    /// Compute topological similarity
898    fn compute_topological_similarity(
899        features1: &CircuitFeatures,
900        features2: &CircuitFeatures,
901    ) -> QuantRS2Result<f64> {
902        // Compare entanglement topology with division by zero protection
903        let max_width = features1
904            .entanglement_structure
905            .max_entanglement_width
906            .max(features2.entanglement_structure.max_entanglement_width);
907
908        let width_similarity = if max_width > 0 {
909            1.0 - (features1.entanglement_structure.max_entanglement_width as f64
910                - features2.entanglement_structure.max_entanglement_width as f64)
911                .abs()
912                / (max_width as f64)
913        } else {
914            1.0 // Both have zero entanglement width - identical
915        };
916
917        Ok(width_similarity)
918    }
919
920    /// Helper methods
921
922    /// Generate unique circuit identifier
923    fn generate_circuit_id<const N: usize>(circuit: &Circuit<N>) -> String {
924        use std::collections::hash_map::DefaultHasher;
925        use std::hash::{Hash, Hasher};
926
927        let mut hasher = DefaultHasher::new();
928        N.hash(&mut hasher);
929
930        for gate in circuit.gates() {
931            gate.name().hash(&mut hasher);
932            for qubit in gate.qubits() {
933                qubit.id().hash(&mut hasher);
934            }
935        }
936
937        format!("{:x}", hasher.finish())
938    }
939
940    /// Compute the parallelism profile of a circuit via greedy ASAP layering.
941    ///
942    /// Gates are scheduled as soon as their qubits are free: a gate is assigned
943    /// to the earliest layer at or after the latest layer occupied by any qubit
944    /// it touches, and each touched qubit then advances past that layer. Gates
945    /// acting on disjoint qubit sets therefore share a layer. The returned
946    /// vector is the *width* of each layer (number of gates executing in
947    /// parallel at that step), so its length is the circuit depth and its sum is
948    /// the total gate count.
949    fn compute_parallelism_profile<const N: usize>(
950        circuit: &Circuit<N>,
951    ) -> QuantRS2Result<Vec<usize>> {
952        // Next free layer for each qubit.
953        let mut qubit_next_layer = vec![0usize; N];
954        // Width (gate count) per layer, grown on demand.
955        let mut layer_widths: Vec<usize> = Vec::new();
956
957        for gate in circuit.gates() {
958            let qubits = gate.qubits();
959
960            // Earliest layer at which every touched qubit is free.
961            let layer = qubits
962                .iter()
963                .map(|q| qubit_next_layer[q.id() as usize])
964                .max()
965                .unwrap_or(0);
966
967            if layer >= layer_widths.len() {
968                layer_widths.resize(layer + 1, 0);
969            }
970            layer_widths[layer] += 1;
971
972            // Each touched qubit becomes busy for this layer.
973            for qubit in qubits {
974                qubit_next_layer[qubit.id() as usize] = layer + 1;
975            }
976        }
977
978        Ok(layer_widths)
979    }
980
981    /// Analyze entanglement structure
982    fn analyze_entanglement_structure<const N: usize>(
983        circuit: &Circuit<N>,
984    ) -> QuantRS2Result<EntanglementStructure> {
985        let mut entangling_layers = Vec::new();
986        let mut current_layer = Vec::new();
987        let mut max_width = 0;
988
989        for gate in circuit.gates() {
990            if gate.qubits().len() == 2 {
991                let qubits: Vec<usize> = gate.qubits().iter().map(|q| q.id() as usize).collect();
992                current_layer.push((qubits[0], qubits[1]));
993                max_width = max_width.max(current_layer.len());
994            } else if !current_layer.is_empty() {
995                entangling_layers.push(current_layer);
996                current_layer = Vec::new();
997            }
998        }
999
1000        if !current_layer.is_empty() {
1001            entangling_layers.push(current_layer);
1002        }
1003
1004        // Create entanglement graph
1005        let mut graph = SciRS2Graph {
1006            nodes: (0..N).collect(),
1007            edges: Vec::new(),
1008            node_attributes: HashMap::new(),
1009            edge_attributes: HashMap::new(),
1010        };
1011
1012        for layer in &entangling_layers {
1013            for &(q1, q2) in layer {
1014                graph.edges.push((q1, q2, 1.0));
1015            }
1016        }
1017
1018        Ok(EntanglementStructure {
1019            entangling_layers,
1020            max_entanglement_width: max_width,
1021            entanglement_graph: graph,
1022        })
1023    }
1024
1025    /// Compare connectivity patterns
1026    fn compare_connectivity_patterns(
1027        pattern1: &[(usize, usize)],
1028        pattern2: &[(usize, usize)],
1029    ) -> f64 {
1030        let set1: HashSet<_> = pattern1.iter().collect();
1031        let set2: HashSet<_> = pattern2.iter().collect();
1032
1033        let intersection = set1.intersection(&set2).count();
1034        let union = set1.union(&set2).count();
1035
1036        if union > 0 {
1037            intersection as f64 / union as f64
1038        } else {
1039            1.0
1040        }
1041    }
1042
1043    /// Compute edit distance between strings
1044    fn string_edit_distance(seq1: &[String], seq2: &[String]) -> usize {
1045        let m = seq1.len();
1046        let n = seq2.len();
1047        let mut dp = vec![vec![0; n + 1]; m + 1];
1048
1049        // Initialize base cases
1050        for i in 0..=m {
1051            dp[i][0] = i;
1052        }
1053        for j in 0..=n {
1054            dp[0][j] = j;
1055        }
1056
1057        // Fill DP table
1058        for i in 1..=m {
1059            for j in 1..=n {
1060                if seq1[i - 1] == seq2[j - 1] {
1061                    dp[i][j] = dp[i - 1][j - 1];
1062                } else {
1063                    dp[i][j] = 1 + dp[i - 1][j].min(dp[i][j - 1]).min(dp[i - 1][j - 1]);
1064                }
1065            }
1066        }
1067
1068        dp[m][n]
1069    }
1070
1071    /// Compute cosine similarity between vectors
1072    fn cosine_similarity(vec1: &[f64], vec2: &[f64]) -> f64 {
1073        if vec1.len() != vec2.len() {
1074            return 0.0;
1075        }
1076
1077        let dot_product: f64 = vec1.iter().zip(vec2.iter()).map(|(a, b)| a * b).sum();
1078        let norm1: f64 = vec1.iter().map(|x| x * x).sum::<f64>().sqrt();
1079        let norm2: f64 = vec2.iter().map(|x| x * x).sum::<f64>().sqrt();
1080
1081        if norm1 > 0.0 && norm2 > 0.0 {
1082            dot_product / (norm1 * norm2)
1083        } else {
1084            0.0
1085        }
1086    }
1087
1088    /// Compute edit distance between circuit features
1089    fn compute_edit_distance(
1090        features1: &CircuitFeatures,
1091        features2: &CircuitFeatures,
1092    ) -> QuantRS2Result<usize> {
1093        // Simplified edit distance based on gate operations
1094        let distance =
1095            Self::string_edit_distance(&features1.critical_path, &features2.critical_path);
1096        Ok(distance)
1097    }
1098
1099    /// Compute Wasserstein distance
1100    const fn compute_wasserstein_distance(
1101        _features1: &CircuitFeatures,
1102        _features2: &CircuitFeatures,
1103    ) -> QuantRS2Result<f64> {
1104        // Simplified Wasserstein distance computation
1105        // In practice, would use SciRS2's optimal transport algorithms
1106        Ok(0.3) // Placeholder
1107    }
1108
1109    /// Compute Hausdorff distance
1110    const fn compute_hausdorff_distance<const N: usize, const M: usize>(
1111        _circuit1: &Circuit<N>,
1112        _circuit2: &Circuit<M>,
1113    ) -> QuantRS2Result<f64> {
1114        // Placeholder for Hausdorff distance computation
1115        Ok(0.25) // Placeholder
1116    }
1117
1118    /// Compute Earth Mover's distance
1119    const fn compute_earth_movers_distance(
1120        _features1: &CircuitFeatures,
1121        _features2: &CircuitFeatures,
1122    ) -> QuantRS2Result<f64> {
1123        // Placeholder for Earth Mover's distance computation
1124        Ok(0.2) // Placeholder
1125    }
1126
1127    /// Compute process fidelity distance
1128    const fn compute_process_fidelity_distance<const N: usize, const M: usize>(
1129        _circuit1: &Circuit<N>,
1130        _circuit2: &Circuit<M>,
1131    ) -> QuantRS2Result<f64> {
1132        if N != M {
1133            return Ok(1.0); // Maximum distance for different dimensions
1134        }
1135
1136        // Placeholder for process fidelity computation
1137        Ok(0.1) // Placeholder
1138    }
1139
1140    /// Compute graph edit distance
1141    fn compute_graph_edit_distance(
1142        graph1: &SciRS2Graph,
1143        graph2: &SciRS2Graph,
1144    ) -> QuantRS2Result<f64> {
1145        // Simplified graph edit distance
1146        let node_diff = (graph1.nodes.len() as f64 - graph2.nodes.len() as f64).abs();
1147        let edge_diff = (graph1.edges.len() as f64 - graph2.edges.len() as f64).abs();
1148        let max_size = (graph1.nodes.len() + graph1.edges.len())
1149            .max(graph2.nodes.len() + graph2.edges.len()) as f64;
1150
1151        let distance = if max_size > 0.0 {
1152            (node_diff + edge_diff) / max_size
1153        } else {
1154            0.0 // Both graphs are empty - identical
1155        };
1156
1157        Ok(1.0 - distance) // Convert to similarity
1158    }
1159
1160    /// Compute spectral similarity
1161    const fn compute_spectral_similarity(
1162        _graph1: &SciRS2Graph,
1163        _graph2: &SciRS2Graph,
1164    ) -> QuantRS2Result<f64> {
1165        // Placeholder for spectral similarity computation
1166        // Would compute eigenvalues of graph Laplacians and compare
1167        Ok(0.7) // Placeholder
1168    }
1169}
1170
1171/// Batch similarity computation for multiple circuits
1172pub struct BatchSimilarityComputer {
1173    analyzer: CircuitSimilarityAnalyzer,
1174}
1175
1176impl BatchSimilarityComputer {
1177    /// Create new batch computer
1178    #[must_use]
1179    pub fn new(config: SimilarityConfig) -> Self {
1180        Self {
1181            analyzer: CircuitSimilarityAnalyzer::new(config),
1182        }
1183    }
1184
1185    /// Compute pairwise similarities for a set of circuits
1186    pub fn compute_pairwise_similarities<const N: usize>(
1187        &mut self,
1188        circuits: &[Circuit<N>],
1189    ) -> QuantRS2Result<Vec<Vec<f64>>> {
1190        let n_circuits = circuits.len();
1191        let mut similarity_matrix = vec![vec![0.0; n_circuits]; n_circuits];
1192
1193        for i in 0..n_circuits {
1194            similarity_matrix[i][i] = 1.0; // Self-similarity
1195
1196            for j in (i + 1)..n_circuits {
1197                let similarity = self
1198                    .analyzer
1199                    .compute_similarity(&circuits[i], &circuits[j])?;
1200                similarity_matrix[i][j] = similarity.overall_similarity;
1201                similarity_matrix[j][i] = similarity.overall_similarity; // Symmetric
1202            }
1203        }
1204
1205        Ok(similarity_matrix)
1206    }
1207
1208    /// Find most similar circuits in a dataset
1209    pub fn find_most_similar<const N: usize>(
1210        &mut self,
1211        query_circuit: &Circuit<N>,
1212        dataset: &[Circuit<N>],
1213        top_k: usize,
1214    ) -> QuantRS2Result<Vec<(usize, f64)>> {
1215        let mut similarities = Vec::new();
1216
1217        for (i, circuit) in dataset.iter().enumerate() {
1218            let similarity = self.analyzer.compute_similarity(query_circuit, circuit)?;
1219            similarities.push((i, similarity.overall_similarity));
1220        }
1221
1222        // Sort by similarity and return top-k
1223        similarities.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
1224        similarities.truncate(top_k);
1225
1226        Ok(similarities)
1227    }
1228}
1229
1230#[cfg(test)]
1231mod tests {
1232    use super::*;
1233    use quantrs2_core::gate::multi::CNOT;
1234    use quantrs2_core::gate::single::Hadamard;
1235
1236    #[test]
1237    fn test_similarity_analyzer_creation() {
1238        let analyzer = CircuitSimilarityAnalyzer::with_default_config();
1239        assert_eq!(analyzer.config.algorithms.len(), 3);
1240    }
1241
1242    #[test]
1243    fn test_parallelism_profile_disjoint_qubits() {
1244        // Two single-qubit gates on disjoint qubits can run in one layer.
1245        let mut circuit = Circuit::<2>::new();
1246        circuit
1247            .add_gate(Hadamard { target: QubitId(0) })
1248            .expect("add H on q0");
1249        circuit
1250            .add_gate(Hadamard { target: QubitId(1) })
1251            .expect("add H on q1");
1252
1253        let profile = CircuitSimilarityAnalyzer::compute_parallelism_profile(&circuit)
1254            .expect("profile computation");
1255        assert_eq!(
1256            profile,
1257            vec![2],
1258            "disjoint gates share one layer of width 2"
1259        );
1260    }
1261
1262    #[test]
1263    fn test_parallelism_profile_serial_chain() {
1264        // Two gates on the same qubit must be in separate layers.
1265        let mut circuit = Circuit::<1>::new();
1266        circuit
1267            .add_gate(Hadamard { target: QubitId(0) })
1268            .expect("add H");
1269        circuit
1270            .add_gate(Hadamard { target: QubitId(0) })
1271            .expect("add H");
1272
1273        let profile = CircuitSimilarityAnalyzer::compute_parallelism_profile(&circuit)
1274            .expect("profile computation");
1275        assert_eq!(
1276            profile,
1277            vec![1, 1],
1278            "serial gates occupy consecutive layers"
1279        );
1280    }
1281
1282    #[test]
1283    fn test_identical_circuits_similarity() {
1284        let mut analyzer = CircuitSimilarityAnalyzer::with_default_config();
1285
1286        let mut circuit = Circuit::<2>::new();
1287        circuit
1288            .add_gate(Hadamard { target: QubitId(0) })
1289            .expect("Failed to add Hadamard gate");
1290
1291        let similarity = analyzer
1292            .compute_similarity(&circuit, &circuit)
1293            .expect("Failed to compute similarity for identical circuits");
1294
1295        // Fixed: Overall similarity should be 1.0 for identical circuits
1296        // All component similarities should also be 1.0 or very close to it
1297        assert!(
1298            !similarity.overall_similarity.is_nan(),
1299            "Similarity should not be NaN for identical circuits. Actual value: {}",
1300            similarity.overall_similarity
1301        );
1302        assert!(
1303            !similarity.overall_similarity.is_infinite(),
1304            "Similarity should not be infinite for identical circuits. Actual value: {}",
1305            similarity.overall_similarity
1306        );
1307        assert!(
1308            similarity.structural_similarity >= 0.9,
1309            "Structural similarity should be high for identical circuits: {}",
1310            similarity.structural_similarity
1311        );
1312        assert!(
1313            similarity.sequence_similarity >= 0.9,
1314            "Sequence similarity should be high for identical circuits: {}",
1315            similarity.sequence_similarity
1316        );
1317        assert!(
1318            similarity.topological_similarity >= 0.9,
1319            "Topological similarity should be high for identical circuits: {}",
1320            similarity.topological_similarity
1321        );
1322        assert!(
1323            similarity.overall_similarity >= 0.8,
1324            "Overall similarity should be high for identical circuits: {}",
1325            similarity.overall_similarity
1326        );
1327    }
1328
1329    #[test]
1330    fn test_different_circuits_similarity() {
1331        let mut analyzer = CircuitSimilarityAnalyzer::with_default_config();
1332
1333        let mut circuit1 = Circuit::<2>::new();
1334        circuit1
1335            .add_gate(Hadamard { target: QubitId(0) })
1336            .expect("Failed to add Hadamard gate to circuit1");
1337
1338        let mut circuit2 = Circuit::<2>::new();
1339        circuit2
1340            .add_gate(CNOT {
1341                control: QubitId(0),
1342                target: QubitId(1),
1343            })
1344            .expect("Failed to add CNOT gate to circuit2");
1345
1346        let similarity = analyzer
1347            .compute_similarity(&circuit1, &circuit2)
1348            .expect("Failed to compute similarity for different circuits");
1349        assert!(similarity.overall_similarity < 1.0);
1350    }
1351
1352    #[test]
1353    fn test_distance_computation() {
1354        let mut analyzer = CircuitSimilarityAnalyzer::with_default_config();
1355
1356        let mut circuit1 = Circuit::<2>::new();
1357        circuit1
1358            .add_gate(Hadamard { target: QubitId(0) })
1359            .expect("Failed to add Hadamard gate to circuit1");
1360
1361        let mut circuit2 = Circuit::<2>::new();
1362        circuit2
1363            .add_gate(CNOT {
1364                control: QubitId(0),
1365                target: QubitId(1),
1366            })
1367            .expect("Failed to add CNOT gate to circuit2");
1368
1369        let distance = analyzer
1370            .compute_distance(&circuit1, &circuit2)
1371            .expect("Failed to compute distance between circuits");
1372        assert!(distance.edit_distance > 0);
1373        assert!(
1374            distance.normalized_edit_distance >= 0.0 && distance.normalized_edit_distance <= 1.0
1375        );
1376    }
1377
1378    #[test]
1379    fn test_feature_extraction() {
1380        let mut analyzer = CircuitSimilarityAnalyzer::with_default_config();
1381
1382        let mut circuit = Circuit::<2>::new();
1383        circuit
1384            .add_gate(Hadamard { target: QubitId(0) })
1385            .expect("Failed to add Hadamard gate");
1386        circuit
1387            .add_gate(CNOT {
1388                control: QubitId(0),
1389                target: QubitId(1),
1390            })
1391            .expect("Failed to add CNOT gate");
1392
1393        let features = analyzer
1394            .extract_circuit_features(&circuit)
1395            .expect("Failed to extract circuit features");
1396        assert_eq!(features.gate_histogram.get("H"), Some(&1));
1397        assert_eq!(features.gate_histogram.get("CNOT"), Some(&1));
1398        assert_eq!(features.two_qubit_gates, 1);
1399    }
1400
1401    #[test]
1402    fn test_batch_similarity_computation() {
1403        let mut computer = BatchSimilarityComputer::new(SimilarityConfig::default());
1404
1405        let mut circuit1 = Circuit::<2>::new();
1406        circuit1
1407            .add_gate(Hadamard { target: QubitId(0) })
1408            .expect("Failed to add Hadamard gate to circuit1");
1409
1410        let mut circuit2 = Circuit::<2>::new();
1411        circuit2
1412            .add_gate(CNOT {
1413                control: QubitId(0),
1414                target: QubitId(1),
1415            })
1416            .expect("Failed to add CNOT gate to circuit2");
1417
1418        let circuits = vec![circuit1, circuit2];
1419        let similarity_matrix = computer
1420            .compute_pairwise_similarities(&circuits)
1421            .expect("Failed to compute pairwise similarities");
1422
1423        assert_eq!(similarity_matrix.len(), 2);
1424        assert_eq!(similarity_matrix[0].len(), 2);
1425        assert_eq!(similarity_matrix[0][0], 1.0); // Self-similarity
1426        assert_eq!(similarity_matrix[1][1], 1.0); // Self-similarity
1427        assert_eq!(similarity_matrix[0][1], similarity_matrix[1][0]); // Symmetry
1428    }
1429
1430    // -----------------------------------------------------------------------
1431    // compute_functional_similarity / compute_unitary_similarity: these must
1432    // now depend on real circuit simulation rather than the old hardcoded
1433    // 0.8 / 0.9 constants.
1434    // -----------------------------------------------------------------------
1435
1436    #[test]
1437    fn test_functional_similarity_identical_circuit_is_one() {
1438        let mut circuit = Circuit::<2>::new();
1439        circuit.add_gate(Hadamard { target: QubitId(0) }).unwrap();
1440        circuit
1441            .add_gate(CNOT {
1442                control: QubitId(0),
1443                target: QubitId(1),
1444            })
1445            .unwrap();
1446
1447        let similarity =
1448            CircuitSimilarityAnalyzer::compute_functional_similarity(&circuit, &circuit)
1449                .expect("compute_functional_similarity should succeed");
1450        assert!(
1451            (similarity - 1.0).abs() < 1e-9,
1452            "identical circuits must have functional similarity 1.0, got {similarity}"
1453        );
1454    }
1455
1456    #[test]
1457    fn test_functional_similarity_orthogonal_outputs_near_zero() {
1458        // Identity (no gates) leaves |00>; an X on qubit 0 produces |10>,
1459        // which is orthogonal to |00>.
1460        let identity_circuit = Circuit::<2>::new();
1461        let mut x_circuit = Circuit::<2>::new();
1462        x_circuit
1463            .add_gate(quantrs2_core::gate::single::PauliX { target: QubitId(0) })
1464            .unwrap();
1465
1466        let similarity =
1467            CircuitSimilarityAnalyzer::compute_functional_similarity(&identity_circuit, &x_circuit)
1468                .expect("compute_functional_similarity should succeed");
1469        assert!(
1470            similarity < 1e-9,
1471            "orthogonal output states must have ~0 functional similarity, got {similarity}"
1472        );
1473    }
1474
1475    #[test]
1476    fn test_unitary_similarity_identical_circuit_is_one() {
1477        let mut circuit = Circuit::<2>::new();
1478        circuit.add_gate(Hadamard { target: QubitId(0) }).unwrap();
1479        circuit
1480            .add_gate(CNOT {
1481                control: QubitId(0),
1482                target: QubitId(1),
1483            })
1484            .unwrap();
1485
1486        let similarity = CircuitSimilarityAnalyzer::compute_unitary_similarity(&circuit, &circuit)
1487            .expect("compute_unitary_similarity should succeed");
1488        assert!(
1489            (similarity - 1.0).abs() < 1e-9,
1490            "identical circuits must have unitary (process fidelity) similarity 1.0, got {similarity}"
1491        );
1492    }
1493
1494    #[test]
1495    fn test_unitary_similarity_different_circuits_less_than_one() {
1496        let mut circuit1 = Circuit::<1>::new();
1497        circuit1.add_gate(Hadamard { target: QubitId(0) }).unwrap();
1498
1499        let mut circuit2 = Circuit::<1>::new();
1500        circuit2
1501            .add_gate(quantrs2_core::gate::single::PauliX { target: QubitId(0) })
1502            .unwrap();
1503
1504        let similarity =
1505            CircuitSimilarityAnalyzer::compute_unitary_similarity(&circuit1, &circuit2)
1506                .expect("compute_unitary_similarity should succeed");
1507        assert!(
1508            similarity < 1.0 - 1e-9,
1509            "H and X are different single-qubit unitaries; similarity must be < 1.0, got {similarity}"
1510        );
1511        assert!((0.0..=1.0).contains(&similarity));
1512    }
1513
1514    #[test]
1515    fn test_unitary_similarity_mismatched_qubit_counts_is_zero() {
1516        let circuit1 = Circuit::<1>::new();
1517        let circuit2 = Circuit::<2>::new();
1518        let similarity =
1519            CircuitSimilarityAnalyzer::compute_unitary_similarity(&circuit1, &circuit2)
1520                .expect("compute_unitary_similarity should succeed");
1521        assert_eq!(similarity, 0.0);
1522    }
1523
1524    // -----------------------------------------------------------------------
1525    // ML embeddings must now be derived from real circuit features, not a
1526    // fixed all-0.5 vector for every circuit.
1527    // -----------------------------------------------------------------------
1528
1529    #[test]
1530    fn test_ml_embeddings_depend_on_circuit_content() {
1531        let mut analyzer = CircuitSimilarityAnalyzer::with_default_config();
1532
1533        let mut circuit1 = Circuit::<2>::new();
1534        circuit1.add_gate(Hadamard { target: QubitId(0) }).unwrap();
1535
1536        let mut circuit2 = Circuit::<2>::new();
1537        circuit2
1538            .add_gate(CNOT {
1539                control: QubitId(0),
1540                target: QubitId(1),
1541            })
1542            .unwrap();
1543        circuit2
1544            .add_gate(CNOT {
1545                control: QubitId(1),
1546                target: QubitId(0),
1547            })
1548            .unwrap();
1549
1550        let model_type = MLModelType::VAE { latent_dim: 8 };
1551        let embedding1 = analyzer
1552            .generate_circuit_embedding(&circuit1, &model_type)
1553            .expect("embedding generation should succeed");
1554        let embedding2 = analyzer
1555            .generate_circuit_embedding(&circuit2, &model_type)
1556            .expect("embedding generation should succeed");
1557
1558        assert_eq!(embedding1.len(), 8);
1559        assert_eq!(embedding2.len(), 8);
1560        assert_ne!(
1561            embedding1, embedding2,
1562            "embeddings for structurally different circuits must differ, not be a constant vector"
1563        );
1564
1565        // Neither embedding should be the old hardcoded all-0.5 vector.
1566        assert!(embedding1.iter().any(|&v| (v - 0.5).abs() > 1e-6));
1567    }
1568
1569    #[test]
1570    fn test_ml_similarity_identical_circuits_is_one() {
1571        let mut analyzer = CircuitSimilarityAnalyzer::with_default_config();
1572
1573        let mut circuit = Circuit::<2>::new();
1574        circuit.add_gate(Hadamard { target: QubitId(0) }).unwrap();
1575        circuit
1576            .add_gate(CNOT {
1577                control: QubitId(0),
1578                target: QubitId(1),
1579            })
1580            .unwrap();
1581
1582        let model_type = MLModelType::GCN {
1583            hidden_dims: vec![32, 16],
1584        };
1585        let similarity = analyzer
1586            .compute_ml_similarity(&circuit, &circuit, &model_type)
1587            .expect("compute_ml_similarity should succeed");
1588        assert!(
1589            (similarity - 1.0).abs() < 1e-9,
1590            "an embedding compared with itself must have cosine similarity 1.0, got {similarity}"
1591        );
1592    }
1593}