use super::stat_primitives::{
inv_normal, phi, quantile, sample_mean, sample_variance, sum, t975, Pcg32,
};
use serde::Serialize;
pub fn percentile(sorted: &[f64], p: f64) -> f64 {
if sorted.is_empty() {
return 0.0;
}
if sorted.len() == 1 {
return sorted[0];
}
let idx = p * (sorted.len() - 1) as f64;
let lo = idx.floor() as usize;
let hi = idx.ceil() as usize;
if lo == hi {
return sorted[lo];
}
sorted[lo] + (sorted[hi] - sorted[lo]) * (idx - lo as f64)
}
pub fn sanitize(values: &[f64]) -> Vec<f64> {
values.iter().copied().filter(|v| v.is_finite()).collect()
}
pub fn mean(values: &[f64]) -> f64 {
if values.is_empty() {
return 0.0;
}
values.iter().sum::<f64>() / values.len() as f64
}
pub fn median(values: &[f64]) -> f64 {
if values.is_empty() {
return 0.0;
}
let mut sorted = values.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
percentile(&sorted, 0.5)
}
pub fn stddev(values: &[f64]) -> f64 {
if values.len() < 2 {
return 0.0;
}
variance(values).sqrt()
}
pub fn variance(values: &[f64]) -> f64 {
if values.len() < 2 {
return 0.0;
}
let m = mean(values);
values.iter().map(|v| (v - m).powi(2)).sum::<f64>() / (values.len() - 1) as f64
}
pub fn trimean(values: &[f64]) -> f64 {
if values.is_empty() {
return 0.0;
}
let mut sorted = values.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let q1 = percentile(&sorted, 0.25);
let q2 = percentile(&sorted, 0.50);
let q3 = percentile(&sorted, 0.75);
(q1 + 2.0 * q2 + q3) / 4.0
}
pub fn modified_trimean(values: &[f64]) -> f64 {
if values.is_empty() {
return 0.0;
}
let mut sorted = values.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let p10 = percentile(&sorted, 0.10);
let p50 = percentile(&sorted, 0.50);
let p90 = percentile(&sorted, 0.90);
(p10 + 8.0 * p50 + p90) / 10.0
}
pub fn filter_outliers_iqr(values: &[f64], k: f64) -> Vec<f64> {
if values.len() < 4 {
return values.to_vec();
}
let mut sorted = values.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let q1 = percentile(&sorted, 0.25);
let q3 = percentile(&sorted, 0.75);
let iqr = q3 - q1;
let lo = q1 - k * iqr;
let hi = q3 + k * iqr;
values
.iter()
.copied()
.filter(|v| *v >= lo && *v <= hi)
.collect()
}
pub fn discard_slow_start(values: &[f64], fraction: f64) -> Vec<f64> {
if values.len() < 4 {
return values.to_vec();
}
let cut = (values.len() as f64 * fraction).ceil() as usize;
values[cut..].to_vec()
}
pub fn winsorize(values: &[f64], lower: f64, upper: f64) -> Vec<f64> {
if values.len() < 4 {
return values.to_vec();
}
let mut sorted = values.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let lo = percentile(&sorted, lower);
let hi = percentile(&sorted, upper);
values.iter().map(|v| v.max(lo).min(hi)).collect()
}
pub fn accurate_bandwidth(samples: &[f64]) -> f64 {
if samples.is_empty() {
return 0.0;
}
let after_slow_start = discard_slow_start(samples, 0.3);
let cleaned = filter_outliers_iqr(&after_slow_start, 1.5);
let iqr_result = if cleaned.is_empty() {
modified_trimean(&after_slow_start)
} else {
modified_trimean(&cleaned)
};
if after_slow_start.len() >= 4 {
let winsorized = winsorize(&after_slow_start, 0.05, 0.95);
let win_result = modified_trimean(&winsorized);
if iqr_result > 0.0 && win_result > 0.0 {
let divergence = (iqr_result - win_result).abs() / iqr_result.max(win_result);
if divergence > 0.15 {
return (iqr_result + win_result) / 2.0;
}
}
}
iqr_result
}
pub fn accurate_upload_bandwidth(samples: &[f64]) -> f64 {
if samples.is_empty() {
return 0.0;
}
let after_slow_start = discard_slow_start(samples, 0.3);
if after_slow_start.len() < 2 {
return accurate_bandwidth(samples);
}
let mut sorted_desc = after_slow_start.clone();
sorted_desc.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
let top_half_count = (sorted_desc.len() as f64 / 2.0).ceil() as usize;
let top_half: Vec<f64> = sorted_desc[..top_half_count].to_vec();
let cleaned = filter_outliers_iqr(&top_half, 1.5);
let iqr_result = if cleaned.is_empty() {
modified_trimean(&top_half)
} else {
modified_trimean(&cleaned)
};
if top_half.len() >= 4 {
let winsorized = winsorize(&top_half, 0.05, 0.95);
let win_result = modified_trimean(&winsorized);
if iqr_result > 0.0 && win_result > 0.0 {
let divergence = (iqr_result - win_result).abs() / iqr_result.max(win_result);
if divergence > 0.15 {
return (iqr_result + win_result) / 2.0;
}
}
}
iqr_result
}
pub fn jitter_rfc3550(samples: &[f64]) -> f64 {
if samples.len() < 2 {
return 0.0;
}
let mut j = 0.0_f64;
for i in 1..samples.len() {
let d = (samples[i] - samples[i - 1]).abs();
j += (d - j) / 16.0;
}
j
}
pub fn jitter_mad(samples: &[f64]) -> f64 {
if samples.len() < 2 {
return 0.0;
}
let sum: f64 = samples.windows(2).map(|w| (w[1] - w[0]).abs()).sum();
sum / (samples.len() - 1) as f64
}
pub fn coefficient_of_variation(values: &[f64]) -> f64 {
let m = mean(values);
if m == 0.0 {
return 0.0;
}
stddev(values) / m
}
pub fn weighted_merge(a: f64, b: f64, weight_a: f64) -> f64 {
let has_a = a > 0.0;
let has_b = b > 0.0;
if has_a && has_b {
a * weight_a + b * (1.0 - weight_a)
} else if has_a {
a
} else {
b
}
}
#[derive(Debug, Clone, Serialize)]
pub struct InverseVarianceResult {
pub value: f64,
pub weight_a: f64,
pub weight_b: f64,
}
pub fn inverse_variance_merge(a: f64, var_a: f64, b: f64, var_b: f64) -> InverseVarianceResult {
if a <= 0.0 && b <= 0.0 {
return InverseVarianceResult {
value: 0.0,
weight_a: 0.5,
weight_b: 0.5,
};
}
if a <= 0.0 {
return InverseVarianceResult {
value: b,
weight_a: 0.0,
weight_b: 1.0,
};
}
if b <= 0.0 {
return InverseVarianceResult {
value: a,
weight_a: 1.0,
weight_b: 0.0,
};
}
if var_a <= 0.0 && var_b <= 0.0 {
return InverseVarianceResult {
value: (a + b) / 2.0,
weight_a: 0.5,
weight_b: 0.5,
};
}
if var_a <= 0.0 {
return InverseVarianceResult {
value: a,
weight_a: 1.0,
weight_b: 0.0,
};
}
if var_b <= 0.0 {
return InverseVarianceResult {
value: b,
weight_a: 0.0,
weight_b: 1.0,
};
}
let w_a = 1.0 / var_a;
let w_b = 1.0 / var_b;
let total = w_a + w_b;
let mut weight_a = w_a / total;
let mut weight_b = w_b / total;
if weight_a < 0.3 {
weight_a = 0.3;
weight_b = 0.7;
} else if weight_a > 0.7 {
weight_a = 0.7;
weight_b = 0.3;
}
InverseVarianceResult {
value: a * weight_a + b * weight_b,
weight_a,
weight_b,
}
}
#[derive(Debug, Clone, Serialize)]
pub struct BootstrapCI {
pub estimate: f64,
pub lower: f64,
pub upper: f64,
pub margin: f64,
}
struct Xorshift64(u64);
impl Xorshift64 {
fn new(seed: u64) -> Self {
Self(if seed == 0 { 0x517cc1b727220a95 } else { seed })
}
fn next(&mut self) -> u64 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.0 = x;
x
}
fn next_usize(&mut self, bound: usize) -> usize {
(self.next() % bound as u64) as usize
}
}
pub fn bootstrap_ci(
samples: &[f64],
stat_fn: fn(&[f64]) -> f64,
b: usize,
alpha: f64,
) -> BootstrapCI {
if samples.len() < 4 {
let est = stat_fn(samples);
return BootstrapCI {
estimate: est,
lower: est,
upper: est,
margin: 0.0,
};
}
let estimate = stat_fn(samples);
let seed = samples.iter().fold(0u64, |acc, v| {
acc.wrapping_add(v.to_bits())
.wrapping_mul(6364136223846793005)
});
let mut rng = Xorshift64::new(seed);
let n = samples.len();
let mut bootstrap_stats: Vec<f64> = Vec::with_capacity(b);
let mut resample = vec![0.0_f64; n];
for _ in 0..b {
for val in resample.iter_mut() {
*val = samples[rng.next_usize(n)];
}
bootstrap_stats.push(stat_fn(&resample));
}
bootstrap_stats.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let lower = percentile(&bootstrap_stats, alpha / 2.0);
let upper = percentile(&bootstrap_stats, 1.0 - alpha / 2.0);
BootstrapCI {
estimate,
lower,
upper,
margin: (upper - lower) / 2.0,
}
}
pub const MIN_MERGE_SAMPLES: usize = 4;
pub fn capability_prior(name: &str) -> Option<f64> {
match name {
"cloudflare" | "applenq" | "fastcom" => Some(1.0),
"librespeed" | "cachefly" | "vultr" => Some(0.95),
"msak" => Some(0.85),
"ndt7" => Some(0.70),
_ => None,
}
}
fn median_of(values: &[f64]) -> f64 {
let mut s = values.to_vec();
s.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
quantile(&s, 0.5)
}
pub fn plateau_start(samples: &[f64]) -> usize {
let n = samples.len();
if n < 8 {
return (0.30_f64 * n as f64).ceil() as usize;
}
let eps = 0.10;
let w_len = 3;
let mut t_star: i64 = -1;
for t in 0..=(n - w_len) {
let ref_med = median_of(&samples[t..]);
if ref_med <= 0.0 {
continue;
}
let mut ok = true;
for &s in &samples[t..t + w_len] {
if (s - ref_med).abs() / ref_med >= eps {
ok = false;
break;
}
}
if ok {
t_star = t as i64;
break;
}
}
if t_star < 0 {
t_star = (0.30_f64 * n as f64).ceil() as i64;
}
let lo = (0.10_f64 * n as f64).ceil() as i64;
let hi = (0.40_f64 * n as f64).floor() as i64;
t_star.max(lo).min(hi) as usize
}
pub fn hodges_lehmann(values: &[f64]) -> f64 {
let n = values.len();
if n == 0 {
return 0.0;
}
if n == 1 {
return values[0];
}
let mut walsh = Vec::with_capacity(n * (n + 1) / 2);
for i in 0..n {
for &vj in &values[i..] {
walsh.push((values[i] + vj) / 2.0);
}
}
walsh.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
quantile(&walsh, 0.5)
}
#[derive(Debug, Clone, Serialize)]
pub struct BlockBootstrapResult {
pub theta_hat: f64,
pub theta_star_mean: f64,
pub variance: f64,
pub ci_lower: f64,
pub ci_upper: f64,
pub block_length: usize,
pub b: usize,
}
fn bca_bound(sorted_theta_star: &[f64], z0: f64, a: f64, alpha: f64) -> f64 {
let z = inv_normal(alpha);
let denom = 1.0 - a * (z0 + z);
let adj = if denom != 0.0 {
z0 + (z0 + z) / denom
} else {
z0
};
let mut aa = phi(adj);
if !aa.is_finite() {
aa = alpha;
}
aa = aa.clamp(0.0, 1.0);
quantile(sorted_theta_star, aa)
}
pub fn circular_block_bootstrap(
cleaned: &[f64],
rng: &mut Pcg32,
b_count: usize,
) -> BlockBootstrapResult {
let n = cleaned.len();
let theta_hat = modified_trimean(cleaned);
if n < 2 {
return BlockBootstrapResult {
theta_hat,
theta_star_mean: theta_hat,
variance: 0.0,
ci_lower: theta_hat,
ci_upper: theta_hat,
block_length: n,
b: b_count,
};
}
let l = 2.max((n as f64).cbrt().round() as usize);
let num_blocks = n.div_ceil(l);
let mut theta_star = vec![0.0_f64; b_count];
let mut resample = vec![0.0_f64; n];
for slot in theta_star.iter_mut() {
let mut filled = 0usize;
let mut blk = 0usize;
while blk < num_blocks && filled < n {
let start = rng.bounded_index(n);
let mut t = 0usize;
while t < l && filled < n {
resample[filled] = cleaned[(start + t) % n];
filled += 1;
t += 1;
}
blk += 1;
}
*slot = modified_trimean(&resample);
}
let theta_star_mean = sample_mean(&theta_star);
let boot_var = sample_variance(&theta_star);
let mut sorted_ts = theta_star.clone();
sorted_ts.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
if boot_var == 0.0 {
return BlockBootstrapResult {
theta_hat,
theta_star_mean,
variance: 0.0,
ci_lower: theta_hat,
ci_upper: theta_hat,
block_length: l,
b: b_count,
};
}
let mut count_less = 0usize;
for &ts in &theta_star {
if ts < theta_hat {
count_less += 1;
}
}
const EPS: f64 = 1e-12;
let prop = (count_less as f64 / b_count as f64).clamp(EPS, 1.0 - EPS);
let z0 = inv_normal(prop);
let mut jack = vec![0.0_f64; n];
let mut loo = vec![0.0_f64; n - 1];
for (i, jack_i) in jack.iter_mut().enumerate() {
let mut idx = 0usize;
for (j, &c) in cleaned.iter().enumerate() {
if j != i {
loo[idx] = c;
idx += 1;
}
}
*jack_i = modified_trimean(&loo);
}
let jack_mean = sample_mean(&jack);
let mut s2 = 0.0_f64;
let mut s3 = 0.0_f64;
for &jk in &jack {
let d = jack_mean - jk;
s2 += d * d;
s3 += d * d * d;
}
let a_den = 6.0 * s2.powf(1.5);
let a = if a_den != 0.0 { s3 / a_den } else { 0.0 };
let ci_lower = bca_bound(&sorted_ts, z0, a, 0.025);
let ci_upper = bca_bound(&sorted_ts, z0, a, 0.975);
BlockBootstrapResult {
theta_hat,
theta_star_mean,
variance: boot_var,
ci_lower,
ci_upper,
block_length: l,
b: b_count,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum AgreementBand {
High,
Moderate,
Low,
VeryLow,
Insufficient,
}
impl AgreementBand {
pub fn as_str(self) -> &'static str {
match self {
AgreementBand::High => "high",
AgreementBand::Moderate => "moderate",
AgreementBand::Low => "low",
AgreementBand::VeryLow => "very-low",
AgreementBand::Insufficient => "insufficient",
}
}
}
#[derive(Debug, Clone, Copy, Serialize, serde::Deserialize)]
pub struct BcaInterval {
pub lower: f64,
pub upper: f64,
}
#[derive(Debug, Clone, Copy, Serialize)]
pub struct CiBounds {
pub lower: f64,
pub upper: f64,
}
#[derive(Debug, Clone, serde::Deserialize)]
pub struct MergeProviderInput {
pub name: String,
pub y: f64,
#[serde(default)]
pub v: Option<f64>,
pub samples: usize,
#[serde(default)]
pub capability: Option<f64>,
#[serde(default)]
pub bca: Option<BcaInterval>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct MergeExclusion {
pub name: String,
pub samples: usize,
}
#[derive(Debug, Clone, Serialize)]
pub struct MergeWeight {
pub name: String,
pub y: f64,
pub v: f64,
pub w_star: f64,
pub w_star_capped: f64,
pub w_cap: f64,
}
#[derive(Debug, Clone, Serialize)]
pub struct MergeResult {
pub k: usize,
pub capacity: f64,
pub consensus: f64,
pub capacity_ci: CiBounds,
pub consensus_ci: CiBounds,
pub tau2: f64,
pub i2: Option<f64>,
pub q: f64,
pub band: AgreementBand,
pub tier: Vec<String>,
pub weights: Vec<MergeWeight>,
pub exclusions: Vec<MergeExclusion>,
}
fn known_variance(v: Option<f64>) -> Option<f64> {
match v {
Some(x) if x.is_finite() && x > 0.0 => Some(x),
_ => None,
}
}
fn empty_merge(exclusions: Vec<MergeExclusion>) -> MergeResult {
MergeResult {
k: 0,
capacity: 0.0,
consensus: 0.0,
capacity_ci: CiBounds {
lower: 0.0,
upper: 0.0,
},
consensus_ci: CiBounds {
lower: 0.0,
upper: 0.0,
},
tau2: 0.0,
i2: None,
q: 0.0,
band: AgreementBand::Insufficient,
tier: Vec::new(),
weights: Vec::new(),
exclusions,
}
}
pub fn merge_providers(inputs: &[MergeProviderInput]) -> MergeResult {
let mut exclusions: Vec<MergeExclusion> = Vec::new();
let mut qualifying: Vec<&MergeProviderInput> = Vec::new();
for p in inputs {
if p.samples >= MIN_MERGE_SAMPLES {
qualifying.push(p);
} else {
exclusions.push(MergeExclusion {
name: p.name.clone(),
samples: p.samples,
});
}
}
let k = qualifying.len();
if k == 0 {
return empty_merge(exclusions);
}
let known_vs: Vec<f64> = qualifying
.iter()
.filter_map(|p| known_variance(p.v))
.collect();
let max_known_v = known_vs.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let v_eff: Vec<f64> = qualifying
.iter()
.map(|p| {
known_variance(p.v).unwrap_or(if known_vs.is_empty() {
1.0
} else {
max_known_v
})
})
.collect();
let capability: Vec<f64> = qualifying
.iter()
.map(|p| {
p.capability
.unwrap_or_else(|| capability_prior(&p.name).unwrap_or(1.0))
})
.collect();
if k == 1 {
let p = qualifying[0];
let (lo, hi) = match p.bca {
Some(b) => (b.lower, b.upper),
None => (p.y, p.y),
};
let w_star = 1.0 / v_eff[0];
return MergeResult {
k: 1,
capacity: p.y,
consensus: p.y,
capacity_ci: CiBounds {
lower: lo,
upper: hi,
},
consensus_ci: CiBounds {
lower: lo,
upper: hi,
},
tau2: 0.0,
i2: None,
q: 0.0,
band: AgreementBand::Insufficient,
tier: vec![p.name.clone()],
weights: vec![MergeWeight {
name: p.name.clone(),
y: p.y,
v: v_eff[0],
w_star,
w_star_capped: w_star,
w_cap: capability[0] / v_eff[0],
}],
exclusions,
};
}
let w: Vec<f64> = v_eff.iter().map(|v| 1.0 / v).collect();
let sum_w = sum(&w);
let mu_f = {
let terms: Vec<f64> = qualifying
.iter()
.enumerate()
.map(|(i, p)| w[i] * p.y)
.collect();
sum(&terms) / sum_w
};
let q = {
let terms: Vec<f64> = qualifying
.iter()
.enumerate()
.map(|(i, p)| {
let d = p.y - mu_f;
w[i] * (d * d)
})
.collect();
sum(&terms)
};
let sum_w2 = {
let terms: Vec<f64> = w.iter().map(|x| x * x).collect();
sum(&terms)
};
let c = sum_w - sum_w2 / sum_w;
let tau2 = if c > 0.0 {
(0.0_f64).max((q - (k - 1) as f64) / c)
} else {
0.0
};
let i2 = if q > 0.0 {
(0.0_f64).max((q - (k - 1) as f64) / q)
} else {
0.0
};
let w_star: Vec<f64> = v_eff.iter().map(|v| 1.0 / (v + tau2)).collect();
let sum_w_star = sum(&w_star);
let cap = 0.70 * sum_w_star;
let w_star_capped: Vec<f64> = w_star.iter().map(|x| x.min(cap)).collect();
let sum_w_star_capped = sum(&w_star_capped);
let consensus = {
let terms: Vec<f64> = qualifying
.iter()
.enumerate()
.map(|(i, p)| w_star_capped[i] * p.y)
.collect();
sum(&terms) / sum_w_star_capped
};
let ymax = qualifying
.iter()
.map(|p| p.y)
.fold(f64::NEG_INFINITY, f64::max);
let mut tier_idx: Vec<usize> = Vec::new();
for (i, p) in qualifying.iter().enumerate() {
if p.y >= 0.85 * ymax {
tier_idx.push(i);
}
}
if k >= 3 && tier_idx.len() < 2 {
let mut order: Vec<usize> = (0..k).collect();
order.sort_by(|&i, &j| {
qualifying[j]
.y
.partial_cmp(&qualifying[i].y)
.unwrap_or(std::cmp::Ordering::Equal)
.then(i.cmp(&j))
});
let mut top2: Vec<usize> = order.into_iter().take(2).collect();
top2.sort_unstable();
tier_idx = top2;
}
let w_cap: Vec<f64> = v_eff
.iter()
.enumerate()
.map(|(i, v)| capability[i] / (v + tau2))
.collect();
let cap_den = {
let terms: Vec<f64> = tier_idx.iter().map(|&i| w_cap[i]).collect();
sum(&terms)
};
let capacity = if cap_den > 0.0 {
let terms: Vec<f64> = tier_idx
.iter()
.map(|&i| w_cap[i] * qualifying[i].y)
.collect();
sum(&terms) / cap_den
} else {
ymax
};
let consensus_ci;
let capacity_ci;
let band;
if k == 2 {
let lower = qualifying
.iter()
.enumerate()
.map(|(i, p)| p.y - 1.96 * v_eff[i].sqrt())
.fold(f64::INFINITY, f64::min);
let upper = qualifying
.iter()
.enumerate()
.map(|(i, p)| p.y + 1.96 * v_eff[i].sqrt())
.fold(f64::NEG_INFINITY, f64::max);
consensus_ci = CiBounds { lower, upper };
capacity_ci = CiBounds { lower, upper };
band = AgreementBand::Insufficient;
} else {
let qc_num = {
let terms: Vec<f64> = qualifying
.iter()
.enumerate()
.map(|(i, p)| {
let d = p.y - consensus;
w_star_capped[i] * (d * d)
})
.collect();
sum(&terms)
};
let se_c = ((1.0_f64).max(qc_num / (k - 1) as f64) / sum_w_star_capped).sqrt();
consensus_ci = CiBounds {
lower: consensus - t975(k - 1) * se_c,
upper: consensus + t975(k - 1) * se_c,
};
let tier_n = tier_idx.len();
if tier_n >= 2 {
let sum_w_star_tier = {
let terms: Vec<f64> = tier_idx.iter().map(|&i| w_star_capped[i]).collect();
sum(&terms)
};
let q_cap_num = {
let terms: Vec<f64> = tier_idx
.iter()
.map(|&i| {
let d = qualifying[i].y - capacity;
w_star_capped[i] * (d * d)
})
.collect();
sum(&terms)
};
let se_cap = ((1.0_f64).max(q_cap_num / (tier_n - 1) as f64) / sum_w_star_tier).sqrt();
capacity_ci = CiBounds {
lower: capacity - t975(tier_n - 1) * se_cap,
upper: capacity + t975(tier_n - 1) * se_cap,
};
} else {
let i = tier_idx[0];
let se = v_eff[i].sqrt();
capacity_ci = CiBounds {
lower: qualifying[i].y - 1.96 * se,
upper: qualifying[i].y + 1.96 * se,
};
}
band = if i2 < 0.25 {
AgreementBand::High
} else if i2 < 0.50 {
AgreementBand::Moderate
} else if i2 < 0.75 {
AgreementBand::Low
} else {
AgreementBand::VeryLow
};
}
let weights: Vec<MergeWeight> = qualifying
.iter()
.enumerate()
.map(|(i, p)| MergeWeight {
name: p.name.clone(),
y: p.y,
v: v_eff[i],
w_star: w_star[i],
w_star_capped: w_star_capped[i],
w_cap: w_cap[i],
})
.collect();
MergeResult {
k,
capacity,
consensus,
capacity_ci,
consensus_ci,
tau2,
i2: Some(i2),
q,
band,
tier: tier_idx
.iter()
.map(|&i| qualifying[i].name.clone())
.collect(),
weights,
exclusions,
}
}
pub fn pdv(rtts: &[f64]) -> f64 {
if rtts.is_empty() {
return 0.0;
}
let mut s = rtts.to_vec();
s.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
quantile(&s, 0.95) - quantile(&s, 0.5)
}
pub fn ipdv_mean(rtts: &[f64]) -> f64 {
if rtts.len() < 2 {
return 0.0;
}
let total: f64 = rtts.windows(2).map(|w| (w[1] - w[0]).abs()).sum();
total / (rtts.len() - 1) as f64
}
pub fn median_absolute_deviation(values: &[f64]) -> f64 {
if values.is_empty() {
return 0.0;
}
let med = median_of(values);
let mut dev: Vec<f64> = values.iter().map(|v| (v - med).abs()).collect();
dev.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1.4826 * quantile(&dev, 0.5)
}
#[derive(Debug, Clone, Serialize)]
pub struct JitterMetrics {
pub pdv: f64,
pub ipdv_mean: f64,
pub mad: f64,
pub jitter_rfc3550: f64,
}
pub fn jitter_metrics(rtts: &[f64]) -> JitterMetrics {
JitterMetrics {
pdv: pdv(rtts),
ipdv_mean: ipdv_mean(rtts),
mad: median_absolute_deviation(rtts),
jitter_rfc3550: jitter_rfc3550(rtts),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum BufferbloatGrade {
#[serde(rename = "A+")]
APlus,
A,
B,
C,
D,
F,
}
impl BufferbloatGrade {
pub fn as_str(self) -> &'static str {
match self {
BufferbloatGrade::APlus => "A+",
BufferbloatGrade::A => "A",
BufferbloatGrade::B => "B",
BufferbloatGrade::C => "C",
BufferbloatGrade::D => "D",
BufferbloatGrade::F => "F",
}
}
}
pub fn bufferbloat_grade(delta_ms: f64) -> BufferbloatGrade {
if delta_ms < 5.0 {
BufferbloatGrade::APlus
} else if delta_ms < 30.0 {
BufferbloatGrade::A
} else if delta_ms < 60.0 {
BufferbloatGrade::B
} else if delta_ms < 200.0 {
BufferbloatGrade::C
} else if delta_ms < 400.0 {
BufferbloatGrade::D
} else {
BufferbloatGrade::F
}
}
#[derive(Debug, Clone, Serialize)]
pub struct BufferbloatDeltaResult {
pub delta_ms: f64,
pub ratio: f64,
pub grade: BufferbloatGrade,
}
pub fn bufferbloat_delta(idle_rtts: &[f64], loaded_rtts: &[f64]) -> BufferbloatDeltaResult {
let p50_idle = if idle_rtts.is_empty() {
0.0
} else {
let mut s = idle_rtts.to_vec();
s.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
quantile(&s, 0.5)
};
let p95_loaded = if loaded_rtts.is_empty() {
0.0
} else {
let mut s = loaded_rtts.to_vec();
s.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
quantile(&s, 0.95)
};
let delta_ms = p95_loaded - p50_idle;
let ratio = if p50_idle > 0.0 {
p95_loaded / p50_idle
} else {
0.0
};
BufferbloatDeltaResult {
delta_ms,
ratio,
grade: bufferbloat_grade(delta_ms),
}
}
pub fn rpm(loaded_rtts: &[f64]) -> f64 {
if loaded_rtts.is_empty() {
return 0.0;
}
let mut s = loaded_rtts.to_vec();
s.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let p50 = quantile(&s, 0.5);
if p50 > 0.0 {
60000.0 / p50
} else {
0.0
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ConfidenceSequence {
pub t: usize,
pub u: f64,
pub mu_hat_mbps: f64,
pub half_width_mbps: f64,
pub width: f64,
pub stop: bool,
}
pub fn empirical_bernstein_cs(
samples_so_far: &[f64],
alpha: f64,
min_rtt_ms: f64,
) -> ConfidenceSequence {
let t = samples_so_far.len();
if t == 0 {
return ConfidenceSequence {
t: 0,
u: 0.0,
mu_hat_mbps: 0.0,
half_width_mbps: f64::INFINITY,
width: f64::INFINITY,
stop: false,
};
}
let mut max_v = 0.0_f64;
for &s in samples_so_far {
if s > max_v {
max_v = s;
}
}
let u = 2.0 * max_v;
if u <= 0.0 {
return ConfidenceSequence {
t,
u: 0.0,
mu_hat_mbps: 0.0,
half_width_mbps: f64::INFINITY,
width: f64::INFINITY,
stop: false,
};
}
let mut x_sum = 0.0_f64;
let mut sig2_sum = 0.0_f64;
for (i, &s) in samples_so_far.iter().enumerate() {
let x = s / u;
let mu_hat_prior = (0.5 + x_sum) / (i as f64 + 1.0);
let d = x - mu_hat_prior;
sig2_sum += d * d;
x_sum += x;
}
let mu_hat_t = (0.5 + x_sum) / (t as f64 + 1.0);
let sig2_t = (0.25 + sig2_sum) / (t as f64 + 1.0);
let ln_term = (2.0 / alpha).ln();
let width = (2.0 * sig2_t * ln_term / t as f64).sqrt() + 3.0 * ln_term / t as f64;
let half_width_mbps = width * u;
let mu_hat_mbps = mu_hat_t * u;
let gated = min_rtt_ms > 50.0;
let stop = !gated && t >= 12 && half_width_mbps <= (0.05 * mu_hat_mbps).max(2.0);
ConfidenceSequence {
t,
u,
mu_hat_mbps,
half_width_mbps,
width,
stop,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sanitize_drops_nan_and_infinite() {
let cleaned = sanitize(&[f64::NAN, 1.0, f64::INFINITY, 2.0, f64::NEG_INFINITY]);
assert_eq!(cleaned, vec![1.0, 2.0]);
}
#[test]
fn sanitize_keeps_clean_input_intact() {
let cleaned = sanitize(&[3.0, 1.0, 2.0]);
assert_eq!(cleaned, vec![3.0, 1.0, 2.0]);
}
fn well_behaved_samples() -> Vec<f64> {
(0..20).map(|i| 95.0 + (i % 5) as f64 * 2.5).collect()
}
#[test]
fn bootstrap_ci_is_deterministic() {
let samples = well_behaved_samples();
let a = bootstrap_ci(&samples, accurate_bandwidth, 1000, 0.05);
let b = bootstrap_ci(&samples, accurate_bandwidth, 1000, 0.05);
assert_eq!(a.lower, b.lower);
assert_eq!(a.upper, b.upper);
assert_eq!(a.estimate, b.estimate);
}
#[test]
fn bootstrap_ci_brackets_estimate() {
let samples = well_behaved_samples();
let ci = bootstrap_ci(&samples, accurate_bandwidth, 1000, 0.05);
assert!(
ci.lower <= ci.estimate && ci.estimate <= ci.upper,
"lower {} <= estimate {} <= upper {}",
ci.lower,
ci.estimate,
ci.upper
);
assert!(ci.margin >= 0.0);
}
#[test]
fn bootstrap_ci_degenerate_below_four() {
let ci = bootstrap_ci(&[100.0, 110.0, 105.0], accurate_bandwidth, 1000, 0.05);
assert_eq!(ci.margin, 0.0);
assert_eq!(ci.lower, ci.upper);
}
}