use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct LttDiagnostic {
pub lambda: f64,
pub n_served: usize,
pub empirical_risk: f64,
pub p_value: f64,
pub certified: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct LttResult {
pub alpha: f64,
pub delta: f64,
pub threshold: f64,
pub feasible: bool,
pub empirical_risk: f64,
pub n_served: usize,
pub false_accept_rate: Option<f64>,
pub diagnostics: Vec<LttDiagnostic>,
}
#[must_use]
pub fn calibrate(pairs: &[(f64, bool)], alpha: f64, delta: f64, min_n: usize) -> LttResult {
if pairs.is_empty() {
return mk_infeasible(alpha, delta, Vec::new());
}
let mut sorted = pairs.to_vec();
sorted.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
let candidates: Vec<f64> = {
let mut c: Vec<f64> = sorted.iter().map(|(s, _)| *s).collect();
c.dedup();
c
};
let n_incorrect = sorted.iter().filter(|(_, c)| !c).count();
let mut diagnostics: Vec<LttDiagnostic> = Vec::with_capacity(candidates.len());
let mut walk_active = true;
let mut best: Option<usize> = None;
let mut ptr = 0usize;
let mut failures = 0usize;
for &lambda in &candidates {
while ptr < sorted.len() && sorted[ptr].0 >= lambda {
if !sorted[ptr].1 {
failures += 1;
}
ptr += 1;
}
let n_served = ptr;
let empirical_risk = if n_served == 0 {
0.0
} else {
failures as f64 / n_served as f64
};
if n_served < min_n {
diagnostics.push(LttDiagnostic {
lambda,
n_served,
empirical_risk,
p_value: 1.0,
certified: false,
});
continue;
}
let p_value = binomial_cdf(n_served, failures, alpha);
let certified = walk_active && p_value <= delta;
if walk_active && !certified {
walk_active = false;
}
if certified {
best = Some(diagnostics.len()); }
diagnostics.push(LttDiagnostic {
lambda,
n_served,
empirical_risk,
p_value,
certified,
});
}
let Some(best_idx) = best else {
return mk_infeasible(alpha, delta, diagnostics);
};
let d = &diagnostics[best_idx];
let false_accept_rate = if n_incorrect == 0 {
None
} else {
let fa = sorted.iter().filter(|(s, c)| !c && *s >= d.lambda).count();
Some(fa as f64 / n_incorrect as f64)
};
LttResult {
alpha,
delta,
threshold: d.lambda,
feasible: true,
empirical_risk: d.empirical_risk,
n_served: d.n_served,
false_accept_rate,
diagnostics,
}
}
fn mk_infeasible(alpha: f64, delta: f64, diagnostics: Vec<LttDiagnostic>) -> LttResult {
LttResult {
alpha,
delta,
threshold: f64::INFINITY,
feasible: false,
empirical_risk: 0.0,
n_served: 0,
false_accept_rate: None,
diagnostics,
}
}
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 log_p = p.ln();
let log_q = (1.0 - p).ln();
let log_ratio = log_p - log_q;
let mut log_term = n as f64 * log_q;
let mut log_terms = Vec::with_capacity(k + 1);
log_terms.push(log_term);
for i in 1..=k {
log_term += ((n - i + 1) as f64).ln() - (i as f64).ln() + log_ratio;
log_terms.push(log_term);
}
let max_log = log_terms.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
if max_log == f64::NEG_INFINITY {
return 0.0;
}
let sum: f64 = log_terms.iter().map(|&l| (l - max_log).exp()).sum();
(sum * max_log.exp()).min(1.0)
}
#[cfg(test)]
mod tests {
use super::*;
fn hash01(seed: u64, a: u64, b: u64) -> f64 {
let mut s = seed
.wrapping_mul(0xD1B5_4A32_D192_ED03)
.wrapping_add(a.wrapping_mul(0x9E37_79B9_7F4A_7C15))
.wrapping_add(b.wrapping_mul(0xC2B2_AE3D_27D4_EB4F))
.wrapping_add(0x1234_5678_9ABC_DEF0);
s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = s;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^= z >> 31;
(z >> 11) as f64 / (1u64 << 53) as f64
}
fn pairs(seed: u64, n: usize) -> Vec<(f64, bool)> {
(0..n as u64)
.map(|id| {
let correct = hash01(seed, id, 1) < 0.7;
let noise = (hash01(seed ^ 0x00C0_FFEE, id, 2) - 0.5) * 0.4;
let base = if correct { 0.72 } else { 0.30 };
((base + noise).clamp(0.0, 1.0), correct)
})
.collect()
}
#[test]
fn binomial_cdf_hand_computed() {
let got = binomial_cdf(10, 0, 0.1);
assert!(
(got - 0.34868_f64).abs() < 1e-4,
"Bin(10,0.1) CDF at 0: {got}"
);
let got = binomial_cdf(10, 1, 0.1);
assert!(
(got - 0.73610_f64).abs() < 1e-4,
"Bin(10,0.1) CDF at 1: {got}"
);
let got = binomial_cdf(5, 2, 0.3);
assert!(
(got - 0.83692_f64).abs() < 1e-4,
"Bin(5,0.3) CDF at 2: {got}"
);
assert_eq!(binomial_cdf(10, 10, 0.5), 1.0, "k=n must be 1");
assert_eq!(binomial_cdf(10, 0, 0.0), 1.0, "p=0 must be 1");
assert_eq!(binomial_cdf(10, 9, 1.0), 0.0, "k<n and p=1 must be 0");
}
#[test]
fn certified_lambda_risk_at_most_alpha_on_held_out() {
let alpha = 0.10;
let calib = pairs(1, 4000);
let test = pairs(2, 4000);
let r = calibrate(&calib, alpha, 0.05, 50);
assert!(
r.feasible,
"should find a feasible threshold on n=4000 gate-like data"
);
let served_n = calib.iter().filter(|(s, _)| *s >= r.threshold).count();
assert!(
served_n as f64 / calib.len() as f64 > 0.2,
"should serve > 20% of calibration set"
);
let held_out_served: Vec<bool> = test
.iter()
.filter(|(s, _)| *s >= r.threshold)
.map(|(_, c)| *c)
.collect();
assert!(
!held_out_served.is_empty(),
"must serve some held-out items"
);
let held_out_risk =
held_out_served.iter().filter(|c| !**c).count() as f64 / held_out_served.len() as f64;
assert!(
held_out_risk <= alpha + 0.03,
"held-out risk {held_out_risk:.3} must be ≤ alpha {alpha} (+tol)"
);
}
#[test]
fn fixed_sequence_stops_at_first_failure_even_if_risk_dips() {
let alpha = 0.10;
let mut data: Vec<(f64, bool)> = Vec::new();
for _ in 0..100 {
data.push((0.95, true));
}
for _ in 0..30 {
data.push((0.80, false));
}
for _ in 0..200 {
data.push((0.60, true));
}
let r = calibrate(&data, alpha, 0.05, 30);
let d095 = r
.diagnostics
.iter()
.find(|d| (d.lambda - 0.95).abs() < 1e-9)
.expect("0.95 must appear in diagnostics");
assert!(d095.certified, "λ=0.95 must be certified");
let d080 = r
.diagnostics
.iter()
.find(|d| (d.lambda - 0.80).abs() < 1e-9)
.expect("0.80 must appear in diagnostics");
assert!(
!d080.certified,
"λ=0.80 has risk > alpha; must not be certified"
);
assert!(
d080.p_value > 0.05,
"λ=0.80 p-value must be large (H0 not rejected)"
);
let d060 = r
.diagnostics
.iter()
.find(|d| (d.lambda - 0.60).abs() < 1e-9)
.expect("0.60 must appear in diagnostics");
assert!(
!d060.certified,
"λ=0.60 must not be certified after walk stopped"
);
assert!(
d060.empirical_risk <= alpha,
"empirical risk at 0.60 dipped to {:.3} but must still be uncertified",
d060.empirical_risk
);
assert!(r.feasible);
assert!((r.threshold - 0.95).abs() < 1e-9, "threshold must be 0.95");
}
#[test]
fn infeasible_on_tiny_n() {
let tiny = vec![(0.9, true), (0.9, true), (0.1, false)];
let r = calibrate(&tiny, 0.10, 0.05, 100);
assert!(!r.feasible, "3 pairs below min_n=100 must be infeasible");
assert_eq!(r.threshold, f64::INFINITY);
assert_eq!(r.n_served, 0);
}
#[test]
fn infeasible_target_zero_serves_nothing() {
let r = calibrate(&pairs(3, 1000), 0.0, 0.05, 30);
assert!(!r.feasible);
}
#[test]
fn false_accept_rate_reported_correctly() {
let mut data: Vec<(f64, bool)> = Vec::new();
for _ in 0..200 {
data.push((0.9, true));
}
for _ in 0..5 {
data.push((0.9, false));
}
for _ in 0..15 {
data.push((0.2, false));
}
let r = calibrate(&data, 0.10, 0.05, 30);
assert!(r.feasible, "must certify a threshold on this data");
let far = r
.false_accept_rate
.expect("must report a false_accept_rate");
assert!(
(far - 0.25).abs() < 1e-9,
"false_accept_rate must be 5/20 = 0.25, got {far}"
);
}
}