use crate::integrity::tpl_scalar::TimeSource;
use crate::portable_math::PortableFloat;
use crate::raim::normal_quantile;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct UtcRealizer(pub u16);
#[derive(Clone, Copy, Debug)]
pub struct SourceBias {
pub expanded_uncertainty_s: f64,
pub coverage_factor: f64,
pub ageing_inflation_s: f64,
pub realizer: UtcRealizer,
}
pub fn integrity_bias_overbound(bias: &SourceBias, target_tail_ir: f64) -> f64 {
debug_assert!(target_tail_ir > 0.0 && target_tail_ir < 1.0);
debug_assert!(bias.coverage_factor > 0.0);
let u_c = bias.expanded_uncertainty_s / bias.coverage_factor;
let k_tail = normal_quantile(1.0 - target_tail_ir / 2.0);
u_c * k_tail + bias.ageing_inflation_s
}
pub fn bias_cross_covariance(
biases: &[SourceBias],
overbounds: &[f64],
rho_common: f64,
) -> Vec<Vec<f64>> {
debug_assert_eq!(biases.len(), overbounds.len());
debug_assert!((0.0..=1.0).contains(&rho_common));
let n = biases.len();
let mut sigma = vec![vec![0.0f64; n]; n];
for i in 0..n {
for j in 0..n {
sigma[i][j] = if i == j {
overbounds[i] * overbounds[i]
} else if biases[i].realizer == biases[j].realizer {
rho_common * overbounds[i] * overbounds[j]
} else {
0.0
};
}
}
sigma
}
pub fn correlated_fused_bias(weights: &[f64], bias_cov: &[Vec<f64>]) -> f64 {
debug_assert!(
weights.iter().all(|&w| w >= 0.0),
"the correlated-bias theorem assumes non-negative fusion weights"
);
quad_form(weights, bias_cov).max(0.0).sqrt()
}
pub fn independent_fused_bias(weights: &[f64], bias_cov: &[Vec<f64>]) -> f64 {
let s: f64 = weights
.iter()
.enumerate()
.map(|(i, w)| w * w * bias_cov[i][i])
.sum();
s.max(0.0).sqrt()
}
fn quad_form(w: &[f64], m: &[Vec<f64>]) -> f64 {
let n = w.len();
let mut acc = 0.0;
for i in 0..n {
for j in 0..n {
acc += w[i] * m[i][j] * w[j];
}
}
acc
}
pub fn source_from_bias(
sigma_s: f64,
bias: &SourceBias,
p_fault: f64,
target_tail_ir: f64,
) -> TimeSource {
TimeSource {
sigma_s,
bias_s: integrity_bias_overbound(bias, target_tail_ir),
p_fault,
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PairedOverbound {
pub mean_left: f64,
pub mean_right: f64,
pub sigma: f64,
}
impl PairedOverbound {
pub fn bound(&self, tail: f64) -> f64 {
self.mean_left.abs().max(self.mean_right.abs()) + self.sigma * inv_normal(1.0 - tail / 2.0)
}
}
fn inv_normal(p: f64) -> f64 {
if p <= 0.0 {
return f64::NEG_INFINITY;
}
if p >= 1.0 {
return f64::INFINITY;
}
const A: [f64; 6] = [
-3.969683028665376e+01,
2.209460984245205e+02,
-2.759285104469687e+02,
1.383_577_518_672_69e2,
-3.066479806614716e+01,
2.506628277459239e+00,
];
const B: [f64; 5] = [
-5.447609879822406e+01,
1.615858368580409e+02,
-1.556989798598866e+02,
6.680131188771972e+01,
-1.328068155288572e+01,
];
const C: [f64; 6] = [
-7.784894002430293e-03,
-3.223964580411365e-01,
-2.400758277161838e+00,
-2.549732539343734e+00,
4.374664141464968e+00,
2.938163982698783e+00,
];
const D: [f64; 4] = [
7.784695709041462e-03,
3.224671290700398e-01,
2.445134137142996e+00,
3.754408661907416e+00,
];
let plow = 0.02425;
let x = if p < plow {
let q = (-2.0 * p.pln()).sqrt();
(((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
/ ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0)
} else if p <= 1.0 - plow {
let q = p - 0.5;
let r = q * q;
(((((A[0] * r + A[1]) * r + A[2]) * r + A[3]) * r + A[4]) * r + A[5]) * q
/ (((((B[0] * r + B[1]) * r + B[2]) * r + B[3]) * r + B[4]) * r + 1.0)
} else {
let q = (-2.0 * (1.0 - p).pln()).sqrt();
-(((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
/ ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0)
};
let e = crate::raim::normal_cdf(x) - p;
let u = e * (2.0 * std::f64::consts::PI).sqrt() * (x * x / 2.0).pexp();
x - u / (1.0 + x * u / 2.0)
}
pub fn paired_overbound(samples: &[f64], tail: f64) -> Option<PairedOverbound> {
if samples.len() < 2 || samples.iter().any(|v| !v.is_finite()) {
return None;
}
let mut x = samples.to_vec();
x.sort_by(f64::total_cmp);
let n = x.len();
let nf = n as f64;
let zr: Vec<(f64, f64)> = (2..=n)
.map(|i| (x[i - 1], inv_normal((i - 1) as f64 / nf)))
.collect();
let zl: Vec<(f64, f64)> = (1..n)
.map(|i| (x[i - 1], inv_normal(i as f64 / nf)))
.collect();
let eval = |s: f64| -> PairedOverbound {
let mr = zr
.iter()
.map(|&(v, z)| v - s * z)
.fold(f64::NEG_INFINITY, f64::max);
let ml = zl
.iter()
.map(|&(v, z)| v - s * z)
.fold(f64::INFINITY, f64::min);
PairedOverbound {
mean_left: ml,
mean_right: mr,
sigma: s,
}
};
let b = |s: f64| eval(s).bound(tail);
let span = (x[n - 1] - x[0]).max(f64::MIN_POSITIVE);
let mut grid: Vec<f64> = vec![0.0];
let (lo, hi) = ((span * 1e-4).pln(), (span * 10.0).pln());
grid.extend((0..120).map(|j| (lo + (hi - lo) * j as f64 / 119.0).pexp()));
let (mut jbest, mut bbest) = (0usize, f64::INFINITY);
for (j, &s) in grid.iter().enumerate() {
let v = b(s);
if v < bbest {
bbest = v;
jbest = j;
}
}
let (mut a, mut c) = (
grid[jbest.saturating_sub(1)],
grid[(jbest + 1).min(grid.len() - 1)],
);
let g = (5f64.sqrt() - 1.0) / 2.0;
for _ in 0..60 {
let s1 = c - g * (c - a);
let s2 = a + g * (c - a);
if b(s1) <= b(s2) {
c = s2;
} else {
a = s1;
}
}
let s_mid = 0.5 * (a + c);
let best = if b(s_mid) < bbest { s_mid } else { grid[jbest] };
Some(eval(best))
}
pub fn source_bias_from_realised_offsets(
offsets_s: &[f64],
increments_s: &[f64],
tail: f64,
realizer: UtcRealizer,
) -> Option<SourceBias> {
let po = paired_overbound(offsets_s, tail)?;
let ib = paired_overbound(increments_s, tail)?.bound(tail);
Some(SourceBias {
expanded_uncertainty_s: po.sigma,
coverage_factor: 1.0,
ageing_inflation_s: po.mean_left.abs().max(po.mean_right.abs()) + ib,
realizer,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn src(u: f64, k: f64, age: f64, r: u16) -> SourceBias {
SourceBias {
expanded_uncertainty_s: u,
coverage_factor: k,
ageing_inflation_s: age,
realizer: UtcRealizer(r),
}
}
#[test]
fn cross_covariance_is_psd_and_groups_by_realizer() {
let b = [
src(4e-9, 2.0, 0.0, 1),
src(3e-9, 2.0, 0.0, 1),
src(5e-9, 2.0, 0.0, 2),
];
let ob = [4e-9, 3e-9, 5e-9];
let rho = 0.6;
let s = bias_cross_covariance(&b, &ob, rho);
assert!((s[0][0] - 16e-18).abs() < 1e-30);
assert!((s[0][1] - rho * 4e-9 * 3e-9).abs() < 1e-30);
assert!((s[0][1] - s[1][0]).abs() < 1e-30, "symmetric");
assert!(s[0][2].abs() < 1e-30 && s[1][2].abs() < 1e-30);
for x in [[1.0, 1.0, 1.0], [1.0, -1.0, 0.0], [-2.0, 1.0, 3.0]] {
assert!(super::quad_form(&x, &s) >= -1e-30);
}
}
#[test]
fn correlated_fused_bias_dominates_independent_when_correlated() {
let b = [src(4e-9, 2.0, 0.0, 1), src(4e-9, 2.0, 0.0, 1)];
let ob = [4e-9, 4e-9];
let w = [0.5, 0.5];
let s_corr = bias_cross_covariance(&b, &ob, 0.7);
let s_indep = bias_cross_covariance(&b, &ob, 0.0);
let corr = correlated_fused_bias(&w, &s_corr);
let indep = independent_fused_bias(&w, &s_corr);
assert!(
corr > indep + 1e-12,
"correlated fused bias must dominate the independent one"
);
let c0 = correlated_fused_bias(&w, &s_indep);
let i0 = independent_fused_bias(&w, &s_indep);
assert!((c0 - i0).abs() < 1e-18, "equal at rho=0");
}
#[test]
fn distinct_realizers_have_no_correlation_penalty() {
let b = [src(4e-9, 2.0, 0.0, 1), src(4e-9, 2.0, 0.0, 2)];
let ob = [4e-9, 4e-9];
let w = [0.5, 0.5];
let s = bias_cross_covariance(&b, &ob, 0.9);
assert!(
(correlated_fused_bias(&w, &s) - independent_fused_bias(&w, &s)).abs() < 1e-18,
"no shared realizer -> no correlation penalty even at high rho"
);
}
#[test]
fn source_from_bias_uses_overbound() {
let b = src(4e-9, 2.0, 1e-9, 1);
let ts = source_from_bias(2e-9, &b, 1e-4, 2e-7);
assert!((ts.bias_s - integrity_bias_overbound(&b, 2e-7)).abs() < 1e-18);
assert!((ts.sigma_s - 2e-9).abs() < 1e-18 && (ts.p_fault - 1e-4).abs() < 1e-18);
}
#[test]
fn overbound_matches_closed_form() {
let b = src(4e-9, 2.0, 1e-9, 1);
let got = integrity_bias_overbound(&b, 2e-7);
let expected = (4e-9 / 2.0) * normal_quantile(1.0 - 1e-7) + 1e-9;
assert!(
(got - expected).abs() < 1e-18,
"closed form b = u_c·Φ⁻¹(1−tail/2)+age"
);
}
#[test]
fn overbound_tightens_toward_zero_tail() {
let b = src(4e-9, 2.0, 0.0, 1);
let loose = integrity_bias_overbound(&b, 5e-2);
let tight = integrity_bias_overbound(&b, 1e-7);
assert!(
tight > loose,
"a smaller integrity tail must inflate the overbound"
);
}
#[test]
fn overbound_recovers_expanded_u_at_matching_tail() {
let k_cov = 2.0;
let b = src(4e-9, k_cov, 0.0, 1);
let tail = 2.0 * (1.0 - 0.977_249_868_051_820_8);
let got = integrity_bias_overbound(&b, tail);
assert!(
(got - 4e-9).abs() < 1e-16,
"at k_cov tail the overbound equals U"
);
}
#[test]
fn rho_one_single_realizer_recovers_linear_sum() {
let b = [
src(4e-9, 2.0, 0.0, 7),
src(3e-9, 2.0, 0.0, 7),
src(5e-9, 2.0, 0.0, 7),
];
let ob = [4e-9, 3e-9, 5e-9];
let w = [0.5, 0.3, 0.2];
let sigma = bias_cross_covariance(&b, &ob, 1.0);
let corr = correlated_fused_bias(&w, &sigma);
let linear: f64 = w.iter().zip(ob.iter()).map(|(wi, bi)| wi * bi).sum();
assert!(
(corr - linear).abs() < 1e-18,
"rho=1 single realizer must recover the linear sum: {corr} vs {linear}"
);
}
#[test]
fn inverse_normal_matches_the_bisection_quantile() {
for &p in &[1e-6, 0.005, 0.0243, 0.1, 0.5, 0.9, 0.995, 1.0 - 1e-6] {
let (a, b) = (inv_normal(p), normal_quantile(p));
assert!((a - b).abs() < 1e-9, "{p}: {a} {b}");
}
}
#[test]
fn paired_overbound_bounds_every_interior_level_and_its_tail() {
let xs: Vec<f64> = (0..400)
.map(|i| {
let u = (i as f64 + 0.5) / 400.0;
3.0 + normal_quantile(u) + 2.0 * u * u
})
.collect();
let po = paired_overbound(&xs, 1e-2).unwrap();
assert!(po.mean_left <= po.mean_right);
let mut s = xs.clone();
s.sort_by(f64::total_cmp);
let n = s.len() as f64;
for (i, &x) in s.iter().enumerate() {
let fi = (i + 1) as f64 / n;
let fim = i as f64 / n;
let left = crate::raim::normal_cdf((x - po.mean_left) / po.sigma);
let right = crate::raim::normal_cdf((x - po.mean_right) / po.sigma);
if fi < 1.0 {
assert!(left >= fi - 1e-9, "left at {i}");
}
if fim > 0.0 {
assert!(right <= fim + 1e-9, "right at {i}");
}
}
let b = po.bound(1e-2);
let exceed = xs.iter().filter(|v| v.abs() > b).count() as f64 / n;
assert!(exceed <= 1e-2);
let inc: Vec<f64> = (0..100).map(|i| (i as f64 - 49.5) * 0.01).collect();
let sb = source_bias_from_realised_offsets(&xs, &inc, 1e-2, UtcRealizer(1)).unwrap();
let ib = paired_overbound(&inc, 1e-2).unwrap().bound(1e-2);
assert!((integrity_bias_overbound(&sb, 1e-2) - (b + ib)).abs() < 1e-6 * (b + ib));
}
}