leodos_protocols/physical/modulator/
eight_psk.rs1use super::clamp_i16;
24
25static GRAY: [u8; 8] = [0, 1, 3, 2, 6, 7, 5, 4];
28
29static GRAY_INV: [u8; 8] = {
32 let mut t = [0u8; 8];
33 let mut i = 0;
34 while i < 8 {
35 t[GRAY[i] as usize] = i as u8;
36 i += 1;
37 }
38 t
39};
40
41static CONSTELLATION: [(f32, f32); 8] = {
44 let s = 0.707_106_77; [
48 (1.0, 0.0), (s, s), (0.0, 1.0), (-s, s), (-1.0, 0.0), (-s, -s), (0.0, -1.0), (s, -s), ]
57};
58
59static BIT_SETS: [[[u8; 4]; 2]; 3] = {
65 let mut sets = [[[0u8; 4]; 2]; 3];
66 let mut bit = 0;
67 while bit < 3 {
68 let mut count0 = 0usize;
69 let mut count1 = 0usize;
70 let mut idx = 0;
71 while idx < 8 {
72 if (GRAY[idx] >> (2 - bit)) & 1 == 0 {
73 sets[bit][0][count0] = idx as u8;
74 count0 += 1;
75 } else {
76 sets[bit][1][count1] = idx as u8;
77 count1 += 1;
78 }
79 idx += 1;
80 }
81 bit += 1;
82 }
83 sets
84};
85
86pub const BITS_PER_SYMBOL: usize = 3;
88
89pub fn modulate_8psk(
97 bits: &[u8],
98 n_bits: usize,
99 symbols_i: &mut [f32],
100 symbols_q: &mut [f32],
101) {
102 assert!(n_bits % 3 == 0);
103 let n_sym = n_bits / 3;
104 assert!(bits.len() * 8 >= n_bits);
105 assert!(symbols_i.len() >= n_sym);
106 assert!(symbols_q.len() >= n_sym);
107
108 for k in 0..n_sym {
109 let base = 3 * k;
110 let b0 = ((bits[base / 8] >> (7 - (base % 8))) & 1) as u8;
111 let b1 =
112 ((bits[(base + 1) / 8] >> (7 - ((base + 1) % 8))) & 1) as u8;
113 let b2 =
114 ((bits[(base + 2) / 8] >> (7 - ((base + 2) % 8))) & 1) as u8;
115
116 let data = (b0 << 2) | (b1 << 1) | b2;
117 let idx = GRAY_INV[data as usize] as usize;
118 let (ci, cq) = CONSTELLATION[idx];
119 symbols_i[k] = ci;
120 symbols_q[k] = cq;
121 }
122}
123
124pub fn demodulate_8psk(
133 symbols_i: &[f32],
134 symbols_q: &[f32],
135 n_bits: usize,
136 noise_var: f32,
137 scale: f32,
138 llr: &mut [i16],
139) {
140 assert!(n_bits % 3 == 0);
141 let n_sym = n_bits / 3;
142 assert!(symbols_i.len() >= n_sym);
143 assert!(symbols_q.len() >= n_sym);
144 assert!(llr.len() >= n_bits);
145
146 let factor = scale / noise_var;
147
148 for k in 0..n_sym {
149 let yi = symbols_i[k];
150 let yq = symbols_q[k];
151
152 let mut dist2 = [0f32; 8];
154 for p in 0..8 {
155 let (ci, cq) = CONSTELLATION[p];
156 let di = yi - ci;
157 let dq = yq - cq;
158 dist2[p] = di * di + dq * dq;
159 }
160
161 for bit in 0..3 {
163 let mut min_d0 = f32::MAX;
164 for &idx in &BIT_SETS[bit][0] {
165 let d = dist2[idx as usize];
166 if d < min_d0 {
167 min_d0 = d;
168 }
169 }
170
171 let mut min_d1 = f32::MAX;
172 for &idx in &BIT_SETS[bit][1] {
173 let d = dist2[idx as usize];
174 if d < min_d1 {
175 min_d1 = d;
176 }
177 }
178
179 let raw = factor * (min_d1 - min_d0);
181 llr[3 * k + bit] = clamp_i16(raw);
182 }
183 }
184}
185
186pub struct EightPsk {
188 noise_var: f32,
189 scale: f32,
190}
191
192impl EightPsk {
193 pub fn new(noise_var: f32, scale: f32) -> Self {
196 Self { noise_var, scale }
197 }
198}
199
200impl super::Modulator for EightPsk {
201 fn modulate(
202 &self,
203 bits: &[u8],
204 n_bits: usize,
205 symbols: &mut [f32],
206 ) -> usize {
207 let n_sym = n_bits / BITS_PER_SYMBOL;
208 let (si, sq) = symbols.split_at_mut(n_sym);
209 modulate_8psk(bits, n_bits, si, sq);
210 n_sym * 2
211 }
212}
213
214impl super::Demodulator for EightPsk {
215 fn demodulate_soft(
216 &self,
217 symbols: &[f32],
218 n_bits: usize,
219 llr: &mut [i16],
220 ) {
221 let n_sym = n_bits / BITS_PER_SYMBOL;
222 let (si, sq) = symbols.split_at(n_sym);
223 demodulate_8psk(
224 si, sq, n_bits, self.noise_var, self.scale, llr,
225 );
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 #[test]
234 fn gray_code_adjacency() {
235 for i in 0..8 {
237 let j = (i + 1) % 8;
238 let diff = GRAY[i] ^ GRAY[j];
239 assert_eq!(
240 diff.count_ones(),
241 1,
242 "points {i} and {j} differ by {} bits",
243 diff.count_ones()
244 );
245 }
246 }
247
248 #[test]
249 fn gray_inverse_roundtrip() {
250 for data in 0..8u8 {
251 let idx = GRAY_INV[data as usize];
252 assert_eq!(GRAY[idx as usize], data);
253 }
254 }
255
256 #[test]
257 fn constellation_unit_energy() {
258 for (i, &(ci, cq)) in CONSTELLATION.iter().enumerate() {
259 let energy = ci * ci + cq * cq;
260 assert!(
261 (energy - 1.0).abs() < 1e-5,
262 "point {i}: energy = {energy}"
263 );
264 }
265 }
266
267 #[test]
268 fn modulate_known_symbols() {
269 let bits = [0b000_000_00u8];
271 let mut si = [0f32; 2];
272 let mut sq = [0f32; 2];
273 modulate_8psk(&bits, 6, &mut si, &mut sq);
274 assert!((si[0] - 1.0).abs() < 1e-6);
275 assert!(sq[0].abs() < 1e-6);
276 }
277
278 #[test]
279 fn roundtrip_no_noise() {
280 let bits = [0b101_011_00, 0b0_110_0000];
282 let mut si = [0f32; 4];
283 let mut sq = [0f32; 4];
284 modulate_8psk(&bits, 12, &mut si, &mut sq);
285
286 let mut llr = [0i16; 12];
287 demodulate_8psk(&si, &sq, 12, 0.5, 100.0, &mut llr);
288
289 let mut recovered = [0u8; 2];
290 for i in 0..12 {
291 if llr[i] < 0 {
292 recovered[i / 8] |= 1 << (7 - (i % 8));
293 }
294 }
295 assert_eq!(recovered[0], 0b10101100);
297 assert_eq!(recovered[1] & 0xF0, 0b01100000);
298 }
299
300 #[test]
301 fn all_symbols_roundtrip() {
302 let bits = [0b000_001_01, 0b0_011_100_1, 0b01_110_111];
305 let mut si = [0f32; 8];
306 let mut sq = [0f32; 8];
307 modulate_8psk(&bits, 24, &mut si, &mut sq);
308
309 let mut llr = [0i16; 24];
310 demodulate_8psk(&si, &sq, 24, 0.5, 100.0, &mut llr);
311
312 let mut recovered = [0u8; 3];
313 for i in 0..24 {
314 if llr[i] < 0 {
315 recovered[i / 8] |= 1 << (7 - (i % 8));
316 }
317 }
318 assert_eq!(recovered, bits);
319 }
320
321 #[test]
322 fn bit_sets_partition() {
323 for bit in 0..3 {
325 let mut seen = [false; 8];
326 for &idx in &BIT_SETS[bit][0] {
327 seen[idx as usize] = true;
328 }
329 for &idx in &BIT_SETS[bit][1] {
330 seen[idx as usize] = true;
331 }
332 assert!(seen.iter().all(|&s| s), "bit {bit} doesn't cover all");
333 }
334 }
335}