ndslice/
utils.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9pub mod stencil {
10    /// Generates the von Neumann stencil for grids of arbitrary
11    /// dimensionality.
12    ///
13    /// The von Neumann neighborhood consists of all neighbors offset by
14    /// ±1 along a single axis, with all other coordinates unchanged. In
15    /// `n` dimensions, this yields `2 * n` neighbors.
16    ///
17    /// For example, in 3D, this returns the offsets:
18    /// ```text
19    /// [-1, 0, 0], [1, 0, 0],
20    /// [0, -1, 0], [0, 1, 0],
21    /// [0, 0, -1], [0, 0, 1]
22    /// ```
23    pub fn von_neumann_neighbors<const N: usize>() -> Vec<[isize; N]> {
24        let mut offsets = Vec::with_capacity(2 * N);
25
26        for axis in 0..N {
27            let mut offset_pos = [0; N];
28            offset_pos[axis] = 1;
29            offsets.push(offset_pos);
30
31            let mut offset_neg = [0; N];
32            offset_neg[axis] = -1;
33            offsets.push(offset_neg);
34        }
35
36        offsets
37    }
38
39    /// Generates the Moore stencil for grids of arbitrary
40    /// dimensionality.
41    ///
42    /// The Moore neighborhood consists of all neighbors where each
43    /// coordinate offset is in {-1, 0, 1}. In `N` dimensions, this
44    /// yields `3^N - 1` neighbors (excluding the center point).
45    ///
46    /// For example, in 3D, this returns offsets like:
47    /// ```text
48    /// [-1, -1, -1], [-1, -1, 0], ..., [1, 1, 1] (excluding [0, 0, 0])
49    /// ```
50    pub fn moore_neighbors<const N: usize>() -> Vec<[isize; N]> {
51        let mut offsets = Vec::new();
52        let mut prefix = [0isize; N];
53
54        fn build<const N: usize>(
55            index: usize,
56            prefix: &mut [isize; N],
57            offsets: &mut Vec<[isize; N]>,
58        ) {
59            if index == N {
60                if prefix.iter().any(|&x| x != 0) {
61                    offsets.push(*prefix);
62                }
63                return;
64            }
65
66            for delta in -1..=1 {
67                prefix[index] = delta;
68                build::<N>(index + 1, prefix, offsets);
69            }
70        }
71
72        build::<N>(0, &mut prefix, &mut offsets);
73        offsets
74    }
75}
76
77/// Applies a stencil pattern to coordinates, returning valid
78/// resulting coordinates.
79///
80/// Given base coordinates and a set of offset vectors (the stencil),
81/// computes the coordinates that result from applying each offset.
82/// Only returns coordinates that fall within the specified bounds.
83///
84/// # Arguments
85///
86/// * `coords` - Base coordinates in N-dimensional space
87/// * `sizes` - Size bounds for each dimension (coordinates must be <
88///   size)
89/// * `offsets` - Collection of offset vectors to apply to the base
90///   coordinates
91///
92/// # Returns
93///
94/// An iterator yielding valid coordinates (as `Vec<usize>`) for each
95/// offset that produces in-bounds results. Out-of-bounds results are
96/// filtered out.
97///
98/// # Panics
99///
100/// Panics if `coords` and `sizes` have different lengths, or if any
101/// offset vector has a different length than `coords`.
102///
103/// # Examples
104///
105/// ```rust
106/// let coords = &[1, 1];
107/// let sizes = &[3, 3];
108/// let offsets: [[isize; 2]; 4] = [[-1, 0], [1, 0], [0, -1], [0, 1]];
109///
110/// let results: Vec<_> = ndslice::utils::apply_stencil(coords, sizes, &offsets).collect();
111/// // Results in: [[0, 1], [2, 1], [1, 0], [1, 2]]
112/// ```
113pub fn apply_stencil<'a, const N: usize>(
114    coords: &'a [usize; N],
115    sizes: &'a [usize; N],
116    offsets: &'a [[isize; N]],
117) -> impl Iterator<Item = [usize; N]> + 'a {
118    offsets.iter().filter_map(move |offset| {
119        let mut p = [0usize; N];
120        for dim in 0..N {
121            let c = coords[dim] as isize;
122            let size = sizes[dim] as isize;
123            let val = c + offset[dim];
124            if val < 0 || val >= size {
125                return None;
126            }
127            p[dim] = val as usize;
128        }
129        Some(p)
130    })
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn test_von_neumann_neighbors_2d() {
139        let offsets = stencil::von_neumann_neighbors::<2>();
140        assert_eq!(offsets.len(), 4);
141
142        let expected: [[isize; 2]; 4] = [[1, 0], [-1, 0], [0, 1], [0, -1]];
143
144        for e in expected.iter() {
145            assert!(offsets.contains(e), "missing offset {:?}", e);
146        }
147    }
148
149    #[test]
150    fn test_von_neumann_neighbors_3d() {
151        let offsets = stencil::von_neumann_neighbors::<3>();
152        assert_eq!(offsets.len(), 6);
153
154        #[rustfmt::skip]
155        let expected: [[isize; 3]; 6] = [
156            [1, 0, 0],  [-1, 0, 0],
157            [0, 1, 0],  [0, -1, 0],
158            [0, 0, 1],  [0, 0, -1],
159        ];
160
161        for e in expected.iter() {
162            assert!(offsets.contains(e), "missing offset {:?}", e);
163        }
164    }
165
166    #[test]
167    fn test_moore_neighbors_2d() {
168        let offsets = stencil::moore_neighbors::<2>();
169        assert_eq!(offsets.len(), 8); // 3^2 - 1
170
171        #[rustfmt::skip]
172        let expected: [[isize; 2]; 8] = [
173            [-1, -1], [-1, 0], [-1, 1],
174            [ 0, -1],          [ 0, 1],
175            [ 1, -1], [ 1, 0], [ 1, 1],
176        ];
177
178        for e in expected.iter() {
179            assert!(offsets.contains(e), "missing offset {:?}", e);
180        }
181    }
182
183    #[test]
184    fn test_moore_neighbors_3d() {
185        let offsets = stencil::moore_neighbors::<3>();
186        assert_eq!(offsets.len(), 26); // 3^3 - 1
187
188        // Spot-check just a few offsets, no need to write out all 26
189        #[rustfmt::skip]
190        let expected: [[isize; 3]; 5] = [
191            [-1,  0,  0],
192            [ 0, -1,  0],
193            [ 0,  0, -1],
194            [ 1,  1,  1],
195            [-1, -1, -1],
196        ];
197
198        for e in expected.iter() {
199            assert!(offsets.contains(e), "missing offset {:?}", e);
200        }
201    }
202
203    #[test]
204    fn test_apply_stencil_2d() {
205        let coords: [usize; 2] = [1, 1];
206        let sizes: [usize; 2] = [3, 3];
207
208        #[rustfmt::skip]
209        let offsets: [[isize; 2]; 4] = [
210            [-1, 0], [1, 0],
211            [0, -1], [0, 1],
212        ];
213
214        let results: Vec<_> = apply_stencil(&coords, &sizes, &offsets).collect();
215        let expected: [[usize; 2]; 4] = [[0, 1], [2, 1], [1, 0], [1, 2]];
216
217        for e in expected.iter() {
218            assert!(results.contains(e), "missing result {:?}", e);
219        }
220        assert_eq!(results.len(), expected.len());
221    }
222}