1use anyhow::Result;
26use ndarray::{Array1, Array2};
27
28use super::inverse::prepare_inverse;
29use super::{ForwardOperator, InverseMethod, InverseOperator};
30
31pub fn make_resolution_matrix(
45 inv: &InverseOperator,
46 fwd: &ForwardOperator,
47 lambda2: f64,
48 method: InverseMethod,
49) -> Result<Array2<f64>> {
50 let prepared = prepare_inverse(inv, lambda2, method, None)?;
51 let kernel = &prepared.kernel; let gain = &fwd.gain; let r_raw = kernel.dot(gain);
56
57 let n_orient = fwd.n_orient();
58 let n_src = fwd.n_sources;
59
60 if n_orient == 1 {
61 let mut r = r_raw;
63 if let Some(ref nn) = prepared.noisenorm {
64 for i in 0..r.nrows() {
65 for j in 0..r.ncols() {
66 r[[i, j]] *= nn[i];
67 }
68 }
69 }
70 Ok(r)
71 } else {
72 let mut r = Array2::zeros((n_src, n_src));
75 for i in 0..n_src {
76 for j in 0..n_src {
77 let mut sum_sq = 0.0;
78 for oi in 0..3 {
79 for oj in 0..3 {
80 let v = r_raw[[i * 3 + oi, j * 3 + oj]];
81 sum_sq += v * v;
82 }
83 }
84 r[[i, j]] = sum_sq.sqrt();
85 }
86 }
87 if let Some(ref nn) = prepared.noisenorm {
89 for i in 0..n_src {
90 for j in 0..n_src {
91 r[[i, j]] *= nn[i];
92 }
93 }
94 }
95 Ok(r)
96 }
97}
98
99pub fn get_point_spread(resolution: &Array2<f64>, source_idx: usize) -> Array1<f64> {
106 resolution.column(source_idx).to_owned()
107}
108
109pub fn get_cross_talk(resolution: &Array2<f64>, source_idx: usize) -> Array1<f64> {
116 resolution.row(source_idx).to_owned()
117}
118
119pub fn peak_localisation_error(resolution: &Array2<f64>) -> Array1<usize> {
126 let n = resolution.ncols();
127 let mut errors = Array1::zeros(n);
128 for j in 0..n {
129 let col = resolution.column(j);
130 let peak = col
131 .iter()
132 .enumerate()
133 .max_by(|(_, a), (_, b)| a.abs().partial_cmp(&b.abs()).unwrap())
134 .map(|(idx, _)| idx)
135 .unwrap_or(j);
136 errors[j] = if peak > j { peak - j } else { j - peak };
137 }
138 errors
139}
140
141pub fn spatial_spread(resolution: &Array2<f64>) -> Array1<usize> {
148 let n = resolution.ncols();
149 let mut widths = Array1::zeros(n);
150 for j in 0..n {
151 let col = resolution.column(j);
152 let peak_abs = col.iter().map(|v| v.abs()).fold(0.0_f64, f64::max);
153 if peak_abs > 0.0 {
154 let threshold = peak_abs * 0.5;
155 widths[j] = col.iter().filter(|v| v.abs() >= threshold).count();
156 }
157 }
158 widths
159}
160
161pub fn relative_amplitude(resolution: &Array2<f64>) -> Array1<f64> {
168 let n = resolution.ncols();
169 let mut rel_amp = Array1::zeros(n);
170 for j in 0..n {
171 let col = resolution.column(j);
172 let peak_abs = col.iter().map(|v| v.abs()).fold(0.0_f64, f64::max);
173 let total_abs: f64 = col.iter().map(|v| v.abs()).sum();
174 rel_amp[j] = if total_abs > 0.0 {
175 peak_abs / total_abs
176 } else {
177 0.0
178 };
179 }
180 rel_amp
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use crate::{make_inverse_operator, ForwardOperator, InverseMethod, NoiseCov};
187 use ndarray::Array2;
188
189 fn make_test_setup() -> (ForwardOperator, NoiseCov) {
190 let n_chan = 16;
191 let n_src = 20;
192 let mut gain = Array2::zeros((n_chan, n_src));
193 for i in 0..n_chan {
194 for j in 0..n_src {
195 let dist =
196 ((i as f64 - j as f64 * n_chan as f64 / n_src as f64).powi(2) + 1.0).sqrt();
197 gain[[i, j]] = 1e-8 / dist;
198 }
199 }
200 (
201 ForwardOperator::new_fixed(gain),
202 NoiseCov::diagonal(vec![1e-12; n_chan]),
203 )
204 }
205
206 #[test]
207 fn test_resolution_matrix_shape() {
208 let (fwd, cov) = make_test_setup();
209 let inv = make_inverse_operator(&fwd, &cov, None).unwrap();
210 let r = make_resolution_matrix(&inv, &fwd, 1.0 / 9.0, InverseMethod::MNE).unwrap();
211 assert_eq!(r.dim(), (20, 20));
212 }
213
214 #[test]
215 fn test_resolution_matrix_diagonal_dominance() {
216 let (fwd, cov) = make_test_setup();
217 let inv = make_inverse_operator(&fwd, &cov, None).unwrap();
218 let r = make_resolution_matrix(&inv, &fwd, 1.0 / 9.0, InverseMethod::MNE).unwrap();
219
220 let mut diag_is_large = 0;
222 for j in 0..20 {
223 let col = r.column(j);
224 let diag = col[j].abs();
225 let max = col.iter().map(|v| v.abs()).fold(0.0_f64, f64::max);
226 if diag >= max * 0.5 {
227 diag_is_large += 1;
228 }
229 }
230 assert!(
231 diag_is_large >= 10,
232 "At least half the sources should have diag ≥ 50% of max, got {diag_is_large}"
233 );
234 }
235
236 #[test]
237 fn test_psf_ctf() {
238 let (fwd, cov) = make_test_setup();
239 let inv = make_inverse_operator(&fwd, &cov, None).unwrap();
240 let r = make_resolution_matrix(&inv, &fwd, 1.0 / 9.0, InverseMethod::MNE).unwrap();
241
242 let psf = get_point_spread(&r, 10);
243 let ctf = get_cross_talk(&r, 10);
244 assert_eq!(psf.len(), 20);
245 assert_eq!(ctf.len(), 20);
246 }
247
248 #[test]
249 fn test_peak_localisation_error() {
250 let (fwd, cov) = make_test_setup();
251 let inv = make_inverse_operator(&fwd, &cov, None).unwrap();
252 let r = make_resolution_matrix(&inv, &fwd, 1.0 / 9.0, InverseMethod::MNE).unwrap();
253
254 let errors = peak_localisation_error(&r);
255 assert_eq!(errors.len(), 20);
256 let mean_error: f64 = errors.iter().map(|&e| e as f64).sum::<f64>() / 20.0;
258 assert!(
259 mean_error < 5.0,
260 "Mean peak error = {mean_error}, expected < 5"
261 );
262 }
263
264 #[test]
265 fn test_spatial_spread_and_relative_amplitude() {
266 let (fwd, cov) = make_test_setup();
267 let inv = make_inverse_operator(&fwd, &cov, None).unwrap();
268 let r = make_resolution_matrix(&inv, &fwd, 1.0 / 9.0, InverseMethod::MNE).unwrap();
269
270 let spread = spatial_spread(&r);
271 let rel_amp = relative_amplitude(&r);
272 assert_eq!(spread.len(), 20);
273 assert_eq!(rel_amp.len(), 20);
274
275 for &v in rel_amp.iter() {
277 assert!(v >= 0.0 && v <= 1.0, "rel_amp = {v} out of range");
278 }
279 }
280
281 #[test]
282 fn test_resolution_with_dspm() {
283 let (fwd, cov) = make_test_setup();
284 let inv = make_inverse_operator(&fwd, &cov, None).unwrap();
285 let r = make_resolution_matrix(&inv, &fwd, 1.0 / 9.0, InverseMethod::DSPM).unwrap();
286 assert_eq!(r.dim(), (20, 20));
287 assert!(r.iter().all(|v| v.is_finite()));
288 }
289}