Skip to main content

ffl/
exponential_average.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::fixed::{Fixed, PromoteAddSub};
6use crate::fixed_format::Value;
7use crate::num_traits;
8
9/// An exponential moving average and variance tracker using fixed-point values.
10///
11/// This structure tracks a value's exponential moving average and its variance
12/// using separate filtering coefficients (`alpha` for positive changes and `beta`
13/// for negative changes, or a single coefficient for both).
14/// Note: If V_FRAC is 0 (integer values), the variance update might lose significant
15/// precision or decay to zero prematurely because internal calculations (like beta * delta^2)
16/// are rounded to the nearest integer. Users should be encouraged to use types with
17/// enough fractional bits for the accumulator if precision in the variance is important.
18///
19/// Additionally, because variance calculations involve squared delta terms (\Delta^2),
20/// there is a high risk of saturation. It is strongly recommended to use larger integer
21/// types (e.g. `i64` or `i128`) as the underlying type if large input ranges or high variance
22/// values are expected.
23pub struct ExponentialAverage<Val, Alpha, Beta> {
24    average: Val,
25    variance: Val,
26    alpha: Alpha,
27    beta: Beta,
28}
29
30impl<V, const V_FRAC: usize, A, const A_FRAC: usize, B, const B_FRAC: usize>
31    ExponentialAverage<Fixed<V, V_FRAC>, Fixed<A, A_FRAC>, Fixed<B, B_FRAC>>
32where
33    V: num_traits::Bounded + num_traits::NumCast + Copy + PromoteAddSub<V> + num_traits::Signed,
34    A: num_traits::Bounded + num_traits::NumCast + Copy + PromoteAddSub<A>,
35    B: num_traits::Bounded + num_traits::NumCast + Copy + PromoteAddSub<B>,
36    V::Output: num_traits::Bounded + num_traits::NumCast + Copy,
37    A::Output: num_traits::Bounded + num_traits::NumCast + Copy,
38    B::Output: num_traits::Bounded + num_traits::NumCast + Copy,
39    Fixed<V, V_FRAC>: Copy
40        + Default
41        + PartialOrd
42        + core::ops::AddAssign
43        + core::ops::Sub<Output = Fixed<V::Output, V_FRAC>>
44        + core::ops::Add<Output = Fixed<V::Output, V_FRAC>>,
45    Fixed<V::Output, V_FRAC>: Copy + PartialOrd,
46    Fixed<A, A_FRAC>: Copy + PartialOrd + core::ops::Sub<Output = Fixed<A::Output, A_FRAC>>,
47    Fixed<B, B_FRAC>: Copy + PartialOrd + core::ops::Sub<Output = Fixed<B::Output, B_FRAC>>,
48    Value<V, 0>: crate::fixed_format::ConvertValue<V, V_FRAC>,
49    Value<V::Output, 0>: crate::fixed_format::ConvertValue<V::Output, V_FRAC>,
50    Value<A, 0>: crate::fixed_format::ConvertValue<A, A_FRAC>,
51    Value<B, 0>: crate::fixed_format::ConvertValue<B, B_FRAC>,
52{
53    /// Creates a new `ExponentialAverage` tracker with separate positive (`alpha`)
54    /// and negative (`beta`) filtering coefficients.
55    ///
56    /// - `alpha` is used when the new sample is greater than or equal to the current average (positive change).
57    /// - `beta` is used when the new sample is less than the current average (negative change).
58    ///
59    /// # Panics
60    ///
61    /// Panics if `alpha` or `beta` is not within the range `[0, 1]`.
62    pub fn new(value: Fixed<V, V_FRAC>, alpha: Fixed<A, A_FRAC>, beta: Fixed<B, B_FRAC>) -> Self {
63        assert!(alpha >= Fixed::from_integer(0));
64        assert!(alpha <= Fixed::from_integer(1));
65        assert!(beta >= Fixed::from_integer(0));
66        assert!(beta <= Fixed::from_integer(1));
67
68        Self { average: value, variance: Fixed::from_integer(0), alpha, beta }
69    }
70
71    /// Adds a new sample to the tracker, updating both the moving average and variance.
72    pub fn add_sample(&mut self, sample: Fixed<V, V_FRAC>) {
73        let delta = sample - self.average;
74        let one_a = Fixed::<A, A_FRAC>::from_integer(1);
75        let one_b = Fixed::<B, B_FRAC>::from_integer(1);
76
77        if delta >= Fixed::from_integer(0) {
78            let term1: Fixed<V, V_FRAC> = self.alpha.mul_to(delta);
79            self.average += term1;
80            let term2: Fixed<V, V_FRAC> = term1.mul_to(delta);
81            let sum = self.variance + term2;
82            self.variance = (one_a - self.alpha).mul_to(sum);
83        } else {
84            let term1: Fixed<V, V_FRAC> = self.beta.mul_to(delta);
85            self.average += term1;
86            let term2: Fixed<V, V_FRAC> = term1.mul_to(delta);
87            let sum = self.variance + term2;
88            self.variance = (one_b - self.beta).mul_to(sum);
89        }
90    }
91
92    /// Returns the current moving average.
93    pub fn value(&self) -> Fixed<V, V_FRAC> {
94        self.average
95    }
96
97    /// Returns the current variance.
98    pub fn variance(&self) -> Fixed<V, V_FRAC> {
99        self.variance
100    }
101}
102
103impl<V, const V_FRAC: usize, A, const A_FRAC: usize>
104    ExponentialAverage<Fixed<V, V_FRAC>, Fixed<A, A_FRAC>, Fixed<A, A_FRAC>>
105where
106    V: num_traits::Bounded + num_traits::NumCast + Copy + PromoteAddSub<V> + num_traits::Signed,
107    A: num_traits::Bounded + num_traits::NumCast + Copy + PromoteAddSub<A>,
108    V::Output: num_traits::Bounded + num_traits::NumCast + Copy,
109    A::Output: num_traits::Bounded + num_traits::NumCast + Copy,
110    Fixed<V, V_FRAC>: Copy
111        + Default
112        + PartialOrd
113        + core::ops::AddAssign
114        + core::ops::Sub<Output = Fixed<V::Output, V_FRAC>>
115        + core::ops::Add<Output = Fixed<V::Output, V_FRAC>>,
116    Fixed<V::Output, V_FRAC>: Copy + PartialOrd,
117    Fixed<A, A_FRAC>: Copy + PartialOrd + core::ops::Sub<Output = Fixed<A::Output, A_FRAC>>,
118    Value<V, 0>: crate::fixed_format::ConvertValue<V, V_FRAC>,
119    Value<V::Output, 0>: crate::fixed_format::ConvertValue<V::Output, V_FRAC>,
120    Value<A, 0>: crate::fixed_format::ConvertValue<A, A_FRAC>,
121{
122    /// Creates a new `ExponentialAverage` tracker using a single coefficient
123    /// for both positive and negative changes.
124    pub fn new_single_rate(value: Fixed<V, V_FRAC>, alpha: Fixed<A, A_FRAC>) -> Self {
125        Self::new(value, alpha, alpha)
126    }
127}