use crate::detection::{Estimates, KBest, Linear, MimoDetector, Ml, Sic, Sphere};
use crate::message_passing::{Amp, Oamp};
use crate::stbc::Ostbc;
use comnum_core::{Error, Result, ensure};
use comnum_mapping::constellation::Constellation;
use comnum_math::C64;
use comnum_math::Jacobian;
use comnum_math::linalg::CMat;
use ndarray::{Array2, ArrayView2};
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DetectorKind {
Mr,
Zf,
Mmse,
Sic,
ZfSic,
Ml,
Sphere,
KBest(usize),
Amp(usize),
Oamp(usize),
}
impl std::str::FromStr for DetectorKind {
type Err = Error;
fn from_str(name: &str) -> Result<DetectorKind> {
let name = name.to_ascii_lowercase();
let canonical = match name.as_str() {
"mmse_sic" => "sic",
"lama" => "amp",
other => other,
};
DetectorKind::ALL
.into_iter()
.find(|d| d.name() == canonical)
.ok_or_else(|| Error::Config(format!("unknown detector {name:?}: {}", DetectorKind::NAMES.join(", "))))
}
}
impl DetectorKind {
pub const DEFAULT_PATHS: usize = 8;
pub const DEFAULT_ITERATIONS: usize = 10;
pub const ALL: [DetectorKind; 10] = [
DetectorKind::Mr,
DetectorKind::Zf,
DetectorKind::Mmse,
DetectorKind::Sic,
DetectorKind::ZfSic,
DetectorKind::Ml,
DetectorKind::Sphere,
DetectorKind::KBest(DetectorKind::DEFAULT_PATHS),
DetectorKind::Amp(DetectorKind::DEFAULT_ITERATIONS),
DetectorKind::Oamp(DetectorKind::DEFAULT_ITERATIONS),
];
pub const NAMES: [&'static str; 10] = {
let mut names = [""; 10];
let mut i = 0;
while i < names.len() {
names[i] = DetectorKind::ALL[i].name();
i += 1;
}
names
};
pub const fn name(self) -> &'static str {
match self {
DetectorKind::Mr => "mr",
DetectorKind::Zf => "zf",
DetectorKind::Mmse => "mmse",
DetectorKind::Sic => "sic",
DetectorKind::ZfSic => "zf_sic",
DetectorKind::Ml => "ml",
DetectorKind::Sphere => "sphere",
DetectorKind::KBest(_) => "kbest",
DetectorKind::Amp(_) => "amp",
DetectorKind::Oamp(_) => "oamp",
}
}
pub fn with_iterations(self, iterations: usize) -> Result<DetectorKind> {
let needs = matches!(self, DetectorKind::Amp(_) | DetectorKind::Oamp(_));
ensure(!needs || iterations >= 1, || format!("{self:?} needs at least one iteration"))?;
Ok(match self {
DetectorKind::Amp(_) => DetectorKind::Amp(iterations),
DetectorKind::Oamp(_) => DetectorKind::Oamp(iterations),
other => other,
})
}
pub fn from_name(name: &str, k: usize) -> Result<DetectorKind> {
Ok(match name.parse()? {
DetectorKind::KBest(_) => {
ensure(k >= 1, || format!("kbest needs at least one path, got k = {k}"))?;
DetectorKind::KBest(k)
}
other => other,
})
}
pub fn has_soft_output(self) -> bool {
Constellation::psk(2).and_then(|c| self.detector(&c)).is_ok_and(|d| d.has_soft_output())
}
pub fn detector(self, c: &Constellation) -> Result<Box<dyn MimoDetector + Send + Sync>> {
self.build(c.clone())
}
fn build(self, c: Constellation) -> Result<Box<dyn MimoDetector + Send + Sync>> {
Ok(match self {
DetectorKind::Mr => Box::new(Linear::mr(c)),
DetectorKind::Zf => Box::new(Linear::zf(c)),
DetectorKind::Mmse => Box::new(Linear::mmse(c)),
DetectorKind::Sic => Box::new(Sic::mmse(c)),
DetectorKind::ZfSic => Box::new(Sic::zf(c)),
DetectorKind::Ml => Box::new(Ml::new(c)),
DetectorKind::Sphere => Box::new(Sphere::new(c)),
DetectorKind::KBest(k) => Box::new(KBest::new(c, k)?),
DetectorKind::Amp(n) => Box::new(Amp::new(c, n)?),
DetectorKind::Oamp(n) => Box::new(Oamp::new(c, n)?),
})
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct SoftOutput {
pub symbols: Vec<C64>,
pub noise_var: Vec<f64>,
pub llr: Vec<f64>,
}
impl SoftOutput {
pub fn append(&mut self, e: Estimates) {
self.symbols.extend(e.symbols);
self.noise_var.extend(e.noise_var);
}
}
pub struct Receiver {
kind: DetectorKind,
detector: Box<dyn MimoDetector + Send + Sync>,
alamouti: Ostbc,
llr: bool,
}
impl std::fmt::Debug for Receiver {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Receiver").field("kind", &self.kind).field("llr", &self.llr).finish_non_exhaustive()
}
}
impl Receiver {
pub fn new(kind: DetectorKind, c: Constellation, llr: bool) -> Result<Receiver> {
let detector = kind.build(c)?;
ensure(!llr || detector.has_soft_output(), || {
format!("the {} detector has no soft output: demap its decisions instead", kind.name())
})?;
Ok(Receiver { kind, detector, alamouti: Ostbc::alamouti(), llr })
}
pub fn constellation(&self) -> &Constellation {
self.detector.constellation()
}
fn append_llr(&self, e: &Estimates, out: &mut SoftOutput) -> Result<()> {
let start = out.llr.len();
out.llr.resize(start + e.symbols.len() * self.constellation().bits_per_symbol(), 0.0);
self.constellation().llr_with_variances_into(
&e.symbols,
&e.noise_var,
None,
Jacobian::Exact,
&mut out.llr[start..],
)
}
pub fn layers(&self, h: &ArrayView2<'_, C64>, y: &[C64], noise_var: f64, out: &mut SoftOutput) -> Result<()> {
if let Some(e) = self.detector.soft_estimate(h, y, noise_var)? {
if self.llr {
self.append_llr(&e, out)?;
}
out.append(e);
} else {
let labels = if self.llr {
let (labels, llr) = self.detector.detect_llr(h, y, noise_var)?;
out.llr.extend(llr);
labels
} else {
self.detector.detect(h, y, noise_var)?
};
let points = self.constellation().points();
out.symbols.extend(labels.iter().map(|&i| points[i]));
out.noise_var.extend(std::iter::repeat_n(noise_var, labels.len()));
}
Ok(())
}
pub fn detect_uses<I, H, Y>(
&self,
uses: I,
alamouti: bool,
noise_var: f64,
mut channel: H,
mut received: Y,
out: &mut SoftOutput,
) -> Result<()>
where
I: IntoIterator<Item = usize>,
H: FnMut(usize) -> Result<CMat>,
Y: FnMut(usize) -> Vec<C64>,
{
let mut uses = uses.into_iter();
if !alamouti {
for i in uses {
self.layers(&channel(i)?.view(), &received(i), noise_var, out)?;
}
return Ok(());
}
while let Some(i0) = uses.next() {
let Some(i1) = uses.next() else {
return Err(Error::Shape("Alamouti's code needs an even number of channel uses".into()));
};
let (y0, y1) = (received(i0), received(i1));
let y = Array2::from_shape_fn((y0.len(), 2), |(r, j)| if j == 0 { y0[r] } else { y1[r] });
self.decode_alamouti(&channel(i0)?.view(), &y.view(), noise_var, out)?;
}
Ok(())
}
pub fn decode_alamouti(
&self,
h: &ArrayView2<'_, C64>,
y: &ArrayView2<'_, C64>,
noise_var: f64,
out: &mut SoftOutput,
) -> Result<()> {
let e = self.alamouti.decode(h, y, noise_var)?;
if self.llr {
self.append_llr(&e, out)?;
}
out.append(e);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use comnum_core::rng::Rng;
use comnum_math::linalg::matvec;
#[test]
fn the_soft_outputs_are_those_of_the_detectors() {
for kind in DetectorKind::ALL {
let former = !matches!(kind, DetectorKind::Sic | DetectorKind::ZfSic | DetectorKind::Sphere);
assert_eq!(kind.has_soft_output(), former, "{kind:?}");
}
assert!(!DetectorKind::KBest(0).has_soft_output() && DetectorKind::KBest(0).detector(&qpsk()).is_err());
assert!(Receiver::new(DetectorKind::Sphere, qpsk(), true).is_err());
}
fn qpsk() -> Constellation {
Constellation::qam(4).unwrap().normalized()
}
#[test]
fn amp_and_oamp_give_their_decoupled_estimates_and_their_llrs() {
let c = qpsk();
let mut rng = Rng::new(31);
let h = Array2::from_shape_fn((16, 8), |_| rng.complex_gaussian() * 0.25);
let x: Vec<C64> = (0..8).map(|_| c.points()[rng.below(4)]).collect();
let y: Vec<C64> = matvec(&h.view(), &x).iter().map(|v| v + rng.complex_gaussian() * 0.1).collect();
for kind in [DetectorKind::Amp(10), DetectorKind::Oamp(10)] {
let rx = Receiver::new(kind, c.clone(), true).unwrap();
let mut out = SoftOutput::default();
rx.layers(&h.view(), &y, 0.01, &mut out).unwrap();
let d = kind.detector(&c).unwrap();
let e = d.soft_estimate(&h.view(), &y, 0.01).unwrap().unwrap();
assert_eq!((out.symbols, out.noise_var), (e.symbols, e.noise_var));
let (_, llr) = d.detect_llr(&h.view(), &y, 0.01).unwrap();
assert!(out.llr.iter().zip(&llr).all(|(a, b)| a.to_bits() == b.to_bits()) && out.llr.len() == 16);
}
}
#[test]
fn the_stored_alamouti_code_decodes_as_a_new_one() {
let c = qpsk();
let mut rng = Rng::new(32);
let rx = Receiver::new(DetectorKind::Mmse, c.clone(), true).unwrap();
let code = Ostbc::alamouti();
let s: Vec<C64> = (0..2).map(|_| c.points()[rng.below(4)]).collect();
let h = Array2::from_shape_fn((2, 2), |_| rng.complex_gaussian());
let y = h.dot(&code.encode(&s).unwrap()).mapv(|v| v + rng.complex_gaussian() * 0.1);
let mut out = SoftOutput::default();
rx.decode_alamouti(&h.view(), &y.view(), 0.01, &mut out).unwrap();
let e = Ostbc::alamouti().decode_blocks(&[h], &y.view(), 0.01).unwrap();
assert_eq!((out.symbols.clone(), out.noise_var.clone()), (e.symbols.clone(), e.noise_var.clone()));
assert_eq!(out.llr, c.llr_with_variances(&e.symbols, &e.noise_var, None, Jacobian::Exact).unwrap());
}
}