diff --git a/src/benchmark/measurement.rs b/src/benchmark/measurement.rs index 2a91811..16067a0 100644 --- a/src/benchmark/measurement.rs +++ b/src/benchmark/measurement.rs @@ -2,10 +2,8 @@ use std::process::ExitStatus; use serde::Serialize; -use crate::quantity::{ - max, mean, median, min, modified_zscores, serialize_information, serialize_time, - standard_deviation, Information, Time, -}; +use crate::quantity::statistics::{max, mean, median, min, modified_zscores, standard_deviation}; +use crate::quantity::{serialize_information, serialize_time, Information, Time}; use crate::util::exit_code::extract_exit_code; fn serialize_exit_status(exit_status: &ExitStatus, serializer: S) -> Result diff --git a/src/quantity/mod.rs b/src/quantity/mod.rs index eca4a96..4442705 100644 --- a/src/quantity/mod.rs +++ b/src/quantity/mod.rs @@ -1,13 +1,9 @@ use core::f64; use std::marker::PhantomData; -use std::ops::Add; -use std::ops::AddAssign; -use std::ops::Div; use serde::ser::SerializeStruct; use serde::Serializer; -use uom::num_traits; use uom::si; pub use si::f64::{Information, Ratio, Time}; @@ -18,6 +14,7 @@ pub use uom::num_traits::Zero; pub use units::{InformationUnit, IsUnit, TimeUnit}; +pub mod statistics; mod units; pub trait FormatQuantity { @@ -83,8 +80,12 @@ impl FormatQuantity for Information { fn suitable_unit(&self) -> InformationUnit { if *self < Information::new::(1.0) { InformationUnit::Byte - } else { + } else if *self < Information::new::(1.0) { InformationUnit::KibiByte + } else if *self < Information::new::(1.0) { + InformationUnit::MebiByte + } else { + InformationUnit::GibiByte } } @@ -132,113 +133,6 @@ where state.end() } -pub trait UnsafeRawValue { - fn unsafe_raw_value(&self) -> f64; - fn unsafe_from_raw_value(value: f64) -> Self; -} - -impl UnsafeRawValue for Time { - fn unsafe_raw_value(&self) -> f64 { - self.get::() - } - - fn unsafe_from_raw_value(value: f64) -> Self { - Time::new::(value) - } -} - -impl UnsafeRawValue for Information { - fn unsafe_raw_value(&self) -> f64 { - self.get::() - } - - fn unsafe_from_raw_value(value: f64) -> Self { - Information::new::(value) - } -} - -macro_rules! quantity_fn { - ($name:ident, $unwrapped_values:ident, $body:expr) => { - pub fn $name(values: &[Q]) -> Q { - let $unwrapped_values: Vec<_> = values.iter().map(|q| q.unsafe_raw_value()).collect(); - let result_value = $body; - - Q::unsafe_from_raw_value(result_value) - } - }; -} - -/// A max function that assumes no NaNs and at least one element -pub fn max(values: impl IntoIterator) -> V { - values - .into_iter() - .max_by(|a, b| a.partial_cmp(b).expect("No NaN values")) - .expect("'max' requires at least one element") -} - -/// A min function that assumes no NaNs and at least one element -pub fn min(values: impl IntoIterator) -> V { - values - .into_iter() - .min_by(|a, b| a.partial_cmp(b).expect("No NaN values")) - .expect("'min' requires at least one element") -} - -/// A mean function that assumes at least one element -pub fn mean(values: impl IntoIterator) -> Q -where - Q: AddAssign + num_traits::Zero + Div, - P: Into, -{ - let mut sum = Q::zero(); - let mut count = 0; - for value in values { - sum += value; - count += 1; - } - - let count = Ratio::new::(count as f64); - (sum / count).into() -} - -pub fn median(values: impl IntoIterator) -> Q -where - Q: Copy + PartialOrd + Add + Div, - P: Into, -{ - let mut values = values.into_iter().collect::>(); - values.sort_by(|a, b| a.partial_cmp(b).expect("No NaN values")); - - let len = values.len(); - if len % 2 == 0 { - let mid = len / 2; - let a = &values[mid - 1]; - let b = &values[mid]; - ((*a + *b) / Ratio::new::(2.)).into() - } else { - values[len / 2] - } -} - -quantity_fn!(standard_deviation, values, { - let mean_value = statistical::mean(&values); - statistical::standard_deviation(&values, Some(mean_value)) -}); - -pub fn modified_zscores(values: &[Q]) -> Vec { - let values: Vec<_> = values.iter().map(|q| q.unsafe_raw_value()).collect(); - crate::outlier_detection::modified_zscores(&values) -} - -#[test] -fn test_max() { - assert_eq!(1.0, max([1.0])); - assert_eq!(-1.0, max([-1.0])); - assert_eq!(-1.0, max([-2.0, -1.0])); - assert_eq!(1.0, max([-1.0, 1.0])); - assert_eq!(1.0, max([-1.0, 1.0, 0.0])); -} - #[test] fn test_time() { let time = Time::new::(123.4); @@ -266,32 +160,7 @@ fn test_information() { } #[test] -fn test_format() { - let time = Time::new::(123.4); - assert_eq!(time.format(TimeUnit::Second), "0.123 s"); - assert_eq!(time.format(TimeUnit::MilliSecond), "123.4 ms"); - assert_eq!(time.format(TimeUnit::MicroSecond), "123400.0 µs"); - - let peak_memory_usage = Information::new::(8.); - assert_eq!(peak_memory_usage.format(InformationUnit::Byte), "8192 B"); - assert_eq!( - peak_memory_usage.format(InformationUnit::KibiByte), - "8.0 KiB" - ); -} - -#[test] -fn test_mean() { - let values = vec![ - Time::new::(123.4), - Time::new::(234.5), - ]; - let result = mean(values.into_iter()); - assert_eq!(result.format(TimeUnit::MilliSecond), "178.9 ms"); -} - -#[test] -fn test_suiteable_unit() { +fn test_suiteable_unit_time() { assert_eq!(Time::new::(1.3).suitable_unit(), TimeUnit::Second); assert_eq!(Time::new::(1.0).suitable_unit(), TimeUnit::Second); assert_eq!( @@ -312,6 +181,41 @@ fn test_suiteable_unit() { ); } +#[test] +fn test_suitable_unit_information() { + assert_eq!( + Information::new::(512.0).suitable_unit(), + InformationUnit::Byte + ); + assert_eq!( + Information::new::(2048.0).suitable_unit(), + InformationUnit::KibiByte + ); + assert_eq!( + Information::new::(2_097_152.0).suitable_unit(), + InformationUnit::MebiByte + ); + assert_eq!( + Information::new::(2_147_483_648.0).suitable_unit(), + InformationUnit::GibiByte + ); +} + +#[test] +fn test_format() { + let time = Time::new::(123.4); + assert_eq!(time.format(TimeUnit::Second), "0.123 s"); + assert_eq!(time.format(TimeUnit::MilliSecond), "123.4 ms"); + assert_eq!(time.format(TimeUnit::MicroSecond), "123400.0 µs"); + + let peak_memory_usage = Information::new::(8.); + assert_eq!(peak_memory_usage.format(InformationUnit::Byte), "8192 B"); + assert_eq!( + peak_memory_usage.format(InformationUnit::KibiByte), + "8.0 KiB" + ); +} + #[test] fn test_format_duration_unit_with_unit() { let out = Time::new::(1.3).format(TimeUnit::Second); @@ -323,34 +227,3 @@ fn test_format_duration_unit_with_unit() { let out = Time::new::(1.3).format(TimeUnit::MicroSecond); assert_eq!("1300000.0 µs", out); } - -#[test] -fn statistics() { - let values = vec![ - Time::new::(1.0), - Time::new::(2.0), - Time::new::(3.0), - ]; - - assert_eq!( - mean(values.iter().copied()).format(TimeUnit::Second), - "2.000 s" - ); - assert_eq!( - median(values.iter().copied()).format(TimeUnit::Second), - "2.000 s" - ); - assert_eq!(min(&values).format(TimeUnit::Second), "1.000 s"); - assert_eq!(max(&values).format(TimeUnit::Second), "3.000 s"); - assert_eq!( - standard_deviation(&values).format(TimeUnit::Second), - "1.000 s" - ); - - let values = vec![ - Information::new::(1.0), - Information::new::(2.0), - Information::new::(3.0), - ]; - mean(values.iter().copied()).format(InformationUnit::Byte); -} diff --git a/src/quantity/statistics.rs b/src/quantity/statistics.rs index e9c0c62..3245e23 100644 --- a/src/quantity/statistics.rs +++ b/src/quantity/statistics.rs @@ -8,64 +8,6 @@ use uom::si::ratio::ratio; use super::{byte, second, Information, Time}; -/// A min function that assumes no NaNs and at least one element -pub fn min(values: impl IntoIterator) -> Q { - values - .into_iter() - .min_by(|a, b| a.partial_cmp(b).expect("No NaN values")) - .expect("'min' requires at least one element") -} - -/// A max function that assumes no NaNs and at least one element -pub fn max(values: impl IntoIterator) -> Q { - values - .into_iter() - .max_by(|a, b| a.partial_cmp(b).expect("No NaN values")) - .expect("'max' requires at least one element") -} - -/// A mean function that assumes at least one element -pub fn mean(values: impl IntoIterator) -> Q -where - Q: AddAssign + num_traits::Zero + Div, - P: Into, -{ - let mut sum = Q::zero(); - let mut count = 0; - for value in values { - sum += value; - count += 1; - } - - let count = Ratio::new::(count as f64); - (sum / count).into() -} - -pub fn median(values: impl IntoIterator) -> Q -where - Q: Copy + PartialOrd + Add + Div, - P: Into, -{ - let mut values = values.into_iter().collect::>(); - assert!( - !values.is_empty(), - "median requires at least one measurement" - ); - values.sort_by(|a, b| a.partial_cmp(b).expect("No NaN values")); - - let len = values.len(); - if len % 2 == 0 { - let mid = len / 2; - let a = &values[mid - 1]; - let b = &values[mid]; - ((*a + *b) / Ratio::new::(2.)).into() - } else { - values[len / 2] - } -} - -/// This is just sad, but after several hours trying to figure out how to write -/// a generic `standard_deviation` function using `uom` quantities, I gave up. pub trait UnsafeRawValue { fn unsafe_raw_value(&self) -> f64; fn unsafe_from_raw_value(value: f64) -> Self; @@ -91,140 +33,123 @@ impl UnsafeRawValue for Information { } } -fn standard_deviation_f64(values: &[f64]) -> f64 { - let mean_value = mean(values.iter().copied()); +/// A max function that assumes no NaNs and at least one element +pub fn max(values: impl IntoIterator) -> Q { + values + .into_iter() + .max_by(|a, b| a.partial_cmp(b).expect("No NaN values")) + .expect("'max' requires at least one element") +} - let mut squared_deviations = 0.; - let mut n = 0; +/// A min function that assumes no NaNs and at least one element +pub fn min(values: impl IntoIterator) -> Q { + values + .into_iter() + .min_by(|a, b| a.partial_cmp(b).expect("No NaN values")) + .expect("'min' requires at least one element") +} + +/// A mean function that assumes at least one element +pub fn mean(values: impl IntoIterator) -> Q +where + Q: AddAssign + num_traits::Zero + Div, + P: Into, +{ + let mut sum = Q::zero(); + let mut count = 0; for value in values { - let deviation = value - mean_value; - squared_deviations += deviation * deviation; - n += 1; + sum += value; + count += 1; } - (1. / ((n - 1) as f64) * squared_deviations).sqrt() + let count = Ratio::new::(count as f64); + (sum / count).into() } -pub fn standard_deviation(values: impl IntoIterator + Clone) -> Q { - let values: Vec<_> = values.into_iter().map(|q| q.unsafe_raw_value()).collect(); - let result = standard_deviation_f64(&values); - Q::unsafe_from_raw_value(result) +pub fn median(values: impl IntoIterator) -> Q +where + Q: Copy + PartialOrd + Add + Div, + P: Into, +{ + let mut values = values.into_iter().collect::>(); + values.sort_by(|a, b| a.partial_cmp(b).expect("No NaN values")); + + let len = values.len(); + if len % 2 == 0 { + let mid = len / 2; + let a = &values[mid - 1]; + let b = &values[mid]; + ((*a + *b) / Ratio::new::(2.)).into() + } else { + values[len / 2] + } } -/// Compute modified Z-scores for a given sample. A (unmodified) Z-score is defined by -/// `(x_i - x_mean)/x_stddev` whereas the modified Z-score is defined by `(x_i - x_median)/MAD` -/// where MAD is the median absolute deviation. -/// -/// References: -/// - -pub fn modified_zscores_f64(xs: &[f64]) -> Vec { - assert!(!xs.is_empty()); - - // Compute sample median: - let x_median = median(xs.iter().copied()); - - // Compute the absolute deviations from the median: - let deviations: Vec = xs.iter().map(|x| (x - x_median).abs()).collect(); - - // Compute median absolute deviation: - let mad = median(deviations.iter().copied()); - - // Handle MAD == 0 case - let mad = if mad > 0.0 { mad } else { f64::EPSILON }; - - // Compute modified Z-scores (x_i - x_median) / MAD - xs.iter().map(|&x| (x - x_median) / mad).collect() +pub fn standard_deviation(values: &[Q]) -> Q { + let values: Vec<_> = values.iter().map(|q| q.unsafe_raw_value()).collect(); + let result_value = { + let mean_value = statistical::mean(&values); + statistical::standard_deviation(&values, Some(mean_value)) + }; + Q::unsafe_from_raw_value(result_value) } pub fn modified_zscores(values: &[Q]) -> Vec { let values: Vec<_> = values.iter().map(|q| q.unsafe_raw_value()).collect(); - modified_zscores_f64(&values) + crate::outlier_detection::modified_zscores(&values) } -#[cfg(test)] -mod tests { - use super::*; - use approx::assert_relative_eq; - use uom::si::information::kibibyte; - use uom::si::time::{microsecond, millisecond}; - - #[test] - fn test_min() { - assert_eq!(1.0, min([1.0])); - assert_eq!(-1.0, min([-1.0])); - assert_eq!(-2.0, min([-2.0, -1.0])); - assert_eq!(-1.0, min([-1.0, 1.0])); - assert_eq!(-1.0, min([1.0, -1.0, 0.0])); - - let values = vec![ - Information::new::(1.0), - Information::new::(2.0), - Information::new::(3.0), - ]; - assert_eq!(min(&values).get::(), 2.0); - } - - #[test] - fn test_max() { - assert_eq!(1.0, max([1.0])); - assert_eq!(-1.0, max([-1.0])); - assert_eq!(-1.0, max([-2.0, -1.0])); - assert_eq!(1.0, max([-1.0, 1.0])); - assert_eq!(1.0, max([-1.0, 1.0, 0.0])); - - let values = vec![ - Information::new::(1.0), - Information::new::(2.0), - Information::new::(3.0), - ]; - assert_eq!(max(&values).get::(), 2.0); - } - - #[test] - fn test_mean() { - assert_eq!(1.0, mean([1.0])); - assert_relative_eq!(2.0, mean([1.0, 3.0])); - - let values = [ - Time::new::(100.0), - Time::new::(200.0), - Time::new::(600_000.0), - ]; - let result = mean(values); - assert_relative_eq!(result.get::(), 300.0); - } - - #[test] - fn test_median() { - assert_eq!(1.0, median([1.0])); - assert_relative_eq!(2.0, median([1.0, 3.0])); - - let values = [ - Time::new::(100.0), - Time::new::(200.0), - Time::new::(600_000.0), - ]; - let result = median(values); - assert_relative_eq!(result.get::(), 200.0); - - let values = [ - Time::new::(100.0), - Time::new::(200.0), - Time::new::(300_000.0), - Time::new::(600_000.0), - ]; - let result = median(values); - assert_relative_eq!(result.get::(), 250.0); - } - - #[test] - fn test_standard_deviation() { - let values = [ - Time::new::(100.0), - Time::new::(200.0), - Time::new::(300_000.0), - ]; - let result = standard_deviation(values); - assert_relative_eq!(result.get::(), 100.0); - } +#[test] +fn test_max() { + assert_eq!(1.0, max([1.0])); + assert_eq!(-1.0, max([-1.0])); + assert_eq!(-1.0, max([-2.0, -1.0])); + assert_eq!(1.0, max([-1.0, 1.0])); + assert_eq!(1.0, max([-1.0, 1.0, 0.0])); +} + +#[test] +fn test_mean() { + use crate::quantity::{FormatQuantity, TimeUnit}; + use uom::si::time::millisecond; + + let values = vec![ + Time::new::(123.4), + Time::new::(234.5), + ]; + let result = mean(values.into_iter()); + assert_eq!(result.format(TimeUnit::MilliSecond), "178.9 ms"); +} + +#[test] +fn statistics() { + use crate::quantity::{FormatQuantity, InformationUnit, TimeUnit}; + + let values = vec![ + Time::new::(1.0), + Time::new::(2.0), + Time::new::(3.0), + ]; + + assert_eq!( + mean(values.iter().copied()).format(TimeUnit::Second), + "2.000 s" + ); + assert_eq!( + median(values.iter().copied()).format(TimeUnit::Second), + "2.000 s" + ); + assert_eq!(min(&values).format(TimeUnit::Second), "1.000 s"); + assert_eq!(max(&values).format(TimeUnit::Second), "3.000 s"); + assert_eq!( + standard_deviation(&values).format(TimeUnit::Second), + "1.000 s" + ); + + let values = vec![ + Information::new::(1.0), + Information::new::(2.0), + Information::new::(3.0), + ]; + mean(values.iter().copied()).format(InformationUnit::Byte); }