Skip to main content

comnum_waveform/ofdm/
link.rs

1//! An OFDM link of several antennas: the frame of resource grids, its transmission and
2//! its reception.
3//!
4//! [`transmit`] maps the symbols of every transmit antenna (port) on resource grids of
5//! `n_symbols` OFDM symbols, the pilots dealt to the ports in turn
6//! ([`ResourceGrid::map_port`]), pads the last grid with random QPSK at the power of a
7//! layer, and modulates every grid ([`Ofdm::modulate`], then scaled). The samples have
8//! the power $K/(L N)$ ($K$ active
9//! subcarriers, IFFT of size $L N$): a resource element of unit energy spreads over
10//! $L N / K$ samples, so that the noise variance per sample is the noise variance per
11//! demodulated resource element.
12//!
13//! [`OfdmFrame`] keeps what the receiver needs: [`OfdmFrame::received_grid`]
14//! demodulates the grids of every receive antenna ([`Ofdm::demodulate`] of the samples of
15//! the grid, unscaled), [`OfdmFrame::estimate`] estimates the
16//! channel from the pilots of every port, and [`OfdmFrame::detect`] detects every data
17//! resource element with a [`Receiver`], spatial multiplexing or Alamouti's code over
18//! pairs of resource elements.
19//!
20//! # Examples
21//! ```
22//! use comnum_waveform::ofdm::Ofdm;
23//! use comnum_waveform::ofdm::grid::{PilotPattern, ResourceGrid};
24//! use comnum_waveform::ofdm::link::{FrameDetection, transmit};
25//! use comnum_mimo::receiver::{DetectorKind, Receiver};
26//! use comnum_mapping::constellation::Constellation;
27//! use comnum_core::rng::Rng;
28//! use comnum_math::C64;
29//! use ndarray::Array2;
30//! let ofdm = Ofdm { n_cp: 4, oversampling: 1, ..Ofdm::new(32, 24)? };
31//! let grid = ResourceGrid { n_subcarriers: 24, n_symbols: 4, pattern: PilotPattern::None, seed: 0 };
32//! let c = Constellation::qam(4)?.normalized();
33//! let x: Vec<C64> = (0..90).map(|i| c.points()[i % 4]).collect();
34//! let (tx, frame) = transmit(&ofdm, &grid, vec![x.clone()], &mut Rng::new(1))?;
35//! let rx = Receiver::new(DetectorKind::Zf, c, false)?;
36//! let eye = |_: usize, _: &[Vec<C64>]| Ok(vec![C64::new(1.0, 0.0); 24 * 4]);
37//! let out = frame.detect(&tx, eye, &rx, &FrameDetection { noise_var: 1e-6, ..FrameDetection::default() })?;
38//! assert!(out.symbols[..90].iter().zip(&x).all(|(a, b)| (a - b).norm() < 1e-12));
39//! # Ok::<(), comnum_core::Error>(())
40//! ```
41
42use super::Ofdm;
43use super::estimation::{FrequencyInterpolation, PortEstimation, estimate_port};
44use super::grid::ResourceGrid;
45use comnum_core::rng::Rng;
46use comnum_core::{Result, ensure, ensure_shape};
47use comnum_math::C64;
48use comnum_mimo::receiver::{Receiver, SoftOutput};
49use ndarray::Array2;
50
51/// An OFDM frame: what the receiver needs to undo [`transmit`].
52#[derive(Clone, Debug, PartialEq)]
53pub struct OfdmFrame {
54    /// The modulator.
55    pub ofdm: Ofdm,
56    /// The resource grid of every group of `n_symbols` OFDM symbols.
57    pub grid: ResourceGrid,
58    /// Grids in the frame.
59    pub n_grids: usize,
60    /// Transmit antennas (ports).
61    pub n_ports: usize,
62    /// Data symbols per antenna before the padding of the last grid.
63    pub n_data_symbols: usize,
64    /// Amplitude of the time samples, $\sqrt{K/(L N)}$.
65    pub scale: f64,
66}
67
68/// Map the symbols of every antenna on resource grids and modulate them (see the
69/// module): the samples of every antenna and the frame.
70///
71/// # Errors
72/// [`comnum_core::Error::Shape`] for antennas of different lengths,
73/// [`comnum_core::Error::Config`] for a grid without data resource element, a grid whose
74/// width is not the number of active subcarriers, or a DFT-spread modulator (the pilots
75/// of the grid are resource elements), and the errors of the modulator.
76pub fn transmit(
77    ofdm: &Ofdm,
78    grid: &ResourceGrid,
79    streams: Vec<Vec<C64>>,
80    rng: &mut Rng,
81) -> Result<(Vec<Vec<C64>>, OfdmFrame)> {
82    let n_ports = streams.len();
83    ensure(n_ports >= 1, || "at least one antenna".into())?;
84    let n_data_symbols = streams[0].len();
85    ensure_shape(streams.iter().all(|x| x.len() == n_data_symbols), || {
86        format!("the antennas carry different lengths: {:?}", streams.iter().map(Vec::len).collect::<Vec<_>>())
87    })?;
88    ofdm.check()?;
89    ensure(grid.n_subcarriers == ofdm.n_active, || {
90        format!(
91            "a grid of {} subcarriers for an OFDM modulator of {} active subcarriers",
92            grid.n_subcarriers, ofdm.n_active
93        )
94    })?;
95    // the pilots of the grid are resource elements: a DFT spreading would mix them with
96    // the data
97    ensure(!ofdm.dft_spread, || "the OFDM link maps a resource grid: DFT spreading is not supported".into())?;
98    grid.check()?;
99    let per_grid = grid.n_data();
100    ensure(per_grid > 0, || "the grid has no data resource element".into())?;
101    let n_grids = n_data_symbols.div_ceil(per_grid).max(1);
102    let scale = (ofdm.n_active as f64 / ofdm.fft_len() as f64).sqrt();
103    // the padding of the last grid: random QPSK at the power of a layer
104    let pad_amp = std::f64::consts::FRAC_1_SQRT_2 / (n_ports as f64).sqrt();
105    let mut out = Vec::with_capacity(n_ports);
106    for (port, x) in streams.into_iter().enumerate() {
107        let mut data = x;
108        while data.len() < n_grids * per_grid {
109            data.push(C64::new(
110                if rng.bit() == 0 { pad_amp } else { -pad_amp },
111                if rng.bit() == 0 { pad_amp } else { -pad_amp },
112            ));
113        }
114        let mut time = Vec::with_capacity(n_grids * grid.n_symbols * ofdm.block_len());
115        for chunk in data.chunks(per_grid) {
116            let g = grid.map_port(chunk, port, n_ports)?;
117            time.extend(ofdm.modulate(&g)?.into_iter().map(|v| v * scale));
118        }
119        out.push(time);
120    }
121    let frame = OfdmFrame { ofdm: ofdm.clone(), grid: grid.clone(), n_grids, n_ports, n_data_symbols, scale };
122    Ok((out, frame))
123}
124
125/// The settings of [`OfdmFrame::detect`].
126#[derive(Clone, Copy, Debug, PartialEq)]
127pub struct FrameDetection {
128    /// The amplitude of a layer, by which the channel is multiplied.
129    pub gain: f64,
130    /// Whether the pairs of consecutive data resource elements carry the blocks of
131    /// Alamouti's code.
132    pub alamouti: bool,
133    /// The noise variance of the detector.
134    pub noise_var: f64,
135}
136
137impl Default for FrameDetection {
138    /// Unit gain, spatial multiplexing, no noise.
139    fn default() -> FrameDetection {
140        FrameDetection { gain: 1.0, alamouti: false, noise_var: 0.0 }
141    }
142}
143
144impl FrameDetection {
145    /// Check the settings.
146    ///
147    /// # Errors
148    /// [`comnum_core::Error::Config`] for a gain or a noise variance that is not finite.
149    pub fn check(&self) -> Result<()> {
150        let (g, v) = (self.gain, self.noise_var);
151        ensure(g.is_finite() && v.is_finite(), || {
152            format!("the gain and the noise variance must be finite, got {g} and {v}")
153        })
154    }
155}
156
157impl OfdmFrame {
158    /// The data resource elements of a grid, symbol after symbol (indices `l K + k`,
159    /// [`ResourceGrid::data_indices`]).
160    pub fn data_res(&self) -> Vec<usize> {
161        self.grid.data_indices()
162    }
163
164    /// The sample at the middle of every OFDM symbol of grid `g` (after the prefix): the
165    /// instant at which a genie-aided receiver takes the channel.
166    pub fn symbol_middles(&self, g: usize) -> Vec<usize> {
167        let (bl, cp, ln) = (self.ofdm.block_len(), self.ofdm.oversampling * self.ofdm.n_cp, self.ofdm.fft_len());
168        (0..self.grid.n_symbols).map(|l| (g * self.grid.n_symbols + l) * bl + cp + ln / 2).collect()
169    }
170
171    /// The FFT bins of the active subcarriers.
172    pub fn active_bins(&self) -> Vec<usize> {
173        (0..self.ofdm.n_active).map(|i| self.ofdm.bin(i)).collect()
174    }
175
176    /// The demodulated grid `g` of every receive antenna, `[rx][l K + k]`.
177    ///
178    /// # Errors
179    /// [`comnum_core::Error::Shape`] if a stream is shorter than the frame.
180    pub fn received_grid(&self, streams: &[Vec<C64>], g: usize) -> Result<Vec<Vec<C64>>> {
181        let len = self.grid.n_symbols * self.ofdm.block_len();
182        let (start, end) = (g * len, (g + 1) * len);
183        streams
184            .iter()
185            .map(|x| {
186                ensure_shape(end <= x.len(), || {
187                    format!("the signal of {} samples is shorter than the OFDM frame ({end} samples)", x.len())
188                })?;
189                // unscaled before the transform, the order of the operations of the receiver
190                let samples: Vec<C64> = x[start..end].iter().map(|v| v / self.scale).collect();
191                self.ofdm.demodulate(&samples)
192            })
193            .collect()
194    }
195
196    /// The channel of every resource element estimated from the pilots of every port,
197    /// `[re][rx][tx]` flattened, from the received grids `rx` ([`OfdmFrame::received_grid`]).
198    ///
199    /// # Errors
200    /// The errors of [`estimate_port`].
201    pub fn estimate(&self, rx: &[Vec<C64>], noise_var: f64, method: &FrequencyInterpolation) -> Result<Vec<C64>> {
202        let (nr, nt) = (rx.len(), self.n_ports);
203        let mut h = vec![C64::new(0.0, 0.0); self.grid.n_subcarriers * self.grid.n_symbols * nr * nt];
204        let mut settings = PortEstimation { method: method.clone(), noise_var, port: 0, n_ports: nt };
205        for (r, y) in rx.iter().enumerate() {
206            for t in 0..nt {
207                settings.port = t;
208                let est = estimate_port(&self.grid, y, &settings)?;
209                for (re, v) in est.into_iter().enumerate() {
210                    h[(re * nr + r) * nt + t] = v;
211                }
212            }
213        }
214        Ok(h)
215    }
216
217    /// Detect every data resource element of the frame received on `streams`: the
218    /// channel of grid `g` is `channel(g, rx)` (`[re][rx][tx]` flattened, from the
219    /// received grids `rx`), multiplied by the gain of `settings`. The padding of the
220    /// last grid is left out.
221    ///
222    /// # Errors
223    /// The errors of [`FrameDetection::check`], of `channel` and of
224    /// [`Receiver::detect_uses`] (an odd number of data resource elements with Alamouti's
225    /// code).
226    pub fn detect<F>(
227        &self,
228        streams: &[Vec<C64>],
229        mut channel: F,
230        receiver: &Receiver,
231        settings: &FrameDetection,
232    ) -> Result<SoftOutput>
233    where
234        F: FnMut(usize, &[Vec<C64>]) -> Result<Vec<C64>>,
235    {
236        settings.check()?;
237        let FrameDetection { gain, alamouti, noise_var } = *settings;
238        let (nr, nt) = (streams.len(), self.n_ports);
239        let data_res = self.data_res();
240        let mut out = SoftOutput::default();
241        for g in 0..self.n_grids {
242            let rx = self.received_grid(streams, g)?;
243            let h = channel(g, &rx)?;
244            let used = self.n_data_symbols.saturating_sub(g * data_res.len()).min(data_res.len());
245            receiver
246                .detect_uses(
247                    data_res[..used].iter().copied(),
248                    alamouti,
249                    noise_var,
250                    |re| Ok(Array2::from_shape_fn((nr, nt), |(r, t)| h[(re * nr + r) * nt + t] * gain)),
251                    |re| rx.iter().map(|v| v[re]).collect(),
252                    &mut out,
253                )
254                .map_err(|e| e.context("the data resource elements"))?;
255        }
256        Ok(out)
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263    use crate::ofdm::grid::PilotPattern;
264
265    /// The link modulated every OFDM symbol and demodulated every block itself before it
266    /// called `Ofdm::modulate` and `Ofdm::demodulate`: the samples and the received grids
267    /// are bit-identical.
268    #[test]
269    fn the_link_on_the_modem_is_the_former_link() {
270        let ofdm = Ofdm { n_cp: 6, oversampling: 2, dc_null: true, ..Ofdm::new(32, 24).unwrap() };
271        let grid = ResourceGrid {
272            n_subcarriers: 24,
273            n_symbols: 5,
274            pattern: PilotPattern::Lattice { freq: 3, time: 2 },
275            seed: 4,
276        };
277        let mut rng = Rng::new(7);
278        let streams: Vec<Vec<C64>> = (0..2).map(|_| (0..250).map(|_| rng.complex_gaussian()).collect()).collect();
279        let (tx, frame) = transmit(&ofdm, &grid, streams.clone(), &mut Rng::new(1)).unwrap();
280        // the former transmitter, with the same padding draws
281        let mut pad_rng = Rng::new(1);
282        let (k, ln) = (ofdm.n_active, ofdm.fft_len());
283        let scale = (k as f64 / ln as f64).sqrt();
284        let amp = std::f64::consts::FRAC_1_SQRT_2 / 2f64.sqrt();
285        for (port, x) in streams.into_iter().enumerate() {
286            let mut data = x;
287            while data.len() < frame.n_grids * grid.n_data() {
288                data.push(C64::new(
289                    if pad_rng.bit() == 0 { amp } else { -amp },
290                    if pad_rng.bit() == 0 { amp } else { -amp },
291                ));
292            }
293            let mut old = Vec::new();
294            for chunk in data.chunks(grid.n_data()) {
295                for sym in grid.map_port(chunk, port, 2).unwrap().chunks(k) {
296                    old.extend(ofdm.with_prefix(&ofdm.to_time(sym).unwrap()).unwrap().into_iter().map(|v| v * scale));
297                }
298            }
299            assert_eq!(tx[port], old);
300        }
301        // the former receiver: the useful part of every block, unscaled, transformed
302        let (bl, cp) = (ofdm.block_len(), ofdm.oversampling * ofdm.n_cp);
303        for g in 0..frame.n_grids {
304            let rx = frame.received_grid(&tx, g).unwrap();
305            for (x, got) in tx.iter().zip(&rx) {
306                let mut old = Vec::new();
307                for l in 0..grid.n_symbols {
308                    let start = (g * grid.n_symbols + l) * bl + cp;
309                    let block: Vec<C64> = x[start..start + ln].iter().map(|v| v / frame.scale).collect();
310                    old.extend(ofdm.to_active(&block).unwrap());
311                }
312                assert_eq!(got, &old);
313            }
314        }
315        assert!(frame.received_grid(&[tx[0][..tx[0].len() - 1].to_vec()], frame.n_grids - 1).is_err());
316    }
317}