ndarray_histogram/histogram/grid.rs
1use super::{bins::Bins, errors::BinsBuildError, strategies::BinsBuildingStrategy};
2use itertools::izip;
3use ndarray::{ArrayBase, Axis, Data, Ix1, Ix2};
4use std::ops::Range;
5
6/// An orthogonal partition of a rectangular region in an *n*-dimensional space, e.g.
7/// [*a*<sub>0</sub>, *b*<sub>0</sub>) × ⋯ × [*a*<sub>*n*−1</sub>, *b*<sub>*n*−1</sub>),
8/// represented as a collection of rectangular *n*-dimensional bins.
9///
10/// The grid is **solely determined by the Cartesian product of its projections** on each coordinate
11/// axis. Therefore, each element in the product set should correspond to a sub-region in the grid.
12///
13/// For example, this partition can be represented as a `Grid` struct:
14///
15/// ```text
16///
17/// g +---+-------+---+
18/// | 3 | 4 | 5 |
19/// f +---+-------+---+
20/// | | | |
21/// | 0 | 1 | 2 |
22/// | | | |
23/// e +---+-------+---+
24/// a b c d
25///
26/// R0: [a, b) × [e, f)
27/// R1: [b, c) × [e, f)
28/// R2: [c, d) × [e, f)
29/// R3: [a, b) × [f, g)
30/// R4: [b, d) × [f, g)
31/// R5: [c, d) × [f, g)
32/// Grid: { [a, b), [b, c), [c, d) } × { [e, f), [f, g) } == { R0, R1, R2, R3, R4, R5 }
33/// ```
34///
35/// while the next one can't:
36///
37/// ```text
38/// g +---+-----+---+
39/// | | 2 | 3 |
40/// (f) | +-----+---+
41/// | 0 | |
42/// | | 1 |
43/// | | |
44/// e +---+-----+---+
45/// a b c d
46///
47/// R0: [a, b) × [e, g)
48/// R1: [b, d) × [e, f)
49/// R2: [b, c) × [f, g)
50/// R3: [c, d) × [f, g)
51/// // 'f', as long as 'R1', 'R2', or 'R3', doesn't appear on LHS
52/// // [b, c) × [e, g), [c, d) × [e, g) doesn't appear on RHS
53/// Grid: { [a, b), [b, c), [c, d) } × { [e, g) } != { R0, R1, R2, R3 }
54/// ```
55///
56/// # Examples
57///
58/// Basic usage, building a `Grid` via [`GridBuilder`], with optimal grid layout determined by
59/// a given [`strategy`], and generating a [`histogram`]:
60///
61/// ```
62/// use ndarray::{Array, array};
63/// use ndarray_histogram::{
64/// HistogramExt,
65/// histogram::{Bins, Edges, Grid, GridBuilder, strategies::Auto},
66/// };
67///
68/// // 1-dimensional observations, as a (n_observations, n_dimension) 2-d matrix
69/// let observations =
70/// Array::from_shape_vec((12, 1), vec![1, 4, 5, 2, 100, 20, 50, 65, 27, 40, 45, 23]).unwrap();
71///
72/// // The optimal grid layout is inferred from the data, given a chosen strategy, Auto in this case
73/// let grid = GridBuilder::<Auto<usize>>::from_array(&observations)
74/// .unwrap()
75/// .build();
76///
77/// let histogram = observations.histogram(grid);
78///
79/// let histogram_matrix = histogram.counts();
80/// // Bins are left-closed, right-open!
81/// let expected = array![4, 3, 3, 1, 0, 1];
82/// assert_eq!(histogram_matrix, expected.into_dyn());
83/// ```
84///
85/// [`histogram`]: trait.HistogramExt.html
86/// [`GridBuilder`]: struct.GridBuilder.html
87/// [`strategy`]: strategies/index.html
88#[derive(Clone, Debug, Eq, PartialEq)]
89pub struct Grid<A: Ord + Send> {
90 projections: Vec<Bins<A>>,
91}
92
93impl<A: Ord + Send> From<Vec<Bins<A>>> for Grid<A> {
94 /// Converts a `Vec<Bins<A>>` into a `Grid<A>`, consuming the vector of bins.
95 ///
96 /// The `i`-th element in `Vec<Bins<A>>` represents the projection of the bin grid onto the
97 /// `i`-th axis.
98 ///
99 /// Alternatively, a `Grid` can be built directly from data using a [`GridBuilder`].
100 ///
101 /// [`GridBuilder`]: struct.GridBuilder.html
102 fn from(projections: Vec<Bins<A>>) -> Self {
103 Grid { projections }
104 }
105}
106
107impl<A: Ord + Send> Grid<A> {
108 /// Returns the number of dimensions of the region partitioned by the grid.
109 ///
110 /// # Examples
111 ///
112 /// ```
113 /// use ndarray_histogram::histogram::{Bins, Edges, Grid};
114 ///
115 /// let edges = Edges::from(vec![0, 1]);
116 /// let bins = Bins::new(edges);
117 /// let square_grid = Grid::from(vec![bins.clone(), bins.clone()]);
118 ///
119 /// assert_eq!(square_grid.ndim(), 2usize)
120 /// ```
121 #[must_use]
122 pub fn ndim(&self) -> usize {
123 self.projections.len()
124 }
125
126 /// Returns the numbers of bins along each coordinate axis.
127 ///
128 /// # Examples
129 ///
130 /// ```
131 /// use ndarray_histogram::histogram::{Bins, Edges, Grid};
132 ///
133 /// let edges_x = Edges::from(vec![0, 1]);
134 /// let edges_y = Edges::from(vec![-1, 0, 1]);
135 /// let bins_x = Bins::new(edges_x);
136 /// let bins_y = Bins::new(edges_y);
137 /// let square_grid = Grid::from(vec![bins_x, bins_y]);
138 ///
139 /// assert_eq!(square_grid.shape(), vec![1usize, 2usize]);
140 /// ```
141 #[must_use]
142 pub fn shape(&self) -> Vec<usize> {
143 self.projections.iter().map(Bins::len).collect()
144 }
145
146 /// Returns the grid projections on each coordinate axis as a slice of immutable references.
147 #[must_use]
148 pub fn projections(&self) -> &[Bins<A>] {
149 &self.projections
150 }
151
152 /// Returns an `n-dimensional` index, of bins along each axis that contains the point, if one
153 /// exists.
154 ///
155 /// Returns `None` if the point is outside the grid.
156 ///
157 /// # Panics
158 ///
159 /// Panics if dimensionality of the point doesn't equal the grid's.
160 ///
161 /// # Examples
162 ///
163 /// Basic usage:
164 ///
165 /// ```
166 /// use ndarray::array;
167 /// use ndarray_histogram::{
168 /// histogram::{Bins, Edges, Grid},
169 /// o64,
170 /// };
171 ///
172 /// let edges = Edges::from(vec![o64(-1.), o64(0.), o64(1.)]);
173 /// let bins = Bins::new(edges);
174 /// let square_grid = Grid::from(vec![bins.clone(), bins.clone()]);
175 ///
176 /// // (0., -0.7) falls in 1st and 0th bin respectively
177 /// assert_eq!(
178 /// square_grid.index_of(&array![o64(0.), o64(-0.7)]),
179 /// Some(vec![1, 0]),
180 /// );
181 /// // Returns `None`, as `1.` is outside the grid since bins are right-open
182 /// assert_eq!(square_grid.index_of(&array![o64(0.), o64(1.)]), None,);
183 /// ```
184 ///
185 /// A panic upon dimensionality mismatch:
186 ///
187 /// ```should_panic
188 /// # use ndarray::array;
189 /// # use ndarray_histogram::{histogram::{Edges, Bins, Grid}, o64};
190 /// # let edges = Edges::from(vec![o64(-1.), o64(0.), o64(1.)]);
191 /// # let bins = Bins::new(edges);
192 /// # let square_grid = Grid::from(vec![bins.clone(), bins.clone()]);
193 /// // the point has 3 dimensions, the grid expected 2 dimensions
194 /// assert_eq!(
195 /// square_grid.index_of(&array![o64(0.), o64(-0.7), o64(0.5)]),
196 /// Some(vec![1, 0, 1]),
197 /// );
198 /// ```
199 pub fn index_of<S>(&self, point: &ArrayBase<S, Ix1>) -> Option<Vec<usize>>
200 where
201 S: Data<Elem = A>,
202 {
203 assert_eq!(
204 point.len(),
205 self.ndim(),
206 "Dimension mismatch: the point has {:?} dimensions, the grid \
207 expected {:?} dimensions.",
208 point.len(),
209 self.ndim()
210 );
211 point
212 .iter()
213 .zip(self.projections.iter())
214 .map(|(v, e)| e.index_of(v))
215 .collect()
216 }
217}
218
219impl<A: Ord + Send + Clone> Grid<A> {
220 /// Given an `n`-dimensional index, `i = (i_0, ..., i_{n-1})`, returns an `n`-dimensional bin,
221 /// `I_{i_0} x ... x I_{i_{n-1}}`, where `I_{i_j}` is the `i_j`-th interval on the `j`-th
222 /// projection of the grid on the coordinate axes.
223 ///
224 /// # Panics
225 ///
226 /// Panics if at least one in the index, `(i_0, ..., i_{n-1})`, is out of bounds on the
227 /// corresponding coordinate axis, i.e. if there exists `j` s.t.
228 /// `i_j >= self.projections[j].len()`.
229 ///
230 /// # Examples
231 ///
232 /// Basic usage:
233 ///
234 /// ```
235 /// use ndarray::array;
236 /// use ndarray_histogram::histogram::{Bins, Edges, Grid};
237 ///
238 /// let edges_x = Edges::from(vec![0, 1]);
239 /// let edges_y = Edges::from(vec![2, 3, 4]);
240 /// let bins_x = Bins::new(edges_x);
241 /// let bins_y = Bins::new(edges_y);
242 /// let square_grid = Grid::from(vec![bins_x, bins_y]);
243 ///
244 /// // Query the 0-th bin on x-axis, and 1-st bin on y-axis
245 /// assert_eq!(square_grid.index(&[0, 1]), vec![0..1, 3..4],);
246 /// ```
247 ///
248 /// A panic upon out-of-bounds:
249 ///
250 /// ```should_panic
251 /// # use ndarray::array;
252 /// # use ndarray_histogram::histogram::{Edges, Bins, Grid};
253 /// # let edges_x = Edges::from(vec![0, 1]);
254 /// # let edges_y = Edges::from(vec![2, 3, 4]);
255 /// # let bins_x = Bins::new(edges_x);
256 /// # let bins_y = Bins::new(edges_y);
257 /// # let square_grid = Grid::from(vec![bins_x, bins_y]);
258 /// // out-of-bound on y-axis
259 /// assert_eq!(square_grid.index(&[0, 2]), vec![0..1, 3..4],);
260 /// ```
261 #[must_use]
262 pub fn index(&self, index: &[usize]) -> Vec<Range<A>> {
263 assert_eq!(
264 index.len(),
265 self.ndim(),
266 "Dimension mismatch: the index has {0:?} dimensions, the grid \
267 expected {1:?} dimensions.",
268 index.len(),
269 self.ndim()
270 );
271 izip!(&self.projections, index)
272 .map(|(bins, &i)| bins.index(i))
273 .collect()
274 }
275}
276
277/// A builder used to create [`Grid`] instances for [`histogram`] computations.
278///
279/// # Examples
280///
281/// Basic usage, creating a `Grid` with some observations and a given [`strategy`]:
282///
283/// ```
284/// use ndarray::Array;
285/// use ndarray_histogram::histogram::{Bins, Edges, Grid, GridBuilder, strategies::Auto};
286///
287/// // 1-dimensional observations, as a (n_observations, n_dimension) 2-d matrix
288/// let observations =
289/// Array::from_shape_vec((12, 1), vec![1, 4, 5, 2, 100, 20, 50, 65, 27, 40, 45, 23]).unwrap();
290///
291/// // The optimal grid layout is inferred from the data, given a chosen strategy, Auto in this case
292/// let grid = GridBuilder::<Auto<usize>>::from_array(&observations)
293/// .unwrap()
294/// .build();
295/// // Equivalently, build a Grid directly
296/// let expected_grid = Grid::from(vec![Bins::new(Edges::from(vec![
297/// 1, 20, 39, 58, 77, 96, 115,
298/// ]))]);
299///
300/// assert_eq!(grid, expected_grid);
301/// ```
302///
303/// [`Grid`]: struct.Grid.html
304/// [`histogram`]: trait.HistogramExt.html
305/// [`strategy`]: strategies/index.html
306#[allow(clippy::module_name_repetitions)]
307pub struct GridBuilder<B: BinsBuildingStrategy> {
308 bin_builders: Vec<B>,
309}
310
311impl<A, B> GridBuilder<B>
312where
313 A: Ord + Send,
314 B: BinsBuildingStrategy<Elem = A>,
315{
316 /// Returns a `GridBuilder` for building a [`Grid`] with a given [`strategy`] and some
317 /// observations in a 2-dimensionalarray with shape `(n_observations, n_dimension)`.
318 ///
319 /// # Errors
320 ///
321 /// It returns [`BinsBuildError`] if it is not possible to build a [`Grid`] given
322 /// the observed data according to the chosen [`strategy`].
323 ///
324 /// # Examples
325 ///
326 /// See [Trait-level examples] for basic usage.
327 ///
328 /// [`Grid`]: struct.Grid.html
329 /// [`strategy`]: strategies/index.html
330 /// [`BinsBuildError`]: errors/enum.BinsBuildError.html
331 /// [Trait-level examples]: struct.GridBuilder.html#examples
332 pub fn from_array<S>(array: &ArrayBase<S, Ix2>) -> Result<Self, BinsBuildError>
333 where
334 S: Data<Elem = A>,
335 {
336 let bin_builders = array
337 .axis_iter(Axis(1))
338 .map(|data| B::from_array(&data))
339 .collect::<Result<Vec<B>, BinsBuildError>>()?;
340 Ok(Self { bin_builders })
341 }
342
343 /// Returns a [`Grid`] instance, with building parameters infered in [`from_array`], according
344 /// to the specified [`strategy`] and observations provided.
345 ///
346 /// # Examples
347 ///
348 /// See [Trait-level examples] for basic usage.
349 ///
350 /// [`Grid`]: struct.Grid.html
351 /// [`strategy`]: strategies/index.html
352 /// [`from_array`]: #method.from_array.html
353 #[must_use]
354 pub fn build(&self) -> Grid<A> {
355 let projections: Vec<_> = self
356 .bin_builders
357 .iter()
358 .map(BinsBuildingStrategy::build)
359 .collect();
360 Grid::from(projections)
361 }
362}