1#[derive(Clone, Copy, Debug, PartialEq)]
9pub struct Grid {
10 pub dims: (usize, usize, usize),
12 pub voxel_size: (f64, f64, f64),
14}
15
16impl Grid {
17 #[inline]
19 pub fn new(nx: usize, ny: usize, nz: usize, vsx: f64, vsy: f64, vsz: f64) -> Self {
20 Self {
21 dims: (nx, ny, nz),
22 voxel_size: (vsx, vsy, vsz),
23 }
24 }
25
26 #[inline]
27 pub fn nx(&self) -> usize { self.dims.0 }
28 #[inline]
29 pub fn ny(&self) -> usize { self.dims.1 }
30 #[inline]
31 pub fn nz(&self) -> usize { self.dims.2 }
32 #[inline]
33 pub fn vsx(&self) -> f64 { self.voxel_size.0 }
34 #[inline]
35 pub fn vsy(&self) -> f64 { self.voxel_size.1 }
36 #[inline]
37 pub fn vsz(&self) -> f64 { self.voxel_size.2 }
38
39 #[inline]
41 pub fn n_total(&self) -> usize {
42 self.dims.0 * self.dims.1 * self.dims.2
43 }
44
45 #[inline]
50 pub fn spacing(&self, axis: usize) -> f64 {
51 match axis {
52 0 => self.voxel_size.0,
53 1 => self.voxel_size.1,
54 2 => self.voxel_size.2,
55 _ => panic!("axis must be 0, 1 or 2 (got {axis})"),
56 }
57 }
58
59 #[inline]
64 pub fn extent(&self, axis: usize) -> usize {
65 match axis {
66 0 => self.dims.0,
67 1 => self.dims.1,
68 2 => self.dims.2,
69 _ => panic!("axis must be 0, 1 or 2 (got {axis})"),
70 }
71 }
72
73 pub fn require_contiguous_slices_if_known(
86 &self,
87 slices: Option<SliceGeometry>,
88 ) -> Result<(), SliceGapError> {
89 match slices {
90 Some(s) => self.require_contiguous_slices(s.thickness, s.axis),
91 None => Ok(()),
92 }
93 }
94
95 pub fn require_contiguous_slices(
122 &self,
123 slice_thickness: f64,
124 slice_axis: usize,
125 ) -> Result<(), SliceGapError> {
126 let pitch = self.spacing(slice_axis);
127 if !slice_thickness.is_finite() || slice_thickness <= 0.0 {
128 return Err(SliceGapError { pitch, thickness: slice_thickness, axis: slice_axis });
129 }
130 if slice_thickness < pitch * 0.99 {
133 return Err(SliceGapError { pitch, thickness: slice_thickness, axis: slice_axis });
134 }
135 Ok(())
136 }
137}
138
139#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
145#[derive(Clone, Copy, Debug, PartialEq)]
146pub struct SliceGeometry {
147 pub thickness: f64,
149 pub axis: usize,
151}
152
153impl SliceGeometry {
154 pub fn contiguous(grid: &Grid, axis: usize) -> Self {
160 Self { thickness: grid.spacing(axis), axis }
161 }
162}
163
164#[derive(Clone, Copy, Debug, PartialEq)]
184pub struct SliceGapError {
185 pub pitch: f64,
187 pub thickness: f64,
189 pub axis: usize,
191}
192
193impl SliceGapError {
194 pub fn gap(&self) -> f64 {
196 self.pitch - self.thickness
197 }
198}
199
200impl std::fmt::Display for SliceGapError {
201 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202 if !self.thickness.is_finite() || self.thickness <= 0.0 {
203 return write!(
204 f,
205 "slice thickness must be a positive number of mm (got {})",
206 self.thickness
207 );
208 }
209 write!(
210 f,
211 "slices along axis {} are {:.4} mm thick but {:.4} mm apart, leaving a {:.4} mm gap: \
212the volume is not contiguous, and the FFT dipole and spherical-mean kernels are defined on a \
213uniform grid, so any field or susceptibility computed from it would be wrong rather than \
214approximate",
215 self.axis, self.thickness, self.pitch, self.gap()
216 )
217 }
218}
219
220impl std::error::Error for SliceGapError {}
221
222#[derive(Clone, Copy, Debug, PartialEq)]
231pub(crate) struct SliceLayout {
232 pub d0: usize,
234 pub d1: usize,
235 pub n_slices: usize,
237 slice_stride: usize,
239 stride0: usize,
241 stride1: usize,
242 pub grid: Grid,
244}
245
246impl SliceLayout {
247 pub fn new(grid: &Grid, axis: usize) -> Self {
250 let (nx, ny, nz) = grid.dims;
251 let (vsx, vsy, vsz) = grid.voxel_size;
252 let (nxy, sy, sz) = (nx * ny, nx, nx * ny);
253 match axis {
254 0 => Self {
255 d0: ny, d1: nz, n_slices: nx, slice_stride: 1, stride0: sy, stride1: sz,
256 grid: Grid::new(ny, nz, 1, vsy, vsz, vsx),
257 },
258 1 => Self {
259 d0: nx, d1: nz, n_slices: ny, slice_stride: sy, stride0: 1, stride1: sz,
260 grid: Grid::new(nx, nz, 1, vsx, vsz, vsy),
261 },
262 2 => Self {
263 d0: nx, d1: ny, n_slices: nz, slice_stride: nxy, stride0: 1, stride1: sy,
264 grid: Grid::new(nx, ny, 1, vsx, vsy, vsz),
265 },
266 _ => panic!("slice_axis must be 0, 1 or 2 (got {axis})"),
267 }
268 }
269
270 #[inline]
272 pub fn n_in_slice(&self) -> usize {
273 self.d0 * self.d1
274 }
275
276 #[inline]
278 pub fn index(&self, s: usize, p: usize) -> usize {
279 let a = p % self.d0;
280 let b = p / self.d0;
281 s * self.slice_stride + a * self.stride0 + b * self.stride1
282 }
283
284 pub fn gather<T: Copy>(&self, volume: &[T], s: usize, out: &mut [T]) {
287 if volume.is_empty() {
288 return;
289 }
290 for (p, o) in out.iter_mut().enumerate() {
291 *o = volume[self.index(s, p)];
292 }
293 }
294
295 pub fn scatter<T: Copy>(&self, volume: &mut [T], s: usize, values: &[T]) {
297 for (p, &v) in values.iter().enumerate() {
298 volume[self.index(s, p)] = v;
299 }
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306
307 #[test]
308 fn test_grid_basic() {
309 let g = Grid::new(64, 64, 32, 1.0, 1.0, 2.0);
310 assert_eq!(g.nx(), 64);
311 assert_eq!(g.ny(), 64);
312 assert_eq!(g.nz(), 32);
313 assert_eq!(g.n_total(), 64 * 64 * 32);
314 assert_eq!(g.vsx(), 1.0);
315 assert_eq!(g.vsz(), 2.0);
316 }
317
318 #[test]
319 fn spacing_and_extent_follow_the_axis() {
320 let g = Grid::new(5, 7, 3, 1.0, 2.0, 4.0);
321 assert_eq!((g.extent(0), g.extent(1), g.extent(2)), (5, 7, 3));
322 assert_eq!((g.spacing(0), g.spacing(1), g.spacing(2)), (1.0, 2.0, 4.0));
323 }
324
325 #[test]
326 #[should_panic(expected = "axis must be 0, 1 or 2")]
327 fn spacing_rejects_a_bad_axis() {
328 Grid::new(2, 2, 2, 1.0, 1.0, 1.0).spacing(3);
329 }
330
331 #[test]
332 #[should_panic(expected = "axis must be 0, 1 or 2")]
333 fn extent_rejects_a_bad_axis() {
334 Grid::new(2, 2, 2, 1.0, 1.0, 1.0).extent(3);
335 }
336
337 #[test]
338 fn contiguous_slices_are_accepted_and_gapped_ones_refused() {
339 let g = Grid::new(8, 8, 4, 1.0, 1.0, 3.0);
340
341 assert!(g.require_contiguous_slices(3.0, 2).is_ok());
343 assert!(g.require_contiguous_slices(2.99, 2).is_ok());
345 assert!(g.require_contiguous_slices(2.9, 2).is_err());
346
347 let err = g.require_contiguous_slices(2.0, 2).unwrap_err();
348 assert_eq!(err.gap(), 1.0);
349 assert_eq!((err.pitch, err.thickness, err.axis), (3.0, 2.0, 2));
350 assert!(err.to_string().contains("not contiguous"));
351
352 assert!(g.require_contiguous_slices(4.0, 2).is_ok());
354
355 assert!(g.require_contiguous_slices(1.0, 0).is_ok());
357 assert!(g.require_contiguous_slices(0.5, 0).is_err());
358 }
359
360 #[test]
361 fn unrecorded_slice_geometry_cannot_refuse_and_does_not_pretend_to() {
362 let g = Grid::new(8, 8, 4, 1.0, 1.0, 3.0);
366 assert!(g.require_contiguous_slices_if_known(None).is_ok());
367 assert!(g
368 .require_contiguous_slices_if_known(Some(SliceGeometry { thickness: 3.0, axis: 2 }))
369 .is_ok());
370 let err = g
371 .require_contiguous_slices_if_known(Some(SliceGeometry { thickness: 2.0, axis: 2 }))
372 .unwrap_err();
373 assert_eq!(err.gap(), 1.0);
374 }
375
376 #[test]
377 fn slice_geometry_contiguous_reads_the_grid_spacing() {
378 let g = Grid::new(8, 8, 4, 1.0, 2.0, 3.0);
379 assert_eq!(SliceGeometry::contiguous(&g, 2), SliceGeometry { thickness: 3.0, axis: 2 });
380 assert_eq!(SliceGeometry::contiguous(&g, 1), SliceGeometry { thickness: 2.0, axis: 1 });
381 for axis in 0..3 {
383 assert!(g
384 .require_contiguous_slices_if_known(Some(SliceGeometry::contiguous(&g, axis)))
385 .is_ok());
386 }
387 }
388
389 #[test]
390 fn a_nonsense_slice_thickness_is_refused_and_says_so() {
391 let g = Grid::new(8, 8, 4, 1.0, 1.0, 3.0);
392 for bad in [0.0, -1.0, f64::NAN, f64::INFINITY] {
393 let err = g.require_contiguous_slices(bad, 2).unwrap_err();
394 assert!(
395 err.to_string().contains("must be a positive number"),
396 "thickness {bad} gave: {err}"
397 );
398 }
399 }
400
401 #[test]
402 fn slice_layout_visits_every_voxel_exactly_once() {
403 let g = Grid::new(5, 7, 3, 1.0, 2.0, 3.0);
404 for axis in 0..3 {
405 let layout = SliceLayout::new(&g, axis);
406 assert_eq!(layout.n_slices * layout.n_in_slice(), g.n_total(), "axis {axis}");
407 let mut seen = vec![0u32; g.n_total()];
408 for s in 0..layout.n_slices {
409 for p in 0..layout.n_in_slice() {
410 seen[layout.index(s, p)] += 1;
411 }
412 }
413 assert!(seen.iter().all(|&c| c == 1), "axis {axis} does not hit each voxel once");
414 }
415 }
416
417 #[test]
418 fn slice_layout_grid_keeps_the_two_in_plane_axes() {
419 let g = Grid::new(5, 7, 3, 1.0, 2.0, 3.0);
420 assert_eq!(SliceLayout::new(&g, 0).grid, Grid::new(7, 3, 1, 2.0, 3.0, 1.0));
421 assert_eq!(SliceLayout::new(&g, 1).grid, Grid::new(5, 3, 1, 1.0, 3.0, 2.0));
422 assert_eq!(SliceLayout::new(&g, 2).grid, Grid::new(5, 7, 1, 1.0, 2.0, 3.0));
423 }
424
425 #[test]
426 fn slice_layout_buffer_order_matches_the_slice_grid() {
427 let g = Grid::new(5, 7, 3, 1.0, 2.0, 3.0);
432 for axis in 0..3 {
433 let layout = SliceLayout::new(&g, axis);
434 assert_eq!(
435 (layout.d0, layout.d1),
436 (layout.grid.nx(), layout.grid.ny()),
437 "axis {axis}: buffer is {}x{} but its grid says {}x{}",
438 layout.d0, layout.d1, layout.grid.nx(), layout.grid.ny()
439 );
440 assert_eq!(layout.index(0, 1) - layout.index(0, 0), match axis {
442 0 => 5, 1 => 1, _ => 1,
445 });
446 }
447 }
448
449 #[test]
450 fn slice_layout_gather_and_scatter_round_trip() {
451 let g = Grid::new(4, 5, 3, 1.0, 1.0, 1.0);
452 let volume: Vec<f64> = (0..g.n_total()).map(|i| i as f64 * 0.25).collect();
453 for axis in 0..3 {
454 let layout = SliceLayout::new(&g, axis);
455 let mut buf = vec![0.0; layout.n_in_slice()];
456 let mut out = vec![-1.0; g.n_total()];
457 for s in 0..layout.n_slices {
458 layout.gather(&volume, s, &mut buf);
459 layout.scatter(&mut out, s, &buf);
460 }
461 assert_eq!(out, volume, "axis {axis}");
462 }
463 }
464
465 #[test]
466 fn slice_layout_gather_of_an_empty_volume_leaves_the_buffer_alone() {
467 let layout = SliceLayout::new(&Grid::new(4, 4, 2, 1.0, 1.0, 1.0), 2);
468 let mut buf = vec![7.0; layout.n_in_slice()];
469 layout.gather(&[] as &[f64], 0, &mut buf);
470 assert!(buf.iter().all(|&v| v == 7.0));
471 }
472
473 #[test]
474 #[should_panic(expected = "slice_axis must be 0, 1 or 2")]
475 fn slice_layout_rejects_a_bad_axis() {
476 SliceLayout::new(&Grid::new(2, 2, 2, 1.0, 1.0, 1.0), 3);
477 }
478
479 #[test]
480 fn test_grid_copy() {
481 let g = Grid::new(10, 20, 30, 0.5, 0.5, 1.0);
482 let g2 = g;
483 assert_eq!(g, g2);
484 }
485}