use crate::backend::{analyze_circuit, BackendType};
use crate::circuit::QuantumCircuit;
use crate::gate::Gate;
use crate::simulator::Simulator;
use crate::stabilizer::StabilizerState;
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VerificationLevel {
Exact,
Statistical,
Trend,
Skipped,
}
#[derive(Debug, Clone)]
pub struct VerificationResult {
pub level: VerificationLevel,
pub passed: bool,
pub primary_backend: BackendType,
pub reference_backend: Option<BackendType>,
pub total_variation_distance: Option<f64>,
pub chi_squared_p_value: Option<f64>,
pub correlation: Option<f64>,
pub explanation: String,
pub discrepancies: Vec<Discrepancy>,
}
#[derive(Debug, Clone)]
pub struct Discrepancy {
pub bitstring: Vec<bool>,
pub primary_probability: f64,
pub reference_probability: f64,
pub absolute_difference: f64,
}
pub fn verify_circuit(
circuit: &QuantumCircuit,
shots: u32,
seed: u64,
) -> VerificationResult {
let analysis = analyze_circuit(circuit);
let num_qubits = circuit.num_qubits();
let is_clifford = is_clifford_circuit(circuit);
if is_clifford && num_qubits <= 25 {
let sv_result = Simulator::run_shots(circuit, shots, Some(seed));
let sv_counts = match sv_result {
Ok(r) => r.counts,
Err(e) => {
return VerificationResult {
level: VerificationLevel::Skipped,
passed: false,
primary_backend: BackendType::StateVector,
reference_backend: None,
total_variation_distance: None,
chi_squared_p_value: None,
correlation: None,
explanation: format!(
"State-vector simulation failed: {}",
e
),
discrepancies: vec![],
};
}
};
let stab_counts = run_stabilizer_shots(circuit, shots, seed);
let mut result = verify_against_reference(
&sv_counts,
&stab_counts,
0.0, );
result.primary_backend = BackendType::StateVector;
result.reference_backend = Some(BackendType::Stabilizer);
if result.passed
&& result
.total_variation_distance
.map_or(false, |d| d == 0.0)
{
result.level = VerificationLevel::Exact;
result.explanation = format!(
"Exact match: {}-qubit Clifford circuit verified across \
state-vector and stabilizer backends ({} shots, \
clifford_fraction={:.2})",
num_qubits, shots, analysis.clifford_fraction
);
} else {
let tight_tolerance = 0.05;
let mut stat_result = verify_against_reference(
&sv_counts,
&stab_counts,
tight_tolerance,
);
stat_result.primary_backend = BackendType::StateVector;
stat_result.reference_backend = Some(BackendType::Stabilizer);
stat_result.explanation = format!(
"Statistical comparison of {}-qubit Clifford circuit across \
state-vector and stabilizer backends ({} shots, TVD={:.6})",
num_qubits,
shots,
stat_result
.total_variation_distance
.unwrap_or(0.0)
);
return stat_result;
}
return result;
}
if !is_clifford && num_qubits <= 25 {
return VerificationResult {
level: VerificationLevel::Skipped,
passed: true,
primary_backend: BackendType::StateVector,
reference_backend: None,
total_variation_distance: None,
chi_squared_p_value: None,
correlation: None,
explanation: format!(
"Verification skipped: {}-qubit circuit contains non-Clifford \
gates (clifford_fraction={:.2}, {} non-Clifford gates). \
No reference backend available for cross-validation.",
num_qubits,
analysis.clifford_fraction,
analysis.non_clifford_gates
),
discrepancies: vec![],
};
}
VerificationResult {
level: VerificationLevel::Skipped,
passed: true,
primary_backend: analysis.recommended_backend,
reference_backend: None,
total_variation_distance: None,
chi_squared_p_value: None,
correlation: None,
explanation: format!(
"Verification skipped: {}-qubit circuit exceeds state-vector \
capacity for cross-backend comparison.",
num_qubits
),
discrepancies: vec![],
}
}
pub fn verify_against_reference(
primary: &HashMap<Vec<bool>, usize>,
reference: &HashMap<Vec<bool>, usize>,
tolerance: f64,
) -> VerificationResult {
let p_norm = normalize_counts(primary);
let q_norm = normalize_counts(reference);
let distance = tvd(&p_norm, &q_norm);
let total_ref: usize = reference.values().sum();
let (chi2_stat, dof) =
chi_squared_statistic(primary, &q_norm, total_ref);
let p_value = if dof > 0 {
chi_squared_p_value(chi2_stat, dof)
} else {
1.0
};
let corr = pearson_correlation(&p_norm, &q_norm);
let mut all_keys: Vec<&Vec<bool>> =
p_norm.keys().chain(q_norm.keys()).collect();
all_keys.sort();
all_keys.dedup();
let mut discrepancies: Vec<Discrepancy> = all_keys
.iter()
.map(|key| {
let pp = p_norm.get(*key).copied().unwrap_or(0.0);
let rp = q_norm.get(*key).copied().unwrap_or(0.0);
Discrepancy {
bitstring: (*key).clone(),
primary_probability: pp,
reference_probability: rp,
absolute_difference: (pp - rp).abs(),
}
})
.filter(|d| d.absolute_difference > 1e-15)
.collect();
discrepancies
.sort_by(|a, b| b.absolute_difference.partial_cmp(&a.absolute_difference).unwrap());
let passed = distance <= tolerance;
let level = if tolerance == 0.0 && passed {
VerificationLevel::Exact
} else {
VerificationLevel::Statistical
};
let explanation = if passed {
format!(
"Verification passed: TVD={:.6}, chi2 p-value={:.4}, \
correlation={:.4}, tolerance={:.6}",
distance, p_value, corr, tolerance
)
} else {
format!(
"Verification FAILED: TVD={:.6} exceeds tolerance={:.6}, \
chi2 p-value={:.4}, correlation={:.4}, \
{} discrepancies found",
distance,
tolerance,
p_value,
corr,
discrepancies.len()
)
};
VerificationResult {
level,
passed,
primary_backend: BackendType::Auto,
reference_backend: None,
total_variation_distance: Some(distance),
chi_squared_p_value: Some(p_value),
correlation: Some(corr),
explanation,
discrepancies,
}
}
pub fn is_clifford_circuit(circuit: &QuantumCircuit) -> bool {
circuit.gates().iter().all(|gate| is_clifford_gate(gate))
}
fn is_clifford_gate(gate: &Gate) -> bool {
matches!(
gate,
Gate::H(_)
| Gate::X(_)
| Gate::Y(_)
| Gate::Z(_)
| Gate::S(_)
| Gate::Sdg(_)
| Gate::CNOT(_, _)
| Gate::CZ(_, _)
| Gate::SWAP(_, _)
| Gate::Measure(_)
| Gate::Reset(_)
| Gate::Barrier
)
}
pub fn run_stabilizer_shots(
circuit: &QuantumCircuit,
shots: u32,
seed: u64,
) -> HashMap<Vec<bool>, usize> {
let n = circuit.num_qubits() as usize;
let mut counts: HashMap<Vec<bool>, usize> = HashMap::new();
let has_measurements = circuit
.gates()
.iter()
.any(|g| matches!(g, Gate::Measure(_)));
for shot in 0..shots {
let shot_seed = seed.wrapping_add(shot as u64);
let mut state = StabilizerState::new_with_seed(n, shot_seed)
.expect("failed to create stabilizer state");
let mut measured_bits: Vec<Option<bool>> = vec![None; n];
for gate in circuit.gates() {
match gate {
Gate::Reset(q) => {
let qubit = *q as usize;
let outcome = state
.measure(qubit)
.expect("stabilizer measurement failed");
if outcome.result {
state.x_gate(qubit);
}
measured_bits[qubit] = None;
}
Gate::Measure(q) => {
let outcomes = state
.apply_gate(gate)
.expect("stabilizer gate application failed");
if let Some(outcome) = outcomes.first() {
measured_bits[*q as usize] = Some(outcome.result);
}
}
_ => {
state
.apply_gate(gate)
.expect("stabilizer gate application failed");
}
}
}
if !has_measurements {
for q in 0..n {
let outcome = state
.measure(q)
.expect("stabilizer measurement failed");
measured_bits[q] = Some(outcome.result);
}
}
let bits: Vec<bool> = measured_bits
.iter()
.map(|mb| mb.unwrap_or(false))
.collect();
*counts.entry(bits).or_insert(0) += 1;
}
counts
}
pub fn normalize_counts(
counts: &HashMap<Vec<bool>, usize>,
) -> HashMap<Vec<bool>, f64> {
let total: usize = counts.values().sum();
if total == 0 {
return HashMap::new();
}
let total_f = total as f64;
counts
.iter()
.map(|(k, &v)| (k.clone(), v as f64 / total_f))
.collect()
}
pub fn tvd(
p: &HashMap<Vec<bool>, f64>,
q: &HashMap<Vec<bool>, f64>,
) -> f64 {
let mut all_keys: Vec<&Vec<bool>> =
p.keys().chain(q.keys()).collect();
all_keys.sort();
all_keys.dedup();
let sum: f64 = all_keys
.iter()
.map(|key| {
let pv = p.get(*key).copied().unwrap_or(0.0);
let qv = q.get(*key).copied().unwrap_or(0.0);
(pv - qv).abs()
})
.sum();
0.5 * sum
}
pub fn chi_squared_statistic(
observed: &HashMap<Vec<bool>, usize>,
expected_probs: &HashMap<Vec<bool>, f64>,
_total: usize,
) -> (f64, usize) {
let obs_total: usize = observed.values().sum();
if obs_total == 0 {
return (0.0, 0);
}
let obs_total_f = obs_total as f64;
let mut all_keys: Vec<&Vec<bool>> = observed
.keys()
.chain(expected_probs.keys())
.collect();
all_keys.sort();
all_keys.dedup();
let mut chi2 = 0.0;
let mut bins_used = 0usize;
let mut other_observed = 0.0;
let mut other_expected = 0.0;
for key in &all_keys {
let obs = observed.get(*key).copied().unwrap_or(0) as f64;
let exp_prob = expected_probs.get(*key).copied().unwrap_or(0.0);
let exp = exp_prob * obs_total_f;
if exp < 5.0 {
other_observed += obs;
other_expected += exp;
} else {
let diff = obs - exp;
chi2 += (diff * diff) / exp;
bins_used += 1;
}
}
if other_expected >= 5.0 {
let diff = other_observed - other_expected;
chi2 += (diff * diff) / other_expected;
bins_used += 1;
} else if other_expected > 0.0 && other_observed > 0.0 {
let diff = other_observed - other_expected;
chi2 += (diff * diff) / other_expected.max(1.0);
bins_used += 1;
}
let dof = if bins_used > 1 { bins_used - 1 } else { 0 };
(chi2, dof)
}
pub fn chi_squared_p_value(statistic: f64, dof: usize) -> f64 {
if dof == 0 {
return 1.0;
}
if statistic <= 0.0 {
return 1.0;
}
let k = dof as f64;
let term = 2.0 / (9.0 * k);
let cube_root = (statistic / k).powf(1.0 / 3.0);
let z = (cube_root - (1.0 - term)) / term.sqrt();
1.0 - standard_normal_cdf(z)
}
fn pearson_correlation(
p: &HashMap<Vec<bool>, f64>,
q: &HashMap<Vec<bool>, f64>,
) -> f64 {
let mut all_keys: Vec<&Vec<bool>> =
p.keys().chain(q.keys()).collect();
all_keys.sort();
all_keys.dedup();
if all_keys.is_empty() {
return 0.0;
}
let n = all_keys.len() as f64;
let p_vals: Vec<f64> = all_keys
.iter()
.map(|k| p.get(*k).copied().unwrap_or(0.0))
.collect();
let q_vals: Vec<f64> = all_keys
.iter()
.map(|k| q.get(*k).copied().unwrap_or(0.0))
.collect();
let p_mean: f64 = p_vals.iter().sum::<f64>() / n;
let q_mean: f64 = q_vals.iter().sum::<f64>() / n;
let mut cov = 0.0;
let mut var_p = 0.0;
let mut var_q = 0.0;
for i in 0..all_keys.len() {
let dp = p_vals[i] - p_mean;
let dq = q_vals[i] - q_mean;
cov += dp * dq;
var_p += dp * dp;
var_q += dq * dq;
}
if var_p < 1e-30 || var_q < 1e-30 {
return 0.0;
}
cov / (var_p.sqrt() * var_q.sqrt())
}
fn standard_normal_cdf(x: f64) -> f64 {
if x < -8.0 {
return 0.0;
}
if x > 8.0 {
return 1.0;
}
let a1 = 0.254829592;
let a2 = -0.284496736;
let a3 = 1.421413741;
let a4 = -1.453152027;
let a5 = 1.061405429;
let p = 0.3275911;
let sign = if x < 0.0 { -1.0 } else { 1.0 };
let abs_x = x.abs() / std::f64::consts::SQRT_2;
let t = 1.0 / (1.0 + p * abs_x);
let t2 = t * t;
let t3 = t2 * t;
let t4 = t3 * t;
let t5 = t4 * t;
let erf_approx =
1.0 - (a1 * t + a2 * t2 + a3 * t3 + a4 * t4 + a5 * t5)
* (-abs_x * abs_x).exp();
0.5 * (1.0 + sign * erf_approx)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::circuit::QuantumCircuit;
fn make_counts(
entries: &[(&[bool], usize)],
) -> HashMap<Vec<bool>, usize> {
entries
.iter()
.map(|(bits, count)| (bits.to_vec(), *count))
.collect()
}
#[test]
fn clifford_circuit_returns_true_for_clifford_only() {
let mut circ = QuantumCircuit::new(3);
circ.h(0).cnot(0, 1).s(2).x(0).y(1).z(2);
circ.cz(0, 2).swap(1, 2);
circ.measure(0).measure(1).measure(2);
assert!(is_clifford_circuit(&circ));
}
#[test]
fn clifford_circuit_returns_false_with_t_gate() {
let mut circ = QuantumCircuit::new(2);
circ.h(0).t(0).cnot(0, 1);
assert!(!is_clifford_circuit(&circ));
}
#[test]
fn clifford_circuit_returns_true_for_sdg_gate() {
let mut circ = QuantumCircuit::new(1);
circ.h(0);
circ.add_gate(Gate::Sdg(0));
assert!(is_clifford_circuit(&circ));
}
#[test]
fn clifford_circuit_returns_false_for_rx_gate() {
let mut circ = QuantumCircuit::new(1);
circ.rx(0, 0.5);
assert!(!is_clifford_circuit(&circ));
}
#[test]
fn clifford_circuit_returns_true_with_reset_and_barrier() {
let mut circ = QuantumCircuit::new(2);
circ.h(0).cnot(0, 1).barrier();
circ.reset(0).measure(1);
assert!(is_clifford_circuit(&circ));
}
#[test]
fn normalize_counts_produces_probabilities() {
let counts = make_counts(&[
(&[false, false], 50),
(&[true, true], 50),
]);
let probs = normalize_counts(&counts);
assert!((probs[&vec![false, false]] - 0.5).abs() < 1e-10);
assert!((probs[&vec![true, true]] - 0.5).abs() < 1e-10);
}
#[test]
fn normalize_counts_empty_returns_empty() {
let counts: HashMap<Vec<bool>, usize> = HashMap::new();
let probs = normalize_counts(&counts);
assert!(probs.is_empty());
}
#[test]
fn identical_distributions_have_zero_tvd() {
let p: HashMap<Vec<bool>, f64> = [
(vec![false, false], 0.5),
(vec![true, true], 0.5),
]
.into_iter()
.collect();
let distance = tvd(&p, &p);
assert!(
distance.abs() < 1e-15,
"TVD of identical distributions should be 0, got {}",
distance
);
}
#[test]
fn completely_different_distributions_have_tvd_near_one() {
let p: HashMap<Vec<bool>, f64> =
[(vec![false], 1.0)].into_iter().collect();
let q: HashMap<Vec<bool>, f64> =
[(vec![true], 1.0)].into_iter().collect();
let distance = tvd(&p, &q);
assert!(
(distance - 1.0).abs() < 1e-15,
"TVD of disjoint distributions should be 1, got {}",
distance
);
}
#[test]
fn tvd_partial_overlap() {
let p: HashMap<Vec<bool>, f64> = [
(vec![false], 0.7),
(vec![true], 0.3),
]
.into_iter()
.collect();
let q: HashMap<Vec<bool>, f64> = [
(vec![false], 0.3),
(vec![true], 0.7),
]
.into_iter()
.collect();
let distance = tvd(&p, &q);
assert!(
(distance - 0.4).abs() < 1e-15,
"Expected TVD=0.4, got {}",
distance
);
}
#[test]
fn chi_squared_perfect_fit_has_low_statistic() {
let observed = make_counts(&[
(&[false], 500),
(&[true], 500),
]);
let expected: HashMap<Vec<bool>, f64> = [
(vec![false], 0.5),
(vec![true], 0.5),
]
.into_iter()
.collect();
let (stat, dof) =
chi_squared_statistic(&observed, &expected, 1000);
assert!(
stat < 1.0,
"Perfect fit should have near-zero chi2, got {}",
stat
);
assert_eq!(dof, 1);
let pval = chi_squared_p_value(stat, dof);
assert!(
pval > 0.05,
"Perfect fit p-value should be large, got {}",
pval
);
}
#[test]
fn chi_squared_bad_fit_has_high_statistic() {
let observed = make_counts(&[
(&[false], 900),
(&[true], 100),
]);
let expected: HashMap<Vec<bool>, f64> = [
(vec![false], 0.5),
(vec![true], 0.5),
]
.into_iter()
.collect();
let (stat, dof) =
chi_squared_statistic(&observed, &expected, 1000);
assert!(
stat > 10.0,
"Bad fit should have large chi2, got {}",
stat
);
assert_eq!(dof, 1);
let pval = chi_squared_p_value(stat, dof);
assert!(
pval < 0.01,
"Bad fit p-value should be very small, got {}",
pval
);
}
#[test]
fn chi_squared_p_value_zero_dof() {
let pval = chi_squared_p_value(5.0, 0);
assert!((pval - 1.0).abs() < 1e-10);
}
#[test]
fn chi_squared_p_value_zero_statistic() {
let pval = chi_squared_p_value(0.0, 5);
assert!((pval - 1.0).abs() < 1e-10);
}
#[test]
fn identical_distributions_pass_verification() {
let counts = make_counts(&[
(&[false, false], 500),
(&[true, true], 500),
]);
let result = verify_against_reference(&counts, &counts, 0.01);
assert!(result.passed);
assert!(
result.total_variation_distance.unwrap() < 1e-10,
"TVD should be 0 for identical counts"
);
}
#[test]
fn very_different_distributions_fail_verification() {
let primary = make_counts(&[(&[false], 1000)]);
let reference = make_counts(&[(&[true], 1000)]);
let result =
verify_against_reference(&primary, &reference, 0.1);
assert!(!result.passed);
assert!(
(result.total_variation_distance.unwrap() - 1.0).abs()
< 1e-10,
"TVD should be 1 for disjoint distributions"
);
}
#[test]
fn discrepancies_sorted_by_absolute_difference() {
let primary = make_counts(&[
(&[false, false], 400),
(&[false, true], 300),
(&[true, false], 200),
(&[true, true], 100),
]);
let reference = make_counts(&[
(&[false, false], 250),
(&[false, true], 250),
(&[true, false], 250),
(&[true, true], 250),
]);
let result =
verify_against_reference(&primary, &reference, 0.5);
for i in 1..result.discrepancies.len() {
assert!(
result.discrepancies[i - 1].absolute_difference
>= result.discrepancies[i].absolute_difference,
"Discrepancies should be sorted descending by \
absolute_difference: {} < {}",
result.discrepancies[i - 1].absolute_difference,
result.discrepancies[i].absolute_difference
);
}
assert!(
result.discrepancies[0].absolute_difference >= 0.14,
"Top discrepancy should have absolute_difference >= 0.14, got {}",
result.discrepancies[0].absolute_difference
);
}
#[test]
fn stabilizer_shots_zero_state_gives_all_zeros() {
let mut circ = QuantumCircuit::new(3);
circ.measure(0).measure(1).measure(2);
let counts = run_stabilizer_shots(&circ, 100, 42);
assert_eq!(counts.len(), 1, "Should have exactly one outcome");
assert_eq!(
counts[&vec![false, false, false]],
100,
"All 100 shots should give |000>"
);
}
#[test]
fn stabilizer_shots_bell_state_gives_correlated_results() {
let mut circ = QuantumCircuit::new(2);
circ.h(0).cnot(0, 1).measure(0).measure(1);
let counts = run_stabilizer_shots(&circ, 1000, 42);
for (bits, _count) in &counts {
assert_eq!(
bits[0], bits[1],
"Bell state qubits must be correlated, got {:?}",
bits
);
}
assert!(
counts.contains_key(&vec![false, false]),
"Should see |00> outcome"
);
assert!(
counts.contains_key(&vec![true, true]),
"Should see |11> outcome"
);
let count_00 =
counts.get(&vec![false, false]).copied().unwrap_or(0);
let count_11 =
counts.get(&vec![true, true]).copied().unwrap_or(0);
assert_eq!(count_00 + count_11, 1000);
assert!(
count_00 > 350 && count_00 < 650,
"Expected roughly 50/50, got {}/{}",
count_00,
count_11
);
}
#[test]
fn stabilizer_shots_implicit_measurement() {
let mut circ = QuantumCircuit::new(2);
circ.h(0).cnot(0, 1);
let counts = run_stabilizer_shots(&circ, 500, 99);
for (bits, _count) in &counts {
assert_eq!(bits[0], bits[1], "Bell state must be correlated");
}
}
#[test]
fn bell_state_passes_exact_verification() {
let mut circ = QuantumCircuit::new(2);
circ.h(0).cnot(0, 1).measure(0).measure(1);
let result = verify_circuit(&circ, 2000, 42);
assert_eq!(result.primary_backend, BackendType::StateVector);
assert_eq!(
result.reference_backend,
Some(BackendType::Stabilizer)
);
assert!(
result.passed,
"Bell state should pass verification: {}",
result.explanation
);
assert!(
result.level == VerificationLevel::Exact
|| result.level == VerificationLevel::Statistical,
"Expected Exact or Statistical, got {:?}",
result.level
);
}
#[test]
fn non_clifford_circuit_is_skipped() {
let mut circ = QuantumCircuit::new(2);
circ.h(0).t(0).cnot(0, 1).measure(0).measure(1);
let result = verify_circuit(&circ, 1000, 42);
assert_eq!(result.level, VerificationLevel::Skipped);
assert!(result.reference_backend.is_none());
assert!(
result.explanation.contains("non-Clifford"),
"Explanation should mention non-Clifford gates: {}",
result.explanation
);
}
#[test]
fn ghz_state_passes_verification() {
let mut circ = QuantumCircuit::new(4);
circ.h(0);
circ.cnot(0, 1).cnot(1, 2).cnot(2, 3);
circ.measure(0).measure(1).measure(2).measure(3);
let result = verify_circuit(&circ, 2000, 123);
assert!(
result.passed,
"GHZ state should pass verification: {}",
result.explanation
);
}
#[test]
fn empty_circuit_passes_verification() {
let mut circ = QuantumCircuit::new(2);
circ.measure(0).measure(1);
let result = verify_circuit(&circ, 100, 0);
assert!(result.passed);
assert_eq!(
result.reference_backend,
Some(BackendType::Stabilizer)
);
}
#[test]
fn pearson_correlation_identical_distributions() {
let p: HashMap<Vec<bool>, f64> = [
(vec![false], 0.3),
(vec![true], 0.7),
]
.into_iter()
.collect();
let corr = pearson_correlation(&p, &p);
assert!(
(corr - 1.0).abs() < 1e-10,
"Identical distributions should have correlation 1.0, got {}",
corr
);
}
#[test]
fn standard_normal_cdf_known_values() {
assert!(
(standard_normal_cdf(0.0) - 0.5).abs() < 1e-6,
"CDF(0) should be 0.5"
);
assert!(
standard_normal_cdf(-10.0) < 1e-10,
"CDF(-10) should be near 0"
);
assert!(
(standard_normal_cdf(10.0) - 1.0).abs() < 1e-10,
"CDF(10) should be near 1"
);
assert!(
(standard_normal_cdf(1.96) - 0.975).abs() < 0.01,
"CDF(1.96) should be near 0.975, got {}",
standard_normal_cdf(1.96)
);
}
}