use comnum_core::signal::{KNOWN_BIT_LLR, Symbols};
use comnum_core::traits::Detect;
use comnum_core::{Error, Result, ensure, ensure_shape};
use comnum_mapping::constellation::Constellation;
use comnum_math::C64;
use comnum_math::Jacobian;
use comnum_math::linalg::{CMat, hermitian, identity, inverse_hpd, matvec, qr};
use ndarray::{Array2, ArrayView2};
pub const ML_MAX_HYPOTHESES: usize = 1 << 22;
pub(crate) const FLOOR: f64 = 1e-300;
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Estimates {
pub symbols: Vec<C64>,
pub noise_var: Vec<f64>,
}
pub trait MimoDetector {
fn constellation(&self) -> &Constellation;
fn has_soft_output(&self) -> bool {
false
}
fn soft_estimate(&self, _h: &ArrayView2<'_, C64>, _y: &[C64], _noise_var: f64) -> Result<Option<Estimates>> {
Ok(None)
}
fn detect(&self, h: &ArrayView2<'_, C64>, y: &[C64], noise_var: f64) -> Result<Vec<usize>>;
fn llr(&self, _h: &ArrayView2<'_, C64>, _y: &[C64], _noise_var: f64) -> Result<Vec<f64>> {
Err(Error::Config("this detector has no soft output".into()))
}
fn detect_llr(&self, h: &ArrayView2<'_, C64>, y: &[C64], noise_var: f64) -> Result<(Vec<usize>, Vec<f64>)> {
Ok((self.detect(h, y, noise_var)?, self.llr(h, y, noise_var)?))
}
fn detect_block(&self, h: &ArrayView2<'_, C64>, y: &ArrayView2<'_, C64>, noise_var: f64) -> Result<Array2<usize>> {
let mut out = Array2::zeros((h.ncols(), y.ncols()));
for (t, col) in y.columns().into_iter().enumerate() {
let d = self.detect(h, &col.to_vec(), noise_var)?;
out.column_mut(t).assign(&ndarray::Array1::from(d));
}
Ok(out)
}
}
pub(crate) fn check_shape(h: &ArrayView2<'_, C64>, y: &[C64]) -> Result<()> {
ensure_shape(y.len() == h.nrows(), || format!("{} received samples for {} receive antennas", y.len(), h.nrows()))
}
fn check(h: &ArrayView2<'_, C64>, y: &[C64], noise_var: f64) -> Result<()> {
check_shape(h, y)?;
ensure(noise_var > 0.0 && noise_var.is_finite(), || format!("the noise variance must be positive, got {noise_var}"))
}
fn tall(h: &ArrayView2<'_, C64>, what: &str) -> Result<()> {
ensure(h.nrows() >= h.ncols(), || {
format!("{what} needs at least as many receive ({}) as transmit ({}) antennas", h.nrows(), h.ncols())
})
}
fn check_block(h: &ArrayView2<'_, C64>, y: &ArrayView2<'_, C64>, noise_var: f64) -> Result<()> {
if y.nrows() != h.nrows() {
return Err(Error::Shape(format!("{} received streams for {} receive antennas", y.nrows(), h.nrows())));
}
check(h, &vec![C64::new(0.0, 0.0); h.nrows()], noise_var)
}
fn by_column(
nt: usize,
y: &ArrayView2<'_, C64>,
mut detect: impl FnMut(&mut [C64]) -> Result<Vec<usize>>,
) -> Result<Array2<usize>> {
let mut out = Array2::zeros((nt, y.ncols()));
let mut col = vec![C64::new(0.0, 0.0); y.nrows()];
for (t, c) in y.columns().into_iter().enumerate() {
col.iter_mut().zip(c).for_each(|(a, b)| *a = *b);
for (o, l) in out.column_mut(t).iter_mut().zip(detect(&mut col)?) {
*o = l;
}
}
Ok(out)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Criterion {
Mr,
Zf,
Mmse,
}
struct Filter {
w: CMat,
bias: Vec<f64>,
var: Vec<f64>,
}
fn regularisation(h: &ArrayView2<'_, C64>, criterion: Criterion, es: f64, noise_var: f64) -> Result<f64> {
match criterion {
Criterion::Mr => Ok(0.0),
Criterion::Zf => tall(h, "zero forcing").map(|()| 0.0),
Criterion::Mmse => Ok(noise_var / es),
}
}
fn filter(h: &ArrayView2<'_, C64>, criterion: Criterion, es: f64, noise_var: f64) -> Result<Filter> {
let n = h.ncols();
let lambda = regularisation(h, criterion, es, noise_var)?;
if n == 1 {
let s = SingleStream::new(h, criterion, es, noise_var, lambda)?;
return Ok(Filter { w: h.t().mapv(|v| v.conj() / s.g), bias: vec![s.bias], var: vec![s.var] });
}
let hh = hermitian(h);
if criterion == Criterion::Mr {
return matched_filter(&hh, h, es, noise_var);
}
let g = hh.dot(h) + identity(n).mapv(|x| x * lambda);
let ginv = inverse_hpd(&g.view()).map_err(|_| Error::Numerical("the channel is rank deficient".into()))?;
let w = ginv.dot(&hh);
let (mut bias, mut var) = (Vec::with_capacity(n), Vec::with_capacity(n));
for k in 0..n {
let (b, v) = unbiased(criterion, noise_var * ginv[[k, k]].re, es);
bias.push(b);
var.push(v);
}
Ok(Filter { w, bias, var })
}
fn matched_filter(hh: &CMat, h: &ArrayView2<'_, C64>, es: f64, noise_var: f64) -> Result<Filter> {
let n = h.ncols();
let g = hh.dot(h);
let mut w = hh.clone();
let mut var = Vec::with_capacity(n);
for k in 0..n {
let gkk = g[[k, k]].re;
if !(gkk > 0.0 && gkk.is_finite()) {
return Err(Error::Numerical("a stream has a zero channel".into()));
}
w.row_mut(k).mapv_inplace(|v| v / gkk);
let interference: f64 = (0..n).filter(|j| *j != k).map(|j| g[[k, j]].norm_sqr()).sum();
var.push((noise_var * gkk + es * interference) / (gkk * gkk));
}
Ok(Filter { w, bias: vec![1.0; n], var })
}
fn unbiased(criterion: Criterion, ekk: f64, es: f64) -> (f64, f64) {
match criterion {
Criterion::Mr | Criterion::Zf => (1.0, ekk),
Criterion::Mmse => ((1.0 - ekk / es).max(FLOOR), es * ekk / (es - ekk).max(FLOOR)),
}
}
struct SingleStream {
g: f64,
bias: f64,
var: f64,
}
impl SingleStream {
fn new(
h: &ArrayView2<'_, C64>,
criterion: Criterion,
es: f64,
noise_var: f64,
lambda: f64,
) -> Result<SingleStream> {
let g = h.iter().map(C64::norm_sqr).sum::<f64>() + lambda;
if !(g > 0.0 && g.is_finite()) {
return Err(Error::Numerical("the channel is rank deficient".into()));
}
let (bias, var) = unbiased(criterion, noise_var / g, es);
Ok(SingleStream { g, bias, var })
}
fn estimate(&self, h: &ArrayView2<'_, C64>, y: &[C64]) -> C64 {
h.column(0).iter().zip(y).map(|(hi, yi)| hi.conj() / self.g * yi).sum::<C64>() / self.bias
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Linear {
pub constellation: Constellation,
pub criterion: Criterion,
}
impl Linear {
pub fn mr(constellation: Constellation) -> Linear {
Linear { constellation, criterion: Criterion::Mr }
}
pub fn zf(constellation: Constellation) -> Linear {
Linear { constellation, criterion: Criterion::Zf }
}
pub fn mmse(constellation: Constellation) -> Linear {
Linear { constellation, criterion: Criterion::Mmse }
}
pub fn estimate(&self, h: &ArrayView2<'_, C64>, y: &[C64], noise_var: f64) -> Result<Estimates> {
check(h, y, noise_var)?;
let (z, var) = self.single(h, y, noise_var)?;
if let Some(z) = z {
return Ok(Estimates { symbols: vec![z], noise_var: vec![var] });
}
let f = filter(h, self.criterion, self.constellation.energy(), noise_var)?;
let symbols = matvec(&f.w.view(), y).iter().zip(&f.bias).map(|(z, b)| z / b).collect();
Ok(Estimates { symbols, noise_var: f.var })
}
fn single(&self, h: &ArrayView2<'_, C64>, y: &[C64], noise_var: f64) -> Result<(Option<C64>, f64)> {
if h.ncols() != 1 {
return Ok((None, 0.0));
}
let es = self.constellation.energy();
let s = SingleStream::new(h, self.criterion, es, noise_var, regularisation(h, self.criterion, es, noise_var)?)?;
Ok((Some(s.estimate(h, y)), s.var))
}
}
impl MimoDetector for Linear {
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>> {
self.estimate(h, y, noise_var).map(Some)
}
fn detect(&self, h: &ArrayView2<'_, C64>, y: &[C64], noise_var: f64) -> Result<Vec<usize>> {
Ok(self.constellation.decide(&self.estimate(h, y, noise_var)?.symbols))
}
fn llr(&self, h: &ArrayView2<'_, C64>, y: &[C64], noise_var: f64) -> Result<Vec<f64>> {
let e = self.estimate(h, y, noise_var)?;
self.constellation.llr_with_variances(&e.symbols, &e.noise_var, None, Jacobian::Exact)
}
fn detect_llr(&self, h: &ArrayView2<'_, C64>, y: &[C64], noise_var: f64) -> Result<(Vec<usize>, Vec<f64>)> {
let e = self.estimate(h, y, noise_var)?;
let llr = self.constellation.llr_with_variances(&e.symbols, &e.noise_var, None, Jacobian::Exact)?;
Ok((self.constellation.decide(&e.symbols), llr))
}
fn detect_block(&self, h: &ArrayView2<'_, C64>, y: &ArrayView2<'_, C64>, noise_var: f64) -> Result<Array2<usize>> {
check_block(h, y, noise_var)?;
let f = filter(h, self.criterion, self.constellation.energy(), noise_var)?;
let z = f.w.dot(y);
Ok(Array2::from_shape_fn(z.dim(), |(k, t)| self.constellation.nearest(z[[k, t]] / f.bias[k])))
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Sic {
pub constellation: Constellation,
pub criterion: Criterion,
pub ordered: bool,
}
impl Sic {
pub fn mmse(constellation: Constellation) -> Sic {
Sic { constellation, criterion: Criterion::Mmse, ordered: true }
}
pub fn zf(constellation: Constellation) -> Sic {
Sic { constellation, criterion: Criterion::Zf, ordered: true }
}
}
struct SicStage {
stream: usize,
w: Vec<C64>,
bias: f64,
column: Vec<C64>,
}
impl Sic {
fn stages(&self, h: &ArrayView2<'_, C64>, noise_var: f64) -> Result<Vec<SicStage>> {
let es = self.constellation.energy();
let mut remaining: Vec<usize> = (0..h.ncols()).collect();
let mut stages = Vec::with_capacity(h.ncols());
while !remaining.is_empty() {
let hc = Array2::from_shape_fn((h.nrows(), remaining.len()), |(i, j)| h[[i, remaining[j]]]);
let f = filter(&hc.view(), self.criterion, es, noise_var)?;
let k = if self.ordered {
(0..remaining.len()).min_by(|a, b| f.var[*a].total_cmp(&f.var[*b])).unwrap_or(0)
} else {
0
};
stages.push(SicStage {
stream: remaining[k],
w: f.w.row(k).to_vec(),
bias: f.bias[k],
column: hc.column(k).to_vec(),
});
remaining.remove(k);
}
Ok(stages)
}
fn cancel(&self, stages: &[SicStage], residual: &mut [C64]) -> Vec<usize> {
let mut out = vec![0; stages.len()];
for s in stages {
let z: C64 = s.w.iter().zip(residual.iter()).map(|(w, v)| w * v).sum::<C64>() / s.bias;
let idx = self.constellation.nearest(z);
let p = self.constellation.points()[idx];
for (r, hk) in residual.iter_mut().zip(&s.column) {
*r -= hk * p;
}
out[s.stream] = idx;
}
out
}
}
impl MimoDetector for Sic {
fn constellation(&self) -> &Constellation {
&self.constellation
}
fn detect(&self, h: &ArrayView2<'_, C64>, y: &[C64], noise_var: f64) -> Result<Vec<usize>> {
check(h, y, noise_var)?;
let stages = self.stages(h, noise_var)?;
Ok(self.cancel(&stages, &mut y.to_vec()))
}
fn detect_block(&self, h: &ArrayView2<'_, C64>, y: &ArrayView2<'_, C64>, noise_var: f64) -> Result<Array2<usize>> {
check_block(h, y, noise_var)?;
let stages = self.stages(h, noise_var)?;
by_column(h.ncols(), y, |col| Ok(self.cancel(&stages, col)))
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Ml {
pub constellation: Constellation,
}
impl Ml {
pub fn new(constellation: Constellation) -> Ml {
Ml { constellation }
}
fn products(&self, h: &ArrayView2<'_, C64>) -> Result<Vec<C64>> {
let (m, nt, nr) = (self.constellation.order(), h.ncols(), h.nrows());
let total = (0..nt).try_fold(1usize, |acc, _| acc.checked_mul(m)).filter(|t| *t <= ML_MAX_HYPOTHESES);
ensure(total.is_some(), || format!("{m}^{nt} hypotheses exceed the limit of the exhaustive search"))?;
let pts = self.constellation.points();
let mut prod = Vec::with_capacity(nt * m * nr);
for k in 0..nt {
for p in pts {
prod.extend((0..nr).map(|r| h[[r, k]] * p));
}
}
Ok(prod)
}
fn search(&self, prod: &[C64], nt: usize, y: &[C64], mut visit: impl FnMut(&[usize], f64)) {
let (m, nr) = (self.constellation.order(), y.len());
let mut idx = vec![0usize; nt];
loop {
let d: f64 = (0..nr)
.map(|r| {
let a: C64 = idx.iter().enumerate().map(|(k, &i)| prod[(k * m + i) * nr + r]).sum();
(y[r] - a).norm_sqr()
})
.sum();
visit(&idx, d);
let mut k = 0;
loop {
if k == nt {
return;
}
idx[k] += 1;
if idx[k] < m {
break;
}
idx[k] = 0;
k += 1;
}
}
}
fn best(&self, prod: &[C64], nt: usize, y: &[C64]) -> Vec<usize> {
let mut best = (f64::INFINITY, vec![0; nt]);
self.search(prod, nt, y, |idx, d| {
if d < best.0 {
best.0 = d;
best.1.copy_from_slice(idx);
}
});
best.1
}
}
fn max_log(best: &[[f64; 2]], noise_var: f64, clip: f64) -> Vec<f64> {
best.iter()
.map(|[d0, d1]| match (d0.is_finite(), d1.is_finite()) {
(true, true) => ((d1 - d0) / noise_var).clamp(-clip, clip),
(true, false) => clip,
(false, true) => -clip,
(false, false) => 0.0,
})
.collect()
}
fn update_bits(best: &mut [[f64; 2]], idx: &[usize], bits: usize, d: f64) {
for (s, &i) in idx.iter().enumerate() {
for p in 0..bits {
let b = (i >> (bits - 1 - p)) & 1;
let slot = &mut best[s * bits + p][b];
if d < *slot {
*slot = d;
}
}
}
}
impl MimoDetector for Ml {
fn constellation(&self) -> &Constellation {
&self.constellation
}
fn has_soft_output(&self) -> bool {
true
}
fn detect(&self, h: &ArrayView2<'_, C64>, y: &[C64], noise_var: f64) -> Result<Vec<usize>> {
check(h, y, noise_var)?;
Ok(self.best(&self.products(h)?, h.ncols(), y))
}
fn llr(&self, h: &ArrayView2<'_, C64>, y: &[C64], noise_var: f64) -> Result<Vec<f64>> {
Ok(self.detect_llr(h, y, noise_var)?.1)
}
fn detect_llr(&self, h: &ArrayView2<'_, C64>, y: &[C64], noise_var: f64) -> Result<(Vec<usize>, Vec<f64>)> {
check(h, y, noise_var)?;
let prod = self.products(h)?;
let (nt, bits) = (h.ncols(), self.constellation.bits_per_symbol());
let mut best = (f64::INFINITY, vec![0; nt]);
let mut per_bit = vec![[f64::INFINITY; 2]; nt * bits];
self.search(&prod, nt, y, |idx, d| {
if d < best.0 {
best.0 = d;
best.1.copy_from_slice(idx);
}
update_bits(&mut per_bit, idx, bits, d);
});
Ok((best.1, max_log(&per_bit, noise_var, f64::INFINITY)))
}
fn detect_block(&self, h: &ArrayView2<'_, C64>, y: &ArrayView2<'_, C64>, noise_var: f64) -> Result<Array2<usize>> {
check_block(h, y, noise_var)?;
let prod = self.products(h)?;
by_column(h.ncols(), y, |col| Ok(self.best(&prod, h.ncols(), col)))
}
}
struct Triangular {
r: CMat,
qh: CMat,
}
impl Triangular {
fn new(h: &ArrayView2<'_, C64>, what: &str) -> Result<Triangular> {
tall(h, what)?;
let (q, r) = qr(h)?;
if (0..r.nrows()).any(|i| r[[i, i]].re <= 0.0) {
return Err(Error::Numerical("the channel is rank deficient".into()));
}
Ok(Triangular { r, qh: hermitian(&q.view()) })
}
fn rotate(&self, y: &[C64]) -> Vec<C64> {
matvec(&self.qh.view(), y)
}
}
type Frame = (Vec<(f64, usize)>, usize, f64);
#[derive(Clone, Debug, PartialEq)]
pub struct Sphere {
pub constellation: Constellation,
}
impl Sphere {
pub fn new(constellation: Constellation) -> Sphere {
Sphere { constellation }
}
fn search(&self, r: &CMat, z: &[C64]) -> Vec<usize> {
let n = r.ncols();
let pts = self.constellation.points();
let mut best = (f64::INFINITY, vec![0usize; n]);
let mut x = vec![C64::new(0.0, 0.0); n];
let mut idx = vec![0usize; n];
let mut frames: Vec<Frame> = Vec::with_capacity(n);
let mut spare: Vec<Vec<(f64, usize)>> = Vec::with_capacity(n);
let candidates = |k: usize, x: &[C64], mut c: Vec<(f64, usize)>| -> Vec<(f64, usize)> {
let mut b = z[k];
for j in k + 1..n {
b -= r[[k, j]] * x[j];
}
c.clear();
c.extend(pts.iter().enumerate().map(|(i, p)| ((b - r[[k, k]] * p).norm_sqr(), i)));
c.sort_by(|a, b| a.0.total_cmp(&b.0));
c
};
frames.push((candidates(n - 1, &x, Vec::with_capacity(pts.len())), 0, 0.0));
while !frames.is_empty() {
let k = n - frames.len();
let Some(f) = frames.last_mut() else { break };
let candidate = f.0.get(f.1).copied();
f.1 += 1;
let base = f.2;
let next = match candidate {
Some((d, i)) if base + d < best.0 => Some((base + d, i)),
_ => None,
};
let Some((partial, i)) = next else {
if let Some(f) = frames.pop() {
spare.push(f.0);
}
continue;
};
x[k] = pts[i];
idx[k] = i;
if k == 0 {
best.0 = partial;
best.1.copy_from_slice(&idx);
} else {
let c = candidates(k - 1, &x, spare.pop().unwrap_or_default());
frames.push((c, 0, partial));
}
}
best.1
}
}
impl MimoDetector for Sphere {
fn constellation(&self) -> &Constellation {
&self.constellation
}
fn detect(&self, h: &ArrayView2<'_, C64>, y: &[C64], noise_var: f64) -> Result<Vec<usize>> {
check(h, y, noise_var)?;
let t = Triangular::new(h, "the sphere decoder")?;
Ok(self.search(&t.r, &t.rotate(y)))
}
fn detect_block(&self, h: &ArrayView2<'_, C64>, y: &ArrayView2<'_, C64>, noise_var: f64) -> Result<Array2<usize>> {
check_block(h, y, noise_var)?;
let t = Triangular::new(h, "the sphere decoder")?;
by_column(h.ncols(), y, |col| Ok(self.search(&t.r, &t.rotate(col))))
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct KBest {
constellation: Constellation,
k: usize,
llr_clip: f64,
}
struct Paths {
labels: Vec<usize>,
dist: Vec<f64>,
}
impl Paths {
fn iter(&self, nt: usize) -> impl Iterator<Item = (&[usize], f64)> {
self.labels.chunks_exact(nt.max(1)).zip(self.dist.iter().copied())
}
fn first(&self, nt: usize) -> Vec<usize> {
self.labels.get(..nt).map(<[usize]>::to_vec).unwrap_or_default()
}
}
impl KBest {
pub fn new(constellation: Constellation, k: usize) -> Result<KBest> {
ensure(k >= 1, || "K-best needs k ≥ 1".into())?;
Ok(KBest { constellation, k, llr_clip: KNOWN_BIT_LLR })
}
pub fn with_llr_clip(mut self, llr_clip: f64) -> Result<KBest> {
ensure(llr_clip > 0.0, || format!("the LLR clip must be positive, got {llr_clip}"))?;
self.llr_clip = llr_clip;
Ok(self)
}
pub fn k(&self) -> usize {
self.k
}
pub fn llr_clip(&self) -> f64 {
self.llr_clip
}
fn search(&self, r: &CMat, z: &[C64]) -> Paths {
let n = r.ncols();
let pts = self.constellation.points();
let mut labels: Vec<usize> = Vec::new();
let mut next_labels: Vec<usize> = Vec::new();
let mut dist = vec![0.0];
let mut children: Vec<(f64, usize, usize)> = Vec::new();
for (depth, k) in (0..n).rev().enumerate() {
children.clear();
for (p, d) in dist.iter().enumerate() {
let mut b = z[k];
for (level, &l) in labels[p * depth..(p + 1) * depth].iter().enumerate() {
let j = n - 1 - level;
b -= r[[k, j]] * pts[l];
}
children.extend(pts.iter().enumerate().map(|(i, s)| (d + (b - r[[k, k]] * s).norm_sqr(), p, i)));
}
children.sort_by(|a, b| a.0.total_cmp(&b.0));
children.truncate(self.k);
next_labels.clear();
dist.clear();
for &(d, p, i) in &children {
next_labels.extend_from_slice(&labels[p * depth..(p + 1) * depth]);
next_labels.push(i);
dist.push(d);
}
std::mem::swap(&mut labels, &mut next_labels);
}
labels.chunks_exact_mut(n.max(1)).for_each(<[usize]>::reverse);
Paths { labels, dist }
}
fn soft(&self, paths: &Paths, nt: usize, noise_var: f64) -> Vec<f64> {
let bits = self.constellation.bits_per_symbol();
let mut best = vec![[f64::INFINITY; 2]; nt * bits];
for (labels, d) in paths.iter(nt) {
update_bits(&mut best, labels, bits, d);
}
max_log(&best, noise_var, self.llr_clip)
}
}
impl MimoDetector for KBest {
fn constellation(&self) -> &Constellation {
&self.constellation
}
fn has_soft_output(&self) -> bool {
true
}
fn detect(&self, h: &ArrayView2<'_, C64>, y: &[C64], noise_var: f64) -> Result<Vec<usize>> {
check(h, y, noise_var)?;
let t = Triangular::new(h, "K-best")?;
Ok(self.search(&t.r, &t.rotate(y)).first(h.ncols()))
}
fn llr(&self, h: &ArrayView2<'_, C64>, y: &[C64], noise_var: f64) -> Result<Vec<f64>> {
Ok(self.detect_llr(h, y, noise_var)?.1)
}
fn detect_llr(&self, h: &ArrayView2<'_, C64>, y: &[C64], noise_var: f64) -> Result<(Vec<usize>, Vec<f64>)> {
check(h, y, noise_var)?;
let t = Triangular::new(h, "K-best")?;
let paths = self.search(&t.r, &t.rotate(y));
Ok((paths.first(h.ncols()), self.soft(&paths, h.ncols(), noise_var)))
}
fn detect_block(&self, h: &ArrayView2<'_, C64>, y: &ArrayView2<'_, C64>, noise_var: f64) -> Result<Array2<usize>> {
check_block(h, y, noise_var)?;
let t = Triangular::new(h, "K-best")?;
by_column(h.ncols(), y, |col| Ok(self.search(&t.r, &t.rotate(col)).first(h.ncols())))
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Coherent<D> {
pub detector: D,
pub h: CMat,
}
impl<D: MimoDetector> Coherent<D> {
pub fn new(detector: D, h: CMat) -> Coherent<D> {
Coherent { detector, h }
}
}
impl<D: MimoDetector> Detect for Coherent<D> {
fn detect_into(&mut self, y: &Symbols, noise_var: f64, out: &mut Symbols) -> Result<()> {
let labels = self.detector.detect_block(&self.h.view(), &y.as_array().view(), noise_var)?;
let pts = self.detector.constellation().points();
out.assign(&labels.mapv(|i| pts[i]));
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use comnum_core::rng::Rng;
fn channel(nr: usize, nt: usize, rng: &mut Rng) -> CMat {
Array2::from_shape_fn((nr, nt), |_| rng.complex_gaussian())
}
fn noisy(h: &CMat, x: &[C64], s2: f64, rng: &mut Rng) -> Vec<C64> {
matvec(&h.view(), x).into_iter().map(|v| v + rng.complex_gaussian() * s2.sqrt()).collect()
}
#[test]
fn the_matched_filter_leaves_the_other_streams_as_interference() {
let mut rng = Rng::new(11);
let (es, s2) = (2.0, 0.3);
let h = channel(6, 3, &mut rng);
let f = filter(&h.view(), Criterion::Mr, es, s2).unwrap();
let g = hermitian(&h.view()).dot(&h);
for k in 0..3 {
let wk = f.w.row(k);
let gain = |j: usize| wk.iter().zip(h.column(j)).map(|(a, b)| a * b).sum::<C64>();
assert!((gain(k) - 1.0).norm() < 1e-14 && f.bias[k] == 1.0);
let expected = s2 * wk.iter().map(C64::norm_sqr).sum::<f64>()
+ es * (0..3).filter(|j| *j != k).map(|j| gain(j).norm_sqr()).sum::<f64>();
assert!((f.var[k] - expected).abs() < 1e-12 * expected);
assert!(
(f.var[k]
- (s2 * g[[k, k]].re + es * (g[[k, (k + 1) % 3]].norm_sqr() + g[[k, (k + 2) % 3]].norm_sqr()))
/ g[[k, k]].re.powi(2))
.abs()
< 1e-12
);
}
let mut q = CMat::zeros((4, 2));
q[[0, 0]] = C64::new(2.0, 0.0);
q[[1, 1]] = C64::new(0.0, 3.0);
let (mr, zf) =
(filter(&q.view(), Criterion::Mr, es, s2).unwrap(), filter(&q.view(), Criterion::Zf, es, s2).unwrap());
assert!(mr.w.iter().zip(zf.w.iter()).all(|(a, b)| (a - b).norm() < 1e-15));
assert!(mr.var.iter().zip(&zf.var).all(|(a, b)| (a - b).abs() < 1e-15));
}
#[test]
fn the_single_stream_filter_is_the_matrix_filter() {
let mut rng = Rng::new(7);
let es = 1.0;
for nr in [1, 3] {
let h = channel(nr, 1, &mut rng);
for (criterion, lambda) in [(Criterion::Zf, 0.0), (Criterion::Mmse, 0.3 / es)] {
let f = filter(&h.view(), criterion, es, 0.3).unwrap();
let hh = hermitian(&h.view());
let ginv = inverse_hpd(&(hh.dot(&h) + identity(1).mapv(|x| x * lambda)).view()).unwrap();
let w = ginv.dot(&hh);
assert!(f.w.iter().zip(w.iter()).all(|(a, b)| (a - b).norm() < 1e-14 * b.norm().max(1.0)));
assert!(
(f.var[0]
- match criterion {
Criterion::Mr | Criterion::Zf => 0.3 * ginv[[0, 0]].re,
Criterion::Mmse => {
let e = 0.3 * ginv[[0, 0]].re;
es * e / (es - e)
}
})
.abs()
< 1e-14
);
}
}
let qpsk = Constellation::psk(4).unwrap().normalized();
let h = channel(3, 1, &mut rng);
let y: Vec<C64> = (0..3).map(|_| rng.complex_gaussian()).collect();
for d in [Linear::mr(qpsk.clone()), Linear::zf(qpsk.clone()), Linear::mmse(qpsk.clone())] {
let f = filter(&h.view(), d.criterion, qpsk.energy(), 0.3).unwrap();
let e = d.estimate(&h.view(), &y, 0.3).unwrap();
assert!((e.symbols[0] - matvec(&f.w.view(), &y)[0] / f.bias[0]).norm() < 1e-14 && e.noise_var == f.var);
}
let zero = Array2::from_elem((2, 1), C64::new(0.0, 0.0));
assert!(filter(&zero.view(), Criterion::Zf, es, 0.3).is_err());
}
#[test]
fn every_detector_is_exact_without_noise() {
let c = Constellation::qam(16).unwrap().normalized();
let mut rng = Rng::new(1);
for _ in 0..50 {
let h = channel(4, 3, &mut rng);
let idx: Vec<usize> = (0..3).map(|_| rng.below(16)).collect();
let x: Vec<C64> = idx.iter().map(|&i| c.points()[i]).collect();
let y = matvec(&h.view(), &x);
let dets: Vec<Box<dyn MimoDetector>> = vec![
Box::new(Linear::zf(c.clone())),
Box::new(Linear::mmse(c.clone())),
Box::new(Sic::zf(c.clone())),
Box::new(Sic::mmse(c.clone())),
Box::new(Ml::new(c.clone())),
Box::new(Sphere::new(c.clone())),
Box::new(KBest::new(c.clone(), 4).unwrap()),
];
for d in &dets {
assert_eq!(d.detect(&h.view(), &y, 1e-6).unwrap(), idx);
assert_eq!(d.constellation(), &c);
}
}
}
#[test]
fn the_sphere_decoder_is_the_exhaustive_ml_detector() {
let c = Constellation::qam(4).unwrap().normalized();
let mut rng = Rng::new(2);
for _ in 0..300 {
let h = channel(3, 3, &mut rng);
let x: Vec<C64> = (0..3).map(|_| c.points()[rng.below(4)]).collect();
let y = noisy(&h, &x, 0.5, &mut rng);
let ml = Ml::new(c.clone()).detect(&h.view(), &y, 0.5).unwrap();
assert_eq!(Sphere::new(c.clone()).detect(&h.view(), &y, 0.5).unwrap(), ml);
assert_eq!(KBest::new(c.clone(), 64).unwrap().detect(&h.view(), &y, 0.5).unwrap(), ml);
}
}
#[test]
fn soft_outputs_agree_in_sign_with_the_decisions() {
let c = Constellation::qam(16).unwrap().normalized();
let mut rng = Rng::new(3);
let h = channel(4, 2, &mut rng);
let x = [c.points()[5], c.points()[10]];
let y = noisy(&h, &x, 1e-3, &mut rng);
for d in
[&Linear::mmse(c.clone()) as &dyn MimoDetector, &Ml::new(c.clone()), &KBest::new(c.clone(), 8).unwrap()]
{
assert!(d.has_soft_output());
let l = d.llr(&h.view(), &y, 1e-3).unwrap();
assert_eq!(l.len(), 8);
let bits: Vec<usize> = [5usize, 10].iter().flat_map(|i| (0..4).map(move |p| (i >> (3 - p)) & 1)).collect();
for (v, b) in l.iter().zip(bits) {
assert!(if b == 0 { *v > 0.0 } else { *v < 0.0 }, "{l:?}");
}
}
assert!(Sphere::new(c.clone()).llr(&h.view(), &y, 1e-3).is_err());
assert!(!Sphere::new(c.clone()).has_soft_output() && !Sic::mmse(c.clone()).has_soft_output());
assert!(KBest::new(c.clone(), 0).is_err() && KBest::new(c, 2).unwrap().with_llr_clip(-1.0).is_err());
}
#[test]
fn linear_block_detection_matches_vector_detection() {
let c = Constellation::qam(4).unwrap().normalized();
let mut rng = Rng::new(4);
let h = channel(2, 2, &mut rng);
let y = Array2::from_shape_fn((2, 20), |_| rng.complex_gaussian());
for d in [Linear::zf(c.clone()), Linear::mmse(c.clone())] {
let block = d.detect_block(&h.view(), &y.view(), 0.1).unwrap();
for t in 0..20 {
assert_eq!(block.column(t).to_vec(), d.detect(&h.view(), &y.column(t).to_vec(), 0.1).unwrap());
}
}
}
#[test]
fn block_and_joint_outputs_match_the_detection_of_every_use() {
let c = Constellation::qam(16).unwrap().normalized();
let mut rng = Rng::new(8);
let dets: Vec<Box<dyn MimoDetector>> = vec![
Box::new(Sic::zf(c.clone())),
Box::new(Sic::mmse(c.clone())),
Box::new(Sic { constellation: c.clone(), criterion: Criterion::Mmse, ordered: false }),
Box::new(Ml::new(c.clone())),
Box::new(Sphere::new(c.clone())),
Box::new(KBest::new(c.clone(), 1).unwrap()),
Box::new(KBest::new(c.clone(), 12).unwrap()),
];
for _ in 0..10 {
let h = channel(4, 3, &mut rng);
let y = Array2::from_shape_fn((4, 8), |_| rng.complex_gaussian());
for d in &dets {
let block = d.detect_block(&h.view(), &y.view(), 0.2).unwrap();
for t in 0..8 {
let col = y.column(t).to_vec();
let labels = d.detect(&h.view(), &col, 0.2).unwrap();
assert_eq!(block.column(t).to_vec(), labels);
if let Ok(llr) = d.llr(&h.view(), &col, 0.2) {
let (l, v) = d.detect_llr(&h.view(), &col, 0.2).unwrap();
assert_eq!(
(l, v.iter().map(|x| x.to_bits()).collect::<Vec<_>>()),
(labels, llr.iter().map(|x| x.to_bits()).collect())
);
}
}
}
}
}
#[test]
fn invalid_inputs_are_errors() {
let c = Constellation::qam(4).unwrap().normalized();
let h = channel(2, 3, &mut Rng::new(5));
let y = vec![C64::new(0.0, 0.0); 2];
assert!(Linear::zf(c.clone()).detect(&h.view(), &y, 0.1).is_err());
assert!(Sphere::new(c.clone()).detect(&h.view(), &y, 0.1).is_err());
assert!(Linear::mmse(c.clone()).detect(&h.view(), &y, 0.1).is_ok()); assert!(Ml::new(c.clone()).detect(&h.view(), &y[..1], 0.1).is_err());
assert!(Ml::new(c).detect(&h.view(), &y, 0.0).is_err());
}
#[test]
fn the_coherent_block_implements_detect() {
let c = Constellation::qam(4).unwrap().normalized();
let mut rng = Rng::new(6);
let h = channel(3, 2, &mut rng);
let x = Array2::from_shape_fn((2, 8), |_| c.points()[rng.below(4)]);
let y = Symbols::new(h.dot(&x));
let mut d = Coherent::new(Sphere::new(c), h);
assert_eq!(d.detect_into_owned(&y).unwrap().as_array(), &x);
}
trait Owned {
fn detect_into_owned(&mut self, y: &Symbols) -> Result<Symbols>;
}
impl<D: MimoDetector> Owned for Coherent<D> {
fn detect_into_owned(&mut self, y: &Symbols) -> Result<Symbols> {
let mut out = Symbols::default();
self.detect_into(y, 1e-3, &mut out)?;
Ok(out)
}
}
}