use std::time::{Duration, Instant};
pub const DEFAULT_INITIAL_MS: f64 = 12.5;
#[derive(Debug, Clone, Copy)]
pub struct AdaptiveThreshold {
value_ms: f64,
increase_gain: f64,
decrease_gain: f64,
last_update: Option<Instant>,
}
impl Default for AdaptiveThreshold {
fn default() -> Self {
Self {
value_ms: DEFAULT_INITIAL_MS,
increase_gain: 0.01,
decrease_gain: 0.00018,
last_update: None,
}
}
}
impl AdaptiveThreshold {
pub fn new() -> Self {
Self::default()
}
pub fn value_ms(&self) -> f64 {
self.value_ms
}
pub fn update(&mut self, now: Instant, estimate_ms: f64) -> f64 {
let elapsed = match self.last_update {
Some(last) => now.saturating_duration_since(last),
None => {
self.last_update = Some(now);
return self.value_ms;
}
};
self.last_update = Some(now);
let magnitude = estimate_ms.abs();
if magnitude > self.value_ms + 15.0 {
return self.value_ms;
}
let gain = if magnitude > self.value_ms {
self.increase_gain
} else {
self.decrease_gain
};
let elapsed_ms = elapsed.as_secs_f64() * 1_000.0;
let step = gain * (magnitude - self.value_ms) * elapsed_ms.min(100.0);
self.value_ms = (self.value_ms + step).clamp(6.0, 600.0);
self.value_ms
}
pub fn since_update(&self, now: Instant) -> Option<Duration> {
self.last_update
.map(|last| now.saturating_duration_since(last))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_starts_at_the_drafts_value() {
assert_eq!(DEFAULT_INITIAL_MS, AdaptiveThreshold::new().value_ms());
}
#[test]
fn a_trend_outside_the_threshold_raises_it() {
let epoch = Instant::now();
let mut threshold = AdaptiveThreshold::new();
threshold.update(epoch, 20.0);
for step in 1..=50u64 {
threshold.update(epoch + Duration::from_millis(step * 20), 20.0);
}
assert!(
threshold.value_ms() > DEFAULT_INITIAL_MS,
"threshold should have risen, got {}",
threshold.value_ms()
);
}
#[test]
fn a_trend_inside_the_threshold_lowers_it() {
let epoch = Instant::now();
let mut threshold = AdaptiveThreshold::new();
threshold.update(epoch, 0.0);
for step in 1..=500u64 {
threshold.update(epoch + Duration::from_millis(step * 20), 0.0);
}
assert!(
threshold.value_ms() < DEFAULT_INITIAL_MS,
"threshold should have fallen, got {}",
threshold.value_ms()
);
}
#[test]
fn it_rises_faster_than_it_falls() {
let epoch = Instant::now();
let mut rising = AdaptiveThreshold::new();
rising.update(epoch, 25.0);
let mut falling = AdaptiveThreshold::new();
falling.update(epoch, 0.0);
for step in 1..=25u64 {
let at = epoch + Duration::from_millis(step * 20);
rising.update(at, 25.0);
falling.update(at, 0.0);
}
let rose = rising.value_ms() - DEFAULT_INITIAL_MS;
let fell = DEFAULT_INITIAL_MS - falling.value_ms();
assert!(
rose > fell,
"K_u must exceed K_d: rose by {rose}, fell by {fell}"
);
}
#[test]
fn an_outlier_does_not_move_it() {
let epoch = Instant::now();
let mut threshold = AdaptiveThreshold::new();
threshold.update(epoch, 0.0);
let before = threshold.value_ms();
threshold.update(epoch + Duration::from_millis(20), 5_000.0);
assert_eq!(
before,
threshold.value_ms(),
"an absurd sample must be ignored, not absorbed"
);
}
#[test]
fn it_stays_within_bounds() {
let epoch = Instant::now();
let mut low = AdaptiveThreshold::new();
low.update(epoch, 0.0);
for step in 1..=100_000u64 {
low.update(epoch + Duration::from_millis(step * 20), 0.0);
}
assert!(low.value_ms() >= 6.0, "floor breached: {}", low.value_ms());
let mut high = AdaptiveThreshold::new();
high.update(epoch, 0.0);
for step in 1..=100_000u64 {
let target = high.value_ms() + 1.0;
high.update(epoch + Duration::from_millis(step * 20), target);
}
assert!(
high.value_ms() <= 600.0,
"ceiling breached: {}",
high.value_ms()
);
}
}