1pub fn voxel_sizes_from_affine(affine: &[f64; 16]) -> (f64, f64, f64) {
24 let col = |j: usize| {
25 (affine[j] * affine[j] + affine[4 + j] * affine[4 + j] + affine[8 + j] * affine[8 + j])
26 .sqrt()
27 };
28 (col(0), col(1), col(2))
29}
30
31pub fn b0_direction_from_affine(affine: &[f64; 16]) -> (f64, f64, f64) {
43 let (sx, sy, sz) = voxel_sizes_from_affine(affine);
44 if sx < 1e-10 || sy < 1e-10 || sz < 1e-10 {
45 return (0.0, 0.0, 1.0);
46 }
47 let (bx, by, bz) = (affine[8] / sx, affine[9] / sy, affine[10] / sz);
49 let norm = (bx * bx + by * by + bz * bz).sqrt();
50 if norm < 1e-10 {
51 return (0.0, 0.0, 1.0);
52 }
53 (bx / norm, by / norm, bz / norm)
54}
55
56pub fn b0_angle_from_affine(affine: &[f64; 16]) -> f64 {
59 let (_, _, bz) = b0_direction_from_affine(affine);
60 bz.clamp(-1.0, 1.0).abs().acos().to_degrees()
61}
62
63pub fn obliquity_axes_from_affine(affine: &[f64; 16]) -> [f64; 3] {
66 let sizes = voxel_sizes_from_affine(affine);
67 let sizes = [sizes.0, sizes.1, sizes.2];
68 let mut out = [0.0f64; 3];
69 for (i, o) in out.iter_mut().enumerate() {
70 let best = (0..3)
72 .map(|j| {
73 if sizes[j] < 1e-10 {
74 0.0
75 } else {
76 (affine[4 * i + j] / sizes[j]).abs()
77 }
78 })
79 .fold(0.0f64, f64::max);
80 *o = best.clamp(0.0, 1.0).acos().to_degrees();
81 }
82 out
83}
84
85pub fn obliquity_from_affine(affine: &[f64; 16]) -> f64 {
92 let a = obliquity_axes_from_affine(affine);
93 (a[0] * a[0] + a[1] * a[1] + a[2] * a[2]).sqrt()
94}
95
96pub struct GridMap {
104 pub dst_dims: (usize, usize, usize),
105 src_dims: (usize, usize, usize),
106 dst_affine: [f64; 16],
108 inv_src: [[f64; 4]; 3],
110}
111
112fn world_to_voxel(affine: &[f64; 16]) -> Option<[[f64; 4]; 3]> {
114 let r = [
115 [affine[0], affine[1], affine[2]],
116 [affine[4], affine[5], affine[6]],
117 [affine[8], affine[9], affine[10]],
118 ];
119 let t = [affine[3], affine[7], affine[11]];
120 let det = r[0][0] * (r[1][1] * r[2][2] - r[1][2] * r[2][1])
121 - r[0][1] * (r[1][0] * r[2][2] - r[1][2] * r[2][0])
122 + r[0][2] * (r[1][0] * r[2][1] - r[1][1] * r[2][0]);
123 if det.abs() < 1e-12 {
124 return None;
125 }
126 let id = 1.0 / det;
127 let inv = [
128 [
129 (r[1][1] * r[2][2] - r[1][2] * r[2][1]) * id,
130 (r[0][2] * r[2][1] - r[0][1] * r[2][2]) * id,
131 (r[0][1] * r[1][2] - r[0][2] * r[1][1]) * id,
132 ],
133 [
134 (r[1][2] * r[2][0] - r[1][0] * r[2][2]) * id,
135 (r[0][0] * r[2][2] - r[0][2] * r[2][0]) * id,
136 (r[0][2] * r[1][0] - r[0][0] * r[1][2]) * id,
137 ],
138 [
139 (r[1][0] * r[2][1] - r[1][1] * r[2][0]) * id,
140 (r[0][1] * r[2][0] - r[0][0] * r[2][1]) * id,
141 (r[0][0] * r[1][1] - r[0][1] * r[1][0]) * id,
142 ],
143 ];
144 let mut out = [[0.0f64; 4]; 3];
145 for (i, row) in out.iter_mut().enumerate() {
146 row[..3].copy_from_slice(&inv[i]);
147 row[3] = -(inv[i][0] * t[0] + inv[i][1] * t[1] + inv[i][2] * t[2]);
148 }
149 Some(out)
150}
151
152impl GridMap {
153 pub fn new(
155 src_dims: (usize, usize, usize),
156 src_affine: &[f64; 16],
157 dst_dims: (usize, usize, usize),
158 dst_affine: &[f64; 16],
159 ) -> Option<Self> {
160 Some(Self {
161 dst_dims,
162 src_dims,
163 dst_affine: *dst_affine,
164 inv_src: world_to_voxel(src_affine)?,
165 })
166 }
167
168 fn source_coord(&self, i: usize, j: usize, k: usize) -> Option<(f64, f64, f64)> {
170 let (i, j, k) = (i as f64, j as f64, k as f64);
171 let a = &self.dst_affine;
172 let w = [
173 a[0] * i + a[1] * j + a[2] * k + a[3],
174 a[4] * i + a[5] * j + a[6] * k + a[7],
175 a[8] * i + a[9] * j + a[10] * k + a[11],
176 ];
177 let m = &self.inv_src;
178 let o = (
179 m[0][0] * w[0] + m[0][1] * w[1] + m[0][2] * w[2] + m[0][3],
180 m[1][0] * w[0] + m[1][1] * w[1] + m[1][2] * w[2] + m[1][3],
181 m[2][0] * w[0] + m[2][1] * w[1] + m[2][2] * w[2] + m[2][3],
182 );
183 let (nx, ny, nz) = self.src_dims;
184 let inside = o.0 >= -0.5 && o.0 <= nx as f64 - 0.5
185 && o.1 >= -0.5 && o.1 <= ny as f64 - 0.5
186 && o.2 >= -0.5 && o.2 <= nz as f64 - 0.5;
187 if inside { Some(o) } else { None }
188 }
189
190 pub fn sample(&self, data: &[f64]) -> Vec<f64> {
192 let (nx, ny, nz) = self.src_dims;
193 let (dx, dy, dz) = self.dst_dims;
194 let mut out = vec![0.0f64; dx * dy * dz];
195 for k in 0..dz {
196 for j in 0..dy {
197 for i in 0..dx {
198 if let Some((ox, oy, oz)) = self.source_coord(i, j, k) {
199 out[i + j * dx + k * dx * dy] = trilinear_sample(data, nx, ny, nz, ox, oy, oz);
200 }
201 }
202 }
203 }
204 out
205 }
206
207 pub fn sample_nearest(&self, data: &[u8]) -> Vec<u8> {
209 let (nx, ny, nz) = self.src_dims;
210 let (dx, dy, dz) = self.dst_dims;
211 let mut out = vec![0u8; dx * dy * dz];
212 for k in 0..dz {
213 for j in 0..dy {
214 for i in 0..dx {
215 if let Some((ox, oy, oz)) = self.source_coord(i, j, k) {
216 let xi = (ox.round() as isize).clamp(0, nx as isize - 1) as usize;
217 let yi = (oy.round() as isize).clamp(0, ny as isize - 1) as usize;
218 let zi = (oz.round() as isize).clamp(0, nz as isize - 1) as usize;
219 out[i + j * dx + k * dx * dy] = data[xi + yi * nx + zi * nx * ny];
220 }
221 }
222 }
223 }
224 out
225 }
226}
227
228pub fn resample_onto(
231 data: &[f64],
232 src_dims: (usize, usize, usize),
233 src_affine: &[f64; 16],
234 dst_dims: (usize, usize, usize),
235 dst_affine: &[f64; 16],
236) -> Option<Vec<f64>> {
237 Some(GridMap::new(src_dims, src_affine, dst_dims, dst_affine)?.sample(data))
238}
239
240pub fn resample_mask_onto(
242 mask: &[u8],
243 src_dims: (usize, usize, usize),
244 src_affine: &[f64; 16],
245 dst_dims: (usize, usize, usize),
246 dst_affine: &[f64; 16],
247) -> Option<Vec<u8>> {
248 Some(GridMap::new(src_dims, src_affine, dst_dims, dst_affine)?.sample_nearest(mask))
249}
250
251pub fn resample_complex_onto(
254 magnitude: &[f64],
255 phase: &[f64],
256 src_dims: (usize, usize, usize),
257 src_affine: &[f64; 16],
258 dst_dims: (usize, usize, usize),
259 dst_affine: &[f64; 16],
260) -> Option<(Vec<f64>, Vec<f64>)> {
261 let n = src_dims.0 * src_dims.1 * src_dims.2;
262 assert_eq!(magnitude.len(), n, "magnitude length does not match source dimensions");
263 assert_eq!(phase.len(), n, "phase length does not match source dimensions");
264 let map = GridMap::new(src_dims, src_affine, dst_dims, dst_affine)?;
265 let real: Vec<f64> = (0..n).map(|i| magnitude[i] * phase[i].cos()).collect();
266 let imag: Vec<f64> = (0..n).map(|i| magnitude[i] * phase[i].sin()).collect();
267 let (re, im) = (map.sample(&real), map.sample(&imag));
268 Some((
269 (0..re.len()).map(|i| re[i].hypot(im[i])).collect(),
270 (0..re.len()).map(|i| im[i].atan2(re[i])).collect(),
271 ))
272}
273
274pub struct AxialGrid {
278 pub dims: (usize, usize, usize),
279 pub voxel_size: (f64, f64, f64),
280 pub affine: [f64; 16],
282 src_dims: (usize, usize, usize),
284 world_min: [f64; 3],
285 inv_r: [[f64; 3]; 3],
287 t: [f64; 3],
288}
289
290pub fn axial_grid_for(
294 nx: usize,
295 ny: usize,
296 nz: usize,
297 affine: &[f64; 16],
298) -> AxialGrid {
299 let r = [
300 [affine[0], affine[1], affine[2]],
301 [affine[4], affine[5], affine[6]],
302 [affine[8], affine[9], affine[10]],
303 ];
304 let t = [affine[3], affine[7], affine[11]];
305 let (vsx, vsy, vsz) = voxel_sizes_from_affine(affine);
306
307 let mut world_min = [f64::INFINITY; 3];
309 let mut world_max = [f64::NEG_INFINITY; 3];
310 for &(vi, vj, vk) in &[
311 (0.0, 0.0, 0.0),
312 (nx as f64 - 1.0, 0.0, 0.0),
313 (0.0, ny as f64 - 1.0, 0.0),
314 (0.0, 0.0, nz as f64 - 1.0),
315 (nx as f64 - 1.0, ny as f64 - 1.0, 0.0),
316 (nx as f64 - 1.0, 0.0, nz as f64 - 1.0),
317 (0.0, ny as f64 - 1.0, nz as f64 - 1.0),
318 (nx as f64 - 1.0, ny as f64 - 1.0, nz as f64 - 1.0),
319 ] {
320 for d in 0..3 {
321 let w = r[d][0] * vi + r[d][1] * vj + r[d][2] * vk + t[d];
322 world_min[d] = world_min[d].min(w);
323 world_max[d] = world_max[d].max(w);
324 }
325 }
326
327 let dims = (
328 ((world_max[0] - world_min[0]) / vsx).ceil() as usize + 1,
329 ((world_max[1] - world_min[1]) / vsy).ceil() as usize + 1,
330 ((world_max[2] - world_min[2]) / vsz).ceil() as usize + 1,
331 );
332
333 let det = r[0][0] * (r[1][1] * r[2][2] - r[1][2] * r[2][1])
334 - r[0][1] * (r[1][0] * r[2][2] - r[1][2] * r[2][0])
335 + r[0][2] * (r[1][0] * r[2][1] - r[1][1] * r[2][0]);
336 let inv_det = if det.abs() < 1e-12 { 0.0 } else { 1.0 / det };
337 let inv_r = [
338 [
339 (r[1][1] * r[2][2] - r[1][2] * r[2][1]) * inv_det,
340 (r[0][2] * r[2][1] - r[0][1] * r[2][2]) * inv_det,
341 (r[0][1] * r[1][2] - r[0][2] * r[1][1]) * inv_det,
342 ],
343 [
344 (r[1][2] * r[2][0] - r[1][0] * r[2][2]) * inv_det,
345 (r[0][0] * r[2][2] - r[0][2] * r[2][0]) * inv_det,
346 (r[0][2] * r[1][0] - r[0][0] * r[1][2]) * inv_det,
347 ],
348 [
349 (r[1][0] * r[2][1] - r[1][1] * r[2][0]) * inv_det,
350 (r[0][1] * r[2][0] - r[0][0] * r[2][1]) * inv_det,
351 (r[0][0] * r[1][1] - r[0][1] * r[1][0]) * inv_det,
352 ],
353 ];
354
355 AxialGrid {
356 dims,
357 voxel_size: (vsx, vsy, vsz),
358 affine: [
359 vsx, 0.0, 0.0, world_min[0],
360 0.0, vsy, 0.0, world_min[1],
361 0.0, 0.0, vsz, world_min[2],
362 0.0, 0.0, 0.0, 1.0,
363 ],
364 src_dims: (nx, ny, nz),
365 world_min,
366 inv_r,
367 t,
368 }
369}
370
371impl AxialGrid {
372 fn source_coord(&self, ni: usize, nj: usize, nk: usize) -> Option<(f64, f64, f64)> {
374 let (nx, ny, nz) = self.src_dims;
375 let dx = self.world_min[0] + ni as f64 * self.voxel_size.0 - self.t[0];
376 let dy = self.world_min[1] + nj as f64 * self.voxel_size.1 - self.t[1];
377 let dz = self.world_min[2] + nk as f64 * self.voxel_size.2 - self.t[2];
378 let ox = self.inv_r[0][0] * dx + self.inv_r[0][1] * dy + self.inv_r[0][2] * dz;
379 let oy = self.inv_r[1][0] * dx + self.inv_r[1][1] * dy + self.inv_r[1][2] * dz;
380 let oz = self.inv_r[2][0] * dx + self.inv_r[2][1] * dy + self.inv_r[2][2] * dz;
381 let inside = ox >= -0.5
382 && ox <= nx as f64 - 0.5
383 && oy >= -0.5
384 && oy <= ny as f64 - 0.5
385 && oz >= -0.5
386 && oz <= nz as f64 - 0.5;
387 if inside { Some((ox, oy, oz)) } else { None }
388 }
389
390 pub fn sample(&self, data: &[f64]) -> Vec<f64> {
392 let (nx, ny, nz) = self.src_dims;
393 let (dx, dy, dz) = self.dims;
394 let mut out = vec![0.0f64; dx * dy * dz];
395 for nk in 0..dz {
396 for nj in 0..dy {
397 for ni in 0..dx {
398 if let Some((ox, oy, oz)) = self.source_coord(ni, nj, nk) {
399 out[ni + nj * dx + nk * dx * dy] =
400 trilinear_sample(data, nx, ny, nz, ox, oy, oz);
401 }
402 }
403 }
404 }
405 out
406 }
407
408 pub fn sample_nearest(&self, data: &[u8]) -> Vec<u8> {
410 let (nx, ny, nz) = self.src_dims;
411 let (dx, dy, dz) = self.dims;
412 let mut out = vec![0u8; dx * dy * dz];
413 for nk in 0..dz {
414 for nj in 0..dy {
415 for ni in 0..dx {
416 if let Some((ox, oy, oz)) = self.source_coord(ni, nj, nk) {
417 let xi = (ox.round() as isize).clamp(0, nx as isize - 1) as usize;
418 let yi = (oy.round() as isize).clamp(0, ny as isize - 1) as usize;
419 let zi = (oz.round() as isize).clamp(0, nz as isize - 1) as usize;
420 out[ni + nj * dx + nk * dx * dy] = data[xi + yi * nx + zi * nx * ny];
421 }
422 }
423 }
424 }
425 out
426 }
427}
428
429fn trilinear_sample(data: &[f64], nx: usize, ny: usize, nz: usize, x: f64, y: f64, z: f64) -> f64 {
431 let x0 = (x.floor() as isize).clamp(0, nx as isize - 1) as usize;
432 let y0 = (y.floor() as isize).clamp(0, ny as isize - 1) as usize;
433 let z0 = (z.floor() as isize).clamp(0, nz as isize - 1) as usize;
434 let x1 = (x0 + 1).min(nx - 1);
435 let y1 = (y0 + 1).min(ny - 1);
436 let z1 = (z0 + 1).min(nz - 1);
437 let (fx, fy, fz) = (x - x0 as f64, y - y0 as f64, z - z0 as f64);
438 let idx = |a: usize, b: usize, c: usize| a + b * nx + c * nx * ny;
439
440 data[idx(x0, y0, z0)] * (1.0 - fx) * (1.0 - fy) * (1.0 - fz)
441 + data[idx(x1, y0, z0)] * fx * (1.0 - fy) * (1.0 - fz)
442 + data[idx(x0, y1, z0)] * (1.0 - fx) * fy * (1.0 - fz)
443 + data[idx(x1, y1, z0)] * fx * fy * (1.0 - fz)
444 + data[idx(x0, y0, z1)] * (1.0 - fx) * (1.0 - fy) * fz
445 + data[idx(x1, y0, z1)] * fx * (1.0 - fy) * fz
446 + data[idx(x0, y1, z1)] * (1.0 - fx) * fy * fz
447 + data[idx(x1, y1, z1)] * fx * fy * fz
448}
449
450pub struct ResampledVolume {
452 pub data: Vec<f64>,
453 pub dims: (usize, usize, usize),
454 pub voxel_size: (f64, f64, f64),
455 pub affine: [f64; 16],
456}
457
458pub struct ResampledComplex {
460 pub magnitude: Vec<f64>,
461 pub phase: Vec<f64>,
463 pub dims: (usize, usize, usize),
464 pub voxel_size: (f64, f64, f64),
465 pub affine: [f64; 16],
466}
467
468#[derive(Debug, Clone, Copy)]
470pub struct AxialResampleParams {
471 pub noise_fill_fraction: Option<f64>,
476}
477
478impl Default for AxialResampleParams {
479 fn default() -> Self {
480 Self { noise_fill_fraction: Some(0.1) }
481 }
482}
483
484pub fn resample_to_axial(
488 data: &[f64],
489 nx: usize,
490 ny: usize,
491 nz: usize,
492 affine: &[f64; 16],
493) -> ResampledVolume {
494 let grid = axial_grid_for(nx, ny, nz, affine);
495 ResampledVolume {
496 data: grid.sample(data),
497 dims: grid.dims,
498 voxel_size: grid.voxel_size,
499 affine: grid.affine,
500 }
501}
502
503pub fn resample_mask_to_axial(
505 mask: &[u8],
506 nx: usize,
507 ny: usize,
508 nz: usize,
509 affine: &[f64; 16],
510) -> Vec<u8> {
511 axial_grid_for(nx, ny, nz, affine).sample_nearest(mask)
512}
513
514pub fn resample_complex_to_axial(
524 magnitude: &[f64],
525 phase: &[f64],
526 nx: usize,
527 ny: usize,
528 nz: usize,
529 affine: &[f64; 16],
530 params: &AxialResampleParams,
531) -> ResampledComplex {
532 let n = nx * ny * nz;
533 assert_eq!(magnitude.len(), n, "magnitude length does not match dimensions");
534 assert_eq!(phase.len(), n, "phase length does not match dimensions");
535
536 let real: Vec<f64> = (0..n).map(|i| magnitude[i] * phase[i].cos()).collect();
537 let imag: Vec<f64> = (0..n).map(|i| magnitude[i] * phase[i].sin()).collect();
538
539 let grid = axial_grid_for(nx, ny, nz, affine);
540 let real_r = grid.sample(&real);
541 let imag_r = grid.sample(&imag);
542
543 let out_n = real_r.len();
544 let mut mag_out = Vec::with_capacity(out_n);
545 let mut pha_out = Vec::with_capacity(out_n);
546 for i in 0..out_n {
547 mag_out.push(real_r[i].hypot(imag_r[i]));
548 pha_out.push(imag_r[i].atan2(real_r[i]));
549 }
550
551 if let Some(frac) = params.noise_fill_fraction {
552 let zeros = pha_out.iter().filter(|p| **p == 0.0).count();
553 if out_n > 0 && (zeros as f64 / out_n as f64) >= frac {
554 let mut rng = SplitMix64::new(seed_from_slice(&pha_out));
556 for p in pha_out.iter_mut() {
557 if *p == 0.0 {
558 *p = rng.uniform_signed_pi();
559 }
560 }
561 }
562 }
563
564 ResampledComplex {
565 magnitude: mag_out,
566 phase: pha_out,
567 dims: grid.dims,
568 voxel_size: grid.voxel_size,
569 affine: grid.affine,
570 }
571}
572
573fn seed_from_slice(data: &[f64]) -> u64 {
575 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
576 for v in data {
577 for b in v.to_bits().to_le_bytes() {
578 h ^= b as u64;
579 h = h.wrapping_mul(0x0000_0100_0000_01b3);
580 }
581 }
582 h | 1
583}
584
585struct SplitMix64(u64);
587
588impl SplitMix64 {
589 fn new(seed: u64) -> Self {
590 Self(seed)
591 }
592 fn next_u64(&mut self) -> u64 {
593 self.0 = self.0.wrapping_add(0x9e37_79b9_7f4a_7c15);
594 let mut z = self.0;
595 z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
596 z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
597 z ^ (z >> 31)
598 }
599 fn uniform_signed_pi(&mut self) -> f64 {
601 let u = (self.next_u64() >> 11) as f64 / (1u64 << 53) as f64; (u - 0.5) * 2.0 * std::f64::consts::PI
603 }
604}
605
606#[cfg(test)]
607mod tests {
608 use super::*;
609 use std::f64::consts::PI;
610
611 fn affine_from(r: [[f64; 3]; 3], t: [f64; 3]) -> [f64; 16] {
613 [
614 r[0][0], r[0][1], r[0][2], t[0],
615 r[1][0], r[1][1], r[1][2], t[1],
616 r[2][0], r[2][1], r[2][2], t[2],
617 0.0, 0.0, 0.0, 1.0,
618 ]
619 }
620
621 fn identity_affine(vs: (f64, f64, f64)) -> [f64; 16] {
622 affine_from([[vs.0, 0.0, 0.0], [0.0, vs.1, 0.0], [0.0, 0.0, vs.2]], [0.0, 0.0, 0.0])
623 }
624
625 fn ukb_swi_affine() -> [f64; 16] {
627 affine_from(
628 [
629 [0.7976, -0.0273, -0.1075],
630 [0.0141, 0.7358, -1.1647],
631 [0.0370, 0.3092, 2.7626],
632 ],
633 [-101.6, -87.4, -60.2],
634 )
635 }
636
637 #[test]
638 fn voxel_sizes_are_column_norms() {
639 let (sx, sy, sz) = voxel_sizes_from_affine(&ukb_swi_affine());
640 assert!((sx - 0.799).abs() < 0.002, "{sx}");
641 assert!((sy - 0.799).abs() < 0.002, "{sy}");
642 assert!((sz - 3.0).abs() < 0.002, "{sz}");
643 }
644
645 #[test]
646 fn b0_direction_axial_is_z() {
647 for vs in [(1.0, 1.0, 1.0), (0.8, 0.8, 3.0), (2.0, 0.5, 1.0)] {
648 let (bx, by, bz) = b0_direction_from_affine(&identity_affine(vs));
649 assert!(bx.abs() < 1e-9 && by.abs() < 1e-9, "{vs:?} -> {bx} {by}");
650 assert!((bz - 1.0).abs() < 1e-9, "{vs:?} -> {bz}");
651 assert!(b0_angle_from_affine(&identity_affine(vs)) < 1e-6);
652 }
653 }
654
655 #[test]
659 fn b0_direction_anisotropic_oblique() {
660 let a = ukb_swi_affine();
661 let (bx, by, bz) = b0_direction_from_affine(&a);
662 assert!((bx - 0.0463).abs() < 1e-3, "bx={bx}");
664 assert!((by - 0.3871).abs() < 1e-3, "by={by}");
665 assert!((bz - 0.9209).abs() < 1e-3, "bz={bz}");
666
667 let angle = b0_angle_from_affine(&a);
669 assert!((angle - 22.9).abs() < 0.5, "angle={angle}");
670
671 let naive = {
673 let r = [[a[0], a[1], a[2]], [a[4], a[5], a[6]], [a[8], a[9], a[10]]];
674 let det = r[0][0] * (r[1][1] * r[2][2] - r[1][2] * r[2][1])
675 - r[0][1] * (r[1][0] * r[2][2] - r[1][2] * r[2][0])
676 + r[0][2] * (r[1][0] * r[2][1] - r[1][1] * r[2][0]);
677 let v = [
678 (r[0][1] * r[1][2] - r[0][2] * r[1][1]) / det,
679 (r[0][2] * r[1][0] - r[0][0] * r[1][2]) / det,
680 (r[0][0] * r[1][1] - r[0][1] * r[1][0]) / det,
681 ];
682 let n = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
683 [v[0] / n, v[1] / n, v[2] / n]
684 };
685 let dot = (bx * naive[0] + by * naive[1] + bz * naive[2]).abs().clamp(0.0, 1.0);
686 let between = dot.acos().to_degrees();
687 assert!(between > 30.0, "the two differ by {between}°, expected >30");
688 }
689
690 #[test]
693 fn b0_direction_isotropic_oblique_agrees_with_naive() {
694 let (c, s) = (30.0f64.to_radians().cos(), 30.0f64.to_radians().sin());
695 let a = affine_from([[1.0, 0.0, 0.0], [0.0, c, -s], [0.0, s, c]], [0.0; 3]);
696 let (bx, by, bz) = b0_direction_from_affine(&a);
697 assert!(bx.abs() < 1e-9, "{bx}");
698 assert!((by - s).abs() < 1e-9, "{by} vs {s}");
699 assert!((bz - c).abs() < 1e-9, "{bz} vs {c}");
700 assert!((b0_angle_from_affine(&a) - 30.0).abs() < 1e-6);
701 }
702
703 #[test]
704 fn obliquity_matches_nibabel() {
705 let axes = obliquity_axes_from_affine(&ukb_swi_affine());
707 for (got, want) in axes.iter().zip([2.838, 22.869, 22.947]) {
708 assert!((got - want).abs() < 0.05, "{axes:?}");
709 }
710 let norm = obliquity_from_affine(&ukb_swi_affine());
711 assert!((norm - 32.521).abs() < 0.05, "{norm}");
712 assert!(obliquity_from_affine(&identity_affine((0.8, 0.8, 3.0))) < 1e-9);
714 }
715
716 #[test]
717 fn axial_grid_of_an_axial_volume_is_the_same_grid() {
718 let a = identity_affine((1.0, 1.0, 2.0));
719 let g = axial_grid_for(4, 5, 6, &a);
720 assert_eq!(g.dims, (4, 5, 6));
721 let (vx, vy, vz) = g.voxel_size;
722 assert!((vx - 1.0).abs() < 1e-9 && (vy - 1.0).abs() < 1e-9 && (vz - 2.0).abs() < 1e-9);
723 let data: Vec<f64> = (0..4 * 5 * 6).map(|i| i as f64).collect();
725 let out = resample_to_axial(&data, 4, 5, 6, &a);
726 for (i, (got, want)) in out.data.iter().zip(data.iter()).enumerate() {
727 assert!((got - want).abs() < 1e-6, "voxel {i}: {got} vs {want}");
728 }
729 }
730
731 #[test]
732 fn resampled_affine_is_cardinal_and_b0_is_z() {
733 let g = axial_grid_for(32, 32, 12, &ukb_swi_affine());
734 for (i, v) in g.affine.iter().enumerate() {
735 let diagonal = matches!(i, 0 | 5 | 10 | 15);
736 let translation = matches!(i, 3 | 7 | 11);
737 if !diagonal && !translation {
738 assert!(v.abs() < 1e-12, "affine[{i}] = {v} should be 0");
739 }
740 }
741 assert!(b0_angle_from_affine(&g.affine) < 1e-9);
742 assert!(obliquity_from_affine(&g.affine) < 1e-9);
743 assert!(g.dims.0 >= 32 && g.dims.1 >= 32 && g.dims.2 >= 12, "{:?}", g.dims);
745 }
746
747 #[test]
750 fn complex_resampling_survives_wraps_where_scalar_does_not() {
751 let (nx, ny, nz) = (24, 24, 8);
752 let n = nx * ny * nz;
753 let (c, s) = (20.0f64.to_radians().cos(), 20.0f64.to_radians().sin());
755 let a = affine_from([[1.0, 0.0, 0.0], [0.0, c, -s], [0.0, s, c]], [0.0; 3]);
756
757 let true_phase = |x: f64, y: f64, _z: f64| 0.8 * x + 0.3 * y;
759 let mut mag = vec![0.0; n];
760 let mut pha = vec![0.0; n];
761 for k in 0..nz {
762 for j in 0..ny {
763 for i in 0..nx {
764 let idx = i + j * nx + k * nx * ny;
765 mag[idx] = 100.0;
766 pha[idx] = wrap(true_phase(i as f64, j as f64, k as f64));
767 }
768 }
769 }
770 let wraps = (0..nz)
772 .flat_map(|k| (0..ny).flat_map(move |j| (1..nx).map(move |i| (i, j, k))))
773 .filter(|&(i, j, k)| {
774 let idx = i + j * nx + k * nx * ny;
775 (pha[idx] - pha[idx - 1]).abs() > PI
776 })
777 .count();
778 assert!(wraps > 500, "test data should contain plenty of wraps, found {wraps}");
779
780 let cpx = resample_complex_to_axial(&mag, &pha, nx, ny, nz, &a, &AxialResampleParams { noise_fill_fraction: None });
781 let scalar = resample_to_axial(&pha, nx, ny, nz, &a);
782 let grid = axial_grid_for(nx, ny, nz, &a);
783
784 let (mut cpx_err, mut scalar_err, mut count) = (0.0f64, 0.0f64, 0usize);
786 let (dx, dy, dz) = grid.dims;
787 for k in 0..dz {
788 for j in 0..dy {
789 for i in 0..dx {
790 let Some((ox, oy, oz)) = grid.source_coord(i, j, k) else { continue };
791 if ox < 1.0 || ox > nx as f64 - 2.0 || oy < 1.0 || oy > ny as f64 - 2.0 || oz < 1.0 || oz > nz as f64 - 2.0 {
793 continue;
794 }
795 let idx = i + j * dx + k * dx * dy;
796 let want = wrap(true_phase(ox, oy, oz));
797 cpx_err += wrap(cpx.phase[idx] - want).abs();
798 scalar_err += wrap(scalar.data[idx] - want).abs();
799 count += 1;
800 }
801 }
802 }
803 assert!(count > 500, "too few interior samples: {count}");
804 let (cpx_err, scalar_err) = (cpx_err / count as f64, scalar_err / count as f64);
805 assert!(cpx_err < 0.05, "complex resampling error {cpx_err} rad is too high");
806 assert!(
807 scalar_err > 10.0 * cpx_err,
808 "scalar {scalar_err} should be far worse than complex {cpx_err}"
809 );
810 for (idx, m) in cpx.magnitude.iter().enumerate() {
816 if grid_interior(&grid, idx) {
817 assert!(*m > 90.0 && *m < 100.5, "magnitude {m} at {idx} outside the expected dip");
818 }
819 }
820 }
821
822 fn grid_interior(grid: &AxialGrid, idx: usize) -> bool {
823 let (dx, dy, _) = grid.dims;
824 let i = idx % dx;
825 let j = (idx / dx) % dy;
826 let k = idx / (dx * dy);
827 grid.source_coord(i, j, k)
828 .map(|(ox, oy, oz)| {
829 let (nx, ny, nz) = grid.src_dims;
830 ox > 1.0 && ox < nx as f64 - 2.0 && oy > 1.0 && oy < ny as f64 - 2.0 && oz > 1.0 && oz < nz as f64 - 2.0
831 })
832 .unwrap_or(false)
833 }
834
835 fn wrap(p: f64) -> f64 {
836 let mut v = (p + PI) % (2.0 * PI);
837 if v < 0.0 {
838 v += 2.0 * PI;
839 }
840 v - PI
841 }
842
843 #[test]
846 fn round_trip_to_axial_and_back_recovers_the_original() {
847 let (nx, ny, nz) = (20, 22, 10);
848 let n = nx * ny * nz;
849 let a = ukb_swi_affine();
850 let data: Vec<f64> = (0..n)
852 .map(|i| {
853 let (x, y, z) = (i % nx, (i / nx) % ny, i / (nx * ny));
854 (x as f64 * 0.1).sin() + (y as f64 * 0.07).cos() + z as f64 * 0.02
855 })
856 .collect();
857
858 let grid = axial_grid_for(nx, ny, nz, &a);
859 let there = resample_to_axial(&data, nx, ny, nz, &a);
860 let back = resample_onto(&there.data, grid.dims, &grid.affine, (nx, ny, nz), &a).unwrap();
861
862 let (mut err, mut count) = (0.0f64, 0usize);
865 for z in 2..nz - 2 {
866 for y in 2..ny - 2 {
867 for x in 2..nx - 2 {
868 let i = x + y * nx + z * nx * ny;
869 err += (back[i] - data[i]).abs();
870 count += 1;
871 }
872 }
873 }
874 let mean = err / count as f64;
875 assert!(mean < 0.05, "round trip lost {mean} per voxel on a smooth field");
876 }
877
878 #[test]
879 fn round_trip_puts_the_data_back_on_the_original_grid() {
880 let (nx, ny, nz) = (16, 16, 8);
881 let a = ukb_swi_affine();
882 let grid = axial_grid_for(nx, ny, nz, &a);
883 assert_ne!(grid.dims, (nx, ny, nz), "the axial grid should differ, else this proves nothing");
884 let there = vec![1.0f64; grid.dims.0 * grid.dims.1 * grid.dims.2];
885 let back = resample_onto(&there, grid.dims, &grid.affine, (nx, ny, nz), &a).unwrap();
886 assert_eq!(back.len(), nx * ny * nz, "output must be on the acquired grid");
887 assert!(back.iter().filter(|v| **v > 0.5).count() > (nx * ny * nz) * 9 / 10);
889 }
890
891 #[test]
892 fn identity_map_is_a_no_op() {
893 let (nx, ny, nz) = (6, 5, 4);
894 let a = identity_affine((1.0, 2.0, 3.0));
895 let data: Vec<f64> = (0..nx * ny * nz).map(|i| i as f64).collect();
896 let out = resample_onto(&data, (nx, ny, nz), &a, (nx, ny, nz), &a).unwrap();
897 for (got, want) in out.iter().zip(data.iter()) {
898 assert!((got - want).abs() < 1e-9, "{got} vs {want}");
899 }
900 }
901
902 #[test]
903 fn singular_affine_is_reported_not_panicked() {
904 let zero = [0.0f64; 16];
905 let a = identity_affine((1.0, 1.0, 1.0));
906 assert!(resample_onto(&[0.0; 8], (2, 2, 2), &zero, (2, 2, 2), &a).is_none());
907 assert!(GridMap::new((2, 2, 2), &zero, (2, 2, 2), &a).is_none());
908 }
909
910 #[test]
911 fn mask_round_trip_stays_binary() {
912 let (nx, ny, nz) = (16, 16, 8);
913 let a = ukb_swi_affine();
914 let mut mask = vec![0u8; nx * ny * nz];
915 for z in 2..nz - 2 {
916 for y in 4..ny - 4 {
917 for x in 4..nx - 4 {
918 mask[x + y * nx + z * nx * ny] = 1;
919 }
920 }
921 }
922 let grid = axial_grid_for(nx, ny, nz, &a);
923 let there = resample_mask_to_axial(&mask, nx, ny, nz, &a);
924 let back = resample_mask_onto(&there, grid.dims, &grid.affine, (nx, ny, nz), &a).unwrap();
925 assert!(back.iter().all(|v| *v == 0 || *v == 1));
926 assert!(back.iter().filter(|v| **v == 1).count() > 100, "mask should survive the trip");
927 }
928
929 #[test]
932 fn complex_round_trip_preserves_wraps() {
933 let (nx, ny, nz) = (20, 20, 8);
934 let n = nx * ny * nz;
935 let a = ukb_swi_affine();
936 let mag = vec![100.0f64; n];
937 let pha: Vec<f64> = (0..n)
938 .map(|i| {
939 let (x, y) = (i % nx, (i / nx) % ny);
940 wrap(0.6 * x as f64 + 0.2 * y as f64)
941 })
942 .collect();
943
944 let grid = axial_grid_for(nx, ny, nz, &a);
945 let out = resample_complex_to_axial(&mag, &pha, nx, ny, nz, &a, &AxialResampleParams { noise_fill_fraction: None });
946 let (_, back) = resample_complex_onto(&out.magnitude, &out.phase, grid.dims, &grid.affine, (nx, ny, nz), &a).unwrap();
947
948 let (mut err, mut count) = (0.0f64, 0usize);
949 for z in 2..nz - 2 {
950 for y in 3..ny - 3 {
951 for x in 3..nx - 3 {
952 let i = x + y * nx + z * nx * ny;
953 err += wrap(back[i] - pha[i]).abs();
954 count += 1;
955 }
956 }
957 }
958 let mean = err / count as f64;
959 assert!(mean < 0.25, "complex round trip drifted {mean} rad per voxel");
960 }
961
962 #[test]
963 fn noise_fill_is_deterministic_and_bounded() {
964 let (nx, ny, nz) = (16, 16, 6);
965 let n = nx * ny * nz;
966 let (c, s) = (25.0f64.to_radians().cos(), 25.0f64.to_radians().sin());
967 let a = affine_from([[1.0, 0.0, 0.0], [0.0, c, -s], [0.0, s, c]], [0.0; 3]);
968 let mag = vec![50.0; n];
969 let pha: Vec<f64> = (0..n).map(|i| wrap(0.4 * (i % nx) as f64)).collect();
970
971 let p = AxialResampleParams::default();
972 let a1 = resample_complex_to_axial(&mag, &pha, nx, ny, nz, &a, &p);
973 let a2 = resample_complex_to_axial(&mag, &pha, nx, ny, nz, &a, &p);
974 assert_eq!(a1.phase, a2.phase, "same input must give the same volume");
975 assert!(a1.phase.iter().all(|v| v.is_finite() && v.abs() <= PI + 1e-9));
976
977 let off = resample_complex_to_axial(&mag, &pha, nx, ny, nz, &a, &AxialResampleParams { noise_fill_fraction: None });
979 assert!(off.phase.iter().any(|v| *v == 0.0));
980 assert!(off.phase.iter().filter(|v| **v == 0.0).count() > a1.phase.iter().filter(|v| **v == 0.0).count());
981 }
982
983 #[test]
984 fn mask_resampling_stays_binary() {
985 let (nx, ny, nz) = (12, 12, 6);
986 let (c, s) = (15.0f64.to_radians().cos(), 15.0f64.to_radians().sin());
987 let a = affine_from([[1.0, 0.0, 0.0], [0.0, c, -s], [0.0, s, c]], [0.0; 3]);
988 let mut mask = vec![0u8; nx * ny * nz];
989 for k in 1..nz - 1 {
990 for j in 3..ny - 3 {
991 for i in 3..nx - 3 {
992 mask[i + j * nx + k * nx * ny] = 1;
993 }
994 }
995 }
996 let out = resample_mask_to_axial(&mask, nx, ny, nz, &a);
997 assert!(out.iter().all(|v| *v == 0 || *v == 1), "mask must stay binary");
998 assert!(out.iter().filter(|v| **v == 1).count() > 100, "mask should survive");
999 }
1000}