Controls.Ewma (Hyper v0.1.0)

Copy Markdown View Source

First-order exponential moving average - a discrete low-pass filter (LPF) with an irregular-sampling-correct gain.

The continuous first-order LPF tau*y' + y = x has the exact discrete solution, for a step-held input over an interval dt:

alpha  = 1 - exp(-dt/tau)
y_n = alpha*x_n + (1-alpha)*y_{n-1}

Deriving alpha from the measured dt (never a hardcoded constant) pins the filter's cutoff at 1/(2*pi*tau) regardless of scheduler jitter or differing per-monitor sample periods. tau (tau_ms) is the time constant: the output reaches ~63 % of a step after one tau and ~95 % after 3tau. The first sample seeds the filter directly, avoiding a warm-up ramp from zero.

A sample is either a plain number or any Unit.Quantity (a Unit.Information, a Unit.Bandwidth, ...). The filter is written as y + alpha*(x - y) using the unit-aware +/- from Unit.Operators, with the alpha* scaling done on the quantity's canonical scalar via Unit.Quantity. A filtered reading therefore keeps its unit, and the filter is not tied to float().

Summary

Types

A filterable sample: a plain number or any unit quantity.

t()

Functions

Build a filter with time constant tau_ms (milliseconds).

Fold one sample, taken dt_ms after the previous one, into the filter.

The current filtered value, or nil before the first sample.

Types

sample()

@type sample() :: number() | Unit.Quantity.t()

A filterable sample: a plain number or any unit quantity.

t()

@type t() :: %Controls.Ewma{tau_ms: pos_integer(), value: sample() | nil}

Functions

new(tau_ms)

@spec new(pos_integer()) :: t()

Build a filter with time constant tau_ms (milliseconds).

update(e, sample, dt_ms)

@spec update(t(), sample(), pos_integer()) :: t()

Fold one sample, taken dt_ms after the previous one, into the filter.

The first sample seeds the average (its dt_ms is ignored).

value(ewma)

@spec value(t()) :: sample() | nil

The current filtered value, or nil before the first sample.