1use crate::detection::{Estimates, FLOOR, MimoDetector, check_shape};
90use comnum_core::{Error, Result, ensure, ensure_shape};
91use comnum_mapping::constellation::Constellation;
92use comnum_math::Jacobian;
93use comnum_math::linalg::svd;
94use comnum_math::{C64, golden_max};
95use ndarray::ArrayView2;
96
97const NODES: usize = 96;
99
100#[derive(Clone, Copy, Debug, PartialEq, Eq)]
102pub enum Domain {
103 Complex,
105 Real,
108}
109
110#[non_exhaustive]
112#[derive(Clone, Debug, PartialEq)]
113pub enum Prior {
114 Discrete {
116 points: Vec<C64>,
118 probs: Vec<f64>,
120 },
121 BernoulliGaussian {
124 rho: f64,
126 },
127}
128
129impl Prior {
130 pub fn uniform(c: &Constellation) -> Prior {
132 let m = c.order();
133 Prior::Discrete { points: c.points().to_vec(), probs: vec![1.0 / m as f64; m] }
134 }
135
136 pub fn with_probs(c: &Constellation, px: Option<&[f64]>) -> Result<Prior> {
142 Ok(Prior::Discrete { points: c.points().to_vec(), probs: c.priors(px)? })
143 }
144
145 pub fn bernoulli_gaussian(rho: f64) -> Result<Prior> {
150 ensure(rho > 0.0 && rho <= 1.0, || format!("the sparsity must be in (0, 1], got {rho}"))?;
151 Ok(Prior::BernoulliGaussian { rho })
152 }
153
154 pub fn mean(&self) -> C64 {
156 match self {
157 Prior::Discrete { points, probs } => points.iter().zip(probs).map(|(a, p)| a * *p).sum(),
158 Prior::BernoulliGaussian { .. } => C64::new(0.0, 0.0),
159 }
160 }
161
162 pub fn variance(&self) -> f64 {
164 match self {
165 Prior::Discrete { points, probs } => {
166 let m = self.mean();
167 points.iter().zip(probs).map(|(a, p)| p * (a - m).norm_sqr()).sum()
168 }
169 Prior::BernoulliGaussian { .. } => 1.0,
170 }
171 }
172
173 pub fn denoise(&self, r: C64, tau: f64, domain: Domain) -> (C64, f64) {
176 let r = match domain {
177 Domain::Complex => r,
178 Domain::Real => C64::new(r.re, 0.0),
179 };
180 let scale = match domain {
182 Domain::Complex => 1.0 / tau,
183 Domain::Real => 0.5 / tau,
184 };
185 match self {
186 Prior::Discrete { points, probs } => {
187 if tau <= 0.0 || !tau.is_finite() {
188 let best = points
189 .iter()
190 .zip(probs)
191 .filter(|(_, p)| **p > 0.0)
192 .min_by(|a, b| (r - a.0).norm_sqr().total_cmp(&(r - b.0).norm_sqr()))
193 .map_or(C64::new(0.0, 0.0), |(a, _)| *a);
194 return (best, 0.0);
195 }
196 let mut best = f64::NEG_INFINITY;
197 for (a, p) in points.iter().zip(probs) {
198 if *p > 0.0 {
199 best = best.max(p.ln() - (r - a).norm_sqr() * scale);
200 }
201 }
202 let (mut sum, mut mean, mut second) = (0.0, C64::new(0.0, 0.0), 0.0);
203 for (a, p) in points.iter().zip(probs) {
204 if *p > 0.0 {
205 let w = (p.ln() - (r - a).norm_sqr() * scale - best).exp();
206 sum += w;
207 mean += a * w;
208 second += w * a.norm_sqr();
209 }
210 }
211 let mean = mean / sum;
212 (mean, (second / sum - mean.norm_sqr()).max(0.0))
213 }
214 Prior::BernoulliGaussian { rho } => {
215 let rho = *rho;
216 let s = 1.0 / rho;
217 let log_lik = |v: f64| match domain {
219 Domain::Complex => -r.norm_sqr() / v - v.ln(),
220 Domain::Real => -0.5 * r.norm_sqr() / v - 0.5 * v.ln(),
221 };
222 let tau = tau.max(f64::MIN_POSITIVE);
223 let l1 = rho.ln() + log_lik(s + tau);
224 let l0 = (1.0 - rho).ln() + log_lik(tau);
225 let pi = if rho >= 1.0 { 1.0 } else { 1.0 / (1.0 + (l0 - l1).exp()) };
227 let m = r * (s / (s + tau));
228 let v = s * tau / (s + tau);
229 let mean = m * pi;
230 (mean, (pi * (v + m.norm_sqr()) - mean.norm_sqr()).max(0.0))
231 }
232 }
233 }
234
235 fn separable(&self) -> Option<Prior> {
239 let Prior::Discrete { points, probs } = self else { return None };
240 let mut levels: Vec<f64> = Vec::new();
241 for a in points {
242 if !levels.iter().any(|l| (l - a.re).abs() <= 1e-9 * (1.0 + a.re.abs())) {
243 levels.push(a.re);
244 }
245 }
246 levels.sort_by(f64::total_cmp);
247 let k = levels.len();
248 if k < 2 || k * k != points.len() {
249 return None;
250 }
251 let index = |x: f64| levels.iter().position(|l| (l - x).abs() <= 1e-9 * (1.0 + x.abs()));
252 let mut joint = vec![f64::NAN; k * k];
253 for (a, p) in points.iter().zip(probs) {
254 let (i, j) = (index(a.re)?, index(a.im)?);
255 joint[i * k + j] = *p;
256 }
257 if joint.iter().any(|p| p.is_nan()) {
258 return None;
259 }
260 let marginal: Vec<f64> = (0..k).map(|i| (0..k).map(|j| joint[i * k + j]).sum()).collect();
261 let independent = (0..k).all(|i| (0..k).all(|j| (joint[i * k + j] - marginal[i] * marginal[j]).abs() <= 1e-12));
262 let symmetric = (0..k).all(|j| ((0..k).map(|i| joint[i * k + j]).sum::<f64>() - marginal[j]).abs() <= 1e-12);
263 (independent && symmetric)
264 .then(|| Prior::Discrete { points: levels.iter().map(|l| C64::new(*l, 0.0)).collect(), probs: marginal })
265 }
266
267 pub fn mse(&self, noise_var: f64, gamma2: f64, domain: Domain) -> (f64, f64) {
271 if domain == Domain::Complex {
272 if let Some(pam) = self.separable() {
273 let (psi, phi) = pam.mse(0.5 * noise_var, 0.5 * gamma2, Domain::Real);
274 return (2.0 * psi, 2.0 * phi);
275 }
276 }
277 let sigma = noise_var.max(0.0).sqrt();
278 let nodes = Quadrature::new(domain);
279 match self {
280 Prior::Discrete { points, probs } => {
281 let (mut psi, mut phi) = (0.0, 0.0);
282 for (a, p) in points.iter().zip(probs).filter(|(_, p)| **p > 0.0) {
283 let a = match domain {
284 Domain::Complex => *a,
285 Domain::Real => C64::new(a.re, 0.0),
286 };
287 nodes.for_each(|z, w| {
288 let (f, g) = self.denoise(a + z * sigma, gamma2, domain);
289 psi += p * w * (f - a).norm_sqr();
290 phi += p * w * g;
291 });
292 }
293 (psi, phi)
294 }
295 Prior::BernoulliGaussian { rho } => {
296 let (mut psi, mut phi) = (0.0, 0.0);
299 for (weight, var) in [(1.0 - rho, noise_var), (*rho, 1.0 / rho + noise_var)] {
300 if weight <= 0.0 {
301 continue;
302 }
303 let sd = var.sqrt();
304 nodes.for_each(|z, w| {
305 let r = z * sd;
306 let (f, g) = self.denoise(r, gamma2, domain);
307 let (f0, g0) = self.denoise(r, noise_var, domain);
308 psi += weight * w * ((f - f0).norm_sqr() + g0);
309 phi += weight * w * g;
310 });
311 }
312 (psi, phi)
313 }
314 }
315 }
316}
317
318struct Quadrature {
321 nodes: Vec<(C64, f64)>,
322}
323
324impl Quadrature {
325 fn new(domain: Domain) -> Quadrature {
326 Quadrature { nodes: comnum_math::gaussian_nodes(1.0, domain == Domain::Complex, NODES) }
327 }
328
329 fn for_each(&self, mut f: impl FnMut(C64, f64)) {
330 for &(z, w) in &self.nodes {
331 f(z, w);
332 }
333 }
334}
335
336#[derive(Clone, Copy, Debug, PartialEq)]
338pub struct StateEvolutionStep {
339 pub noise_var: f64,
342 pub mse: f64,
344}
345
346pub fn amp_state_evolution(config: &MessagePassing, beta: f64, noise_var: f64) -> Vec<StateEvolutionStep> {
352 let MessagePassing { prior, domain, iterations } = config;
353 let (domain, iterations) = (*domain, *iterations);
354 let mut s2 = noise_var + beta * prior.variance();
355 let mut out = Vec::with_capacity(iterations);
356 for _ in 0..iterations {
357 let mse = prior.mse(s2, s2, domain).0;
358 out.push(StateEvolutionStep { noise_var: s2, mse });
359 s2 = noise_var + beta * mse;
360 }
361 out
362}
363
364#[derive(Clone, Copy, Debug, PartialEq)]
366pub struct AmpThresholds {
367 pub beta_min: f64,
370 pub beta_max: f64,
372 pub n0_min: f64,
374 pub n0_max: f64,
376}
377
378pub fn amp_thresholds(prior: &Prior, domain: Domain) -> AmpThresholds {
384 let var = prior.variance();
385 let psi = |s2: f64| prior.mse(s2, s2, domain).0;
386 let dpsi = |s2: f64| {
388 let h = 1e-4;
389 (psi(s2 * (1.0 + h)) - psi(s2 * (1.0 - h))) / (2.0 * h * s2)
390 };
391 let grid: Vec<f64> = (0..=240).map(|i| var * 10f64.powf(-6.0 + 8.0 * f64::from(i) / 240.0)).collect();
393 let refine = |f: &dyn Fn(f64) -> f64| -> f64 {
394 let (i, _) = grid
395 .iter()
396 .enumerate()
397 .map(|(i, s)| (i, f(*s)))
398 .fold((0, f64::NEG_INFINITY), |b, (i, v)| if v > b.1 { (i, v) } else { b });
399 let lo = grid[i.saturating_sub(1)].ln();
400 let hi = grid[(i + 1).min(grid.len() - 1)].ln();
401 golden_max(|x| f(x.exp()), lo, hi, 80).exp()
402 };
403 let s_max = refine(&|s2| psi(s2) / s2);
404 let beta_max = s_max / psi(s_max);
405 let s_min = refine(&dpsi);
406 let beta_min = 1.0 / dpsi(s_min);
407 let n0_min = s_min - beta_min * psi(s_min);
408 let h = |s2: f64| beta_max * dpsi(s2) - 1.0;
410 let mut n0_max = 0.0f64;
411 for w in grid.windows(2) {
412 let (a, b) = (h(w[0]), h(w[1]));
413 if a.signum() != b.signum() {
414 let x = comnum_math::bisect(|x| h(x.exp()).signum() == a.signum(), w[0].ln(), w[1].ln(), 80).exp();
415 n0_max = n0_max.max(x - beta_max * psi(x));
416 }
417 }
418 AmpThresholds { beta_min, beta_max, n0_min, n0_max }
419}
420
421#[derive(Clone, Copy, Debug, PartialEq, Eq)]
423pub enum AmpVariance {
424 Postulated,
426 Residual,
429}
430
431impl std::str::FromStr for AmpVariance {
432 type Err = Error;
433
434 fn from_str(name: &str) -> Result<AmpVariance> {
446 match name.to_ascii_lowercase().as_str() {
447 "postulated" => Ok(AmpVariance::Postulated),
448 "residual" => Ok(AmpVariance::Residual),
449 other => Err(Error::Config(format!("unknown variance {other:?}: postulated or residual"))),
450 }
451 }
452}
453
454#[derive(Clone, Debug, PartialEq)]
456pub struct Trace {
457 pub z: Vec<C64>,
460 pub noise_var: f64,
462 pub estimates: Vec<Vec<C64>>,
464 pub noise_vars: Vec<f64>,
466}
467
468impl Trace {
469 pub fn estimate(&self) -> &[C64] {
471 self.estimates.last().map_or(&[], Vec::as_slice)
472 }
473}
474
475#[derive(Clone, Debug, PartialEq)]
490pub struct MessagePassing {
491 pub prior: Prior,
493 pub domain: Domain,
495 pub iterations: usize,
497}
498
499impl Default for MessagePassing {
500 fn default() -> MessagePassing {
503 MessagePassing { prior: Prior::BernoulliGaussian { rho: 1.0 }, domain: Domain::Complex, iterations: 10 }
504 }
505}
506
507impl MessagePassing {
508 pub fn check(&self) -> Result<()> {
515 ensure(self.iterations >= 1, || "at least one iteration is needed".into())?;
516 match &self.prior {
517 Prior::Discrete { points, probs } => {
518 ensure_shape(!points.is_empty(), || "a discrete prior needs at least one point".into())?;
519 comnum_core::same_len(points.len(), probs.len(), "points and probabilities of the prior")
520 }
521 Prior::BernoulliGaussian { rho } => Prior::bernoulli_gaussian(*rho).map(drop),
522 }
523 }
524}
525
526fn check(h: &ArrayView2<'_, C64>, y: &[C64], noise_var: f64, iterations: usize) -> Result<()> {
527 ensure_shape(h.nrows() == y.len(), || {
528 format!("the channel has {} rows for {} received samples", h.nrows(), y.len())
529 })?;
530 ensure_shape(h.ncols() >= 1 && h.nrows() >= 1, || "the channel must not be empty".into())?;
531 ensure(noise_var >= 0.0 && noise_var.is_finite(), || {
532 format!("the noise variance must be finite and non-negative, got {noise_var}")
533 })?;
534 ensure(iterations >= 1, || "at least one iteration is needed".into())
535}
536
537fn residual(h: &ArrayView2<'_, C64>, y: &[C64], s: &[C64]) -> Vec<C64> {
538 y.iter().enumerate().map(|(i, yi)| yi - h.row(i).iter().zip(s).map(|(a, b)| a * b).sum::<C64>()).collect()
539}
540
541fn adjoint(h: &ArrayView2<'_, C64>, r: &[C64]) -> Vec<C64> {
542 (0..h.ncols()).map(|j| h.column(j).iter().zip(r).map(|(a, b)| a.conj() * b).sum()).collect()
543}
544
545pub fn lama(
555 h: &ArrayView2<'_, C64>,
556 y: &[C64],
557 noise_var: f64,
558 config: &MessagePassing,
559 variance: AmpVariance,
560) -> Result<Trace> {
561 let MessagePassing { prior, domain, iterations } = config;
562 let (domain, iterations) = (*domain, *iterations);
563 check(h, y, noise_var, iterations)?;
564 let (m, n) = h.dim();
565 let c = h.iter().map(C64::norm_sqr).sum::<f64>() / n as f64;
566 ensure(c > 0.0, || "the channel is zero".into())?;
567 let k = 1.0 / c.sqrt();
568 let h = h.mapv(|v| v * k);
569 let y: Vec<C64> = y.iter().map(|v| v * k).collect();
570 let n0 = noise_var / c;
571 let beta = n as f64 / m as f64;
572 let mut s = vec![prior.mean(); n];
573 let mut gamma = n0 + beta * prior.variance();
574 let mut r = residual(&h.view(), &y, &s);
575 let mut trace =
576 Trace { z: Vec::new(), noise_var: gamma, estimates: Vec::with_capacity(iterations), noise_vars: Vec::new() };
577 for _ in 0..iterations {
578 if variance == AmpVariance::Residual {
579 gamma = (r.iter().map(C64::norm_sqr).sum::<f64>() / m as f64).max(FLOOR);
580 }
581 let back = adjoint(&h.view(), &r);
582 let z: Vec<C64> = s.iter().zip(&back).map(|(a, b)| a + b).collect();
583 let (next, g): (Vec<C64>, Vec<f64>) = z.iter().map(|zi| prior.denoise(*zi, gamma, domain)).unzip();
584 let gbar = g.iter().sum::<f64>() / n as f64;
585 let onsager = beta * gbar / gamma;
586 let fit = residual(&h.view(), &y, &next);
587 r = fit.iter().zip(&r).map(|(f, old)| f + old * onsager).collect();
588 trace.z = z;
589 trace.noise_var = gamma;
590 trace.noise_vars.push(gamma);
591 trace.estimates.push(next.clone());
592 s = next;
593 gamma = n0 + beta * gbar;
594 }
595 trace.noise_var *= c;
597 for v in &mut trace.noise_vars {
598 *v *= c;
599 }
600 Ok(trace)
601}
602
603#[derive(Clone, Copy, Debug, PartialEq, Eq)]
605pub enum OampLinear {
606 MatchedFilter,
608 PseudoInverse,
610 Lmmse,
612}
613
614impl std::str::FromStr for OampLinear {
615 type Err = Error;
616
617 fn from_str(name: &str) -> Result<OampLinear> {
630 match name.to_ascii_lowercase().as_str() {
631 "lmmse" => Ok(OampLinear::Lmmse),
632 "pinv" => Ok(OampLinear::PseudoInverse),
633 "mf" => Ok(OampLinear::MatchedFilter),
634 other => Err(Error::Config(format!("unknown linear estimator {other:?}: lmmse, pinv or mf"))),
635 }
636 }
637}
638
639impl OampLinear {
640 fn gains(self, lambda: &[f64], v2: f64, noise_var: f64) -> Vec<f64> {
642 let top = lambda.iter().copied().fold(0.0, f64::max);
643 lambda
644 .iter()
645 .map(|&l| match self {
646 OampLinear::MatchedFilter => l,
647 OampLinear::PseudoInverse => {
648 if l > 1e-12 * top {
649 1.0 / l
650 } else {
651 0.0
652 }
653 }
654 OampLinear::Lmmse => v2 * l / (v2 * l * l + noise_var),
655 })
656 .collect()
657 }
658}
659
660fn decorrelated(lambda: &[f64], ghat: &[f64], n: usize, v2: f64, noise_var: f64) -> (Vec<f64>, f64) {
663 let trace: f64 = ghat.iter().zip(lambda).map(|(g, l)| g * l).sum();
664 let c = if trace > 0.0 { n as f64 / trace } else { 0.0 };
665 let g: Vec<f64> = ghat.iter().map(|x| x * c).collect();
666 let bb = g.iter().zip(lambda).map(|(g, l)| (1.0 - g * l).powi(2)).sum::<f64>() + (n - lambda.len()) as f64;
667 let ww = g.iter().map(|g| g * g).sum::<f64>();
668 (g, (bb * v2 + ww * noise_var) / n as f64)
669}
670
671pub fn oamp(
678 h: &ArrayView2<'_, C64>,
679 y: &[C64],
680 noise_var: f64,
681 config: &MessagePassing,
682 linear: OampLinear,
683) -> Result<Trace> {
684 let MessagePassing { prior, domain, iterations } = config;
685 let (domain, iterations) = (*domain, *iterations);
686 check(h, y, noise_var, iterations)?;
687 let (m, n) = h.dim();
688 let (u, lambda, v) = svd(h);
690 let frob: f64 = lambda.iter().map(|l| l * l).sum();
691 ensure(frob > 0.0, || "the channel is zero".into())?;
692 let floor = 1e-12 * prior.variance().max(FLOOR);
693 let mut s = vec![prior.mean(); n];
694 let mut trace =
695 Trace { z: Vec::new(), noise_var: 0.0, estimates: Vec::with_capacity(iterations), noise_vars: Vec::new() };
696 for t in 0..iterations {
697 let res = residual(h, y, &s);
698 let energy: f64 = res.iter().map(C64::norm_sqr).sum();
699 let v2 = ((energy - m as f64 * noise_var) / frob).max(floor);
700 let (g, tau2) = decorrelated(&lambda, &linear.gains(&lambda, v2, noise_var), n, v2, noise_var);
701 let tau2 = tau2.max(FLOOR);
702 let proj: Vec<C64> = (0..lambda.len())
704 .map(|i| u.column(i).iter().zip(&res).map(|(a, b)| a.conj() * b).sum::<C64>() * g[i])
705 .collect();
706 let r: Vec<C64> = (0..n).map(|j| s[j] + (0..proj.len()).map(|i| v[[j, i]] * proj[i]).sum::<C64>()).collect();
707 let (f, gv): (Vec<C64>, Vec<f64>) = r.iter().map(|ri| prior.denoise(*ri, tau2, domain)).unzip();
708 let mmse = gv.iter().sum::<f64>() / n as f64;
709 trace.noise_vars.push(tau2);
710 trace.estimates.push(f.clone());
711 trace.z.clone_from(&r);
712 trace.noise_var = tau2;
713 if t + 1 < iterations {
714 let c = tau2 / (tau2 - mmse).max(1e-12 * tau2);
715 s = f.iter().zip(&r).map(|(fi, ri)| (fi - ri * (mmse / tau2)) * c).collect();
716 }
717 }
718 Ok(trace)
719}
720
721pub fn oamp_state_evolution(
727 config: &MessagePassing,
728 singular_values: &[f64],
729 n: usize,
730 noise_var: f64,
731 linear: OampLinear,
732) -> Vec<StateEvolutionStep> {
733 let MessagePassing { prior, domain, iterations } = config;
734 let (domain, iterations) = (*domain, *iterations);
735 let lambda: Vec<f64> = singular_values.iter().copied().take(n).collect();
736 let mut v2 = prior.variance();
737 let mut out = Vec::with_capacity(iterations);
738 for _ in 0..iterations {
739 let (_, tau2) = decorrelated(&lambda, &linear.gains(&lambda, v2, noise_var), n, v2, noise_var);
740 let mmse = prior.mse(tau2, tau2, domain).0;
741 out.push(StateEvolutionStep { noise_var: tau2, mse: mmse });
742 v2 = if mmse < tau2 { 1.0 / (1.0 / mmse.max(FLOOR) - 1.0 / tau2) } else { tau2 };
743 }
744 out
745}
746
747#[derive(Clone, Debug, PartialEq)]
750pub struct Amp {
751 constellation: Constellation,
753 iterations: usize,
755 pub variance: AmpVariance,
757}
758
759impl Amp {
760 pub fn new(constellation: Constellation, iterations: usize) -> Result<Amp> {
765 ensure(iterations >= 1, || "at least one iteration is needed".into())?;
766 Ok(Amp { constellation, iterations, variance: AmpVariance::Postulated })
767 }
768
769 pub fn iterations(&self) -> usize {
771 self.iterations
772 }
773
774 fn trace(&self, h: &ArrayView2<'_, C64>, y: &[C64], noise_var: f64) -> Result<Trace> {
775 check_shape(h, y)?;
776 let config = MessagePassing {
777 prior: Prior::uniform(&self.constellation),
778 domain: Domain::Complex,
779 iterations: self.iterations,
780 };
781 lama(h, y, noise_var, &config, self.variance)
782 }
783}
784
785#[derive(Clone, Debug, PartialEq)]
787pub struct Oamp {
788 constellation: Constellation,
790 iterations: usize,
792 pub linear: OampLinear,
794}
795
796impl Oamp {
797 pub fn new(constellation: Constellation, iterations: usize) -> Result<Oamp> {
802 ensure(iterations >= 1, || "at least one iteration is needed".into())?;
803 Ok(Oamp { constellation, iterations, linear: OampLinear::Lmmse })
804 }
805
806 pub fn iterations(&self) -> usize {
808 self.iterations
809 }
810
811 fn trace(&self, h: &ArrayView2<'_, C64>, y: &[C64], noise_var: f64) -> Result<Trace> {
812 check_shape(h, y)?;
813 let config = MessagePassing {
814 prior: Prior::uniform(&self.constellation),
815 domain: Domain::Complex,
816 iterations: self.iterations,
817 };
818 oamp(h, y, noise_var, &config, self.linear)
819 }
820}
821
822fn decoupled_var(trace: &Trace) -> f64 {
825 trace.noise_var.max(FLOOR)
826}
827
828macro_rules! decoupled_detector {
831 ($t:ty) => {
832 impl MimoDetector for $t {
833 fn constellation(&self) -> &Constellation {
834 &self.constellation
835 }
836
837 fn has_soft_output(&self) -> bool {
838 true
839 }
840
841 fn soft_estimate(&self, h: &ArrayView2<'_, C64>, y: &[C64], noise_var: f64) -> Result<Option<Estimates>> {
842 let t = self.trace(h, y, noise_var)?;
843 let v = decoupled_var(&t);
844 Ok(Some(Estimates { noise_var: vec![v; t.z.len()], symbols: t.z }))
845 }
846
847 fn detect(&self, h: &ArrayView2<'_, C64>, y: &[C64], noise_var: f64) -> Result<Vec<usize>> {
848 Ok(self.constellation.decide(&self.trace(h, y, noise_var)?.z))
849 }
850
851 fn llr(&self, h: &ArrayView2<'_, C64>, y: &[C64], noise_var: f64) -> Result<Vec<f64>> {
852 let t = self.trace(h, y, noise_var)?;
853 self.constellation.llr(&t.z, decoupled_var(&t), None, Jacobian::Exact)
854 }
855
856 fn detect_llr(&self, h: &ArrayView2<'_, C64>, y: &[C64], noise_var: f64) -> Result<(Vec<usize>, Vec<f64>)> {
857 let t = self.trace(h, y, noise_var)?;
858 Ok((
859 self.constellation.decide(&t.z),
860 self.constellation.llr(&t.z, decoupled_var(&t), None, Jacobian::Exact)?,
861 ))
862 }
863 }
864 };
865}
866
867decoupled_detector!(Amp);
868decoupled_detector!(Oamp);
869
870#[cfg(test)]
871mod tests {
872 use super::*;
873 use comnum_core::rng::Rng;
874 use ndarray::Array2;
875
876 fn qpsk() -> Constellation {
877 Constellation::qam(4).unwrap().normalized()
878 }
879
880 #[test]
881 fn the_denoiser_is_the_posterior_of_the_alphabet() {
882 let p = Prior::uniform(&qpsk());
883 let a = qpsk().points()[2];
885 let (f, g) = p.denoise(a * 1.01, 1e-4, Domain::Complex);
886 assert!((f - a).norm() < 1e-9 && g < 1e-9);
887 let (f, g) = p.denoise(C64::new(0.0, 0.0), 1e9, Domain::Complex);
889 assert!(f.norm() < 1e-9 && (g - 1.0).abs() < 1e-6);
890 let bpsk = Prior::Discrete { points: vec![C64::new(-1.0, 0.0), C64::new(1.0, 0.0)], probs: vec![0.5, 0.5] };
892 let (f, g) = bpsk.denoise(C64::new(0.3, 7.0), 0.5, Domain::Real);
893 assert!((f.re - (0.3f64 / 0.5).tanh()).abs() < 1e-12 && (g - (1.0 - f.re * f.re)).abs() < 1e-12);
894 }
895
896 #[test]
897 fn the_mse_of_separable_qam_is_that_of_its_pam() {
898 let p = Prior::uniform(&Constellation::qam(16).unwrap().normalized());
899 let Prior::Discrete { points, probs } = &p else { unreachable!() };
900 let direct = |s2: f64| {
902 let q = Quadrature::new(Domain::Complex);
903 let mut psi = 0.0;
904 for (a, pa) in points.iter().zip(probs) {
905 q.for_each(|z, w| psi += pa * w * (p.denoise(a + z * s2.sqrt(), s2, Domain::Complex).0 - a).norm_sqr());
906 }
907 psi
908 };
909 for s2 in [0.01, 0.1, 1.0] {
910 let fast = p.mse(s2, s2, Domain::Complex).0;
911 assert!((fast / direct(s2) - 1.0).abs() < 1e-6, "{s2}: {fast} {}", direct(s2));
912 }
913 }
914
915 #[test]
916 fn lama_and_oamp_recover_a_noiseless_large_system() {
917 let c = qpsk();
918 let mut rng = Rng::new(4);
919 let (m, n) = (96, 64);
920 let h = Array2::from_shape_fn((m, n), |_| rng.complex_gaussian());
921 let idx: Vec<usize> = (0..n).map(|_| rng.below(4)).collect();
922 let x: Vec<C64> = idx.iter().map(|&i| c.points()[i]).collect();
923 let y = comnum_math::linalg::matvec(&h.view(), &x);
924 assert_eq!(Amp::new(c.clone(), 20).unwrap().detect(&h.view(), &y, 1e-6).unwrap(), idx);
925 assert_eq!(Oamp::new(c.clone(), 10).unwrap().detect(&h.view(), &y, 1e-6).unwrap(), idx);
926 assert!(Amp::new(c.clone(), 0).is_err() && Oamp::new(c.clone(), 0).is_err());
927 for d in [&Amp::new(c.clone(), 5).unwrap() as &dyn MimoDetector, &Oamp::new(c.clone(), 5).unwrap()] {
929 assert!(matches!(d.detect(&h.view(), &y[1..], 1e-6), Err(Error::Shape(_))));
930 let e = d.soft_estimate(&h.view(), &y, 1e-6).unwrap().unwrap();
932 let (labels, llr) = d.detect_llr(&h.view(), &y, 1e-6).unwrap();
933 assert_eq!(c.decide(&e.symbols), labels);
934 let from = c.llr_with_variances(&e.symbols, &e.noise_var, None, Jacobian::Exact).unwrap();
935 assert!(from.iter().zip(&llr).all(|(a, b)| a.to_bits() == b.to_bits()));
936 }
937 }
938
939 #[test]
940 fn the_state_evolution_of_lama_decreases_to_its_fixed_point() {
941 let config = MessagePassing { prior: Prior::uniform(&qpsk()), domain: Domain::Complex, iterations: 30 };
942 let se = amp_state_evolution(&config, 0.5, 0.05);
943 assert!(se.windows(2).all(|w| w[1].noise_var <= w[0].noise_var + 1e-15));
944 let last = se[se.len() - 1];
945 assert!((last.noise_var - (0.05 + 0.5 * last.mse)).abs() < 1e-9);
946 }
947}