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}