use super::ValueGenerator;
pub struct SpikeGenerator {
baseline: f64,
magnitude: f64,
duration_ticks: f64,
interval_ticks: f64,
}
impl SpikeGenerator {
pub fn new(
baseline: f64,
magnitude: f64,
duration_secs: f64,
interval_secs: f64,
rate: f64,
) -> Self {
let duration_ticks = duration_secs * rate;
let interval_ticks = interval_secs * rate;
Self {
baseline,
magnitude,
duration_ticks,
interval_ticks,
}
}
}
impl ValueGenerator for SpikeGenerator {
fn value(&self, tick: u64) -> f64 {
let position = (tick as f64) % self.interval_ticks;
if position < self.duration_ticks {
self.baseline + self.magnitude
} else {
self.baseline
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const EPSILON: f64 = 1e-10;
fn spike_rate1(
baseline: f64,
magnitude: f64,
duration_secs: f64,
interval_secs: f64,
) -> SpikeGenerator {
SpikeGenerator::new(baseline, magnitude, duration_secs, interval_secs, 1.0)
}
#[test]
fn tick_during_baseline_period_returns_baseline() {
let gen = spike_rate1(50.0, 200.0, 10.0, 60.0);
assert!((gen.value(15) - 50.0).abs() < EPSILON);
assert!((gen.value(30) - 50.0).abs() < EPSILON);
assert!((gen.value(59) - 50.0).abs() < EPSILON);
}
#[test]
fn tick_during_spike_period_returns_baseline_plus_magnitude() {
let gen = spike_rate1(50.0, 200.0, 10.0, 60.0);
let expected = 50.0 + 200.0;
assert!((gen.value(0) - expected).abs() < EPSILON);
assert!((gen.value(5) - expected).abs() < EPSILON);
assert!((gen.value(9) - expected).abs() < EPSILON);
}
#[test]
fn exact_boundary_at_spike_start() {
let gen = spike_rate1(10.0, 90.0, 5.0, 20.0);
assert!(
(gen.value(0) - 100.0).abs() < EPSILON,
"tick 0 should be in spike"
);
}
#[test]
fn exact_boundary_at_spike_end() {
let gen = spike_rate1(10.0, 90.0, 5.0, 20.0);
assert!(
(gen.value(5) - 10.0).abs() < EPSILON,
"tick at duration boundary should be baseline"
);
}
#[test]
fn exact_boundary_at_second_interval() {
let gen = spike_rate1(10.0, 90.0, 5.0, 20.0);
assert!(
(gen.value(20) - 100.0).abs() < EPSILON,
"tick at second interval should be in spike"
);
assert!(
(gen.value(25) - 10.0).abs() < EPSILON,
"tick after second spike should be baseline"
);
}
#[test]
fn duration_ge_interval_always_spikes() {
let gen = spike_rate1(10.0, 90.0, 60.0, 60.0);
for tick in 0..200 {
assert!(
(gen.value(tick) - 100.0).abs() < EPSILON,
"duration >= interval: tick {tick} should always be spike"
);
}
}
#[test]
fn duration_greater_than_interval_always_spikes() {
let gen = spike_rate1(10.0, 90.0, 100.0, 60.0);
for tick in 0..200 {
assert!(
(gen.value(tick) - 100.0).abs() < EPSILON,
"duration > interval: tick {tick} should always be spike"
);
}
}
#[test]
fn duration_zero_always_baseline() {
let gen = spike_rate1(50.0, 200.0, 0.0, 60.0);
for tick in 0..200 {
assert!(
(gen.value(tick) - 50.0).abs() < EPSILON,
"duration=0: tick {tick} should always be baseline"
);
}
}
#[test]
fn negative_magnitude_dips_below_baseline() {
let gen = spike_rate1(100.0, -50.0, 5.0, 20.0);
assert!((gen.value(0) - 50.0).abs() < EPSILON);
assert!((gen.value(10) - 100.0).abs() < EPSILON);
}
#[test]
fn determinism_same_tick_same_value() {
let gen = spike_rate1(50.0, 200.0, 10.0, 60.0);
for tick in 0..100 {
assert_eq!(
gen.value(tick),
gen.value(tick),
"tick {tick} must be deterministic"
);
}
}
#[test]
fn large_tick_values_do_not_panic() {
let gen = spike_rate1(50.0, 200.0, 10.0, 60.0);
let _ = gen.value(u64::MAX);
let _ = gen.value(u64::MAX - 1);
let _ = gen.value(1_000_000_000);
}
#[test]
fn different_rate_values_produce_different_tick_boundaries() {
let gen_r1 = SpikeGenerator::new(50.0, 200.0, 10.0, 60.0, 1.0);
let gen_r2 = SpikeGenerator::new(50.0, 200.0, 10.0, 60.0, 2.0);
assert!(
(gen_r1.value(10) - 50.0).abs() < EPSILON,
"rate=1, tick=10 should be baseline"
);
assert!(
(gen_r2.value(10) - 250.0).abs() < EPSILON,
"rate=2, tick=10 should be spike"
);
}
#[test]
fn rate_adjusts_period_ticks() {
let gen = SpikeGenerator::new(50.0, 200.0, 10.0, 60.0, 10.0);
assert!(
(gen.value(99) - 250.0).abs() < EPSILON,
"rate=10, tick=99 should be in spike (99 < 100)"
);
assert!(
(gen.value(100) - 50.0).abs() < EPSILON,
"rate=10, tick=100 should be baseline (100 >= 100)"
);
}
}