use ndarray::{Array2, Array3};
use num_complex::Complex32;
use crate::prewhiten::{cholesky_lower, invert_lower_triangular};
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum SenseError {
#[error("SENSE: acceleration factor must be >= 1")]
BadAcceleration,
#[error("SENSE: aliased and map shapes do not match ({aliased:?} vs {maps:?})")]
ShapeMismatch {
aliased: (usize, usize, usize),
maps: (usize, usize, usize),
},
#[error("SENSE: Ny ({ny}) must be divisible by R ({r})")]
IndivisibleNy { ny: usize, r: usize },
}
pub fn sense_unfold_1d(
aliased: &Array3<Complex32>,
maps: &Array3<Complex32>,
r: usize,
ridge: f32,
) -> Result<Array2<Complex32>, SenseError> {
let (nc, ny, nx) = aliased.dim();
if r < 1 {
return Err(SenseError::BadAcceleration);
}
if maps.dim() != (nc, ny, nx) {
return Err(SenseError::ShapeMismatch {
aliased: aliased.dim(),
maps: maps.dim(),
});
}
if ny % r != 0 {
return Err(SenseError::IndivisibleNy { ny, r });
}
let ny_red = ny / r;
let mut out = Array2::<Complex32>::zeros((ny, nx));
if nc == 0 || nx == 0 || ny_red == 0 {
return Ok(out);
}
let mut c_mat = Array2::<Complex32>::zeros((nc, r));
let mut chc = Array2::<Complex32>::zeros((r, r));
let mut rhs = vec![Complex32::new(0.0, 0.0); r];
for x in 0..nx {
for y0 in 0..ny_red {
for c in 0..nc {
for k in 0..r {
c_mat[[c, k]] = maps[[c, y0 + k * ny_red, x]];
}
}
for a in 0..r {
for b in 0..r {
let mut acc = Complex32::new(0.0, 0.0);
for c in 0..nc {
acc += c_mat[[c, a]].conj() * c_mat[[c, b]];
}
if a == b {
acc += Complex32::new(ridge, 0.0);
}
chc[[a, b]] = acc;
}
}
for a in 0..r {
let mut acc = Complex32::new(0.0, 0.0);
for c in 0..nc {
acc += c_mat[[c, a]].conj() * aliased[[c, y0, x]];
}
rhs[a] = acc * Complex32::new(r as f32, 0.0);
}
let Some(lower) = cholesky_lower(&chc) else {
continue;
};
let Some(inv) = invert_lower_triangular(&lower) else {
continue;
};
let mut tmp = vec![Complex32::new(0.0, 0.0); r];
for i in 0..r {
let mut acc = Complex32::new(0.0, 0.0);
for j in 0..=i {
acc += inv[[i, j]] * rhs[j];
}
tmp[i] = acc;
}
let mut rho = vec![Complex32::new(0.0, 0.0); r];
for i in 0..r {
let mut acc = Complex32::new(0.0, 0.0);
for j in i..r {
acc += inv[[j, i]].conj() * tmp[j];
}
rho[i] = acc;
}
for k in 0..r {
out[[y0 + k * ny_red, x]] = rho[k];
}
}
}
Ok(out)
}
pub fn sense_gfactor_1d(
maps: &Array3<Complex32>,
r: usize,
ridge: f32,
) -> Result<Array2<f32>, SenseError> {
let (nc, ny, nx) = maps.dim();
if r < 1 {
return Err(SenseError::BadAcceleration);
}
if ny % r != 0 {
return Err(SenseError::IndivisibleNy { ny, r });
}
let ny_red = ny / r;
let mut g = Array2::<f32>::zeros((ny, nx));
if nc == 0 || nx == 0 || ny_red == 0 {
return Ok(g);
}
let mut c_mat = Array2::<Complex32>::zeros((nc, r));
let mut chc = Array2::<Complex32>::zeros((r, r));
for x in 0..nx {
for y0 in 0..ny_red {
for c in 0..nc {
for k in 0..r {
c_mat[[c, k]] = maps[[c, y0 + k * ny_red, x]];
}
}
for a in 0..r {
for b in 0..r {
let mut acc = Complex32::new(0.0, 0.0);
for c in 0..nc {
acc += c_mat[[c, a]].conj() * c_mat[[c, b]];
}
if a == b {
acc += Complex32::new(ridge, 0.0);
}
chc[[a, b]] = acc;
}
}
let Some(lower) = cholesky_lower(&chc) else {
continue;
};
let Some(inv_l) = invert_lower_triangular(&lower) else {
continue;
};
for k in 0..r {
let mut inv_diag = 0.0f32;
for j in k..r {
inv_diag += inv_l[[j, k]].norm_sqr();
}
let chc_diag = chc[[k, k]].re.max(0.0);
let val = (inv_diag * chc_diag).max(0.0).sqrt();
g[[y0 + k * ny_red, x]] = val;
}
}
}
Ok(g)
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::Array1;
use rustfft::FftPlanner;
use std::f32::consts::PI;
#[test]
fn sense_unfolds_r2_phantom_below_threshold() {
let nc = 4usize;
let ny = 32usize;
let nx = 24usize;
let r = 2usize;
let mut truth = Array2::<f32>::zeros((ny, nx));
for y in 6..26 {
for x in 4..20 {
truth[[y, x]] = 1.0;
}
}
for y in 14..18 {
for x in 10..14 {
truth[[y, x]] = 0.3;
}
}
let mut maps = Array3::<Complex32>::zeros((nc, ny, nx));
let centres = [(0.25, 0.25), (0.25, 0.75), (0.75, 0.25), (0.75, 0.75)];
for (c, (fy, fx)) in centres.iter().enumerate() {
let cy = fy * ny as f32;
let cx = fx * nx as f32;
for y in 0..ny {
for x in 0..nx {
let dy = y as f32 - cy;
let dx = x as f32 - cx;
let mag = (-(dy * dy + dx * dx) / 200.0).exp();
let ph = 0.2 * (c as f32);
maps[[c, y, x]] = Complex32::new(mag * ph.cos(), mag * ph.sin());
}
}
}
let mut coil_img = Array3::<Complex32>::zeros((nc, ny, nx));
for c in 0..nc {
for y in 0..ny {
for x in 0..nx {
coil_img[[c, y, x]] = maps[[c, y, x]] * truth[[y, x]];
}
}
}
let mut planner = FftPlanner::<f32>::new();
let fft_y = planner.plan_fft_forward(ny);
let ifft_y = planner.plan_fft_inverse(ny);
let mut aliased = Array3::<Complex32>::zeros((nc, ny, nx));
let mut buf = vec![Complex32::new(0.0, 0.0); ny];
for c in 0..nc {
for x in 0..nx {
for y in 0..ny {
buf[y] = coil_img[[c, y, x]];
}
fft_y.process(&mut buf);
for (i, v) in buf.iter_mut().enumerate() {
if i % r != 0 {
*v = Complex32::new(0.0, 0.0);
}
}
ifft_y.process(&mut buf);
let scale = 1.0 / ny as f32;
for y in 0..ny {
aliased[[c, y, x]] = buf[y] * scale;
}
}
}
let err: f32 = (0..nc)
.flat_map(|c| (0..nx).map(move |x| (c, x)))
.map(|(c, x)| (aliased[[c, 0, x]] - aliased[[c, ny / r, x]]).norm_sqr())
.sum::<f32>()
.sqrt();
assert!(
err < 1e-3,
"aliased image should be ny/r-periodic, err={}",
err
);
let out = sense_unfold_1d(&aliased, &maps, r, 1e-5).expect("sense_unfold_1d failed");
let mut num = 0.0f32;
let mut den = 0.0f32;
for y in 0..ny {
for x in 0..nx {
let t = truth[[y, x]];
let m = out[[y, x]].norm();
num += (t - m) * (t - m);
den += t * t;
}
}
let nrmse = (num / den.max(1e-20)).sqrt();
assert!(nrmse < 0.1, "SENSE NRMSE {} too large", nrmse);
}
#[test]
fn sense_r1_passthrough() {
let nc = 2;
let ny = 8;
let nx = 6;
let mut maps = Array3::<Complex32>::zeros((nc, ny, nx));
let mut aliased = Array3::<Complex32>::zeros((nc, ny, nx));
for c in 0..nc {
for y in 0..ny {
for x in 0..nx {
maps[[c, y, x]] = Complex32::new(1.0, 0.0);
let v = ((y as f32) * 0.1 + (x as f32) * 0.05 + c as f32).sin();
aliased[[c, y, x]] = Complex32::new(v, 0.0);
}
}
}
let out = sense_unfold_1d(&aliased, &maps, 1, 0.0).expect("r1 passthrough failed");
let _ = Array1::<f32>::zeros(1);
for y in 0..ny {
for x in 0..nx {
let mut mean = Complex32::new(0.0, 0.0);
for c in 0..nc {
mean += aliased[[c, y, x]];
}
mean *= Complex32::new(1.0 / nc as f32, 0.0);
let err = (out[[y, x]] - mean).norm();
assert!(err < 1e-4, "R=1 passthrough err={}", err);
}
}
let _ = PI;
}
#[test]
fn gfactor_is_unity_when_r1_unit_maps() {
let nc = 3;
let ny = 6;
let nx = 5;
let mut maps = Array3::<Complex32>::zeros((nc, ny, nx));
for c in 0..nc {
for y in 0..ny {
for x in 0..nx {
maps[[c, y, x]] = Complex32::new(1.0, 0.0);
}
}
}
let g = sense_gfactor_1d(&maps, 1, 0.0).expect("gfactor r1 failed");
for y in 0..ny {
for x in 0..nx {
assert!(
(g[[y, x]] - 1.0).abs() < 1e-4,
"g[{},{}]={}",
y,
x,
g[[y, x]]
);
}
}
}
#[test]
fn gfactor_is_unity_for_orthogonal_maps_r2() {
let ny = 4; let nx = 2;
let nc = 2;
let mut maps = Array3::<Complex32>::zeros((nc, ny, nx));
for x in 0..nx {
for y in 0..2 {
maps[[0, y, x]] = Complex32::new(1.0, 0.0);
}
for y in 2..4 {
maps[[1, y, x]] = Complex32::new(1.0, 0.0);
}
}
let g = sense_gfactor_1d(&maps, 2, 0.0).expect("gfactor r2 failed");
for y in 0..ny {
for x in 0..nx {
assert!(
(g[[y, x]] - 1.0).abs() < 1e-4,
"g[{},{}]={}",
y,
x,
g[[y, x]]
);
}
}
}
#[test]
fn sense_nc1_r1_passthrough() {
let nc = 1;
let ny = 8;
let nx = 6;
let mut maps = Array3::<Complex32>::zeros((nc, ny, nx));
let mut aliased = Array3::<Complex32>::zeros((nc, ny, nx));
for y in 0..ny {
for x in 0..nx {
let s = (1.0 + y as f32 * 0.1).sqrt();
maps[[0, y, x]] = Complex32::new(s, 0.0);
aliased[[0, y, x]] = Complex32::new(s * (y as f32 + x as f32), 0.0);
}
}
let out = sense_unfold_1d(&aliased, &maps, 1, 0.0).expect("nc=1 r=1 failed");
for y in 0..ny {
for x in 0..nx {
let s = maps[[0, y, x]];
let want = aliased[[0, y, x]] / s;
let err = (out[[y, x]] - want).norm();
assert!(err < 1e-4, "nc=1 r=1 err={} at ({},{})", err, y, x);
}
}
}
}