use super::{Rng, StreamGenerator, TaskType};
#[derive(Debug, Clone)]
pub struct PeriodicStream {
rng: Rng,
period: f64,
amplitude: f64,
harmonics: usize,
noise_std: f64,
window: usize,
phases: Vec<f64>,
signal_buf: Vec<f64>,
t: usize,
}
impl PeriodicStream {
pub const DEFAULT_PERIOD: usize = 20;
pub const DEFAULT_AMPLITUDE: f64 = 1.0;
pub const DEFAULT_HARMONICS: usize = 3;
pub const DEFAULT_NOISE_STD: f64 = 0.05;
pub const DEFAULT_WINDOW: usize = 10;
pub fn new(seed: u64, window: usize, period: usize) -> Self {
Self::with_config(
seed,
period,
Self::DEFAULT_AMPLITUDE,
Self::DEFAULT_HARMONICS,
Self::DEFAULT_NOISE_STD,
window,
)
}
pub fn with_config(
seed: u64,
period: usize,
amplitude: f64,
harmonics: usize,
noise_std: f64,
window: usize,
) -> Self {
assert!(period > 0, "period must be > 0");
assert!(harmonics > 0, "harmonics must be > 0");
assert!(window > 0, "window must be > 0");
let mut phase_rng = Rng::new(seed.wrapping_add(0xDEAD_BEEF));
let phases: Vec<f64> = (0..harmonics)
.map(|_| phase_rng.uniform_range(0.0, 2.0 * std::f64::consts::PI))
.collect();
let mut signal_buf = Vec::with_capacity(window + 1);
for step in 0..=window {
signal_buf.push(Self::eval_signal(
step,
period as f64,
amplitude,
harmonics,
&phases,
));
}
Self {
rng: Rng::new(seed),
period: period as f64,
amplitude,
harmonics,
noise_std,
window,
phases,
signal_buf,
t: window, }
}
fn eval_signal(t: usize, period: f64, amplitude: f64, harmonics: usize, phases: &[f64]) -> f64 {
let mut y = 0.0;
for k in 1..=harmonics {
let amp_k = amplitude / k as f64;
let freq = 2.0 * std::f64::consts::PI * k as f64 / period;
y += amp_k * (freq * t as f64 + phases[k - 1]).sin();
}
y
}
fn eval_current(&self, t: usize) -> f64 {
Self::eval_signal(t, self.period, self.amplitude, self.harmonics, &self.phases)
}
pub fn period(&self) -> f64 {
self.period
}
pub fn window(&self) -> usize {
self.window
}
}
impl StreamGenerator for PeriodicStream {
fn next_sample(&mut self) -> (Vec<f64>, f64) {
let features: Vec<f64> = (0..self.window)
.map(|i| {
let clean = self.signal_buf[i];
if self.noise_std > 0.0 {
clean + self.rng.normal(0.0, self.noise_std)
} else {
clean
}
})
.collect();
let next_t = self.t + 1;
let clean_next = self.eval_current(next_t);
self.signal_buf.remove(0);
self.signal_buf.push(clean_next);
self.t = next_t;
(features, clean_next)
}
fn n_features(&self) -> usize {
self.window
}
fn task_type(&self) -> TaskType {
TaskType::Regression
}
fn drift_occurred(&self) -> bool {
false }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn periodic_produces_correct_n_features() {
let window = PeriodicStream::DEFAULT_WINDOW;
let period = PeriodicStream::DEFAULT_PERIOD;
let mut gen = PeriodicStream::new(42, window, period);
let (features, _) = gen.next_sample();
assert_eq!(
features.len(),
window,
"features should have window={} dims, got {}",
window,
features.len()
);
}
#[test]
fn periodic_task_type_is_regression() {
let gen = PeriodicStream::new(42, 10, 20);
assert_eq!(gen.task_type(), TaskType::Regression);
}
#[test]
fn periodic_no_drift() {
let mut gen = PeriodicStream::new(42, 10, 20);
for _ in 0..500 {
gen.next_sample();
assert!(!gen.drift_occurred(), "periodic signal should not drift");
}
}
#[test]
fn periodic_produces_finite_values() {
let mut gen = PeriodicStream::new(13, 10, 20);
for i in 0..1000 {
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);
}
assert!(target.is_finite(), "target at sample {} is not finite", i);
}
}
#[test]
fn periodic_deterministic_with_same_seed() {
let mut gen1 = PeriodicStream::new(99, 10, 20);
let mut gen2 = PeriodicStream::new(99, 10, 20);
for _ in 0..500 {
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: {} vs {}",
t1, t2
);
}
}
#[test]
fn periodic_signal_has_expected_variance() {
let mut gen = PeriodicStream::with_config(7, 20, 1.0, 3, 0.0, 5);
let mut targets: Vec<f64> = Vec::new();
for _ in 0..200 {
let (_, t) = gen.next_sample();
targets.push(t);
}
let mean: f64 = targets.iter().sum::<f64>() / targets.len() as f64;
let var: f64 =
targets.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / targets.len() as f64;
assert!(
var > 0.01,
"periodic signal should have substantial variance, got {}",
var
);
}
#[test]
fn periodic_noiseless_target_is_deterministic() {
let mut gen1 = PeriodicStream::with_config(42, 10, 1.0, 2, 0.0, 5);
let mut gen2 = PeriodicStream::with_config(42, 10, 1.0, 2, 0.0, 5);
for _ in 0..100 {
let (_, t1) = gen1.next_sample();
let (_, t2) = gen2.next_sample();
assert!(
(t1 - t2).abs() < 1e-12,
"noiseless targets must match: {} vs {}",
t1,
t2
);
}
}
#[test]
fn periodic_custom_window_dimension() {
let mut gen = PeriodicStream::with_config(1, 30, 2.0, 2, 0.1, 15);
assert_eq!(gen.n_features(), 15);
let (features, _) = gen.next_sample();
assert_eq!(features.len(), 15);
}
}