pub const HARD_ERASURE: u8 = 0xFF;
pub trait BranchMetric {
type Sample: Copy;
const MAX_SPREAD: u32;
const PER_BIT_MAX: u32 = u32::MAX;
fn branch_distance(expected: u32, received: &[Self::Sample]) -> u32;
fn erasure() -> Self::Sample;
}
pub struct HardHamming;
impl BranchMetric for HardHamming {
type Sample = u8;
const MAX_SPREAD: u32 = 256;
const PER_BIT_MAX: u32 = 1;
fn branch_distance(expected: u32, received: &[Self::Sample]) -> u32 {
let n = received.len();
let mut d: u32 = 0;
for (j, &s) in received.iter().enumerate() {
if s == HARD_ERASURE {
continue;
}
let shift = n - 1 - j;
let exp_bit = if shift < 32 {
((expected >> shift) & 1) as u8
} else {
0
};
if exp_bit != (s & 1) {
d = d.saturating_add(1); }
}
d
}
fn erasure() -> u8 {
HARD_ERASURE
}
}
pub const SOFT_MAX_SPREAD: u32 = 4096;
pub struct SoftLlr;
impl BranchMetric for SoftLlr {
type Sample = i8;
const MAX_SPREAD: u32 = SOFT_MAX_SPREAD;
const PER_BIT_MAX: u32 = 128;
fn branch_distance(expected: u32, received: &[Self::Sample]) -> u32 {
let n = received.len();
let mut d: u32 = 0;
for (j, &lam) in received.iter().enumerate() {
let shift = n - 1 - j; let exp_bit = if shift < 32 {
(expected >> shift) & 1
} else {
0
};
let disagree = (exp_bit == 0 && lam < 0) || (exp_bit == 1 && lam > 0);
if disagree {
d = d.saturating_add(u32::from(lam.unsigned_abs()));
}
}
d
}
fn erasure() -> i8 {
0
}
}
impl SoftLlr {
#[must_use]
pub fn llr_from_bpsk(sample: f32, scale: f32) -> i8 {
debug_assert!(
scale.partial_cmp(&0.0) != Some(core::cmp::Ordering::Less),
"llr_from_bpsk scale is a magnitude; negative is a likely caller mistake"
);
(sample * scale.abs()).round().clamp(-127.0, 127.0) as i8
}
#[must_use]
pub fn llr_from_bpsk_int(sample: i16, shift: u8) -> i8 {
(sample >> shift.min(15)).clamp(-127, 127) as i8
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hamming_counts_bit_mismatches() {
assert_eq!(HardHamming::branch_distance(0b10, &[1, 0]), 0);
assert_eq!(HardHamming::branch_distance(0b10, &[0, 0]), 1);
assert_eq!(HardHamming::branch_distance(0b10, &[0, 1]), 2);
}
#[test]
fn erasure_sample_is_free() {
assert_eq!(HardHamming::erasure(), 0xFF);
assert_eq!(HardHamming::branch_distance(0b11, &[0xFF, 0xFF]), 0);
assert_eq!(HardHamming::branch_distance(0b11, &[0xFF, 0]), 1); }
#[test]
fn soft_llr_agree_costs_zero_disagree_costs_abs() {
assert_eq!(SoftLlr::branch_distance(0b10, &[40, -20]), 60); assert_eq!(SoftLlr::branch_distance(0b10, &[-40, 20]), 0); }
#[test]
fn soft_llr_erasure_zero_and_min_is_safe() {
assert_eq!(SoftLlr::erasure(), 0);
assert_eq!(SoftLlr::branch_distance(0b11, &[0, 0]), 0); assert_eq!(SoftLlr::branch_distance(0b00, &[i8::MIN, i8::MIN]), 256); assert_eq!(SoftLlr::branch_distance(0b11, &[i8::MIN, i8::MIN]), 0); }
#[test]
fn soft_llr_reduces_to_hamming_at_unit_llr() {
assert_eq!(SoftLlr::branch_distance(0b10, &[-1, 1]), 0);
assert_eq!(SoftLlr::branch_distance(0b10, &[1, 1]), 1);
assert_eq!(SoftLlr::branch_distance(0b10, &[1, -1]), 2);
}
#[test]
fn soft_llr_equals_hamming_over_all_2bit_symbols() {
for expected in 0u32..4 {
for &a in &[-1i8, 1] {
for &b in &[-1i8, 1] {
let hard = [u8::from(a < 0), u8::from(b < 0)];
assert_eq!(
SoftLlr::branch_distance(expected, &[a, b]),
HardHamming::branch_distance(expected, &hard),
"reduction mismatch at expected={expected} a={a} b={b}"
);
}
}
}
}
#[test]
fn llr_helpers_signed_clipped_never_min() {
assert!(SoftLlr::llr_from_bpsk(10.0, 100.0) > 0);
assert_eq!(SoftLlr::llr_from_bpsk(1000.0, 1000.0), 127);
assert_eq!(SoftLlr::llr_from_bpsk(-1000.0, 1000.0), -127);
assert_eq!(SoftLlr::llr_from_bpsk_int(i16::MIN, 0), -127); assert_eq!(SoftLlr::llr_from_bpsk_int(i16::MIN, 99), -1); assert_eq!(SoftLlr::llr_from_bpsk(f32::NAN, 100.0), 0); }
}