Coverage Report

Created: 2026-11-03 08:22

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