use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct ConfidenceInterval {
pub lower: f64,
pub upper: f64,
pub point_estimate: f64,
pub confidence_level: f64,
pub method: &'static str,
}
#[derive(Debug, Clone)]
pub struct ChiSquaredResult {
pub statistic: f64,
pub degrees_of_freedom: usize,
pub p_value: f64,
pub significant: bool,
}
pub struct ConvergenceMonitor {
estimates: Vec<f64>,
window_size: usize,
}
pub fn z_score(confidence: f64) -> f64 {
assert!(
confidence > 0.0 && confidence < 1.0,
"confidence must be in (0, 1)"
);
let p = (1.0 + confidence) / 2.0; let tail = 1.0 - p;
let t = (-2.0_f64 * tail.ln()).sqrt();
let c0 = 2.515517;
let c1 = 0.802853;
let c2 = 0.010328;
let d1 = 1.432788;
let d2 = 0.189269;
let d3 = 0.001308;
t - (c0 + c1 * t + c2 * t * t) / (1.0 + d1 * t + d2 * t * t + d3 * t * t * t)
}
pub fn wilson_interval(successes: usize, trials: usize, confidence: f64) -> ConfidenceInterval {
assert!(trials > 0, "trials must be > 0");
assert!(
confidence > 0.0 && confidence < 1.0,
"confidence must be in (0, 1)"
);
let n = trials as f64;
let p_hat = successes as f64 / n;
let z = z_score(confidence);
let z2 = z * z;
let denom = 1.0 + z2 / n;
let centre = (p_hat + z2 / (2.0 * n)) / denom;
let half_width = z * (p_hat * (1.0 - p_hat) / n + z2 / (4.0 * n * n)).sqrt() / denom;
let lower = (centre - half_width).max(0.0);
let upper = (centre + half_width).min(1.0);
ConfidenceInterval {
lower,
upper,
point_estimate: p_hat,
confidence_level: confidence,
method: "wilson",
}
}
pub fn clopper_pearson(successes: usize, trials: usize, confidence: f64) -> ConfidenceInterval {
assert!(trials > 0, "trials must be > 0");
assert!(
confidence > 0.0 && confidence < 1.0,
"confidence must be in (0, 1)"
);
let alpha = 1.0 - confidence;
let n = trials;
let k = successes;
let p_hat = k as f64 / n as f64;
let lower = if k == 0 {
0.0
} else {
bisect_binomial_cdf(n, k - 1, 1.0 - alpha / 2.0)
};
let upper = if k == n {
1.0
} else {
bisect_binomial_cdf(n, k, alpha / 2.0)
};
ConfidenceInterval {
lower,
upper,
point_estimate: p_hat,
confidence_level: confidence,
method: "clopper-pearson",
}
}
fn bisect_binomial_cdf(n: usize, k: usize, target: f64) -> f64 {
let mut lo = 0.0_f64;
let mut hi = 1.0_f64;
for _ in 0..200 {
let mid = (lo + hi) / 2.0;
let cdf = binomial_cdf(n, k, mid);
if cdf < target {
hi = mid;
} else {
lo = mid;
}
if (hi - lo) < 1e-15 {
break;
}
}
(lo + hi) / 2.0
}
fn binomial_cdf(n: usize, k: usize, p: f64) -> f64 {
if p <= 0.0 {
return 1.0;
}
if p >= 1.0 {
return if k >= n { 1.0 } else { 0.0 };
}
if k >= n {
return 1.0;
}
let mut cdf = 0.0_f64;
let ln_p = p.ln();
let ln_1mp = (1.0 - p).ln();
let mut log_binom = 0.0_f64; cdf += (log_binom + ln_1mp * n as f64).exp();
for i in 1..=k {
log_binom += ((n - i + 1) as f64).ln() - (i as f64).ln();
let log_term = log_binom + ln_p * i as f64 + ln_1mp * (n - i) as f64;
cdf += log_term.exp();
}
cdf.min(1.0).max(0.0)
}
pub fn expectation_confidence(
counts: &HashMap<Vec<bool>, usize>,
qubit: u32,
confidence: f64,
) -> ConfidenceInterval {
assert!(
confidence > 0.0 && confidence < 1.0,
"confidence must be in (0, 1)"
);
let mut n_zero: usize = 0;
let mut n_one: usize = 0;
for (bits, &count) in counts {
if let Some(&b) = bits.get(qubit as usize) {
if b {
n_one += count;
} else {
n_zero += count;
}
}
}
let total = (n_zero + n_one) as f64;
assert!(total > 0.0, "no shots found for the given qubit");
let p0 = n_zero as f64 / total;
let p1 = n_one as f64 / total;
let exp_z = p0 - p1;
let var_single = 1.0 - exp_z * exp_z;
let se = (var_single / total).sqrt();
let z = z_score(confidence);
let lower = (exp_z - z * se).max(-1.0);
let upper = (exp_z + z * se).min(1.0);
ConfidenceInterval {
lower,
upper,
point_estimate: exp_z,
confidence_level: confidence,
method: "expectation-z-se",
}
}
pub fn required_shots(epsilon: f64, delta: f64) -> usize {
assert!(
epsilon > 0.0 && epsilon < 1.0,
"epsilon must be in (0, 1)"
);
assert!(delta > 0.0 && delta < 1.0, "delta must be in (0, 1)");
let n = (2.0_f64 / delta).ln() / (2.0 * epsilon * epsilon);
n.ceil() as usize
}
pub fn total_variation_distance(
p: &HashMap<Vec<bool>, usize>,
q: &HashMap<Vec<bool>, usize>,
) -> f64 {
let total_p: f64 = p.values().sum::<usize>() as f64;
let total_q: f64 = q.values().sum::<usize>() as f64;
if total_p == 0.0 && total_q == 0.0 {
return 0.0;
}
let mut all_keys: Vec<&Vec<bool>> = Vec::new();
for key in p.keys() {
all_keys.push(key);
}
for key in q.keys() {
if !p.contains_key(key) {
all_keys.push(key);
}
}
let mut tvd = 0.0_f64;
for key in &all_keys {
let pi = if total_p > 0.0 {
*p.get(*key).unwrap_or(&0) as f64 / total_p
} else {
0.0
};
let qi = if total_q > 0.0 {
*q.get(*key).unwrap_or(&0) as f64 / total_q
} else {
0.0
};
tvd += (pi - qi).abs();
}
0.5 * tvd
}
pub fn chi_squared_test(
observed: &HashMap<Vec<bool>, usize>,
expected: &HashMap<Vec<bool>, usize>,
) -> ChiSquaredResult {
let total_observed: f64 = observed.values().sum::<usize>() as f64;
let total_expected: f64 = expected.values().sum::<usize>() as f64;
assert!(
total_expected > 0.0,
"expected distribution must have nonzero total"
);
let mut all_keys: Vec<&Vec<bool>> = Vec::new();
for key in observed.keys() {
all_keys.push(key);
}
for key in expected.keys() {
if !observed.contains_key(key) {
all_keys.push(key);
}
}
let mut statistic = 0.0_f64;
let mut num_categories = 0_usize;
for key in &all_keys {
let o = *observed.get(*key).unwrap_or(&0) as f64;
let e_raw = *expected.get(*key).unwrap_or(&0) as f64;
let e = e_raw * total_observed / total_expected;
if e > 0.0 {
statistic += (o - e) * (o - e) / e;
num_categories += 1;
}
}
let df = if num_categories > 1 {
num_categories - 1
} else {
1
};
let p_value = chi_squared_survival(statistic, df);
ChiSquaredResult {
statistic,
degrees_of_freedom: df,
p_value,
significant: p_value < 0.05,
}
}
fn chi_squared_survival(x: f64, df: usize) -> f64 {
if df == 0 {
return if x > 0.0 { 0.0 } else { 1.0 };
}
if x <= 0.0 {
return 1.0;
}
let k = df as f64;
let term = 2.0 / (9.0 * k);
let cube_root = (x / k).powf(1.0 / 3.0);
let z = (cube_root - (1.0 - term)) / term.sqrt();
normal_cdf(-z)
}
fn normal_cdf(x: f64) -> f64 {
let sign = if x < 0.0 { -1.0 } else { 1.0 };
let x_abs = x.abs();
let t = 1.0 / (1.0 + 0.2316419 * x_abs);
let d = 0.3989422804014327; let p = d * (-x_abs * x_abs / 2.0).exp();
let poly = t
* (0.319381530
+ t * (-0.356563782
+ t * (1.781477937 + t * (-1.821255978 + t * 1.330274429))));
if sign > 0.0 {
1.0 - p * poly
} else {
p * poly
}
}
impl ConvergenceMonitor {
pub fn new(window_size: usize) -> Self {
assert!(window_size > 0, "window_size must be > 0");
Self {
estimates: Vec::new(),
window_size,
}
}
pub fn add_estimate(&mut self, value: f64) {
self.estimates.push(value);
}
pub fn has_converged(&self, epsilon: f64) -> bool {
if self.estimates.len() < self.window_size {
return false;
}
let window = &self.estimates[self.estimates.len() - self.window_size..];
let min = window
.iter()
.copied()
.fold(f64::INFINITY, f64::min);
let max = window
.iter()
.copied()
.fold(f64::NEG_INFINITY, f64::max);
(max - min) < epsilon
}
pub fn current_estimate(&self) -> Option<f64> {
self.estimates.last().copied()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn z_score_95() {
let z = z_score(0.95);
assert!(
(z - 1.96).abs() < 0.01,
"z_score(0.95) = {z}, expected ~1.96"
);
}
#[test]
fn z_score_99() {
let z = z_score(0.99);
assert!(
(z - 2.576).abs() < 0.02,
"z_score(0.99) = {z}, expected ~2.576"
);
}
#[test]
fn z_score_90() {
let z = z_score(0.90);
assert!(
(z - 1.645).abs() < 0.01,
"z_score(0.90) = {z}, expected ~1.645"
);
}
#[test]
fn wilson_contains_true_proportion() {
let ci = wilson_interval(50, 100, 0.95);
assert!(ci.lower < 0.5 && ci.upper > 0.5, "Wilson CI should contain 0.5: {ci:?}");
assert_eq!(ci.method, "wilson");
assert!((ci.point_estimate - 0.5).abs() < 1e-12);
}
#[test]
fn wilson_asymmetric() {
let ci = wilson_interval(1, 100, 0.95);
assert!(ci.lower >= 0.0);
assert!(ci.upper <= 1.0);
assert!(ci.lower < 0.01);
assert!(ci.upper > 0.01);
}
#[test]
fn wilson_zero_successes() {
let ci = wilson_interval(0, 100, 0.95);
assert_eq!(ci.lower, 0.0);
assert!(ci.upper > 0.0);
assert!((ci.point_estimate - 0.0).abs() < 1e-12);
}
#[test]
fn clopper_pearson_contains_true_proportion() {
let ci = clopper_pearson(50, 100, 0.95);
assert!(
ci.lower < 0.5 && ci.upper > 0.5,
"Clopper-Pearson CI should contain 0.5: {ci:?}"
);
assert_eq!(ci.method, "clopper-pearson");
}
#[test]
fn clopper_pearson_is_conservative() {
let cp = clopper_pearson(50, 100, 0.95);
let w = wilson_interval(50, 100, 0.95);
let cp_width = cp.upper - cp.lower;
let w_width = w.upper - w.lower;
assert!(
cp_width >= w_width - 1e-10,
"Clopper-Pearson width ({cp_width}) should be >= Wilson width ({w_width})"
);
}
#[test]
fn clopper_pearson_edge_zero() {
let ci = clopper_pearson(0, 100, 0.95);
assert_eq!(ci.lower, 0.0);
assert!(ci.upper > 0.0);
}
#[test]
fn clopper_pearson_edge_all() {
let ci = clopper_pearson(100, 100, 0.95);
assert_eq!(ci.upper, 1.0);
assert!(ci.lower < 1.0);
}
#[test]
fn expectation_all_zero() {
let mut counts = HashMap::new();
counts.insert(vec![false], 1000);
let ci = expectation_confidence(&counts, 0, 0.95);
assert!((ci.point_estimate - 1.0).abs() < 1e-12);
assert!(ci.lower <= 1.0);
assert!(ci.upper >= 1.0 - 1e-6);
}
#[test]
fn expectation_all_one() {
let mut counts = HashMap::new();
counts.insert(vec![true], 1000);
let ci = expectation_confidence(&counts, 0, 0.95);
assert!((ci.point_estimate - (-1.0)).abs() < 1e-12);
}
#[test]
fn expectation_balanced() {
let mut counts = HashMap::new();
counts.insert(vec![false], 500);
counts.insert(vec![true], 500);
let ci = expectation_confidence(&counts, 0, 0.95);
assert!(
ci.point_estimate.abs() < 1e-12,
"expected 0.0, got {}",
ci.point_estimate
);
assert!(ci.lower < 0.0);
assert!(ci.upper > 0.0);
}
#[test]
fn expectation_multi_qubit() {
let mut counts = HashMap::new();
counts.insert(vec![false, true], 1000);
let ci0 = expectation_confidence(&counts, 0, 0.95);
let ci1 = expectation_confidence(&counts, 1, 0.95);
assert!((ci0.point_estimate - 1.0).abs() < 1e-12);
assert!((ci1.point_estimate - (-1.0)).abs() < 1e-12);
}
#[test]
fn required_shots_standard() {
let n = required_shots(0.01, 0.05);
assert!(
(n as i64 - 18445).abs() <= 1,
"required_shots(0.01, 0.05) = {n}, expected ~18445"
);
}
#[test]
fn required_shots_loose() {
let n = required_shots(0.1, 0.1);
assert!(n >= 149 && n <= 151, "expected ~150, got {n}");
}
#[test]
fn tvd_identical() {
let mut p = HashMap::new();
p.insert(vec![false, false], 250);
p.insert(vec![false, true], 250);
p.insert(vec![true, false], 250);
p.insert(vec![true, true], 250);
let tvd = total_variation_distance(&p, &p);
assert!(tvd.abs() < 1e-12, "TVD of identical distributions should be 0, got {tvd}");
}
#[test]
fn tvd_completely_different() {
let mut p = HashMap::new();
p.insert(vec![false], 1000);
let mut q = HashMap::new();
q.insert(vec![true], 1000);
let tvd = total_variation_distance(&p, &q);
assert!(
(tvd - 1.0).abs() < 1e-12,
"TVD of completely different distributions should be 1.0, got {tvd}"
);
}
#[test]
fn tvd_partial_overlap() {
let mut p = HashMap::new();
p.insert(vec![false], 600);
p.insert(vec![true], 400);
let mut q = HashMap::new();
q.insert(vec![false], 400);
q.insert(vec![true], 600);
let tvd = total_variation_distance(&p, &q);
assert!(
(tvd - 0.2).abs() < 1e-12,
"expected 0.2, got {tvd}"
);
}
#[test]
fn tvd_empty() {
let p: HashMap<Vec<bool>, usize> = HashMap::new();
let q: HashMap<Vec<bool>, usize> = HashMap::new();
let tvd = total_variation_distance(&p, &q);
assert!(tvd.abs() < 1e-12);
}
#[test]
fn chi_squared_matching() {
let mut obs = HashMap::new();
obs.insert(vec![false, false], 250);
obs.insert(vec![false, true], 250);
obs.insert(vec![true, false], 250);
obs.insert(vec![true, true], 250);
let result = chi_squared_test(&obs, &obs);
assert!(
result.statistic < 1e-12,
"statistic should be ~0 for identical distributions, got {}",
result.statistic
);
assert!(
result.p_value > 0.05,
"p-value should be high for matching distributions, got {}",
result.p_value
);
assert!(!result.significant);
}
#[test]
fn chi_squared_very_different() {
let mut obs = HashMap::new();
obs.insert(vec![false], 1000);
obs.insert(vec![true], 0);
let mut exp = HashMap::new();
exp.insert(vec![false], 500);
exp.insert(vec![true], 500);
let result = chi_squared_test(&obs, &exp);
assert!(result.statistic > 100.0, "statistic should be large");
assert!(result.p_value < 0.05, "p-value should be small: {}", result.p_value);
assert!(result.significant);
}
#[test]
fn chi_squared_degrees_of_freedom() {
let mut obs = HashMap::new();
obs.insert(vec![false, false], 100);
obs.insert(vec![false, true], 100);
obs.insert(vec![true, false], 100);
obs.insert(vec![true, true], 100);
let result = chi_squared_test(&obs, &obs);
assert_eq!(result.degrees_of_freedom, 3);
}
#[test]
fn convergence_detects_stable() {
let mut monitor = ConvergenceMonitor::new(5);
for &v in &[0.5, 0.52, 0.49, 0.501, 0.499, 0.5001, 0.4999, 0.5002, 0.4998, 0.5001] {
monitor.add_estimate(v);
}
assert!(
monitor.has_converged(0.01),
"should have converged: last 5 values are within 0.01"
);
}
#[test]
fn convergence_rejects_unstable() {
let mut monitor = ConvergenceMonitor::new(5);
for &v in &[0.1, 0.9, 0.1, 0.9, 0.1, 0.9, 0.1, 0.9, 0.1, 0.9] {
monitor.add_estimate(v);
}
assert!(
!monitor.has_converged(0.01),
"should NOT have converged: values oscillate widely"
);
}
#[test]
fn convergence_insufficient_data() {
let mut monitor = ConvergenceMonitor::new(10);
monitor.add_estimate(1.0);
monitor.add_estimate(1.0);
assert!(
!monitor.has_converged(0.1),
"not enough data for window_size=10"
);
}
#[test]
fn convergence_current_estimate() {
let mut monitor = ConvergenceMonitor::new(3);
assert_eq!(monitor.current_estimate(), None);
monitor.add_estimate(42.0);
assert_eq!(monitor.current_estimate(), Some(42.0));
monitor.add_estimate(43.0);
assert_eq!(monitor.current_estimate(), Some(43.0));
}
#[test]
fn binomial_cdf_edge_cases() {
let c = binomial_cdf(10, 10, 0.5);
assert!((c - 1.0).abs() < 1e-12);
let c = binomial_cdf(10, 0, 0.5);
assert!((c - 0.0009765625).abs() < 1e-8);
}
#[test]
fn normal_cdf_values() {
assert!((normal_cdf(0.0) - 0.5).abs() < 1e-6);
assert!((normal_cdf(1.96) - 0.975).abs() < 0.002);
assert!((normal_cdf(-1.96) - 0.025).abs() < 0.002);
}
}