use crate::types::*;
use num_traits::ToPrimitive;
use std::collections::VecDeque;
use std::rc::Rc;
use std::time::Duration;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Weighting {
Count,
Time,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum EwmaSpan {
PerTick(f64),
HalfLife(Duration),
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Window {
Count(usize),
Time(Duration),
Unbounded,
}
pub trait StatisticsOperators<T: Element + ToPrimitive> {
#[must_use]
fn mean(self: &Rc<Self>, window: Window, weighting: Weighting) -> Rc<dyn Stream<f64>>;
#[must_use]
fn variance(self: &Rc<Self>, window: Window, weighting: Weighting) -> Rc<dyn Stream<f64>>;
#[must_use]
fn std(self: &Rc<Self>, window: Window, weighting: Weighting) -> Rc<dyn Stream<f64>>;
#[must_use]
fn sum(self: &Rc<Self>, window: Window) -> Rc<dyn Stream<f64>>;
#[must_use]
fn min(self: &Rc<Self>, window: Window) -> Rc<dyn Stream<f64>>;
#[must_use]
fn max(self: &Rc<Self>, window: Window) -> Rc<dyn Stream<f64>>;
#[must_use]
fn median(self: &Rc<Self>, window: Window, weighting: Weighting) -> Rc<dyn Stream<f64>>;
#[must_use]
fn ewma(self: &Rc<Self>, span: EwmaSpan) -> Rc<dyn Stream<f64>>;
}
impl<T: Element + ToPrimitive + 'static> StatisticsOperators<T> for dyn Stream<T> {
fn mean(self: &Rc<Self>, window: Window, weighting: Weighting) -> Rc<dyn Stream<f64>> {
self.moment(Moment::Mean, window, weighting)
}
fn variance(self: &Rc<Self>, window: Window, weighting: Weighting) -> Rc<dyn Stream<f64>> {
self.moment(Moment::Var, window, weighting)
}
fn std(self: &Rc<Self>, window: Window, weighting: Weighting) -> Rc<dyn Stream<f64>> {
self.moment(Moment::Std, window, weighting)
}
fn sum(self: &Rc<Self>, window: Window) -> Rc<dyn Stream<f64>> {
match window {
Window::Unbounded => {
CumulativeStream::new(self.clone(), CumulativeStat::Sum).into_stream()
}
Window::Count(n) => RollingSumStream::new(self.clone(), n).into_stream(),
Window::Time(_) => {
WindowStream::new(self.clone(), WindowStat::Sum, Weighting::Count, window)
.into_stream()
}
}
}
fn min(self: &Rc<Self>, window: Window) -> Rc<dyn Stream<f64>> {
match window {
Window::Unbounded => {
CumulativeStream::new(self.clone(), CumulativeStat::Min).into_stream()
}
Window::Count(n) => {
RollingExtremeStream::new(self.clone(), Extreme::Min, n).into_stream()
}
Window::Time(_) => {
WindowStream::new(self.clone(), WindowStat::Min, Weighting::Count, window)
.into_stream()
}
}
}
fn max(self: &Rc<Self>, window: Window) -> Rc<dyn Stream<f64>> {
match window {
Window::Unbounded => {
CumulativeStream::new(self.clone(), CumulativeStat::Max).into_stream()
}
Window::Count(n) => {
RollingExtremeStream::new(self.clone(), Extreme::Max, n).into_stream()
}
Window::Time(_) => {
WindowStream::new(self.clone(), WindowStat::Max, Weighting::Count, window)
.into_stream()
}
}
}
fn median(self: &Rc<Self>, window: Window, weighting: Weighting) -> Rc<dyn Stream<f64>> {
WindowStream::new(self.clone(), WindowStat::Median, weighting, window).into_stream()
}
fn ewma(self: &Rc<Self>, span: EwmaSpan) -> Rc<dyn Stream<f64>> {
let decay = match span {
EwmaSpan::PerTick(alpha) => {
debug_assert!(
(0.0..=1.0).contains(&alpha),
"ewma PerTick smoothing factor must be in [0, 1], got {alpha}"
);
EwmaDecay::PerTick(alpha)
}
EwmaSpan::HalfLife(half_life) => EwmaDecay::HalfLife(half_life.as_nanos() as f64),
};
EwmaStream::new(self.clone(), decay).into_stream()
}
}
impl<T: Element + ToPrimitive + 'static> dyn Stream<T> {
fn moment(
self: &Rc<Self>,
moment: Moment,
window: Window,
weighting: Weighting,
) -> Rc<dyn Stream<f64>> {
match window {
Window::Unbounded => MomentStream::new(self.clone(), moment, weighting).into_stream(),
_ => RollingMomentStream::new(self.clone(), moment, weighting, window).into_stream(),
}
}
}
#[derive(Default)]
struct WeightedMoments {
w_sum: f64,
count: u64,
mean: f64,
m2: f64,
}
impl WeightedMoments {
fn push(&mut self, x: f64, weight: f64) {
if weight <= 0.0 {
return;
}
self.w_sum += weight;
self.count += 1;
let mean_old = self.mean;
self.mean += (weight / self.w_sum) * (x - mean_old);
self.m2 += weight * (x - mean_old) * (x - self.mean);
}
fn remove(&mut self, x: f64, weight: f64) {
if weight <= 0.0 {
return;
}
let w_new = self.w_sum - weight;
if self.count <= 1 || w_new <= 0.0 {
*self = Self::default();
return;
}
self.count -= 1;
let mean_old = (self.w_sum * self.mean - weight * x) / w_new;
self.m2 -= weight * (x - mean_old) * (x - self.mean);
if self.m2 < 0.0 {
self.m2 = 0.0;
}
self.mean = mean_old;
self.w_sum = w_new;
}
fn is_empty(&self) -> bool {
self.w_sum <= 0.0
}
fn mean(&self) -> f64 {
self.mean
}
fn variance(&self, weighting: Weighting) -> f64 {
match weighting {
Weighting::Count => {
if self.count < 2 {
return 0.0;
}
self.m2 / (self.count as f64 - 1.0)
}
Weighting::Time => {
if self.w_sum <= 0.0 {
return 0.0;
}
self.m2 / self.w_sum
}
}
}
}
#[derive(Clone, Copy)]
pub(crate) enum Moment {
Mean,
Var,
Std,
}
pub(crate) struct MomentStream<T: Element> {
upstream: Rc<dyn Stream<T>>,
moment: Moment,
weighting: Weighting,
moments: WeightedMoments,
last_time: Option<NanoTime>,
prev_value: f64,
value: f64,
}
#[node(active = [upstream], output = value: f64)]
impl<T: Element + ToPrimitive> MutableNode for MomentStream<T> {
fn cycle(&mut self, state: &mut GraphState) -> anyhow::Result<bool> {
let sample = self.upstream.peek_value().to_f64().unwrap_or(f64::NAN);
match self.weighting {
Weighting::Count => self.moments.push(sample, 1.0),
Weighting::Time => {
let now = state.time();
if let Some(prev_t) = self.last_time {
let dt = f64::from(now - prev_t);
self.moments.push(self.prev_value, dt);
}
self.prev_value = sample;
self.last_time = Some(now);
}
}
self.value = self.output(sample);
Ok(true)
}
}
impl<T: Element> MomentStream<T> {
pub fn new(upstream: Rc<dyn Stream<T>>, moment: Moment, weighting: Weighting) -> Self {
Self {
upstream,
moment,
weighting,
moments: WeightedMoments::default(),
last_time: None,
prev_value: f64::NAN,
value: f64::NAN,
}
}
fn output(&self, current: f64) -> f64 {
match self.moment {
Moment::Mean if self.moments.is_empty() => current,
Moment::Mean => self.moments.mean(),
Moment::Var => self.moments.variance(self.weighting),
Moment::Std => self.moments.variance(self.weighting).max(0.0).sqrt(),
}
}
}
pub(crate) struct RollingMomentStream<T: Element> {
upstream: Rc<dyn Stream<T>>,
moment: Moment,
weighting: Weighting,
window: Window,
buffer: VecDeque<(f64, NanoTime)>,
moments: WeightedMoments,
value: f64,
}
#[node(active = [upstream], output = value: f64)]
impl<T: Element + ToPrimitive> MutableNode for RollingMomentStream<T> {
fn cycle(&mut self, state: &mut GraphState) -> anyhow::Result<bool> {
let now = state.time();
let sample = self.upstream.peek_value().to_f64().unwrap_or(f64::NAN);
match self.weighting {
Weighting::Count => self.moments.push(sample, 1.0),
Weighting::Time => {
if let Some(&(prev_v, prev_t)) = self.buffer.back() {
self.moments.push(prev_v, f64::from(now - prev_t));
}
}
}
self.buffer.push_back((sample, now));
while self.should_evict(now) {
let (old_v, old_t) = self
.buffer
.pop_front()
.expect("invariant: should_evict implies a front sample");
match self.weighting {
Weighting::Count => self.moments.remove(old_v, 1.0),
Weighting::Time => {
if let Some(&(_, next_t)) = self.buffer.front() {
self.moments.remove(old_v, f64::from(next_t - old_t));
}
}
}
}
self.value = self.output(sample);
Ok(true)
}
}
impl<T: Element> RollingMomentStream<T> {
pub fn new(
upstream: Rc<dyn Stream<T>>,
moment: Moment,
weighting: Weighting,
window: Window,
) -> Self {
let window = match window {
Window::Count(n) => Window::Count(n.max(1)),
Window::Time(_) | Window::Unbounded => window,
};
Self {
upstream,
moment,
weighting,
window,
buffer: VecDeque::new(),
moments: WeightedMoments::default(),
value: f64::NAN,
}
}
fn should_evict(&self, now: NanoTime) -> bool {
match self.window {
Window::Count(n) => self.buffer.len() > n,
Window::Time(duration) => {
let duration = duration.as_nanos() as u64;
self.buffer
.front()
.is_some_and(|&(_, t)| u64::from(now) - u64::from(t) > duration)
}
Window::Unbounded => false,
}
}
fn output(&self, current: f64) -> f64 {
match self.moment {
Moment::Mean if self.moments.is_empty() => current,
Moment::Mean => self.moments.mean(),
Moment::Var => self.moments.variance(self.weighting),
Moment::Std => self.moments.variance(self.weighting).max(0.0).sqrt(),
}
}
}
#[derive(Clone, Copy)]
pub(crate) enum EwmaDecay {
PerTick(f64),
HalfLife(f64),
}
pub(crate) struct EwmaStream<T: Element> {
upstream: Rc<dyn Stream<T>>,
decay: EwmaDecay,
value: f64,
initialised: bool,
last_time: Option<NanoTime>,
}
#[node(active = [upstream], output = value: f64)]
impl<T: Element + ToPrimitive> MutableNode for EwmaStream<T> {
fn cycle(&mut self, state: &mut GraphState) -> anyhow::Result<bool> {
let sample = self.upstream.peek_value().to_f64().unwrap_or(f64::NAN);
if !self.initialised {
self.value = sample;
self.initialised = true;
self.last_time = Some(state.time());
return Ok(true);
}
let alpha = match self.decay {
EwmaDecay::PerTick(alpha) => alpha,
EwmaDecay::HalfLife(half_life) => {
let now = state.time();
let prev = self
.last_time
.expect("invariant: last_time set once initialised");
self.last_time = Some(now);
if half_life <= 0.0 {
1.0
} else {
let dt = f64::from(now - prev);
1.0 - (-(dt / half_life) * std::f64::consts::LN_2).exp()
}
}
};
self.value += alpha * (sample - self.value);
Ok(true)
}
}
impl<T: Element> EwmaStream<T> {
pub fn new(upstream: Rc<dyn Stream<T>>, decay: EwmaDecay) -> Self {
Self {
upstream,
decay,
value: f64::NAN,
initialised: false,
last_time: None,
}
}
}
#[derive(Clone, Copy)]
pub(crate) enum CumulativeStat {
Sum,
Min,
Max,
}
pub(crate) struct CumulativeStream<T: Element> {
upstream: Rc<dyn Stream<T>>,
stat: CumulativeStat,
acc: f64,
seeded: bool,
value: f64,
}
#[node(active = [upstream], output = value: f64)]
impl<T: Element + ToPrimitive> MutableNode for CumulativeStream<T> {
fn cycle(&mut self, _state: &mut GraphState) -> anyhow::Result<bool> {
let sample = self.upstream.peek_value().to_f64().unwrap_or(f64::NAN);
self.acc = if !self.seeded {
sample
} else {
match self.stat {
CumulativeStat::Sum => self.acc + sample,
CumulativeStat::Min => self.acc.min(sample),
CumulativeStat::Max => self.acc.max(sample),
}
};
self.seeded = true;
self.value = self.acc;
Ok(true)
}
}
impl<T: Element> CumulativeStream<T> {
pub fn new(upstream: Rc<dyn Stream<T>>, stat: CumulativeStat) -> Self {
Self {
upstream,
stat,
acc: 0.0,
seeded: false,
value: f64::NAN,
}
}
}
pub(crate) struct RollingSumStream<T: Element> {
upstream: Rc<dyn Stream<T>>,
window: usize,
buffer: VecDeque<f64>,
sum: f64,
value: f64,
}
#[node(active = [upstream], output = value: f64)]
impl<T: Element + ToPrimitive> MutableNode for RollingSumStream<T> {
fn cycle(&mut self, _state: &mut GraphState) -> anyhow::Result<bool> {
let sample = self.upstream.peek_value().to_f64().unwrap_or(f64::NAN);
self.buffer.push_back(sample);
self.sum += sample;
if self.buffer.len() > self.window {
let oldest = self
.buffer
.pop_front()
.expect("invariant: len > window >= 1 implies non-empty");
self.sum -= oldest;
}
self.value = self.sum;
Ok(true)
}
}
impl<T: Element> RollingSumStream<T> {
pub fn new(upstream: Rc<dyn Stream<T>>, window: usize) -> Self {
let window = window.max(1);
Self {
upstream,
window,
buffer: VecDeque::with_capacity(window),
sum: 0.0,
value: f64::NAN,
}
}
}
#[derive(Clone, Copy)]
pub(crate) enum Extreme {
Min,
Max,
}
pub(crate) struct RollingExtremeStream<T: Element> {
upstream: Rc<dyn Stream<T>>,
extreme: Extreme,
window: u64,
deque: VecDeque<(u64, f64)>,
index: u64,
value: f64,
}
#[node(active = [upstream], output = value: f64)]
impl<T: Element + ToPrimitive> MutableNode for RollingExtremeStream<T> {
fn cycle(&mut self, _state: &mut GraphState) -> anyhow::Result<bool> {
let sample = self.upstream.peek_value().to_f64().unwrap_or(f64::NAN);
let i = self.index;
self.index += 1;
while let Some(&(_, back)) = self.deque.back() {
let dominated = match self.extreme {
Extreme::Min => back >= sample,
Extreme::Max => back <= sample,
};
if dominated {
self.deque.pop_back();
} else {
break;
}
}
self.deque.push_back((i, sample));
while let Some(&(idx, _)) = self.deque.front() {
if i - idx >= self.window {
self.deque.pop_front();
} else {
break;
}
}
self.value = self
.deque
.front()
.expect("invariant: deque holds the current sample")
.1;
Ok(true)
}
}
impl<T: Element> RollingExtremeStream<T> {
pub fn new(upstream: Rc<dyn Stream<T>>, extreme: Extreme, window: usize) -> Self {
Self {
upstream,
extreme,
window: window.max(1) as u64,
deque: VecDeque::new(),
index: 0,
value: f64::NAN,
}
}
}
#[derive(Clone, Copy)]
pub(crate) enum WindowStat {
Sum,
Min,
Max,
Median,
}
pub(crate) struct WindowStream<T: Element> {
upstream: Rc<dyn Stream<T>>,
stat: WindowStat,
weighting: Weighting,
window: Window,
buffer: VecDeque<(f64, NanoTime)>,
value: f64,
}
#[node(active = [upstream], output = value: f64)]
impl<T: Element + ToPrimitive> MutableNode for WindowStream<T> {
fn cycle(&mut self, state: &mut GraphState) -> anyhow::Result<bool> {
let now = state.time();
let sample = self.upstream.peek_value().to_f64().unwrap_or(f64::NAN);
self.buffer.push_back((sample, now));
match self.window {
Window::Count(n) => {
while self.buffer.len() > n {
self.buffer.pop_front();
}
}
Window::Time(duration) => {
let duration = duration.as_nanos() as u64;
while let Some(&(_, t)) = self.buffer.front() {
if u64::from(now) - u64::from(t) > duration {
self.buffer.pop_front();
} else {
break;
}
}
}
Window::Unbounded => {}
}
self.value = self.compute(now);
Ok(true)
}
}
impl<T: Element> WindowStream<T> {
fn new(
upstream: Rc<dyn Stream<T>>,
stat: WindowStat,
weighting: Weighting,
window: Window,
) -> Self {
let window = match window {
Window::Count(n) => Window::Count(n.max(1)),
Window::Time(_) | Window::Unbounded => window,
};
Self {
upstream,
stat,
weighting,
window,
buffer: VecDeque::new(),
value: f64::NAN,
}
}
fn for_each_weight(&self, now: NanoTime, mut f: impl FnMut(f64, f64)) {
match self.weighting {
Weighting::Count => {
for &(v, _) in &self.buffer {
f(v, 1.0);
}
}
Weighting::Time => {
let n = self.buffer.len();
for i in 0..n {
let (v, t) = self.buffer[i];
let next_t = if i + 1 < n { self.buffer[i + 1].1 } else { now };
f(v, f64::from(next_t - t));
}
}
}
}
fn weighted_median(&self, now: NanoTime) -> f64 {
let mut pairs: Vec<(f64, f64)> = Vec::with_capacity(self.buffer.len());
self.for_each_weight(now, |v, w| {
if w > 0.0 {
pairs.push((v, w));
}
});
if pairs.is_empty() {
return self.buffer.back().expect("invariant: buffer non-empty").0;
}
pairs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let half = pairs.iter().map(|&(_, w)| w).sum::<f64>() / 2.0;
let mut cumulative = 0.0;
for (i, &(v, w)) in pairs.iter().enumerate() {
cumulative += w;
if cumulative > half {
return v;
}
if cumulative == half {
return match pairs.get(i + 1) {
Some(&(next, _)) => (v + next) / 2.0,
None => v,
};
}
}
pairs.last().expect("invariant: pairs non-empty").0
}
fn compute(&self, now: NanoTime) -> f64 {
if self.buffer.is_empty() {
return f64::NAN;
}
match self.stat {
WindowStat::Sum => self.buffer.iter().map(|&(v, _)| v).sum(),
WindowStat::Min => self
.buffer
.iter()
.map(|&(v, _)| v)
.fold(f64::INFINITY, f64::min),
WindowStat::Max => self
.buffer
.iter()
.map(|&(v, _)| v)
.fold(f64::NEG_INFINITY, f64::max),
WindowStat::Median => self.weighted_median(now),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::*;
use crate::nodes::*;
fn counter() -> Rc<dyn Stream<u64>> {
ticker(Duration::from_nanos(100)).count()
}
#[test]
fn ewma_seeds_on_first_sample() {
let ewma = ticker(Duration::from_nanos(100))
.count()
.map(|_: u64| 5u64)
.ewma(EwmaSpan::PerTick(0.3));
ewma.run(RunMode::HistoricalFrom(NanoTime::ZERO), RunFor::Cycles(4))
.unwrap();
assert!((ewma.peek_value() - 5.0).abs() < 1e-10);
}
#[test]
fn ewma_of_sequence() {
let ewma = counter().ewma(EwmaSpan::PerTick(0.5));
ewma.run(RunMode::HistoricalFrom(NanoTime::ZERO), RunFor::Cycles(4))
.unwrap();
assert!((ewma.peek_value() - 3.125).abs() < 1e-10);
}
#[test]
fn ewma_does_not_reset_at_zero() {
let ewma = counter()
.map(|n: u64| if n <= 2 { 0.0 } else { 5.0 })
.ewma(EwmaSpan::PerTick(0.5));
ewma.run(RunMode::HistoricalFrom(NanoTime::ZERO), RunFor::Cycles(3))
.unwrap();
assert!((ewma.peek_value() - 2.5).abs() < 1e-10);
}
#[test]
fn ewma_decay_matches_per_tick_when_dt_equals_half_life() {
let ewma = counter().ewma(EwmaSpan::HalfLife(Duration::from_nanos(100)));
ewma.run(RunMode::HistoricalFrom(NanoTime::ZERO), RunFor::Cycles(4))
.unwrap();
assert!((ewma.peek_value() - 3.125).abs() < 1e-10);
}
#[test]
fn ewma_decay_constant_stream_is_constant() {
let ewma = ticker(Duration::from_nanos(100))
.count()
.map(|_: u64| 7u64)
.ewma(EwmaSpan::HalfLife(Duration::from_nanos(250)));
ewma.run(RunMode::HistoricalFrom(NanoTime::ZERO), RunFor::Cycles(6))
.unwrap();
assert!((ewma.peek_value() - 7.0).abs() < 1e-10);
}
#[test]
fn mean_count_is_arithmetic_mean() {
let avg = counter().mean(Window::Unbounded, Weighting::Count);
avg.run(RunMode::HistoricalFrom(NanoTime::ZERO), RunFor::Cycles(5))
.unwrap();
assert!((avg.peek_value() - 3.0).abs() < 1e-10);
}
#[test]
fn mean_time_weighted_lags_by_one_interval() {
let avg = counter().mean(Window::Unbounded, Weighting::Time);
avg.run(RunMode::HistoricalFrom(NanoTime::ZERO), RunFor::Cycles(5))
.unwrap();
assert!((avg.peek_value() - 2.5).abs() < 1e-10);
}
#[test]
fn variance_count_is_sample_variance() {
let var = counter().variance(Window::Unbounded, Weighting::Count);
let std = counter().std(Window::Unbounded, Weighting::Count);
var.run(RunMode::HistoricalFrom(NanoTime::ZERO), RunFor::Cycles(5))
.unwrap();
std.run(RunMode::HistoricalFrom(NanoTime::ZERO), RunFor::Cycles(5))
.unwrap();
assert!((var.peek_value() - 2.5).abs() < 1e-10);
assert!((std.peek_value() - 2.5_f64.sqrt()).abs() < 1e-10);
}
#[test]
fn variance_time_weighted_is_population_over_weight() {
let var = counter().variance(Window::Unbounded, Weighting::Time);
var.run(RunMode::HistoricalFrom(NanoTime::ZERO), RunFor::Cycles(5))
.unwrap();
assert!((var.peek_value() - 1.25).abs() < 1e-10);
}
#[test]
fn rolling_sum_over_window() {
let s = counter().sum(Window::Count(3));
s.run(RunMode::HistoricalFrom(NanoTime::ZERO), RunFor::Cycles(5))
.unwrap();
assert!((s.peek_value() - 12.0).abs() < 1e-10);
}
#[test]
fn rolling_mean_over_window() {
let s = counter().mean(Window::Count(3), Weighting::Count);
s.run(RunMode::HistoricalFrom(NanoTime::ZERO), RunFor::Cycles(5))
.unwrap();
assert!((s.peek_value() - 4.0).abs() < 1e-10);
}
#[test]
fn rolling_min_max_over_window() {
let mn = counter().min(Window::Count(2));
let mx = counter().max(Window::Count(2));
mn.run(RunMode::HistoricalFrom(NanoTime::ZERO), RunFor::Cycles(5))
.unwrap();
mx.run(RunMode::HistoricalFrom(NanoTime::ZERO), RunFor::Cycles(5))
.unwrap();
assert!((mn.peek_value() - 4.0).abs() < 1e-10);
assert!((mx.peek_value() - 5.0).abs() < 1e-10);
}
#[test]
fn rolling_min_max_match_brute_force_every_tick() {
const N: u64 = 60;
const W: usize = 5;
let seq = |n: u64| ((n * 7) % 13) as f64;
let mn = ticker(Duration::from_nanos(100))
.count()
.map(seq)
.min(Window::Count(W))
.collect();
let mx = ticker(Duration::from_nanos(100))
.count()
.map(seq)
.max(Window::Count(W))
.collect();
mn.run(
RunMode::HistoricalFrom(NanoTime::ZERO),
RunFor::Cycles(N as u32),
)
.unwrap();
mx.run(
RunMode::HistoricalFrom(NanoTime::ZERO),
RunFor::Cycles(N as u32),
)
.unwrap();
let got_min = mn.peek_value();
let got_max = mx.peek_value();
for k in 0..got_min.len() {
let n = (k + 1) as u64;
let start = if n > W as u64 { n - W as u64 + 1 } else { 1 };
let window: Vec<f64> = (start..=n).map(seq).collect();
let emin = window.iter().copied().fold(f64::INFINITY, f64::min);
let emax = window.iter().copied().fold(f64::NEG_INFINITY, f64::max);
assert_eq!(got_min[k].value, emin, "min mismatch at tick {k}");
assert_eq!(got_max[k].value, emax, "max mismatch at tick {k}");
}
}
#[test]
fn rolling_var_std_over_window() {
let var = counter().variance(Window::Count(3), Weighting::Count);
let std = counter().std(Window::Count(3), Weighting::Count);
var.run(RunMode::HistoricalFrom(NanoTime::ZERO), RunFor::Cycles(5))
.unwrap();
std.run(RunMode::HistoricalFrom(NanoTime::ZERO), RunFor::Cycles(5))
.unwrap();
assert!((var.peek_value() - 1.0).abs() < 1e-10);
assert!((std.peek_value() - 1.0).abs() < 1e-10);
}
#[test]
fn rolling_std_of_constant_window_is_zero_not_nan() {
let std = ticker(Duration::from_nanos(100))
.count()
.map(|_: u64| 7u64)
.std(Window::Count(3), Weighting::Count);
std.run(RunMode::HistoricalFrom(NanoTime::ZERO), RunFor::Cycles(6))
.unwrap();
let v = std.peek_value();
assert!(!v.is_nan(), "rolling_std must not be NaN");
assert!(
v.abs() < 1e-10,
"constant window std should be 0.0, got {v}"
);
}
#[test]
fn rolling_var_is_zero_with_single_sample() {
let var = counter().variance(Window::Count(3), Weighting::Count);
var.run(RunMode::HistoricalFrom(NanoTime::ZERO), RunFor::Cycles(1))
.unwrap();
assert_eq!(var.peek_value(), 0.0);
}
#[test]
fn rolling_median_over_window() {
let med = counter().median(Window::Count(3), Weighting::Count);
med.run(RunMode::HistoricalFrom(NanoTime::ZERO), RunFor::Cycles(5))
.unwrap();
assert!((med.peek_value() - 4.0).abs() < 1e-10);
}
const WIN: Duration = Duration::from_nanos(250);
#[test]
fn rolling_sum_over_time_window() {
let s = counter().sum(Window::Time(WIN));
s.run(RunMode::HistoricalFrom(NanoTime::ZERO), RunFor::Cycles(5))
.unwrap();
assert!((s.peek_value() - 12.0).abs() < 1e-10); }
#[test]
fn rolling_mean_over_time_window_count_and_time() {
let mean_c = counter().mean(Window::Time(WIN), Weighting::Count);
mean_c
.run(RunMode::HistoricalFrom(NanoTime::ZERO), RunFor::Cycles(5))
.unwrap();
assert!((mean_c.peek_value() - 4.0).abs() < 1e-10);
let mean_t = counter().mean(Window::Time(WIN), Weighting::Time);
mean_t
.run(RunMode::HistoricalFrom(NanoTime::ZERO), RunFor::Cycles(5))
.unwrap();
assert!((mean_t.peek_value() - 3.5).abs() < 1e-10);
}
#[test]
fn rolling_min_max_over_time_window() {
let mn = counter().min(Window::Time(WIN));
let mx = counter().max(Window::Time(WIN));
mn.run(RunMode::HistoricalFrom(NanoTime::ZERO), RunFor::Cycles(5))
.unwrap();
mx.run(RunMode::HistoricalFrom(NanoTime::ZERO), RunFor::Cycles(5))
.unwrap();
assert!((mn.peek_value() - 3.0).abs() < 1e-10);
assert!((mx.peek_value() - 5.0).abs() < 1e-10);
}
#[test]
fn rolling_var_over_time_window_count() {
let var = counter().variance(Window::Time(WIN), Weighting::Count);
var.run(RunMode::HistoricalFrom(NanoTime::ZERO), RunFor::Cycles(5))
.unwrap();
assert!((var.peek_value() - 1.0).abs() < 1e-10);
}
#[test]
fn rolling_var_std_over_time_window_time_weighted() {
let var = counter().variance(Window::Time(WIN), Weighting::Time);
let std = counter().std(Window::Time(WIN), Weighting::Time);
var.run(RunMode::HistoricalFrom(NanoTime::ZERO), RunFor::Cycles(5))
.unwrap();
std.run(RunMode::HistoricalFrom(NanoTime::ZERO), RunFor::Cycles(5))
.unwrap();
assert!((var.peek_value() - 0.25).abs() < 1e-10);
assert!((std.peek_value() - 0.5).abs() < 1e-10);
}
#[test]
fn rolling_moments_incremental_match_direct_recompute() {
const N: u64 = 200;
const W: usize = 10;
let seq = |n: u64| ((n % 7) as f64) * 1.5 - 3.0;
let mean = ticker(Duration::from_nanos(100))
.count()
.map(seq)
.mean(Window::Count(W), Weighting::Count);
let var = ticker(Duration::from_nanos(100))
.count()
.map(seq)
.variance(Window::Count(W), Weighting::Count);
mean.run(
RunMode::HistoricalFrom(NanoTime::ZERO),
RunFor::Cycles(N as u32),
)
.unwrap();
var.run(
RunMode::HistoricalFrom(NanoTime::ZERO),
RunFor::Cycles(N as u32),
)
.unwrap();
let window: Vec<f64> = ((N - W as u64 + 1)..=N).map(seq).collect();
let expected_mean = window.iter().sum::<f64>() / W as f64;
let expected_var = window
.iter()
.map(|v| (v - expected_mean).powi(2))
.sum::<f64>()
/ (W as f64 - 1.0);
assert!(
(mean.peek_value() - expected_mean).abs() < 1e-9,
"mean {} vs {expected_mean}",
mean.peek_value()
);
assert!(
(var.peek_value() - expected_var).abs() < 1e-9,
"var {} vs {expected_var}",
var.peek_value()
);
}
#[test]
fn rolling_median_over_time_window() {
let med = counter().median(Window::Time(WIN), Weighting::Count);
med.run(RunMode::HistoricalFrom(NanoTime::ZERO), RunFor::Cycles(5))
.unwrap();
assert!((med.peek_value() - 4.0).abs() < 1e-10); }
#[test]
fn rolling_median_time_weighted() {
let med = counter().median(Window::Count(4), Weighting::Time);
med.run(RunMode::HistoricalFrom(NanoTime::ZERO), RunFor::Cycles(5))
.unwrap();
assert!((med.peek_value() - 3.0).abs() < 1e-10);
let med_c = counter().median(Window::Count(4), Weighting::Count);
med_c
.run(RunMode::HistoricalFrom(NanoTime::ZERO), RunFor::Cycles(5))
.unwrap();
assert!((med_c.peek_value() - 3.5).abs() < 1e-10);
}
#[test]
fn cumulative_sum_min_max() {
let s = counter().sum(Window::Unbounded);
let mn = counter()
.map(|n: u64| 6 - n) .min(Window::Unbounded);
let mx = counter().max(Window::Unbounded);
s.run(RunMode::HistoricalFrom(NanoTime::ZERO), RunFor::Cycles(5))
.unwrap();
mn.run(RunMode::HistoricalFrom(NanoTime::ZERO), RunFor::Cycles(5))
.unwrap();
mx.run(RunMode::HistoricalFrom(NanoTime::ZERO), RunFor::Cycles(5))
.unwrap();
assert!((s.peek_value() - 15.0).abs() < 1e-10);
assert!((mn.peek_value() - 1.0).abs() < 1e-10);
assert!((mx.peek_value() - 5.0).abs() < 1e-10);
}
#[test]
fn cumulative_median_over_all_samples() {
let med = counter().median(Window::Unbounded, Weighting::Count);
med.run(RunMode::HistoricalFrom(NanoTime::ZERO), RunFor::Cycles(5))
.unwrap();
assert!((med.peek_value() - 3.0).abs() < 1e-10);
}
}