simple_moving_average-1.0.2/.cargo_vcs_info.json0000644000000001360000000000100153760ustar { "git": { "sha1": "3b51c436bedbc5161b283dc65b69850d2da0e843" }, "path_in_vcs": "" }simple_moving_average-1.0.2/.gitignore000064400000000000000000000000421046102023000161520ustar 00000000000000/target Cargo.lock /tmp_test_data simple_moving_average-1.0.2/Cargo.toml0000644000000025640000000000100134030ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "simple_moving_average" version = "1.0.2" authors = ["Oskar Gustafsson "] exclude = [ "Makefile", "README.tpl", "rustfmt.toml", "/res", "/test_coverage", "/.github", "/.vscode", ] description = "Library of simple moving average (SMA) algorithms" readme = "README.md" keywords = [ "math", "rolling", "moving", "average", "mean", ] categories = [ "mathematics", "science", ] license = "MIT" repository = "https://github.com/oskargustafsson/moving_average" [lib] name = "simple_moving_average" path = "src/lib.rs" [dependencies.num-traits] version = "0.2.17" [dev-dependencies.cgmath] version = "0.18.0" [dev-dependencies.euclid] version = "0.22.9" [dev-dependencies.nalgebra] version = "0.32.3" [dev-dependencies.rand] version = "0.8.5" features = ["small_rng"] [dev-dependencies.rayon] version = "1.8.0" simple_moving_average-1.0.2/Cargo.toml.orig000064400000000000000000000013711046102023000170570ustar 00000000000000[package] name = "simple_moving_average" version = "1.0.2" edition = "2018" authors = ["Oskar Gustafsson "] description = "Library of simple moving average (SMA) algorithms" readme = "README.md" repository = "https://github.com/oskargustafsson/moving_average" categories = ["mathematics", "science"] keywords = ["math", "rolling", "moving", "average", "mean"] license = "MIT" exclude = [ "Makefile", "README.tpl", "rustfmt.toml", "/res", "/test_coverage", "/.github", "/.vscode", ] [lib] name = "simple_moving_average" path = "src/lib.rs" [dependencies] num-traits = "0.2.17" [dev-dependencies] rand = { version = "0.8.5", features = ["small_rng"] } nalgebra = "0.32.3" euclid = "0.22.9" cgmath = "0.18.0" rayon = "1.8.0" simple_moving_average-1.0.2/LICENSE000064400000000000000000000020761046102023000152000ustar 00000000000000Copyright 2021 Oskar Gustafsson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. simple_moving_average-1.0.2/README.md000064400000000000000000000170741046102023000154560ustar 00000000000000# simple_moving_average [![Test coverage](test_coverage/html/badges/flat_square.svg)](test_coverage/) This crate provides several algorithms for calculating the [simple moving average](https://en.wikipedia.org/wiki/Moving_average#Simple_moving_average) (SMA) of a series of data samples. SMAs are commonly used to implement [low-pass filters](https://en.wikipedia.org/wiki/Low-pass_filter), the second-most useful filter type, bested only by coffee filters. All algorithms implement the [SMA] trait, which provides an implementation-agnostic interface. The interface is generic over sample type, meaning that any type that supports addition and division by a scalar can be averaged. This includes most primitive numeric types ([f32](https://doc.rust-lang.org/std/primitive.f32.html), [u32](https://doc.rust-lang.org/std/primitive.u32.html), ...), [Duration](https://doc.rust-lang.org/std/time/struct.Duration.html) and many third party math library (e.g. [nalgebra](https://docs.rs/nalgebra/), [euclid](https://docs.rs/euclid/), [cgmath](https://docs.rs/cgmath/), ...) vector and matrix types. ### Project status The library is actively used, feature complete, fully tested and there are no known bugs. It can be considered production ready, but it is not actively developed, as there are no obvious needs motivating it. Bug reports, feature requests and pull requests are welcome through [GitHub](https://github.com/oskargustafsson/moving_average). ### Examples *Scalars* ```rust let mut ma = SumTreeSMA::<_, f32, 2>::new(); // Sample window size = 2 ma.add_sample(1.0); ma.add_sample(2.0); ma.add_sample(3.0); assert_eq!(ma.get_average(), 2.5); // = (2 + 3) / 2 ``` *Vectors* ```rust let mut ma = NoSumSMA::<_, f64, 2>::new(); ma.add_sample(Vector3::new(1.0, 2.0, 3.0)); ma.add_sample(Vector3::new(-4.0, -2.0, -1.0)); assert_eq!(ma.get_average(), Vector3::new(-1.5, 0.0, 1.0)); ``` *Durations* ```rust let mut ma = SingleSumSMA::<_, _, 10>::from_zero(Duration::ZERO); loop { let instant = Instant::now(); // [ application code ] ma.add_sample(instant.elapsed()); dbg!("Average iteration duration: {}", ma.get_average()); # break; } ``` ### Algorithm implementations One way to achieve good performance when calculating simple moving averages is to cache previous calculations, specifically the sum of the samples currently in the sample window. Caching this sum has both pros and cons, which is what motivates the three different implementations presented below. | Implementation | Add sample | Get average | Caveat | |----------------|-------------|-------------|---------------------------------------------| | [NoSumSMA] | `O(1)` | `O(N)` | - | | [SingleSumSMA] | `O(1)` | `O(1)` | Accumulates floating point rounding errors. | | [SumTreeSMA] | `O(log(N))` | `O(1)` | - | `N` refers to the size of the sample window. All implementations have `O(N)` space complexity. [NoSumSMA] and [SingleSumSMA] are completely // backed by arrays, so they are by default stack allocated. [SumTreeSMA] stores some data in an array, but its sum tree is stored in a Vec. #### NoSumSMA The most straightforward way of implementing a moving average is to not cache any sum at all, hence the name if this implementation. The sum of all samples is calculated from scratch, at `O(N)` time complexity (`N` being the sample window size), every time the average is requested. **When to use** - When the sample window size is so small that the samples summation cost is negligible. - When new samples are written significantly more often than the average value is read. #### SingleSumSMA This implementation caches the sum of all samples in the sample window as a single value, leading to `O(1)` time complexity for both writing new samples and reading their average. A problem with this approach is that most floating point numbers can't be stored exactly, so every time a such a number is added to the cached sum, there is a risk of accumulating a rounding error. The magnitude of the accumulated error depends on many factors, including sample window size and sample distribution. Below is a visualization of how the absolute difference in average value between [SingleSumSMA] and [NoSumSMA] (which does not suffer from accumulated rounding errors) grows with the number of samples, for a typical window size and set of samples. `Sample type: f32`, `Sample window size: 10`, `Sample distribution: Uniform[-100, 100]` ![Difference between SingleSumSMA and NoSumSMA](https://raw.githubusercontent.com/oskargustafsson/moving_average/master/res/single_sum_diff.png) *Note:* Both axes of the graph are logarithmic. The Y axis values represent the maximum differences found over 100 test runs. One way to reduce the error is to use wider type, e.g. `f64` instead of `f32`. The absolute error is also less prominent when the samples lie near the interval `[-1, 1]`, as that is where floating point precision is at its highest. **When to use** - When sample values can be represented exactly in memory, in which case there is no downside to this approach. This is true for all [primitive integer](https://doc.rust-lang.org/book/ch03-02-data-types.html#integer-types) types and [Duration](https://doc.rust-lang.org/std/time/struct.Duration.html). - When performance is more important than numerical accuracy. #### SumTreeSMA There is a way of avoiding the accumulated floating point rounding errors, without having to re-calculate the whole samples sum every time the average value is requested. The downside though, is that it involves both math and binary trees: A sum is the result of applying the binary and [associative](https://en.wikipedia.org/wiki/Associative_property) addition operation to a set of operands, which means that it can be represented as a binary tree of sums. For example `(1) + (2) + (3) + (4) + (5) + (6)` = `(1 + 2) + (3 + 4) + (5 + 6)` = `(3) + (7) + (11)` = `(3 + 7) + (11)` = `(10) + (11)` = `(10 + 11)` = `(21)` can be represented as the following tree. ``` ‌ 21 ‌ / \ ‌ / \ ‌ 10 11 ‌ / \ \ ‌ / \ \ ‌ 3 7 11 ‌ / \ / \ / \ ‌ 1 2 3 4 5 6 ``` If one of the leaf nodes (i.e. samples) were to change, only the nodes comprising the direct path between that leaf and the root need to be re-calculated, leading to `log(N)` calculations, `N` being the window size. This is exactly what happens when a sample is added; the oldest sample gets replaced with the new sample and sum tree leaf node corresponding to the oldest sample is updated with the new sample value. One existing leaf node (i.e. sample value) is always re-read when updating that leaf node's neighbor, meaning that after N samples have been added, all the leaf nodes have been re-read. This is what keeps the floating point rounding error from accumulating. *Author's note:* If anyone has the brains and will to prove this formally, they are most welcome to submit a [PR](https://github.com/oskargustafsson/moving_average/pulls). In the mean time, there is a unit test that empirically proves that the rounding error does not accumulate. Part of that test's output data is visualized in the graph below, showing no accumulated rounding errors when compared with [NoSumSMA]. ![Difference between SumTreeSMA and NoSumSMA](https://raw.githubusercontent.com/oskargustafsson/moving_average/master/res/sum_tree_diff.png) **When to use** - In most cases where floating point data is involved, unless writes are much more common than reads. ## License MIT simple_moving_average-1.0.2/src/common.rs000064400000000000000000000015671046102023000166240ustar 00000000000000use std::any::type_name; use num_traits::FromPrimitive; pub fn cast_to_divisor_type(divisor: usize) -> Divisor { Divisor::from_usize(divisor).unwrap_or_else(|| { panic!( "Failed to create a divisor of type {} from {}", type_name::(), divisor ) }) } pub fn wrapping_add(lhs: usize, rhs: usize) -> usize { (lhs + rhs) % MAX_VAL } pub fn wrapping_sub(lhs: usize, rhs: usize) -> usize { debug_assert!(rhs <= MAX_VAL); if lhs < rhs { (MAX_VAL - rhs) + lhs } else { lhs - rhs } } #[cfg(test)] mod tests { use super::*; #[test] fn cast_to_divisor_type_success() { let divisor = cast_to_divisor_type::(u32::MAX as usize); assert_eq!(divisor, u32::MAX); } #[test] #[should_panic] fn cast_to_divisor_type_fail() { cast_to_divisor_type::(u32::MAX as usize + 1); } } simple_moving_average-1.0.2/src/iterator.rs000064400000000000000000000014651046102023000171620ustar 00000000000000use crate::common::{wrapping_add, wrapping_sub}; #[derive(Debug)] pub struct Iter<'a, Item: 'a, const CAPACITY: usize> { items: &'a [Item], cursor_idx: usize, num_items_left: usize, } impl<'a, Item: 'a, const CAPACITY: usize> Iter<'a, Item, CAPACITY> { pub fn new(items: &'a [Item], end_idx: usize, num_items: usize) -> Self { Self { items, cursor_idx: wrapping_sub::(end_idx, num_items), num_items_left: num_items, } } } impl<'a, Item, const CAPACITY: usize> Iterator for Iter<'a, Item, CAPACITY> { type Item = &'a Item; fn next(&mut self) -> Option { if self.num_items_left == 0 { return None; } self.num_items_left -= 1; let cursor_idx = self.cursor_idx; self.cursor_idx = wrapping_add::(self.cursor_idx, 1); Some(&self.items[cursor_idx]) } } simple_moving_average-1.0.2/src/lib.rs000064400000000000000000000404641046102023000161010ustar 00000000000000/*! This crate provides several algorithms for calculating the [simple moving average](https://en.wikipedia.org/wiki/Moving_average#Simple_moving_average) (SMA) of a series of data samples. SMAs are commonly used to implement [low-pass filters](https://en.wikipedia.org/wiki/Low-pass_filter), the second-most useful filter type, bested only by coffee filters. All algorithms implement the [SMA] trait, which provides an implementation-agnostic interface. The interface is generic over sample type, meaning that any type that supports addition and division by a scalar can be averaged. This includes most primitive numeric types ([f32](https://doc.rust-lang.org/std/primitive.f32.html), [u32](https://doc.rust-lang.org/std/primitive.u32.html), ...), [Duration](https://doc.rust-lang.org/std/time/struct.Duration.html) and many third party math library (e.g. [nalgebra](https://docs.rs/nalgebra/), [euclid](https://docs.rs/euclid/), [cgmath](https://docs.rs/cgmath/), ...) vector and matrix types. ## Project status The library is actively used, feature complete, fully tested and there are no known bugs. It can be considered production ready, but it is not actively developed, as there are no obvious needs motivating it. Bug reports, feature requests and pull requests are welcome through [GitHub](https://github.com/oskargustafsson/moving_average). ## Examples *Scalars* ``` # use simple_moving_average::{SMA, SumTreeSMA}; let mut ma = SumTreeSMA::<_, f32, 2>::new(); // Sample window size = 2 ma.add_sample(1.0); ma.add_sample(2.0); ma.add_sample(3.0); assert_eq!(ma.get_average(), 2.5); // = (2 + 3) / 2 ``` *Vectors* ``` # use simple_moving_average::{SMA, NoSumSMA}; # use std::time::{Duration, Instant}; # use nalgebra::Vector3; let mut ma = NoSumSMA::<_, f64, 2>::new(); ma.add_sample(Vector3::new(1.0, 2.0, 3.0)); ma.add_sample(Vector3::new(-4.0, -2.0, -1.0)); assert_eq!(ma.get_average(), Vector3::new(-1.5, 0.0, 1.0)); ``` *Durations* ``` # use simple_moving_average::{SMA, SingleSumSMA}; # use std::time::{Duration, Instant}; let mut ma = SingleSumSMA::<_, _, 10>::from_zero(Duration::ZERO); loop { let instant = Instant::now(); // [ application code ] ma.add_sample(instant.elapsed()); dbg!("Average iteration duration: {}", ma.get_average()); # break; } ``` ## Algorithm implementations One way to achieve good performance when calculating simple moving averages is to cache previous calculations, specifically the sum of the samples currently in the sample window. Caching this sum has both pros and cons, which is what motivates the three different implementations presented below. | Implementation | Add sample | Get average | Caveat | |----------------|-------------|-------------|---------------------------------------------| | [NoSumSMA] | `O(1)` | `O(N)` | - | | [SingleSumSMA] | `O(1)` | `O(1)` | Accumulates floating point rounding errors. | | [SumTreeSMA] | `O(log(N))` | `O(1)` | - | `N` refers to the size of the sample window. All implementations have `O(N)` space complexity. [NoSumSMA] and [SingleSumSMA] are completely // backed by arrays, so they are by default stack allocated. [SumTreeSMA] stores some data in an array, but its sum tree is stored in a Vec. ### NoSumSMA The most straightforward way of implementing a moving average is to not cache any sum at all, hence the name if this implementation. The sum of all samples is calculated from scratch, at `O(N)` time complexity (`N` being the sample window size), every time the average is requested. **When to use** - When the sample window size is so small that the samples summation cost is negligible. - When new samples are written significantly more often than the average value is read. ### SingleSumSMA This implementation caches the sum of all samples in the sample window as a single value, leading to `O(1)` time complexity for both writing new samples and reading their average. A problem with this approach is that most floating point numbers can't be stored exactly, so every time a such a number is added to the cached sum, there is a risk of accumulating a rounding error. The magnitude of the accumulated error depends on many factors, including sample window size and sample distribution. Below is a visualization of how the absolute difference in average value between [SingleSumSMA] and [NoSumSMA] (which does not suffer from accumulated rounding errors) grows with the number of samples, for a typical window size and set of samples. `Sample type: f32`, `Sample window size: 10`, `Sample distribution: Uniform[-100, 100]` ![Difference between SingleSumSMA and NoSumSMA](https://raw.githubusercontent.com/oskargustafsson/moving_average/master/res/single_sum_diff.png) *Note:* Both axes of the graph are logarithmic. The Y axis values represent the maximum differences found over 100 test runs. One way to reduce the error is to use wider type, e.g. `f64` instead of `f32`. The absolute error is also less prominent when the samples lie near the interval `[-1, 1]`, as that is where floating point precision is at its highest. **When to use** - When sample values can be represented exactly in memory, in which case there is no downside to this approach. This is true for all [primitive integer](https://doc.rust-lang.org/book/ch03-02-data-types.html#integer-types) types and [Duration](https://doc.rust-lang.org/std/time/struct.Duration.html). - When performance is more important than numerical accuracy. ### SumTreeSMA There is a way of avoiding the accumulated floating point rounding errors, without having to re-calculate the whole samples sum every time the average value is requested. The downside though, is that it involves both math and binary trees: A sum is the result of applying the binary and [associative](https://en.wikipedia.org/wiki/Associative_property) addition operation to a set of operands, which means that it can be represented as a binary tree of sums. For example `(1) + (2) + (3) + (4) + (5) + (6)` = `(1 + 2) + (3 + 4) + (5 + 6)` = `(3) + (7) + (11)` = `(3 + 7) + (11)` = `(10) + (11)` = `(10 + 11)` = `(21)` can be represented as the following tree. ```text ‌ 21 ‌ / \ ‌ / \ ‌ 10 11 ‌ / \ \ ‌ / \ \ ‌ 3 7 11 ‌ / \ / \ / \ ‌ 1 2 3 4 5 6 ``` If one of the leaf nodes (i.e. samples) were to change, only the nodes comprising the direct path between that leaf and the root need to be re-calculated, leading to `log(N)` calculations, `N` being the window size. This is exactly what happens when a sample is added; the oldest sample gets replaced with the new sample and sum tree leaf node corresponding to the oldest sample is updated with the new sample value. One existing leaf node (i.e. sample value) is always re-read when updating that leaf node's neighbor, meaning that after N samples have been added, all the leaf nodes have been re-read. This is what keeps the floating point rounding error from accumulating. *Author's note:* If anyone has the brains and will to prove this formally, they are most welcome to submit a [PR](https://github.com/oskargustafsson/moving_average/pulls). In the mean time, there is a unit test that empirically proves that the rounding error does not accumulate. Part of that test's output data is visualized in the graph below, showing no accumulated rounding errors when compared with [NoSumSMA]. ![Difference between SumTreeSMA and NoSumSMA](https://raw.githubusercontent.com/oskargustafsson/moving_average/master/res/sum_tree_diff.png) **When to use** - In most cases where floating point data is involved, unless writes are much more common than reads. */ mod common; mod iterator; mod no_sum_sma; mod ring_buffer; mod single_sum_sma; mod sma; mod sum_tree; mod sum_tree_sma; pub use crate::iterator::Iter; pub use crate::no_sum_sma::NoSumSMA; pub use crate::single_sum_sma::SingleSumSMA; pub use crate::sma::SMA; pub use crate::sum_tree_sma::SumTreeSMA; #[cfg(test)] mod tests { use crate::{NoSumSMA, SingleSumSMA, SumTreeSMA, SMA}; macro_rules! get_sma_impls { ( $divisor_type:ty, $window_size:expr, $ctor:ident $(, $zero:expr)? ) => {{ let ma_impls: [Box>; 3] = [ Box::new(SingleSumSMA::<_, _, $window_size>::$ctor($($zero ,)?)), Box::new(SumTreeSMA::<_, _, $window_size>::$ctor($($zero ,)?)), Box::new(NoSumSMA::<_, _, $window_size>::$ctor($($zero ,)?)), ]; ma_impls }}; } #[test] fn f32_samples() { for sma in &mut get_sma_impls!(f32, 3, new) { assert_eq!(sma.get_average(), 0.0); assert_eq!(sma.get_num_samples(), 0); sma.add_sample(4.0); assert_eq!(sma.get_average(), 4.0); assert_eq!(sma.get_num_samples(), 1); sma.add_sample(8.0); assert_eq!(sma.get_average(), 6.0); assert_eq!(sma.get_num_samples(), 2); sma.add_sample(3.0); assert_eq!(sma.get_average(), 5.0); assert_eq!(sma.get_num_samples(), 3); // Here we reach window_size and start to pop old samples sma.add_sample(7.0); assert_eq!(sma.get_average(), 6.0); assert_eq!(sma.get_num_samples(), 3); sma.add_sample(11.0); assert_eq!(sma.get_average(), 7.0); assert_eq!(sma.get_num_samples(), 3); sma.add_sample(0.0); assert_eq!(sma.get_average(), 6.0); assert_eq!(sma.get_num_samples(), 3); sma.add_sample(-23.0); assert_eq!(sma.get_average(), -4.0); assert_eq!(sma.get_num_samples(), 3); } } #[test] fn u32_samples() { for sma in &mut get_sma_impls!(u32, 3, new) { assert_eq!(sma.get_average(), 0); sma.add_sample(4); assert_eq!(sma.get_average(), 4); sma.add_sample(8); assert_eq!(sma.get_average(), 6); sma.add_sample(3); assert_eq!(sma.get_average(), 5); sma.add_sample(7); assert_eq!(sma.get_average(), 6); sma.add_sample(11); assert_eq!(sma.get_average(), 7); sma.add_sample(0); assert_eq!(sma.get_average(), 6); } } #[test] fn u32_samples_2() { for sma in &mut get_sma_impls!(u32, 3, new) { sma.add_sample(1); assert_eq!(sma.get_average(), 1); sma.add_sample(2); assert_eq!(sma.get_average(), 1); sma.add_sample(3); assert_eq!(sma.get_average(), 2); sma.add_sample(4); assert_eq!(sma.get_average(), 3); sma.add_sample(10); assert_eq!(sma.get_average(), 5); } } #[test] fn nalgebra_vector2_f32_samples() { use nalgebra::Vector2; for sma in &mut get_sma_impls!(f32, 3, new) { assert_eq!(sma.get_average(), Vector2::new(0.0, 0.0)); sma.add_sample(Vector2::new(4.0, 8.0)); assert_eq!(sma.get_average(), Vector2::new(4.0, 8.0)); sma.add_sample(Vector2::new(6.0, 0.0)); assert_eq!(sma.get_average(), Vector2::new(5.0, 4.0)); sma.add_sample(Vector2::new(2.0, 10.0)); assert_eq!(sma.get_average(), Vector2::new(4.0, 6.0)); sma.add_sample(Vector2::new(-17.0, 20.0)); assert_eq!(sma.get_average(), Vector2::new(-3.0, 10.0)); sma.add_sample(Vector2::new(0.0, -21.0)); assert_eq!(sma.get_average(), Vector2::new(-5.0, 3.0)); } } #[test] fn euclid_vector2_f32_samples() { use euclid::default::Vector2D; for sma in &mut get_sma_impls!(f32, 3, from_zero, Vector2D::zero()) { assert_eq!(sma.get_average(), Vector2D::new(0.0, 0.0)); sma.add_sample(Vector2D::new(4.0, 8.0)); assert_eq!(sma.get_average(), Vector2D::new(4.0, 8.0)); sma.add_sample(Vector2D::new(6.0, 0.0)); assert_eq!(sma.get_average(), Vector2D::new(5.0, 4.0)); sma.add_sample(Vector2D::new(2.0, 10.0)); assert_eq!(sma.get_average(), Vector2D::new(4.0, 6.0)); sma.add_sample(Vector2D::new(-17.0, 20.0)); assert_eq!(sma.get_average(), Vector2D::new(-3.0, 10.0)); sma.add_sample(Vector2D::new(0.0, -21.0)); assert_eq!(sma.get_average(), Vector2D::new(-5.0, 3.0)); } } #[test] fn cgmath_vector2_f32_samples() { use cgmath::Vector2; for sma in &mut get_sma_impls!(f32, 3, new) { assert_eq!(sma.get_average(), Vector2::new(0.0, 0.0)); sma.add_sample(Vector2::new(4.0, 8.0)); assert_eq!(sma.get_average(), Vector2::new(4.0, 8.0)); sma.add_sample(Vector2::new(6.0, 0.0)); assert_eq!(sma.get_average(), Vector2::new(5.0, 4.0)); sma.add_sample(Vector2::new(2.0, 10.0)); assert_eq!(sma.get_average(), Vector2::new(4.0, 6.0)); sma.add_sample(Vector2::new(-17.0, 20.0)); assert_eq!(sma.get_average(), Vector2::new(-3.0, 10.0)); sma.add_sample(Vector2::new(0.0, -21.0)); assert_eq!(sma.get_average(), Vector2::new(-5.0, 3.0)); } } #[test] fn duration_samples() { use std::time::Duration; for sma in &mut get_sma_impls!(u32, 3, from_zero, Duration::ZERO) { assert_eq!(sma.get_average(), Duration::from_secs(0)); sma.add_sample(Duration::from_secs(10)); assert_eq!(sma.get_average(), Duration::from_secs(10)); sma.add_sample(Duration::from_secs(20)); assert_eq!(sma.get_average(), Duration::from_secs(15)); sma.add_sample(Duration::from_secs(30)); assert_eq!(sma.get_average(), Duration::from_secs(20)); sma.add_sample(Duration::from_secs(1)); assert_eq!(sma.get_average(), Duration::from_secs(17)); sma.add_sample(Duration::from_secs(32)); assert_eq!(sma.get_average(), Duration::from_secs(21)); } } #[test] fn edge_case_zero_sized() { for sma in &mut get_sma_impls!(u32, 0, new) { assert_eq!(sma.get_average(), 0); assert_eq!(sma.get_num_samples(), 0); sma.add_sample(16); assert_eq!(sma.get_average(), 0); assert_eq!(sma.get_num_samples(), 0); } } #[test] fn misc_getters() { for sma in &mut get_sma_impls!(u32, 5, new) { assert_eq!(sma.get_average(), 0); assert_eq!(sma.get_sample_window_size(), 5); assert_eq!(sma.get_num_samples(), 0); assert_eq!(sma.get_most_recent_sample(), None); assert_eq!(sma.get_sample_window_iter().collect::>().len(), 0); sma.add_sample(13); assert_eq!(sma.get_average(), 13); assert_eq!(sma.get_sample_window_size(), 5); assert_eq!(sma.get_num_samples(), 1); assert_eq!(sma.get_most_recent_sample(), Some(13)); assert_eq!( sma.get_sample_window_iter().collect::>(), vec![&13] ); sma.add_sample(37); assert_eq!(sma.get_average(), 25); assert_eq!(sma.get_sample_window_size(), 5); assert_eq!(sma.get_num_samples(), 2); assert_eq!(sma.get_most_recent_sample(), Some(37)); assert_eq!( sma.get_sample_window_iter().collect::>(), vec![&13, &37] ); } } #[test] fn f32_random_samples_max_algorithm_diffs() { use rand::{distributions::Uniform, rngs::SmallRng, Rng, SeedableRng}; use rayon::prelude::*; const WINDOW_SIZE: usize = 10; const VALUE_RANGES: [(usize, usize); 6] = [ (0, 10), (10, 100), (100, 1000), (1000, 10000), (10000, 100000), (100000, 1000000), ]; let seeds: Vec = SmallRng::seed_from_u64(0xCAFEBABE) .sample_iter(&Uniform::from(0..u64::MAX)) .take(100) .collect(); let averages_array_vec: Vec<[[f32; 3]; VALUE_RANGES.len()]> = seeds .par_iter() .map(|seed| { let random_values: Vec = SmallRng::seed_from_u64(*seed) .sample_iter(&Uniform::from(-100.0..100.0)) .take(1000000) .collect(); let mut single_sum_sma = SingleSumSMA::<_, f32, WINDOW_SIZE>::new(); let mut sum_tree_sma = SumTreeSMA::<_, f32, WINDOW_SIZE>::new(); let mut no_sum_sma = NoSumSMA::<_, f32, WINDOW_SIZE>::new(); VALUE_RANGES.map(|value_range| { for random_value in &random_values[value_range.0..value_range.1] { single_sum_sma.add_sample(*random_value); sum_tree_sma.add_sample(*random_value); no_sum_sma.add_sample(*random_value); } [ single_sum_sma.get_average(), sum_tree_sma.get_average(), no_sum_sma.get_average(), ] }) }) .collect(); let mut maximum_absolute_diffs_array = [[0.0f32; VALUE_RANGES.len()]; 2]; for averages_array in averages_array_vec { for (idx, averages) in averages_array.iter().enumerate() { for i in 0..2 { let abs_diff = (averages[i] - averages[2]).abs(); if maximum_absolute_diffs_array[i][idx] < abs_diff { maximum_absolute_diffs_array[i][idx] = abs_diff; } } } } let [single_sum_maximum_absolute_diff, sum_tree_maximum_absolute_diff]: [f32; 2] = maximum_absolute_diffs_array.map(|maximum_absolute_diffs| { *maximum_absolute_diffs .iter() .max_by(|a, b| a.abs().partial_cmp(&b.abs()).unwrap()) .unwrap() }); assert!(single_sum_maximum_absolute_diff < 0.002); assert!(sum_tree_maximum_absolute_diff < 0.000006); } } simple_moving_average-1.0.2/src/no_sum_sma.rs000064400000000000000000000047671046102023000175010ustar 00000000000000use super::SMA; use crate::{common::cast_to_divisor_type, ring_buffer::RingBuffer, Iter}; use num_traits::{FromPrimitive, Zero}; use std::{ marker::{self, PhantomData}, ops::{AddAssign, Div}, }; /// An SMA implementation that does not cache any intermediate sample sum. #[derive(Clone, Copy, Debug)] pub struct NoSumSMA { samples: RingBuffer, zero: Sample, _marker: marker::PhantomData, } impl SMA for NoSumSMA where Sample: Copy + AddAssign + Div, Divisor: FromPrimitive, { fn add_sample(&mut self, new_sample: Sample) { if WINDOW_SIZE == 0 { return; } self.samples.push_front(new_sample); } fn get_average(&self) -> Sample { let num_samples = self.samples.len(); if num_samples == 0 { return self.zero; } let sum = { let mut sum = self.zero; for sample in self.samples.iter() { sum += *sample; } sum }; sum / cast_to_divisor_type(num_samples) } fn get_most_recent_sample(&self) -> Option { self.samples.front().cloned() } fn get_num_samples(&self) -> usize { self.samples.len() } fn get_sample_window_size(&self) -> usize { WINDOW_SIZE } fn get_sample_window_iter(&self) -> Iter { self.samples.iter() } } impl NoSumSMA { /// Constructs a new [NoSumSMA] with window size `WINDOW_SIZE`. This constructor is /// only available for `Sample` types that implement [num_traits::Zero]. If the `Sample` type /// does not, use the [from_zero](NoSumSMA::from_zero) constructor instead. /// /// Note that the `Divisor` type usually cannot be derived by the compiler when using this /// constructor and must be explicitly stated, even if it is the same as the `Sample` type. pub fn new() -> Self { Self { samples: RingBuffer::new(Sample::zero()), zero: Sample::zero(), _marker: PhantomData, } } } impl NoSumSMA { /// Constructs a new [NoSumSMA] with window size `WINDOW_SIZE` from the given /// `zero` sample. If the `Sample` type implements [num_traits::Zero], the /// [new](NoSumSMA::new) constructor might be preferable to this. pub fn from_zero(zero: Sample) -> Self { Self { samples: RingBuffer::new(zero), zero, _marker: PhantomData, } } } simple_moving_average-1.0.2/src/ring_buffer.rs000064400000000000000000000051261046102023000176170ustar 00000000000000use crate::{ common::{wrapping_add, wrapping_sub}, Iter, }; #[derive(Clone, Copy, Debug)] pub struct RingBuffer { items: [Item; CAPACITY], front_idx: usize, num_items: usize, } impl RingBuffer { pub fn new(zero: Item) -> Self { Self { items: [zero; CAPACITY], front_idx: 0, // Index of the next available slot num_items: 0, } } pub fn shift(&mut self, item: Item) -> Option { let popped_item = if self.len() == CAPACITY { self.pop_back() } else { None }; self.push_front(item); popped_item } pub fn push_front(&mut self, item: Item) { self.items[self.front_idx] = item; self.front_idx = wrapping_add::(self.front_idx, 1); self.num_items = CAPACITY.min(self.num_items + 1); } pub fn pop_back(&mut self) -> Option { if 0 < self.num_items { let num_items = self.num_items; self.num_items -= 1; Some(self.items[wrapping_sub::(self.front_idx, num_items)]) } else { None } } pub fn front(&self) -> Option<&Item> { if 0 < self.num_items { Some(&self.items[wrapping_sub::(self.front_idx, 1)]) } else { None } } pub fn len(&self) -> usize { self.num_items } pub fn iter(&self) -> Iter<'_, Item, CAPACITY> { Iter::new(&self.items, self.front_idx, self.num_items) } } #[cfg(test)] mod tests { use super::*; fn assert_rb_state(rb: &RingBuffer, items: &[u32]) { assert_eq!(rb.len(), items.len()); assert_eq!(rb.front(), items.get(items.len().wrapping_sub(1))); assert_eq!( rb.iter().collect::>(), items.iter().collect::>() ); } #[test] fn basics() { let mut rb: RingBuffer = RingBuffer::new(0); assert_eq!(rb.pop_back(), None); assert_rb_state(&rb, &[]); rb.push_front(1); assert_rb_state(&rb, &[1]); assert_eq!(rb.pop_back(), Some(1)); assert_rb_state(&rb, &[]); rb.push_front(1); assert_eq!(rb.shift(2), None); assert_rb_state(&rb, &[1, 2]); assert_eq!(rb.shift(3), None); assert_rb_state(&rb, &[1, 2, 3]); assert_eq!(rb.shift(4), Some(1)); assert_rb_state(&rb, &[2, 3, 4]); rb.push_front(5); assert_rb_state(&rb, &[3, 4, 5]); assert_eq!(rb.pop_back(), Some(3)); assert_rb_state(&rb, &[4, 5]); assert_eq!(rb.pop_back(), Some(4)); assert_rb_state(&rb, &[5]); assert_eq!(rb.shift(6), None); assert_rb_state(&rb, &[5, 6]); assert_eq!(rb.pop_back(), Some(5)); assert_rb_state(&rb, &[6]); assert_eq!(rb.pop_back(), Some(6)); assert_rb_state(&rb, &[]); assert_eq!(rb.pop_back(), None); assert_rb_state(&rb, &[]); } } simple_moving_average-1.0.2/src/single_sum_sma.rs000064400000000000000000000050771046102023000203410ustar 00000000000000use super::SMA; use crate::{common::cast_to_divisor_type, ring_buffer::RingBuffer, Iter}; use num_traits::{FromPrimitive, Zero}; use std::{ marker::{self, PhantomData}, ops::{AddAssign, Div, SubAssign}, }; /// An SMA implementation that caches the sum of all samples currently in the sample window as a /// single value. #[derive(Clone, Copy, Debug)] pub struct SingleSumSMA { samples: RingBuffer, sum: Sample, _marker: marker::PhantomData, } impl SMA for SingleSumSMA where Sample: Copy + AddAssign + SubAssign + Div, Divisor: FromPrimitive, { fn add_sample(&mut self, new_sample: Sample) { if WINDOW_SIZE == 0 { return; } self.sum += new_sample; if let Some(shifted_sample) = self.samples.shift(new_sample) { self.sum -= shifted_sample; } } fn get_average(&self) -> Sample { let num_samples = self.samples.len(); if num_samples == 0 { return self.sum; } self.sum / cast_to_divisor_type(num_samples) } fn get_most_recent_sample(&self) -> Option { self.samples.front().cloned() } fn get_num_samples(&self) -> usize { self.samples.len() } fn get_sample_window_size(&self) -> usize { WINDOW_SIZE } fn get_sample_window_iter(&self) -> Iter { self.samples.iter() } } impl SingleSumSMA { /// Constructs a new [SingleSumSMA] with window size `WINDOW_SIZE`. This constructor is /// only available for `Sample` types that implement [num_traits::Zero]. If the `Sample` type /// does not, use the [from_zero](SingleSumSMA::from_zero) constructor instead. /// /// Note that the `Divisor` type usually cannot be derived by the compiler when using this /// constructor and must be explicitly stated, even if it is the same as the `Sample` type. pub fn new() -> Self { Self { samples: RingBuffer::new(Sample::zero()), sum: Sample::zero(), _marker: PhantomData, } } } impl SingleSumSMA { /// Constructs a new [SingleSumSMA] with window size `WINDOW_SIZE` from the given /// `zero` sample. If the `Sample` type implements [num_traits::Zero], the /// [new](SingleSumSMA::new) constructor might be preferable to this. pub fn from_zero(zero: Sample) -> Self { Self { samples: RingBuffer::new(zero), sum: zero, _marker: PhantomData, } } } simple_moving_average-1.0.2/src/sma.rs000064400000000000000000000033341046102023000161060ustar 00000000000000use crate::Iter; /// This trait provides an common interface for algorithms that can calculate a simple moving /// average. /// /// In this crate, a simple moving average is defined as `sum(window(samples, N)) / length(window(samples, N))`. /// Here `samples` is a possibly infinite series of samples. The `window` function extracts the last /// `N` of those samples. /// /// *Implementation detail:* For the purposes of this library, there is no point in keeping samples /// outside the sample window around, so they are discarded when newer samples push them out of the /// window. This allows the implementations to have constant memory requirements and be stack /// allocated. /// /// Terminology: /// - Sample: A data point, a value. /// - Sample window: The subset of all samples used for average calculations. pub trait SMA { /// Adds a sample to the series of samples. If the sample window is full, this will cause the /// oldest sample to be dropped, i.e. no longer contribute to the average. fn add_sample(&mut self, new_sample: Sample); /// Returns the simple moving average value of all the samples in the sample window. fn get_average(&self) -> Sample; /// Returns the total number of samples currently in the in the sample window. This value never /// exceeds the sample window size. fn get_num_samples(&self) -> usize; /// Returns the maximum number of samples that fit in the sample window. fn get_sample_window_size(&self) -> usize; // Returns an iterator over the samples currently in the sample window. fn get_sample_window_iter(&self) -> Iter; /// Returns the most recently added sample, if any. fn get_most_recent_sample(&self) -> Option; } simple_moving_average-1.0.2/src/sum_tree.rs000064400000000000000000000067411046102023000171560ustar 00000000000000use std::ops::Add; #[derive(Clone, Debug)] pub struct SumTree { // TODO: Convert this to an array and use it as SumTreeSMA's main data storage, once // https://github.com/rust-lang/rust/issues/76560 is stable nodes: Vec, } enum Position { Left, Right, } const ROOT_NODE_IDX: usize = 1; impl SumTree where Sample: Copy + Add, { pub fn get_root_sum(&self) -> Sample { self.nodes[ROOT_NODE_IDX] } pub fn get_leaf_node_sum(&self, leaf_node_idx: &usize) -> Sample { self.nodes[self.get_leaf_nodes_offset() + leaf_node_idx] } pub fn update_leaf_node_sample(&mut self, leaf_node_idx: usize, new_sample: Sample) { let node_idx = self.get_leaf_nodes_offset() + leaf_node_idx; *self.get_node_mut(node_idx) = new_sample; self.update_parent_recursive(node_idx, new_sample); } fn update_parent_recursive(&mut self, child_node_idx: usize, new_child_subtree_sum: Sample) { let node_idx = get_parent_node_idx(child_node_idx); let other_child_subtree_sum = match get_position(child_node_idx) { Position::Left => *self.get_node(get_right_child_node_idx(node_idx)), Position::Right => *self.get_node(get_left_child_node_idx(node_idx)), }; let node = self.get_node_mut(node_idx); let new_subtree_sum = new_child_subtree_sum + other_child_subtree_sum; *node = new_subtree_sum; if node_idx != ROOT_NODE_IDX { self.update_parent_recursive(node_idx, new_subtree_sum) } } fn get_node(&mut self, node_idx: usize) -> &Sample { self.get_node_mut(node_idx) } fn get_node_mut(&mut self, node_idx: usize) -> &mut Sample { &mut self.nodes[node_idx] } fn get_leaf_nodes_offset(&self) -> usize { self.nodes.len() / 2 } pub fn get_leaf_nodes(&self, num_nodes: usize) -> &[Sample] { let leaf_nodes_start = self.get_leaf_nodes_offset(); let leaf_nodes_end = leaf_nodes_start + num_nodes; &self.nodes[leaf_nodes_start..leaf_nodes_end] } } impl SumTree where Sample: Copy, { pub fn new(zero: Sample, num_leaf_nodes: usize) -> Self { // Let's create a perfect binary tree, large enough to accomodate all leaf nodes. // The extra nodes will contain only zeros, which is alright for our purposes. let num_leaf_nodes = 2 * num_leaf_nodes.checked_next_power_of_two().unwrap(); Self { nodes: vec![zero; num_leaf_nodes], } } } fn get_position(node_idx: usize) -> Position { if node_idx % 2 == 0 { Position::Left } else { Position::Right } } fn get_parent_node_idx(node_idx: usize) -> usize { node_idx / 2 } fn get_left_child_node_idx(node_idx: usize) -> usize { 2 * node_idx } fn get_right_child_node_idx(node_idx: usize) -> usize { 2 * node_idx + 1 } #[cfg(test)] mod tests { use super::*; #[test] fn basics() { let mut sum_tree = SumTree::new(0, 6); // Insert new nodes sum_tree.update_leaf_node_sample(0, 1); assert_eq!(sum_tree.get_root_sum(), 1); sum_tree.update_leaf_node_sample(1, 2); assert_eq!(sum_tree.get_root_sum(), 3); sum_tree.update_leaf_node_sample(2, 3); assert_eq!(sum_tree.get_root_sum(), 6); sum_tree.update_leaf_node_sample(3, 4); assert_eq!(sum_tree.get_root_sum(), 10); sum_tree.update_leaf_node_sample(4, 5); assert_eq!(sum_tree.get_root_sum(), 15); sum_tree.update_leaf_node_sample(5, 6); assert_eq!(sum_tree.get_root_sum(), 21); // Update exisitng nodes sum_tree.update_leaf_node_sample(0, 7); // 1 -> 7 assert_eq!(sum_tree.get_root_sum(), 27); sum_tree.update_leaf_node_sample(1, 8); // 2 -> 8 assert_eq!(sum_tree.get_root_sum(), 33); } } simple_moving_average-1.0.2/src/sum_tree_sma.rs000064400000000000000000000057411046102023000200150ustar 00000000000000use super::{sum_tree::SumTree, SMA}; use crate::{common::cast_to_divisor_type, ring_buffer::RingBuffer, Iter}; use num_traits::{FromPrimitive, Zero}; use std::{ marker::{self, PhantomData}, ops::{Add, Div}, }; type SumTreeNodeIdx = usize; /// An SMA implementation that caches the sum of all samples currently in the sample window as a /// tree of sums. #[derive(Clone, Debug)] pub struct SumTreeSMA { samples: RingBuffer, sum_tree: SumTree, _marker: marker::PhantomData, } impl SMA for SumTreeSMA where Sample: Copy + Add + Div, Divisor: FromPrimitive, { fn add_sample(&mut self, new_sample: Sample) { if WINDOW_SIZE == 0 { return; } let tree_node_idx = if self.samples.len() < WINDOW_SIZE { self.samples.len() } else { self.samples.pop_back().unwrap() }; self.samples.push_front(tree_node_idx); self.sum_tree .update_leaf_node_sample(tree_node_idx, new_sample); } fn get_average(&self) -> Sample { let num_samples = self.samples.len(); if num_samples == 0 { return self.sum_tree.get_root_sum(); } self.sum_tree.get_root_sum() / cast_to_divisor_type(num_samples) } fn get_most_recent_sample(&self) -> Option { self.samples .front() .map(|node_idx| self.sum_tree.get_leaf_node_sum(node_idx)) } fn get_num_samples(&self) -> usize { self.samples.len() } fn get_sample_window_size(&self) -> usize { WINDOW_SIZE } fn get_sample_window_iter(&self) -> Iter { let num_samples = self.get_num_samples(); Iter::new( self.sum_tree.get_leaf_nodes(num_samples), num_samples, num_samples, ) } } impl SumTreeSMA { /// Constructs a new [SumTreeSMA] with window size `WINDOW_SIZE`. This constructor is /// only available for `Sample` types that implement [num_traits::Zero]. If the `Sample` type /// does not, use the [from_zero](SumTreeSMA::from_zero) constructor instead. /// /// Note that the `Divisor` type usually cannot be derived by the compiler when using this /// constructor and must be explicitly stated, even if it is the same as the `Sample` type. pub fn new() -> Self { Self { samples: RingBuffer::new(0), sum_tree: SumTree::new(Sample::zero(), WINDOW_SIZE), _marker: PhantomData, } } } impl SumTreeSMA { /// Constructs a new [SumTreeSMA] with window size `WINDOW_SIZE` from the given /// `zero` sample. If the `Sample` type implements [num_traits::Zero], the /// [new](SumTreeSMA::new) constructor might be preferable to this. pub fn from_zero(zero: Sample) -> Self { Self { samples: RingBuffer::new(0), sum_tree: SumTree::new(zero, WINDOW_SIZE), _marker: PhantomData, } } } simple_moving_average-1.0.2/test_coverage.sh000075500000000000000000000006651046102023000173660ustar 00000000000000#!/bin/bash set -uexo pipefail rm -rf test_coverage tmp_test_data mkdir test_coverage export RUSTFLAGS="-Cinstrument-coverage" cargo build export LLVM_PROFILE_FILE="tmp_test_data/simple_moving_average-%p-%m.profraw" cargo test grcov \ ./tmp_test_data \ --source-dir . \ --binary-path ./target/debug/ \ --output-types html \ --branch \ --ignore-not-existing \ --output-path ./test_coverage/ ls tmp_test_data rm -rf tmp_test_data