1use super::config::*;
12
13pub(crate) fn echo_magnitudes<'a>(
18 method: &str,
19 magnitude: Option<&'a [f64]>,
20 echo_times: &[f64],
21 n_voxels: usize,
22) -> Result<crate::inversion::EchoMagnitudes<'a>, PipelineError> {
23 let n_echoes = echo_times.len();
24 if n_echoes == 0 {
25 return Err(PipelineError::InvalidInput(format!("{method} needs the echo times")));
26 }
27 if let Some(problem) = crate::fieldmap::echo_spacing_problem(echo_times) {
28 return Err(PipelineError::InvalidInput(format!("{method}'s noise map (Fit_ppm_complex) {problem}")));
29 }
30 match magnitude {
31 Some(m) if m.len() == n_voxels * n_echoes => {
32 Ok(crate::inversion::EchoMagnitudes { data: m, n_echoes })
33 }
34 Some(m) => Err(PipelineError::InvalidInput(format!(
35 "{method} needs the magnitude of every echo ({n_echoes} volumes, {} values); got {} values",
36 n_voxels * n_echoes, m.len(),
37 ))),
38 None => Err(PipelineError::InvalidInput(format!(
39 "{method} needs the magnitude of every echo ({n_echoes} volumes)"
40 ))),
41 }
42}
43
44pub(crate) fn scan_echo_times(
47 method: &str,
48 te: crate::EchoTime,
49 metadata: &ScanMetadata,
50) -> Result<Vec<f64>, PipelineError> {
51 te.resolve(&metadata.echo_times)
52 .map_err(|e| PipelineError::InvalidInput(format!("{method}: {e}")))?;
53 Ok(metadata.echo_times.clone())
54}
55
56pub fn run_dipole_inversion(
86 field_ppm: &[f64],
87 mask: &[u8],
88 metadata: &ScanMetadata,
89 config: &InversionConfig,
90 magnitude: Option<&[f64]>,
91 progress: &mut dyn FnMut(usize, usize),
92) -> Result<Vec<f64>, PipelineError> {
93 if let Some(problem) = heidi_seed_problem(config) {
94 return Err(PipelineError::InvalidConfig(problem));
95 }
96 let chi = invert(field_ppm, mask, metadata, config, magnitude, &mut *progress)?;
97 if config.cone_fill_after() {
98 return run_heidi_cone_fill(&chi, field_ppm, mask, metadata, config, progress);
99 }
100 Ok(chi)
101}
102
103fn invert(
105 field_ppm: &[f64],
106 mask: &[u8],
107 metadata: &ScanMetadata,
108 config: &InversionConfig,
109 magnitude: Option<&[f64]>,
110 progress: &mut dyn FnMut(usize, usize),
111) -> Result<Vec<f64>, PipelineError> {
112 metadata.require_contiguous_slices()?;
114 let grid = metadata.grid();
115 let bdir = metadata.b0_direction;
116 let n_voxels = grid.n_total();
117
118 if field_ppm.len() != n_voxels {
119 return Err(PipelineError::DimensionMismatch {
120 expected: n_voxels,
121 got: field_ppm.len(),
122 });
123 }
124
125 let chi = match config.algorithm {
126 InversionAlgorithm::Tkd => {
127 crate::inversion::tkd(
128 field_ppm, mask, &grid, bdir, &config.tkd,
129 )
130 }
131 InversionAlgorithm::Tsvd => {
132 crate::inversion::tsvd(
133 field_ppm, mask, &grid, bdir, &config.tsvd,
134 )
135 }
136 InversionAlgorithm::Tikhonov => {
137 crate::inversion::tikhonov(
138 field_ppm, mask, &grid, bdir, &config.tikhonov,
139 )
140 }
141 InversionAlgorithm::Tv => {
142 crate::inversion::tv_admm(
143 field_ppm, mask, &grid, bdir, &config.tv, progress,
144 )
145 }
146 InversionAlgorithm::Rts => {
147 crate::inversion::rts(
148 field_ppm, mask, &grid, bdir, &config.rts, progress,
149 )
150 }
151 InversionAlgorithm::Nltv => {
152 let mag = magnitude.ok_or_else(|| PipelineError::InvalidInput(
157 "NLTV needs the magnitude (its data weight)".into(),
158 ))?;
159 let params = crate::inversion::NltvParams {
160 b0: metadata.field_strength,
161 echo_times: scan_echo_times("NLTV", config.nltv.te, metadata)?,
162 ..config.nltv.clone()
163 };
164 crate::inversion::nltv(
165 field_ppm, mask, mag, &grid, bdir, ¶ms, progress,
166 )
167 }
168 InversionAlgorithm::Medi => {
169 let te = &metadata.echo_times;
175 let mag = crate::pipeline::inversion::echo_magnitudes("MEDI", magnitude, te, n_voxels)?;
176 if config.medi.msmv.is_some() {
179 if let Some(problem) = crate::utils::r2star::arlo_echo_times_problem(te) {
180 return Err(PipelineError::InvalidInput(format!(
181 "MEDI-mSMV needs R2* for its vessel mask: {problem}"
182 )));
183 }
184 }
185 let params = crate::inversion::MediParams {
186 b0: metadata.field_strength,
187 echo_times: scan_echo_times("MEDI", config.medi.te, metadata)?,
188 ..config.medi.clone()
189 };
190 crate::inversion::medi(field_ppm, mask, mag, &grid, bdir, ¶ms, progress)
191 }
192 InversionAlgorithm::Tfi => {
193 let te = &metadata.echo_times;
200 let mag = echo_magnitudes("TFI", magnitude, te, n_voxels)?;
201 let params = crate::inversion::TfiParams {
202 b0: metadata.field_strength,
203 echo_times: scan_echo_times("TFI", config.tfi.te, metadata)?,
204 ..config.tfi.clone()
205 };
206 crate::inversion::tfi(field_ppm, mask, mag, &grid, bdir, ¶ms, progress)
207 }
208 InversionAlgorithm::Ilsqr => {
209 config.ilsqr.validate().map_err(PipelineError::InvalidConfig)?;
210 let (chi, _, _, _) = crate::inversion::ilsqr(
211 field_ppm, mask, &grid, bdir, &config.ilsqr, &mut *progress,
212 );
213 chi
214 }
215 InversionAlgorithm::Star => {
216 let params = crate::inversion::StarParams {
221 echo_times: scan_echo_times("STAR-QSM", config.star.te, metadata)?,
222 b0: metadata.field_strength,
223 ..config.star.clone()
224 };
225 params.validate().map_err(PipelineError::InvalidConfig)?;
226 crate::inversion::star_qsm(field_ppm, mask, &grid, bdir, ¶ms, &mut *progress)
227 }
228 InversionAlgorithm::Ndi => {
229 let magnitude = magnitude.ok_or_else(|| PipelineError::InvalidConfig(
233 "NDI needs the magnitude as its data weight".into()))?;
234 let params = crate::inversion::NdiParams {
235 b0: metadata.field_strength,
236 echo_times: scan_echo_times("NDI", config.ndi.te, metadata)?,
237 ..config.ndi.clone()
238 };
239 crate::inversion::ndi(
240 field_ppm, mask, magnitude, &grid, bdir, ¶ms, progress,
241 )
242 }
243 InversionAlgorithm::Fansi | InversionAlgorithm::FansiTgv | InversionAlgorithm::L1qsm => {
244 let magnitude = magnitude.ok_or_else(|| {
249 PipelineError::InvalidInput(format!(
250 "{:?} requires magnitude data (its data weight)", config.algorithm
251 ))
252 })?;
253 let b0 = metadata.field_strength;
254 if config.algorithm == InversionAlgorithm::L1qsm {
255 let params = crate::inversion::L1QsmParams {
256 b0,
257 echo_times: scan_echo_times("L1-QSM", config.l1qsm.te, metadata)?,
258 ..config.l1qsm.clone()
259 };
260 crate::inversion::l1qsm(
261 field_ppm, mask, magnitude, &grid, bdir, ¶ms, progress,
262 )
263 } else {
264 let params = crate::inversion::FansiParams {
265 b0,
266 echo_times: scan_echo_times("FANSI", config.fansi.te, metadata)?,
267 is_tgv: config.algorithm == InversionAlgorithm::FansiTgv,
268 ..config.fansi.clone()
269 };
270 crate::inversion::fansi(
271 field_ppm, mask, magnitude, &grid, bdir, ¶ms, progress,
272 )
273 }
274 }
275 InversionAlgorithm::Whqsm => {
276 if config.whqsm.weight.needs_magnitude() && magnitude.is_none() {
281 return Err(PipelineError::InvalidInput(format!(
282 "WH-QSM's {:?} weight needs magnitude data", config.whqsm.weight
283 )));
284 }
285 let params = crate::inversion::WhQsmParams {
286 b0: metadata.field_strength,
287 echo_times: scan_echo_times("WH-QSM", config.whqsm.te, metadata)?,
288 ..config.whqsm.clone()
289 };
290 crate::inversion::whqsm(
291 field_ppm, mask, magnitude, &grid, bdir, ¶ms, progress,
292 )
293 }
294 InversionAlgorithm::Hdqsm => {
295 let te = &metadata.echo_times;
298 let n_echoes = te.len();
299 let echoes = match magnitude {
300 Some(m) if n_echoes > 0 && m.len() == n_voxels * n_echoes => m,
301 Some(m) => return Err(PipelineError::InvalidInput(format!(
302 "HD-QSM needs the magnitude of every echo ({n_echoes} volumes, {} values); got {} values",
303 n_voxels * n_echoes, m.len(),
304 ))),
305 None => return Err(PipelineError::InvalidInput(format!(
306 "HD-QSM needs the magnitude of every echo ({n_echoes} volumes)"
307 ))),
308 };
309 let mag = crate::inversion::hdqsm_magnitude(echoes, te);
310 crate::inversion::hdqsm(
311 field_ppm, mask, Some(&mag), &grid, bdir, &config.hdqsm, progress,
312 )
313 }
314 InversionAlgorithm::AmpPe => {
315 let params = crate::inversion::AmpPeParams {
321 b0: metadata.field_strength,
322 ..config.amp_pe.clone()
323 };
324 crate::inversion::amp_pe(
325 field_ppm, mask, magnitude, None, &grid, bdir, ¶ms, progress,
326 )
327 .map_err(|e| PipelineError::InvalidConfig(e.0))?
328 }
329 InversionAlgorithm::Lsqr if config.apply_heidi => {
330 let lsqr_params = crate::inversion::LsqrQsmParams {
334 b0: metadata.field_strength,
335 ..config.lsqr.clone()
336 };
337 crate::inversion::heidi(
338 field_ppm, mask, magnitude, &grid, bdir, &lsqr_params, &heidi_params(config, metadata)?, progress,
339 )
340 }
341 InversionAlgorithm::Lsqr => {
342 let params = crate::inversion::LsqrQsmParams {
343 b0: metadata.field_strength,
344 ..config.lsqr.clone()
345 };
346 crate::inversion::lsqr_qsm(
347 field_ppm, mask, magnitude, &grid, bdir, ¶ms, progress,
348 )
349 }
350 InversionAlgorithm::Xqsm => run_xqsm(field_ppm, mask, &grid, config.tile, progress)?,
351 InversionAlgorithm::Qsmnet => run_qsmnet(field_ppm, mask, &grid, "qsmnet", config.tile, progress)?,
352 InversionAlgorithm::QsmnetPlus => run_qsmnet(field_ppm, mask, &grid, "qsmnet-plus", config.tile, progress)?,
353 InversionAlgorithm::Qsmnetme => run_qsmnetme(field_ppm, mask, &grid, bdir, config.tile)?,
354 InversionAlgorithm::Autoqsm => run_autoqsm(field_ppm, mask, &grid)?,
355 InversionAlgorithm::Qsmgan => run_qsmgan(field_ppm, mask, &grid)?,
356 InversionAlgorithm::Ir2qsm => run_ir2qsm(field_ppm, mask, &grid, config.tile, progress)?,
357 InversionAlgorithm::Lpcnn => run_lpcnn(field_ppm, mask, &grid, bdir, config.tile, progress)?,
358 InversionAlgorithm::ModlQsm => run_modl_qsm(field_ppm, mask, &grid, bdir, config.tile, progress)?,
359 InversionAlgorithm::Nextqsm => run_nextqsm(field_ppm, mask, metadata, config.tile, progress)?,
361 InversionAlgorithm::Tgv | InversionAlgorithm::Qsmart => {
362 return Err(PipelineError::InvalidConfig(
363 format!("{:?} should use run_tgv or run_qsmart", config.algorithm),
364 ));
365 }
366 InversionAlgorithm::Iqsm | InversionAlgorithm::IqsmPlus => {
368 return Err(PipelineError::InvalidConfig(
369 format!("{:?} should use run_iqsm / run_iqsm_plus (it takes wrapped phase)", config.algorithm),
370 ));
371 }
372 };
373
374 Ok(chi)
375}
376
377fn heidi_params(config: &InversionConfig, metadata: &ScanMetadata) -> Result<crate::inversion::HeidiParams, PipelineError> {
382 Ok(crate::inversion::HeidiParams {
383 b0: metadata.field_strength,
384 echo_times: scan_echo_times("HEIDI", config.heidi.te, metadata)?,
385 ..config.heidi.clone()
386 })
387}
388
389pub fn heidi_seed_problem(config: &InversionConfig) -> Option<String> {
401 if !config.apply_heidi {
402 return None;
403 }
404 let cone = config.heidi.cone_threshold;
405 let truncation = match config.algorithm {
406 InversionAlgorithm::Tkd => Some(("TKD", config.tkd.threshold)),
407 InversionAlgorithm::Tsvd => Some(("TSVD", config.tsvd.threshold)),
408 _ => None,
409 };
410 if let Some((name, threshold)) = truncation {
411 if threshold > cone {
412 return Some(format!(
413 "HEIDI after {name}: {name}'s threshold ({threshold}) is above HEIDI's cone threshold \
414 ({cone}), so coefficients {name} truncated would be kept as data; lower it to {cone} or less"
415 ));
416 }
417 }
418 (config.input(1) != InversionInput::LocalField).then(|| {
419 format!("HEIDI needs the local field the seed was inverted from; {} has none", config.algorithm.id())
420 })
421}
422
423pub fn run_heidi_cone_fill(
437 chi_seed: &[f64],
438 field_ppm: &[f64],
439 mask: &[u8],
440 metadata: &ScanMetadata,
441 config: &InversionConfig,
442 progress: &mut dyn FnMut(usize, usize),
443) -> Result<Vec<f64>, PipelineError> {
444 if let Some(problem) = heidi_seed_problem(&InversionConfig { apply_heidi: true, ..config.clone() }) {
445 return Err(PipelineError::InvalidConfig(problem));
446 }
447 metadata.require_contiguous_slices()?;
448 let grid = metadata.grid();
449 let n_voxels = grid.n_total();
450 for (got, what) in [(chi_seed.len(), "seed"), (field_ppm.len(), "field"), (mask.len(), "mask")] {
451 if got != n_voxels {
452 return Err(PipelineError::InvalidInput(format!(
453 "HEIDI's {what} has {got} voxels, the scan {n_voxels}"
454 )));
455 }
456 }
457 let params = heidi_params(config, metadata)?;
458 Ok(crate::inversion::heidi_cone_fill(
459 field_ppm, mask, chi_seed, &grid, metadata.b0_direction, ¶ms, progress,
460 ))
461}
462
463#[cfg(feature = "onnx")]
467fn run_xqsm(
468 local_field_ppm: &[f64],
469 mask: &[u8],
470 grid: &crate::Grid,
471 tile: Option<(usize, usize)>,
472 progress: &mut dyn FnMut(usize, usize),
473) -> Result<Vec<f64>, PipelineError> {
474 let spec = crate::models::find_model("xqsm")
475 .ok_or_else(|| PipelineError::InvalidConfig("xqsm not in model registry".into()))?;
476 let bytes = crate::models::primary_weight_bytes(spec)
477 .map_err(PipelineError::InvalidConfig)?;
478 match tile {
479 Some((core, halo)) => crate::inversion::xqsm_tiled(
480 local_field_ppm, mask, grid, &bytes, &crate::inversion::TileConfig { core, halo }, progress,
481 ),
482 None => crate::inversion::xqsm(local_field_ppm, mask, grid, &bytes),
483 }
484 .map_err(|e| PipelineError::AlgorithmError(e.to_string()))
485}
486
487#[cfg(not(feature = "onnx"))]
488fn run_xqsm(
489 _local_field_ppm: &[f64],
490 _mask: &[u8],
491 _grid: &crate::Grid,
492 _tile: Option<(usize, usize)>,
493 _progress: &mut dyn FnMut(usize, usize),
494) -> Result<Vec<f64>, PipelineError> {
495 Err(PipelineError::InvalidConfig(
496 "xQSM requires building qsm-core with the 'onnx' feature".into(),
497 ))
498}
499
500#[cfg(feature = "onnx")]
517pub fn run_nextqsm(
518 total_field_ppm: &[f64],
519 mask: &[u8],
520 metadata: &ScanMetadata,
521 tile: Option<(usize, usize)>,
522 progress: &mut dyn FnMut(usize, usize),
523) -> Result<Vec<f64>, PipelineError> {
524 metadata.require_contiguous_slices()?;
528 let grid = &metadata.grid();
529 let bdir = metadata.b0_direction;
530 let spec = crate::models::find_model("nextqsm")
531 .ok_or_else(|| PipelineError::InvalidConfig("nextqsm not in model registry".into()))?;
532 let files = crate::models::all_weight_bytes(spec).map_err(PipelineError::InvalidConfig)?;
533 let [bf, vjp] = &files[..] else {
534 return Err(PipelineError::InvalidConfig(
535 format!("nextqsm expects 2 weight files (BFR, VJP), got {}", files.len()),
536 ));
537 };
538 match tile {
539 Some((core, halo)) => crate::inversion::nextqsm_tiled(
540 total_field_ppm, mask, grid, bdir, bf, vjp, &crate::inversion::TileConfig { core, halo }, progress,
541 ),
542 None => crate::inversion::nextqsm(total_field_ppm, mask, grid, bdir, bf, vjp),
543 }
544 .map_err(|e| PipelineError::AlgorithmError(e.to_string()))
545}
546
547#[cfg(not(feature = "onnx"))]
549pub fn run_nextqsm(
550 _total_field_ppm: &[f64],
551 _mask: &[u8],
552 _metadata: &ScanMetadata,
553 _tile: Option<(usize, usize)>,
554 _progress: &mut dyn FnMut(usize, usize),
555) -> Result<Vec<f64>, PipelineError> {
556 Err(PipelineError::InvalidConfig(
557 "NeXtQSM requires building qsm-core with the 'onnx' feature".into(),
558 ))
559}
560
561#[cfg(feature = "onnx")]
565fn run_qsmnet(
566 local_field_ppm: &[f64],
567 mask: &[u8],
568 grid: &crate::Grid,
569 model_id: &str,
570 tile: Option<(usize, usize)>,
571 progress: &mut dyn FnMut(usize, usize),
572) -> Result<Vec<f64>, PipelineError> {
573 use crate::inversion::QsmnetNorm;
574 let norm = match model_id {
575 "qsmnet-plus" => QsmnetNorm::qsmnet_plus(),
576 _ => QsmnetNorm::qsmnet(),
577 };
578 let spec = crate::models::find_model(model_id).ok_or_else(|| {
579 PipelineError::InvalidConfig(format!("{model_id} not in model registry"))
580 })?;
581 let bytes = crate::models::primary_weight_bytes(spec)
582 .map_err(PipelineError::InvalidConfig)?;
583 match tile {
584 Some((core, halo)) => crate::inversion::qsmnet_tiled(
585 local_field_ppm, mask, grid, &bytes, &norm, &crate::inversion::TileConfig { core, halo }, progress,
586 ),
587 None => crate::inversion::qsmnet(local_field_ppm, mask, grid, &bytes, &norm),
588 }
589 .map_err(|e| PipelineError::AlgorithmError(e.to_string()))
590}
591
592#[cfg(not(feature = "onnx"))]
593fn run_qsmnet(
594 _local_field_ppm: &[f64],
595 _mask: &[u8],
596 _grid: &crate::Grid,
597 _model_id: &str,
598 _tile: Option<(usize, usize)>,
599 _progress: &mut dyn FnMut(usize, usize),
600) -> Result<Vec<f64>, PipelineError> {
601 Err(PipelineError::InvalidConfig(
602 "QSMnet requires building qsm-core with the 'onnx' feature".into(),
603 ))
604}
605
606#[cfg(feature = "onnx")]
610fn run_qsmnetme(
611 local_field_ppm: &[f64],
612 mask: &[u8],
613 grid: &crate::Grid,
614 bdir: (f64, f64, f64),
615 tile: Option<(usize, usize)>,
616) -> Result<Vec<f64>, PipelineError> {
617 let bytes = crate::models::primary_weight("qsmnetme").map_err(PipelineError::InvalidConfig)?;
618 let params = crate::inversion::QsmnetmeParams {
619 patch: tile.map(|(core, halo)| {
620 let p = (core + 2 * halo).max(16).div_ceil(16) * 16;
621 (p, p, p)
622 }),
623 };
624 crate::inversion::qsmnetme(
625 local_field_ppm, mask, grid, bdir, &bytes, &crate::inversion::QsmnetmeNorm::default(), ¶ms,
626 )
627 .map_err(|e| PipelineError::AlgorithmError(e.to_string()))
628}
629
630#[cfg(not(feature = "onnx"))]
631fn run_qsmnetme(
632 _local_field_ppm: &[f64],
633 _mask: &[u8],
634 _grid: &crate::Grid,
635 _bdir: (f64, f64, f64),
636 _tile: Option<(usize, usize)>,
637) -> Result<Vec<f64>, PipelineError> {
638 Err(PipelineError::InvalidConfig(
639 "QSMnetME requires building qsm-core with the 'onnx' feature".into(),
640 ))
641}
642
643#[cfg(feature = "onnx")]
646fn run_autoqsm(
647 field: &[f64],
648 mask: &[u8],
649 grid: &crate::Grid,
650) -> Result<Vec<f64>, PipelineError> {
651 let spec = crate::models::find_model("autoqsm")
652 .ok_or_else(|| PipelineError::InvalidConfig("autoqsm not in model registry".into()))?;
653 let bytes = crate::models::primary_weight_bytes(spec).map_err(PipelineError::InvalidConfig)?;
654 crate::inversion::autoqsm(field, mask, grid, &bytes)
655 .map_err(|e| PipelineError::AlgorithmError(e.to_string()))
656}
657
658#[cfg(not(feature = "onnx"))]
659fn run_autoqsm(
660 _field: &[f64],
661 _mask: &[u8],
662 _grid: &crate::Grid,
663) -> Result<Vec<f64>, PipelineError> {
664 Err(PipelineError::InvalidConfig(
665 "AutoQSM requires building qsm-core with the 'onnx' feature".into(),
666 ))
667}
668
669#[cfg(feature = "onnx")]
671fn run_qsmgan(local_field_ppm: &[f64], mask: &[u8], grid: &crate::Grid) -> Result<Vec<f64>, PipelineError> {
672 let bytes = crate::models::primary_weight("qsmgan").map_err(PipelineError::InvalidConfig)?;
673 crate::inversion::qsmgan(local_field_ppm, mask, grid, &bytes)
674 .map_err(|e| PipelineError::AlgorithmError(e.to_string()))
675}
676
677#[cfg(feature = "onnx")]
679fn run_ir2qsm(
680 local_field_ppm: &[f64],
681 mask: &[u8],
682 grid: &crate::Grid,
683 tile: Option<(usize, usize)>,
684 progress: &mut dyn FnMut(usize, usize),
685) -> Result<Vec<f64>, PipelineError> {
686 let bytes = crate::models::primary_weight("ir2qsm").map_err(PipelineError::InvalidConfig)?;
687 match tile {
688 Some((core, halo)) => crate::inversion::ir2qsm_tiled(
689 local_field_ppm, mask, grid, &bytes, &crate::inversion::TileConfig { core, halo }, progress,
690 ),
691 None => crate::inversion::ir2qsm(local_field_ppm, mask, grid, &bytes),
692 }
693 .map_err(|e| PipelineError::AlgorithmError(e.to_string()))
694}
695
696#[cfg(feature = "onnx")]
699fn run_lpcnn(
700 local_field_ppm: &[f64],
701 mask: &[u8],
702 grid: &crate::Grid,
703 bdir: (f64, f64, f64),
704 tile: Option<(usize, usize)>,
705 progress: &mut dyn FnMut(usize, usize),
706) -> Result<Vec<f64>, PipelineError> {
707 let bytes = crate::models::primary_weight("lpcnn").map_err(PipelineError::InvalidConfig)?;
708 match tile {
709 Some((core, halo)) => crate::inversion::lpcnn_tiled(
710 local_field_ppm, mask, grid, bdir, &bytes, &crate::inversion::TileConfig { core, halo }, progress,
711 ),
712 None => crate::inversion::lpcnn(local_field_ppm, mask, grid, bdir, &bytes),
713 }
714 .map_err(|e| PipelineError::AlgorithmError(e.to_string()))
715}
716
717#[cfg(feature = "onnx")]
720fn run_modl_qsm(
721 local_field_ppm: &[f64],
722 mask: &[u8],
723 grid: &crate::Grid,
724 bdir: (f64, f64, f64),
725 tile: Option<(usize, usize)>,
726 progress: &mut dyn FnMut(usize, usize),
727) -> Result<Vec<f64>, PipelineError> {
728 let bytes = crate::models::primary_weight("modl-qsm").map_err(PipelineError::InvalidConfig)?;
729 match tile {
730 Some((core, halo)) => crate::inversion::modl_qsm_tiled(
731 local_field_ppm, mask, grid, bdir, &bytes, &crate::inversion::TileConfig { core, halo }, progress,
732 ),
733 None => crate::inversion::modl_qsm(local_field_ppm, mask, grid, bdir, &bytes),
734 }
735 .map_err(|e| PipelineError::AlgorithmError(e.to_string()))
736}
737
738#[cfg(not(feature = "onnx"))]
739fn run_qsmgan(_f: &[f64], _m: &[u8], _g: &crate::Grid) -> Result<Vec<f64>, PipelineError> {
740 Err(PipelineError::InvalidConfig("QSMGAN requires building qsm-core with the 'onnx' feature".into()))
741}
742#[cfg(not(feature = "onnx"))]
743fn run_ir2qsm(
744 _f: &[f64],
745 _m: &[u8],
746 _g: &crate::Grid,
747 _tile: Option<(usize, usize)>,
748 _progress: &mut dyn FnMut(usize, usize),
749) -> Result<Vec<f64>, PipelineError> {
750 Err(PipelineError::InvalidConfig("IR2QSM requires building qsm-core with the 'onnx' feature".into()))
751}
752#[cfg(not(feature = "onnx"))]
753fn run_lpcnn(
754 _f: &[f64],
755 _m: &[u8],
756 _g: &crate::Grid,
757 _b: (f64, f64, f64),
758 _tile: Option<(usize, usize)>,
759 _progress: &mut dyn FnMut(usize, usize),
760) -> Result<Vec<f64>, PipelineError> {
761 Err(PipelineError::InvalidConfig("LPCNN requires building qsm-core with the 'onnx' feature".into()))
762}
763#[cfg(not(feature = "onnx"))]
764fn run_modl_qsm(_f: &[f64], _m: &[u8], _g: &crate::Grid, _b: (f64, f64, f64), _tile: Option<(usize, usize)>, _progress: &mut dyn FnMut(usize, usize)) -> Result<Vec<f64>, PipelineError> {
765 Err(PipelineError::InvalidConfig("MoDL-QSM requires building qsm-core with the 'onnx' feature".into()))
766}
767
768#[cfg(feature = "onnx")]
772const IQSM_PHASE_SIGN: f64 = -1.0;
773#[cfg(feature = "onnx")]
774const IQSM_ERODED_RAD: i32 = 3;
775
776#[cfg(feature = "onnx")]
786pub fn run_iqsm(
787 phases: &[&[f64]],
788 magnitudes: &[&[f64]],
789 mask: &[u8],
790 metadata: &ScanMetadata,
791 reference: QsmReference,
792 tile: Option<(usize, usize)>,
793 echo_weighting: crate::inversion::IqsmEchoWeighting,
794) -> Result<Vec<f64>, PipelineError> {
795 metadata.require_contiguous_slices()?;
796 let grid = metadata.grid();
797 let bytes = crate::models::primary_weight("iqsm").map_err(PipelineError::InvalidConfig)?;
798 let tile = tile.map(|(core, halo)| crate::inversion::TileConfig { core, halo });
799 let chi = crate::inversion::iqsm_multi_echo(
800 phases, magnitudes, mask, &grid, &metadata.echo_times,
801 metadata.field_strength, IQSM_PHASE_SIGN, IQSM_ERODED_RAD, &bytes, tile.as_ref(), echo_weighting,
802 ).map_err(|e| PipelineError::AlgorithmError(e.to_string()))?;
803 Ok(super::referencing::apply_reference(&chi, mask, reference))
804}
805
806#[cfg(not(feature = "onnx"))]
807pub fn run_iqsm(
808 _phases: &[&[f64]], _magnitudes: &[&[f64]], _mask: &[u8],
809 _metadata: &ScanMetadata, _reference: QsmReference, _tile: Option<(usize, usize)>,
810 _echo_weighting: crate::inversion::IqsmEchoWeighting,
811) -> Result<Vec<f64>, PipelineError> {
812 Err(PipelineError::InvalidConfig("iQSM requires building qsm-core with the 'onnx' feature".into()))
813}
814
815#[cfg(feature = "onnx")]
819pub fn run_iqsm_plus(
820 phases: &[&[f64]],
821 magnitudes: &[&[f64]],
822 mask: &[u8],
823 metadata: &ScanMetadata,
824 reference: QsmReference,
825 tile: Option<(usize, usize)>,
826) -> Result<Vec<f64>, PipelineError> {
827 metadata.require_contiguous_slices()?;
828 let grid = metadata.grid();
829 let bytes = crate::models::primary_weight("iqsm-plus").map_err(PipelineError::InvalidConfig)?;
830 let tile = tile.map(|(core, halo)| crate::inversion::TileConfig { core, halo });
831 let chi = crate::inversion::iqsm_plus_multi_echo(
832 phases, magnitudes, mask, &grid, &metadata.echo_times,
833 metadata.field_strength, metadata.b0_direction, IQSM_PHASE_SIGN, IQSM_ERODED_RAD, &bytes,
834 tile.as_ref(),
835 ).map_err(|e| PipelineError::AlgorithmError(e.to_string()))?;
836 Ok(super::referencing::apply_reference(&chi, mask, reference))
837}
838
839#[cfg(not(feature = "onnx"))]
840pub fn run_iqsm_plus(
841 _phases: &[&[f64]], _magnitudes: &[&[f64]], _mask: &[u8],
842 _metadata: &ScanMetadata, _reference: QsmReference, _tile: Option<(usize, usize)>,
843) -> Result<Vec<f64>, PipelineError> {
844 Err(PipelineError::InvalidConfig("iQSM+ requires building qsm-core with the 'onnx' feature".into()))
845}
846
847#[cfg(feature = "onnx")]
854pub fn run_iqfm(
855 phases: &[&[f64]],
856 magnitudes: &[&[f64]],
857 mask: &[u8],
858 metadata: &ScanMetadata,
859 tile: Option<(usize, usize)>,
860 echo_weighting: crate::inversion::IqsmEchoWeighting,
861) -> Result<Vec<f64>, PipelineError> {
862 metadata.require_contiguous_slices()?;
863 let grid = metadata.grid();
864 let bytes = crate::models::primary_weight("iqfm").map_err(PipelineError::InvalidConfig)?;
865 let tile = tile.map(|(core, halo)| crate::inversion::TileConfig { core, halo });
866 crate::inversion::iqfm_multi_echo(
867 phases, magnitudes, mask, &grid, &metadata.echo_times,
868 metadata.field_strength, IQSM_PHASE_SIGN, IQSM_ERODED_RAD, &bytes, tile.as_ref(), echo_weighting,
869 ).map_err(|e| PipelineError::AlgorithmError(e.to_string()))
870}
871
872#[cfg(not(feature = "onnx"))]
873pub fn run_iqfm(
874 _phases: &[&[f64]], _magnitudes: &[&[f64]], _mask: &[u8], _metadata: &ScanMetadata,
875 _tile: Option<(usize, usize)>, _echo_weighting: crate::inversion::IqsmEchoWeighting,
876) -> Result<Vec<f64>, PipelineError> {
877 Err(PipelineError::InvalidConfig("iQFM requires building qsm-core with the 'onnx' feature".into()))
878}
879
880pub fn run_tgv(
895 phases: &[&[f64]],
896 magnitudes: Option<&[&[f64]]>,
897 mask: &[u8],
898 metadata: &ScanMetadata,
899 field_mapping_config: &FieldMappingConfig,
900 tgv_params: &crate::inversion::TgvParams,
901 reference: QsmReference,
902 progress: &mut dyn FnMut(usize, usize),
903) -> Result<Vec<f64>, PipelineError> {
904 use crate::inversion::TgvInput;
905
906 metadata.require_contiguous_slices()?;
907 let grid = metadata.grid();
908 let (bx, by, bz) = metadata.b0_direction;
909
910 let echo_times = &metadata.echo_times;
913 let te_s = if phases.len() > 1 {
914 tgv_params.te.resolve(echo_times)
915 } else {
916 crate::EchoTime::First.resolve(echo_times)
917 }
918 .map_err(|e| PipelineError::InvalidInput(format!("TGV: {e}")))?;
919 let b0_t = metadata.field_strength;
920 if !(b0_t.is_finite() && b0_t > 0.0) {
921 return Err(PipelineError::InvalidInput(format!(
922 "TGV needs a positive field strength, got B0 = {b0_t} T"
923 )));
924 }
925
926 let (chi_ppm, chi_mask) = if phases.len() > 1 {
927 let field_result = super::field_mapping::run_field_mapping(
928 phases, magnitudes, mask, metadata,
929 field_mapping_config, &mut |_, _| {},
930 )?;
931 let chi = crate::inversion::tgv_qsm(
934 TgvInput::FieldPpm { field: &field_result.b0_field_ppm, echo_times, b0_t },
935 &field_result.valid_mask, &grid, tgv_params, (bx, by, bz), &mut *progress,
936 )?;
937 (chi, field_result.valid_mask)
938 } else {
939 let input = TgvInput::WrappedPhase {
940 phase: phases[0],
941 te_s,
942 b0_t,
943 };
944 (crate::inversion::tgv_qsm(input, mask, &grid, tgv_params, (bx, by, bz), &mut *progress)?, mask.to_vec())
945 };
946
947 Ok(super::referencing::apply_reference(&chi_ppm, &chi_mask, reference))
948}
949
950#[cfg(test)]
951mod tests {
952 use super::*;
953
954 #[test]
969 fn nextqsm_refuses_a_gapped_acquisition_through_the_dispatcher() {
970 let (nx, ny, nz) = (8, 8, 8);
971 let n = nx * ny * nz;
972 let config = InversionConfig {
973 algorithm: InversionAlgorithm::Nextqsm,
974 ..InversionConfig::default()
975 };
976 let err = run_dipole_inversion(
977 &vec![0.01; n], &vec![1u8; n], &gapped_meta(nx, ny, nz), &config, None, &mut |_, _| {},
978 )
979 .expect_err("a gapped volume must be refused");
980 let PipelineError::InvalidInput(msg) = &err else { panic!("{err:?}") };
981 assert!(msg.contains("1.0000 mm gap"), "{msg}");
982 }
983
984 #[cfg(feature = "onnx")]
988 #[test]
989 fn nextqsm_refuses_a_gapped_acquisition_standalone() {
990 let (nx, ny, nz) = (8, 8, 8);
991 let n = nx * ny * nz;
992 let err = run_nextqsm(
993 &vec![0.01; n], &vec![1u8; n], &gapped_meta(nx, ny, nz), None, &mut |_, _| {},
994 )
995 .expect_err("a gapped volume must be refused");
996 let PipelineError::InvalidInput(msg) = &err else { panic!("{err:?}") };
997 assert!(msg.contains("1.0000 mm gap"), "{msg}");
998 }
999
1000 fn gapped_meta(nx: usize, ny: usize, nz: usize) -> ScanMetadata {
1002 ScanMetadata {
1003 dims: (nx, ny, nz),
1004 voxel_size: (1.0, 1.0, 3.0),
1005 echo_times: vec![0.005],
1006 field_strength: 3.0,
1007 b0_direction: (0.0, 0.0, 1.0),
1008 slice_geometry: Some(crate::grid::SliceGeometry { thickness: 2.0, axis: 2 }),
1009 affine: None,
1010 }
1011 }
1012
1013 #[test]
1014 fn a_gapped_acquisition_is_refused_before_any_work() {
1015 let (nx, ny, nz) = (8, 8, 8);
1016 let n = nx * ny * nz;
1017 let field = vec![0.01; n];
1018 let mask = vec![1u8; n];
1019 let mut meta = ScanMetadata {
1020 dims: (nx, ny, nz),
1021 voxel_size: (1.0, 1.0, 3.0),
1022 echo_times: vec![0.005],
1023 field_strength: 3.0,
1024 b0_direction: (0.0, 0.0, 1.0),
1025 slice_geometry: None,
1026 affine: None,
1027 };
1028 let config = InversionConfig::default();
1029
1030 assert!(run_dipole_inversion(&field, &mask, &meta, &config, None, &mut |_, _| {}).is_ok());
1031 meta.slice_geometry = Some(crate::grid::SliceGeometry { thickness: 3.0, axis: 2 });
1032 assert!(run_dipole_inversion(&field, &mask, &meta, &config, None, &mut |_, _| {}).is_ok());
1033
1034 meta.slice_geometry = Some(crate::grid::SliceGeometry { thickness: 2.0, axis: 2 });
1035 let err = run_dipole_inversion(&field, &mask, &meta, &config, None, &mut |_, _| {})
1036 .expect_err("a gapped volume must be refused");
1037 let PipelineError::InvalidInput(msg) = &err else { panic!("{err:?}") };
1038 assert!(msg.contains("1.0000 mm gap"), "{msg}");
1039 }
1040
1041 #[test]
1042 fn test_inversion_tkd() {
1043 let (nx, ny, nz) = (8, 8, 8);
1044 let n = nx * ny * nz;
1045 let field = vec![0.01; n];
1046 let mask = vec![1u8; n];
1047 let meta = ScanMetadata {
1048 dims: (nx, ny, nz),
1049 voxel_size: (1.0, 1.0, 1.0),
1050 echo_times: vec![0.005],
1051 field_strength: 3.0,
1052 b0_direction: (0.0, 0.0, 1.0),
1053 slice_geometry: None,
1054 affine: None,
1055 };
1056 let config = InversionConfig {
1057 algorithm: InversionAlgorithm::Tkd,
1058 ..Default::default()
1059 };
1060
1061 let result = run_dipole_inversion(
1062 &field, &mask, &meta, &config, None, &mut |_, _| {},
1063 );
1064 assert!(result.is_ok());
1065 assert_eq!(result.unwrap().len(), n);
1066 }
1067
1068 fn make_inversion_test(alg: InversionAlgorithm) -> Vec<f64> {
1069 make_inversion_test_with(InversionConfig { algorithm: alg, ..Default::default() })
1070 }
1071
1072 fn make_inversion_test_with(config: InversionConfig) -> Vec<f64> {
1073 let (nx, ny, nz) = (8, 8, 8);
1074 let n = nx * ny * nz;
1075 let field = vec![0.01; n];
1076 let mask = vec![1u8; n];
1077 let meta = ScanMetadata {
1078 dims: (nx, ny, nz), voxel_size: (1.0, 1.0, 1.0),
1079 echo_times: vec![0.005], field_strength: 3.0, b0_direction: (0.0, 0.0, 1.0),
1080 slice_geometry: None,
1081 affine: None,
1082 };
1083 run_dipole_inversion(&field, &mask, &meta, &config, None, &mut |_, _| {}).unwrap()
1084 }
1085
1086 #[test]
1087 fn test_inversion_tsvd() {
1088 let chi = make_inversion_test(InversionAlgorithm::Tsvd);
1089 assert_eq!(chi.len(), 8 * 8 * 8);
1090 }
1091
1092 #[test]
1093 fn test_inversion_tikhonov() {
1094 let chi = make_inversion_test(InversionAlgorithm::Tikhonov);
1095 assert_eq!(chi.len(), 8 * 8 * 8);
1096 }
1097
1098 #[test]
1099 fn test_inversion_tv() {
1100 let chi = make_inversion_test(InversionAlgorithm::Tv);
1101 assert_eq!(chi.len(), 8 * 8 * 8);
1102 }
1103
1104 #[test]
1105 fn test_inversion_rts() {
1106 let chi = make_inversion_test(InversionAlgorithm::Rts);
1107 assert_eq!(chi.len(), 8 * 8 * 8);
1108 }
1109
1110 #[test]
1111 fn test_inversion_nltv() {
1112 let n = 8 * 8 * 8;
1113 let field = vec![0.01; n];
1114 let mask = vec![1u8; n];
1115 let mag = vec![1.0; n];
1116 let meta = ScanMetadata {
1117 dims: (8, 8, 8), voxel_size: (1.0, 1.0, 1.0),
1118 echo_times: vec![0.005], field_strength: 3.0, b0_direction: (0.0, 0.0, 1.0),
1119 slice_geometry: None,
1120 affine: None,
1121 };
1122 let config = InversionConfig { algorithm: InversionAlgorithm::Nltv, ..Default::default() };
1123 let chi = run_dipole_inversion(&field, &mask, &meta, &config, Some(&mag), &mut |_, _| {}).unwrap();
1124 assert_eq!(chi.len(), n);
1125 let r = run_dipole_inversion(&field, &mask, &meta, &config, None, &mut |_, _| {});
1127 assert!(matches!(r, Err(PipelineError::InvalidInput(_))));
1128 }
1129
1130 #[test]
1134 fn test_inversion_hdqsm_takes_the_echo_magnitudes() {
1135 let n = 8 * 8 * 8;
1136 let field: Vec<f64> = (0..n).map(|i| 0.01 * ((i % 7) as f64 - 3.0)).collect();
1137 let mask = vec![1u8; n];
1138 let echoes: Vec<f64> = (0..2 * n).map(|i| 1.0 + (i % 5) as f64).collect();
1139 let meta = ScanMetadata {
1140 dims: (8, 8, 8), voxel_size: (1.0, 1.0, 1.0),
1141 echo_times: vec![0.005, 0.01], field_strength: 3.0, b0_direction: (0.0, 0.0, 1.0),
1142 slice_geometry: None,
1143 affine: None,
1144 };
1145 let hdqsm = crate::inversion::HdQsmParams { max_iter_l1: 2, max_iter_l2: 2, ..Default::default() };
1146 let config = InversionConfig { algorithm: InversionAlgorithm::Hdqsm, hdqsm: hdqsm.clone(), ..Default::default() };
1147 let chi = run_dipole_inversion(&field, &mask, &meta, &config, Some(&echoes), &mut |_, _| {}).unwrap();
1148 let mag = crate::inversion::hdqsm_magnitude(&echoes, &meta.echo_times);
1149 let grid = meta.grid();
1150 let direct = crate::inversion::hdqsm(&field, &mask, Some(&mag), &grid, meta.b0_direction, &hdqsm, |_, _| {});
1151 assert_eq!(chi, direct);
1152 for magnitude in [Some(&echoes[..n]), None] {
1153 let r = run_dipole_inversion(&field, &mask, &meta, &config, magnitude, &mut |_, _| {});
1154 assert!(matches!(r, Err(PipelineError::InvalidInput(_))));
1155 }
1156 }
1157
1158 #[test]
1159 fn test_inversion_ilsqr() {
1160 let ilsqr = crate::inversion::IlsqrParams { max_iter: 3, ..Default::default() };
1161 let chi = make_inversion_test_with(InversionConfig { algorithm: InversionAlgorithm::Ilsqr, ilsqr, ..Default::default() });
1162 assert_eq!(chi.len(), 8 * 8 * 8);
1163 }
1164
1165 #[test]
1166 fn test_inversion_ilsqr_rejects_bad_padding() {
1167 let meta = ScanMetadata {
1168 dims: (8, 8, 8), voxel_size: (1.0, 1.0, 1.0),
1169 echo_times: vec![0.005], field_strength: 3.0, b0_direction: (0.0, 0.0, 1.0),
1170 slice_geometry: None,
1171 affine: None,
1172 };
1173 for pad_mm in [-1.0, f64::NAN, f64::INFINITY] {
1174 let ilsqr = crate::inversion::IlsqrParams { pad_mm, ..Default::default() };
1175 let config = InversionConfig { algorithm: InversionAlgorithm::Ilsqr, ilsqr, ..Default::default() };
1176 let r = run_dipole_inversion(&[0.01; 512], &[1u8; 512], &meta, &config, None, &mut |_, _| {});
1177 assert!(matches!(r, Err(PipelineError::InvalidConfig(_))), "pad {pad_mm} accepted");
1178 }
1179 }
1180
1181 #[test]
1182 fn test_inversion_star() {
1183 let star = crate::inversion::StarParams { pad: [4; 3], ..Default::default() };
1184 let chi = make_inversion_test_with(InversionConfig { algorithm: InversionAlgorithm::Star, star, ..Default::default() });
1185 assert_eq!(chi.len(), 8 * 8 * 8);
1186 assert!(chi.iter().all(|v| v.is_finite()));
1187 }
1188
1189 #[test]
1190 fn test_inversion_star_rejects_a_bad_field_strength() {
1191 let meta = ScanMetadata {
1192 dims: (8, 8, 8), voxel_size: (1.0, 1.0, 1.0),
1193 echo_times: vec![0.005], field_strength: 0.0, b0_direction: (0.0, 0.0, 1.0),
1194 slice_geometry: None,
1195 affine: None,
1196 };
1197 let config = InversionConfig { algorithm: InversionAlgorithm::Star, ..Default::default() };
1198 let r = run_dipole_inversion(&[0.01; 512], &[1u8; 512], &meta, &config, None, &mut |_, _| {});
1199 assert!(matches!(r, Err(PipelineError::InvalidConfig(_))));
1200 }
1201
1202 #[test]
1205 fn test_inversion_star_uses_metadata_b0_direction() {
1206 let (nx, ny, nz) = (12, 12, 12);
1207 let n = nx * ny * nz;
1208 let field: Vec<f64> = (0..n).map(|l| 0.01 * ((l % nx) as f64 - 5.5) + 0.02 * ((l / (nx * ny)) as f64 - 5.5)).collect();
1209 let mask = vec![1u8; n];
1210 let bdir = (0.3, -0.2, 0.932);
1211 let meta = ScanMetadata {
1212 dims: (nx, ny, nz), voxel_size: (1.0, 1.0, 1.0),
1213 echo_times: vec![0.005], field_strength: 3.0, b0_direction: bdir,
1214 slice_geometry: None,
1215 affine: None,
1216 };
1217 let star = crate::inversion::StarParams { pad: [2; 3], ..Default::default() };
1218 let config = InversionConfig { algorithm: InversionAlgorithm::Star, star: star.clone(), ..Default::default() };
1219 let chi = run_dipole_inversion(&field, &mask, &meta, &config, None, &mut |_, _| {}).unwrap();
1220 let grid = crate::Grid::new(nx, ny, nz, 1.0, 1.0, 1.0);
1221 let params = crate::inversion::StarParams { echo_times: vec![0.005], b0: 3.0, ..star };
1222 let want = crate::inversion::star_qsm(&field, &mask, &grid, bdir, ¶ms, |_, _| {});
1223 let axial = crate::inversion::star_qsm(&field, &mask, &grid, (0.0, 0.0, 1.0), ¶ms, |_, _| {});
1224 assert_eq!(chi, want);
1225 assert_ne!(chi, axial, "the direction must matter for this test to mean anything");
1226 }
1227
1228 #[test]
1229 fn test_fansi_family_requires_magnitude() {
1230 let meta = ScanMetadata {
1231 dims: (8, 8, 8), voxel_size: (1.0, 1.0, 1.0),
1232 echo_times: vec![0.005], field_strength: 3.0, b0_direction: (0.0, 0.0, 1.0),
1233 slice_geometry: None,
1234 affine: None,
1235 };
1236 for algorithm in [InversionAlgorithm::Fansi, InversionAlgorithm::FansiTgv, InversionAlgorithm::L1qsm] {
1237 let mut config = InversionConfig { algorithm, ..Default::default() };
1238 config.fansi.max_iter = 2;
1239 config.l1qsm.max_iter = 2;
1240 let r = run_dipole_inversion(&[0.01; 512], &[1u8; 512], &meta, &config, None, &mut |_, _| {});
1241 assert!(matches!(r, Err(PipelineError::InvalidInput(_))), "{algorithm:?} ran without magnitude");
1242 let mag = [1.0; 512];
1243 let r = run_dipole_inversion(&[0.01; 512], &[1u8; 512], &meta, &config, Some(&mag), &mut |_, _| {});
1244 assert!(r.is_ok(), "{algorithm:?} failed with magnitude");
1245 }
1246 }
1247
1248 #[test]
1249 fn test_inversion_amp_pe() {
1250 let chi = make_inversion_test(InversionAlgorithm::AmpPe);
1251 assert_eq!(chi.len(), 8 * 8 * 8);
1252 }
1253
1254 #[test]
1255 fn test_inversion_lsqr() {
1256 let chi = make_inversion_test(InversionAlgorithm::Lsqr);
1257 assert_eq!(chi.len(), 8 * 8 * 8);
1258 assert!(chi.iter().all(|v| v.is_finite()));
1259 }
1260
1261 #[test]
1262 fn test_inversion_heidi() {
1263 let heidi = crate::inversion::HeidiParams { continuation_steps: 2, inner_iterations: 20, ..Default::default() };
1265 let chi = make_inversion_test_with(InversionConfig { algorithm: InversionAlgorithm::Lsqr, apply_heidi: true, heidi, ..Default::default() });
1266 assert_eq!(chi.len(), 8 * 8 * 8);
1267 assert!(chi.iter().all(|v| v.is_finite()));
1268 }
1269
1270 #[test]
1273 fn heidi_seed_problems() {
1274 let mut c = InversionConfig { algorithm: InversionAlgorithm::Tkd, ..Default::default() };
1275 assert_eq!(heidi_seed_problem(&c), None, "HEIDI off");
1276 c.apply_heidi = true;
1277 assert!(heidi_seed_problem(&c).is_some(), "TKD's default 0.15 is above the cone's 0.1");
1278 c.tkd.threshold = 0.1;
1279 assert_eq!(heidi_seed_problem(&c), None);
1280 c.heidi.cone_threshold = 0.05;
1281 assert!(heidi_seed_problem(&c).is_some(), "checks the configured cone threshold");
1282 c.algorithm = InversionAlgorithm::Tsvd;
1283 c.tsvd.threshold = 0.2;
1284 assert!(heidi_seed_problem(&c).is_some());
1285 for a in [InversionAlgorithm::Tfi, InversionAlgorithm::Qsmart, InversionAlgorithm::Tgv, InversionAlgorithm::Iqsm] {
1286 let c = InversionConfig { algorithm: a, apply_heidi: true, ..Default::default() };
1287 assert!(heidi_seed_problem(&c).is_some(), "{a:?} has no local field");
1288 }
1289 for a in [InversionAlgorithm::Lsqr, InversionAlgorithm::Rts, InversionAlgorithm::Medi, InversionAlgorithm::Ndi] {
1290 let c = InversionConfig { algorithm: a, apply_heidi: true, ..Default::default() };
1291 assert_eq!(heidi_seed_problem(&c), None, "{a:?}");
1292 }
1293 }
1294
1295 #[test]
1298 fn heidi_after_tkd() {
1299 let heidi = crate::inversion::HeidiParams { continuation_steps: 2, inner_iterations: 5, ..Default::default() };
1300 let mut config = InversionConfig { algorithm: InversionAlgorithm::Tkd, apply_heidi: true, heidi, ..Default::default() };
1301 assert!(config.cone_fill_after());
1302 let meta = ScanMetadata {
1303 dims: (8, 8, 8), voxel_size: (1.0, 1.0, 1.0),
1304 echo_times: vec![0.005], field_strength: 3.0, b0_direction: (0.0, 0.0, 1.0),
1305 slice_geometry: None,
1306 affine: None,
1307 };
1308 let r = run_dipole_inversion(&[0.01; 512], &[1u8; 512], &meta, &config, None, &mut |_, _| {});
1309 assert!(matches!(r, Err(PipelineError::InvalidConfig(_))), "TKD 0.15 > cone 0.1 must be refused");
1310 config.tkd.threshold = 0.1;
1311 let chi = make_inversion_test_with(config.clone());
1312 assert!(chi.iter().all(|v| v.is_finite()));
1313 config.apply_heidi = false;
1314 let tkd = make_inversion_test_with(config);
1315 assert_ne!(chi, tkd, "the cone was not filled");
1316 }
1317
1318 #[test]
1321 fn test_heidi_progress_is_monotonic() {
1322 let (nx, ny, nz) = (8, 8, 8);
1323 let n = nx * ny * nz;
1324 let meta = ScanMetadata {
1325 dims: (nx, ny, nz), voxel_size: (1.0, 1.0, 1.0),
1326 echo_times: vec![0.005], field_strength: 3.0, b0_direction: (0.0, 0.0, 1.0),
1327 slice_geometry: None,
1328 affine: None,
1329 };
1330 let config = InversionConfig {
1331 algorithm: InversionAlgorithm::Lsqr,
1332 apply_heidi: true,
1333 heidi: crate::inversion::HeidiParams {
1334 continuation_steps: 2,
1335 inner_iterations: 4,
1336 ..Default::default()
1337 },
1338 ..Default::default()
1339 };
1340
1341 let mut seen: Vec<(usize, usize)> = Vec::new();
1342 run_dipole_inversion(
1343 &vec![0.01; n], &vec![1u8; n], &meta, &config, None,
1344 &mut |i, t| seen.push((i, t)),
1345 )
1346 .unwrap();
1347
1348 assert!(!seen.is_empty(), "no progress was reported");
1349 assert!(
1350 seen.windows(2).all(|w| w[0].0 <= w[1].0),
1351 "progress went backwards: {seen:?}"
1352 );
1353 let total = seen[0].1;
1354 assert!(seen.iter().all(|&(_, t)| t == total), "total changed mid-run");
1355 assert!(seen.last().unwrap().0 <= total);
1356 }
1357
1358 #[test]
1361 fn test_inversion_medi() {
1362 let (nx, ny, nz) = (8, 8, 8);
1363 let n = nx * ny * nz;
1364 let field: Vec<f64> = (0..n).map(|i| 0.01 * ((i * 7) % 5) as f64).collect();
1365 let mask = vec![1u8; n];
1366 let mag: Vec<f64> = (0..n).map(|i| 1.0 + ((i * 3) % 7) as f64).collect();
1367 let medi = crate::inversion::MediParams { max_iter: 2, cg_max_iter: 5, smv: false, ..Default::default() };
1368 let config = InversionConfig { algorithm: InversionAlgorithm::Medi, medi: medi.clone(), ..Default::default() };
1369 use crate::inversion::EchoMagnitudes;
1370 let echoes: Vec<f64> = (0..3 * n).map(|i| 1.0 + ((i * 5) % 9) as f64).collect();
1371 let cases: [(Vec<f64>, &[f64], usize); 2] = [
1372 (vec![0.004, 0.010, 0.016], &echoes, 3),
1373 (vec![0.02], &mag, 1),
1374 ];
1375 for (tes, given, n_echoes) in cases {
1376 let meta = ScanMetadata {
1377 dims: (nx, ny, nz), voxel_size: (1.0, 1.0, 1.0),
1378 echo_times: tes, field_strength: 7.0, b0_direction: (0.0, 0.0, 1.0),
1379 slice_geometry: None,
1380 affine: None,
1381 };
1382 let via_stage = run_dipole_inversion(&field, &mask, &meta, &config, Some(given), &mut |_, _| {}).unwrap();
1383 let params = crate::inversion::MediParams { b0: 7.0, echo_times: meta.echo_times.clone(), ..medi.clone() };
1384 let direct = crate::inversion::medi(
1385 &field, &mask, EchoMagnitudes { data: given, n_echoes }, &meta.grid(), meta.b0_direction, ¶ms, |_, _| {},
1386 );
1387 assert_eq!(via_stage, direct);
1388 assert!(run_dipole_inversion(&field, &mask, &meta, &config, None, &mut |_, _| {}).is_err());
1389 assert!(run_dipole_inversion(&field, &mask, &meta, &config, Some(&given[..n - 1]), &mut |_, _| {}).is_err());
1390 }
1391 let meta = |tes: Vec<f64>| ScanMetadata {
1393 dims: (nx, ny, nz), voxel_size: (1.0, 1.0, 1.0),
1394 echo_times: tes, field_strength: 7.0, b0_direction: (0.0, 0.0, 1.0),
1395 slice_geometry: None,
1396 affine: None,
1397 };
1398 assert!(run_dipole_inversion(&field, &mask, &meta(vec![0.004, 0.010, 0.016]), &config, Some(&mag), &mut |_, _| {}).is_err());
1399 assert!(run_dipole_inversion(&field, &mask, &meta(vec![0.004, 0.010, 0.020]), &config, Some(&echoes), &mut |_, _| {}).is_err());
1400
1401 let msmv = InversionConfig {
1404 medi: crate::inversion::MediParams { msmv: Some(crate::inversion::MsmvParams::default()), ..medi.clone() },
1405 ..config.clone()
1406 };
1407 let err = run_dipole_inversion(&field, &mask, &meta(vec![0.02]), &msmv, Some(&mag), &mut |_, _| {});
1408 assert!(matches!(err, Err(PipelineError::InvalidInput(ref m)) if m.contains("mSMV")), "{err:?}");
1409 let m3 = meta(vec![0.004, 0.010, 0.016]);
1410 let via_stage = run_dipole_inversion(&field, &mask, &m3, &msmv, Some(&echoes), &mut |_, _| {}).unwrap();
1411 let params = crate::inversion::MediParams { b0: 7.0, echo_times: m3.echo_times.clone(), ..msmv.medi.clone() };
1412 let direct = crate::inversion::medi(
1413 &field, &mask, EchoMagnitudes { data: &echoes, n_echoes: 3 }, &m3.grid(), m3.b0_direction, ¶ms, |_, _| {},
1414 );
1415 assert_eq!(via_stage, direct);
1416 }
1417
1418 #[test]
1424 fn echo_time_defaults_are_the_references() {
1425 use crate::EchoTime::{First, Last, Spacing, Sum};
1426 let c = InversionConfig::default();
1427 assert_eq!(c.ndi.te, First);
1428 assert_eq!(c.fansi.te, Spacing);
1429 assert_eq!(c.l1qsm.te, First);
1430 assert_eq!(c.whqsm.te, First);
1431 assert_eq!(c.whqsm.weight, crate::inversion::WhQsmWeight::Flat);
1432 assert_eq!(c.nltv.te, First);
1433 assert_eq!(c.tgv.te, First);
1434 assert_eq!(c.star.te, Sum);
1435 assert_eq!(c.medi.te, Spacing);
1436 assert_eq!(c.tfi.te, Spacing);
1437 assert_eq!(c.heidi.te, Last);
1438 }
1439
1440 #[test]
1443 fn dispatcher_converts_at_the_chosen_echo_time() {
1444 let (nx, ny, nz) = (8, 8, 8);
1445 let n = nx * ny * nz;
1446 let field: Vec<f64> = (0..n).map(|i| 0.05 * ((i as f64) * 0.37).sin()).collect();
1447 let mask = vec![1u8; n];
1448 let mag = vec![1.0; n];
1449 let meta = ScanMetadata {
1450 dims: (nx, ny, nz), voxel_size: (1.0, 1.0, 1.0),
1451 echo_times: vec![0.004, 0.012, 0.020], field_strength: 3.0, b0_direction: (0.0, 0.0, 1.0),
1452 slice_geometry: None,
1453 affine: None,
1454 };
1455 let ndi = crate::inversion::NdiParams { max_iter: 3, ..Default::default() };
1456 let run = |te| {
1457 let config = InversionConfig {
1458 algorithm: InversionAlgorithm::Ndi,
1459 ndi: crate::inversion::NdiParams { te, ..ndi.clone() },
1460 ..Default::default()
1461 };
1462 run_dipole_inversion(&field, &mask, &meta, &config, Some(&mag), &mut |_, _| {})
1463 };
1464 let direct = |te_s: f64| crate::inversion::ndi(
1465 &field, &mask, &mag, &meta.grid(), meta.b0_direction,
1466 &crate::inversion::NdiParams { echo_times: vec![te_s], te: crate::EchoTime::First, ..ndi.clone() },
1467 |_, _| {},
1468 );
1469 assert_eq!(run(crate::EchoTime::Spacing).unwrap(), direct(0.012 - 0.004));
1471 assert_eq!(run(crate::EchoTime::Last).unwrap(), direct(0.020));
1472 assert_ne!(direct(0.004), direct(0.012 - 0.004));
1473 let meta_none = ScanMetadata { echo_times: vec![], ..meta.clone() };
1474 let config = InversionConfig { algorithm: InversionAlgorithm::Ndi, ndi, ..Default::default() };
1475 let err = run_dipole_inversion(&field, &mask, &meta_none, &config, Some(&mag), &mut |_, _| {})
1476 .unwrap_err();
1477 assert!(err.to_string().contains("first echo time"), "{err}");
1478 let whqsm = crate::inversion::WhQsmParams { max_iter: 2, ..Default::default() };
1480 let config = InversionConfig { algorithm: InversionAlgorithm::Whqsm, whqsm: whqsm.clone(), ..Default::default() };
1481 assert!(run_dipole_inversion(&field, &mask, &meta, &config, None, &mut |_, _| {}).is_ok());
1482 let config = InversionConfig {
1483 whqsm: crate::inversion::WhQsmParams { weight: crate::inversion::WhQsmWeight::MaxNormalised, ..whqsm },
1484 ..config
1485 };
1486 let err = run_dipole_inversion(&field, &mask, &meta, &config, None, &mut |_, _| {}).unwrap_err();
1487 assert!(err.to_string().contains("needs magnitude data"), "{err}");
1488 }
1489
1490 #[test]
1491 fn tfi_gets_the_total_field_and_scan_parameters() {
1492 let (nx, ny, nz) = (10, 10, 10);
1493 let n = nx * ny * nz;
1494 let field: Vec<f64> = (0..n)
1496 .map(|i| {
1497 let (x, y, z) = ((i % nx) as f64, ((i / nx) % ny) as f64, (i / (nx * ny)) as f64);
1498 0.5 * (z - 4.5) / 4.5 + 0.02 * (-((x - 4.5).powi(2) + (y - 4.5).powi(2) + (z - 4.5).powi(2)) / 4.0).exp()
1499 })
1500 .collect();
1501 let mask: Vec<u8> = (0..n)
1502 .map(|i| {
1503 let (x, y, z) = (i % nx, (i / nx) % ny, i / (nx * ny));
1504 u8::from((2..8).contains(&x) && (2..8).contains(&y) && (2..8).contains(&z))
1505 })
1506 .collect();
1507 let mag: Vec<f64> = (0..n).map(|i| 1.0 + (i % 3) as f64).collect();
1508 let meta = ScanMetadata {
1509 dims: (nx, ny, nz), voxel_size: (1.0, 1.0, 1.0),
1510 echo_times: vec![0.004, 0.010, 0.016], field_strength: 7.0, b0_direction: (0.0, 0.0, 1.0),
1511 slice_geometry: None,
1512 affine: None,
1513 };
1514 let config = InversionConfig { algorithm: InversionAlgorithm::Tfi, ..Default::default() };
1515 assert_eq!(config.input(2), InversionInput::TotalField);
1516 let tfi = crate::inversion::TfiParams { max_iter: 2, cg_max_iter: 5, ..config.tfi.clone() };
1517 let config = InversionConfig { tfi: tfi.clone(), ..config };
1518 let params = crate::inversion::TfiParams {
1520 b0: 7.0, echo_times: vec![0.006], te: crate::EchoTime::First, ..tfi
1521 };
1522 let echoes: Vec<f64> = (0..3 * n).map(|i| 1.0 + ((i * 5) % 9) as f64).collect();
1523 let via_stage = run_dipole_inversion(&field, &mask, &meta, &config, Some(&echoes), &mut |_, _| {}).unwrap();
1524 let direct = crate::inversion::tfi(
1525 &field, &mask, crate::inversion::EchoMagnitudes { data: &echoes, n_echoes: 3 },
1526 &meta.grid(), meta.b0_direction, ¶ms, |_, _| {},
1527 );
1528 assert_eq!(via_stage, direct);
1529 assert!(via_stage.iter().all(|v| v.is_finite()));
1530 assert!(run_dipole_inversion(&field, &mask, &meta, &config, Some(&mag), &mut |_, _| {}).is_err());
1532 assert!(run_dipole_inversion(&field, &mask, &meta, &config, Some(&mag[..n - 1]), &mut |_, _| {}).is_err());
1533 assert!(run_dipole_inversion(&field, &mask, &meta, &config, None, &mut |_, _| {}).is_err());
1534 }
1535
1536 #[test]
1537 fn test_inversion_rejects_tgv() {
1538 let n = 64;
1539 let meta = ScanMetadata {
1540 dims: (4, 4, 4),
1541 voxel_size: (1.0, 1.0, 1.0),
1542 echo_times: vec![0.005],
1543 field_strength: 3.0,
1544 b0_direction: (0.0, 0.0, 1.0),
1545 slice_geometry: None,
1546 affine: None,
1547 };
1548 let config = InversionConfig {
1549 algorithm: InversionAlgorithm::Tgv,
1550 ..Default::default()
1551 };
1552
1553 let result = run_dipole_inversion(
1554 &vec![0.0; n], &vec![1u8; n], &meta, &config, None, &mut |_, _| {},
1555 );
1556 assert!(result.is_err());
1557 }
1558}