use scirs2_core::ndarray::Array1;
use scirs2_core::random::prelude::*;
use scirs2_core::random::rngs::StdRng;
use scirs2_core::random::{Normal, RngExt};
use sklears_core::error::{Result, SklearsError};
use std::f64::consts::PI;
pub fn make_ar_process(
n_samples: usize,
order: usize,
coeffs: &[f64],
noise_std: f64,
random_state: Option<u64>,
) -> Result<Array1<f64>> {
if coeffs.len() != order {
return Err(SklearsError::InvalidInput(
"coeffs.len() must equal order".to_string(),
));
}
if n_samples <= order {
return Err(SklearsError::InvalidInput(
"n_samples must exceed the AR order".to_string(),
));
}
if noise_std < 0.0 {
return Err(SklearsError::InvalidInput(
"noise_std must be non-negative".to_string(),
));
}
let mut rng = if let Some(seed) = random_state {
StdRng::seed_from_u64(seed)
} else {
StdRng::from_rng(&mut scirs2_core::random::thread_rng())
};
let normal = Normal::new(0.0, noise_std).expect("operation should succeed");
let burn_in = (order * 20).max(50);
let total = burn_in + n_samples;
let mut y = vec![0.0_f64; order + total];
for t in 0..total {
let ar_term: f64 = coeffs
.iter()
.enumerate()
.map(|(i, &c)| c * y[order + t - 1 - i])
.sum();
let eps: f64 = rng.sample(normal);
y[order + t] = ar_term + eps;
}
let series: Vec<f64> = y[(order + burn_in)..(order + total)].to_vec();
Ok(Array1::from_vec(series))
}
pub fn make_ma_process(
n_samples: usize,
order: usize,
coeffs: &[f64],
noise_std: f64,
random_state: Option<u64>,
) -> Result<Array1<f64>> {
if coeffs.len() != order {
return Err(SklearsError::InvalidInput(
"coeffs.len() must equal order".to_string(),
));
}
if n_samples <= order {
return Err(SklearsError::InvalidInput(
"n_samples must exceed the MA order".to_string(),
));
}
if noise_std < 0.0 {
return Err(SklearsError::InvalidInput(
"noise_std must be non-negative".to_string(),
));
}
let mut rng = if let Some(seed) = random_state {
StdRng::seed_from_u64(seed)
} else {
StdRng::from_rng(&mut scirs2_core::random::thread_rng())
};
let normal = Normal::new(0.0, noise_std).expect("operation should succeed");
let mut eps = vec![0.0_f64; order + n_samples];
for e in eps.iter_mut() {
*e = rng.sample(normal);
}
let mut y = Array1::zeros(n_samples);
for t in 0..n_samples {
let idx = order + t;
let ma_term: f64 = coeffs
.iter()
.enumerate()
.map(|(i, &c)| c * eps[idx - 1 - i])
.sum();
y[t] = eps[idx] + ma_term;
}
Ok(y)
}
pub fn make_arma_process(
n_samples: usize,
ar_coeffs: &[f64],
ma_coeffs: &[f64],
noise_std: f64,
random_state: Option<u64>,
) -> Result<Array1<f64>> {
let p = ar_coeffs.len();
let q = ma_coeffs.len();
let max_order = p.max(q);
if n_samples <= max_order {
return Err(SklearsError::InvalidInput(
"n_samples must exceed the larger of the AR and MA orders".to_string(),
));
}
if noise_std < 0.0 {
return Err(SklearsError::InvalidInput(
"noise_std must be non-negative".to_string(),
));
}
let mut rng = if let Some(seed) = random_state {
StdRng::seed_from_u64(seed)
} else {
StdRng::from_rng(&mut scirs2_core::random::thread_rng())
};
let normal = Normal::new(0.0, noise_std).expect("operation should succeed");
let burn_in = (max_order * 20).max(50);
let total = burn_in + n_samples;
let mut eps = vec![0.0_f64; q + total];
for e in eps.iter_mut() {
*e = rng.sample(normal);
}
let mut y = vec![0.0_f64; p + total];
for t in 0..total {
let ar_term: f64 = ar_coeffs
.iter()
.enumerate()
.map(|(i, &c)| c * y[p + t - 1 - i])
.sum();
let ma_term: f64 = ma_coeffs
.iter()
.enumerate()
.map(|(j, &c)| c * eps[q + t - 1 - j])
.sum();
y[p + t] = ar_term + eps[q + t] + ma_term;
}
let series: Vec<f64> = y[(p + burn_in)..(p + total)].to_vec();
Ok(Array1::from_vec(series))
}
pub fn make_seasonal_trend(
n_samples: usize,
period: usize,
trend_slope: f64,
amplitude: f64,
noise_std: f64,
random_state: Option<u64>,
) -> Result<Array1<f64>> {
if period < 1 {
return Err(SklearsError::InvalidInput(
"period must be at least 1".to_string(),
));
}
if n_samples <= period {
return Err(SklearsError::InvalidInput(
"n_samples must exceed one full period".to_string(),
));
}
if noise_std < 0.0 {
return Err(SklearsError::InvalidInput(
"noise_std must be non-negative".to_string(),
));
}
let mut rng = if let Some(seed) = random_state {
StdRng::seed_from_u64(seed)
} else {
StdRng::from_rng(&mut scirs2_core::random::thread_rng())
};
let normal = Normal::new(0.0, noise_std).expect("operation should succeed");
let mut y = Array1::zeros(n_samples);
for t in 0..n_samples {
let trend = trend_slope * t as f64;
let seasonal = amplitude * (2.0 * PI * t as f64 / period as f64).sin();
y[t] = trend + seasonal + rng.sample::<f64, _>(normal);
}
Ok(y)
}
pub fn make_random_walk(
n_samples: usize,
step_std: f64,
drift: f64,
random_state: Option<u64>,
) -> Result<Array1<f64>> {
if n_samples < 1 {
return Err(SklearsError::InvalidInput(
"n_samples must be at least 1".to_string(),
));
}
if step_std < 0.0 {
return Err(SklearsError::InvalidInput(
"step_std must be non-negative".to_string(),
));
}
let mut rng = if let Some(seed) = random_state {
StdRng::seed_from_u64(seed)
} else {
StdRng::from_rng(&mut scirs2_core::random::thread_rng())
};
let normal = Normal::new(0.0, step_std).expect("operation should succeed");
let mut y = Array1::zeros(n_samples);
for t in 1..n_samples {
y[t] = y[t - 1] + drift + rng.sample::<f64, _>(normal);
}
Ok(y)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_make_ar_process_length() {
let series =
make_ar_process(200, 2, &[0.5, -0.2], 1.0, Some(42)).expect("operation should succeed");
assert_eq!(series.len(), 200);
}
#[test]
fn test_make_ar_process_deterministic_with_seed() {
let a =
make_ar_process(150, 2, &[0.5, -0.2], 1.0, Some(7)).expect("operation should succeed");
let b =
make_ar_process(150, 2, &[0.5, -0.2], 1.0, Some(7)).expect("operation should succeed");
assert_eq!(a, b);
}
#[test]
fn test_make_ar_process_invalid_coeffs_len() {
assert!(make_ar_process(100, 2, &[0.5], 1.0, Some(42)).is_err());
}
#[test]
fn test_make_ar_process_invalid_n_samples() {
assert!(make_ar_process(2, 2, &[0.5, -0.2], 1.0, Some(42)).is_err());
}
#[test]
fn test_make_ar_process_invalid_noise_std() {
assert!(make_ar_process(100, 1, &[0.5], -1.0, Some(42)).is_err());
}
#[test]
fn test_ar1_stationary_variance() {
let series =
make_ar_process(20_000, 1, &[0.5], 1.0, Some(42)).expect("operation should succeed");
assert!(series.iter().all(|v| v.is_finite()));
let empirical_var = series.var(0.0);
assert!(
(empirical_var - 1.3333).abs() < 0.3,
"empirical variance {empirical_var} too far from theoretical 1.3333"
);
}
#[test]
fn test_make_ma_process_length() {
let series =
make_ma_process(200, 2, &[0.5, -0.2], 1.0, Some(42)).expect("operation should succeed");
assert_eq!(series.len(), 200);
}
#[test]
fn test_make_ma_process_deterministic_with_seed() {
let a =
make_ma_process(150, 2, &[0.5, -0.2], 1.0, Some(7)).expect("operation should succeed");
let b =
make_ma_process(150, 2, &[0.5, -0.2], 1.0, Some(7)).expect("operation should succeed");
assert_eq!(a, b);
}
#[test]
fn test_make_ma_process_invalid_coeffs_len() {
assert!(make_ma_process(100, 2, &[0.5], 1.0, Some(42)).is_err());
}
#[test]
fn test_make_ma_process_invalid_n_samples() {
assert!(make_ma_process(2, 2, &[0.5, -0.2], 1.0, Some(42)).is_err());
}
#[test]
fn test_make_ma_process_invalid_noise_std() {
assert!(make_ma_process(100, 1, &[0.5], -1.0, Some(42)).is_err());
}
#[test]
fn test_ma1_stationary_variance() {
let series =
make_ma_process(20_000, 1, &[0.5], 1.0, Some(42)).expect("operation should succeed");
assert!(series.iter().all(|v| v.is_finite()));
let empirical_var = series.var(0.0);
assert!(
(empirical_var - 1.25).abs() < 0.3,
"empirical variance {empirical_var} too far from theoretical 1.25"
);
}
#[test]
fn test_make_arma_process_length() {
let series = make_arma_process(300, &[0.4], &[0.3], 1.0, Some(42))
.expect("operation should succeed");
assert_eq!(series.len(), 300);
}
#[test]
fn test_make_arma_process_deterministic_with_seed() {
let a = make_arma_process(150, &[0.4, -0.1], &[0.3], 1.0, Some(9))
.expect("operation should succeed");
let b = make_arma_process(150, &[0.4, -0.1], &[0.3], 1.0, Some(9))
.expect("operation should succeed");
assert_eq!(a, b);
}
#[test]
fn test_make_arma_process_invalid_n_samples() {
assert!(make_arma_process(2, &[0.4, -0.1], &[0.3], 1.0, Some(42)).is_err());
}
#[test]
fn test_make_arma_process_invalid_noise_std() {
assert!(make_arma_process(100, &[0.4], &[0.3], -1.0, Some(42)).is_err());
}
#[test]
fn test_make_arma_process_degrades_to_pure_ar() {
let series =
make_arma_process(200, &[0.5], &[], 1.0, Some(42)).expect("operation should succeed");
assert_eq!(series.len(), 200);
assert!(series.iter().all(|v| v.is_finite()));
}
#[test]
fn test_make_arma_process_degrades_to_pure_ma() {
let series =
make_arma_process(200, &[], &[0.5], 1.0, Some(42)).expect("operation should succeed");
assert_eq!(series.len(), 200);
assert!(series.iter().all(|v| v.is_finite()));
}
#[test]
fn test_make_arma_process_degrades_to_white_noise() {
let series =
make_arma_process(200, &[], &[], 1.0, Some(42)).expect("operation should succeed");
assert_eq!(series.len(), 200);
assert!(series.iter().all(|v| v.is_finite()));
}
#[test]
fn test_make_seasonal_trend_length() {
let series = make_seasonal_trend(500, 50, 0.01, 5.0, 0.5, Some(42))
.expect("operation should succeed");
assert_eq!(series.len(), 500);
}
#[test]
fn test_make_seasonal_trend_deterministic_with_seed() {
let a = make_seasonal_trend(300, 20, 0.01, 5.0, 0.5, Some(3))
.expect("operation should succeed");
let b = make_seasonal_trend(300, 20, 0.01, 5.0, 0.5, Some(3))
.expect("operation should succeed");
assert_eq!(a, b);
}
#[test]
fn test_make_seasonal_trend_invalid_period() {
assert!(make_seasonal_trend(100, 0, 0.01, 5.0, 0.5, Some(42)).is_err());
}
#[test]
fn test_make_seasonal_trend_invalid_n_samples() {
assert!(make_seasonal_trend(10, 50, 0.01, 5.0, 0.5, Some(42)).is_err());
}
#[test]
fn test_make_seasonal_trend_invalid_noise_std() {
assert!(make_seasonal_trend(100, 10, 0.01, 5.0, -0.5, Some(42)).is_err());
}
#[test]
fn test_make_seasonal_trend_periodic_with_zero_noise() {
let period = 50;
let series = make_seasonal_trend(300, period, 0.0, 5.0, 0.0, Some(42))
.expect("operation should succeed");
for t in [0usize, 10, 25, 49, 100] {
assert!(t + period < series.len());
let diff = (series[t] - series[t + period]).abs();
assert!(diff < 1e-9, "series not periodic at t={t}: diff={diff}");
}
}
#[test]
fn test_make_random_walk_length() {
let series = make_random_walk(500, 1.0, 0.1, Some(42)).expect("operation should succeed");
assert_eq!(series.len(), 500);
}
#[test]
fn test_make_random_walk_deterministic_with_seed() {
let a = make_random_walk(300, 1.0, 0.1, Some(5)).expect("operation should succeed");
let b = make_random_walk(300, 1.0, 0.1, Some(5)).expect("operation should succeed");
assert_eq!(a, b);
}
#[test]
fn test_make_random_walk_invalid_n_samples() {
assert!(make_random_walk(0, 1.0, 0.1, Some(42)).is_err());
}
#[test]
fn test_make_random_walk_invalid_step_std() {
assert!(make_random_walk(100, -1.0, 0.1, Some(42)).is_err());
}
#[test]
fn test_make_random_walk_increment_statistics() {
let step_std = 1.0;
let drift = 0.1;
let series =
make_random_walk(5000, step_std, drift, Some(42)).expect("operation should succeed");
let diffs: Vec<f64> = (1..series.len())
.map(|t| series[t] - series[t - 1])
.collect();
let n = diffs.len() as f64;
let mean: f64 = diffs.iter().sum::<f64>() / n;
let variance: f64 = diffs.iter().map(|d| (d - mean).powi(2)).sum::<f64>() / n;
let std = variance.sqrt();
assert!(
(mean - drift).abs() < 0.15,
"increment mean {mean} too far from drift {drift}"
);
assert!(
(std - step_std).abs() < 0.15,
"increment std {std} too far from step_std {step_std}"
);
}
}