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}