use crate::{Error, Result};
#[derive(Debug, Clone)]
pub struct Xorshift(u64);
impl Xorshift {
pub fn new(seed: u64) -> Self {
Xorshift(if seed == 0 { 0x9E3779B97F4A7C15 } else { seed })
}
pub fn next_u64(&mut self) -> u64 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.0 = x;
x
}
pub fn uniform(&mut self) -> f64 {
(self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
}
pub fn normal(&mut self) -> f64 {
let u1 = self.uniform().max(1e-300);
let u2 = self.uniform();
(-2.0 * u1.ln()).sqrt() * (2.0 * core::f64::consts::PI * u2).cos()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Innovations {
Gaussian,
StudentT { df: u32 },
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Contamination {
pub prob: f64,
pub magnitude: f64,
}
#[derive(Debug, Clone, Copy)]
pub struct ArmaSim<'a> {
pub phi: &'a [f64],
pub theta: &'a [f64],
pub innovations: Innovations,
pub additive_outliers: Option<Contamination>,
pub obs_noise_sd: f64,
pub burn_in: usize,
}
impl Default for ArmaSim<'static> {
fn default() -> Self {
ArmaSim {
phi: &[],
theta: &[],
innovations: Innovations::Gaussian,
additive_outliers: None,
obs_noise_sd: 0.0,
burn_in: 500,
}
}
}
impl ArmaSim<'_> {
pub fn simulate(&self, n: usize, seed: u64) -> Result<Vec<f64>> {
if n == 0 {
return Err(Error::Invalid("n must be >= 1"));
}
if let Innovations::StudentT { df } = self.innovations {
if df == 0 {
return Err(Error::Invalid("StudentT df must be >= 1"));
}
}
if let Some(c) = self.additive_outliers {
if !(0.0..=1.0).contains(&c.prob) || !c.magnitude.is_finite() {
return Err(Error::Invalid("contamination prob must be in [0, 1]"));
}
}
if !self.obs_noise_sd.is_finite() || self.obs_noise_sd < 0.0 {
return Err(Error::Invalid("obs_noise_sd must be finite and >= 0"));
}
let mut rng = Xorshift::new(seed);
let total = n + self.burn_in;
let e: Vec<f64> = (0..total)
.map(|_| draw(&mut rng, self.innovations))
.collect();
let start = self.phi.len().max(self.theta.len());
let mut x = vec![0.0; total];
for t in start..total {
let mut v = e[t];
for (j, p) in self.phi.iter().enumerate() {
v += p * x[t - j - 1];
}
for (j, th) in self.theta.iter().enumerate() {
v += th * e[t - j - 1];
}
x[t] = v;
}
let mut out = x[self.burn_in..].to_vec();
if let Some(c) = self.additive_outliers {
for v in out.iter_mut() {
if rng.uniform() < c.prob {
let sign = if rng.uniform() < 0.5 { -1.0 } else { 1.0 };
*v += sign * c.magnitude;
}
}
}
if self.obs_noise_sd > 0.0 {
for v in out.iter_mut() {
*v += self.obs_noise_sd * rng.normal();
}
}
Ok(out)
}
}
fn draw(rng: &mut Xorshift, innovations: Innovations) -> f64 {
match innovations {
Innovations::Gaussian => rng.normal(),
Innovations::StudentT { df } => {
let z = rng.normal();
let chi2: f64 = (0..df).map(|_| rng.normal().powi(2)).sum();
z / (chi2 / df as f64).sqrt()
}
}
}
pub fn arma(phi: &[f64], theta: &[f64], n: usize, seed: u64) -> Vec<f64> {
ArmaSim {
phi,
theta,
..Default::default()
}
.simulate(n, seed)
.expect("default ArmaSim configuration is valid")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn same_seed_same_series() {
let a = arma(&[0.5], &[], 100, 42);
let b = arma(&[0.5], &[], 100, 42);
assert_eq!(a, b);
let c = arma(&[0.5], &[], 100, 43);
assert_ne!(a, c);
}
#[test]
fn student_t_has_heavier_tails_than_gaussian() {
let base = ArmaSim::default();
let t3 = ArmaSim {
innovations: Innovations::StudentT { df: 3 },
..Default::default()
};
let g = base.simulate(20000, 7).unwrap();
let t = t3.simulate(20000, 7).unwrap();
let kurt = |x: &[f64]| {
let n = x.len() as f64;
let m2 = x.iter().map(|v| v * v).sum::<f64>() / n;
let m4 = x.iter().map(|v| v.powi(4)).sum::<f64>() / n;
m4 / (m2 * m2)
};
assert!(kurt(&g) < 3.5, "gaussian kurtosis {}", kurt(&g));
assert!(kurt(&t) > 4.5, "t3 kurtosis {}", kurt(&t));
}
#[test]
fn invalid_configs_are_errors() {
assert!(matches!(
ArmaSim {
innovations: Innovations::StudentT { df: 0 },
..Default::default()
}
.simulate(100, 1),
Err(Error::Invalid(_))
));
assert!(matches!(
ArmaSim {
additive_outliers: Some(Contamination {
prob: 1.5,
magnitude: 9.0
}),
..Default::default()
}
.simulate(100, 1),
Err(Error::Invalid(_))
));
}
#[test]
fn outliers_and_noise_change_only_the_observed_series() {
let clean = ArmaSim {
phi: &[0.7],
..Default::default()
};
let dirty = ArmaSim {
phi: &[0.7],
additive_outliers: Some(Contamination {
prob: 0.05,
magnitude: 10.0,
}),
obs_noise_sd: 0.5,
..Default::default()
};
let a = clean.simulate(500, 11).unwrap();
let b = dirty.simulate(500, 11).unwrap();
let n_big = a
.iter()
.zip(&b)
.filter(|(u, v)| (*u - *v).abs() > 5.0)
.count();
assert!(n_big > 5, "expected some outliers, got {n_big}");
}
}