ndhistogram/axis/
uniformcyclic.rs

1use crate::error::AxisError;
2
3use super::{Axis, BinInterval, UniformNoFlow};
4use std::fmt::{Debug, Display};
5
6use num_traits::{Float, Num, NumCast, NumOps};
7
8/// A wrap-around axis with equal-sized bins.
9///
10/// An axis with `N` equally-spaced, equal-sized bins, in `[low, high)`.
11/// Entries outside this interval get wrapped around.
12/// There are no overflow bins so this axis has exactly `N` bins.
13///
14/// # Examples
15/// 1D histogram with 4 bins distributed around a circle.
16/// ```
17/// use ndhistogram::{ndhistogram, Histogram};
18/// use ndhistogram::axis::{Axis, BinInterval, UniformCyclic};
19/// # fn main() -> Result<(), ndhistogram::Error> {
20/// let mut hist = ndhistogram!(UniformCyclic::new(4, 0.0, 360.0)?);
21/// hist.fill(& 45.0         ); // Add entry at 45 degrees
22/// hist.fill(&(45.0 + 360.0)); // Add entry at 45 degrees + one whole turn
23/// hist.fill(&(45.0 - 360.0)); // Add entry at 45 degrees + one whole turn backwards
24/// // All 3 above entries end up in the same bin
25/// assert_eq!(hist.value(&45.0), Some(&3.0));
26/// // Lookup also wraps around
27/// assert_eq!(hist.value(&(45.0 + 360.0)), Some(&3.0));
28/// assert_eq!(hist.value(&(45.0 - 360.0)), Some(&3.0));
29/// # Ok(()) }
30/// ```
31/// Time of day
32/// ```
33/// use ndhistogram::{ndhistogram, Histogram};
34/// use ndhistogram::axis::{Axis, BinInterval, UniformCyclic};
35///
36/// # fn main() -> Result<(), ndhistogram::Error> {
37/// let bins_per_day = 24;
38/// let hours_per_bin = 1;
39/// let start_at_zero = 0;
40/// let four_pm = 16;
41/// let mut hist = ndhistogram!(UniformCyclic::with_step_size(
42///     bins_per_day, start_at_zero, hours_per_bin
43/// )?);
44/// hist.fill(&40);                               // The 40th hour of the week ...
45/// assert_eq!(hist.value(&four_pm), Some(&1.0)); // ... is at 4 pm.
46/// # Ok(()) }
47/// ````
48#[derive(Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
49#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
50pub struct UniformCyclic<T = f64> {
51    #[cfg_attr(feature = "serde", serde(flatten))]
52    axis: UniformNoFlow<T>,
53}
54
55impl<T> UniformCyclic<T>
56where
57    T: PartialOrd + Num + NumCast + NumOps + Copy,
58{
59    /// Create a wrap-around axis with `nbins` uniformly-spaced bins in the range `[low, high)`.
60    ///
61    /// Only implemented for [Float]. Use [UniformCyclic::with_step_size] for integers.
62    ///
63    /// For floating point types, infinities and NaN do not map to any bin.
64    ///
65    /// The parameters have the same constraints as [UniformNoFlow::new], otherwise an error in returned.
66    pub fn new(nbins: usize, low: T, high: T) -> Result<Self, AxisError>
67    where
68        T: Float,
69    {
70        Ok(Self {
71            axis: UniformNoFlow::new(nbins, low, high)?,
72        })
73    }
74
75    /// Create a wrap-around axis with `nbins` uniformly-spaced bins in the range `[low, low+num*step)`.
76    /// The parameters have the same constraints as [UniformNoFlow::new], otherwise an error is returned.
77    pub fn with_step_size(nbins: usize, low: T, step: T) -> Result<Self, AxisError> {
78        Ok(Self {
79            axis: UniformNoFlow::with_step_size(nbins, low, step)?,
80        })
81    }
82}
83
84impl<T> UniformCyclic<T> {
85    /// Low edge of axis (excluding wrap-around)
86    #[inline]
87    pub fn low(&self) -> &T {
88        self.axis.low()
89    }
90    /// High edge of axis (excluding wrap-around)
91    #[inline]
92    pub fn high(&self) -> &T {
93        self.axis.high()
94    }
95}
96
97impl<T: PartialOrd + Num + NumCast + NumOps + Copy> Axis for UniformCyclic<T> {
98    type Coordinate = T;
99    type BinInterval = BinInterval<T>;
100
101    #[inline]
102    fn index(&self, coordinate: &Self::Coordinate) -> Option<usize> {
103        let (mut x, hi, lo) = (*coordinate, *self.axis.high(), *self.axis.low());
104        let range = hi - lo;
105        x = (x - lo) % range;
106        if x < T::zero() {
107            x = range + x;
108        }
109        x = x + lo;
110        self.axis.index(&x)
111    }
112
113    #[inline]
114    fn num_bins(&self) -> usize {
115        self.axis.num_bins()
116    }
117
118    #[inline]
119    fn bin(&self, index: usize) -> Option<<Self as Axis>::BinInterval> {
120        self.axis.bin(index)
121    }
122}
123
124impl<T> Display for UniformCyclic<T>
125where
126    T: PartialOrd + NumCast + NumOps + Copy + Display,
127{
128    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129        write!(
130            f,
131            "Axis{{# bins={}, range=[{}, {}), class={}}}",
132            self.axis.num_bins(),
133            self.axis.low(),
134            self.axis.high(),
135            stringify!(UniformCyclic)
136        )
137    }
138}
139
140impl<'a, T> IntoIterator for &'a UniformCyclic<T>
141where
142    UniformCyclic<T>: Axis,
143{
144    type Item = (usize, <UniformCyclic<T> as Axis>::BinInterval);
145    type IntoIter = Box<dyn Iterator<Item = Self::Item> + 'a>;
146    fn into_iter(self) -> Self::IntoIter {
147        self.iter()
148    }
149}