use super::{Rng, StreamGenerator, TaskType};
#[derive(Debug, Clone)]
pub struct MackeyGlass {
series: Vec<f64>,
cursor: usize,
n_lags: usize,
}
impl MackeyGlass {
pub fn new(seed: u64) -> Self {
Self::with_config(seed, 17, 6, 500, 100_000)
}
pub fn with_config(
seed: u64,
tau: usize,
n_lags: usize,
warmup: usize,
max_samples: usize,
) -> Self {
let mut rng = Rng::new(seed);
let beta = 0.2;
let gamma = 0.1;
let n_exp = 10.0;
let total = warmup + n_lags + max_samples + 1;
let mut x = vec![0.0; total.max(tau + 1)];
for val in x.iter_mut().take(tau) {
*val = 0.9 + rng.uniform_range(-0.1, 0.1);
}
for t in tau..total {
let x_tau = x[t - tau];
x[t] = x[t - 1] + beta * x_tau / (1.0 + x_tau.powf(n_exp)) - gamma * x[t - 1];
}
let start = warmup;
let series: Vec<f64> = x[start..total].to_vec();
Self {
series,
cursor: n_lags, n_lags,
}
}
}
impl StreamGenerator for MackeyGlass {
fn next_sample(&mut self) -> (Vec<f64>, f64) {
let start = self.cursor - self.n_lags;
let features: Vec<f64> = self.series[start..self.cursor].to_vec();
let target = if self.cursor < self.series.len() - 1 {
self.series[self.cursor + 1]
} else {
self.series[self.cursor]
};
self.cursor += 1;
if self.cursor >= self.series.len() - 1 {
self.cursor = self.n_lags;
}
(features, target)
}
fn n_features(&self) -> usize {
self.n_lags
}
fn task_type(&self) -> TaskType {
TaskType::Regression
}
fn drift_occurred(&self) -> bool {
false }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mackey_glass_produces_correct_n_features() {
let mut gen = MackeyGlass::new(42);
let (features, _) = gen.next_sample();
assert_eq!(
features.len(),
6,
"MackeyGlass default should produce 6 lag features, got {}",
features.len()
);
}
#[test]
fn mackey_glass_custom_lags() {
let mut gen = MackeyGlass::with_config(42, 17, 10, 100, 1000);
let (features, _) = gen.next_sample();
assert_eq!(
features.len(),
10,
"MackeyGlass with 10 lags should produce 10 features"
);
}
#[test]
fn mackey_glass_task_type_is_regression() {
let gen = MackeyGlass::new(42);
assert_eq!(gen.task_type(), TaskType::Regression);
}
#[test]
fn mackey_glass_produces_finite_values() {
let mut gen = MackeyGlass::new(123);
for i in 0..5000 {
let (features, target) = gen.next_sample();
for (j, f) in features.iter().enumerate() {
assert!(
f.is_finite(),
"feature {} at sample {} is not finite: {}",
j,
i,
f
);
}
assert!(
target.is_finite(),
"target at sample {} is not finite: {}",
i,
target
);
}
}
#[test]
fn mackey_glass_values_bounded() {
let mut gen = MackeyGlass::new(42);
for i in 0..5000 {
let (features, target) = gen.next_sample();
for (j, &f) in features.iter().enumerate() {
assert!(
f.abs() < 5.0,
"feature {} at sample {} has unexpected magnitude: {}",
j,
i,
f
);
}
assert!(
target.abs() < 5.0,
"target at sample {} has unexpected magnitude: {}",
i,
target
);
}
}
#[test]
fn mackey_glass_no_drift() {
let mut gen = MackeyGlass::new(42);
for _ in 0..500 {
gen.next_sample();
assert!(
!gen.drift_occurred(),
"MackeyGlass should never signal drift"
);
}
}
#[test]
fn mackey_glass_deterministic_with_same_seed() {
let mut gen1 = MackeyGlass::new(42);
let mut gen2 = MackeyGlass::new(42);
for _ in 0..200 {
let (f1, t1) = gen1.next_sample();
let (f2, t2) = gen2.next_sample();
assert_eq!(f1, f2, "same seed should produce identical features");
assert_eq!(t1, t2, "same seed should produce identical targets");
}
}
#[test]
fn mackey_glass_series_is_chaotic() {
let mut gen = MackeyGlass::new(42);
let mut values = Vec::new();
for _ in 0..100 {
let (_, target) = gen.next_sample();
values.push(target);
}
let mean: f64 = values.iter().sum::<f64>() / values.len() as f64;
let var: f64 = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / values.len() as f64;
assert!(
var > 1e-6,
"Mackey-Glass series should have non-trivial variance, got {}",
var
);
}
}