Skip to main content

subdiv_kernels/
stencil.rs

1//! CSR-packed stencil tables for subdivision interpolation.
2
3use crate::Interpolatable;
4
5/// A sparse linear map from input points to output points.
6///
7/// Each output point is a weighted sum of a few input points — its *stencil*.
8/// Apply it to any [`Interpolatable`] data with [`interpolate()`](Self::interpolate).
9///
10/// Stored compressed (CSR): output `i`'s source indices and weights are the
11/// slices `indices[offsets[i]..offsets[i + 1]]` and the matching `weights`.
12#[derive(Debug, Clone, PartialEq)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14#[must_use]
15pub struct StencilTable {
16    /// CSR row offsets. Length = output_count + 1.
17    pub offsets: Vec<u32>,
18    /// Source indices (into the input buffer), flat.
19    pub indices: Vec<u32>,
20    /// Source weights (parallel to `indices`).
21    pub weights: Vec<f32>,
22}
23
24impl StencilTable {
25    /// Number of output points this table produces.
26    #[inline]
27    pub fn output_count(&self) -> usize {
28        self.offsets.len().saturating_sub(1)
29    }
30
31    /// Apply stencils to an input buffer, producing one output value per stencil.
32    #[inline]
33    pub fn interpolate<T: Interpolatable>(&self, input: &[T]) -> Vec<T> {
34        (0..self.output_count())
35            .map(|i| {
36                let start = self.offsets[i] as usize;
37                let end = self.offsets[i + 1] as usize;
38                let mut result = T::default();
39                self.indices[start..end]
40                    .iter()
41                    .zip(&self.weights[start..end])
42                    .for_each(|(&idx, &w)| result.add_with_weight(&input[idx as usize], w));
43                result
44            })
45            .collect()
46    }
47
48    /// Apply stencils for only `rows`, scattering each result into
49    /// `output[row]` and leaving every other entry untouched.
50    ///
51    /// The CPU analogue of an indexed (sparse) dispatch: pair with
52    /// [`affected_outputs`](crate::InverseStencilMap::affected_outputs) to
53    /// re-evaluate only the outputs a control-point edit changed, splicing them
54    /// into the previous output buffer. Bit-identical to
55    /// [`interpolate`](Self::interpolate) on the recomputed rows.
56    ///
57    /// `output` must have at least [`output_count`](Self::output_count) entries
58    /// and every index in `rows` must be `< output_count`.
59    pub fn interpolate_rows<T: Interpolatable>(&self, input: &[T], rows: &[u32], output: &mut [T]) {
60        for &row in rows {
61            let r = row as usize;
62            let start = self.offsets[r] as usize;
63            let end = self.offsets[r + 1] as usize;
64            let mut result = T::default();
65            self.indices[start..end]
66                .iter()
67                .zip(&self.weights[start..end])
68                .for_each(|(&idx, &w)| result.add_with_weight(&input[idx as usize], w));
69            output[r] = result;
70        }
71    }
72
73    /// Compose two stencil tables: `self` maps A→B, `other` maps B→C.
74    /// The result maps A→C by substituting B's stencils into C's.
75    pub fn compose(&self, other: &StencilTable) -> Self {
76        let mut offsets = Vec::with_capacity(other.output_count() + 1);
77        let mut indices = Vec::new();
78        let mut weights = Vec::new();
79
80        offsets.push(0);
81
82        (0..other.output_count()).for_each(|c| {
83            // For output point c in `other`, accumulate the composed stencil.
84            // other's stencil for c references points in B-space.
85            // For each B-point, expand via self's stencil into A-space.
86            let c_start = other.offsets[c] as usize;
87            let c_end = other.offsets[c + 1] as usize;
88
89            // Accumulate into a sparse map: A-index → combined weight.
90            let mut combined: Vec<(u32, f32)> = Vec::new();
91
92            other.indices[c_start..c_end]
93                .iter()
94                .zip(&other.weights[c_start..c_end])
95                .for_each(|(&b_idx, &b_weight)| {
96                    let b = b_idx as usize;
97                    let b_start = self.offsets[b] as usize;
98                    let b_end = self.offsets[b + 1] as usize;
99
100                    self.indices[b_start..b_end]
101                        .iter()
102                        .zip(&self.weights[b_start..b_end])
103                        .for_each(|(&a_idx, &a_weight)| {
104                            let w = b_weight * a_weight;
105                            // Merge into combined list.
106                            if let Some(entry) = combined.iter_mut().find(|(idx, _)| *idx == a_idx)
107                            {
108                                entry.1 += w;
109                            } else {
110                                combined.push((a_idx, w));
111                            }
112                        });
113                });
114
115            combined.iter().for_each(|&(idx, w)| {
116                indices.push(idx);
117                weights.push(w);
118            });
119            offsets.push(indices.len() as u32);
120        });
121
122        Self {
123            offsets,
124            indices,
125            weights,
126        }
127    }
128
129    /// Identity table of `count` rows: each output copies its input unchanged.
130    pub fn identity(count: usize) -> Self {
131        let offsets = (0..=count as u32).collect();
132        let indices = (0..count as u32).collect();
133        let weights = vec![1.0; count];
134        Self {
135            offsets,
136            indices,
137            weights,
138        }
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn identity_preserves_input() {
148        let table = StencilTable::identity(3);
149        let input: Vec<[f32; 2]> = vec![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
150        let output = table.interpolate(&input);
151        assert_eq!(output, input);
152    }
153
154    #[test]
155    fn midpoint_stencil() {
156        let table = StencilTable {
157            offsets: vec![0, 2],
158            indices: vec![0, 1],
159            weights: vec![0.5, 0.5],
160        };
161        let input = vec![[0.0_f32, 0.0], [2.0, 4.0]];
162        let output = table.interpolate(&input);
163        assert_eq!(output, vec![[1.0, 2.0]]);
164    }
165
166    #[test]
167    fn compose_identity_is_identity() {
168        let a = StencilTable::identity(3);
169        let b = StencilTable {
170            offsets: vec![0, 2, 4],
171            indices: vec![0, 1, 1, 2],
172            weights: vec![0.5, 0.5, 0.5, 0.5],
173        };
174        let composed = a.compose(&b);
175        let input = vec![1.0_f32, 3.0, 5.0];
176        assert_eq!(composed.interpolate(&input), b.interpolate(&input));
177    }
178
179    #[test]
180    fn compose_chains_correctly() {
181        // A→B: 2 inputs → 3 outputs
182        // B[0] = 0.5*A[0] + 0.5*A[1], B[1] = 1.0*A[1], B[2] = 1.0*A[0]
183        let ab = StencilTable {
184            offsets: vec![0, 2, 3, 4],
185            indices: vec![0, 1, 1, 0],
186            weights: vec![0.5, 0.5, 1.0, 1.0],
187        };
188        // B→C: 3 inputs → 1 output (average of all B points)
189        let bc = StencilTable {
190            offsets: vec![0, 3],
191            indices: vec![0, 1, 2],
192            weights: vec![1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0],
193        };
194
195        let ac = ab.compose(&bc);
196        let input = vec![2.0_f32, 8.0];
197
198        let b = ab.interpolate(&input);
199        let c_via_b = bc.interpolate(&b);
200        let c_direct = ac.interpolate(&input);
201
202        c_via_b
203            .iter()
204            .zip(c_direct.iter())
205            .for_each(|(a, b)| assert!((a - b).abs() < 1e-6));
206    }
207
208    #[test]
209    fn interpolate_f64() {
210        let table = StencilTable {
211            offsets: vec![0, 3],
212            indices: vec![0, 1, 2],
213            weights: vec![0.25, 0.5, 0.25],
214        };
215        let input: Vec<[f64; 3]> = vec![[0.0, 0.0, 0.0], [4.0, 8.0, 12.0], [0.0, 0.0, 0.0]];
216        let output = table.interpolate(&input);
217        assert_eq!(output, vec![[2.0, 4.0, 6.0]]);
218    }
219
220    #[test]
221    fn interpolate_rows_matches_full_on_subset_and_preserves_others() {
222        // out0 = in0, out1 = (in0+in1)/2, out2 = in1, out3 = (in1+in2)/2.
223        let table = StencilTable {
224            offsets: vec![0, 1, 3, 4, 6],
225            indices: vec![0, 0, 1, 1, 1, 2],
226            weights: vec![1.0, 0.5, 0.5, 1.0, 0.5, 0.5],
227        };
228        let input: Vec<[f32; 3]> = vec![[1.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 4.0]];
229        let full = table.interpolate(&input);
230
231        let sentinel = [-9.0_f32, -9.0, -9.0];
232        let mut out = vec![sentinel; table.output_count()];
233        table.interpolate_rows(&input, &[1, 3], &mut out);
234
235        // Recomputed rows match the full eval bit-for-bit...
236        assert_eq!(out[1], full[1]);
237        assert_eq!(out[3], full[3]);
238        // ...and rows not listed are left exactly as they were.
239        assert_eq!(out[0], sentinel);
240        assert_eq!(out[2], sentinel);
241    }
242}