qsm_core/pipeline/
phase_utils.rs1use std::f64::consts::PI;
6
7pub fn scale_phase_to_pi(data: &mut [f64]) {
13 if data.is_empty() {
14 return;
15 }
16
17 let mut min_val = f64::INFINITY;
18 let mut max_val = f64::NEG_INFINITY;
19 for &v in data.iter() {
20 if v.is_finite() {
21 if v < min_val {
22 min_val = v;
23 }
24 if v > max_val {
25 max_val = v;
26 }
27 }
28 }
29
30 for v in data.iter_mut() {
32 if !v.is_finite() {
33 *v = 0.0;
34 }
35 }
36
37 let range = max_val - min_val;
38 if range < 1e-10 {
39 return;
40 }
41
42 let tol = 0.1 * PI;
44 if (min_val + PI).abs() < tol && (max_val - PI).abs() < tol {
45 return;
46 }
47
48 let scale = 2.0 * PI / range;
50 for v in data.iter_mut() {
51 *v = (*v - min_val) * scale - PI;
52 }
53}
54
55pub fn hz_to_ppm(field_hz: &[f64], field_strength: f64) -> Vec<f64> {
57 let gamma_hz = 42.576e6; let scale = 1e6 / (gamma_hz * field_strength);
59 field_hz.iter().map(|&v| v * scale).collect()
60}
61
62pub fn rads_to_ppm(field_rads: &[f64], field_strength: f64) -> Vec<f64> {
64 let gamma_hz = 42.576e6;
65 let scale = 1e6 / (2.0 * PI * gamma_hz * field_strength);
66 field_rads.iter().map(|&v| v * scale).collect()
67}
68
69pub fn rss_combine(magnitudes: &[&[f64]]) -> Vec<f64> {
71 if magnitudes.is_empty() {
72 return Vec::new();
73 }
74 let n = magnitudes[0].len();
75 let mut combined = vec![0.0f64; n];
76 for mag in magnitudes {
77 for (i, &v) in mag.iter().enumerate() {
78 combined[i] += v * v;
79 }
80 }
81 for v in &mut combined {
82 *v = v.sqrt();
83 }
84 combined
85}
86
87pub use crate::utils::mask::{erode_mask, dilate_mask};
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93
94 #[test]
95 fn test_scale_phase_already_in_range() {
96 let mut data = vec![-PI, 0.0, PI];
97 let original = data.clone();
98 scale_phase_to_pi(&mut data);
99 for (a, b) in data.iter().zip(original.iter()) {
100 assert!((a - b).abs() < 1e-10);
101 }
102 }
103
104 #[test]
105 fn test_scale_phase_rescales_0_to_4096() {
106 let mut data = vec![0.0, 2048.0, 4096.0];
107 scale_phase_to_pi(&mut data);
108 assert!((data[0] - (-PI)).abs() < 1e-10);
109 assert!((data[2] - PI).abs() < 1e-10);
110 assert!(data[1].abs() < 1e-10);
111 }
112
113 #[test]
114 fn test_scale_phase_nan_replaced() {
115 let mut data = vec![0.0, f64::NAN, 4096.0];
116 scale_phase_to_pi(&mut data);
117 assert!(data[1].is_finite());
118 }
119
120 #[test]
121 fn test_scale_phase_empty() {
122 let mut data: Vec<f64> = vec![];
123 scale_phase_to_pi(&mut data);
124 assert!(data.is_empty());
125 }
126
127 #[test]
128 fn test_hz_to_ppm_3t() {
129 let gamma_hz = 42.576e6;
130 let field = vec![gamma_hz * 3.0];
131 let ppm = hz_to_ppm(&field, 3.0);
132 assert!((ppm[0] - 1e6).abs() < 1.0);
133 }
134
135 #[test]
136 fn test_rads_to_ppm_3t() {
137 let gamma_hz = 42.576e6;
138 let rads = vec![2.0 * PI * gamma_hz * 3.0];
139 let ppm = rads_to_ppm(&rads, 3.0);
140 assert!((ppm[0] - 1e6).abs() < 1.0);
141 }
142
143 #[test]
144 fn test_rss_combine() {
145 let a = vec![3.0, 0.0];
146 let b = vec![4.0, 1.0];
147 let result = rss_combine(&[&a, &b]);
148 assert!((result[0] - 5.0).abs() < 1e-10);
149 assert!((result[1] - 1.0).abs() < 1e-10);
150 }
151
152 #[test]
153 fn test_rss_combine_empty() {
154 let result = rss_combine(&[]);
155 assert!(result.is_empty());
156 }
157
158 #[test]
159 fn test_erode_mask_cube() {
160 let mask = vec![1u8; 27]; let grid = crate::Grid::new(3, 3, 3, 1.0, 1.0, 1.0);
162 let result = erode_mask(&mask, &grid, 1);
163 let center = 1 + 3 + 9;
164 assert_eq!(result[center], 1);
165 let total: u32 = result.iter().map(|&v| v as u32).sum();
166 assert_eq!(total, 1);
167 }
168
169 #[test]
170 fn test_dilate_mask_single() {
171 let mut mask = vec![0u8; 27];
172 mask[1 + 3 + 9] = 1; let grid = crate::Grid::new(3, 3, 3, 1.0, 1.0, 1.0);
174 let result = dilate_mask(&mask, &grid, 1);
175 let total: u32 = result.iter().map(|&v| v as u32).sum();
176 assert_eq!(total, 7); }
178}