1use ndarray::Array2;
27use std::collections::HashMap;
28
29pub fn ico_source_space(
43 n_subdivisions: usize,
44 radius: f64,
45 center: [f64; 3],
46) -> (Array2<f64>, Array2<f64>) {
47 let (verts, faces) = make_icosahedron();
48 let (verts, _faces) = subdivide_ico(verts, faces, n_subdivisions);
49
50 let n = verts.len();
51 let mut positions = Array2::zeros((n, 3));
52 let mut normals = Array2::zeros((n, 3));
53
54 for (i, v) in verts.iter().enumerate() {
55 let len = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
56 let nx = v[0] / len;
57 let ny = v[1] / len;
58 let nz = v[2] / len;
59
60 positions[[i, 0]] = nx * radius + center[0];
61 positions[[i, 1]] = ny * radius + center[1];
62 positions[[i, 2]] = nz * radius + center[2];
63
64 normals[[i, 0]] = nx;
65 normals[[i, 1]] = ny;
66 normals[[i, 2]] = nz;
67 }
68
69 (positions, normals)
70}
71
72pub fn grid_source_space(
85 spacing: f64,
86 radius: f64,
87 center: [f64; 3],
88) -> (Array2<f64>, Array2<f64>) {
89 assert!(spacing > 0.0, "Spacing must be positive");
90 assert!(radius > 0.0, "Radius must be positive");
91
92 let n_half = (radius / spacing).ceil() as i32;
93 let mut points = Vec::new();
94
95 for ix in -n_half..=n_half {
96 for iy in -n_half..=n_half {
97 for iz in -n_half..=n_half {
98 let x = ix as f64 * spacing;
99 let y = iy as f64 * spacing;
100 let z = iz as f64 * spacing;
101 if x * x + y * y + z * z <= radius * radius {
102 points.push([x + center[0], y + center[1], z + center[2]]);
103 }
104 }
105 }
106 }
107
108 let n = points.len();
109 let mut positions = Array2::zeros((n, 3));
110 let mut normals = Array2::zeros((n, 3));
111
112 for (i, p) in points.iter().enumerate() {
113 positions[[i, 0]] = p[0];
114 positions[[i, 1]] = p[1];
115 positions[[i, 2]] = p[2];
116 normals[[i, 2]] = 1.0;
118 }
119
120 (positions, normals)
121}
122
123pub fn ico_n_vertices(n_subdivisions: usize) -> usize {
125 10 * 4_usize.pow(n_subdivisions as u32) + 2
127}
128
129fn make_icosahedron() -> (Vec<[f64; 3]>, Vec<[usize; 3]>) {
133 let phi = (1.0 + 5.0_f64.sqrt()) / 2.0; let a = 1.0;
135 let b = 1.0 / phi;
136
137 #[rustfmt::skip]
139 let raw_verts: Vec<[f64; 3]> = vec![
140 [ 0.0, b, -a], [ b, a, 0.0], [-b, a, 0.0],
141 [ 0.0, b, a], [-a, 0.0, b], [ 0.0, -b, a],
142 [ a, 0.0, b], [ a, 0.0, -b], [ 0.0, -b, -a],
143 [-a, 0.0, -b], [-b, -a, 0.0], [ b, -a, 0.0],
144 ];
145
146 let verts: Vec<[f64; 3]> = raw_verts
147 .iter()
148 .map(|v| {
149 let len = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
150 [v[0] / len, v[1] / len, v[2] / len]
151 })
152 .collect();
153
154 #[rustfmt::skip]
155 let faces: Vec<[usize; 3]> = vec![
156 [0, 1, 2], [3, 2, 1], [3, 4, 5], [3, 5, 6],
157 [0, 7, 1], [0, 8, 7], [0, 9, 8], [0, 2, 9],
158 [3, 1, 6], [3, 4, 2], [5, 4, 10], [5, 10, 11],
159 [5, 11, 6], [7, 6, 11], [7, 11, 8], [8, 11, 10],
160 [8, 10, 9], [9, 10, 4], [9, 4, 2], [6, 7, 1],
161 ];
162
163 (verts, faces)
164}
165
166fn subdivide_ico(
171 mut verts: Vec<[f64; 3]>,
172 mut faces: Vec<[usize; 3]>,
173 n: usize,
174) -> (Vec<[f64; 3]>, Vec<[usize; 3]>) {
175 for _ in 0..n {
176 let mut edge_midpoint: HashMap<(usize, usize), usize> = HashMap::new();
177 let mut new_faces = Vec::with_capacity(faces.len() * 4);
178
179 for face in &faces {
180 let [a, b, c] = *face;
181 let ab = get_or_insert_midpoint(a, b, &mut verts, &mut edge_midpoint);
182 let bc = get_or_insert_midpoint(b, c, &mut verts, &mut edge_midpoint);
183 let ca = get_or_insert_midpoint(c, a, &mut verts, &mut edge_midpoint);
184
185 new_faces.push([a, ab, ca]);
186 new_faces.push([b, bc, ab]);
187 new_faces.push([c, ca, bc]);
188 new_faces.push([ab, bc, ca]);
189 }
190
191 faces = new_faces;
192 }
193
194 (verts, faces)
195}
196
197fn get_or_insert_midpoint(
200 i: usize,
201 j: usize,
202 verts: &mut Vec<[f64; 3]>,
203 cache: &mut HashMap<(usize, usize), usize>,
204) -> usize {
205 let key = if i < j { (i, j) } else { (j, i) };
206 if let Some(&idx) = cache.get(&key) {
207 return idx;
208 }
209
210 let a = verts[i];
211 let b = verts[j];
212 let mx = (a[0] + b[0]) / 2.0;
213 let my = (a[1] + b[1]) / 2.0;
214 let mz = (a[2] + b[2]) / 2.0;
215 let len = (mx * mx + my * my + mz * mz).sqrt();
216
217 let idx = verts.len();
218 verts.push([mx / len, my / len, mz / len]);
219 cache.insert(key, idx);
220 idx
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226
227 #[test]
228 fn test_ico_vertex_counts() {
229 assert_eq!(ico_n_vertices(0), 12);
230 assert_eq!(ico_n_vertices(1), 42);
231 assert_eq!(ico_n_vertices(2), 162);
232 assert_eq!(ico_n_vertices(3), 642);
233 assert_eq!(ico_n_vertices(4), 2562);
234 }
235
236 #[test]
237 fn test_ico_source_space_shape() {
238 for subdiv in 0..=3 {
239 let (pos, nn) = ico_source_space(subdiv, 0.07, [0.0, 0.0, 0.04]);
240 let expected = ico_n_vertices(subdiv);
241 assert_eq!(
242 pos.nrows(),
243 expected,
244 "subdiv={subdiv}: expected {expected} vertices, got {}",
245 pos.nrows()
246 );
247 assert_eq!(pos.ncols(), 3);
248 assert_eq!(nn.dim(), pos.dim());
249 }
250 }
251
252 #[test]
253 fn test_ico_source_space_radius() {
254 let radius = 0.07;
255 let center = [0.0, 0.0, 0.04];
256 let (pos, _) = ico_source_space(2, radius, center);
257
258 for i in 0..pos.nrows() {
259 let dx = pos[[i, 0]] - center[0];
260 let dy = pos[[i, 1]] - center[1];
261 let dz = pos[[i, 2]] - center[2];
262 let r = (dx * dx + dy * dy + dz * dz).sqrt();
263 approx::assert_abs_diff_eq!(r, radius, epsilon = 1e-10);
264 }
265 }
266
267 #[test]
268 fn test_ico_normals_unit_length() {
269 let (_, nn) = ico_source_space(2, 0.07, [0.0, 0.0, 0.0]);
270 for i in 0..nn.nrows() {
271 let len = (nn[[i, 0]].powi(2) + nn[[i, 1]].powi(2) + nn[[i, 2]].powi(2)).sqrt();
272 approx::assert_abs_diff_eq!(len, 1.0, epsilon = 1e-10);
273 }
274 }
275
276 #[test]
277 fn test_grid_source_space_inside_sphere() {
278 let radius = 0.05;
279 let center = [0.0, 0.0, 0.03];
280 let (pos, _) = grid_source_space(0.01, radius, center);
281
282 assert!(pos.nrows() > 0, "Should have some sources");
283 for i in 0..pos.nrows() {
284 let dx = pos[[i, 0]] - center[0];
285 let dy = pos[[i, 1]] - center[1];
286 let dz = pos[[i, 2]] - center[2];
287 let r = (dx * dx + dy * dy + dz * dz).sqrt();
288 assert!(
289 r <= radius + 1e-10,
290 "Source at r={r} exceeds radius={radius}"
291 );
292 }
293 }
294
295 #[test]
296 fn test_grid_source_space_includes_center() {
297 let center = [0.01, 0.02, 0.03];
298 let (pos, _) = grid_source_space(0.01, 0.05, center);
299
300 let mut min_dist = f64::MAX;
302 for i in 0..pos.nrows() {
303 let dx = pos[[i, 0]] - center[0];
304 let dy = pos[[i, 1]] - center[1];
305 let dz = pos[[i, 2]] - center[2];
306 let r = (dx * dx + dy * dy + dz * dz).sqrt();
307 if r < min_dist {
308 min_dist = r;
309 }
310 }
311 assert!(min_dist < 0.015, "Closest point to center is {min_dist} m away");
312 }
313
314 #[test]
315 fn test_grid_spacing_smaller_gives_more_sources() {
316 let (p1, _) = grid_source_space(0.02, 0.05, [0.0, 0.0, 0.0]);
317 let (p2, _) = grid_source_space(0.01, 0.05, [0.0, 0.0, 0.0]);
318 assert!(
319 p2.nrows() > p1.nrows(),
320 "Finer grid should have more sources: {} vs {}",
321 p2.nrows(),
322 p1.nrows()
323 );
324 }
325}