use crate::detection::{Estimates, FLOOR, MimoDetector, check_shape};
use comnum_core::{Error, Result, ensure, ensure_shape};
use comnum_mapping::constellation::Constellation;
use comnum_math::Jacobian;
use comnum_math::linalg::svd;
use comnum_math::{C64, golden_max};
use ndarray::ArrayView2;
const NODES: usize = 96;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Domain {
Complex,
Real,
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq)]
pub enum Prior {
Discrete {
points: Vec<C64>,
probs: Vec<f64>,
},
BernoulliGaussian {
rho: f64,
},
}
impl Prior {
pub fn uniform(c: &Constellation) -> Prior {
let m = c.order();
Prior::Discrete { points: c.points().to_vec(), probs: vec![1.0 / m as f64; m] }
}
pub fn with_probs(c: &Constellation, px: Option<&[f64]>) -> Result<Prior> {
Ok(Prior::Discrete { points: c.points().to_vec(), probs: c.priors(px)? })
}
pub fn bernoulli_gaussian(rho: f64) -> Result<Prior> {
ensure(rho > 0.0 && rho <= 1.0, || format!("the sparsity must be in (0, 1], got {rho}"))?;
Ok(Prior::BernoulliGaussian { rho })
}
pub fn mean(&self) -> C64 {
match self {
Prior::Discrete { points, probs } => points.iter().zip(probs).map(|(a, p)| a * *p).sum(),
Prior::BernoulliGaussian { .. } => C64::new(0.0, 0.0),
}
}
pub fn variance(&self) -> f64 {
match self {
Prior::Discrete { points, probs } => {
let m = self.mean();
points.iter().zip(probs).map(|(a, p)| p * (a - m).norm_sqr()).sum()
}
Prior::BernoulliGaussian { .. } => 1.0,
}
}
pub fn denoise(&self, r: C64, tau: f64, domain: Domain) -> (C64, f64) {
let r = match domain {
Domain::Complex => r,
Domain::Real => C64::new(r.re, 0.0),
};
let scale = match domain {
Domain::Complex => 1.0 / tau,
Domain::Real => 0.5 / tau,
};
match self {
Prior::Discrete { points, probs } => {
if tau <= 0.0 || !tau.is_finite() {
let best = points
.iter()
.zip(probs)
.filter(|(_, p)| **p > 0.0)
.min_by(|a, b| (r - a.0).norm_sqr().total_cmp(&(r - b.0).norm_sqr()))
.map_or(C64::new(0.0, 0.0), |(a, _)| *a);
return (best, 0.0);
}
let mut best = f64::NEG_INFINITY;
for (a, p) in points.iter().zip(probs) {
if *p > 0.0 {
best = best.max(p.ln() - (r - a).norm_sqr() * scale);
}
}
let (mut sum, mut mean, mut second) = (0.0, C64::new(0.0, 0.0), 0.0);
for (a, p) in points.iter().zip(probs) {
if *p > 0.0 {
let w = (p.ln() - (r - a).norm_sqr() * scale - best).exp();
sum += w;
mean += a * w;
second += w * a.norm_sqr();
}
}
let mean = mean / sum;
(mean, (second / sum - mean.norm_sqr()).max(0.0))
}
Prior::BernoulliGaussian { rho } => {
let rho = *rho;
let s = 1.0 / rho;
let log_lik = |v: f64| match domain {
Domain::Complex => -r.norm_sqr() / v - v.ln(),
Domain::Real => -0.5 * r.norm_sqr() / v - 0.5 * v.ln(),
};
let tau = tau.max(f64::MIN_POSITIVE);
let l1 = rho.ln() + log_lik(s + tau);
let l0 = (1.0 - rho).ln() + log_lik(tau);
let pi = if rho >= 1.0 { 1.0 } else { 1.0 / (1.0 + (l0 - l1).exp()) };
let m = r * (s / (s + tau));
let v = s * tau / (s + tau);
let mean = m * pi;
(mean, (pi * (v + m.norm_sqr()) - mean.norm_sqr()).max(0.0))
}
}
}
fn separable(&self) -> Option<Prior> {
let Prior::Discrete { points, probs } = self else { return None };
let mut levels: Vec<f64> = Vec::new();
for a in points {
if !levels.iter().any(|l| (l - a.re).abs() <= 1e-9 * (1.0 + a.re.abs())) {
levels.push(a.re);
}
}
levels.sort_by(f64::total_cmp);
let k = levels.len();
if k < 2 || k * k != points.len() {
return None;
}
let index = |x: f64| levels.iter().position(|l| (l - x).abs() <= 1e-9 * (1.0 + x.abs()));
let mut joint = vec![f64::NAN; k * k];
for (a, p) in points.iter().zip(probs) {
let (i, j) = (index(a.re)?, index(a.im)?);
joint[i * k + j] = *p;
}
if joint.iter().any(|p| p.is_nan()) {
return None;
}
let marginal: Vec<f64> = (0..k).map(|i| (0..k).map(|j| joint[i * k + j]).sum()).collect();
let independent = (0..k).all(|i| (0..k).all(|j| (joint[i * k + j] - marginal[i] * marginal[j]).abs() <= 1e-12));
let symmetric = (0..k).all(|j| ((0..k).map(|i| joint[i * k + j]).sum::<f64>() - marginal[j]).abs() <= 1e-12);
(independent && symmetric)
.then(|| Prior::Discrete { points: levels.iter().map(|l| C64::new(*l, 0.0)).collect(), probs: marginal })
}
pub fn mse(&self, noise_var: f64, gamma2: f64, domain: Domain) -> (f64, f64) {
if domain == Domain::Complex {
if let Some(pam) = self.separable() {
let (psi, phi) = pam.mse(0.5 * noise_var, 0.5 * gamma2, Domain::Real);
return (2.0 * psi, 2.0 * phi);
}
}
let sigma = noise_var.max(0.0).sqrt();
let nodes = Quadrature::new(domain);
match self {
Prior::Discrete { points, probs } => {
let (mut psi, mut phi) = (0.0, 0.0);
for (a, p) in points.iter().zip(probs).filter(|(_, p)| **p > 0.0) {
let a = match domain {
Domain::Complex => *a,
Domain::Real => C64::new(a.re, 0.0),
};
nodes.for_each(|z, w| {
let (f, g) = self.denoise(a + z * sigma, gamma2, domain);
psi += p * w * (f - a).norm_sqr();
phi += p * w * g;
});
}
(psi, phi)
}
Prior::BernoulliGaussian { rho } => {
let (mut psi, mut phi) = (0.0, 0.0);
for (weight, var) in [(1.0 - rho, noise_var), (*rho, 1.0 / rho + noise_var)] {
if weight <= 0.0 {
continue;
}
let sd = var.sqrt();
nodes.for_each(|z, w| {
let r = z * sd;
let (f, g) = self.denoise(r, gamma2, domain);
let (f0, g0) = self.denoise(r, noise_var, domain);
psi += weight * w * ((f - f0).norm_sqr() + g0);
phi += weight * w * g;
});
}
(psi, phi)
}
}
}
}
struct Quadrature {
nodes: Vec<(C64, f64)>,
}
impl Quadrature {
fn new(domain: Domain) -> Quadrature {
Quadrature { nodes: comnum_math::gaussian_nodes(1.0, domain == Domain::Complex, NODES) }
}
fn for_each(&self, mut f: impl FnMut(C64, f64)) {
for &(z, w) in &self.nodes {
f(z, w);
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct StateEvolutionStep {
pub noise_var: f64,
pub mse: f64,
}
pub fn amp_state_evolution(config: &MessagePassing, beta: f64, noise_var: f64) -> Vec<StateEvolutionStep> {
let MessagePassing { prior, domain, iterations } = config;
let (domain, iterations) = (*domain, *iterations);
let mut s2 = noise_var + beta * prior.variance();
let mut out = Vec::with_capacity(iterations);
for _ in 0..iterations {
let mse = prior.mse(s2, s2, domain).0;
out.push(StateEvolutionStep { noise_var: s2, mse });
s2 = noise_var + beta * mse;
}
out
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AmpThresholds {
pub beta_min: f64,
pub beta_max: f64,
pub n0_min: f64,
pub n0_max: f64,
}
pub fn amp_thresholds(prior: &Prior, domain: Domain) -> AmpThresholds {
let var = prior.variance();
let psi = |s2: f64| prior.mse(s2, s2, domain).0;
let dpsi = |s2: f64| {
let h = 1e-4;
(psi(s2 * (1.0 + h)) - psi(s2 * (1.0 - h))) / (2.0 * h * s2)
};
let grid: Vec<f64> = (0..=240).map(|i| var * 10f64.powf(-6.0 + 8.0 * f64::from(i) / 240.0)).collect();
let refine = |f: &dyn Fn(f64) -> f64| -> f64 {
let (i, _) = grid
.iter()
.enumerate()
.map(|(i, s)| (i, f(*s)))
.fold((0, f64::NEG_INFINITY), |b, (i, v)| if v > b.1 { (i, v) } else { b });
let lo = grid[i.saturating_sub(1)].ln();
let hi = grid[(i + 1).min(grid.len() - 1)].ln();
golden_max(|x| f(x.exp()), lo, hi, 80).exp()
};
let s_max = refine(&|s2| psi(s2) / s2);
let beta_max = s_max / psi(s_max);
let s_min = refine(&dpsi);
let beta_min = 1.0 / dpsi(s_min);
let n0_min = s_min - beta_min * psi(s_min);
let h = |s2: f64| beta_max * dpsi(s2) - 1.0;
let mut n0_max = 0.0f64;
for w in grid.windows(2) {
let (a, b) = (h(w[0]), h(w[1]));
if a.signum() != b.signum() {
let x = comnum_math::bisect(|x| h(x.exp()).signum() == a.signum(), w[0].ln(), w[1].ln(), 80).exp();
n0_max = n0_max.max(x - beta_max * psi(x));
}
}
AmpThresholds { beta_min, beta_max, n0_min, n0_max }
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AmpVariance {
Postulated,
Residual,
}
impl std::str::FromStr for AmpVariance {
type Err = Error;
fn from_str(name: &str) -> Result<AmpVariance> {
match name.to_ascii_lowercase().as_str() {
"postulated" => Ok(AmpVariance::Postulated),
"residual" => Ok(AmpVariance::Residual),
other => Err(Error::Config(format!("unknown variance {other:?}: postulated or residual"))),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Trace {
pub z: Vec<C64>,
pub noise_var: f64,
pub estimates: Vec<Vec<C64>>,
pub noise_vars: Vec<f64>,
}
impl Trace {
pub fn estimate(&self) -> &[C64] {
self.estimates.last().map_or(&[], Vec::as_slice)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct MessagePassing {
pub prior: Prior,
pub domain: Domain,
pub iterations: usize,
}
impl Default for MessagePassing {
fn default() -> MessagePassing {
MessagePassing { prior: Prior::BernoulliGaussian { rho: 1.0 }, domain: Domain::Complex, iterations: 10 }
}
}
impl MessagePassing {
pub fn check(&self) -> Result<()> {
ensure(self.iterations >= 1, || "at least one iteration is needed".into())?;
match &self.prior {
Prior::Discrete { points, probs } => {
ensure_shape(!points.is_empty(), || "a discrete prior needs at least one point".into())?;
comnum_core::same_len(points.len(), probs.len(), "points and probabilities of the prior")
}
Prior::BernoulliGaussian { rho } => Prior::bernoulli_gaussian(*rho).map(drop),
}
}
}
fn check(h: &ArrayView2<'_, C64>, y: &[C64], noise_var: f64, iterations: usize) -> Result<()> {
ensure_shape(h.nrows() == y.len(), || {
format!("the channel has {} rows for {} received samples", h.nrows(), y.len())
})?;
ensure_shape(h.ncols() >= 1 && h.nrows() >= 1, || "the channel must not be empty".into())?;
ensure(noise_var >= 0.0 && noise_var.is_finite(), || {
format!("the noise variance must be finite and non-negative, got {noise_var}")
})?;
ensure(iterations >= 1, || "at least one iteration is needed".into())
}
fn residual(h: &ArrayView2<'_, C64>, y: &[C64], s: &[C64]) -> Vec<C64> {
y.iter().enumerate().map(|(i, yi)| yi - h.row(i).iter().zip(s).map(|(a, b)| a * b).sum::<C64>()).collect()
}
fn adjoint(h: &ArrayView2<'_, C64>, r: &[C64]) -> Vec<C64> {
(0..h.ncols()).map(|j| h.column(j).iter().zip(r).map(|(a, b)| a.conj() * b).sum()).collect()
}
pub fn lama(
h: &ArrayView2<'_, C64>,
y: &[C64],
noise_var: f64,
config: &MessagePassing,
variance: AmpVariance,
) -> Result<Trace> {
let MessagePassing { prior, domain, iterations } = config;
let (domain, iterations) = (*domain, *iterations);
check(h, y, noise_var, iterations)?;
let (m, n) = h.dim();
let c = h.iter().map(C64::norm_sqr).sum::<f64>() / n as f64;
ensure(c > 0.0, || "the channel is zero".into())?;
let k = 1.0 / c.sqrt();
let h = h.mapv(|v| v * k);
let y: Vec<C64> = y.iter().map(|v| v * k).collect();
let n0 = noise_var / c;
let beta = n as f64 / m as f64;
let mut s = vec![prior.mean(); n];
let mut gamma = n0 + beta * prior.variance();
let mut r = residual(&h.view(), &y, &s);
let mut trace =
Trace { z: Vec::new(), noise_var: gamma, estimates: Vec::with_capacity(iterations), noise_vars: Vec::new() };
for _ in 0..iterations {
if variance == AmpVariance::Residual {
gamma = (r.iter().map(C64::norm_sqr).sum::<f64>() / m as f64).max(FLOOR);
}
let back = adjoint(&h.view(), &r);
let z: Vec<C64> = s.iter().zip(&back).map(|(a, b)| a + b).collect();
let (next, g): (Vec<C64>, Vec<f64>) = z.iter().map(|zi| prior.denoise(*zi, gamma, domain)).unzip();
let gbar = g.iter().sum::<f64>() / n as f64;
let onsager = beta * gbar / gamma;
let fit = residual(&h.view(), &y, &next);
r = fit.iter().zip(&r).map(|(f, old)| f + old * onsager).collect();
trace.z = z;
trace.noise_var = gamma;
trace.noise_vars.push(gamma);
trace.estimates.push(next.clone());
s = next;
gamma = n0 + beta * gbar;
}
trace.noise_var *= c;
for v in &mut trace.noise_vars {
*v *= c;
}
Ok(trace)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OampLinear {
MatchedFilter,
PseudoInverse,
Lmmse,
}
impl std::str::FromStr for OampLinear {
type Err = Error;
fn from_str(name: &str) -> Result<OampLinear> {
match name.to_ascii_lowercase().as_str() {
"lmmse" => Ok(OampLinear::Lmmse),
"pinv" => Ok(OampLinear::PseudoInverse),
"mf" => Ok(OampLinear::MatchedFilter),
other => Err(Error::Config(format!("unknown linear estimator {other:?}: lmmse, pinv or mf"))),
}
}
}
impl OampLinear {
fn gains(self, lambda: &[f64], v2: f64, noise_var: f64) -> Vec<f64> {
let top = lambda.iter().copied().fold(0.0, f64::max);
lambda
.iter()
.map(|&l| match self {
OampLinear::MatchedFilter => l,
OampLinear::PseudoInverse => {
if l > 1e-12 * top {
1.0 / l
} else {
0.0
}
}
OampLinear::Lmmse => v2 * l / (v2 * l * l + noise_var),
})
.collect()
}
}
fn decorrelated(lambda: &[f64], ghat: &[f64], n: usize, v2: f64, noise_var: f64) -> (Vec<f64>, f64) {
let trace: f64 = ghat.iter().zip(lambda).map(|(g, l)| g * l).sum();
let c = if trace > 0.0 { n as f64 / trace } else { 0.0 };
let g: Vec<f64> = ghat.iter().map(|x| x * c).collect();
let bb = g.iter().zip(lambda).map(|(g, l)| (1.0 - g * l).powi(2)).sum::<f64>() + (n - lambda.len()) as f64;
let ww = g.iter().map(|g| g * g).sum::<f64>();
(g, (bb * v2 + ww * noise_var) / n as f64)
}
pub fn oamp(
h: &ArrayView2<'_, C64>,
y: &[C64],
noise_var: f64,
config: &MessagePassing,
linear: OampLinear,
) -> Result<Trace> {
let MessagePassing { prior, domain, iterations } = config;
let (domain, iterations) = (*domain, *iterations);
check(h, y, noise_var, iterations)?;
let (m, n) = h.dim();
let (u, lambda, v) = svd(h);
let frob: f64 = lambda.iter().map(|l| l * l).sum();
ensure(frob > 0.0, || "the channel is zero".into())?;
let floor = 1e-12 * prior.variance().max(FLOOR);
let mut s = vec![prior.mean(); n];
let mut trace =
Trace { z: Vec::new(), noise_var: 0.0, estimates: Vec::with_capacity(iterations), noise_vars: Vec::new() };
for t in 0..iterations {
let res = residual(h, y, &s);
let energy: f64 = res.iter().map(C64::norm_sqr).sum();
let v2 = ((energy - m as f64 * noise_var) / frob).max(floor);
let (g, tau2) = decorrelated(&lambda, &linear.gains(&lambda, v2, noise_var), n, v2, noise_var);
let tau2 = tau2.max(FLOOR);
let proj: Vec<C64> = (0..lambda.len())
.map(|i| u.column(i).iter().zip(&res).map(|(a, b)| a.conj() * b).sum::<C64>() * g[i])
.collect();
let r: Vec<C64> = (0..n).map(|j| s[j] + (0..proj.len()).map(|i| v[[j, i]] * proj[i]).sum::<C64>()).collect();
let (f, gv): (Vec<C64>, Vec<f64>) = r.iter().map(|ri| prior.denoise(*ri, tau2, domain)).unzip();
let mmse = gv.iter().sum::<f64>() / n as f64;
trace.noise_vars.push(tau2);
trace.estimates.push(f.clone());
trace.z.clone_from(&r);
trace.noise_var = tau2;
if t + 1 < iterations {
let c = tau2 / (tau2 - mmse).max(1e-12 * tau2);
s = f.iter().zip(&r).map(|(fi, ri)| (fi - ri * (mmse / tau2)) * c).collect();
}
}
Ok(trace)
}
pub fn oamp_state_evolution(
config: &MessagePassing,
singular_values: &[f64],
n: usize,
noise_var: f64,
linear: OampLinear,
) -> Vec<StateEvolutionStep> {
let MessagePassing { prior, domain, iterations } = config;
let (domain, iterations) = (*domain, *iterations);
let lambda: Vec<f64> = singular_values.iter().copied().take(n).collect();
let mut v2 = prior.variance();
let mut out = Vec::with_capacity(iterations);
for _ in 0..iterations {
let (_, tau2) = decorrelated(&lambda, &linear.gains(&lambda, v2, noise_var), n, v2, noise_var);
let mmse = prior.mse(tau2, tau2, domain).0;
out.push(StateEvolutionStep { noise_var: tau2, mse: mmse });
v2 = if mmse < tau2 { 1.0 / (1.0 / mmse.max(FLOOR) - 1.0 / tau2) } else { tau2 };
}
out
}
#[derive(Clone, Debug, PartialEq)]
pub struct Amp {
constellation: Constellation,
iterations: usize,
pub variance: AmpVariance,
}
impl Amp {
pub fn new(constellation: Constellation, iterations: usize) -> Result<Amp> {
ensure(iterations >= 1, || "at least one iteration is needed".into())?;
Ok(Amp { constellation, iterations, variance: AmpVariance::Postulated })
}
pub fn iterations(&self) -> usize {
self.iterations
}
fn trace(&self, h: &ArrayView2<'_, C64>, y: &[C64], noise_var: f64) -> Result<Trace> {
check_shape(h, y)?;
let config = MessagePassing {
prior: Prior::uniform(&self.constellation),
domain: Domain::Complex,
iterations: self.iterations,
};
lama(h, y, noise_var, &config, self.variance)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Oamp {
constellation: Constellation,
iterations: usize,
pub linear: OampLinear,
}
impl Oamp {
pub fn new(constellation: Constellation, iterations: usize) -> Result<Oamp> {
ensure(iterations >= 1, || "at least one iteration is needed".into())?;
Ok(Oamp { constellation, iterations, linear: OampLinear::Lmmse })
}
pub fn iterations(&self) -> usize {
self.iterations
}
fn trace(&self, h: &ArrayView2<'_, C64>, y: &[C64], noise_var: f64) -> Result<Trace> {
check_shape(h, y)?;
let config = MessagePassing {
prior: Prior::uniform(&self.constellation),
domain: Domain::Complex,
iterations: self.iterations,
};
oamp(h, y, noise_var, &config, self.linear)
}
}
fn decoupled_var(trace: &Trace) -> f64 {
trace.noise_var.max(FLOOR)
}
macro_rules! decoupled_detector {
($t:ty) => {
impl MimoDetector for $t {
fn constellation(&self) -> &Constellation {
&self.constellation
}
fn has_soft_output(&self) -> bool {
true
}
fn soft_estimate(&self, h: &ArrayView2<'_, C64>, y: &[C64], noise_var: f64) -> Result<Option<Estimates>> {
let t = self.trace(h, y, noise_var)?;
let v = decoupled_var(&t);
Ok(Some(Estimates { noise_var: vec![v; t.z.len()], symbols: t.z }))
}
fn detect(&self, h: &ArrayView2<'_, C64>, y: &[C64], noise_var: f64) -> Result<Vec<usize>> {
Ok(self.constellation.decide(&self.trace(h, y, noise_var)?.z))
}
fn llr(&self, h: &ArrayView2<'_, C64>, y: &[C64], noise_var: f64) -> Result<Vec<f64>> {
let t = self.trace(h, y, noise_var)?;
self.constellation.llr(&t.z, decoupled_var(&t), None, Jacobian::Exact)
}
fn detect_llr(&self, h: &ArrayView2<'_, C64>, y: &[C64], noise_var: f64) -> Result<(Vec<usize>, Vec<f64>)> {
let t = self.trace(h, y, noise_var)?;
Ok((
self.constellation.decide(&t.z),
self.constellation.llr(&t.z, decoupled_var(&t), None, Jacobian::Exact)?,
))
}
}
};
}
decoupled_detector!(Amp);
decoupled_detector!(Oamp);
#[cfg(test)]
mod tests {
use super::*;
use comnum_core::rng::Rng;
use ndarray::Array2;
fn qpsk() -> Constellation {
Constellation::qam(4).unwrap().normalized()
}
#[test]
fn the_denoiser_is_the_posterior_of_the_alphabet() {
let p = Prior::uniform(&qpsk());
let a = qpsk().points()[2];
let (f, g) = p.denoise(a * 1.01, 1e-4, Domain::Complex);
assert!((f - a).norm() < 1e-9 && g < 1e-9);
let (f, g) = p.denoise(C64::new(0.0, 0.0), 1e9, Domain::Complex);
assert!(f.norm() < 1e-9 && (g - 1.0).abs() < 1e-6);
let bpsk = Prior::Discrete { points: vec![C64::new(-1.0, 0.0), C64::new(1.0, 0.0)], probs: vec![0.5, 0.5] };
let (f, g) = bpsk.denoise(C64::new(0.3, 7.0), 0.5, Domain::Real);
assert!((f.re - (0.3f64 / 0.5).tanh()).abs() < 1e-12 && (g - (1.0 - f.re * f.re)).abs() < 1e-12);
}
#[test]
fn the_mse_of_separable_qam_is_that_of_its_pam() {
let p = Prior::uniform(&Constellation::qam(16).unwrap().normalized());
let Prior::Discrete { points, probs } = &p else { unreachable!() };
let direct = |s2: f64| {
let q = Quadrature::new(Domain::Complex);
let mut psi = 0.0;
for (a, pa) in points.iter().zip(probs) {
q.for_each(|z, w| psi += pa * w * (p.denoise(a + z * s2.sqrt(), s2, Domain::Complex).0 - a).norm_sqr());
}
psi
};
for s2 in [0.01, 0.1, 1.0] {
let fast = p.mse(s2, s2, Domain::Complex).0;
assert!((fast / direct(s2) - 1.0).abs() < 1e-6, "{s2}: {fast} {}", direct(s2));
}
}
#[test]
fn lama_and_oamp_recover_a_noiseless_large_system() {
let c = qpsk();
let mut rng = Rng::new(4);
let (m, n) = (96, 64);
let h = Array2::from_shape_fn((m, n), |_| rng.complex_gaussian());
let idx: Vec<usize> = (0..n).map(|_| rng.below(4)).collect();
let x: Vec<C64> = idx.iter().map(|&i| c.points()[i]).collect();
let y = comnum_math::linalg::matvec(&h.view(), &x);
assert_eq!(Amp::new(c.clone(), 20).unwrap().detect(&h.view(), &y, 1e-6).unwrap(), idx);
assert_eq!(Oamp::new(c.clone(), 10).unwrap().detect(&h.view(), &y, 1e-6).unwrap(), idx);
assert!(Amp::new(c.clone(), 0).is_err() && Oamp::new(c.clone(), 0).is_err());
for d in [&Amp::new(c.clone(), 5).unwrap() as &dyn MimoDetector, &Oamp::new(c.clone(), 5).unwrap()] {
assert!(matches!(d.detect(&h.view(), &y[1..], 1e-6), Err(Error::Shape(_))));
let e = d.soft_estimate(&h.view(), &y, 1e-6).unwrap().unwrap();
let (labels, llr) = d.detect_llr(&h.view(), &y, 1e-6).unwrap();
assert_eq!(c.decide(&e.symbols), labels);
let from = c.llr_with_variances(&e.symbols, &e.noise_var, None, Jacobian::Exact).unwrap();
assert!(from.iter().zip(&llr).all(|(a, b)| a.to_bits() == b.to_bits()));
}
}
#[test]
fn the_state_evolution_of_lama_decreases_to_its_fixed_point() {
let config = MessagePassing { prior: Prior::uniform(&qpsk()), domain: Domain::Complex, iterations: 30 };
let se = amp_state_evolution(&config, 0.5, 0.05);
assert!(se.windows(2).all(|w| w[1].noise_var <= w[0].noise_var + 1e-15));
let last = se[se.len() - 1];
assert!((last.noise_var - (0.05 + 0.5 * last.mse)).abs() < 1e-9);
}
}