Skip to main content

leodos_protocols/physical/modulator/
bpsk.rs

1//! BPSK modulation and demodulation.
2//!
3//! Binary Phase Shift Keying maps one bit per symbol:
4//! - bit 0 → +1.0
5//! - bit 1 → −1.0
6
7use super::clamp_i16;
8
9/// Modulate packed bits to BPSK symbols (+1.0 / −1.0).
10///
11/// Reads `n_bits` from `bits` (MSB-first) and writes one `f32`
12/// symbol per bit into `symbols`.
13pub fn modulate(
14    bits: &[u8],
15    n_bits: usize,
16    symbols: &mut [f32],
17) {
18    assert!(bits.len() * 8 >= n_bits);
19    assert!(symbols.len() >= n_bits);
20
21    for i in 0..n_bits {
22        let byte = bits[i / 8];
23        let bit = (byte >> (7 - (i % 8))) & 1;
24        symbols[i] = 1.0 - 2.0 * bit as f32;
25    }
26}
27
28/// Demodulate BPSK symbols to soft-decision i16 LLRs.
29///
30/// `noise_var` is σ² (noise variance = N₀/2). The `scale`
31/// factor converts floating-point LLRs to the i16 range used
32/// by the LDPC decoder. A value of 100–500 works well.
33///
34/// The exact LLR is `2 · y / σ²`, quantized as
35/// `clamp(round(scale · 2 · y / σ²), −32767, 32767)`.
36pub fn demodulate(
37    symbols: &[f32],
38    n_bits: usize,
39    noise_var: f32,
40    scale: f32,
41    llr: &mut [i16],
42) {
43    assert!(symbols.len() >= n_bits);
44    assert!(llr.len() >= n_bits);
45
46    let factor = scale * 2.0 / noise_var;
47    for i in 0..n_bits {
48        let v = symbols[i] * factor;
49        llr[i] = clamp_i16(v);
50    }
51}
52
53/// BPSK modulator/demodulator with configurable noise parameters.
54pub struct Bpsk {
55    noise_var: f32,
56    scale: f32,
57}
58
59impl Bpsk {
60    /// Creates a BPSK modem with the given noise variance and
61    /// LLR scale factor.
62    pub fn new(noise_var: f32, scale: f32) -> Self {
63        Self { noise_var, scale }
64    }
65}
66
67impl super::Modulator for Bpsk {
68    fn modulate(
69        &self,
70        bits: &[u8],
71        n_bits: usize,
72        symbols: &mut [f32],
73    ) -> usize {
74        modulate(bits, n_bits, symbols);
75        n_bits
76    }
77}
78
79impl super::Demodulator for Bpsk {
80    fn demodulate_soft(
81        &self,
82        symbols: &[f32],
83        n_bits: usize,
84        llr: &mut [i16],
85    ) {
86        demodulate(symbols, n_bits, self.noise_var, self.scale, llr);
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn modulate_basic() {
96        // 0xA5 = 0b10100101 → [-1,+1,-1,+1,+1,-1,+1,-1]
97        let bits = [0xA5u8];
98        let mut sym = [0f32; 8];
99        modulate(&bits, 8, &mut sym);
100        let expected = [-1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0];
101        assert_eq!(sym, expected);
102    }
103
104    #[test]
105    fn roundtrip_hard() {
106        let bits = [0xC3, 0x5A]; // 16 bits
107        let mut sym = [0f32; 16];
108        modulate(&bits, 16, &mut sym);
109
110        // No noise → perfect LLRs
111        let mut llr = [0i16; 16];
112        demodulate(&sym, 16, 0.5, 100.0, &mut llr);
113
114        // Recover bits from LLR signs
115        let mut recovered = [0u8; 2];
116        for i in 0..16 {
117            if llr[i] < 0 {
118                recovered[i / 8] |= 1 << (7 - (i % 8));
119            }
120        }
121        assert_eq!(recovered, bits);
122    }
123
124    #[test]
125    fn partial_byte() {
126        // Only 3 bits from 0xE0 = 0b111_00000
127        let bits = [0xE0u8];
128        let mut sym = [0f32; 3];
129        modulate(&bits, 3, &mut sym);
130        assert_eq!(sym, [-1.0, -1.0, -1.0]);
131    }
132
133    #[test]
134    fn with_awgn() {
135        // Simulate light noise and verify demodulation still works
136        let bits = [0xA5u8]; // 10100101
137        let mut sym = [0f32; 8];
138        modulate(&bits, 8, &mut sym);
139
140        // Add small deterministic "noise"
141        for (i, s) in sym.iter_mut().enumerate() {
142            *s += 0.1 * (i as f32 - 4.0) / 4.0;
143        }
144
145        let mut llr = [0i16; 8];
146        demodulate(&sym, 8, 0.5, 100.0, &mut llr);
147
148        // Should still decode correctly (noise is small)
149        let mut recovered = [0u8; 1];
150        for i in 0..8 {
151            if llr[i] < 0 {
152                recovered[0] |= 1 << (7 - i);
153            }
154        }
155        assert_eq!(recovered[0], 0xA5);
156    }
157}