Skip to main content

leodos_protocols/physical/modulator/
qpsk.rs

1//! QPSK modulation and demodulation.
2//!
3//! Quadrature Phase Shift Keying maps two bits per complex symbol:
4//! - bit 0 → I = ±1/√2
5//! - bit 1 → Q = ±1/√2
6
7use super::clamp_i16;
8
9/// Modulate packed bits to QPSK symbols.
10///
11/// Maps consecutive bit pairs (MSB-first) to I/Q components:
12/// - bit 0 → I = 1−2·b₀
13/// - bit 1 → Q = 1−2·b₁
14///
15/// Each component is ±1/√2 for unit energy per symbol.
16/// `n_bits` must be even. Writes `n_bits/2` I values and
17/// `n_bits/2` Q values.
18pub fn modulate(
19    bits: &[u8],
20    n_bits: usize,
21    symbols_i: &mut [f32],
22    symbols_q: &mut [f32],
23) {
24    assert!(n_bits % 2 == 0);
25    let n_sym = n_bits / 2;
26    assert!(bits.len() * 8 >= n_bits);
27    assert!(symbols_i.len() >= n_sym);
28    assert!(symbols_q.len() >= n_sym);
29
30    let s = core::f32::consts::FRAC_1_SQRT_2;
31
32    for k in 0..n_sym {
33        let bit_i = 2 * k;
34        let bit_q = 2 * k + 1;
35        let b0 = ((bits[bit_i / 8] >> (7 - (bit_i % 8))) & 1) as f32;
36        let b1 = ((bits[bit_q / 8] >> (7 - (bit_q % 8))) & 1) as f32;
37        symbols_i[k] = s * (1.0 - 2.0 * b0);
38        symbols_q[k] = s * (1.0 - 2.0 * b1);
39    }
40}
41
42/// Demodulate QPSK symbols to soft-decision i16 LLRs.
43///
44/// Produces `n_bits` LLRs from `n_bits/2` I/Q symbol pairs.
45/// LLRs are interleaved: even indices from I, odd from Q.
46pub fn demodulate(
47    symbols_i: &[f32],
48    symbols_q: &[f32],
49    n_bits: usize,
50    noise_var: f32,
51    scale: f32,
52    llr: &mut [i16],
53) {
54    assert!(n_bits % 2 == 0);
55    let n_sym = n_bits / 2;
56    assert!(symbols_i.len() >= n_sym);
57    assert!(symbols_q.len() >= n_sym);
58    assert!(llr.len() >= n_bits);
59
60    let s = core::f32::consts::FRAC_1_SQRT_2;
61    let factor = scale * 2.0 / noise_var;
62
63    // Undo the 1/√2 scaling: multiply received by √2
64    // so effective LLR = 2·(y·√2)/σ² = 2·y/(σ²/√2)
65    // Equivalently: factor already includes the geometry.
66    let f = factor * s;
67
68    for k in 0..n_sym {
69        // I component → even bit, Q component → odd bit
70        llr[2 * k] = clamp_i16(symbols_i[k] * f);
71        llr[2 * k + 1] = clamp_i16(symbols_q[k] * f);
72    }
73}
74
75/// QPSK modulator/demodulator with configurable noise parameters.
76pub struct Qpsk {
77    noise_var: f32,
78    scale: f32,
79}
80
81impl Qpsk {
82    /// Creates a QPSK modem with the given noise variance and
83    /// LLR scale factor.
84    pub fn new(noise_var: f32, scale: f32) -> Self {
85        Self { noise_var, scale }
86    }
87}
88
89impl super::Modulator for Qpsk {
90    fn modulate(
91        &self,
92        bits: &[u8],
93        n_bits: usize,
94        symbols: &mut [f32],
95    ) -> usize {
96        let n_sym = n_bits / 2;
97        let (si, sq) = symbols.split_at_mut(n_sym);
98        modulate(bits, n_bits, si, sq);
99        n_sym * 2
100    }
101}
102
103impl super::Demodulator for Qpsk {
104    fn demodulate_soft(
105        &self,
106        symbols: &[f32],
107        n_bits: usize,
108        llr: &mut [i16],
109    ) {
110        let n_sym = n_bits / 2;
111        let (si, sq) = symbols.split_at(n_sym);
112        demodulate(si, sq, n_bits, self.noise_var, self.scale, llr);
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119    use super::super::noise_variance;
120
121    #[test]
122    fn modulate_unit_energy() {
123        let bits = [0xFF]; // 8 bits → 4 QPSK symbols
124        let mut si = [0f32; 4];
125        let mut sq = [0f32; 4];
126        modulate(&bits, 8, &mut si, &mut sq);
127
128        // All bits = 1, so all components = -1/√2
129        let s = -core::f32::consts::FRAC_1_SQRT_2;
130        for k in 0..4 {
131            assert!((si[k] - s).abs() < 1e-6);
132            assert!((sq[k] - s).abs() < 1e-6);
133            // Unit energy: I² + Q² = 1
134            let energy = si[k] * si[k] + sq[k] * sq[k];
135            assert!((energy - 1.0).abs() < 1e-6);
136        }
137    }
138
139    #[test]
140    fn roundtrip_hard() {
141        let bits = [0xC3, 0x5A]; // 16 bits → 8 symbols
142        let mut si = [0f32; 8];
143        let mut sq = [0f32; 8];
144        modulate(&bits, 16, &mut si, &mut sq);
145
146        let mut llr = [0i16; 16];
147        demodulate(&si, &sq, 16, 0.5, 100.0, &mut llr);
148
149        let mut recovered = [0u8; 2];
150        for i in 0..16 {
151            if llr[i] < 0 {
152                recovered[i / 8] |= 1 << (7 - (i % 8));
153            }
154        }
155        assert_eq!(recovered, bits);
156    }
157
158    #[test]
159    fn noise_variance_known_values() {
160        // Eb/N0 = 0 dB, rate 1/2 → σ² = 1/(2·0.5·1) = 1.0
161        let v = noise_variance(0.0, 0.5);
162        assert!((v - 1.0).abs() < 1e-6);
163
164        // Eb/N0 = 10 dB, rate 1 → σ² = 1/(2·1·10) = 0.05
165        let v = noise_variance(10.0, 1.0);
166        assert!((v - 0.05).abs() < 1e-4);
167    }
168}