Skip to main content

ruqu_core/
verification.rs

1//! Cross-backend automatic verification for quantum circuit simulation.
2//!
3//! This module provides tools to verify simulation results by running circuits
4//! on multiple backends and comparing their output distributions. For pure
5//! Clifford circuits, the stabilizer backend serves as an efficient reference
6//! implementation that can be compared bitwise against the state-vector backend.
7//!
8//! # Verification levels
9//!
10//! | Level | Method | When used |
11//! |-------|--------|-----------|
12//! | Exact | Bitwise match of distributions | Clifford circuits, <= 25 qubits |
13//! | Statistical | Chi-squared + TVD | General comparison of two distributions |
14//! | Trend | Correlation of energy landscape | Future: Hamiltonian-level comparison |
15//! | Skipped | N/A | Non-Clifford or no reference available |
16
17use crate::backend::{analyze_circuit, BackendType};
18use crate::circuit::QuantumCircuit;
19use crate::gate::Gate;
20use crate::simulator::Simulator;
21use crate::stabilizer::StabilizerState;
22
23use std::collections::HashMap;
24
25// ---------------------------------------------------------------------------
26// Public types
27// ---------------------------------------------------------------------------
28
29/// How rigorously the verification was performed.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum VerificationLevel {
32    /// Bitwise match (Clifford circuits: stabilizer vs state vector).
33    Exact,
34    /// Chi-squared test within tolerance.
35    Statistical,
36    /// Correlation of energy landscape.
37    Trend,
38    /// Verification not applicable.
39    Skipped,
40}
41
42/// Outcome of a cross-backend verification run.
43#[derive(Debug, Clone)]
44pub struct VerificationResult {
45    /// The level of verification that was performed.
46    pub level: VerificationLevel,
47    /// Whether the verification passed.
48    pub passed: bool,
49    /// The backend used for the primary simulation.
50    pub primary_backend: BackendType,
51    /// The backend used for the reference simulation, if any.
52    pub reference_backend: Option<BackendType>,
53    /// Total variation distance between the two distributions.
54    pub total_variation_distance: Option<f64>,
55    /// P-value from the chi-squared goodness-of-fit test.
56    pub chi_squared_p_value: Option<f64>,
57    /// Pearson correlation coefficient between distributions.
58    pub correlation: Option<f64>,
59    /// Human-readable explanation of the verification outcome.
60    pub explanation: String,
61    /// Individual bitstring discrepancies, sorted by absolute difference.
62    pub discrepancies: Vec<Discrepancy>,
63}
64
65/// A single bitstring where the primary and reference distributions disagree.
66#[derive(Debug, Clone)]
67pub struct Discrepancy {
68    /// The bitstring (one bool per qubit, qubit 0 first).
69    pub bitstring: Vec<bool>,
70    /// Probability of this bitstring in the primary distribution.
71    pub primary_probability: f64,
72    /// Probability of this bitstring in the reference distribution.
73    pub reference_probability: f64,
74    /// Absolute difference between the two probabilities.
75    pub absolute_difference: f64,
76}
77
78// ---------------------------------------------------------------------------
79// Main verification entry point
80// ---------------------------------------------------------------------------
81
82/// Verify a quantum circuit by running it on multiple backends and comparing
83/// the resulting measurement distributions.
84///
85/// # Algorithm
86///
87/// 1. Analyze the circuit to determine its Clifford fraction.
88/// 2. If the circuit is pure Clifford AND has <= 25 qubits, run on both the
89///    state-vector and stabilizer backends, then compare the distributions at
90///    the Exact level.
91/// 3. If the circuit is NOT pure Clifford AND has <= 25 qubits, run on the
92///    state-vector backend only and report verification as Skipped.
93/// 4. For circuits exceeding 25 qubits, report as Skipped.
94///
95/// # Arguments
96///
97/// * `circuit` - The quantum circuit to verify.
98/// * `shots` - Number of measurement shots per backend.
99/// * `seed` - Deterministic seed for reproducibility.
100pub fn verify_circuit(
101    circuit: &QuantumCircuit,
102    shots: u32,
103    seed: u64,
104) -> VerificationResult {
105    let analysis = analyze_circuit(circuit);
106    let num_qubits = circuit.num_qubits();
107    let is_clifford = is_clifford_circuit(circuit);
108
109    // Case 1: Pure Clifford AND small enough for state vector comparison.
110    if is_clifford && num_qubits <= 25 {
111        // Run on state-vector backend.
112        let sv_result = Simulator::run_shots(circuit, shots, Some(seed));
113        let sv_counts = match sv_result {
114            Ok(r) => r.counts,
115            Err(e) => {
116                return VerificationResult {
117                    level: VerificationLevel::Skipped,
118                    passed: false,
119                    primary_backend: BackendType::StateVector,
120                    reference_backend: None,
121                    total_variation_distance: None,
122                    chi_squared_p_value: None,
123                    correlation: None,
124                    explanation: format!(
125                        "State-vector simulation failed: {}",
126                        e
127                    ),
128                    discrepancies: vec![],
129                };
130            }
131        };
132
133        // Run on stabilizer backend.
134        let stab_counts = run_stabilizer_shots(circuit, shots, seed);
135
136        // Compare the two distributions.
137        let mut result = verify_against_reference(
138            &sv_counts,
139            &stab_counts,
140            0.0, // Exact match: zero tolerance for Clifford circuits
141        );
142
143        result.primary_backend = BackendType::StateVector;
144        result.reference_backend = Some(BackendType::Stabilizer);
145
146        // Upgrade to Exact level if the distributions match perfectly.
147        if result.passed
148            && result
149                .total_variation_distance
150                .map_or(false, |d| d == 0.0)
151        {
152            result.level = VerificationLevel::Exact;
153            result.explanation = format!(
154                "Exact match: {}-qubit Clifford circuit verified across \
155                 state-vector and stabilizer backends ({} shots, \
156                 clifford_fraction={:.2})",
157                num_qubits, shots, analysis.clifford_fraction
158            );
159        } else {
160            // Even for Clifford circuits, sampling noise may cause small
161            // differences. Use statistical comparison with a tight tolerance.
162            let tight_tolerance = 0.05;
163            let mut stat_result = verify_against_reference(
164                &sv_counts,
165                &stab_counts,
166                tight_tolerance,
167            );
168            stat_result.primary_backend = BackendType::StateVector;
169            stat_result.reference_backend = Some(BackendType::Stabilizer);
170            stat_result.explanation = format!(
171                "Statistical comparison of {}-qubit Clifford circuit across \
172                 state-vector and stabilizer backends ({} shots, TVD={:.6})",
173                num_qubits,
174                shots,
175                stat_result
176                    .total_variation_distance
177                    .unwrap_or(0.0)
178            );
179            return stat_result;
180        }
181
182        return result;
183    }
184
185    // Case 2: Not Clifford AND small enough for state vector.
186    if !is_clifford && num_qubits <= 25 {
187        return VerificationResult {
188            level: VerificationLevel::Skipped,
189            passed: true,
190            primary_backend: BackendType::StateVector,
191            reference_backend: None,
192            total_variation_distance: None,
193            chi_squared_p_value: None,
194            correlation: None,
195            explanation: format!(
196                "Verification skipped: {}-qubit circuit contains non-Clifford \
197                 gates (clifford_fraction={:.2}, {} non-Clifford gates). \
198                 No reference backend available for cross-validation.",
199                num_qubits,
200                analysis.clifford_fraction,
201                analysis.non_clifford_gates
202            ),
203            discrepancies: vec![],
204        };
205    }
206
207    // Case 3: Too many qubits for state-vector comparison.
208    VerificationResult {
209        level: VerificationLevel::Skipped,
210        passed: true,
211        primary_backend: analysis.recommended_backend,
212        reference_backend: None,
213        total_variation_distance: None,
214        chi_squared_p_value: None,
215        correlation: None,
216        explanation: format!(
217            "Verification skipped: {}-qubit circuit exceeds state-vector \
218             capacity for cross-backend comparison.",
219            num_qubits
220        ),
221        discrepancies: vec![],
222    }
223}
224
225// ---------------------------------------------------------------------------
226// Distribution comparison
227// ---------------------------------------------------------------------------
228
229/// Compare two measurement distributions and produce a verification result.
230///
231/// # Arguments
232///
233/// * `primary` - Counts from the primary backend.
234/// * `reference` - Counts from the reference backend.
235/// * `tolerance` - Maximum allowed total variation distance for a pass.
236///
237/// # Returns
238///
239/// A `VerificationResult` at the `Statistical` level (or `Exact` if TVD is
240/// exactly zero and tolerance is zero).
241pub fn verify_against_reference(
242    primary: &HashMap<Vec<bool>, usize>,
243    reference: &HashMap<Vec<bool>, usize>,
244    tolerance: f64,
245) -> VerificationResult {
246    let p_norm = normalize_counts(primary);
247    let q_norm = normalize_counts(reference);
248
249    let distance = tvd(&p_norm, &q_norm);
250
251    let total_ref: usize = reference.values().sum();
252    let (chi2_stat, dof) =
253        chi_squared_statistic(primary, &q_norm, total_ref);
254    let p_value = if dof > 0 {
255        chi_squared_p_value(chi2_stat, dof)
256    } else {
257        1.0
258    };
259
260    let corr = pearson_correlation(&p_norm, &q_norm);
261
262    // Build sorted discrepancy list.
263    let mut all_keys: Vec<&Vec<bool>> =
264        p_norm.keys().chain(q_norm.keys()).collect();
265    all_keys.sort();
266    all_keys.dedup();
267
268    let mut discrepancies: Vec<Discrepancy> = all_keys
269        .iter()
270        .map(|key| {
271            let pp = p_norm.get(*key).copied().unwrap_or(0.0);
272            let rp = q_norm.get(*key).copied().unwrap_or(0.0);
273            Discrepancy {
274                bitstring: (*key).clone(),
275                primary_probability: pp,
276                reference_probability: rp,
277                absolute_difference: (pp - rp).abs(),
278            }
279        })
280        .filter(|d| d.absolute_difference > 1e-15)
281        .collect();
282
283    // Sort by absolute difference, descending.
284    discrepancies
285        .sort_by(|a, b| b.absolute_difference.partial_cmp(&a.absolute_difference).unwrap());
286
287    let passed = distance <= tolerance;
288
289    let level = if tolerance == 0.0 && passed {
290        VerificationLevel::Exact
291    } else {
292        VerificationLevel::Statistical
293    };
294
295    let explanation = if passed {
296        format!(
297            "Verification passed: TVD={:.6}, chi2 p-value={:.4}, \
298             correlation={:.4}, tolerance={:.6}",
299            distance, p_value, corr, tolerance
300        )
301    } else {
302        format!(
303            "Verification FAILED: TVD={:.6} exceeds tolerance={:.6}, \
304             chi2 p-value={:.4}, correlation={:.4}, \
305             {} discrepancies found",
306            distance,
307            tolerance,
308            p_value,
309            corr,
310            discrepancies.len()
311        )
312    };
313
314    VerificationResult {
315        level,
316        passed,
317        primary_backend: BackendType::Auto,
318        reference_backend: None,
319        total_variation_distance: Some(distance),
320        chi_squared_p_value: Some(p_value),
321        correlation: Some(corr),
322        explanation,
323        discrepancies,
324    }
325}
326
327// ---------------------------------------------------------------------------
328// Clifford circuit detection
329// ---------------------------------------------------------------------------
330
331/// Check if ALL gates in a circuit are Clifford-compatible.
332///
333/// Clifford-compatible gates are: H, X, Y, Z, S, Sdg, CNOT, CZ, SWAP,
334/// Measure, Reset, and Barrier. Any other gate (T, Tdg, rotations, custom
335/// unitaries) makes the circuit non-Clifford.
336pub fn is_clifford_circuit(circuit: &QuantumCircuit) -> bool {
337    circuit.gates().iter().all(|gate| is_clifford_gate(gate))
338}
339
340/// Check if a single gate is Clifford-compatible.
341fn is_clifford_gate(gate: &Gate) -> bool {
342    matches!(
343        gate,
344        Gate::H(_)
345            | Gate::X(_)
346            | Gate::Y(_)
347            | Gate::Z(_)
348            | Gate::S(_)
349            | Gate::Sdg(_)
350            | Gate::CNOT(_, _)
351            | Gate::CZ(_, _)
352            | Gate::SWAP(_, _)
353            | Gate::Measure(_)
354            | Gate::Reset(_)
355            | Gate::Barrier
356    )
357}
358
359// ---------------------------------------------------------------------------
360// Stabilizer shot execution
361// ---------------------------------------------------------------------------
362
363/// Execute a Clifford circuit on the stabilizer backend for multiple shots.
364///
365/// For each shot, creates a fresh `StabilizerState`, applies all gates in
366/// order, and collects measurement outcomes into a histogram. If the circuit
367/// contains no explicit `Measure` gates, all qubits are measured at the end.
368///
369/// `Reset` gates are handled by measuring the qubit and conditionally
370/// applying an X gate to force it back to |0>.
371///
372/// # Panics
373///
374/// Panics if a non-Clifford gate is encountered (the caller must ensure the
375/// circuit is Clifford-only via `is_clifford_circuit`).
376pub fn run_stabilizer_shots(
377    circuit: &QuantumCircuit,
378    shots: u32,
379    seed: u64,
380) -> HashMap<Vec<bool>, usize> {
381    let n = circuit.num_qubits() as usize;
382    let mut counts: HashMap<Vec<bool>, usize> = HashMap::new();
383
384    let has_measurements = circuit
385        .gates()
386        .iter()
387        .any(|g| matches!(g, Gate::Measure(_)));
388
389    for shot in 0..shots {
390        let shot_seed = seed.wrapping_add(shot as u64);
391        let mut state = StabilizerState::new_with_seed(n, shot_seed)
392            .expect("failed to create stabilizer state");
393
394        let mut measured_bits: Vec<Option<bool>> = vec![None; n];
395
396        for gate in circuit.gates() {
397            match gate {
398                Gate::Reset(q) => {
399                    // Implement reset: measure, then conditionally flip.
400                    let qubit = *q as usize;
401                    let outcome = state
402                        .measure(qubit)
403                        .expect("stabilizer measurement failed");
404                    if outcome.result {
405                        state.x_gate(qubit);
406                    }
407                    // Clear the measured bit since reset puts qubit back to |0>.
408                    measured_bits[qubit] = None;
409                }
410                Gate::Measure(q) => {
411                    let outcomes = state
412                        .apply_gate(gate)
413                        .expect("stabilizer gate application failed");
414                    if let Some(outcome) = outcomes.first() {
415                        measured_bits[*q as usize] = Some(outcome.result);
416                    }
417                }
418                _ => {
419                    state
420                        .apply_gate(gate)
421                        .expect("stabilizer gate application failed");
422                }
423            }
424        }
425
426        // If no explicit measurements, measure all qubits.
427        if !has_measurements {
428            for q in 0..n {
429                let outcome = state
430                    .measure(q)
431                    .expect("stabilizer measurement failed");
432                measured_bits[q] = Some(outcome.result);
433            }
434        }
435
436        // Build the bit-vector for this shot.
437        let bits: Vec<bool> = measured_bits
438            .iter()
439            .map(|mb| mb.unwrap_or(false))
440            .collect();
441
442        *counts.entry(bits).or_insert(0) += 1;
443    }
444
445    counts
446}
447
448// ---------------------------------------------------------------------------
449// Helper functions: distribution normalization and metrics
450// ---------------------------------------------------------------------------
451
452/// Convert raw counts to a probability distribution.
453///
454/// Each count is divided by the total number of shots to produce a
455/// probability in [0, 1].
456pub fn normalize_counts(
457    counts: &HashMap<Vec<bool>, usize>,
458) -> HashMap<Vec<bool>, f64> {
459    let total: usize = counts.values().sum();
460    if total == 0 {
461        return HashMap::new();
462    }
463    let total_f = total as f64;
464    counts
465        .iter()
466        .map(|(k, &v)| (k.clone(), v as f64 / total_f))
467        .collect()
468}
469
470/// Compute the total variation distance between two probability distributions.
471///
472/// TVD = 0.5 * sum_x |p(x) - q(x)|
473///
474/// Returns a value in [0, 1] where 0 means identical distributions and 1
475/// means completely disjoint support.
476pub fn tvd(
477    p: &HashMap<Vec<bool>, f64>,
478    q: &HashMap<Vec<bool>, f64>,
479) -> f64 {
480    let mut all_keys: Vec<&Vec<bool>> =
481        p.keys().chain(q.keys()).collect();
482    all_keys.sort();
483    all_keys.dedup();
484
485    let sum: f64 = all_keys
486        .iter()
487        .map(|key| {
488            let pv = p.get(*key).copied().unwrap_or(0.0);
489            let qv = q.get(*key).copied().unwrap_or(0.0);
490            (pv - qv).abs()
491        })
492        .sum();
493
494    0.5 * sum
495}
496
497/// Compute the chi-squared statistic for a goodness-of-fit test.
498///
499/// Tests whether the observed counts (from the primary distribution) are
500/// consistent with the expected probabilities (from the reference
501/// distribution).
502///
503/// Returns `(statistic, degrees_of_freedom)`. Bins with an expected count
504/// below 5 are merged into an "other" bin to maintain test validity.
505///
506/// # Arguments
507///
508/// * `observed` - Raw counts from the primary distribution.
509/// * `expected_probs` - Probability distribution from the reference.
510/// * `total` - Total number of reference shots (used to scale expected probs
511///   to expected counts).
512pub fn chi_squared_statistic(
513    observed: &HashMap<Vec<bool>, usize>,
514    expected_probs: &HashMap<Vec<bool>, f64>,
515    _total: usize,
516) -> (f64, usize) {
517    let obs_total: usize = observed.values().sum();
518    if obs_total == 0 {
519        return (0.0, 0);
520    }
521    let obs_total_f = obs_total as f64;
522
523    let mut all_keys: Vec<&Vec<bool>> = observed
524        .keys()
525        .chain(expected_probs.keys())
526        .collect();
527    all_keys.sort();
528    all_keys.dedup();
529
530    let mut chi2 = 0.0;
531    let mut bins_used = 0usize;
532    let mut other_observed = 0.0;
533    let mut other_expected = 0.0;
534
535    for key in &all_keys {
536        let obs = observed.get(*key).copied().unwrap_or(0) as f64;
537        let exp_prob = expected_probs.get(*key).copied().unwrap_or(0.0);
538        let exp = exp_prob * obs_total_f;
539
540        if exp < 5.0 {
541            // Merge into the "other" bin.
542            other_observed += obs;
543            other_expected += exp;
544        } else {
545            let diff = obs - exp;
546            chi2 += (diff * diff) / exp;
547            bins_used += 1;
548        }
549    }
550
551    // Process the merged "other" bin.
552    if other_expected >= 5.0 {
553        let diff = other_observed - other_expected;
554        chi2 += (diff * diff) / other_expected;
555        bins_used += 1;
556    } else if other_expected > 0.0 && other_observed > 0.0 {
557        // Small expected count; include but note reduced reliability.
558        let diff = other_observed - other_expected;
559        chi2 += (diff * diff) / other_expected.max(1.0);
560        bins_used += 1;
561    }
562
563    // Degrees of freedom = number of bins - 1 (constraint: totals match).
564    let dof = if bins_used > 1 { bins_used - 1 } else { 0 };
565
566    (chi2, dof)
567}
568
569/// Approximate the chi-squared p-value using the Wilson-Hilferty
570/// normal approximation.
571///
572/// For a chi-squared random variable X with k degrees of freedom:
573///
574/// ```text
575/// z = ((X/k)^(1/3) - (1 - 2/(9k))) / sqrt(2/(9k))
576/// ```
577///
578/// The p-value is then `1 - Phi(z)` where `Phi` is the standard normal CDF.
579///
580/// This approximation is accurate for k >= 3 and reasonable for k >= 1.
581pub fn chi_squared_p_value(statistic: f64, dof: usize) -> f64 {
582    if dof == 0 {
583        return 1.0;
584    }
585    if statistic <= 0.0 {
586        return 1.0;
587    }
588
589    let k = dof as f64;
590
591    // Wilson-Hilferty approximation.
592    let term = 2.0 / (9.0 * k);
593    let cube_root = (statistic / k).powf(1.0 / 3.0);
594    let z = (cube_root - (1.0 - term)) / term.sqrt();
595
596    // Standard normal survival function: 1 - Phi(z).
597    // Use the complementary error function approximation.
598    1.0 - standard_normal_cdf(z)
599}
600
601// ---------------------------------------------------------------------------
602// Pearson correlation
603// ---------------------------------------------------------------------------
604
605/// Compute the Pearson correlation coefficient between two distributions.
606///
607/// Returns a value in [-1, 1]. Returns 0.0 if either distribution has zero
608/// variance (constant).
609fn pearson_correlation(
610    p: &HashMap<Vec<bool>, f64>,
611    q: &HashMap<Vec<bool>, f64>,
612) -> f64 {
613    let mut all_keys: Vec<&Vec<bool>> =
614        p.keys().chain(q.keys()).collect();
615    all_keys.sort();
616    all_keys.dedup();
617
618    if all_keys.is_empty() {
619        return 0.0;
620    }
621
622    let n = all_keys.len() as f64;
623
624    let p_vals: Vec<f64> = all_keys
625        .iter()
626        .map(|k| p.get(*k).copied().unwrap_or(0.0))
627        .collect();
628    let q_vals: Vec<f64> = all_keys
629        .iter()
630        .map(|k| q.get(*k).copied().unwrap_or(0.0))
631        .collect();
632
633    let p_mean: f64 = p_vals.iter().sum::<f64>() / n;
634    let q_mean: f64 = q_vals.iter().sum::<f64>() / n;
635
636    let mut cov = 0.0;
637    let mut var_p = 0.0;
638    let mut var_q = 0.0;
639
640    for i in 0..all_keys.len() {
641        let dp = p_vals[i] - p_mean;
642        let dq = q_vals[i] - q_mean;
643        cov += dp * dq;
644        var_p += dp * dp;
645        var_q += dq * dq;
646    }
647
648    if var_p < 1e-30 || var_q < 1e-30 {
649        return 0.0;
650    }
651
652    cov / (var_p.sqrt() * var_q.sqrt())
653}
654
655// ---------------------------------------------------------------------------
656// Standard normal CDF approximation
657// ---------------------------------------------------------------------------
658
659/// Approximate the standard normal CDF using the Abramowitz and Stegun
660/// rational approximation (formula 26.2.17).
661///
662/// Accurate to approximately 7.5 decimal digits.
663fn standard_normal_cdf(x: f64) -> f64 {
664    if x < -8.0 {
665        return 0.0;
666    }
667    if x > 8.0 {
668        return 1.0;
669    }
670
671    // Constants for the approximation.
672    let a1 = 0.254829592;
673    let a2 = -0.284496736;
674    let a3 = 1.421413741;
675    let a4 = -1.453152027;
676    let a5 = 1.061405429;
677    let p = 0.3275911;
678
679    let sign = if x < 0.0 { -1.0 } else { 1.0 };
680    let abs_x = x.abs() / std::f64::consts::SQRT_2;
681
682    let t = 1.0 / (1.0 + p * abs_x);
683    let t2 = t * t;
684    let t3 = t2 * t;
685    let t4 = t3 * t;
686    let t5 = t4 * t;
687
688    let erf_approx =
689        1.0 - (a1 * t + a2 * t2 + a3 * t3 + a4 * t4 + a5 * t5)
690            * (-abs_x * abs_x).exp();
691
692    0.5 * (1.0 + sign * erf_approx)
693}
694
695// ---------------------------------------------------------------------------
696// Tests
697// ---------------------------------------------------------------------------
698
699#[cfg(test)]
700mod tests {
701    use super::*;
702    use crate::circuit::QuantumCircuit;
703
704    // -- Helper to build a count map from a list of (bitstring, count) pairs --
705
706    fn make_counts(
707        entries: &[(&[bool], usize)],
708    ) -> HashMap<Vec<bool>, usize> {
709        entries
710            .iter()
711            .map(|(bits, count)| (bits.to_vec(), *count))
712            .collect()
713    }
714
715    // -----------------------------------------------------------------------
716    // is_clifford_circuit
717    // -----------------------------------------------------------------------
718
719    #[test]
720    fn clifford_circuit_returns_true_for_clifford_only() {
721        let mut circ = QuantumCircuit::new(3);
722        circ.h(0).cnot(0, 1).s(2).x(0).y(1).z(2);
723        circ.cz(0, 2).swap(1, 2);
724        circ.measure(0).measure(1).measure(2);
725        assert!(is_clifford_circuit(&circ));
726    }
727
728    #[test]
729    fn clifford_circuit_returns_false_with_t_gate() {
730        let mut circ = QuantumCircuit::new(2);
731        circ.h(0).t(0).cnot(0, 1);
732        assert!(!is_clifford_circuit(&circ));
733    }
734
735    #[test]
736    fn clifford_circuit_returns_true_for_sdg_gate() {
737        let mut circ = QuantumCircuit::new(1);
738        circ.h(0);
739        circ.add_gate(Gate::Sdg(0));
740        assert!(is_clifford_circuit(&circ));
741    }
742
743    #[test]
744    fn clifford_circuit_returns_false_for_rx_gate() {
745        let mut circ = QuantumCircuit::new(1);
746        circ.rx(0, 0.5);
747        assert!(!is_clifford_circuit(&circ));
748    }
749
750    #[test]
751    fn clifford_circuit_returns_true_with_reset_and_barrier() {
752        let mut circ = QuantumCircuit::new(2);
753        circ.h(0).cnot(0, 1).barrier();
754        circ.reset(0).measure(1);
755        assert!(is_clifford_circuit(&circ));
756    }
757
758    // -----------------------------------------------------------------------
759    // normalize_counts
760    // -----------------------------------------------------------------------
761
762    #[test]
763    fn normalize_counts_produces_probabilities() {
764        let counts = make_counts(&[
765            (&[false, false], 50),
766            (&[true, true], 50),
767        ]);
768        let probs = normalize_counts(&counts);
769        assert!((probs[&vec![false, false]] - 0.5).abs() < 1e-10);
770        assert!((probs[&vec![true, true]] - 0.5).abs() < 1e-10);
771    }
772
773    #[test]
774    fn normalize_counts_empty_returns_empty() {
775        let counts: HashMap<Vec<bool>, usize> = HashMap::new();
776        let probs = normalize_counts(&counts);
777        assert!(probs.is_empty());
778    }
779
780    // -----------------------------------------------------------------------
781    // tvd
782    // -----------------------------------------------------------------------
783
784    #[test]
785    fn identical_distributions_have_zero_tvd() {
786        let p: HashMap<Vec<bool>, f64> = [
787            (vec![false, false], 0.5),
788            (vec![true, true], 0.5),
789        ]
790        .into_iter()
791        .collect();
792
793        let distance = tvd(&p, &p);
794        assert!(
795            distance.abs() < 1e-15,
796            "TVD of identical distributions should be 0, got {}",
797            distance
798        );
799    }
800
801    #[test]
802    fn completely_different_distributions_have_tvd_near_one() {
803        let p: HashMap<Vec<bool>, f64> =
804            [(vec![false], 1.0)].into_iter().collect();
805        let q: HashMap<Vec<bool>, f64> =
806            [(vec![true], 1.0)].into_iter().collect();
807
808        let distance = tvd(&p, &q);
809        assert!(
810            (distance - 1.0).abs() < 1e-15,
811            "TVD of disjoint distributions should be 1, got {}",
812            distance
813        );
814    }
815
816    #[test]
817    fn tvd_partial_overlap() {
818        let p: HashMap<Vec<bool>, f64> = [
819            (vec![false], 0.7),
820            (vec![true], 0.3),
821        ]
822        .into_iter()
823        .collect();
824
825        let q: HashMap<Vec<bool>, f64> = [
826            (vec![false], 0.3),
827            (vec![true], 0.7),
828        ]
829        .into_iter()
830        .collect();
831
832        let distance = tvd(&p, &q);
833        // TVD = 0.5 * (|0.7-0.3| + |0.3-0.7|) = 0.5 * (0.4 + 0.4) = 0.4
834        assert!(
835            (distance - 0.4).abs() < 1e-15,
836            "Expected TVD=0.4, got {}",
837            distance
838        );
839    }
840
841    // -----------------------------------------------------------------------
842    // chi_squared_statistic and chi_squared_p_value
843    // -----------------------------------------------------------------------
844
845    #[test]
846    fn chi_squared_perfect_fit_has_low_statistic() {
847        let observed = make_counts(&[
848            (&[false], 500),
849            (&[true], 500),
850        ]);
851        let expected: HashMap<Vec<bool>, f64> = [
852            (vec![false], 0.5),
853            (vec![true], 0.5),
854        ]
855        .into_iter()
856        .collect();
857
858        let (stat, dof) =
859            chi_squared_statistic(&observed, &expected, 1000);
860        assert!(
861            stat < 1.0,
862            "Perfect fit should have near-zero chi2, got {}",
863            stat
864        );
865        assert_eq!(dof, 1);
866
867        let pval = chi_squared_p_value(stat, dof);
868        assert!(
869            pval > 0.05,
870            "Perfect fit p-value should be large, got {}",
871            pval
872        );
873    }
874
875    #[test]
876    fn chi_squared_bad_fit_has_high_statistic() {
877        // Observed is heavily biased; expected is uniform.
878        let observed = make_counts(&[
879            (&[false], 900),
880            (&[true], 100),
881        ]);
882        let expected: HashMap<Vec<bool>, f64> = [
883            (vec![false], 0.5),
884            (vec![true], 0.5),
885        ]
886        .into_iter()
887        .collect();
888
889        let (stat, dof) =
890            chi_squared_statistic(&observed, &expected, 1000);
891        assert!(
892            stat > 10.0,
893            "Bad fit should have large chi2, got {}",
894            stat
895        );
896        assert_eq!(dof, 1);
897
898        let pval = chi_squared_p_value(stat, dof);
899        assert!(
900            pval < 0.01,
901            "Bad fit p-value should be very small, got {}",
902            pval
903        );
904    }
905
906    #[test]
907    fn chi_squared_p_value_zero_dof() {
908        let pval = chi_squared_p_value(5.0, 0);
909        assert!((pval - 1.0).abs() < 1e-10);
910    }
911
912    #[test]
913    fn chi_squared_p_value_zero_statistic() {
914        let pval = chi_squared_p_value(0.0, 5);
915        assert!((pval - 1.0).abs() < 1e-10);
916    }
917
918    // -----------------------------------------------------------------------
919    // verify_against_reference
920    // -----------------------------------------------------------------------
921
922    #[test]
923    fn identical_distributions_pass_verification() {
924        let counts = make_counts(&[
925            (&[false, false], 500),
926            (&[true, true], 500),
927        ]);
928        let result = verify_against_reference(&counts, &counts, 0.01);
929        assert!(result.passed);
930        assert!(
931            result.total_variation_distance.unwrap() < 1e-10,
932            "TVD should be 0 for identical counts"
933        );
934    }
935
936    #[test]
937    fn very_different_distributions_fail_verification() {
938        let primary = make_counts(&[(&[false], 1000)]);
939        let reference = make_counts(&[(&[true], 1000)]);
940
941        let result =
942            verify_against_reference(&primary, &reference, 0.1);
943        assert!(!result.passed);
944        assert!(
945            (result.total_variation_distance.unwrap() - 1.0).abs()
946                < 1e-10,
947            "TVD should be 1 for disjoint distributions"
948        );
949    }
950
951    #[test]
952    fn discrepancies_sorted_by_absolute_difference() {
953        let primary = make_counts(&[
954            (&[false, false], 400),
955            (&[false, true], 300),
956            (&[true, false], 200),
957            (&[true, true], 100),
958        ]);
959        let reference = make_counts(&[
960            (&[false, false], 250),
961            (&[false, true], 250),
962            (&[true, false], 250),
963            (&[true, true], 250),
964        ]);
965
966        let result =
967            verify_against_reference(&primary, &reference, 0.5);
968
969        // Verify discrepancies are sorted descending by absolute_difference.
970        for i in 1..result.discrepancies.len() {
971            assert!(
972                result.discrepancies[i - 1].absolute_difference
973                    >= result.discrepancies[i].absolute_difference,
974                "Discrepancies should be sorted descending by \
975                 absolute_difference: {} < {}",
976                result.discrepancies[i - 1].absolute_difference,
977                result.discrepancies[i].absolute_difference
978            );
979        }
980
981        // The largest discrepancy should be for [false, false] or [true, true].
982        // primary [false,false] = 0.4, reference = 0.25, diff = 0.15
983        // primary [true,true] = 0.1, reference = 0.25, diff = 0.15
984        // primary [false,true] = 0.3, reference = 0.25, diff = 0.05
985        // primary [true,false] = 0.2, reference = 0.25, diff = 0.05
986        assert!(
987            result.discrepancies[0].absolute_difference >= 0.14,
988            "Top discrepancy should have absolute_difference >= 0.14, got {}",
989            result.discrepancies[0].absolute_difference
990        );
991    }
992
993    // -----------------------------------------------------------------------
994    // run_stabilizer_shots
995    // -----------------------------------------------------------------------
996
997    #[test]
998    fn stabilizer_shots_zero_state_gives_all_zeros() {
999        // Circuit with no gates, just measure all qubits.
1000        let mut circ = QuantumCircuit::new(3);
1001        circ.measure(0).measure(1).measure(2);
1002
1003        let counts = run_stabilizer_shots(&circ, 100, 42);
1004
1005        // All outcomes should be [false, false, false].
1006        assert_eq!(counts.len(), 1, "Should have exactly one outcome");
1007        assert_eq!(
1008            counts[&vec![false, false, false]],
1009            100,
1010            "All 100 shots should give |000>"
1011        );
1012    }
1013
1014    #[test]
1015    fn stabilizer_shots_bell_state_gives_correlated_results() {
1016        let mut circ = QuantumCircuit::new(2);
1017        circ.h(0).cnot(0, 1).measure(0).measure(1);
1018
1019        let counts = run_stabilizer_shots(&circ, 1000, 42);
1020
1021        // A Bell state should only produce |00> and |11>.
1022        for (bits, _count) in &counts {
1023            assert_eq!(
1024                bits[0], bits[1],
1025                "Bell state qubits must be correlated, got {:?}",
1026                bits
1027            );
1028        }
1029
1030        // Both outcomes should appear (with high probability at 1000 shots).
1031        assert!(
1032            counts.contains_key(&vec![false, false]),
1033            "Should see |00> outcome"
1034        );
1035        assert!(
1036            counts.contains_key(&vec![true, true]),
1037            "Should see |11> outcome"
1038        );
1039
1040        // Check roughly 50/50 split (within a generous margin).
1041        let count_00 =
1042            counts.get(&vec![false, false]).copied().unwrap_or(0);
1043        let count_11 =
1044            counts.get(&vec![true, true]).copied().unwrap_or(0);
1045        assert_eq!(count_00 + count_11, 1000);
1046        assert!(
1047            count_00 > 350 && count_00 < 650,
1048            "Expected roughly 50/50, got {}/{}",
1049            count_00,
1050            count_11
1051        );
1052    }
1053
1054    #[test]
1055    fn stabilizer_shots_implicit_measurement() {
1056        // No explicit measure gates: all qubits measured at the end.
1057        let mut circ = QuantumCircuit::new(2);
1058        circ.h(0).cnot(0, 1);
1059
1060        let counts = run_stabilizer_shots(&circ, 500, 99);
1061
1062        // Bell state: only |00> and |11> should appear.
1063        for (bits, _count) in &counts {
1064            assert_eq!(bits[0], bits[1], "Bell state must be correlated");
1065        }
1066    }
1067
1068    // -----------------------------------------------------------------------
1069    // verify_circuit (integration tests)
1070    // -----------------------------------------------------------------------
1071
1072    #[test]
1073    fn bell_state_passes_exact_verification() {
1074        let mut circ = QuantumCircuit::new(2);
1075        circ.h(0).cnot(0, 1).measure(0).measure(1);
1076
1077        let result = verify_circuit(&circ, 2000, 42);
1078
1079        assert_eq!(result.primary_backend, BackendType::StateVector);
1080        assert_eq!(
1081            result.reference_backend,
1082            Some(BackendType::Stabilizer)
1083        );
1084        assert!(
1085            result.passed,
1086            "Bell state should pass verification: {}",
1087            result.explanation
1088        );
1089        // Should be Exact or Statistical (both acceptable for Clifford).
1090        assert!(
1091            result.level == VerificationLevel::Exact
1092                || result.level == VerificationLevel::Statistical,
1093            "Expected Exact or Statistical, got {:?}",
1094            result.level
1095        );
1096    }
1097
1098    #[test]
1099    fn non_clifford_circuit_is_skipped() {
1100        let mut circ = QuantumCircuit::new(2);
1101        circ.h(0).t(0).cnot(0, 1).measure(0).measure(1);
1102
1103        let result = verify_circuit(&circ, 1000, 42);
1104
1105        assert_eq!(result.level, VerificationLevel::Skipped);
1106        assert!(result.reference_backend.is_none());
1107        assert!(
1108            result.explanation.contains("non-Clifford"),
1109            "Explanation should mention non-Clifford gates: {}",
1110            result.explanation
1111        );
1112    }
1113
1114    #[test]
1115    fn ghz_state_passes_verification() {
1116        let mut circ = QuantumCircuit::new(4);
1117        circ.h(0);
1118        circ.cnot(0, 1).cnot(1, 2).cnot(2, 3);
1119        circ.measure(0).measure(1).measure(2).measure(3);
1120
1121        let result = verify_circuit(&circ, 2000, 123);
1122
1123        assert!(
1124            result.passed,
1125            "GHZ state should pass verification: {}",
1126            result.explanation
1127        );
1128    }
1129
1130    // -----------------------------------------------------------------------
1131    // Edge cases
1132    // -----------------------------------------------------------------------
1133
1134    #[test]
1135    fn empty_circuit_passes_verification() {
1136        let mut circ = QuantumCircuit::new(2);
1137        circ.measure(0).measure(1);
1138
1139        let result = verify_circuit(&circ, 100, 0);
1140
1141        assert!(result.passed);
1142        // Pure Clifford (only measurements), should do cross-backend check.
1143        assert_eq!(
1144            result.reference_backend,
1145            Some(BackendType::Stabilizer)
1146        );
1147    }
1148
1149    #[test]
1150    fn pearson_correlation_identical_distributions() {
1151        let p: HashMap<Vec<bool>, f64> = [
1152            (vec![false], 0.3),
1153            (vec![true], 0.7),
1154        ]
1155        .into_iter()
1156        .collect();
1157
1158        let corr = pearson_correlation(&p, &p);
1159        assert!(
1160            (corr - 1.0).abs() < 1e-10,
1161            "Identical distributions should have correlation 1.0, got {}",
1162            corr
1163        );
1164    }
1165
1166    #[test]
1167    fn standard_normal_cdf_known_values() {
1168        // Phi(0) = 0.5
1169        assert!(
1170            (standard_normal_cdf(0.0) - 0.5).abs() < 1e-6,
1171            "CDF(0) should be 0.5"
1172        );
1173        // Phi(-inf) -> 0
1174        assert!(
1175            standard_normal_cdf(-10.0) < 1e-10,
1176            "CDF(-10) should be near 0"
1177        );
1178        // Phi(+inf) -> 1
1179        assert!(
1180            (standard_normal_cdf(10.0) - 1.0).abs() < 1e-10,
1181            "CDF(10) should be near 1"
1182        );
1183        // Phi(1.96) ~ 0.975
1184        assert!(
1185            (standard_normal_cdf(1.96) - 0.975).abs() < 0.01,
1186            "CDF(1.96) should be near 0.975, got {}",
1187            standard_normal_cdf(1.96)
1188        );
1189    }
1190}