use crate::portable_math::PortableFloat;
use crate::raim::{normal_cdf, normal_quantile};
const Y_INTERVALS: usize = 4_000;
const Y_LIMIT: f64 = 9.0;
const EARTH_RADIUS_KM: f64 = 6_371.008_8;
pub fn great_circle_km(lat1_deg: f64, lon1_deg: f64, lat2_deg: f64, lon2_deg: f64) -> f64 {
let (p1, p2) = (lat1_deg.to_radians(), lat2_deg.to_radians());
let dl = (lon2_deg - lon1_deg).to_radians();
let a = ((p2 - p1) / 2.0).psin().ppowi(2) + p1.pcos() * p2.pcos() * (dl / 2.0).psin().ppowi(2);
2.0 * EARTH_RADIUS_KM * a.sqrt().min(1.0).pasin()
}
pub fn correlation_matrix(sites: &[(f64, f64)], decorrelation_km: f64) -> Vec<Vec<f64>> {
let n = sites.len();
let mut m = vec![vec![1.0; n]; n];
for i in 0..n {
for j in 0..n {
if i != j {
let d = great_circle_km(sites[i].0, sites[i].1, sites[j].0, sites[j].1);
m[i][j] = (-d / decorrelation_km).pexp();
}
}
}
m
}
pub fn independent_union(avail: &[f64]) -> f64 {
1.0 - avail.iter().map(|a| 1.0 - a).product::<f64>()
}
pub fn network_availability(avail: &[f64], rho: &[Vec<f64>]) -> f64 {
let n = avail.len();
if n == 0 {
return 0.0;
}
if n == 1 {
return avail[0];
}
if avail.iter().any(|&a| a >= 1.0) {
return 1.0;
}
let all_one = (0..n).all(|i| (0..n).all(|j| i == j || rho[i][j] >= 1.0));
if all_one {
return avail.iter().cloned().fold(0.0, f64::max);
}
let live: Vec<usize> = (0..n).filter(|&i| avail[i] > 0.0).collect();
if live.is_empty() {
return 0.0;
}
let z: Vec<f64> = (0..n).map(|i| normal_quantile(avail[i])).collect();
let load: Vec<f64> = (0..n)
.map(|i| {
let m = (0..n).filter(|&j| j != i).map(|j| rho[i][j]).sum::<f64>() / (n - 1) as f64;
m.clamp(0.0, 1.0).sqrt().min(1.0 - 1e-9)
})
.collect();
if load.iter().all(|&l| l == 0.0) {
return independent_union(avail);
}
let h = 2.0 * Y_LIMIT / Y_INTERVALS as f64;
let f = |y: f64| {
let phi = (-0.5 * y * y).pexp() / (2.0 * std::f64::consts::PI).sqrt();
let all_down: f64 = live
.iter()
.map(|&i| 1.0 - normal_cdf((z[i] - load[i] * y) / (1.0 - load[i] * load[i]).sqrt()))
.product();
phi * all_down
};
let mut sum = f(-Y_LIMIT) + f(Y_LIMIT);
for k in 1..Y_INTERVALS {
let c = if k % 2 == 1 { 4.0 } else { 2.0 };
sum += c * f(-Y_LIMIT + h * k as f64);
}
(1.0 - sum * h / 3.0).clamp(0.0, 1.0)
}
pub fn uniform_correlation(n: usize, rho: f64) -> Vec<Vec<f64>> {
(0..n)
.map(|i| (0..n).map(|j| if i == j { 1.0 } else { rho }).collect())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn limits_and_monotonicity() {
let a = [0.6, 0.7, 0.5, 0.65];
let ind = independent_union(&a);
assert!((network_availability(&a, &uniform_correlation(4, 0.0)) - ind).abs() < 1e-12);
let same = [0.62; 3];
assert_eq!(
network_availability(&same, &uniform_correlation(3, 1.0)),
0.62
);
let mut prev = 1.0;
for r in [0.0, 0.2, 0.5, 0.8, 0.95] {
let v = network_availability(&a, &uniform_correlation(4, r));
assert!(v <= prev + 1e-12, "rho {r}: {v} > {prev}");
assert!(v >= a[1] - 1e-9);
prev = v;
}
let mut prev = 0.0;
for n in 1..=4 {
let v = network_availability(&a[..n], &uniform_correlation(n, 0.4));
assert!(v >= prev - 1e-12);
prev = v;
}
}
#[test]
fn distance_decorrelates() {
let m = correlation_matrix(&[(50.0, 10.0), (50.0, 80.0)], 500.0);
assert!(m[0][1] < 1e-3);
let near = correlation_matrix(&[(50.0, 10.0), (50.0, 10.5)], 500.0);
assert!(near[0][1] > 0.9);
assert!((great_circle_km(0.0, 0.0, 0.0, 90.0) - 10_007.54).abs() < 1.0);
}
}