blob: 5fc97662ad6d1537bdcc408a5d5fc100c67b4348 [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use crate::client::types as client_types;
use anyhow::{format_err, Error};
use tracing::error;
/// Update a weighted average with a new measurement
fn calculate_ewma_update(current: f64, next: f64, weighting_factor: f64) -> f64 {
let weight = 2.0 / (1.0 + weighting_factor);
return weight * next + (1.0 - weight) * current;
}
/// Struct for maintaining a dB or dBm exponentially weighted moving average. Differs from
/// SignalStrengthAverage, which is not exponentially weighted.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct EwmaPseudoDecibel {
current: f64,
weighting_factor: f64,
}
impl EwmaPseudoDecibel {
pub fn new(n: usize, initial_signal: impl Into<f64>) -> Self {
Self { current: initial_signal.into(), weighting_factor: n as f64 }
}
/// Returns the current EWMA value
pub fn get(&self) -> f64 {
self.current
}
pub fn update_average(&mut self, next: impl Into<f64>) {
self.current = calculate_ewma_update(self.current, next.into(), self.weighting_factor);
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct EwmaSignalData {
pub ewma_rssi: EwmaPseudoDecibel,
pub ewma_snr: EwmaPseudoDecibel,
}
impl EwmaSignalData {
pub fn new(
initial_rssi: impl Into<f64>,
initial_snr: impl Into<f64>,
ewma_weight: usize,
) -> Self {
Self {
ewma_rssi: EwmaPseudoDecibel::new(ewma_weight, initial_rssi),
ewma_snr: EwmaPseudoDecibel::new(ewma_weight, initial_snr),
}
}
pub fn new_from_list(
signals: &Vec<client_types::Signal>,
ewma_weight: usize,
) -> Result<Self, anyhow::Error> {
if signals.is_empty() {
return Err(format_err!("At least one signal must be provided"));
}
let mut ewma = Self::new(signals[0].rssi_dbm, signals[0].snr_db, ewma_weight);
for signal in signals.iter().skip(1) {
ewma.update_with_new_measurement(signal.rssi_dbm, signal.snr_db);
}
Ok(ewma)
}
pub fn update_with_new_measurement(&mut self, rssi: impl Into<f64>, snr: impl Into<f64>) {
self.ewma_rssi.update_average(rssi);
self.ewma_snr.update_average(snr);
}
}
/// Calculates the rate of change across a vector of dB measurements by determining
/// the slope of the line of best fit using least squares regression. Return is technically
/// dB(f64)/t where t is the unit of time used in the vector. Returns error if integer overflows.
///
/// Note: This is the linear velocity (not the logarithmic velocity), but it is a useful
/// abstraction for monitoring real-world signal changes.
///
/// Intended to be used for RSSI Values, ranging from -128 to -1.
fn calculate_raw_velocity(samples: Vec<f64>) -> Result<f64, Error> {
let n = i32::try_from(samples.len())?;
if n < 2 {
return Err(format_err!("At least two data points required to calculate velocity"));
}
// Using i32 for the calculations, to allow more room for preventing overflows
let mut sum_x: i32 = 0;
let mut sum_y: i32 = 0;
let mut sum_xy: i32 = 0;
let mut sum_x2: i32 = 0;
// Least squares regression summations, returning an error if there are any overflows
for (i, y) in samples.iter().enumerate() {
let x = i32::try_from(i).map_err(|_| format_err!("failed to convert index to i32"))?;
sum_x = sum_x.checked_add(x).ok_or_else(|| format_err!("overflow of X summation"))?;
sum_y =
sum_y.checked_add(*y as i32).ok_or_else(|| format_err!("overflow of Y summation"))?;
sum_xy = sum_xy
.checked_add(x.checked_mul(*y as i32).ok_or_else(|| format_err!("overflow of X * Y"))?)
.ok_or_else(|| format_err!("overflow of XY summation"))?;
sum_x2 = sum_x2
.checked_add(x.checked_mul(x).ok_or_else(|| format_err!("overflow of X**2"))?)
.ok_or_else(|| format_err!("overflow of X2 summation"))?;
}
// Calculate velocity from summations, returning an error if there are any overflows. Note that
// in practice, the try_from should never fail, since the input values are bound from 0 to -128.
let velocity = (n.checked_mul(sum_xy).ok_or_else(|| format_err!("overflow in n * sum_xy"))?
- sum_x.checked_mul(sum_y).ok_or_else(|| format_err!("overflow in sum_x * sum_y"))?)
/ (n.checked_mul(sum_x2).ok_or_else(|| format_err!("overflow in n * sum_x2"))?
- sum_x.checked_mul(sum_x).ok_or_else(|| format_err!("overflow in sum_x**2"))?);
Ok(velocity.into())
}
// Struct for tracking the exponentially weighted moving average (EWMA) signal measurements.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RssiVelocity {
curr_velocity: f64,
prev_rssi: f64,
}
impl RssiVelocity {
pub fn new(initial_rssi: impl Into<f64>) -> Self {
Self { curr_velocity: 0.0, prev_rssi: initial_rssi.into() }
}
pub fn new_from_list(rssi_samples: &Vec<f64>) -> Result<Self, anyhow::Error> {
let last = rssi_samples.last().ok_or(format_err!("empty list"))?;
match calculate_raw_velocity(rssi_samples.to_vec()) {
Ok(velocity) => Ok(Self { curr_velocity: velocity, prev_rssi: *last }),
Err(e) => Err(e),
}
}
pub fn get(&mut self) -> f64 {
self.curr_velocity
}
pub fn update(&mut self, rssi: impl Into<f64>) {
let rssi: f64 = rssi.into();
match calculate_raw_velocity(vec![self.prev_rssi, rssi]) {
Ok(velocity) => self.curr_velocity = velocity,
Err(e) => {
error!("Failed to update velocity: {:?}", e);
}
}
self.prev_rssi = rssi;
}
}
#[cfg(test)]
mod tests {
use super::*;
use test_util::{assert_gt, assert_lt};
#[fuchsia::test]
fn test_ewma_pseudo_decibel_simple_averaging_calculations() {
let mut ewma_signal = EwmaPseudoDecibel::new(10, -50);
assert_eq!(ewma_signal.get(), -50.0);
// Validate average moves using exponential weighting
ewma_signal.update_average(-60);
assert_lt!(ewma_signal.get(), -50.0);
assert_gt!(ewma_signal.get(), -60.0);
// Validate average will eventually stabilize.
for _ in 0..20 {
ewma_signal.update_average(-60)
}
assert_eq!(ewma_signal.get().round(), -60.0);
}
#[fuchsia::test]
fn test_ewma_pseudo_decibel_small_variation_averaging() {
let mut ewma_signal = EwmaPseudoDecibel::new(5, -90);
assert_eq!(ewma_signal.get(), -90.0);
// Validate that a small change that does not change the i8 dBm average still changes the
// internal f64 average.
ewma_signal.update_average(-91);
assert_eq!(ewma_signal.get().round(), -90.0);
assert_lt!(ewma_signal.current, -90.0);
// Validate that eventually the small changes are enough to change the i8 dbm average.
for _ in 0..5 {
ewma_signal.update_average(-91);
}
assert_lt!(ewma_signal.get(), -90.9);
}
#[fuchsia::test]
fn test_ewma_signal_data_new_from_list() {
let signals = vec![
client_types::Signal { rssi_dbm: -40, snr_db: 30 },
client_types::Signal { rssi_dbm: -60, snr_db: 15 },
];
let ewma = EwmaSignalData::new_from_list(&signals, 10).unwrap();
assert_lt!(ewma.ewma_rssi.get(), -40.0);
assert_gt!(ewma.ewma_rssi.get(), -60.0);
assert_lt!(ewma.ewma_snr.get(), 30.0);
assert_gt!(ewma.ewma_snr.get(), 15.0);
}
#[fuchsia::test]
fn test_ewma_signal_data_new_from_list_empty() {
assert!(EwmaSignalData::new_from_list(&vec![], 10).is_err());
}
#[fuchsia::test]
fn test_ewma_signal_data_update_with_new_measurements() {
let mut signal_data = EwmaSignalData::new(-40, 30, 10);
signal_data.update_with_new_measurement(-60, 15);
assert_lt!(signal_data.ewma_rssi.get(), -40.0);
assert_gt!(signal_data.ewma_rssi.get(), -60.0);
assert_lt!(signal_data.ewma_snr.get(), 30.0);
assert_gt!(signal_data.ewma_snr.get(), 15.0);
}
/// Vector argument must have length >=2.
#[fuchsia::test]
fn test_calculate_raw_velocity_insufficient_args() {
assert!(calculate_raw_velocity(vec![]).is_err());
assert!(calculate_raw_velocity(vec![-60.0]).is_err());
}
#[fuchsia::test]
fn test_calculate_raw_velocity_negative() {
assert_eq!(calculate_raw_velocity(vec![-60.0, -75.0]).expect("failed to calculate"), -15.0);
assert_eq!(
calculate_raw_velocity(vec![-40.0, -50.0, -58.0, -64.0]).expect("failed to calculate"),
-8.0
);
}
#[fuchsia::test]
fn test_calculate_raw_velocity_positive() {
assert_eq!(calculate_raw_velocity(vec![-48.0, -45.0]).expect("failed to calculate"), 3.0);
assert_eq!(
calculate_raw_velocity(vec![-70.0, -55.0, -45.0, -30.0]).expect("failed to calculate"),
13.0
);
}
#[fuchsia::test]
fn test_calculate_raw_velocity_constant_zero() {
assert_eq!(
calculate_raw_velocity(vec![-25.0, -25.0, -25.0, -25.0, -25.0, -25.0])
.expect("failed to calculate"),
0.0
);
}
#[fuchsia::test]
fn test_calculate_raw_velocity_oscillating_zero() {
assert_eq!(
calculate_raw_velocity(vec![-35.0, -45.0, -35.0, -25.0, -35.0, -45.0, -35.0,])
.expect("failed to calculate"),
0.0
);
}
#[fuchsia::test]
fn test_calculate_raw_velocity_min_max() {
assert_eq!(
calculate_raw_velocity(vec![-1.0, -128.0]).expect("failed to calculate"),
-127.0
);
assert_eq!(calculate_raw_velocity(vec![-128.0, -1.0]).expect("failed to calculate"), 127.0);
}
#[fuchsia::test]
fn test_rssi_velocity_update() {
let mut velocity = RssiVelocity::new(-40.0);
velocity.update(-80.0);
assert_lt!(velocity.get(), 0.0);
let mut velocity = RssiVelocity::new(-40.0);
velocity.update(-20.0);
assert_gt!(velocity.get(), 0.0);
}
#[fuchsia::test]
fn test_rssi_velocity_new_from_list_insufficent_args() {
assert!(RssiVelocity::new_from_list(&vec![]).is_err());
assert!(RssiVelocity::new_from_list(&vec![-40.0]).is_err());
}
#[fuchsia::test]
fn test_rssi_velocity_new_from_list_negative() {
assert_eq!(
RssiVelocity::new_from_list(&vec![-60.0, -75.0]).expect("failed to calculate").get(),
-15.0
);
assert_eq!(
RssiVelocity::new_from_list(&vec![-40.0, -50.0, -58.0, -64.0])
.expect("failed to calculate")
.get(),
-8.0
);
}
#[fuchsia::test]
fn test_rssi_velocity_new_from_list_positive() {
assert_eq!(
RssiVelocity::new_from_list(&vec![-48.0, -45.0]).expect("failed to calculate").get(),
3.0
);
assert_eq!(
RssiVelocity::new_from_list(&vec![-70.0, -55.0, -45.0, -30.0])
.expect("failed to calculate")
.get(),
13.0
);
}
#[fuchsia::test]
fn test_rssi_velocity_new_from_list_constant_zero() {
assert_eq!(
RssiVelocity::new_from_list(&vec![-25.0, -25.0, -25.0, -25.0, -25.0, -25.0])
.expect("failed to calculate")
.get(),
0.0
);
}
#[fuchsia::test]
fn test_rssi_velocity_new_from_list_oscillating_zero() {
assert_eq!(
RssiVelocity::new_from_list(&vec![-35.0, -45.0, -35.0, -25.0, -35.0, -45.0, -35.0,])
.expect("failed to calculate")
.get(),
0.0
);
}
#[fuchsia::test]
fn test_rssi_velocity_new_from_list_min_max() {
assert_eq!(
RssiVelocity::new_from_list(&vec![-1.0, -128.0]).expect("failed to calculate").get(),
-127.0
);
assert_eq!(
RssiVelocity::new_from_list(&vec![-128.0, -1.0]).expect("failed to calculate").get(),
127.0
);
}
}