1use crate::Interpolatable;
4
5#[derive(Debug, Clone, PartialEq)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14#[must_use]
15pub struct StencilTable {
16 pub offsets: Vec<u32>,
18 pub indices: Vec<u32>,
20 pub weights: Vec<f32>,
22}
23
24impl StencilTable {
25 #[inline]
27 pub fn output_count(&self) -> usize {
28 self.offsets.len().saturating_sub(1)
29 }
30
31 #[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 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 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 let c_start = other.offsets[c] as usize;
87 let c_end = other.offsets[c + 1] as usize;
88
89 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 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 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 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 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 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 assert_eq!(out[1], full[1]);
237 assert_eq!(out[3], full[3]);
238 assert_eq!(out[0], sentinel);
240 assert_eq!(out[2], sentinel);
241 }
242}