Skip to main content

comnum_waveform/ofdm/
grid.rs

1//! The OFDM resource grid: data and pilot resource elements.
2//!
3//! A grid holds $K$ subcarriers × $L$ OFDM symbols; a [`PilotPattern`] marks the
4//! resource elements (REs) that carry known pilots, the others carry data. Pilots are
5//! unit-energy QPSK symbols drawn from a seed (known to the receiver), except those of a
6//! [`PilotTable`], which carries its own values (the sequences and boosts of a standard).
7//!
8//! | Pattern | Pilots | Suited to |
9//! |---|---|---|
10//! | [`PilotPattern::Block`] | every subcarrier of one symbol in `period` | slow fading, frequency-selective channels |
11//! | [`PilotPattern::Comb`] | one subcarrier in `spacing`, in every symbol | fast fading |
12//! | [`PilotPattern::Lattice`] | one subcarrier in `freq`, one symbol in `time` | both, at a lower overhead |
13//! | [`PilotPattern::Explicit`] | the positions and values of a [`PilotTable`] | the pilots of a standard (scattered, staggered, boosted) |
14//!
15//! The grid is stored symbol after symbol: RE $(k, l)$ is at index $l K + k$;
16//! [`ResourceGrid::data_indices`] lists the indices of the data REs in that order, the
17//! one walk of the grid that [`ResourceGrid::map`], [`ResourceGrid::extract`] and the
18//! OFDM link share. [`ResourceGrid::check`] refuses a spacing of zero (which
19//! [`ResourceGrid::is_pilot`] would otherwise read as one) and a table of another size.
20//!
21//! # Examples
22//! ```
23//! use comnum_waveform::ofdm::grid::{PilotPattern, ResourceGrid};
24//! use comnum_math::C64;
25//! let g = ResourceGrid { n_subcarriers: 12, n_symbols: 7, pattern: PilotPattern::Lattice { freq: 3, time: 3 }, seed: 1 };
26//! assert_eq!(g.pilot_positions().len(), 4 * 3);
27//! let d = vec![C64::new(1.0, 0.0); g.n_data()];
28//! let grid = g.map(&d)?;
29//! assert_eq!(g.extract(&grid)?, d);
30//! # Ok::<(), comnum_core::Error>(())
31//! ```
32//!
33//! # References
34//!
35//! 1. S. Coleri, M. Ergen, A. Puri and A. Bahai, "Channel estimation techniques based on
36//!    pilot arrangement in OFDM systems," *IEEE Trans. Broadcast.*, 48(3), 223–229, 2002.
37
38use comnum_core::{Error, Result, ensure, ensure_shape};
39use comnum_math::C64;
40use std::sync::Arc;
41
42/// The positions of the pilots in the grid.
43#[non_exhaustive]
44#[derive(Clone, Debug, PartialEq)]
45pub enum PilotPattern {
46    /// Every subcarrier of the symbols $l$ with $l \bmod$ `period` $= 0$.
47    Block {
48        /// Pilot symbol period.
49        period: usize,
50    },
51    /// The subcarriers $k$ with $k \bmod$ `spacing` $= 0$ of every symbol.
52    Comb {
53        /// Pilot subcarrier spacing.
54        spacing: usize,
55    },
56    /// The subcarriers $k \bmod$ `freq` $= 0$ of the symbols $l \bmod$ `time` $= 0$.
57    Lattice {
58        /// Pilot subcarrier spacing.
59        freq: usize,
60        /// Pilot symbol period.
61        time: usize,
62    },
63    /// The pilots of a table: any positions, with their values.
64    Explicit(Arc<PilotTable>),
65    /// No pilots (a receiver that knows the channel).
66    None,
67}
68
69/// The pilots of a grid given one by one: their positions and their values (the
70/// sequences, phases and power boosts that a standard prescribes).
71#[derive(Clone, Debug, PartialEq)]
72pub struct PilotTable {
73    n_subcarriers: usize,
74    n_symbols: usize,
75    /// The value of every RE, symbol after symbol (`None` for a data RE).
76    values: Vec<Option<C64>>,
77}
78
79impl PilotTable {
80    /// A table of `n_subcarriers` × `n_symbols` REs with pilots `((k, l), value)`; a
81    /// position given twice keeps its last value.
82    ///
83    /// # Errors
84    /// [`Error::Config`] for an empty grid, a position outside it or a pilot of zero or
85    /// non-finite value.
86    pub fn new(
87        n_subcarriers: usize,
88        n_symbols: usize,
89        pilots: impl IntoIterator<Item = ((usize, usize), C64)>,
90    ) -> Result<PilotTable> {
91        ensure(n_subcarriers > 0 && n_symbols > 0, || {
92            format!("a pilot table needs at least one RE, got {n_subcarriers} × {n_symbols}")
93        })?;
94        let mut values = vec![None; n_subcarriers * n_symbols];
95        for ((k, l), v) in pilots {
96            ensure(k < n_subcarriers && l < n_symbols, || {
97                format!("pilot ({k}, {l}) outside the grid of {n_subcarriers} × {n_symbols}")
98            })?;
99            ensure(v.norm() > 0.0 && v.re.is_finite() && v.im.is_finite(), || {
100                format!("the pilot ({k}, {l}) must be finite and non-zero, got {v}")
101            })?;
102            values[l * n_subcarriers + k] = Some(v);
103        }
104        Ok(PilotTable { n_subcarriers, n_symbols, values })
105    }
106
107    /// Subcarriers of the table.
108    pub fn n_subcarriers(&self) -> usize {
109        self.n_subcarriers
110    }
111
112    /// OFDM symbols of the table.
113    pub fn n_symbols(&self) -> usize {
114        self.n_symbols
115    }
116
117    /// The pilot of RE $(k, l)$, `None` for a data RE or a position outside the table.
118    pub fn get(&self, k: usize, l: usize) -> Option<C64> {
119        if k < self.n_subcarriers && l < self.n_symbols { self.values[l * self.n_subcarriers + k] } else { None }
120    }
121
122    /// Number of pilots.
123    pub fn len(&self) -> usize {
124        self.values.iter().filter(|v| v.is_some()).count()
125    }
126
127    /// Whether the table has no pilot.
128    pub fn is_empty(&self) -> bool {
129        self.len() == 0
130    }
131}
132
133impl PilotPattern {
134    /// [`PilotPattern::Block`]: every subcarrier of one symbol in `period`.
135    ///
136    /// # Errors
137    /// [`Error::Config`] for a period of zero.
138    ///
139    /// # Examples
140    /// ```
141    /// use comnum_waveform::ofdm::grid::PilotPattern;
142    /// assert_eq!(PilotPattern::block(4)?, PilotPattern::Block { period: 4 });
143    /// assert!(PilotPattern::block(0).is_err());
144    /// # Ok::<(), comnum_core::Error>(())
145    /// ```
146    pub fn block(period: usize) -> Result<PilotPattern> {
147        Ok(PilotPattern::Block { period: positive(period, "period")? })
148    }
149
150    /// [`PilotPattern::Comb`]: one subcarrier in `spacing` of every symbol.
151    ///
152    /// # Errors
153    /// [`Error::Config`] for a spacing of zero.
154    pub fn comb(spacing: usize) -> Result<PilotPattern> {
155        Ok(PilotPattern::Comb { spacing: positive(spacing, "spacing")? })
156    }
157
158    /// [`PilotPattern::Lattice`]: one subcarrier in `freq` of one symbol in `time`.
159    ///
160    /// # Errors
161    /// [`Error::Config`] for a spacing of zero.
162    pub fn lattice(freq: usize, time: usize) -> Result<PilotPattern> {
163        Ok(PilotPattern::Lattice { freq: positive(freq, "freq")?, time: positive(time, "time")? })
164    }
165}
166
167/// `v` if it is at least 1, else [`Error::Config`] naming the spacing `what`.
168fn positive(v: usize, what: &str) -> Result<usize> {
169    if v >= 1 { Ok(v) } else { Err(Error::Config(format!("the pilot {what} must be at least 1, got {v}"))) }
170}
171
172/// A grid of `n_subcarriers` × `n_symbols` resource elements with pilots.
173#[derive(Clone, Debug, PartialEq)]
174pub struct ResourceGrid {
175    /// Subcarriers $K$ (the active subcarriers of the OFDM modulator).
176    pub n_subcarriers: usize,
177    /// OFDM symbols $L$ per grid.
178    pub n_symbols: usize,
179    /// The pilot pattern.
180    pub pattern: PilotPattern,
181    /// Seed of the pilot symbols.
182    pub seed: u64,
183}
184
185impl ResourceGrid {
186    /// Validate the grid.
187    ///
188    /// # Errors
189    /// [`Error::Config`] for an empty grid, a pilot period or spacing of zero, or a pilot
190    /// table of another size than the grid.
191    ///
192    /// # Examples
193    /// ```
194    /// use comnum_waveform::ofdm::grid::{PilotPattern, ResourceGrid};
195    /// let g = ResourceGrid { n_subcarriers: 12, n_symbols: 7, pattern: PilotPattern::Comb { spacing: 0 }, seed: 1 };
196    /// assert!(g.check().is_err() && g.map(&[]).is_err());
197    /// ```
198    pub fn check(&self) -> Result<()> {
199        let (k, l) = (self.n_subcarriers, self.n_symbols);
200        ensure(k >= 1 && l >= 1, || format!("a resource grid needs at least one RE, got {k} × {l}"))?;
201        match &self.pattern {
202            PilotPattern::Block { period } => positive(*period, "period").map(drop),
203            PilotPattern::Comb { spacing } => positive(*spacing, "spacing").map(drop),
204            PilotPattern::Lattice { freq, time } => {
205                positive(*freq, "freq").and_then(|_| positive(*time, "time")).map(drop)
206            }
207            PilotPattern::Explicit(t) => ensure(t.n_subcarriers == k && t.n_symbols == l, || {
208                format!("a pilot table of {} × {} for a grid of {k} × {l}", t.n_subcarriers, t.n_symbols)
209            }),
210            PilotPattern::None => Ok(()),
211        }
212    }
213
214    /// The grid of a pilot table, of its size.
215    pub fn from_table(table: PilotTable) -> ResourceGrid {
216        ResourceGrid {
217            n_subcarriers: table.n_subcarriers,
218            n_symbols: table.n_symbols,
219            pattern: PilotPattern::Explicit(Arc::new(table)),
220            seed: 0,
221        }
222    }
223
224    /// Whether RE $(k, l)$ carries a pilot (a spacing of zero, which
225    /// [`ResourceGrid::check`] refuses, is read as one).
226    pub fn is_pilot(&self, k: usize, l: usize) -> bool {
227        match &self.pattern {
228            PilotPattern::Block { period } => l % (*period).max(1) == 0,
229            PilotPattern::Comb { spacing } => k % (*spacing).max(1) == 0,
230            PilotPattern::Lattice { freq, time } => k % (*freq).max(1) == 0 && l % (*time).max(1) == 0,
231            PilotPattern::Explicit(t) => t.get(k, l).is_some(),
232            PilotPattern::None => false,
233        }
234    }
235
236    /// The pilot subcarriers of symbol `l` that belong to port `port` out of `n_ports`:
237    /// the pilots of a symbol are dealt to the ports in turn (frequency-division
238    /// orthogonal pilots; the other ports are silent there).
239    pub fn port_pilots(&self, l: usize, port: usize, n_ports: usize) -> Vec<usize> {
240        (0..self.n_subcarriers)
241            .filter(|&k| self.is_pilot(k, l))
242            .enumerate()
243            .filter(|(j, _)| j % n_ports.max(1) == port)
244            .map(|(_, k)| k)
245            .collect()
246    }
247
248    /// The grid of port `port` out of `n_ports`: [`ResourceGrid::map`] with the pilots
249    /// of the other ports set to zero.
250    ///
251    /// # Errors
252    /// As [`ResourceGrid::map`].
253    pub fn map_port(&self, data: &[C64], port: usize, n_ports: usize) -> Result<Vec<C64>> {
254        let mut g = self.map(data)?;
255        if n_ports > 1 {
256            for l in 0..self.n_symbols {
257                let mine = self.port_pilots(l, port, n_ports);
258                for k in (0..self.n_subcarriers).filter(|&k| self.is_pilot(k, l) && !mine.contains(&k)) {
259                    g[l * self.n_subcarriers + k] = C64::new(0.0, 0.0);
260                }
261            }
262        }
263        Ok(g)
264    }
265
266    /// The pilot symbol of RE $(k, l)$: the value of the table of an explicit pattern,
267    /// unit-energy QPSK drawn from the seed otherwise.
268    pub fn pilot(&self, k: usize, l: usize) -> C64 {
269        if let PilotPattern::Explicit(t) = &self.pattern {
270            if let Some(v) = t.get(k, l) {
271                return v;
272            }
273        }
274        comnum_mapping::source::qpsk(&mut comnum_core::rng::member(self.seed, (l * self.n_subcarriers + k) as u64))
275    }
276
277    /// Every RE $(k, l)$, symbol after symbol (the order of the storage).
278    fn res(&self) -> impl Iterator<Item = (usize, usize)> + '_ {
279        (0..self.n_symbols).flat_map(move |l| (0..self.n_subcarriers).map(move |k| (k, l)))
280    }
281
282    /// The pilot REs $(k, l)$, symbol after symbol.
283    pub fn pilot_positions(&self) -> Vec<(usize, usize)> {
284        self.res().filter(|&(k, l)| self.is_pilot(k, l)).collect()
285    }
286
287    /// The indices $l K + k$ of the data REs, symbol after symbol: the order in which
288    /// [`ResourceGrid::map`] places the data and [`ResourceGrid::extract`] takes them.
289    ///
290    /// # Examples
291    /// ```
292    /// use comnum_waveform::ofdm::grid::{PilotPattern, ResourceGrid};
293    /// let g = ResourceGrid { n_subcarriers: 4, n_symbols: 2, pattern: PilotPattern::Comb { spacing: 2 }, seed: 0 };
294    /// assert_eq!(g.data_indices(), vec![1, 3, 5, 7]);
295    /// ```
296    pub fn data_indices(&self) -> Vec<usize> {
297        self.res().filter(|&(k, l)| !self.is_pilot(k, l)).map(|(k, l)| l * self.n_subcarriers + k).collect()
298    }
299
300    /// The symbols that carry at least one pilot.
301    pub fn pilot_symbols(&self) -> Vec<usize> {
302        (0..self.n_symbols).filter(|&l| (0..self.n_subcarriers).any(|k| self.is_pilot(k, l))).collect()
303    }
304
305    /// Number of data REs per grid (counted, nothing allocated).
306    pub fn n_data(&self) -> usize {
307        self.res().filter(|&(k, l)| !self.is_pilot(k, l)).count()
308    }
309
310    /// Place `data` (exactly [`ResourceGrid::n_data`] symbols) and the pilots: the grid,
311    /// symbol after symbol.
312    ///
313    /// # Errors
314    /// The errors of [`ResourceGrid::check`]; [`Error::Shape`] for another number of data
315    /// symbols.
316    pub fn map(&self, data: &[C64]) -> Result<Vec<C64>> {
317        self.check()?;
318        let n_data = self.n_data();
319        ensure_shape(data.len() == n_data, || format!("{} data symbols for a grid of {n_data} data REs", data.len()))?;
320        let mut it = data.iter();
321        Ok(self
322            .res()
323            .map(
324                |(k, l)| if self.is_pilot(k, l) { self.pilot(k, l) } else { *it.next().unwrap_or(&C64::new(0.0, 0.0)) },
325            )
326            .collect())
327    }
328
329    /// The data REs of a grid, in the order of [`ResourceGrid::map`].
330    ///
331    /// # Errors
332    /// [`Error::Shape`] unless `grid` holds the $K L$ REs of the grid.
333    pub fn extract<T: Copy>(&self, grid: &[T]) -> Result<Vec<T>> {
334        let n = self.n_subcarriers * self.n_symbols;
335        ensure_shape(grid.len() == n, || format!("{} REs for a grid of {n}", grid.len()))?;
336        Ok(self.data_indices().into_iter().map(|i| grid[i]).collect())
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343
344    /// The walks of the grid were written in `map`, `extract`, `n_data` and the OFDM
345    /// link before `data_indices`: the same indices, counts and grids for every pattern.
346    #[test]
347    fn the_data_indices_are_the_former_walks() {
348        let table = scattered(49, 8);
349        let patterns = [
350            PilotPattern::Block { period: 3 },
351            PilotPattern::Comb { spacing: 4 },
352            PilotPattern::Lattice { freq: 3, time: 2 },
353            PilotPattern::Explicit(Arc::new(table)),
354            PilotPattern::None,
355        ];
356        for pattern in patterns {
357            let g = ResourceGrid { n_subcarriers: 49, n_symbols: 8, pattern, seed: 5 };
358            let former: Vec<usize> = (0..8)
359                .flat_map(|l| (0..49).map(move |k| (k, l)))
360                .filter(|&(k, l)| !g.is_pilot(k, l))
361                .map(|(k, l)| l * 49 + k)
362                .collect();
363            assert_eq!(g.data_indices(), former);
364            assert_eq!(g.n_data(), 49 * 8 - g.pilot_positions().len());
365            let data: Vec<C64> = (0..g.n_data()).map(|i| C64::new(i as f64, -1.0)).collect();
366            let mut it = data.iter();
367            let mut old = Vec::new();
368            for l in 0..8 {
369                for k in 0..49 {
370                    old.push(if g.is_pilot(k, l) { g.pilot(k, l) } else { *it.next().unwrap() });
371                }
372            }
373            assert_eq!(g.map(&data).unwrap(), old);
374        }
375        let bad = ResourceGrid {
376            n_subcarriers: 12,
377            n_symbols: 7,
378            pattern: PilotPattern::Lattice { freq: 3, time: 0 },
379            seed: 0,
380        };
381        assert!(bad.check().is_err());
382        let wrong_size = ResourceGrid { n_subcarriers: 48, ..ResourceGrid::from_table(scattered(49, 8)) };
383        assert!(wrong_size.check().is_err());
384    }
385
386    #[test]
387    fn patterns_place_their_pilots() {
388        let block =
389            ResourceGrid { n_subcarriers: 8, n_symbols: 6, pattern: PilotPattern::Block { period: 3 }, seed: 0 };
390        assert_eq!(block.pilot_symbols(), vec![0, 3]);
391        assert_eq!(block.n_data(), 8 * 4);
392        let comb = ResourceGrid { pattern: PilotPattern::Comb { spacing: 4 }, ..block.clone() };
393        assert_eq!(comb.pilot_positions().len(), 2 * 6);
394        assert!((comb.pilot(4, 2).norm() - 1.0).abs() < 1e-12);
395        assert!(block.map(&[C64::new(0.0, 0.0); 3]).is_err());
396    }
397
398    /// Scattered pilots in the manner of DVB-T: subcarriers $k \equiv 3 (l \bmod 4)
399    /// \pmod{12}$, BPSK boosted to the amplitude 4/3.
400    fn scattered(k_n: usize, l_n: usize) -> PilotTable {
401        let pilots = (0..l_n).flat_map(move |l| {
402            (0..k_n).filter(move |k| k % 12 == 3 * (l % 4)).map(move |k| {
403                let sign = if (k / 12) % 2 == 0 { 1.0 } else { -1.0 };
404                ((k, l), C64::new(sign * 4.0 / 3.0, 0.0))
405            })
406        });
407        PilotTable::new(k_n, l_n, pilots).unwrap()
408    }
409
410    #[test]
411    fn explicit_tables_carry_their_positions_and_values() {
412        let g = ResourceGrid::from_table(scattered(49, 8));
413        // 5 + 4 + 4 + 4 pilots every four symbols
414        assert_eq!(g.pilot_positions().len(), 34);
415        assert_eq!(g.pilot_symbols(), (0..8).collect::<Vec<_>>());
416        assert!(g.is_pilot(15, 1) && !g.is_pilot(15, 0));
417        assert_eq!(g.pilot(12, 0), C64::new(-4.0 / 3.0, 0.0));
418        let data: Vec<C64> = (0..g.n_data()).map(|i| C64::new(i as f64, 1.0)).collect();
419        let grid = g.map(&data).unwrap();
420        assert_eq!(grid[12], C64::new(-4.0 / 3.0, 0.0));
421        assert_eq!(g.extract(&grid).unwrap(), data);
422        assert!(g.extract(&grid[1..]).is_err());
423        let one = C64::new(1.0, 0.0);
424        assert!(PilotTable::new(4, 2, [((4, 0), one)]).is_err());
425        assert!(PilotTable::new(4, 2, [((0, 2), one)]).is_err());
426        assert!(PilotTable::new(4, 2, [((0, 0), C64::new(0.0, 0.0))]).is_err());
427        assert!(PilotTable::new(0, 2, []).is_err());
428    }
429}