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}