1use crate::constellation::{Constellation, log_priors};
49use comnum_core::signal::{Bits, Llr, Symbols};
50use comnum_core::traits::{Demap, Demapping, Map};
51use comnum_core::{Result, ensure, same_len};
52use comnum_math::{C64, Jacobian};
53
54pub const STACK_POINTS: usize = 1024;
57
58#[derive(Clone, Copy, Debug, PartialEq)]
82pub struct LabelHalves {
83 max: [f64; 2],
84 sum: [f64; 2],
85 jacobian: Jacobian,
86}
87
88impl LabelHalves {
89 pub fn new(metric: &[f64], shift: usize, jacobian: Jacobian) -> LabelHalves {
92 let (mut m0, mut m1) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
94 for (i, &v) in metric.iter().enumerate() {
95 if (i >> shift) & 1 == 0 {
96 m0 = m0.max(v);
97 } else {
98 m1 = m1.max(v);
99 }
100 }
101 let mut sum = [1.0, 1.0];
102 if jacobian.is_exact() && (m0.is_finite() || m1.is_finite()) {
103 let (mut s0, mut s1) = (0.0, 0.0);
104 for (i, &v) in metric.iter().enumerate() {
105 if (i >> shift) & 1 == 0 {
106 s0 += (v - m0).exp();
107 } else {
108 s1 += (v - m1).exp();
109 }
110 }
111 sum = [s0, s1];
112 }
113 LabelHalves { max: [m0, m1], sum, jacobian }
114 }
115
116 pub fn log_sum(&self, bit: usize) -> f64 {
119 let m = self.max[bit & 1];
120 if !self.jacobian.is_exact() || !m.is_finite() { m } else { m + self.sum[bit & 1].ln() }
121 }
122
123 pub fn llr(&self) -> f64 {
126 let [m0, m1] = self.max;
127 if !self.jacobian.is_exact() || !m0.is_finite() || !m1.is_finite() {
128 m0 - m1
129 } else {
130 m0 + self.sum[0].ln() - m1 - self.sum[1].ln()
131 }
132 }
133}
134
135pub fn llr_from_metrics(metric: &[f64], jacobian: Jacobian, out: &mut [f64]) {
142 let b = metric.len().trailing_zeros() as usize;
143 for (o, k) in out[..b].iter_mut().zip((0..b).rev()) {
144 *o = LabelHalves::new(metric, k, jacobian).llr();
145 }
146}
147
148impl Constellation {
149 pub fn llr(&self, z: &[C64], noise_var: f64, px: Option<&[f64]>, jacobian: Jacobian) -> Result<Vec<f64>> {
161 let mut out = vec![0.0; z.len() * self.bits_per_symbol()];
162 self.llr_into(z, noise_var, px, jacobian, &mut out)?;
163 Ok(out)
164 }
165
166 pub fn llr_into(
185 &self,
186 z: &[C64],
187 noise_var: f64,
188 px: Option<&[f64]>,
189 jacobian: Jacobian,
190 out: &mut [f64],
191 ) -> Result<()> {
192 ensure_variance(noise_var)?;
193 same_len(out.len(), z.len() * self.bits_per_symbol(), "LLRs and bits of the symbols")?;
194 self.llr_kernel(z, |_| noise_var, self.log_priors_of(px)?.as_deref(), jacobian, out);
195 Ok(())
196 }
197
198 pub fn llr_with_variances(
216 &self,
217 z: &[C64],
218 noise_vars: &[f64],
219 px: Option<&[f64]>,
220 jacobian: Jacobian,
221 ) -> Result<Vec<f64>> {
222 let mut out = vec![0.0; z.len() * self.bits_per_symbol()];
223 self.llr_with_variances_into(z, noise_vars, px, jacobian, &mut out)?;
224 Ok(out)
225 }
226
227 pub fn llr_with_variances_into(
233 &self,
234 z: &[C64],
235 noise_vars: &[f64],
236 px: Option<&[f64]>,
237 jacobian: Jacobian,
238 out: &mut [f64],
239 ) -> Result<()> {
240 same_len(noise_vars.len(), z.len(), "noise variances and symbols")?;
241 for &v in noise_vars {
242 ensure_variance(v)?;
243 }
244 same_len(out.len(), z.len() * self.bits_per_symbol(), "LLRs and bits of the symbols")?;
245 self.llr_kernel(z, |i| noise_vars[i], self.log_priors_of(px)?.as_deref(), jacobian, out);
246 Ok(())
247 }
248
249 fn log_priors_of(&self, px: Option<&[f64]>) -> Result<Option<Vec<f64>>> {
252 Ok(match px {
253 None => None,
254 Some(_) => Some(log_priors(&self.priors(px)?)),
255 })
256 }
257
258 fn check_log_priors(&self, log_px: &[f64]) -> Result<()> {
261 same_len(log_px.len(), self.order(), "log-priors and constellation points")?;
262 ensure(log_px.iter().all(|v| !v.is_nan() && *v < f64::INFINITY), || {
263 "the log-priors must be below +inf and not NaN".into()
264 })?;
265 ensure(log_px.iter().any(|v| v.is_finite()), || "the log-priors leave no point possible".into())
266 }
267
268 fn llr_kernel(
271 &self,
272 z: &[C64],
273 var: impl Fn(usize) -> f64,
274 log_px: Option<&[f64]>,
275 jacobian: Jacobian,
276 out: &mut [f64],
277 ) {
278 let m = self.order();
279 let b = self.bits_per_symbol();
280 let uniform = -(m as f64).ln();
281 let mut stack = [0.0; STACK_POINTS];
282 let mut heap = Vec::new();
283 let metric: &mut [f64] = if m <= STACK_POINTS {
284 &mut stack[..m]
285 } else {
286 heap.resize(m, 0.0);
287 &mut heap
288 };
289 let points = self.points();
290 for (n, (&y, o)) in z.iter().zip(out.chunks_exact_mut(b)).enumerate() {
291 let sigma2 = var(n);
292 for i in 0..m {
293 metric[i] = -(y - points[i]).norm_sqr() / sigma2 + log_px.map_or(uniform, |l| l[i]);
294 }
295 llr_from_metrics(metric, jacobian, o);
296 }
297 }
298}
299
300pub fn bpsk_llr(y: &[f64], noise_var: f64) -> Vec<f64> {
310 y.iter().map(|v| 2.0 * v / noise_var).collect()
311}
312
313fn ensure_variance(noise_var: f64) -> Result<()> {
314 ensure(noise_var > 0.0 && noise_var.is_finite(), || format!("the noise variance must be positive, got {noise_var}"))
315}
316
317impl Map for Constellation {
318 fn map_into(&self, bits: &Bits, out: &mut Symbols) -> Result<()> {
319 let b = self.bits_per_symbol();
320 if bits.len() % b != 0 {
321 return Err(comnum_core::Error::Shape(format!(
322 "{} bits per stream is not a multiple of {b} bits per symbol",
323 bits.len()
324 )));
325 }
326 out.resize(bits.n_streams(), bits.len() / b);
327 for s in 0..bits.n_streams() {
328 let idx = self.indices(bits.stream(s))?;
329 for (o, i) in out.stream_mut(s).iter_mut().zip(idx) {
330 *o = self.points()[i];
331 }
332 }
333 Ok(())
334 }
335}
336
337impl Demap for Constellation {
340 fn demap_with_into(&self, y: &Symbols, noise_var: f64, demapping: Demapping<'_>, out: &mut Llr) -> Result<()> {
345 ensure_variance(noise_var)?;
346 if let Some(lp) = demapping.log_priors {
347 self.check_log_priors(lp)?;
348 }
349 out.resize(y.n_streams(), y.len() * self.bits_per_symbol());
350 for s in 0..y.n_streams() {
351 self.llr_kernel(y.stream(s), |_| noise_var, demapping.log_priors, demapping.jacobian, out.stream_mut(s));
352 }
353 Ok(())
354 }
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360
361 fn received(c: &Constellation) -> Symbols {
363 let p = c.points();
364 let row = |k: usize| {
365 (0..40)
366 .map(|i| p[(7 * i + k) % 16] + C64::new(0.3 * ((i % 5) as f64 - 2.0), 0.1 * (i % 3) as f64))
367 .collect()
368 };
369 Symbols::from_streams(&[row(0), row(3)]).unwrap()
370 }
371
372 #[test]
376 fn demap_with_equals_llr_with_and_without_priors() {
377 let c = Constellation::qam(16).unwrap().normalized();
378 let y = received(&c);
379 let px: Vec<f64> = (1..=16).map(|i| f64::from(i).sqrt()).collect();
380 let lp = log_priors(&c.priors(Some(&px)).unwrap());
381 for jacobian in [Jacobian::Exact, Jacobian::MaxLog] {
382 for (log_priors, probabilities) in [(None, None), (Some(&lp[..]), Some(&px[..]))] {
383 let got = c.demap_with(&y, 0.2, Demapping { log_priors, jacobian }).unwrap();
384 for s in 0..2 {
385 assert_eq!(
386 got.stream(s),
387 c.llr(y.stream(s), 0.2, probabilities, jacobian).unwrap(),
388 "{jacobian:?}"
389 );
390 }
391 }
392 }
393 assert_eq!(c.demap(&y, 0.2).unwrap(), c.demap_with(&y, 0.2, Demapping::default()).unwrap());
394 }
395
396 #[test]
398 fn demap_with_into_reuses_the_output() {
399 for c in [Constellation::qam(16).unwrap(), Constellation::qam(4096).unwrap()] {
400 let y = Symbols::from_streams(&[c.points()[..8].to_vec(), c.points()[8..16].to_vec()]).unwrap();
401 let mut out = Llr::default();
402 c.demap_into(&y, 0.5, &mut out).unwrap();
403 let ptr = out.as_slice().as_ptr();
404 c.demap_with_into(&y, 0.5, Demapping { log_priors: None, jacobian: Jacobian::MaxLog }, &mut out).unwrap();
405 assert_eq!(out.as_slice().as_ptr(), ptr);
406 assert_eq!(out.stream(1), c.llr(y.stream(1), 0.5, None, Jacobian::MaxLog).unwrap());
407 }
408 }
409
410 #[test]
413 fn demap_with_checks_the_log_priors() {
414 let c = Constellation::qam(4).unwrap().normalized();
415 let y = Symbols::from_stream(c.points().to_vec()).unwrap();
416 let with = |lp: &[f64]| c.demap_with(&y, 0.1, Demapping { log_priors: Some(lp), jacobian: Jacobian::Exact });
417 assert!(matches!(with(&[0.0; 3]), Err(comnum_core::Error::Shape(_))));
418 assert!(matches!(with(&[0.0, f64::NAN, 0.0, 0.0]), Err(comnum_core::Error::Config(_))));
419 assert!(matches!(with(&[0.0, f64::INFINITY, 0.0, 0.0]), Err(comnum_core::Error::Config(_))));
420 assert!(matches!(with(&[f64::NEG_INFINITY; 4]), Err(comnum_core::Error::Config(_))));
421 assert!(matches!(c.demap(&y, 0.0), Err(comnum_core::Error::Config(_))));
422 let shifted = with(&[3.0; 4]).unwrap();
423 let uniform = c.demap(&y, 0.1).unwrap();
424 for (a, b) in shifted.as_slice().iter().zip(uniform.as_slice()) {
425 assert!((a - b).abs() < 1e-12);
426 }
427 }
428
429 #[test]
430 fn llr_signs_and_max_log() {
431 let c = Constellation::qam(16).unwrap().normalized();
432 let x = c.points()[0b1010];
433 let l = c.llr(&[x], 0.01, None, Jacobian::Exact).unwrap();
434 let expected = [1u8, 0, 1, 0];
435 for (v, b) in l.iter().zip(expected) {
436 assert!(if b == 0 { *v > 0.0 } else { *v < 0.0 }, "{l:?}");
437 }
438 let lm = c.llr(&[x], 0.01, None, Jacobian::MaxLog).unwrap();
439 for (a, b) in l.iter().zip(&lm) {
440 assert!((a - b).abs() < 1e-3 * a.abs().max(1.0));
441 }
442 }
443
444 fn former_llr(c: &Constellation, z: &[C64], sigma2: f64, log_px: &[f64], max_log: bool) -> Vec<f64> {
447 let (m, b) = (c.order(), c.bits_per_symbol());
448 let mut out = Vec::new();
449 let mut metric = vec![0.0; m];
450 for &y in z {
451 for i in 0..m {
452 metric[i] = -(y - c.points()[i]).norm_sqr() / sigma2 + log_px[i];
453 }
454 for k in (0..b).rev() {
455 let (mut m0, mut m1) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
456 for (i, &v) in metric.iter().enumerate() {
457 if (i >> k) & 1 == 0 {
458 m0 = m0.max(v);
459 } else {
460 m1 = m1.max(v);
461 }
462 }
463 if max_log || !m0.is_finite() || !m1.is_finite() {
464 out.push(m0 - m1);
465 continue;
466 }
467 let (mut s0, mut s1) = (0.0, 0.0);
468 for (i, &v) in metric.iter().enumerate() {
469 if (i >> k) & 1 == 0 {
470 s0 += (v - m0).exp();
471 } else {
472 s1 += (v - m1).exp();
473 }
474 }
475 out.push(m0 + s0.ln() - m1 - s1.ln());
476 }
477 }
478 out
479 }
480
481 #[test]
482 fn the_shared_kernel_gives_the_former_llrs_bit_for_bit() {
483 let mut rng = comnum_core::rng::Rng::new(77);
484 for c in [
485 Constellation::qam(16).unwrap().normalized(),
486 Constellation::psk(8).unwrap().normalized(),
487 Constellation::pam(4).unwrap().normalized(),
488 Constellation::qam(256).unwrap().normalized(),
489 ] {
490 let m = c.order();
491 let z: Vec<C64> = (0..200).map(|i| c.points()[i % m] + rng.complex_gaussian() * 0.3).collect();
492 let shaped: Vec<f64> = (0..m).map(|i| 1.0 + (i % 3) as f64).collect();
493 let lp = log_priors(&c.priors(Some(&shaped)).unwrap());
494 let uniform = vec![-(m as f64).ln(); m];
495 for (px, table) in [(None, &uniform), (Some(&shaped[..]), &lp)] {
496 for max_log in [false, true] {
497 for s2 in [1e-3, 0.1, 2.0] {
498 let new = c.llr(&z, s2, px, Jacobian::from_max_log(max_log)).unwrap();
499 let old = former_llr(&c, &z, s2, table, max_log);
500 assert!(new.iter().zip(&old).all(|(a, b)| a.to_bits() == b.to_bits()), "M {m}, {s2}");
501 }
502 }
503 }
504 let mut out = vec![0.0; c.bits_per_symbol() + 1];
505 assert!(c.llr_into(&z[..1], 0.1, None, Jacobian::Exact, &mut out).is_err());
506 }
507 }
508
509 #[test]
510 fn multi_stream_map_demap_round_trip() {
511 let c = Constellation::qam(16).unwrap().normalized();
512 let mut rng = comnum_core::rng::Rng::new(5);
513 let streams: Vec<Vec<u8>> = (0..3).map(|_| (0..64).map(|_| rng.bit()).collect()).collect();
514 let bits = Bits::from_streams(&streams).unwrap();
515 let x = c.map(&bits).unwrap();
516 assert_eq!((x.n_streams(), x.len()), (3, 16));
517 let llr = c.demap(&x, 1e-3).unwrap();
518 let hard: Vec<u8> = comnum_core::signal::hard_decisions(llr.as_slice());
519 assert_eq!(hard, bits.as_slice());
520 assert!(c.map(&Bits::from_stream(vec![0; 3]).unwrap()).is_err());
521 let _ = C64::new(0.0, 0.0);
522 }
523}