qsm_core/pipeline/
phase_utils.rs1use std::f64::consts::PI;
6
7use crate::constants::GAMMA_BAR_HZ_PER_T;
8
9pub fn scale_phase_to_pi(data: &mut [f64]) {
15 if data.is_empty() {
16 return;
17 }
18
19 let mut min_val = f64::INFINITY;
20 let mut max_val = f64::NEG_INFINITY;
21 for &v in data.iter() {
22 if v.is_finite() {
23 if v < min_val {
24 min_val = v;
25 }
26 if v > max_val {
27 max_val = v;
28 }
29 }
30 }
31
32 for v in data.iter_mut() {
34 if !v.is_finite() {
35 *v = 0.0;
36 }
37 }
38
39 let range = max_val - min_val;
40 if range < 1e-10 {
41 return;
42 }
43
44 let tol = 0.1 * PI;
46 if (min_val + PI).abs() < tol && (max_val - PI).abs() < tol {
47 return;
48 }
49
50 let scale = 2.0 * PI / range;
52 for v in data.iter_mut() {
53 *v = (*v - min_val) * scale - PI;
54 }
55}
56
57pub fn hz_to_ppm(field_hz: &[f64], field_strength: f64) -> Vec<f64> {
59 let gamma_hz = GAMMA_BAR_HZ_PER_T;
60 let scale = 1e6 / (gamma_hz * field_strength);
61 field_hz.iter().map(|&v| v * scale).collect()
62}
63
64pub fn rads_to_ppm(field_rads: &[f64], field_strength: f64) -> Vec<f64> {
66 let gamma_hz = GAMMA_BAR_HZ_PER_T;
67 let scale = 1e6 / (2.0 * PI * gamma_hz * field_strength);
68 field_rads.iter().map(|&v| v * scale).collect()
69}
70
71pub fn ppm_to_hz(field_ppm: &[f64], field_strength: f64) -> Vec<f64> {
73 let scale = GAMMA_BAR_HZ_PER_T * field_strength * 1e-6;
74 field_ppm.iter().map(|&v| v * scale).collect()
75}
76
77pub fn ppm_to_rads(field_ppm: &[f64], field_strength: f64) -> Vec<f64> {
79 let scale = 2.0 * PI * GAMMA_BAR_HZ_PER_T * field_strength * 1e-6;
80 field_ppm.iter().map(|&v| v * scale).collect()
81}
82
83pub fn rss_combine(magnitudes: &[&[f64]]) -> Result<Vec<f64>, crate::error::InputError> {
93 if magnitudes.is_empty() {
94 return Ok(Vec::new());
95 }
96 let n = magnitudes[0].len();
97 for (e, mag) in magnitudes.iter().enumerate() {
98 if mag.len() != n {
99 return Err(crate::error::InputError(format!(
100 "rss_combine: echo {e} has {} voxels but echo 0 has {n} \
101 (all echoes must come from the same acquisition)",
102 mag.len()
103 )));
104 }
105 }
106 let mut combined = vec![0.0f64; n];
107 for mag in magnitudes {
108 for (i, &v) in mag.iter().enumerate() {
109 combined[i] += v * v;
110 }
111 }
112 for v in &mut combined {
113 *v = v.sqrt();
114 }
115 Ok(combined)
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn test_scale_phase_already_in_range() {
124 let mut data = vec![-PI, 0.0, PI];
125 let original = data.clone();
126 scale_phase_to_pi(&mut data);
127 for (a, b) in data.iter().zip(original.iter()) {
128 assert!((a - b).abs() < 1e-10);
129 }
130 }
131
132 #[test]
133 fn test_scale_phase_rescales_0_to_4096() {
134 let mut data = vec![0.0, 2048.0, 4096.0];
135 scale_phase_to_pi(&mut data);
136 assert!((data[0] - (-PI)).abs() < 1e-10);
137 assert!((data[2] - PI).abs() < 1e-10);
138 assert!(data[1].abs() < 1e-10);
139 }
140
141 #[test]
142 fn test_scale_phase_nan_replaced() {
143 let mut data = vec![0.0, f64::NAN, 4096.0];
144 scale_phase_to_pi(&mut data);
145 assert!(data[1].is_finite());
146 }
147
148 #[test]
149 fn test_scale_phase_empty() {
150 let mut data: Vec<f64> = vec![];
151 scale_phase_to_pi(&mut data);
152 assert!(data.is_empty());
153 }
154
155 #[test]
157 fn test_scale_phase_constant_is_left_alone() {
158 let mut data = vec![5.0, 5.0, 5.0];
159 scale_phase_to_pi(&mut data);
160 assert!(data.iter().all(|v| (v - 5.0).abs() < 1e-10), "{data:?}");
161 }
162
163 #[test]
165 fn test_scale_phase_all_nan_is_zero() {
166 let mut data = vec![f64::NAN, f64::INFINITY, f64::NAN];
167 scale_phase_to_pi(&mut data);
168 assert_eq!(data, vec![0.0, 0.0, 0.0]);
169 }
170
171 #[test]
172 fn test_hz_to_ppm_3t() {
173 let gamma_hz = GAMMA_BAR_HZ_PER_T;
174 let field = vec![gamma_hz * 3.0];
175 let ppm = hz_to_ppm(&field, 3.0);
176 assert!((ppm[0] - 1e6).abs() < 1.0);
177 }
178
179 #[test]
180 fn test_rads_to_ppm_3t() {
181 let gamma_hz = GAMMA_BAR_HZ_PER_T;
182 let rads = vec![2.0 * PI * gamma_hz * 3.0];
183 let ppm = rads_to_ppm(&rads, 3.0);
184 assert!((ppm[0] - 1e6).abs() < 1.0);
185 }
186
187 #[test]
188 fn ppm_hz_rads_round_trip() {
189 let ppm = vec![-0.25, 0.0, 1.5];
190 for (a, b) in hz_to_ppm(&ppm_to_hz(&ppm, 7.0), 7.0).iter().zip(&ppm) {
191 assert!((a - b).abs() < 1e-12);
192 }
193 for (a, b) in rads_to_ppm(&ppm_to_rads(&ppm, 3.0), 3.0).iter().zip(&ppm) {
194 assert!((a - b).abs() < 1e-12);
195 }
196 assert!((ppm_to_hz(&[1.0], 3.0)[0] - 127.728).abs() < 1e-9);
198 }
199
200 #[test]
201 fn test_rss_combine() {
202 let a = vec![3.0, 0.0];
203 let b = vec![4.0, 1.0];
204 let result = rss_combine(&[&a, &b]).unwrap();
205 assert!((result[0] - 5.0).abs() < 1e-10);
206 assert!((result[1] - 1.0).abs() < 1e-10);
207 }
208
209 #[test]
210 fn test_rss_combine_empty() {
211 let result = rss_combine(&[]).unwrap();
212 assert!(result.is_empty());
213 }
214
215 #[test]
219 fn rss_combine_refuses_a_longer_echo() {
220 let e0 = vec![1.0; 512];
221 let e1 = vec![1.0; 576];
222 let err = rss_combine(&[&e0, &e1]).unwrap_err();
223 assert!(err.to_string().contains("rss_combine: echo 1 has 576 voxels but echo 0 has 512"), "{err}");
224 }
225
226 #[test]
227 fn rss_combine_refuses_a_shorter_echo() {
228 let e0 = vec![1.0; 512];
231 let e1 = vec![1.0; 216];
232 let err = rss_combine(&[&e0, &e1]).unwrap_err();
233 assert!(err.to_string().contains("rss_combine: echo 1 has 216 voxels but echo 0 has 512"), "{err}");
234 }
235}