Skip to main content

comnum_core/
signal.rs

1//! Multi-stream containers: bits, log-likelihood ratios, symbols, labels and waveforms.
2//!
3//! Every container is two-dimensional, streams × elements, in standard (row-major)
4//! layout; SISO is `n_streams == 1`. MIMO, polarisation multiplexing and space-division
5//! multiplexing therefore reuse the same blocks. [`DualPol`], the two polarisations of
6//! an optical field as two vectors, is kept for the slice-based optical blocks.
7//!
8//! # Model
9//!
10//! The sign convention of the log-likelihood ratios is fixed library-wide: a positive
11//! LLR favours the bit 0,
12//!
13//! $$
14//! L(b) = \ln \frac{P(b = 0 \mid y)}{P(b = 1 \mid y)}
15//! $$
16//!
17//! # Examples
18//! ```
19//! use comnum_core::signal::{Bits, Signal};
20//! use comnum_core::units::Hertz;
21//! let b = Bits::from_stream(vec![0, 1, 1, 0]).unwrap();
22//! assert_eq!((b.n_streams(), b.len()), (1, 4));
23//! let s = Signal::zeros(2, 1024, Hertz(64e9));
24//! assert_eq!((s.n_streams(), s.len()), (2, 1024));
25//! assert_eq!(s.stream(1).len(), 1024);
26//! ```
27
28use crate::C64;
29use crate::error::{Error, Result, expect_len, same_len};
30use crate::units::Hertz;
31use ndarray::{Array2, ArrayView1, ArrayViewMut1};
32
33/// The magnitude of the LLR of a bit taken as known: $+30$ for a known 0 (a filler or
34/// shortened bit of a rate matching), $\pm 30$ for a decision whose counter-hypothesis
35/// left a tree search; $e^{-30} \approx 10^{-13}$, a probability no channel output
36/// reaches, and small enough for the check-node products of the decoders.
37pub const KNOWN_BIT_LLR: f64 = 30.0;
38
39/// The hard decisions of log-likelihood ratios: a negative LLR is a 1, any other a 0
40/// (see the module for the sign convention).
41///
42/// # Examples
43/// ```
44/// use comnum_core::signal::hard_decisions;
45/// assert_eq!(hard_decisions(&[2.0, -0.5, 0.0]), vec![0, 1, 0]);
46/// ```
47pub fn hard_decisions(llr: &[f64]) -> Vec<u8> {
48    llr.iter().map(|l| u8::from(*l < 0.0)).collect()
49}
50
51macro_rules! container {
52    ($(#[$doc:meta])* $name:ident, $elem:ty, $zero:expr) => {
53        $(#[$doc])*
54        #[derive(Clone, Debug, PartialEq, Default)]
55        pub struct $name(Array2<$elem>);
56
57        impl $name {
58            /// A container from a `[n_streams, n]` array.
59            pub fn new(data: Array2<$elem>) -> $name {
60                $name(data.as_standard_layout().into_owned())
61            }
62
63            /// `n_streams` streams of `n` zeros.
64            pub fn zeros(n_streams: usize, n: usize) -> $name {
65                $name(Array2::from_elem((n_streams, n), $zero))
66            }
67
68            /// One stream.
69            ///
70            /// # Errors
71            /// Never in practice; the signature matches [`Self::from_streams`].
72            pub fn from_stream(v: Vec<$elem>) -> Result<$name> {
73                let n = v.len();
74                Array2::from_shape_vec((1, n), v)
75                    .map($name)
76                    .map_err(|e| Error::Shape(e.to_string()))
77            }
78
79            /// Several streams of equal length.
80            ///
81            /// # Errors
82            /// [`Error::Shape`] if the streams have different lengths.
83            pub fn from_streams(streams: &[Vec<$elem>]) -> Result<$name> {
84                let n = streams.first().map_or(0, Vec::len);
85                for s in streams {
86                    same_len(s.len(), n, "streams")?;
87                }
88                let flat: Vec<$elem> = streams.iter().flatten().copied().collect();
89                Array2::from_shape_vec((streams.len(), n), flat)
90                    .map($name)
91                    .map_err(|e| Error::Shape(e.to_string()))
92            }
93
94            /// Number of streams.
95            pub fn n_streams(&self) -> usize {
96                self.0.nrows()
97            }
98
99            /// Number of elements per stream.
100            pub fn len(&self) -> usize {
101                self.0.ncols()
102            }
103
104            /// Whether the streams are empty.
105            pub fn is_empty(&self) -> bool {
106                self.0.is_empty()
107            }
108
109            /// Stream `i` as a slice (the layout is always standard).
110            pub fn stream(&self, i: usize) -> &[$elem] {
111                let n = self.len();
112                &self.as_slice()[i * n..(i + 1) * n]
113            }
114
115            /// Stream `i` as an `ndarray` view.
116            pub fn stream_view(&self, i: usize) -> ArrayView1<'_, $elem> {
117                self.0.row(i)
118            }
119
120            /// Stream `i` as a mutable `ndarray` view.
121            pub fn stream_view_mut(&mut self, i: usize) -> ArrayViewMut1<'_, $elem> {
122                self.0.row_mut(i)
123            }
124
125            /// All the elements, stream after stream.
126            pub fn as_slice(&self) -> &[$elem] {
127                self.0.as_slice().unwrap_or(&[])
128            }
129
130            /// Make `self` the single stream `x`, reusing its storage when the length is
131            /// unchanged (ADR 0011).
132            pub fn set_stream(&mut self, x: &[$elem]) {
133                self.resize(1, x.len());
134                self.as_slice_mut().copy_from_slice(x);
135            }
136
137            /// Make `self` a copy of `a`, reusing its storage when the shape is
138            /// unchanged (ADR 0011).
139            pub fn assign(&mut self, a: &Array2<$elem>) {
140                self.resize(a.nrows(), a.ncols());
141                self.0.assign(a);
142            }
143
144            /// Stream `i` as a mutable slice.
145            pub fn stream_mut(&mut self, i: usize) -> &mut [$elem] {
146                let n = self.len();
147                &mut self.as_slice_mut()[i * n..(i + 1) * n]
148            }
149
150            /// All the elements, stream after stream, mutable.
151            pub fn as_slice_mut(&mut self) -> &mut [$elem] {
152                self.0.as_slice_mut().unwrap_or(&mut [])
153            }
154
155            /// The underlying `[n_streams, n]` array.
156            pub fn as_array(&self) -> &Array2<$elem> {
157                &self.0
158            }
159
160            /// The underlying array, mutable.
161            pub fn as_array_mut(&mut self) -> &mut Array2<$elem> {
162                &mut self.0
163            }
164
165            /// Resize to `n_streams × n` (the content is reset when the shape changes),
166            /// so that an `_into` method can reuse a caller-owned buffer.
167            pub fn resize(&mut self, n_streams: usize, n: usize) {
168                if self.0.dim() != (n_streams, n) {
169                    self.0 = Array2::from_elem((n_streams, n), $zero);
170                }
171            }
172        }
173    };
174}
175
176container!(
177    /// Hard bits, one row per stream; the values are 0 or 1.
178    Bits, u8, 0
179);
180container!(
181    /// Log-likelihood ratios, one row per stream (positive favours 0, see the module
182    /// documentation).
183    Llr, f64, 0.0
184);
185container!(
186    /// Complex symbols at one sample per symbol, one row per stream.
187    Symbols, C64, C64::new(0.0, 0.0)
188);
189container!(
190    /// The labels of symbols, one row per stream: the index $a \in \\{0, \dots, M - 1\\}$
191    /// of every symbol of an alphabet of $M$ (a chirp of chirp spread spectrum, a level of
192    /// continuous-phase modulation, a point of a constellation), the input and the
193    /// decisions of the waveforms that carry an index (`Modulate<Labels>`).
194    Labels, usize, 0
195);
196
197/// A complex baseband waveform, `[n_streams, n_samples]`, with its sample rate.
198#[derive(Clone, Debug, PartialEq)]
199pub struct Signal {
200    data: Symbols,
201    sample_rate: Hertz,
202}
203
204impl Signal {
205    /// A waveform from its samples (`[n_streams, n_samples]`) and its sample rate.
206    pub fn new(data: Array2<C64>, sample_rate: Hertz) -> Signal {
207        Signal { data: Symbols::new(data), sample_rate }
208    }
209
210    /// `n_streams` streams of `n_samples` zeros.
211    pub fn zeros(n_streams: usize, n_samples: usize, sample_rate: Hertz) -> Signal {
212        Signal { data: Symbols::zeros(n_streams, n_samples), sample_rate }
213    }
214
215    /// A single-stream waveform.
216    ///
217    /// # Errors
218    /// Never in practice (see [`Symbols::from_stream`]).
219    pub fn from_stream(x: Vec<C64>, sample_rate: Hertz) -> Result<Signal> {
220        Ok(Signal { data: Symbols::from_stream(x)?, sample_rate })
221    }
222
223    /// A waveform from several streams of equal length.
224    ///
225    /// # Errors
226    /// [`Error::Shape`] if the streams have different lengths.
227    pub fn from_streams(streams: &[Vec<C64>], sample_rate: Hertz) -> Result<Signal> {
228        Ok(Signal { data: Symbols::from_streams(streams)?, sample_rate })
229    }
230
231    /// Sample rate.
232    pub fn sample_rate(&self) -> Hertz {
233        self.sample_rate
234    }
235
236    /// Number of streams.
237    pub fn n_streams(&self) -> usize {
238        self.data.n_streams()
239    }
240
241    /// Number of samples per stream.
242    pub fn len(&self) -> usize {
243        self.data.len()
244    }
245
246    /// Whether the streams are empty.
247    pub fn is_empty(&self) -> bool {
248        self.data.is_empty()
249    }
250
251    /// Stream `i` as a slice.
252    pub fn stream(&self, i: usize) -> &[C64] {
253        self.data.stream(i)
254    }
255
256    /// Stream `i` as an `ndarray` view.
257    pub fn stream_view(&self, i: usize) -> ArrayView1<'_, C64> {
258        self.data.stream_view(i)
259    }
260
261    /// Stream `i` as a mutable slice.
262    pub fn stream_mut(&mut self, i: usize) -> &mut [C64] {
263        self.data.stream_mut(i)
264    }
265
266    /// The streams, one vector each (the inverse of [`Signal::from_streams`]).
267    ///
268    /// # Examples
269    /// ```
270    /// use comnum_core::signal::Signal;
271    /// use comnum_core::units::Hertz;
272    /// use num_complex::Complex64 as C64;
273    /// let streams = vec![vec![C64::new(1.0, 0.0); 3], vec![C64::new(0.0, 1.0); 3]];
274    /// let x = Signal::from_streams(&streams, Hertz(1.0))?;
275    /// assert_eq!(x.to_streams(), streams);
276    /// # Ok::<(), comnum_core::Error>(())
277    /// ```
278    pub fn to_streams(&self) -> Vec<Vec<C64>> {
279        (0..self.n_streams()).map(|i| self.stream(i).to_vec()).collect()
280    }
281
282    /// The samples, `[n_streams, n_samples]`.
283    pub fn as_array(&self) -> &Array2<C64> {
284        self.data.as_array()
285    }
286
287    /// The samples, mutable.
288    pub fn as_array_mut(&mut self) -> &mut Array2<C64> {
289        self.data.as_array_mut()
290    }
291
292    /// Resize to `n_streams × n_samples` at `sample_rate` (the content is reset when the
293    /// shape changes), for the `_into` methods.
294    pub fn resize(&mut self, n_streams: usize, n_samples: usize, sample_rate: Hertz) {
295        self.data.resize(n_streams, n_samples);
296        self.sample_rate = sample_rate;
297    }
298
299    /// Make `self` the single stream `x` at `sample_rate`, reusing its storage when the
300    /// length is unchanged (ADR 0011).
301    pub fn set_stream(&mut self, x: &[C64], sample_rate: Hertz) {
302        self.resize(1, x.len(), sample_rate);
303        self.data.as_slice_mut().copy_from_slice(x);
304    }
305
306    /// Make `self` a copy of `other`, reusing its storage when the shape is unchanged
307    /// (ADR 0011).
308    pub fn assign(&mut self, other: &Signal) {
309        self.data.assign(other.data.as_array());
310        self.sample_rate = other.sample_rate;
311    }
312
313    /// Make `self` the streams `streams` at `sample_rate`, reusing its storage when the
314    /// shape is unchanged (ADR 0011).
315    ///
316    /// # Errors
317    /// [`Error::Shape`] if the streams have different lengths (`self` is then left
318    /// unchanged).
319    pub fn set_streams(&mut self, streams: &[Vec<C64>], sample_rate: Hertz) -> Result<()> {
320        let n = streams.first().map_or(0, Vec::len);
321        for s in streams {
322            same_len(s.len(), n, "streams")?;
323        }
324        self.resize(streams.len(), n, sample_rate);
325        for (i, x) in streams.iter().enumerate() {
326            self.data.stream_mut(i).copy_from_slice(x);
327        }
328        Ok(())
329    }
330
331    /// Fill `out` with `f(stream index, stream)` for every stream of `self`, at the
332    /// same shape and sampling rate: the body of the `_into` methods of the blocks that
333    /// process the streams independently.
334    ///
335    /// # Errors
336    /// The first error of `f`; [`Error::Shape`] if `f` returns another number of
337    /// samples than it was given, rather than leaving the samples of an earlier use of
338    /// `out` in place.
339    pub fn try_map_streams(
340        &self,
341        out: &mut Signal,
342        mut f: impl FnMut(usize, &[C64]) -> Result<Vec<C64>>,
343    ) -> Result<()> {
344        out.resize(self.n_streams(), self.len(), self.sample_rate());
345        for s in 0..self.n_streams() {
346            let y = f(s, self.stream(s))?;
347            expect_len(y.len(), self.len(), "samples returned")?;
348            for (o, v) in out.as_array_mut().row_mut(s).iter_mut().zip(y) {
349                *o = v;
350            }
351        }
352        Ok(())
353    }
354
355    /// [`Signal::try_map_streams`] for an `f` that cannot fail.
356    ///
357    /// # Panics
358    /// If `f` returns another number of samples than it was given (a broken invariant
359    /// of the calling block).
360    pub fn map_streams(&self, out: &mut Signal, mut f: impl FnMut(usize, &[C64]) -> Vec<C64>) {
361        if let Err(e) = self.try_map_streams(out, |s, x| Ok(f(s, x))) {
362            panic!("map_streams: {e}");
363        }
364    }
365
366    /// Make `out` a copy of `self` (its storage reused when the shape is unchanged, ADR
367    /// 0011), then apply `f(stream index, stream)` to every stream of `out` in place: the
368    /// body of the `_into` methods of the blocks that transform the samples where they
369    /// are, with no allocation per stream.
370    ///
371    /// # Examples
372    /// ```
373    /// use comnum_core::signal::Signal;
374    /// use comnum_core::units::Hertz;
375    /// use num_complex::Complex64 as C64;
376    /// let x = Signal::from_streams(&[vec![C64::new(1.0, 0.0); 3], vec![C64::new(0.0, 1.0); 3]], Hertz(1.0))?;
377    /// let mut out = Signal::zeros(0, 0, Hertz(1.0));
378    /// x.map_streams_in_place(&mut out, |s, y| y.iter_mut().for_each(|v| *v *= (s + 1) as f64));
379    /// assert_eq!(out.stream(1), &[C64::new(0.0, 2.0); 3]);
380    /// # Ok::<(), comnum_core::Error>(())
381    /// ```
382    pub fn map_streams_in_place(&self, out: &mut Signal, mut f: impl FnMut(usize, &mut [C64])) {
383        out.assign(self);
384        for s in 0..out.n_streams() {
385            f(s, out.data.stream_mut(s));
386        }
387    }
388
389    /// Mean power of every stream, `mean(|x|²)`.
390    pub fn power(&self) -> Vec<f64> {
391        let n = self.len().max(1) as f64;
392        (0..self.n_streams()).map(|i| self.stream(i).iter().map(C64::norm_sqr).sum::<f64>() / n).collect()
393    }
394}
395
396/// A field in two polarisations, `x` and `y`, of equal length.
397#[derive(Clone, Debug, PartialEq)]
398pub struct DualPol {
399    /// Polarisation x.
400    pub x: Vec<C64>,
401    /// Polarisation y.
402    pub y: Vec<C64>,
403}
404
405impl DualPol {
406    /// Two polarisations of equal length.
407    ///
408    /// # Errors
409    /// [`Error::Shape`] if the lengths differ.
410    pub fn new(x: Vec<C64>, y: Vec<C64>) -> Result<DualPol> {
411        same_len(x.len(), y.len(), "polarisations")?;
412        Ok(DualPol { x, y })
413    }
414
415    /// Number of samples per polarisation.
416    pub fn len(&self) -> usize {
417        self.x.len()
418    }
419
420    /// Whether the field is empty.
421    pub fn is_empty(&self) -> bool {
422        self.x.is_empty()
423    }
424
425    /// Mean total power `mean(|x|² + |y|²)`.
426    pub fn power(&self) -> f64 {
427        let n = self.len().max(1) as f64;
428        self.x.iter().chain(&self.y).map(C64::norm_sqr).sum::<f64>() / n
429    }
430
431    /// Multiply both polarisations by `s`.
432    pub fn scale(&mut self, s: f64) {
433        for v in self.x.iter_mut().chain(self.y.iter_mut()) {
434            *v *= s;
435        }
436    }
437
438    /// The two polarisations as a two-stream [`Signal`].
439    ///
440    /// # Errors
441    /// Never for a valid `DualPol`.
442    pub fn to_signal(&self, sample_rate: Hertz) -> Result<Signal> {
443        Signal::from_streams(&[self.x.clone(), self.y.clone()], sample_rate)
444    }
445
446    /// The field of a two-stream [`Signal`]: `x` its first stream, `y` its second (the
447    /// inverse of [`DualPol::to_signal`]).
448    ///
449    /// # Errors
450    /// [`Error::Shape`] unless the signal has exactly two streams.
451    pub fn from_signal(s: &Signal) -> Result<DualPol> {
452        same_len(s.n_streams(), 2, "streams of a dual-polarisation field")?;
453        Ok(DualPol { x: s.stream(0).to_vec(), y: s.stream(1).to_vec() })
454    }
455}
456
457#[cfg(test)]
458mod tests {
459    use super::*;
460
461    #[test]
462    fn the_setters_reuse_the_storage_of_an_unchanged_shape() {
463        let mut out = Signal::zeros(1, 4, Hertz(1.0));
464        let p = out.as_array().as_ptr();
465        out.set_stream(&[C64::new(1.0, 0.0); 4], Hertz(2.0));
466        assert_eq!((out.as_array().as_ptr(), out.sample_rate()), (p, Hertz(2.0)));
467        out.set_streams(&[vec![C64::new(0.0, 1.0); 2], vec![C64::new(0.0, 2.0); 2]], Hertz(2.0)).unwrap();
468        assert_eq!(out.stream(1), &[C64::new(0.0, 2.0); 2]);
469        assert!(out.set_streams(&[vec![C64::new(0.0, 1.0); 2], vec![]], Hertz(2.0)).is_err());
470        let mut b = Bits::zeros(2, 3);
471        let p = b.as_slice().as_ptr();
472        b.assign(&Array2::from_elem((2, 3), 1));
473        b.stream_mut(1)[0] = 0;
474        assert_eq!((b.as_slice().as_ptr(), b.as_slice()), (p, &[1, 1, 1, 0, 1, 1][..]));
475    }
476
477    #[test]
478    fn streams_and_reshape() {
479        let s = Signal::from_streams(&[vec![C64::new(1.0, 0.0); 3], vec![C64::new(0.0, 2.0); 3]], Hertz(1.0)).unwrap();
480        assert_eq!(s.power(), vec![1.0, 4.0]);
481        assert!(Symbols::from_streams(&[vec![C64::new(0.0, 0.0); 2], vec![]]).is_err());
482        let mut b = Bits::zeros(1, 2);
483        b.resize(2, 5);
484        assert_eq!((b.n_streams(), b.len(), b.as_slice().len()), (2, 5, 10));
485        let d = DualPol::from_signal(&s).unwrap();
486        assert_eq!(d.to_signal(Hertz(1.0)).unwrap(), s);
487        assert!((d.power() - 5.0).abs() < 1e-12);
488    }
489
490    #[test]
491    fn try_map_streams_rejects_a_short_stream_instead_of_keeping_stale_samples() {
492        let x = Signal::from_streams(&[vec![C64::new(1.0, 0.0); 4]], Hertz(1.0)).unwrap();
493        // a reused buffer of the same shape, full of samples of an earlier block
494        let mut out = Signal::from_streams(&[vec![C64::new(9.0, 9.0); 4]], Hertz(1.0)).unwrap();
495        let err = x.try_map_streams(&mut out, |_, s| Ok(s[..3].to_vec())).unwrap_err();
496        assert!(matches!(err, Error::Shape(_)), "{err}");
497        assert!(x.try_map_streams(&mut out, |_, s| Ok([s, s].concat())).is_err());
498        x.try_map_streams(&mut out, |_, s| Ok(s.iter().map(|v| v * 2.0).collect())).unwrap();
499        assert_eq!(out.stream(0), &[C64::new(2.0, 0.0); 4]);
500    }
501
502    #[test]
503    fn map_streams_in_place_reuses_the_output_and_keeps_the_rate() {
504        let x = Signal::from_streams(&[vec![C64::new(1.0, 0.0); 4], vec![C64::new(0.0, 1.0); 4]], Hertz(3.0)).unwrap();
505        let mut out = Signal::zeros(2, 4, Hertz(1.0));
506        let p = out.as_array().as_ptr();
507        x.map_streams_in_place(&mut out, |s, y| y[0] *= (s + 2) as f64);
508        assert_eq!((out.as_array().as_ptr(), out.sample_rate()), (p, Hertz(3.0)));
509        assert_eq!(
510            (out.stream(0)[0], out.stream(1)[0], out.stream(1)[1]),
511            (C64::new(2.0, 0.0), C64::new(0.0, 3.0), C64::new(0.0, 1.0))
512        );
513    }
514
515    #[test]
516    #[should_panic(expected = "map_streams")]
517    fn map_streams_panics_on_a_short_stream() {
518        let x = Signal::from_streams(&[vec![C64::new(1.0, 0.0); 4]], Hertz(1.0)).unwrap();
519        let mut out = x.clone();
520        x.map_streams(&mut out, |_, s| s[..2].to_vec());
521    }
522}