Skip to main content

comnum_mimo/
receiver.rs

1//! The receiver of a link: one detector applied channel use after channel use, with
2//! soft outputs.
3//!
4//! [`Receiver`] detects the layers of every channel use $y = H x + w$ (spatial
5//! multiplexing) or every block of Alamouti's code, and accumulates a [`SoftOutput`]:
6//!
7//! - the estimate of every transmitted symbol: the estimate of the detector when it forms
8//!   one ([`MimoDetector::soft_estimate`]: the unbiased linear estimate of ZF and MMSE,
9//!   the decoupled channel of AMP and OAMP), the decided point with the other detectors;
10//! - its noise variance: that of the estimate, or the noise variance of the channel for
11//!   a decision;
12//! - optionally the bit LLRs of the detector (ZF, MMSE, ML, K-best, AMP, OAMP, Alamouti:
13//!   [`MimoDetector::has_soft_output`]).
14//!
15//! # Examples
16//! ```
17//! use comnum_mimo::receiver::{DetectorKind, Receiver, SoftOutput};
18//! use comnum_mapping::constellation::Constellation;
19//! use comnum_math::C64;
20//! use ndarray::array;
21//! let c = Constellation::qam(4)?.normalized();
22//! let rx = Receiver::new(DetectorKind::Ml, c.clone(), true)?;
23//! let h = array![[C64::new(1.0, 0.0), C64::new(0.2, 0.1)], [C64::new(-0.3, 0.0), C64::new(0.9, 0.0)]];
24//! let x = [c.points()[1], c.points()[2]];
25//! let y: Vec<C64> = (0..2).map(|r| h[[r, 0]] * x[0] + h[[r, 1]] * x[1]).collect();
26//! let mut out = SoftOutput::default();
27//! rx.layers(&h.view(), &y, 1e-3, &mut out)?;
28//! assert_eq!(out.symbols, x.to_vec());
29//! assert_eq!(out.llr.len(), 4);
30//! # Ok::<(), comnum_core::Error>(())
31//! ```
32
33use crate::detection::{Estimates, KBest, Linear, MimoDetector, Ml, Sic, Sphere};
34use crate::message_passing::{Amp, Oamp};
35use crate::stbc::Ostbc;
36use comnum_core::{Error, Result, ensure};
37use comnum_mapping::constellation::Constellation;
38use comnum_math::C64;
39use comnum_math::Jacobian;
40use comnum_math::linalg::CMat;
41use ndarray::{Array2, ArrayView2};
42
43/// A detector of [`crate::detection`].
44#[non_exhaustive]
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub enum DetectorKind {
47    /// Maximum ratio (matched filter).
48    Mr,
49    /// Zero forcing.
50    Zf,
51    /// Unbiased MMSE.
52    Mmse,
53    /// MMSE successive interference cancellation (V-BLAST).
54    Sic,
55    /// Zero-forcing successive interference cancellation.
56    ZfSic,
57    /// Exhaustive maximum likelihood.
58    Ml,
59    /// The sphere decoder (maximum likelihood).
60    Sphere,
61    /// K-best with this number of paths.
62    KBest(usize),
63    /// LAMA, the complex Bayesian AMP of large systems, with this number of iterations.
64    Amp(usize),
65    /// OAMP with the LMMSE estimator, with this number of iterations.
66    Oamp(usize),
67}
68
69impl std::str::FromStr for DetectorKind {
70    type Err = Error;
71
72    /// The detector of a name: `mr`, `zf`, `mmse`, `sic` (`mmse_sic`), `zf_sic`, `ml`,
73    /// `sphere`, `kbest` (with [`DetectorKind::DEFAULT_PATHS`] paths), `amp` (`lama`)
74    /// or `oamp` (with [`DetectorKind::DEFAULT_ITERATIONS`] iterations),
75    /// case-insensitive.
76    ///
77    /// # Errors
78    /// [`Error::Config`] for another name.
79    fn from_str(name: &str) -> Result<DetectorKind> {
80        let name = name.to_ascii_lowercase();
81        let canonical = match name.as_str() {
82            "mmse_sic" => "sic",
83            "lama" => "amp",
84            other => other,
85        };
86        DetectorKind::ALL
87            .into_iter()
88            .find(|d| d.name() == canonical)
89            .ok_or_else(|| Error::Config(format!("unknown detector {name:?}: {}", DetectorKind::NAMES.join(", "))))
90    }
91}
92
93impl DetectorKind {
94    /// The paths of K-best when its name does not give them.
95    pub const DEFAULT_PATHS: usize = 8;
96
97    /// The iterations of AMP and OAMP when their name does not give them.
98    pub const DEFAULT_ITERATIONS: usize = 10;
99
100    /// Every detector, K-best with [`DetectorKind::DEFAULT_PATHS`] paths and AMP and
101    /// OAMP with [`DetectorKind::DEFAULT_ITERATIONS`] iterations.
102    pub const ALL: [DetectorKind; 10] = [
103        DetectorKind::Mr,
104        DetectorKind::Zf,
105        DetectorKind::Mmse,
106        DetectorKind::Sic,
107        DetectorKind::ZfSic,
108        DetectorKind::Ml,
109        DetectorKind::Sphere,
110        DetectorKind::KBest(DetectorKind::DEFAULT_PATHS),
111        DetectorKind::Amp(DetectorKind::DEFAULT_ITERATIONS),
112        DetectorKind::Oamp(DetectorKind::DEFAULT_ITERATIONS),
113    ];
114
115    /// The names of [`DetectorKind::ALL`] ([`DetectorKind::name`]), in order: the list
116    /// of the detector names that the specifications take theirs from.
117    ///
118    /// # Examples
119    /// ```
120    /// use comnum_mimo::receiver::DetectorKind;
121    /// assert_eq!(DetectorKind::NAMES.len(), DetectorKind::ALL.len());
122    /// for (name, d) in DetectorKind::NAMES.iter().zip(DetectorKind::ALL) {
123    ///     assert_eq!((d.name(), name.parse::<DetectorKind>()?), (*name, d));
124    /// }
125    /// # Ok::<(), comnum_core::Error>(())
126    /// ```
127    pub const NAMES: [&'static str; 10] = {
128        let mut names = [""; 10];
129        let mut i = 0;
130        while i < names.len() {
131            names[i] = DetectorKind::ALL[i].name();
132            i += 1;
133        }
134        names
135    };
136
137    /// The name of the detector, as [`DetectorKind::from_str`](std::str::FromStr::from_str)
138    /// reads it.
139    pub const fn name(self) -> &'static str {
140        match self {
141            DetectorKind::Mr => "mr",
142            DetectorKind::Zf => "zf",
143            DetectorKind::Mmse => "mmse",
144            DetectorKind::Sic => "sic",
145            DetectorKind::ZfSic => "zf_sic",
146            DetectorKind::Ml => "ml",
147            DetectorKind::Sphere => "sphere",
148            DetectorKind::KBest(_) => "kbest",
149            DetectorKind::Amp(_) => "amp",
150            DetectorKind::Oamp(_) => "oamp",
151        }
152    }
153
154    /// The same detector with `iterations` iterations, for AMP and OAMP (the others are
155    /// returned unchanged).
156    ///
157    /// # Errors
158    /// [`Error::Config`] for zero iterations of AMP or OAMP.
159    pub fn with_iterations(self, iterations: usize) -> Result<DetectorKind> {
160        let needs = matches!(self, DetectorKind::Amp(_) | DetectorKind::Oamp(_));
161        ensure(!needs || iterations >= 1, || format!("{self:?} needs at least one iteration"))?;
162        Ok(match self {
163            DetectorKind::Amp(_) => DetectorKind::Amp(iterations),
164            DetectorKind::Oamp(_) => DetectorKind::Oamp(iterations),
165            other => other,
166        })
167    }
168
169    /// The detector of a name ([`DetectorKind::from_str`](std::str::FromStr::from_str)),
170    /// K-best with `k` paths.
171    ///
172    /// # Errors
173    /// [`Error::Config`] for another name or zero paths.
174    pub fn from_name(name: &str, k: usize) -> Result<DetectorKind> {
175        Ok(match name.parse()? {
176            DetectorKind::KBest(_) => {
177                ensure(k >= 1, || format!("kbest needs at least one path, got k = {k}"))?;
178                DetectorKind::KBest(k)
179            }
180            other => other,
181        })
182    }
183
184    /// Whether the detector computes bit LLRs ([`MimoDetector::has_soft_output`] of its
185    /// detector; false for a detector that cannot be built, K-best with no path).
186    pub fn has_soft_output(self) -> bool {
187        Constellation::psk(2).and_then(|c| self.detector(&c)).is_ok_and(|d| d.has_soft_output())
188    }
189
190    /// The detector for the alphabet `c`.
191    ///
192    /// # Errors
193    /// [`Error::Config`] for K-best with no path or AMP and OAMP with no iteration.
194    pub fn detector(self, c: &Constellation) -> Result<Box<dyn MimoDetector + Send + Sync>> {
195        self.build(c.clone())
196    }
197
198    /// [`DetectorKind::detector`] with the alphabet `c` moved into the detector.
199    fn build(self, c: Constellation) -> Result<Box<dyn MimoDetector + Send + Sync>> {
200        Ok(match self {
201            DetectorKind::Mr => Box::new(Linear::mr(c)),
202            DetectorKind::Zf => Box::new(Linear::zf(c)),
203            DetectorKind::Mmse => Box::new(Linear::mmse(c)),
204            DetectorKind::Sic => Box::new(Sic::mmse(c)),
205            DetectorKind::ZfSic => Box::new(Sic::zf(c)),
206            DetectorKind::Ml => Box::new(Ml::new(c)),
207            DetectorKind::Sphere => Box::new(Sphere::new(c)),
208            DetectorKind::KBest(k) => Box::new(KBest::new(c, k)?),
209            DetectorKind::Amp(n) => Box::new(Amp::new(c, n)?),
210            DetectorKind::Oamp(n) => Box::new(Oamp::new(c, n)?),
211        })
212    }
213}
214
215/// The accumulated output of a [`Receiver`]: the [`Estimates`] of the channel uses one
216/// after the other (the same fields), and their bit LLRs.
217#[derive(Clone, Debug, Default, PartialEq)]
218pub struct SoftOutput {
219    /// The estimate of every symbol.
220    pub symbols: Vec<C64>,
221    /// The noise variance of every estimate.
222    pub noise_var: Vec<f64>,
223    /// The bit LLRs (positive: bit 0), when asked.
224    pub llr: Vec<f64>,
225}
226
227impl SoftOutput {
228    /// Append the estimates `e` of one channel use.
229    pub fn append(&mut self, e: Estimates) {
230        self.symbols.extend(e.symbols);
231        self.noise_var.extend(e.noise_var);
232    }
233}
234
235/// A detector applied channel use after channel use (see the module).
236pub struct Receiver {
237    kind: DetectorKind,
238    detector: Box<dyn MimoDetector + Send + Sync>,
239    /// Alamouti's code, built (and checked) once.
240    alamouti: Ostbc,
241    llr: bool,
242}
243
244impl std::fmt::Debug for Receiver {
245    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
246        f.debug_struct("Receiver").field("kind", &self.kind).field("llr", &self.llr).finish_non_exhaustive()
247    }
248}
249
250impl Receiver {
251    /// The receiver of the detector `kind` for the alphabet `c`, with bit LLRs if `llr`.
252    ///
253    /// # Errors
254    /// [`Error::Config`] when LLRs are asked from a detector without soft output, or for
255    /// a detector that cannot be built ([`DetectorKind::detector`]).
256    pub fn new(kind: DetectorKind, c: Constellation, llr: bool) -> Result<Receiver> {
257        let detector = kind.build(c)?;
258        ensure(!llr || detector.has_soft_output(), || {
259            format!("the {} detector has no soft output: demap its decisions instead", kind.name())
260        })?;
261        Ok(Receiver { kind, detector, alamouti: Ostbc::alamouti(), llr })
262    }
263
264    /// The alphabet of every stream.
265    pub fn constellation(&self) -> &Constellation {
266        self.detector.constellation()
267    }
268
269    /// Append the bit LLRs of the estimates `e` (one noise variance each) to `out`.
270    fn append_llr(&self, e: &Estimates, out: &mut SoftOutput) -> Result<()> {
271        let start = out.llr.len();
272        out.llr.resize(start + e.symbols.len() * self.constellation().bits_per_symbol(), 0.0);
273        self.constellation().llr_with_variances_into(
274            &e.symbols,
275            &e.noise_var,
276            None,
277            Jacobian::Exact,
278            &mut out.llr[start..],
279        )
280    }
281
282    /// Detect the layers of one channel use $y = H x + w$ and append them to `out`.
283    ///
284    /// # Errors
285    /// The errors of the detector.
286    pub fn layers(&self, h: &ArrayView2<'_, C64>, y: &[C64], noise_var: f64, out: &mut SoftOutput) -> Result<()> {
287        if let Some(e) = self.detector.soft_estimate(h, y, noise_var)? {
288            // the LLRs of a detector that estimates come from its estimates: one pass
289            if self.llr {
290                self.append_llr(&e, out)?;
291            }
292            out.append(e);
293        } else {
294            // the decisions and the LLRs of one search
295            let labels = if self.llr {
296                let (labels, llr) = self.detector.detect_llr(h, y, noise_var)?;
297                out.llr.extend(llr);
298                labels
299            } else {
300                self.detector.detect(h, y, noise_var)?
301            };
302            let points = self.constellation().points();
303            out.symbols.extend(labels.iter().map(|&i| points[i]));
304            out.noise_var.extend(std::iter::repeat_n(noise_var, labels.len()));
305        }
306        Ok(())
307    }
308
309    /// Detect the channel uses `uses` in turn and append them to `out`: use `i` has the
310    /// channel `channel(i)` ($n_{r} \times n_{t}$) and the received vector
311    /// `received(i)` ($n_{r}$); with `alamouti`, the pairs of consecutive uses carry the
312    /// blocks of Alamouti's code, detected with the channel of the first use of the pair
313    /// ([`Receiver::decode_alamouti`]), otherwise every use carries one vector of layers
314    /// ([`Receiver::layers`]).
315    ///
316    /// # Errors
317    /// The errors of `channel` and of the detector; [`Error::Shape`] for an odd number of
318    /// uses with Alamouti's code.
319    ///
320    /// # Examples
321    /// ```
322    /// use comnum_mimo::receiver::{DetectorKind, Receiver, SoftOutput};
323    /// use comnum_mapping::constellation::Constellation;
324    /// use comnum_math::{C64, linalg::identity};
325    /// let c = Constellation::qam(4)?.normalized();
326    /// let rx = Receiver::new(DetectorKind::Zf, c.clone(), false)?;
327    /// let x: Vec<Vec<C64>> = (0..3).map(|i| vec![c.points()[i], c.points()[3 - i]]).collect();
328    /// let mut out = SoftOutput::default();
329    /// rx.detect_uses(0..3, false, 1e-3, |_| Ok(identity(2)), |i| x[i].clone(), &mut out)?;
330    /// assert!(out.symbols.iter().zip(x.concat()).all(|(a, b)| (a - b).norm() < 1e-12));
331    /// # Ok::<(), comnum_core::Error>(())
332    /// ```
333    pub fn detect_uses<I, H, Y>(
334        &self,
335        uses: I,
336        alamouti: bool,
337        noise_var: f64,
338        mut channel: H,
339        mut received: Y,
340        out: &mut SoftOutput,
341    ) -> Result<()>
342    where
343        I: IntoIterator<Item = usize>,
344        H: FnMut(usize) -> Result<CMat>,
345        Y: FnMut(usize) -> Vec<C64>,
346    {
347        let mut uses = uses.into_iter();
348        if !alamouti {
349            for i in uses {
350                self.layers(&channel(i)?.view(), &received(i), noise_var, out)?;
351            }
352            return Ok(());
353        }
354        while let Some(i0) = uses.next() {
355            let Some(i1) = uses.next() else {
356                return Err(Error::Shape("Alamouti's code needs an even number of channel uses".into()));
357            };
358            let (y0, y1) = (received(i0), received(i1));
359            let y = Array2::from_shape_fn((y0.len(), 2), |(r, j)| if j == 0 { y0[r] } else { y1[r] });
360            self.decode_alamouti(&channel(i0)?.view(), &y.view(), noise_var, out)?;
361        }
362        Ok(())
363    }
364
365    /// Decode one block of Alamouti's code (the two channel uses are the columns of `y`,
366    /// $n_{r} \times 2$) and append its two symbols to `out`.
367    ///
368    /// # Errors
369    /// The errors of [`Ostbc::decode`].
370    pub fn decode_alamouti(
371        &self,
372        h: &ArrayView2<'_, C64>,
373        y: &ArrayView2<'_, C64>,
374        noise_var: f64,
375        out: &mut SoftOutput,
376    ) -> Result<()> {
377        let e = self.alamouti.decode(h, y, noise_var)?;
378        if self.llr {
379            self.append_llr(&e, out)?;
380        }
381        out.append(e);
382        Ok(())
383    }
384}
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389    use comnum_core::rng::Rng;
390    use comnum_math::linalg::matvec;
391
392    #[test]
393    fn the_soft_outputs_are_those_of_the_detectors() {
394        // the list that Receiver::new kept by hand before the trait method
395        for kind in DetectorKind::ALL {
396            let former = !matches!(kind, DetectorKind::Sic | DetectorKind::ZfSic | DetectorKind::Sphere);
397            assert_eq!(kind.has_soft_output(), former, "{kind:?}");
398        }
399        assert!(!DetectorKind::KBest(0).has_soft_output() && DetectorKind::KBest(0).detector(&qpsk()).is_err());
400        assert!(Receiver::new(DetectorKind::Sphere, qpsk(), true).is_err());
401    }
402
403    fn qpsk() -> Constellation {
404        Constellation::qam(4).unwrap().normalized()
405    }
406
407    #[test]
408    fn amp_and_oamp_give_their_decoupled_estimates_and_their_llrs() {
409        let c = qpsk();
410        let mut rng = Rng::new(31);
411        let h = Array2::from_shape_fn((16, 8), |_| rng.complex_gaussian() * 0.25);
412        let x: Vec<C64> = (0..8).map(|_| c.points()[rng.below(4)]).collect();
413        let y: Vec<C64> = matvec(&h.view(), &x).iter().map(|v| v + rng.complex_gaussian() * 0.1).collect();
414        for kind in [DetectorKind::Amp(10), DetectorKind::Oamp(10)] {
415            let rx = Receiver::new(kind, c.clone(), true).unwrap();
416            let mut out = SoftOutput::default();
417            rx.layers(&h.view(), &y, 0.01, &mut out).unwrap();
418            let d = kind.detector(&c).unwrap();
419            let e = d.soft_estimate(&h.view(), &y, 0.01).unwrap().unwrap();
420            assert_eq!((out.symbols, out.noise_var), (e.symbols, e.noise_var));
421            // the LLRs of the former path: those of the joint detection and decision
422            let (_, llr) = d.detect_llr(&h.view(), &y, 0.01).unwrap();
423            assert!(out.llr.iter().zip(&llr).all(|(a, b)| a.to_bits() == b.to_bits()) && out.llr.len() == 16);
424        }
425    }
426
427    #[test]
428    fn the_stored_alamouti_code_decodes_as_a_new_one() {
429        let c = qpsk();
430        let mut rng = Rng::new(32);
431        let rx = Receiver::new(DetectorKind::Mmse, c.clone(), true).unwrap();
432        let code = Ostbc::alamouti();
433        let s: Vec<C64> = (0..2).map(|_| c.points()[rng.below(4)]).collect();
434        let h = Array2::from_shape_fn((2, 2), |_| rng.complex_gaussian());
435        let y = h.dot(&code.encode(&s).unwrap()).mapv(|v| v + rng.complex_gaussian() * 0.1);
436        let mut out = SoftOutput::default();
437        rx.decode_alamouti(&h.view(), &y.view(), 0.01, &mut out).unwrap();
438        let e = Ostbc::alamouti().decode_blocks(&[h], &y.view(), 0.01).unwrap();
439        assert_eq!((out.symbols.clone(), out.noise_var.clone()), (e.symbols.clone(), e.noise_var.clone()));
440        assert_eq!(out.llr, c.llr_with_variances(&e.symbols, &e.noise_var, None, Jacobian::Exact).unwrap());
441    }
442}