Skip to main content

comnum_spec/blocks/
frame.rs

1//! The state of one trial along a link: [`Frame`] and the contexts it carries.
2
3use super::{Csi, OfdmFrame, WdmFrame};
4use comnum_core::{Error, Result, ensure};
5use comnum_mapping::constellation::{Constellation, indices_to_bits};
6use comnum_math::C64;
7
8/// The state carried along a link during one trial: what was transmitted, the
9/// current samples, and the contexts that some blocks set for the following ones.
10#[derive(Clone, Debug, Default)]
11pub struct Frame {
12    /// The constellation of the source (unit mean energy for uniform symbols).
13    pub constellation: Option<Constellation>,
14    /// The probabilities of the points of the constellation (probabilistic shaping);
15    /// uniform if `None`. The rate metrics, the soft demapper and `awgn_ebn0` use them.
16    pub pmf: Option<Vec<f64>>,
17    /// Transmitted bits (Gray labels of the constellation, MSB first).
18    pub bits: Vec<u8>,
19    /// Transmitted symbol indices.
20    pub indices: Vec<usize>,
21    /// Transmitted symbols (of polarisation x, then y, with two polarisations).
22    pub symbols: Vec<C64>,
23    /// Symbols excluded from the metrics (pilots, preambles), one flag per symbol.
24    pub excluded: Vec<bool>,
25    /// Current samples of antenna (or polarisation) 0.
26    pub samples: Vec<C64>,
27    /// The samples of the other antennas or polarisations (1, 2, …); empty for one.
28    pub antennas: Vec<Vec<C64>>,
29    /// Current samples per symbol.
30    pub sps: f64,
31    /// Symbol rate (Bd; 1 for a normalised time axis).
32    pub symbol_rate: f64,
33    /// What the streams are: antennas, layers, polarisations.
34    pub layout: Layout,
35    /// The channel of the trial, as a genie-aided receiver knows it.
36    pub channel: ChannelState,
37    /// What the receiver declares about its outputs.
38    pub receiver: ReceiverState,
39    /// The information bits, the LLRs and the decisions of a coded link.
40    pub coding: Coding,
41    /// The frames between a transmitter block and its receiver (OFDM, WDM).
42    pub in_flight: InFlight,
43    /// A measured capture.
44    pub capture: CaptureState,
45    /// The index of the trial of the condition (set by the runner; 0 outside a
46    /// campaign), for the blocks that save or label a trial.
47    pub trial: u64,
48    /// Diagnostic values recorded by the blocks, reported as means.
49    pub values: Vec<(String, f64)>,
50    /// The metrics measured by the `probe` blocks, merged into the results of the trial.
51    pub probes: comnum_sim::scenario::Record,
52}
53
54/// What the streams of a [`Frame`] are.
55#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
56pub struct Layout {
57    /// The streams are the polarisations of one optical channel, each with its own
58    /// transmitted symbols: the blocks written for one polarisation process each of
59    /// them.
60    pub polarisations: bool,
61    /// The layers of a multi-antenna transmitter (`mimo_tx`); 0 or 1 for one antenna.
62    pub layers: usize,
63    /// The transmitter uses Alamouti's code (`mimo_tx` with `scheme: alamouti`).
64    pub alamouti: bool,
65}
66
67/// The channel of a trial, as a genie-aided receiver knows it.
68#[derive(Clone, Debug, Default)]
69pub struct ChannelState {
70    /// The fading of the trial.
71    pub csi: Option<Csi>,
72    /// Variance of the complex noise added per sample by the noise blocks.
73    pub noise_var: Option<f64>,
74    /// Mean power of the signal that entered the fading channel: the reference power of
75    /// the noise blocks after a fading block, so that the SNR is an average over the
76    /// fading and not the SNR of every realisation.
77    pub faded_power: Option<f64>,
78}
79
80impl ChannelState {
81    /// The noise variance per sample for the detectors, floored at `1e-12` (and `1e-12`
82    /// when no noise block ran).
83    pub fn detector_noise_var(&self) -> f64 {
84        self.noise_var.unwrap_or(1e-12).max(1e-12)
85    }
86}
87
88/// What the receiver declares about its outputs.
89#[derive(Clone, Debug, Default)]
90pub struct ReceiverState {
91    /// The noise variance the receiver believes in, on the scale of the unit-energy
92    /// constellation: with the samples, the receiver's posterior
93    /// $q(x \mid z) \propto e^{-\lvert z - x \rvert^{2} / \hat{\sigma}^{2}}$ (declared by `noise_estimate`, or by a learned
94    /// receiver).
95    pub noise_var: Option<f64>,
96    /// Noise variance of every received symbol after equalisation or detection (empty:
97    /// unknown).
98    pub symbol_noise: Vec<f64>,
99}
100
101/// The coded blocks of a trial.
102#[derive(Clone, Debug, Default)]
103pub struct Coding {
104    /// Information bits of the coded blocks, block after block.
105    pub payload: Vec<u8>,
106    /// Coded blocks per trial.
107    pub n_blocks: usize,
108    /// Code rate (information bits per coded bit); `None` for an uncoded link.
109    pub code_rate: Option<f64>,
110    /// Bit LLRs of the demapper or the detector (positive: bit 0 more likely).
111    pub llr: Vec<f64>,
112    /// Information bits decided by the decoder.
113    pub decoded: Vec<u8>,
114}
115
116/// The frames between a transmitter block and its receiver.
117#[derive(Clone, Debug, Default)]
118pub struct InFlight {
119    /// What `ofdm_rx` needs to undo `ofdm_tx`.
120    pub ofdm: Option<OfdmFrame>,
121    /// The WDM grid between `wdm_tx` and `wdm_rx`.
122    pub wdm: Option<WdmFrame>,
123}
124
125/// A measured capture (`capture`, `reference_sync`).
126#[derive(Clone, Debug, Default)]
127pub struct CaptureState {
128    /// One period of the transmitted indices (empty for a simulated source).
129    pub reference: Vec<usize>,
130    /// The transmitted symbols are not yet aligned on the samples (before
131    /// `reference_sync`).
132    pub unaligned: bool,
133}
134
135impl Frame {
136    /// Sampling rate `symbol_rate · sps` (Hz).
137    pub fn fs(&self) -> f64 {
138        self.symbol_rate * self.sps
139    }
140
141    /// Number of transmitted symbols.
142    pub fn n_symbols(&self) -> usize {
143        self.symbols.len()
144    }
145
146    /// The constellation, or an error naming the block that needs it.
147    ///
148    /// # Errors
149    /// [`Error::Config`] if no source has set the constellation before `block`.
150    pub fn constellation(&self, block: &str) -> Result<&Constellation> {
151        self.constellation.as_ref().ok_or_else(|| {
152            Error::Config(format!(
153                "block {block:?} needs a constellation (it comes before the source, or the source is gaussian)"
154            ))
155        })
156    }
157
158    /// Set the transmitted symbols from indices of the constellation.
159    pub(super) fn set_indices(&mut self, idx: Vec<usize>) {
160        if let Some(c) = &self.constellation {
161            self.symbols = c.symbols(&idx);
162            self.bits = indices_to_bits(&idx, c.bits_per_symbol());
163        }
164        self.excluded = vec![false; idx.len()];
165        self.indices = idx;
166    }
167
168    /// The integer samples per symbol, or an error naming the block.
169    ///
170    /// # Errors
171    /// [`Error::Config`] unless the signal is aligned at an integer number of samples per symbol.
172    pub fn int_sps(&self, block: &str) -> Result<usize> {
173        let k = self.sps.round();
174        ensure(k >= 1.0 && (self.sps - k).abs() < 1e-9, || {
175            format!("block {block:?} needs an integer number of samples per symbol, the signal has {}", self.sps)
176        })?;
177        Ok(k as usize)
178    }
179
180    /// Check that the signal is at one sample per symbol: the symbols of a source, or
181    /// the output of a decimation.
182    ///
183    /// # Errors
184    /// [`Error::Config`] naming `block` unless the signal is at one sample per symbol.
185    pub fn require_one_sps(&self, block: &str) -> Result<()> {
186        ensure(self.sps == 1.0, || {
187            format!("block {block:?} needs the signal at 1 sample per symbol, it has {} samples per symbol", self.sps)
188        })
189    }
190
191    /// Check that the samples are at one sample per symbol and aligned on the symbols.
192    ///
193    /// # Errors
194    /// [`Error::Config`] unless the signal is aligned at one sample per symbol.
195    pub fn require_symbols(&self, block: &str) -> Result<()> {
196        ensure(!self.capture.unaligned, || {
197            format!(
198                "block {block:?} needs the transmitted symbols aligned on the capture: add reference_sync before it"
199            )
200        })?;
201        ensure(self.sps == 1.0 && self.samples.len() == self.symbols.len(), || {
202            format!(
203                "block {block:?} needs the signal at 1 sample per symbol aligned on the {} transmitted symbols \
204                 (it has {} samples at {} samples per symbol): place it after decimation and frame synchronisation",
205                self.symbols.len(),
206                self.samples.len(),
207                self.sps
208            )
209        })
210    }
211
212    /// Record a diagnostic value.
213    pub fn record(&mut self, label: &str, name: &str, value: f64) {
214        self.values.push((format!("{label}.{name}"), value));
215    }
216
217    /// Number of antenna streams (1 for a single antenna).
218    pub fn n_streams(&self) -> usize {
219        1 + self.antennas.len()
220    }
221
222    /// The samples of every antenna.
223    pub fn streams(&self) -> impl Iterator<Item = &Vec<C64>> {
224        std::iter::once(&self.samples).chain(&self.antennas)
225    }
226
227    /// The samples of every antenna, mutable.
228    pub fn streams_mut(&mut self) -> impl Iterator<Item = &mut Vec<C64>> {
229        std::iter::once(&mut self.samples).chain(&mut self.antennas)
230    }
231
232    /// Take the samples of every antenna, leaving the signal without samples.
233    pub fn take_streams(&mut self) -> Vec<Vec<C64>> {
234        let mut out = vec![std::mem::take(&mut self.samples)];
235        out.append(&mut self.antennas);
236        out
237    }
238
239    /// Set the samples of every antenna (at least one stream).
240    pub fn set_streams(&mut self, mut streams: Vec<Vec<C64>>) {
241        if streams.is_empty() {
242            streams.push(Vec::new());
243        }
244        self.samples = streams.remove(0);
245        self.antennas = streams;
246    }
247
248    /// The reference power of a noise block: the power entering the fading channel if
249    /// there is one, else the mean power of the samples.
250    pub fn reference_power(&self) -> f64 {
251        self.channel.faded_power.unwrap_or_else(|| self.mean_power())
252    }
253
254    /// Mean power of the samples over every antenna: their energy
255    /// ([`comnum_math::energy`]) over their number.
256    pub fn mean_power(&self) -> f64 {
257        let (e, n) = self.streams().fold((0.0, 0usize), |(e, n), x| (e + comnum_math::energy(x), n + x.len()));
258        if n == 0 { 0.0 } else { e / n as f64 }
259    }
260
261    /// The signal with the polarisations of a dual-polarisation channel concatenated
262    /// into one stream, aligned on `symbols` (the signal itself otherwise).
263    pub fn pooled(&self) -> std::borrow::Cow<'_, Frame> {
264        if self.layout.polarisations && !self.antennas.is_empty() {
265            let mut s = self.clone();
266            let mut all = std::mem::take(&mut s.samples);
267            for a in std::mem::take(&mut s.antennas) {
268                all.extend(a);
269            }
270            s.samples = all;
271            std::borrow::Cow::Owned(s)
272        } else {
273            std::borrow::Cow::Borrowed(self)
274        }
275    }
276
277    /// The noise variance of every symbol the receiver sees: `symbol_noise`, else the
278    /// declared posterior `noise_var`, else the channel's `noise_var`.
279    ///
280    /// # Errors
281    /// [`Error::Config`] if no block has set the noise variance.
282    pub fn symbol_noise(&self, block: &str) -> Result<Vec<f64>> {
283        let known = &self.receiver.symbol_noise;
284        if known.len() == self.samples.len() && !known.is_empty() {
285            return Ok(known.clone());
286        }
287        let v = self.receiver.noise_var.or(self.channel.noise_var).ok_or_else(|| {
288            Error::Config(format!(
289                "block {block:?} needs the noise variance: add a noise block (awgn, awgn_ebn0) or noise_estimate before it"
290            ))
291        })?;
292        Ok(vec![v; self.samples.len()])
293    }
294}