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
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
@type sample() :: number() | Unit.Quantity.t()
A filterable sample: a plain number or any unit quantity.
@type t() :: %Controls.Ewma{tau_ms: pos_integer(), value: sample() | nil}
Functions
@spec new(pos_integer()) :: t()
Build a filter with time constant tau_ms (milliseconds).
@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).
The current filtered value, or nil before the first sample.