wlancfg_lib/util/
pseudo_energy.rs

1// Copyright 2022 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::client::types as client_types;
6use anyhow::{format_err, Error};
7use log::error;
8
9/// Update a weighted average with a new measurement
10fn calculate_ewma_update(current: f64, next: f64, weighting_factor: f64) -> f64 {
11    let weight = 2.0 / (1.0 + weighting_factor);
12    weight * next + (1.0 - weight) * current
13}
14
15/// Struct for maintaining a dB or dBm exponentially weighted moving average. Differs from
16/// SignalStrengthAverage, which is not exponentially weighted.
17#[derive(Clone, Copy, Debug, PartialEq)]
18pub struct EwmaPseudoDecibel {
19    current: f64,
20    weighting_factor: f64,
21}
22
23impl EwmaPseudoDecibel {
24    pub fn new(n: usize, initial_signal: impl Into<f64>) -> Self {
25        Self { current: initial_signal.into(), weighting_factor: n as f64 }
26    }
27
28    /// Returns the current EWMA value
29    pub fn get(&self) -> f64 {
30        self.current
31    }
32
33    pub fn update_average(&mut self, next: impl Into<f64>) {
34        self.current = calculate_ewma_update(self.current, next.into(), self.weighting_factor);
35    }
36}
37
38#[derive(Clone, Copy, Debug, PartialEq)]
39pub struct EwmaSignalData {
40    pub ewma_rssi: EwmaPseudoDecibel,
41    pub ewma_snr: EwmaPseudoDecibel,
42}
43
44impl EwmaSignalData {
45    pub fn new(
46        initial_rssi: impl Into<f64>,
47        initial_snr: impl Into<f64>,
48        ewma_weight: usize,
49    ) -> Self {
50        Self {
51            ewma_rssi: EwmaPseudoDecibel::new(ewma_weight, initial_rssi),
52            ewma_snr: EwmaPseudoDecibel::new(ewma_weight, initial_snr),
53        }
54    }
55
56    #[allow(clippy::ptr_arg, reason = "mass allow for https://fxbug.dev/381896734")]
57    pub fn new_from_list(
58        signals: &Vec<client_types::Signal>,
59        ewma_weight: usize,
60    ) -> Result<Self, anyhow::Error> {
61        let first_signal =
62            signals.first().ok_or_else(|| format_err!("At least one signal must be provided"))?;
63
64        let mut ewma = Self::new(first_signal.rssi_dbm, first_signal.snr_db, ewma_weight);
65        for signal in signals.iter().skip(1) {
66            ewma.update_with_new_measurement(signal.rssi_dbm, signal.snr_db);
67        }
68        Ok(ewma)
69    }
70
71    pub fn update_with_new_measurement(&mut self, rssi: impl Into<f64>, snr: impl Into<f64>) {
72        self.ewma_rssi.update_average(rssi);
73        self.ewma_snr.update_average(snr);
74    }
75}
76
77/// Calculates the rate of change across a vector of dB measurements by determining
78/// the slope of the line of best fit using least squares regression. Return is technically
79/// dB(f64)/t where t is the unit of time used in the vector. Returns error if integer overflows.
80///
81/// Note: This is the linear velocity (not the logarithmic velocity), but it is a useful
82/// abstraction for monitoring real-world signal changes.
83///
84/// Intended to be used for RSSI Values, ranging from -128 to -1.
85fn calculate_raw_velocity(samples: Vec<f64>) -> Result<f64, Error> {
86    let n = i32::try_from(samples.len())?;
87    if n < 2 {
88        return Err(format_err!("At least two data points required to calculate velocity"));
89    }
90    // Using i32 for the calculations, to allow more room for preventing overflows
91    let mut sum_x: i32 = 0;
92    let mut sum_y: i32 = 0;
93    let mut sum_xy: i32 = 0;
94    let mut sum_x2: i32 = 0;
95
96    // Least squares regression summations, returning an error if there are any overflows
97    for (i, y) in samples.iter().enumerate() {
98        let x = i32::try_from(i).map_err(|_| format_err!("failed to convert index to i32"))?;
99        sum_x = sum_x.checked_add(x).ok_or_else(|| format_err!("overflow of X summation"))?;
100        sum_y =
101            sum_y.checked_add(*y as i32).ok_or_else(|| format_err!("overflow of Y summation"))?;
102        sum_xy = sum_xy
103            .checked_add(x.checked_mul(*y as i32).ok_or_else(|| format_err!("overflow of X * Y"))?)
104            .ok_or_else(|| format_err!("overflow of XY summation"))?;
105        sum_x2 = sum_x2
106            .checked_add(x.checked_mul(x).ok_or_else(|| format_err!("overflow of X**2"))?)
107            .ok_or_else(|| format_err!("overflow of X2 summation"))?;
108    }
109
110    // Calculate velocity from summations, returning an error if there are any overflows. Note that
111    // in practice, the try_from should never fail, since the input values are bound from 0 to -128.
112    let velocity = (n.checked_mul(sum_xy).ok_or_else(|| format_err!("overflow in n * sum_xy"))?
113        - sum_x.checked_mul(sum_y).ok_or_else(|| format_err!("overflow in sum_x * sum_y"))?)
114        / (n.checked_mul(sum_x2).ok_or_else(|| format_err!("overflow in n * sum_x2"))?
115            - sum_x.checked_mul(sum_x).ok_or_else(|| format_err!("overflow in sum_x**2"))?);
116    Ok(velocity.into())
117}
118
119// Struct for tracking the exponentially weighted moving average (EWMA) signal measurements.
120#[derive(Clone, Copy, Debug, PartialEq)]
121pub struct RssiVelocity {
122    curr_velocity: f64,
123    prev_rssi: f64,
124}
125impl RssiVelocity {
126    pub fn new(initial_rssi: impl Into<f64>) -> Self {
127        Self { curr_velocity: 0.0, prev_rssi: initial_rssi.into() }
128    }
129
130    #[allow(clippy::ptr_arg, reason = "mass allow for https://fxbug.dev/381896734")]
131    pub fn new_from_list(rssi_samples: &Vec<f64>) -> Result<Self, anyhow::Error> {
132        let last = rssi_samples.last().ok_or_else(|| format_err!("empty list"))?;
133        match calculate_raw_velocity(rssi_samples.to_vec()) {
134            Ok(velocity) => Ok(Self { curr_velocity: velocity, prev_rssi: *last }),
135            Err(e) => Err(e),
136        }
137    }
138
139    pub fn get(&mut self) -> f64 {
140        self.curr_velocity
141    }
142
143    pub fn update(&mut self, rssi: impl Into<f64>) {
144        let rssi: f64 = rssi.into();
145        match calculate_raw_velocity(vec![self.prev_rssi, rssi]) {
146            Ok(velocity) => self.curr_velocity = velocity,
147            Err(e) => {
148                error!("Failed to update velocity: {:?}", e);
149            }
150        }
151        self.prev_rssi = rssi;
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use test_util::{assert_gt, assert_lt};
159
160    #[fuchsia::test]
161    fn test_ewma_pseudo_decibel_simple_averaging_calculations() {
162        let mut ewma_signal = EwmaPseudoDecibel::new(10, -50);
163        assert_eq!(ewma_signal.get(), -50.0);
164
165        // Validate average moves using exponential weighting
166        ewma_signal.update_average(-60);
167        assert_lt!(ewma_signal.get(), -50.0);
168        assert_gt!(ewma_signal.get(), -60.0);
169
170        // Validate average will eventually stabilize.
171        for _ in 0..20 {
172            ewma_signal.update_average(-60)
173        }
174        assert_eq!(ewma_signal.get().round(), -60.0);
175    }
176
177    #[fuchsia::test]
178    fn test_ewma_pseudo_decibel_small_variation_averaging() {
179        let mut ewma_signal = EwmaPseudoDecibel::new(5, -90);
180        assert_eq!(ewma_signal.get(), -90.0);
181
182        // Validate that a small change that does not change the i8 dBm average still changes the
183        // internal f64 average.
184        ewma_signal.update_average(-91);
185        assert_eq!(ewma_signal.get().round(), -90.0);
186        assert_lt!(ewma_signal.current, -90.0);
187
188        // Validate that eventually the small changes are enough to change the i8 dbm average.
189        for _ in 0..5 {
190            ewma_signal.update_average(-91);
191        }
192        assert_lt!(ewma_signal.get(), -90.9);
193    }
194
195    #[fuchsia::test]
196    fn test_ewma_signal_data_new_from_list() {
197        let signals = vec![
198            client_types::Signal { rssi_dbm: -40, snr_db: 30 },
199            client_types::Signal { rssi_dbm: -60, snr_db: 15 },
200        ];
201        let ewma = EwmaSignalData::new_from_list(&signals, 10).unwrap();
202        assert_lt!(ewma.ewma_rssi.get(), -40.0);
203        assert_gt!(ewma.ewma_rssi.get(), -60.0);
204        assert_lt!(ewma.ewma_snr.get(), 30.0);
205        assert_gt!(ewma.ewma_snr.get(), 15.0);
206    }
207
208    #[fuchsia::test]
209    fn test_ewma_signal_data_new_from_list_empty() {
210        assert!(EwmaSignalData::new_from_list(&vec![], 10).is_err());
211    }
212
213    #[fuchsia::test]
214    fn test_ewma_signal_data_update_with_new_measurements() {
215        let mut signal_data = EwmaSignalData::new(-40, 30, 10);
216        signal_data.update_with_new_measurement(-60, 15);
217        assert_lt!(signal_data.ewma_rssi.get(), -40.0);
218        assert_gt!(signal_data.ewma_rssi.get(), -60.0);
219        assert_lt!(signal_data.ewma_snr.get(), 30.0);
220        assert_gt!(signal_data.ewma_snr.get(), 15.0);
221    }
222
223    /// Vector argument must have length >=2.
224    #[fuchsia::test]
225    fn test_calculate_raw_velocity_insufficient_args() {
226        assert!(calculate_raw_velocity(vec![]).is_err());
227        assert!(calculate_raw_velocity(vec![-60.0]).is_err());
228    }
229
230    #[fuchsia::test]
231    fn test_calculate_raw_velocity_negative() {
232        assert_eq!(calculate_raw_velocity(vec![-60.0, -75.0]).expect("failed to calculate"), -15.0);
233        assert_eq!(
234            calculate_raw_velocity(vec![-40.0, -50.0, -58.0, -64.0]).expect("failed to calculate"),
235            -8.0
236        );
237    }
238
239    #[fuchsia::test]
240    fn test_calculate_raw_velocity_positive() {
241        assert_eq!(calculate_raw_velocity(vec![-48.0, -45.0]).expect("failed to calculate"), 3.0);
242        assert_eq!(
243            calculate_raw_velocity(vec![-70.0, -55.0, -45.0, -30.0]).expect("failed to calculate"),
244            13.0
245        );
246    }
247
248    #[fuchsia::test]
249    fn test_calculate_raw_velocity_constant_zero() {
250        assert_eq!(
251            calculate_raw_velocity(vec![-25.0, -25.0, -25.0, -25.0, -25.0, -25.0])
252                .expect("failed to calculate"),
253            0.0
254        );
255    }
256
257    #[fuchsia::test]
258    fn test_calculate_raw_velocity_oscillating_zero() {
259        assert_eq!(
260            calculate_raw_velocity(vec![-35.0, -45.0, -35.0, -25.0, -35.0, -45.0, -35.0,])
261                .expect("failed to calculate"),
262            0.0
263        );
264    }
265
266    #[fuchsia::test]
267    fn test_calculate_raw_velocity_min_max() {
268        assert_eq!(
269            calculate_raw_velocity(vec![-1.0, -128.0]).expect("failed to calculate"),
270            -127.0
271        );
272        assert_eq!(calculate_raw_velocity(vec![-128.0, -1.0]).expect("failed to calculate"), 127.0);
273    }
274
275    #[fuchsia::test]
276    fn test_rssi_velocity_update() {
277        let mut velocity = RssiVelocity::new(-40.0);
278        velocity.update(-80.0);
279        assert_lt!(velocity.get(), 0.0);
280
281        let mut velocity = RssiVelocity::new(-40.0);
282        velocity.update(-20.0);
283        assert_gt!(velocity.get(), 0.0);
284    }
285
286    #[fuchsia::test]
287    fn test_rssi_velocity_new_from_list_insufficent_args() {
288        assert!(RssiVelocity::new_from_list(&vec![]).is_err());
289        assert!(RssiVelocity::new_from_list(&vec![-40.0]).is_err());
290    }
291
292    #[fuchsia::test]
293    fn test_rssi_velocity_new_from_list_negative() {
294        assert_eq!(
295            RssiVelocity::new_from_list(&vec![-60.0, -75.0]).expect("failed to calculate").get(),
296            -15.0
297        );
298        assert_eq!(
299            RssiVelocity::new_from_list(&vec![-40.0, -50.0, -58.0, -64.0])
300                .expect("failed to calculate")
301                .get(),
302            -8.0
303        );
304    }
305
306    #[fuchsia::test]
307    fn test_rssi_velocity_new_from_list_positive() {
308        assert_eq!(
309            RssiVelocity::new_from_list(&vec![-48.0, -45.0]).expect("failed to calculate").get(),
310            3.0
311        );
312        assert_eq!(
313            RssiVelocity::new_from_list(&vec![-70.0, -55.0, -45.0, -30.0])
314                .expect("failed to calculate")
315                .get(),
316            13.0
317        );
318    }
319
320    #[fuchsia::test]
321    fn test_rssi_velocity_new_from_list_constant_zero() {
322        assert_eq!(
323            RssiVelocity::new_from_list(&vec![-25.0, -25.0, -25.0, -25.0, -25.0, -25.0])
324                .expect("failed to calculate")
325                .get(),
326            0.0
327        );
328    }
329
330    #[fuchsia::test]
331    fn test_rssi_velocity_new_from_list_oscillating_zero() {
332        assert_eq!(
333            RssiVelocity::new_from_list(&vec![-35.0, -45.0, -35.0, -25.0, -35.0, -45.0, -35.0,])
334                .expect("failed to calculate")
335                .get(),
336            0.0
337        );
338    }
339
340    #[fuchsia::test]
341    fn test_rssi_velocity_new_from_list_min_max() {
342        assert_eq!(
343            RssiVelocity::new_from_list(&vec![-1.0, -128.0]).expect("failed to calculate").get(),
344            -127.0
345        );
346        assert_eq!(
347            RssiVelocity::new_from_list(&vec![-128.0, -1.0]).expect("failed to calculate").get(),
348            127.0
349        );
350    }
351}