1use num_complex::Complex32;
31use crate::kernels::dipole::dipole_kernel_f32;
32use crate::utils::simd_ops::{
33 dot_product_f32, norm_squared_f32, axpy_f32, xpby_f32,
34 compute_p_weights_f32, negate_f32,
35};
36use crate::Grid;
37
38use super::medi::{
39 MediWorkspace, MediOpBuffers,
40 apply_medi_operator_core, apply_dipole_conv, compute_rhs_inplace,
41 dataterm_mask_f32, gradient_mask_f32, fgrad_periodic_inplace_f32,
42};
43
44#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
46#[derive(Clone, Debug)]
47pub struct TfiParams {
48 pub lambda: f64,
50 pub precond: f64,
52 pub merit: bool,
54 pub data_weighting: i32,
56 pub percentage: f64,
58 pub cg_tol: f64,
60 pub cg_max_iter: usize,
62 pub max_iter: usize,
64 pub tol: f64,
66}
67
68impl Default for TfiParams {
69 fn default() -> Self {
70 Self {
71 lambda: 7.5e-5,
75 precond: 30.0,
76 merit: false,
77 data_weighting: 1,
78 percentage: 0.9,
79 cg_tol: 0.01,
80 cg_max_iter: 100,
81 max_iter: 10,
82 tol: 0.1,
83 }
84 }
85}
86
87#[allow(clippy::too_many_arguments)]
92fn cg_solve_tfi<F>(
93 ws: &mut MediWorkspace,
94 precond: &[f32],
95 w: &[Complex32],
96 d_kernel: &[f32],
97 mx: &[f32],
98 my: &[f32],
99 mz: &[f32],
100 vr: &[f32],
101 lambda: f32,
102 b: &[f32],
103 x: &mut [f32],
104 tol: f32,
105 max_iter: usize,
106 mut progress_callback: F,
107) where
108 F: FnMut(usize, usize),
109{
110 let n = ws.n_total;
111 let (nx, ny, nz) = (ws.nx, ws.ny, ws.nz);
112 let (vsx, vsy, vsz) = (ws.vsx, ws.vsy, ws.vsz);
113
114 x.fill(0.0);
115 ws.cg_r.copy_from_slice(b);
116 ws.cg_p.copy_from_slice(&ws.cg_r);
117
118 let mut rsold: f32 = norm_squared_f32(&ws.cg_r);
119 let b_norm: f32 = norm_squared_f32(b).sqrt();
120 if b_norm < 1e-10 {
121 return;
122 }
123
124 let mut p_scaled = vec![0.0f32; n];
126
127 for cg_iter in 0..max_iter {
128 progress_callback(cg_iter + 1, max_iter);
129
130 for i in 0..n {
132 p_scaled[i] = precond[i] * ws.cg_p[i];
133 }
134
135 {
137 let mut bufs = MediOpBuffers {
138 gx: &mut ws.gx,
139 gy: &mut ws.gy,
140 gz: &mut ws.gz,
141 reg_x: &mut ws.reg_x,
142 reg_y: &mut ws.reg_y,
143 reg_z: &mut ws.reg_z,
144 div_buf: &mut ws.div_buf,
145 dipole_buf: &mut ws.dipole_buf,
146 complex_buf: &mut ws.complex_buf,
147 complex_buf2: &mut ws.complex_buf2,
148 };
149 apply_medi_operator_core(
150 &mut ws.fft_ws, &mut bufs, n, nx, ny, nz, vsx, vsy, vsz,
151 &p_scaled, w, d_kernel, mx, my, mz, vr, lambda, &mut ws.cg_ap,
152 );
153 }
154
155 for i in 0..n {
157 ws.cg_ap[i] *= precond[i];
158 }
159
160 let pap: f32 = dot_product_f32(&ws.cg_p, &ws.cg_ap);
161 if pap.abs() < 1e-15 {
162 break;
163 }
164
165 let alpha = rsold / pap;
166 axpy_f32(x, alpha, &ws.cg_p);
167 axpy_f32(&mut ws.cg_r, -alpha, &ws.cg_ap);
168
169 let rsnew: f32 = norm_squared_f32(&ws.cg_r);
170 let residual = rsnew.sqrt();
171 if residual < tol * b_norm {
172 break;
173 }
174
175 let beta = rsnew / rsold;
176 xpby_f32(&mut ws.cg_p, &ws.cg_r, beta);
177 rsold = rsnew;
178 }
179}
180
181#[allow(clippy::too_many_arguments)]
200pub fn tfi(
201 total_field: &[f64],
202 n_std: &[f64],
203 magnitude: &[f64],
204 mask: &[u8],
205 grid: &Grid,
206 bdir: (f64, f64, f64),
207 params: &TfiParams,
208 mut progress: impl FnMut(usize, usize),
209) -> Vec<f64> {
210 let (nx, ny, nz) = grid.dims;
211 let n_total = grid.n_total();
212
213 let vsx_f32 = grid.vsx() as f32;
214 let vsy_f32 = grid.vsy() as f32;
215 let vsz_f32 = grid.vsz() as f32;
216 let lambda_f32 = params.lambda as f32;
217 let bdir_f32 = (bdir.0 as f32, bdir.1 as f32, bdir.2 as f32);
218 let percentage_f32 = params.percentage as f32;
219 let cg_tol_f32 = params.cg_tol as f32;
220 let tol_f32 = params.tol as f32;
221 let max_iter = params.max_iter;
222 let cg_max_iter = params.cg_max_iter;
223 let data_weighting = params.data_weighting;
224
225 let field_f32: Vec<f32> = total_field.iter().map(|&v| v as f32).collect();
226 let n_std_f32: Vec<f32> = n_std.iter().map(|&v| v as f32).collect();
227 let magnitude_f32: Vec<f32> = magnitude.iter().map(|&v| v as f32).collect();
228
229 let mut ws = MediWorkspace::new(grid);
230
231 let precond_out = params.precond as f32;
233 let precond: Vec<f32> = mask.iter()
234 .map(|&m| if m != 0 { 1.0 } else { precond_out })
235 .collect();
236
237 let d_kernel = dipole_kernel_f32(grid, bdir_f32);
239
240 let mut tempn: Vec<f32> = n_std_f32.clone();
243 for i in 0..n_total {
244 if mask[i] == 0 {
245 tempn[i] = 0.0;
246 }
247 }
248 let m = dataterm_mask_f32(data_weighting, &tempn, mask);
249
250 let b0: Vec<Complex32> = field_f32.iter()
252 .zip(m.iter())
253 .map(|(&f, &mi)| {
254 let phase = Complex32::new(0.0, f);
255 mi * phase.exp()
256 })
257 .collect();
258
259 let (mut w_gx, mut w_gy, mut w_gz) = gradient_mask_f32(
262 &magnitude_f32, mask, nx, ny, nz, vsx_f32, vsy_f32, vsz_f32, percentage_f32,
263 );
264 if !w_gx.iter().any(|&v| v != 0.0) { w_gx = magnitude_f32.clone(); }
266 if !w_gy.iter().any(|&v| v != 0.0) { w_gy = magnitude_f32.clone(); }
267 if !w_gz.iter().any(|&v| v != 0.0) { w_gz = magnitude_f32.clone(); }
268 for i in 0..n_total {
270 if mask[i] == 0 {
271 w_gx[i] = 1.0;
272 w_gy[i] = 1.0;
273 w_gz[i] = 1.0;
274 }
275 }
276
277 let mut y = vec![0.0f32; n_total];
279 let mut chi = vec![0.0f32; n_total];
280 let mut dy = vec![0.0f32; n_total];
281 let mut rhs = vec![0.0f32; n_total];
282 let mut vr = vec![0.0f32; n_total];
283 let mut w: Vec<Complex32> = vec![Complex32::new(0.0, 0.0); n_total];
284 let mut y_prev = vec![0.0f32; n_total];
285
286 let beta = 1.49e-8_f32;
287 let total_steps = max_iter * cg_max_iter;
288
289 for iter in 0..max_iter {
290 y_prev.copy_from_slice(&y);
291
292 for i in 0..n_total {
294 chi[i] = precond[i] * y[i];
295 }
296
297 fgrad_periodic_inplace_f32(
299 &mut ws.gx, &mut ws.gy, &mut ws.gz,
300 &chi, nx, ny, nz, vsx_f32, vsy_f32, vsz_f32,
301 );
302 compute_p_weights_f32(&mut vr, &w_gx, &w_gy, &w_gz, &ws.gx, &ws.gy, &ws.gz, beta);
303
304 apply_dipole_conv(&mut ws.fft_ws, &chi, &d_kernel, &mut ws.dipole_buf, &mut ws.complex_buf);
306 for i in 0..n_total {
307 let phase = Complex32::new(0.0, ws.dipole_buf[i]);
308 w[i] = m[i] * phase.exp();
309 }
310
311 compute_rhs_inplace(&chi, &w, &b0, &d_kernel, &w_gx, &w_gy, &w_gz, &vr, lambda_f32, &mut rhs, &mut ws);
313
314 for i in 0..n_total {
316 rhs[i] *= precond[i];
317 }
318
319 negate_f32(&mut rhs);
321
322 let gn_iter = iter;
323 cg_solve_tfi(
324 &mut ws, &precond, &w, &d_kernel, &w_gx, &w_gy, &w_gz, &vr, lambda_f32,
325 &rhs, &mut dy, cg_tol_f32, cg_max_iter,
326 |cg_iter, cg_total| {
327 let current = gn_iter * cg_total + cg_iter;
328 progress(current, total_steps);
329 },
330 );
331
332 axpy_f32(&mut y, 1.0, &dy);
334
335 let norm_dy = norm_squared_f32(&dy).sqrt();
337 let norm_y = norm_squared_f32(&y_prev).sqrt();
338 let rel_change = norm_dy / (norm_y + 1e-6);
339 if rel_change < tol_f32 {
340 progress(total_steps, total_steps);
341 break;
342 }
343 }
344
345 let _ = params.merit;
349 y.iter()
350 .zip(precond.iter())
351 .zip(mask.iter())
352 .map(|((&yi, &pi), &m)| if m == 0 { 0.0 } else { (pi * yi) as f64 })
353 .collect()
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359
360 fn test_tfi_params() -> TfiParams {
361 TfiParams {
362 lambda: 1e-3, percentage: 0.9, cg_tol: 0.1,
363 cg_max_iter: 10, max_iter: 3, tol: 0.1,
364 ..TfiParams::default()
365 }
366 }
367
368 #[test]
369 fn test_tfi_zero_field() {
370 let n = 8;
371 let field = vec![0.0; n * n * n];
372 let mask = vec![1u8; n * n * n];
373 let mag = vec![1.0; n * n * n];
374 let n_std = vec![1.0; n * n * n];
375 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
376
377 let chi = tfi(
378 &field, &n_std, &mag, &mask, &grid,
379 (0.0, 0.0, 1.0), &test_tfi_params(), |_, _| {},
380 );
381
382 for &val in chi.iter() {
383 assert!(val.abs() < 1e-4, "Zero field should give near-zero chi, got {}", val);
384 }
385 }
386
387 #[test]
388 fn test_tfi_finite() {
389 let n = 8;
390 let field: Vec<f64> = (0..n*n*n).map(|i| (i as f64) * 0.001).collect();
391 let mask = vec![1u8; n * n * n];
392 let mag = vec![1.0; n * n * n];
393 let n_std = vec![1.0; n * n * n];
394 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
395
396 let chi = tfi(
397 &field, &n_std, &mag, &mask, &grid,
398 (0.0, 0.0, 1.0), &test_tfi_params(), |_, _| {},
399 );
400
401 for (i, &val) in chi.iter().enumerate() {
402 assert!(val.is_finite(), "Chi should be finite at index {}", i);
403 }
404 }
405
406 #[test]
407 fn test_tfi_finite_with_background() {
408 let n = 8;
410 let field: Vec<f64> = (0..n*n*n).map(|i| (i as f64) * 0.001).collect();
411 let mut mask = vec![0u8; n * n * n];
412 for z in 2..6 { for y in 2..6 { for x in 2..6 {
414 mask[x + y*n + z*n*n] = 1;
415 }}}
416 let mag = vec![1.0; n * n * n];
417 let n_std = vec![1.0; n * n * n];
418 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
419
420 let chi = tfi(
421 &field, &n_std, &mag, &mask, &grid,
422 (0.0, 0.0, 1.0), &test_tfi_params(), |_, _| {},
423 );
424
425 assert_eq!(chi.len(), n * n * n);
426 for (i, &val) in chi.iter().enumerate() {
427 assert!(val.is_finite(), "Chi should be finite at index {} (whole FOV)", i);
428 }
429 }
430
431 #[test]
434 #[ignore]
435 fn test_tfi_phantom() {
436
437 let base = "/home/ashley/repos/qsm/qsmci/qsmci/data/sim/dev";
438 let field_path = format!("{}/groundtruth/totalfield.nii.gz", base);
439 let mask_path = format!("{}/inputs/mask.nii.gz", base);
440 let mag_path = format!("{}/inputs/magnitude.nii.gz", base);
441 let chi_path = format!("{}/groundtruth/chimap.nii.gz", base);
442
443 if !std::path::Path::new(&field_path).exists() {
444 eprintln!("Skipping: {} not found", field_path);
445 return;
446 }
447
448 let field_nii = crate::io::load_nifti(&std::fs::read(&field_path).unwrap()).unwrap();
450 let (nx, ny, nz) = field_nii.dims;
451 let (vsx, vsy, vsz) = field_nii.voxel_size;
452 let n_total = nx * ny * nz;
453 eprintln!("Dims: {}x{}x{} voxel: {}x{}x{}", nx, ny, nz, vsx, vsy, vsz);
454
455 let field_ppm = field_nii.data;
456
457 let mask_nii = crate::io::load_nifti(&std::fs::read(&mask_path).unwrap()).unwrap();
459 let mask: Vec<u8> = mask_nii.data.iter().map(|&v| if v > 0.5 { 1 } else { 0 }).collect();
460 let mask_count = mask.iter().filter(|&&m| m != 0).count();
461 eprintln!("Mask voxels: {} / {}", mask_count, n_total);
462
463 let (mag4d, mag_dims, _, _) = crate::io::load_nifti_4d(&std::fs::read(&mag_path).unwrap()).unwrap();
465 let (mnx, mny, mnz, nt) = mag_dims;
466 assert_eq!(mnx * mny * mnz, n_total, "magnitude spatial dims mismatch");
467 let mut magnitude = vec![0.0f64; n_total];
468 for i in 0..n_total {
469 let mut ss = 0.0;
470 for t in 0..nt {
471 let v = mag4d[i + t * n_total];
472 ss += v * v;
473 }
474 magnitude[i] = ss.sqrt();
475 }
476
477 let chi_gt_nii = crate::io::load_nifti(&std::fs::read(&chi_path).unwrap()).unwrap();
479 let chi_gt_ppm = chi_gt_nii.data;
480
481 let bdir = (0.0, 0.0, 1.0);
483
484 let n_std = vec![1.0f64; n_total];
486 let grid = Grid::new(nx, ny, nz, vsx, vsy, vsz);
487
488 let lambda = std::env::var("TFI_LAMBDA").ok()
491 .and_then(|s| s.parse::<f64>().ok()).unwrap_or(1e-4);
492 let precond = std::env::var("TFI_PRECOND").ok()
493 .and_then(|s| s.parse::<f64>().ok()).unwrap_or(3.0);
494 let params = TfiParams {
495 lambda,
496 precond,
497 percentage: 0.9,
498 cg_tol: 0.01,
499 cg_max_iter: 100,
500 max_iter: 20,
501 tol: 0.005,
502 ..TfiParams::default()
503 };
504 eprintln!("params: lambda={} precond={}", lambda, precond);
505
506 eprintln!("Running TFI...");
507 let chi_ppm = tfi(
508 &field_ppm, &n_std, &magnitude, &mask, &grid, bdir, ¶ms,
509 |c, t| { if c % 200 == 0 || c == t { eprintln!(" progress {}/{}", c, t); } },
510 );
511
512 let idx: Vec<usize> = (0..n_total).filter(|&i| mask[i] != 0).collect();
514 let a: Vec<f64> = idx.iter().map(|&i| chi_ppm[i]).collect();
515 let b: Vec<f64> = idx.iter().map(|&i| chi_gt_ppm[i]).collect();
516 let nm = a.len() as f64;
517
518 let mean_a = a.iter().sum::<f64>() / nm;
519 let mean_b = b.iter().sum::<f64>() / nm;
520
521 let mut cov = 0.0; let mut va = 0.0; let mut vb = 0.0;
523 for k in 0..a.len() {
524 let da = a[k] - mean_a; let db = b[k] - mean_b;
525 cov += da * db; va += da * da; vb += db * db;
526 }
527 let corr = cov / (va.sqrt() * vb.sqrt());
528
529 let mut num = 0.0; let mut den = 0.0;
531 for k in 0..a.len() {
532 num += (a[k] - b[k]).powi(2);
533 den += b[k].powi(2);
534 }
535 let nrmse = 100.0 * (num / den).sqrt();
536
537 let mut num_d = 0.0; let mut den_d = 0.0;
539 for k in 0..a.len() {
540 num_d += ((a[k] - mean_a) - (b[k] - mean_b)).powi(2);
541 den_d += (b[k] - mean_b).powi(2);
542 }
543 let nrmse_d = 100.0 * (num_d / den_d).sqrt();
544
545 eprintln!("=== TFI dev phantom results (within mask) ===");
546 eprintln!(" Pearson correlation : {:.5}", corr);
547 eprintln!(" NRMSE : {:.3}", nrmse);
548 eprintln!(" detrended NRMSE : {:.3}", nrmse_d);
549 eprintln!(" chi_out mean={:.4} min={:.4} max={:.4}",
550 mean_a,
551 a.iter().cloned().fold(f64::MAX, f64::min),
552 a.iter().cloned().fold(f64::MIN, f64::max));
553 eprintln!(" chi_gt mean={:.4} min={:.4} max={:.4}",
554 mean_b,
555 b.iter().cloned().fold(f64::MAX, f64::min),
556 b.iter().cloned().fold(f64::MIN, f64::max));
557
558 assert!(corr > 0.95, "TFI correlation {:.5} below acceptance 0.95", corr);
559 }
560
561 #[test]
568 #[ignore]
569 fn test_tfi_background() {
570 let base = "/home/ashley/bids/derivatives/qsm-forward/sub-1/anat";
571 let total_path = format!("{}/sub-1_fieldmap.nii", base);
572 if !std::path::Path::new(&total_path).exists() {
573 eprintln!("Skipping: {} not found", total_path);
574 return;
575 }
576 let load = |p: &str| crate::io::load_nifti(&std::fs::read(p).unwrap()).unwrap();
577 let total_nii = load(&total_path);
578 let (nx, ny, nz) = total_nii.dims;
579 let (vsx, vsy, vsz) = total_nii.voxel_size;
580 let n = nx * ny * nz;
581 let total_field = total_nii.data; let local_field = load(&format!("{}/sub-1_fieldmap-local.nii", base)).data;
583 let chi_gt = load(&format!("{}/sub-1_Chimap.nii", base)).data;
584 let mask: Vec<u8> = load(&format!("{}/sub-1_mask.nii", base)).data
585 .iter().map(|&v| if v > 0.5 { 1 } else { 0 }).collect();
586
587 let grid = Grid::new(nx, ny, nz, vsx, vsy, vsz);
588 let bdir = (0.0, 0.0, 1.0);
589 let n_std = vec![1.0f64; n];
590 let magnitude = vec![1.0f64; n];
591
592 let std_in = |v: &[f64]| {
593 let m: Vec<f64> = (0..n).filter(|&i| mask[i] != 0).map(|i| v[i]).collect();
594 let mean = m.iter().sum::<f64>() / m.len() as f64;
595 (m.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / m.len() as f64).sqrt()
596 };
597 eprintln!("dims {}x{}x{} in-brain field std: total={:.5} local={:.5}",
598 nx, ny, nz, std_in(&total_field), std_in(&local_field));
599
600 let corr_in = |a: &[f64], b: &[f64]| {
601 let idx: Vec<usize> = (0..n).filter(|&i| mask[i] != 0).collect();
602 let (mut sa, mut sb) = (0.0, 0.0);
603 for &i in &idx { sa += a[i]; sb += b[i]; }
604 let (ma, mb) = (sa / idx.len() as f64, sb / idx.len() as f64);
605 let (mut num, mut da, mut db) = (0.0, 0.0, 0.0);
606 for &i in &idx {
607 num += (a[i] - ma) * (b[i] - mb);
608 da += (a[i] - ma).powi(2); db += (b[i] - mb).powi(2);
609 }
610 num / (da.sqrt() * db.sqrt())
611 };
612
613 let p_local = TfiParams { lambda: 1e-4, precond: 1.0, ..TfiParams::default() };
615 let chi_local = tfi(&local_field, &n_std, &magnitude, &mask, &grid, bdir, &p_local, |_, _| {});
616 eprintln!("TFI on LOCAL field (ceiling): corr={:.4}", corr_in(&chi_local, &chi_gt));
617
618 eprintln!("=== TFI on TOTAL field — precond × lambda sweep (corr vs GT χ, within mask) ===");
619 for &lambda in &[5e-5f64, 7.5e-5, 1e-4] {
620 for &precond in &[30.0f64] {
621 let params = TfiParams { lambda, precond, ..TfiParams::default() };
622 let chi = tfi(&total_field, &n_std, &magnitude, &mask, &grid, bdir, ¶ms, |_, _| {});
623 eprintln!(" lambda={:>6.0e} precond={:>5} corr={:.4}", lambda, precond, corr_in(&chi, &chi_gt));
624 }
625 }
626 }
627}