1use crate::utils::wavelet::{dwt_max_level, WaveletPlan};
38use crate::Grid;
39
40#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
42#[derive(Clone, Debug)]
43pub struct WaveSepParams {
44 pub dr_pos: f64,
46 pub dr_neg: f64,
48 pub alpha: f64,
50 pub lambda: f64,
52 pub wavelet_order: usize,
54 pub max_iter: usize,
56 pub tol: f64,
58}
59
60impl Default for WaveSepParams {
61 fn default() -> Self {
62 Self {
63 dr_pos: 137.0,
64 dr_neg: 137.0,
65 alpha: 0.2,
66 lambda: 0.02,
67 wavelet_order: 4,
68 max_iter: 100,
69 tol: 1e-3,
70 }
71 }
72}
73
74pub fn wavesep(
90 chi_total: &[f64],
91 r2prime: &[f64],
92 mask: &[u8],
93 grid: &Grid,
94 params: &WaveSepParams,
95 mut progress: impl FnMut(usize, usize),
96) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
97 let (nx, ny, nz) = grid.dims;
98 let n = nx * ny * nz;
99 assert_eq!(chi_total.len(), n, "chi_total length must match grid");
100 assert_eq!(r2prime.len(), n, "r2prime length must match grid");
101 assert_eq!(mask.len(), n, "mask length must match grid");
102
103 let dec_len = 2 * params.wavelet_order;
105 let (pdims, level) = pad_spec((nx, ny, nz), dec_len);
106 let (pnx, pny, pnz) = pdims;
107 let pn = pnx * pny * pnz;
108
109 let maskf: Vec<f64> = mask.iter().map(|&m| m as f64).collect();
111 let mask_p = pad3d(&maskf, (nx, ny, nz), pdims);
112 let qsm_p = {
113 let mut q = pad3d(chi_total, (nx, ny, nz), pdims);
114 for i in 0..pn {
115 q[i] *= mask_p[i];
116 }
117 q
118 };
119 let r2p_p = {
120 let mut r = pad3d(r2prime, (nx, ny, nz), pdims);
121 for i in 0..pn {
122 r[i] *= mask_p[i];
123 }
124 r
125 };
126
127 let plan = WaveletPlan::new(params.wavelet_order, pdims, level);
128 let th = params.alpha * params.lambda;
129 let dr_ratio = params.dr_neg / params.dr_pos;
130
131 let mut xp = vec![0.0_f64; pn];
134 let mut xn = vec![0.0_f64; pn];
135 let mut prev = vec![0.0_f64; 2 * pn];
136
137 for it in 0..params.max_iter {
138 progress(it + 1, params.max_iter);
139 for i in 0..pn {
143 let gq = xp[i] + xn[i] - qsm_p[i];
144 let r = xp[i] - dr_ratio * xn[i] - r2p_p[i] / params.dr_pos;
145 xp[i] -= params.alpha * (gq + r);
146 xn[i] -= params.alpha * (gq - dr_ratio * r);
147 }
148
149 prox_wavelet_l1(&mut xp, &plan, th);
151 prox_wavelet_l1(&mut xn, &plan, th);
152
153 let mut diff = 0.0_f64;
155 let mut norm = 0.0_f64;
156 for i in 0..pn {
157 let m = mask_p[i];
158 xp[i] = if xp[i] < 0.0 { 0.0 } else { xp[i] } * m;
159 xn[i] = if xn[i] > 0.0 { 0.0 } else { xn[i] } * m;
160 let dp = xp[i] - prev[i];
161 let dn = xn[i] - prev[pn + i];
162 diff += dp * dp + dn * dn;
163 norm += xp[i] * xp[i] + xn[i] * xn[i];
164 prev[i] = xp[i];
165 prev[pn + i] = xn[i];
166 }
167 if norm > 0.0 && (diff.sqrt() / norm.sqrt()) < params.tol {
168 break;
169 }
170 }
171
172 let xp_c = unpad3d(&xp, pdims, (nx, ny, nz));
174 let xn_c = unpad3d(&xn, pdims, (nx, ny, nz));
175 let mut chi_pos = vec![0.0_f64; n];
176 let mut chi_neg = vec![0.0_f64; n];
177 let mut chi_out = vec![0.0_f64; n];
178 for i in 0..n {
179 if mask[i] != 0 {
180 chi_pos[i] = xp_c[i];
181 chi_neg[i] = xn_c[i];
182 chi_out[i] = xp_c[i] + xn_c[i];
183 }
184 }
185 (chi_pos, chi_neg, chi_out)
186}
187
188fn prox_wavelet_l1(x: &mut [f64], plan: &WaveletPlan, th: f64) {
191 let mut coef = plan.forward(x);
192 for c in coef.iter_mut() {
193 *c = soft_threshold(*c, th);
194 }
195 let rec = plan.inverse(&coef);
196 x.copy_from_slice(&rec);
197}
198
199#[inline]
200fn soft_threshold(z: f64, th: f64) -> f64 {
201 if z > th {
202 z - th
203 } else if z < -th {
204 z + th
205 } else {
206 0.0
207 }
208}
209
210fn pad_spec(dims: (usize, usize, usize), dec_len: usize) -> ((usize, usize, usize), usize) {
214 let ds = [dims.0, dims.1, dims.2];
215 let mut p = 16usize;
216 loop {
217 let padded = [
218 ds[0].div_ceil(p) * p,
219 ds[1].div_ceil(p) * p,
220 ds[2].div_ceil(p) * p,
221 ];
222 let min_padded = *padded.iter().min().unwrap();
223 let level = dwt_max_level(min_padded, dec_len);
224 let m = 1usize << level;
225 if padded.iter().all(|&d| d % m == 0) {
226 return ((padded[0], padded[1], padded[2]), level);
227 }
228 p *= 2;
229 }
230}
231
232fn pad3d(a: &[f64], from: (usize, usize, usize), to: (usize, usize, usize)) -> Vec<f64> {
235 let (fx, fy, fz) = from;
236 let (tx, ty, tz) = to;
237 let mut out = vec![0.0_f64; tx * ty * tz];
238 for k in 0..fz {
239 for j in 0..fy {
240 let src = (k * fy + j) * fx;
241 let dst = (k * ty + j) * tx;
242 out[dst..dst + fx].copy_from_slice(&a[src..src + fx]);
243 }
244 }
245 out
246}
247
248fn unpad3d(a: &[f64], from: (usize, usize, usize), to: (usize, usize, usize)) -> Vec<f64> {
250 let (fx, fy, _fz) = from;
251 let (tx, ty, tz) = to;
252 let mut out = vec![0.0_f64; tx * ty * tz];
253 for k in 0..tz {
254 for j in 0..ty {
255 let src = (k * fy + j) * fx;
256 let dst = (k * ty + j) * tx;
257 out[dst..dst + tx].copy_from_slice(&a[src..src + tx]);
258 }
259 }
260 out
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 fn grid(nx: usize, ny: usize, nz: usize) -> Grid {
268 Grid::new(nx, ny, nz, 1.0, 1.0, 1.0)
269 }
270
271 #[test]
272 fn pad_spec_divisible_and_covers() {
273 for dims in [(64, 64, 40), (100, 100, 60), (48, 56, 72)] {
274 let (pd, level) = pad_spec(dims, 8);
275 let m = 1usize << level;
276 assert!(pd.0 % m == 0 && pd.1 % m == 0 && pd.2 % m == 0, "divisible {dims:?}");
277 assert!(pd.0 >= dims.0 && pd.1 >= dims.1 && pd.2 >= dims.2, "covers {dims:?}");
278 }
279 }
280
281 #[test]
282 fn pad_unpad_roundtrip() {
283 let from = (5, 6, 7);
284 let to = (8, 8, 8);
285 let n = 5 * 6 * 7;
286 let a: Vec<f64> = (0..n).map(|i| i as f64).collect();
287 let p = pad3d(&a, from, to);
288 let back = unpad3d(&p, to, from);
289 assert_eq!(a, back);
290 }
291
292 #[test]
295 fn recovers_model_sources() {
296 let (nx, ny, nz) = (16, 16, 16);
297 let g = grid(nx, ny, nz);
298 let n = nx * ny * nz;
299 let dr = 137.0;
300
301 let mut chi_pos = vec![0.0_f64; n];
303 let mut chi_neg = vec![0.0_f64; n]; let mut mask = vec![0u8; n];
305 for k in 0..nz {
306 for j in 0..ny {
307 for i in 0..nx {
308 let idx = i + j * nx + k * nx * ny;
309 let inside = (3..13).contains(&i) && (3..13).contains(&j) && (3..13).contains(&k);
310 if inside {
311 mask[idx] = 1;
312 let fx = i as f64 / nx as f64;
313 let fy = j as f64 / ny as f64;
314 chi_pos[idx] = 0.10 * (1.0 + (fx * 6.0).sin()).max(0.0);
315 chi_neg[idx] = -0.08 * (1.0 + (fy * 5.0).cos()).max(0.0);
316 }
317 }
318 }
319 }
320 let chi_total: Vec<f64> = (0..n).map(|i| chi_pos[i] + chi_neg[i]).collect();
322 let r2p: Vec<f64> = (0..n).map(|i| dr * (chi_pos[i] - chi_neg[i])).collect();
323
324 let params = WaveSepParams {
325 dr_pos: dr,
326 dr_neg: dr,
327 alpha: 0.2,
328 lambda: 0.002, wavelet_order: 4,
330 max_iter: 300,
331 tol: 1e-4,
332 };
333 let (para, neg, total) = wavesep(&chi_total, &r2p, &mask, &g, ¶ms, |_, _| {});
334
335 let corr = |a: &[f64], b: &[f64]| {
337 let idx: Vec<usize> = (0..n).filter(|&i| mask[i] == 1).collect();
338 let m = idx.len() as f64;
339 let ma = idx.iter().map(|&i| a[i]).sum::<f64>() / m;
340 let mb = idx.iter().map(|&i| b[i]).sum::<f64>() / m;
341 let mut sab = 0.0;
342 let mut saa = 0.0;
343 let mut sbb = 0.0;
344 for &i in &idx {
345 let da = a[i] - ma;
346 let db = b[i] - mb;
347 sab += da * db;
348 saa += da * da;
349 sbb += db * db;
350 }
351 sab / (saa.sqrt() * sbb.sqrt() + 1e-20)
352 };
353 let cp = corr(¶, &chi_pos);
355 let cn = corr(&neg, &chi_neg);
356 assert!(cp > 0.9, "χ+ correlation too low: {cp:.3}");
357 assert!(cn > 0.9, "χ− correlation too low: {cn:.3}");
358 for i in 0..n {
360 if mask[i] == 1 {
361 assert!(neg[i] <= 1e-9, "χ− must be ≤ 0");
362 assert!((total[i] - (para[i] + neg[i])).abs() < 1e-9, "χ_total = χ+ + χ−");
363 }
364 }
365 }
366}