use crate::integrity::tpl_scalar::TimeSource;
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,
}
}
#[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}"
);
}
}