use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
#[cfg(feature = "parallel")]
use rayon::prelude::*;
use super::sequential::Detector;
#[derive(Debug, Clone, Copy, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum CriticalValue {
#[default]
Lookup,
Simulate {
samples: usize,
npts: usize,
seed: Option<u64>,
},
Fixed(f64),
}
#[inline]
fn standard_normal<R: Rng>(rng: &mut R) -> f64 {
let u1: f64 = rng.gen::<f64>().max(f64::MIN_POSITIVE);
let u2: f64 = rng.gen();
(-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
}
#[inline]
fn wiener_path<R: Rng>(rng: &mut R, npts: usize) -> Vec<f64> {
let scale = (1.0 / npts as f64).sqrt();
let mut path = Vec::with_capacity(npts);
let mut acc = 0.0;
for _ in 0..npts {
acc += standard_normal(rng) * scale;
path.push(acc);
}
path
}
#[inline]
fn c1(w: &[f64], gamma: f64) -> f64 {
let npts = w.len() as f64;
let mut best = f64::NEG_INFINITY;
for (i, &wi) in w.iter().enumerate() {
let t = (i + 1) as f64 / npts;
let denom = t.powf(gamma);
let v = wi / denom;
if v > best {
best = v;
}
}
best
}
#[inline]
fn c2(w: &[f64], gamma: f64) -> f64 {
let npts = w.len() as f64;
let mut best = f64::NEG_INFINITY;
for (i, &wi) in w.iter().enumerate() {
let t = (i + 1) as f64 / npts;
let denom = t.powf(gamma);
let v = wi.abs() / denom;
if v > best {
best = v;
}
}
best
}
fn p1(w: &[f64], gamma: f64) -> f64 {
let npts = w.len();
let n_f = npts as f64;
let mut best = f64::NEG_INFINITY;
for t in 1..npts {
let t_f = t as f64 / n_f;
let one_minus_t = 1.0 - t_f;
let mut min_adj = f64::INFINITY;
for s in 1..=t {
let s_f = s as f64 / n_f;
let denom = 1.0 - s_f;
if denom <= 0.0 {
continue;
}
let adj = (one_minus_t / denom) * w[s - 1];
if adj < min_adj {
min_adj = adj;
}
}
if min_adj == f64::INFINITY {
continue;
}
let v = (w[t - 1] - min_adj) / t_f.powf(gamma);
if v > best {
best = v;
}
}
best
}
fn p2(w: &[f64], gamma: f64) -> f64 {
let npts = w.len();
let n_f = npts as f64;
let mut best = f64::NEG_INFINITY;
for t in 1..npts {
let t_f = t as f64 / n_f;
let one_minus_t = 1.0 - t_f;
let wt = w[t - 1];
let mut max_abs = f64::NEG_INFINITY;
for s in 1..=t {
let s_f = s as f64 / n_f;
let denom = 1.0 - s_f;
if denom <= 0.0 {
continue;
}
let adj = (one_minus_t / denom) * w[s - 1];
let v = (wt - adj).abs();
if v > max_abs {
max_abs = v;
}
}
if max_abs == f64::NEG_INFINITY {
continue;
}
let v = max_abs / t_f.powf(gamma);
if v > best {
best = v;
}
}
best
}
#[inline]
fn apply_functional(detector: Detector, w: &[f64], gamma: f64) -> f64 {
match detector {
Detector::Cusum1 => c1(w, gamma),
Detector::Cusum => c2(w, gamma),
Detector::PageCusum1 => p1(w, gamma),
Detector::PageCusum => p2(w, gamma),
}
}
fn empirical_quantile(samples: &mut [f64], alpha: f64) -> f64 {
samples.sort_by(|a, b| a.partial_cmp(b).unwrap());
let n = samples.len();
if n == 0 {
return 0.0;
}
let q = 1.0 - alpha;
let h = q * (n as f64 - 1.0);
let lo = h.floor() as usize;
let hi = (lo + 1).min(n - 1);
let frac = h - lo as f64;
samples[lo] * (1.0 - frac) + samples[hi] * frac
}
pub fn simulate_critical_value(
detector: Detector,
gamma: f64,
alpha: f64,
samples: usize,
npts: usize,
seed: Option<u64>,
) -> f64 {
assert!((0.0..0.5).contains(&gamma), "gamma must be in [0, 0.5)");
assert!((0.0..=1.0).contains(&alpha), "alpha must be in [0, 1]");
assert!(samples >= 20, "samples must be at least 20");
assert!(npts >= 20, "npts must be at least 20");
let base_seed = seed.unwrap_or(0xC4_D5_E6_F7_01_23_45_67);
#[cfg(not(feature = "parallel"))]
let mut stats: Vec<f64> = {
let mut rng = StdRng::seed_from_u64(base_seed);
(0..samples)
.map(|_| {
let path = wiener_path(&mut rng, npts);
apply_functional(detector, &path, gamma)
})
.collect()
};
#[cfg(feature = "parallel")]
let mut stats: Vec<f64> = (0..samples)
.into_par_iter()
.map(|i| {
let mut rng = StdRng::seed_from_u64(base_seed.wrapping_add(i as u64));
let path = wiener_path(&mut rng, npts);
apply_functional(detector, &path, gamma)
})
.collect();
empirical_quantile(&mut stats, alpha)
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn quantile_basic() {
let mut v = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let q = empirical_quantile(&mut v, 0.05);
assert_relative_eq!(q, 4.8, epsilon = 1e-10);
}
#[test]
fn wiener_path_length_and_start() {
let mut rng = StdRng::seed_from_u64(42);
let path = wiener_path(&mut rng, 100);
assert_eq!(path.len(), 100);
assert!(path[0].is_finite());
}
#[test]
fn c1_matches_hand_calc() {
let w: Vec<f64> = (1..=10).map(|i| i as f64 / 10.0).collect();
assert_relative_eq!(c1(&w, 0.0), 1.0, epsilon = 1e-10);
}
#[test]
fn c2_matches_c1_for_positive_path() {
let w: Vec<f64> = (1..=10).map(|i| i as f64 / 10.0).collect();
assert_relative_eq!(c1(&w, 0.0), c2(&w, 0.0), epsilon = 1e-10);
}
#[test]
fn simulate_cusum1_gamma0_matches_half_normal() {
let cv = simulate_critical_value(Detector::Cusum1, 0.0, 0.05, 2000, 500, Some(7));
assert!(
(1.5..3.5).contains(&cv),
"expected cv in [1.5, 3.5], got {}",
cv
);
}
#[test]
fn simulate_deterministic_with_seed() {
let a = simulate_critical_value(Detector::PageCusum, 0.0, 0.05, 200, 200, Some(123));
let b = simulate_critical_value(Detector::PageCusum, 0.0, 0.05, 200, 200, Some(123));
assert_relative_eq!(a, b, epsilon = 1e-12);
}
#[test]
fn simulate_all_four_detectors_finite() {
for d in [
Detector::Cusum,
Detector::Cusum1,
Detector::PageCusum,
Detector::PageCusum1,
] {
let cv = simulate_critical_value(d, 0.0, 0.05, 200, 100, Some(99));
assert!(cv.is_finite() && cv > 0.0, "detector {:?}: cv={}", d, cv);
}
}
#[test]
fn simulate_higher_alpha_lower_critical_value() {
let cv_01 = simulate_critical_value(Detector::PageCusum, 0.0, 0.01, 500, 200, Some(1));
let cv_10 = simulate_critical_value(Detector::PageCusum, 0.0, 0.10, 500, 200, Some(1));
assert!(
cv_01 > cv_10,
"cv(α=0.01)={} should exceed cv(α=0.10)={}",
cv_01,
cv_10
);
}
}