Skip to main content

odem_rs_util/si/
stats.rs

1//! Enables compatibility between `uom` SI units and statistical collections.
2//!
3//! This module provides additional functionality that allows SI quantities
4//! from [`uom`] to be used with [`RandomVariable`] collections. It ensures that
5//! SI units can be formatted, displayed, and used in statistical analyses
6//! within ODEM-rs.
7//!
8//! ## Example Usage
9//!
10//! ```
11//! use odem_rs_util::{si::{time::*, UnitExt as _}, random_variable::RandomVariable};
12//!
13//! let mut rv = RandomVariable::new::<2>();
14//! rv.tabulate(second::new(3.0));
15//! rv.tabulate(second::new(5.0));
16//!
17//! let formatted = rv.display(minute); // display using SI unit of minute
18//! println!("{:?}", formatted);
19//! ```
20//!
21//! ## Notes
22//! - This functionality is enabled via the **"uom"** feature flag.
23//! - The display methods (`display`, `display_full`) allow formatting results
24//!   in different styles.
25//! - The `uom_compatible!` macro ensures compatibility across a wide range of
26//!   SI dimensions.
27
28use uom::{
29	Conversion,
30	fmt::DisplayStyle,
31	num_traits::{AsPrimitive, FromPrimitive, Num},
32	si::{self, Dimension, Quantity, Unit, Units, fmt::QuantityArguments},
33};
34
35use crate::random_variable::{RandomVariable, Sample};
36use core::{borrow::Borrow, fmt, marker::PhantomData};
37
38/// A displayable representation of a [`RandomVariable`] using SI units.
39///
40/// This struct allows statistical results to be formatted with appropriate
41/// units, either in an **abbreviated** (e.g., "5 m/s") or **descriptive**
42/// (e.g., "5 meters per second") style.
43///
44/// # Example
45///
46/// ```
47/// use odem_rs_util::{
48///     si::{velocity::*, UnitExt as _},
49///     random_variable::RandomVariable,
50/// };
51///
52/// let mut rv = RandomVariable::new::<2>();
53/// rv.tabulate(meter_per_second::new(12.0));
54///
55/// let formatted = rv.display(knot);
56/// println!("{:?}", formatted); // Outputs statistics in knots
57/// ```
58pub struct RandomQuantity<B, D, U, V, N, const M: usize>
59where
60	B: Borrow<RandomVariable<Quantity<D, U, V>, M>>,
61	D: Dimension + ?Sized,
62	U: Units<V> + ?Sized,
63	V: Num + Conversion<V>,
64	Quantity<D, U, V>: Sample,
65{
66	/// Reference to the random variable containing statistical samples.
67	rv: B,
68	/// The [`DisplayStyle`] used to format the unit.
69	style: DisplayStyle,
70	/// The unit type used for formatting.
71	unit: N,
72	/// Marker for the borrowed `Quantity`.
73	_marker: PhantomData<Quantity<D, U, V>>,
74}
75
76impl<B, D, U, V, N, const M: usize> RandomQuantity<B, D, U, V, N, M>
77where
78	B: Borrow<RandomVariable<Quantity<D, U, V>, M>>,
79	D: Dimension + ?Sized,
80	U: Units<V> + ?Sized,
81	V: Num + Conversion<V>,
82	Quantity<D, U, V>: Sample,
83{
84	const fn new(rv: B, style: DisplayStyle, unit: N) -> Self {
85		Self {
86			rv,
87			style,
88			unit,
89			_marker: PhantomData,
90		}
91	}
92
93	/// Extracts the inner [`Quantity`].
94	pub fn into_inner(self) -> B {
95		self.rv
96	}
97}
98
99/// A trait to indicate which [dimensions] a [unit] is compatible
100/// with.
101///
102/// Unfortunately, the `uom`-crate doesn't ship with a meta-function like this,
103/// so we have to improvise.
104///
105/// [dimensions]: Dimension
106/// [unit]: Unit
107pub trait Compatible<D: Dimension + ?Sized>: Unit {
108	/// Creates a statically type-checked and displayable object that can be
109	/// used for formatting [Quantities](Quantity).
110	fn format_args<U: Units<V> + ?Sized, V: Num + Conversion<V>>(
111		self,
112		value: Quantity<D, U, V>,
113		style: DisplayStyle,
114	) -> QuantityArguments<D, U, V, Self>;
115}
116
117// allow SI quantities to be used in RandomVariables
118impl<D, U, V> Sample for Quantity<D, U, V>
119where
120	D: Dimension + ?Sized,
121	U: Units<V> + ?Sized,
122	V: Conversion<V> + Num + AsPrimitive<f64> + FromPrimitive + PartialEq + PartialOrd,
123{
124	fn quantify(&self) -> f64 {
125		self.value.as_()
126	}
127
128	fn qualify(val: f64) -> Self {
129		Quantity {
130			dimension: PhantomData,
131			units: PhantomData,
132			value: V::from_f64(val).unwrap(),
133		}
134	}
135}
136
137// extend RandomVariables with display-methods for compatible SI quantities
138impl<D, U, V, const M: usize> RandomVariable<Quantity<D, U, V>, M>
139where
140	Quantity<D, U, V>: Sample,
141	D: Dimension + ?Sized,
142	U: Units<V> + ?Sized,
143	V: Conversion<V> + Num,
144{
145	/// Returns a borrowed [displayable](fmt::Debug) object that can be used
146	/// as a format argument and displays the computed statistical moments
147	/// with units of measurement in [abbreviated] form.
148	///
149	/// [abbreviated]: DisplayStyle::Abbreviation
150	pub fn display<N>(&self, unit: N) -> RandomQuantity<&'_ Self, D, U, V, N, M>
151	where
152		N: Compatible<D> + Conversion<V, T = <V as Conversion<V>>::T>,
153	{
154		RandomQuantity::new(self, DisplayStyle::Abbreviation, unit)
155	}
156
157	/// Returns an owning [displayable](fmt::Debug) object that can be used
158	/// as a format argument and displays the computed statistical moments
159	/// with units of measurement in [abbreviated] form.
160	///
161	/// [abbreviated]: DisplayStyle::Abbreviation
162	pub fn displayed<N>(self, unit: N) -> RandomQuantity<Self, D, U, V, N, M>
163	where
164		N: Compatible<D> + Conversion<V, T = <V as Conversion<V>>::T>,
165	{
166		RandomQuantity::new(self, DisplayStyle::Abbreviation, unit)
167	}
168
169	/// Returns a borrowed [displayable](fmt::Debug) object that can be used
170	/// as a format argument and displays the computed statistical moments
171	/// with units of measurement in [descriptive] form.
172	///
173	/// [descriptive]: DisplayStyle::Description
174	pub fn display_full<N>(&self, unit: N) -> RandomQuantity<&'_ Self, D, U, V, N, M>
175	where
176		N: Compatible<D> + Conversion<V, T = <V as Conversion<V>>::T>,
177	{
178		RandomQuantity::new(self, DisplayStyle::Description, unit)
179	}
180
181	/// Returns an owned [displayable](fmt::Debug) object that can be used
182	/// as a format argument and displays the computed statistical moments
183	/// with units of measurement in [descriptive] form.
184	///
185	/// [descriptive]: DisplayStyle::Description
186	pub fn displayed_full<N>(self, unit: N) -> RandomQuantity<Self, D, U, V, N, M>
187	where
188		N: Compatible<D> + Conversion<V, T = <V as Conversion<V>>::T>,
189	{
190		RandomQuantity::new(self, DisplayStyle::Description, unit)
191	}
192}
193
194/// Generates a `Debug` implementation for the `RandomQuantity` display helper.
195macro_rules! impl_debug_fmt {
196	($($n:tt),* $(,)?) => {$(
197		impl<B, D, U, V, N> fmt::Debug for RandomQuantity<B, D, U, V, N, $n>
198		where
199			Quantity<D, U, V>: Sample,
200			B: Borrow<RandomVariable<Quantity<D, U, V>, $n>>,
201			D: Dimension + ?Sized,
202			U: Units<V> + ?Sized,
203			V: Conversion<V> + Num + fmt::Debug,
204			N: Compatible<D> + Conversion<V, T = <V as Conversion<V>>::T>,
205		{
206			fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207				let mut dbs = f.debug_struct("RandomVariable");
208				let rv = self.rv.borrow();
209				let len = rv.count();
210
211				dbs.field("n", &len);
212
213				if len > 0 {
214					impl_debug_fmt!(@inner_impl self, rv, dbs, $n);
215				}
216
217				dbs.finish()
218			}
219		}
220	)*};
221
222	(@inner_impl $self:ident, $rv:ident, $debug:ident, 0) => {
223		$debug
224			.field("min", &$self.unit.format_args($rv.min().unwrap(), $self.style))
225			.field("max", &$self.unit.format_args($rv.max().unwrap(), $self.style));
226	};
227
228	(@inner_impl $self:ident, $rv:ident, $debug:ident, 1) => {
229		impl_debug_fmt!(@inner_impl $self, $rv, $debug, 0);
230		$debug.field(
231			"mean",
232			&$self.unit.format_args(Quantity::qualify($rv.mean()), $self.style)
233		);
234	};
235
236	(@inner_impl $self:ident, $rv:ident, $debug:ident, 2) => {
237		impl_debug_fmt!(@inner_impl $self, $rv, $debug, 1);
238		#[cfg(any(feature = "std", feature = "libm"))]
239		{
240			$debug.field(
241				"sdev",
242				&$self.unit.format_args(Quantity::qualify($rv.std_dev()), $self.style)
243			);
244		}
245		#[cfg(not(any(feature = "std", feature = "libm")))]
246		{
247			$debug.field("variance", &$rv.variance());
248		}
249	};
250
251	(@inner_impl $self:ident, $rv:ident, $debug:ident, 3) => {
252		impl_debug_fmt!(@inner_impl $self, $rv, $debug, 2);
253		$debug.field("skew", &$rv.skew());
254	};
255
256	(@inner_impl $self:ident, $rv:ident, $debug:ident, 4) => {
257		impl_debug_fmt!(@inner_impl $self, $rv, $debug, 3);
258		$debug.field("kurt", &$rv.kurtosis());
259	};
260}
261
262// display (dimension-) compatible RandomQuantities
263impl_debug_fmt!(0, 1, 2);
264
265#[cfg(any(feature = "std", feature = "libm"))]
266impl_debug_fmt!(3, 4);
267
268/// Extends `uom` SI quantities to be compatible with simulation statistics.
269///
270/// This macro ensures that SI dimensions can be properly formatted and
271/// displayed when used in statistical computations.
272macro_rules! uom_compatible {
273	($U:ident, $T:ident) => {
274		impl<T> Compatible<::uom::si::$U::Dimension> for T
275		where
276			T: si::$U::Unit,
277		{
278			fn format_args<U: si::Units<V> + ?Sized, V: Num + Conversion<V>>(
279				self,
280				value: si::$U::$T<U, V>,
281				style: DisplayStyle,
282			) -> QuantityArguments<si::$U::Dimension, U, V, Self> {
283				value.into_format_args(self, style)
284			}
285		}
286	};
287}
288
289// implement the glue trait for all si quantities
290super::uom_quantity!(uom_compatible!);