Skip to main content

leodos_protocols/physical/modulator/
mod.rs

1//! Modulation and demodulation schemes.
2//!
3//! Each submodule implements a modulation scheme with `modulate()`
4//! and `demodulate()` functions producing soft-decision LLRs.
5
6/// Binary Phase Shift Keying (1 bit/symbol).
7pub mod bpsk;
8/// Quadrature Phase Shift Keying (2 bits/symbol).
9pub mod qpsk;
10/// Offset QPSK (Proximity-1, CCSDS 211.0).
11pub mod oqpsk;
12/// Gray-coded 8PSK (3 bits/symbol, CCSDS 131.2-B).
13pub mod eight_psk;
14/// Gaussian Minimum Shift Keying (CCSDS 211.0).
15pub mod gmsk;
16
17/// Modulation scheme selector.
18#[derive(Debug, Copy, Clone, Eq, PartialEq)]
19pub enum Scheme {
20    /// Binary Phase Shift Keying (1 bit/symbol).
21    Bpsk,
22    /// Quadrature Phase Shift Keying (2 bits/symbol).
23    Qpsk,
24}
25
26impl Scheme {
27    /// Bits carried per symbol.
28    pub const fn bits_per_symbol(self) -> usize {
29        match self {
30            Self::Bpsk => 1,
31            Self::Qpsk => 2,
32        }
33    }
34
35    /// Number of symbols needed for `n_bits` bits.
36    pub const fn symbols_for(self, n_bits: usize) -> usize {
37        let bps = self.bits_per_symbol();
38        (n_bits + bps - 1) / bps
39    }
40}
41
42// ── Group traits ──────────────────────────────────────────────
43
44/// Maps coded bits to baseband symbols for transmission.
45pub trait Modulator {
46    /// Modulates `n_bits` from `bits` (MSB-first) into `symbols`.
47    ///
48    /// For real-valued schemes (BPSK), symbols are one `f32` per
49    /// bit. For complex schemes (QPSK, OQPSK, 8PSK, GMSK),
50    /// symbols are interleaved I/Q pairs: `[I₀, Q₀, I₁, Q₁, …]`.
51    ///
52    /// Returns the number of `f32` values written to `symbols`.
53    fn modulate(
54        &self,
55        bits: &[u8],
56        n_bits: usize,
57        symbols: &mut [f32],
58    ) -> usize;
59}
60
61/// Converts received symbols to soft-decision LLRs for the decoder.
62pub trait Demodulator {
63    /// Demodulates `symbols` into `n_bits` soft-decision i16 LLRs.
64    ///
65    /// Positive LLR → probably bit 0, negative → probably bit 1.
66    /// Symbol layout matches the corresponding [`Modulator`].
67    fn demodulate_soft(
68        &self,
69        symbols: &[f32],
70        n_bits: usize,
71        llr: &mut [i16],
72    );
73}
74
75// ── Helpers ──────────────────────────────────────────────────
76
77/// Clamps a float to the i16 range (−32767..32767) and truncates.
78pub fn clamp_i16(v: f32) -> i16 {
79    if v > 32767.0 {
80        32767
81    } else if v < -32767.0 {
82        -32767
83    } else {
84        v as i16
85    }
86}
87
88/// Compute noise variance σ² from Eb/N₀ (in dB) and code rate.
89///
90/// For BPSK: σ² = 1 / (2 · rate · 10^(eb_n0_db/10))
91/// For QPSK: same formula (QPSK has same BER vs Eb/N₀ as BPSK).
92pub fn noise_variance(eb_n0_db: f32, code_rate: f32) -> f32 {
93    let eb_n0_lin = libm::powf(10.0, eb_n0_db / 10.0);
94    1.0 / (2.0 * code_rate * eb_n0_lin)
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn scheme_bits_per_symbol() {
103        assert_eq!(Scheme::Bpsk.bits_per_symbol(), 1);
104        assert_eq!(Scheme::Qpsk.bits_per_symbol(), 2);
105        assert_eq!(Scheme::Bpsk.symbols_for(2048), 2048);
106        assert_eq!(Scheme::Qpsk.symbols_for(2048), 1024);
107    }
108}