const N: usize = 240;
const K_INFO: usize = 101;
const N_PARITY: usize = N - K_INFO;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Mode {
UltraRobust,
Robust,
Standard,
Express,
}
impl Mode {
pub const fn header_code(self) -> u8 {
match self {
Mode::UltraRobust => 0,
Mode::Robust => 1,
Mode::Standard => 2,
Mode::Express => 3,
}
}
pub const fn from_header_code(code: u8) -> Option<Self> {
match code {
0 => Some(Mode::UltraRobust),
1 => Some(Mode::Robust),
2 => Some(Mode::Standard),
3 => Some(Mode::Express),
_ => None,
}
}
pub const fn ch_bits_per_block(self) -> usize {
match self {
Mode::UltraRobust => 240,
Mode::Robust => 240,
Mode::Standard => 202,
Mode::Express => 134,
}
}
pub const fn nsps(self) -> usize {
match self {
Mode::UltraRobust => 20,
_ => 10,
}
}
const fn parity_kept(self) -> usize {
self.ch_bits_per_block() - K_INFO
}
}
pub fn keep_indices(mode: Mode) -> Vec<usize> {
use std::sync::OnceLock;
static ROBUST: OnceLock<Vec<usize>> = OnceLock::new();
static STANDARD: OnceLock<Vec<usize>> = OnceLock::new();
static EXPRESS: OnceLock<Vec<usize>> = OnceLock::new();
let cell = match mode {
Mode::UltraRobust | Mode::Robust => &ROBUST,
Mode::Standard => &STANDARD,
Mode::Express => &EXPRESS,
};
cell.get_or_init(|| keep_indices_kSR_greedy(mode)).clone()
}
pub fn keep_indices_uniform(mode: Mode) -> Vec<usize> {
let mut keep = Vec::with_capacity(mode.ch_bits_per_block());
keep.extend(0..K_INFO);
let n_keep = mode.parity_kept();
if n_keep == N_PARITY {
keep.extend(K_INFO..N);
} else if n_keep > 0 {
for p in 0..N_PARITY {
let cur = ((p + 1) * n_keep) / N_PARITY;
let prev = (p * n_keep) / N_PARITY;
if cur > prev {
keep.push(K_INFO + p);
}
}
}
debug_assert_eq!(keep.len(), mode.ch_bits_per_block());
keep
}
#[allow(non_snake_case)]
pub fn keep_indices_kSR_greedy(mode: Mode) -> Vec<usize> {
keep_indices_kSR_greedy_with_count(N_PARITY - mode.parity_kept())
}
#[allow(non_snake_case)]
pub fn keep_indices_kSR_greedy_with_count(target_punctures: usize) -> Vec<usize> {
use crate::fec::ldpc::Ldpc240_101Params as P;
assert!(
target_punctures <= N_PARITY,
"target_punctures must not exceed parity count {N_PARITY}",
);
if target_punctures == 0 {
return (0..N).collect();
}
let mut punctured = vec![false; N];
for _ in 0..target_punctures {
let mut best_p = K_INFO;
let mut best_score = (i64::MIN, i64::MIN);
for p in K_INFO..N {
if punctured[p] {
continue;
}
punctured[p] = true;
let lvls = classify_kSR::<P>(&punctured, 12);
let s = score_levels(&lvls);
if s > best_score {
best_score = s;
best_p = p;
}
punctured[p] = false;
}
punctured[best_p] = true;
}
(0..N).filter(|&i| !punctured[i]).collect()
}
#[allow(non_snake_case)]
pub(crate) fn classify_kSR<P: crate::fec::ldpc::LdpcParams>(
punctured: &[bool],
max_k: u32,
) -> Vec<u32> {
let mut level = vec![u32::MAX; P::N];
for v in 0..P::N {
if !punctured[v] {
level[v] = 0;
}
}
for k in 1..=max_k {
for v in 0..P::N {
if !punctured[v] || level[v] != u32::MAX {
continue;
}
let checks = P::mn(v);
for &c_idx in &checks {
let c = c_idx as usize;
let row_w = P::nrw(c) as usize;
let mut all_lower = true;
for slot in 0..row_w {
let v2 = P::nm(c, slot) as usize;
if v2 == v {
continue;
}
if level[v2] >= k {
all_lower = false;
break;
}
}
if all_lower {
level[v] = k;
break;
}
}
}
}
level
}
fn score_levels(levels: &[u32]) -> (i64, i64) {
let mut unrecoverable = 0i64;
let mut sum_levels = 0i64;
for &l in levels {
if l == u32::MAX {
unrecoverable += 1;
} else {
sum_levels += l as i64;
}
}
(-unrecoverable, -sum_levels)
}
pub fn puncture(codeword: &[u8], mode: Mode) -> Vec<u8> {
assert_eq!(codeword.len(), N, "codeword must be 240 bits");
let keep = keep_indices(mode);
keep.iter().map(|&i| codeword[i]).collect()
}
pub fn de_puncture_llr(channel_llrs: &[f32], mode: Mode) -> Vec<f32> {
assert_eq!(
channel_llrs.len(),
mode.ch_bits_per_block(),
"channel LLR vector must equal mode's ch_bits_per_block",
);
let keep = keep_indices(mode);
let mut out = vec![0.0f32; N];
for (k, &j) in keep.iter().enumerate() {
out[j] = channel_llrs[k];
}
out
}
#[cfg(test)]
mod tests {
use super::*;
const ALL_MODES: [Mode; 4] = [
Mode::Robust,
Mode::Standard,
Mode::UltraRobust,
Mode::Express,
];
#[test]
fn ch_bits_match_keep_indices_len() {
for mode in ALL_MODES {
assert_eq!(keep_indices(mode).len(), mode.ch_bits_per_block());
}
}
#[test]
fn info_bits_always_kept_first() {
for mode in ALL_MODES {
let keep = keep_indices(mode);
for (i, &k) in keep.iter().take(K_INFO).enumerate() {
assert_eq!(k, i, "{mode:?}: info position {i} not kept first");
}
}
}
#[test]
fn keep_indices_sorted_no_dup() {
for mode in ALL_MODES {
let keep = keep_indices(mode);
for w in keep.windows(2) {
assert!(
w[0] < w[1],
"{mode:?}: indices not strictly ascending: {} >= {}",
w[0],
w[1]
);
}
assert!(keep.iter().all(|&i| i < N));
}
}
#[test]
fn puncture_de_puncture_roundtrip() {
let codeword: Vec<u8> = (0..N).map(|i| (i & 1) as u8).collect();
for mode in ALL_MODES {
let punctured = puncture(&codeword, mode);
assert_eq!(punctured.len(), mode.ch_bits_per_block());
let llrs: Vec<f32> = punctured
.iter()
.map(|&b| if b == 0 { 10.0 } else { -10.0 })
.collect();
let expanded = de_puncture_llr(&llrs, mode);
let keep = keep_indices(mode);
let kept_set: std::collections::HashSet<_> = keep.iter().copied().collect();
for i in 0..N {
if kept_set.contains(&i) {
let expected = if codeword[i] == 0 { 10.0 } else { -10.0 };
assert!(
(expanded[i] - expected).abs() < 1e-6,
"{mode:?}: position {i} llr {} ≠ {expected}",
expanded[i]
);
} else {
assert_eq!(expanded[i], 0.0, "{mode:?}: punctured position {i} not 0");
}
}
}
}
#[test]
fn robust_keeps_everything() {
let keep = keep_indices(Mode::Robust);
assert_eq!(keep, (0..N).collect::<Vec<_>>());
}
#[test]
fn rates_match_published_design() {
let cases = [
(Mode::UltraRobust, 240, 0.421),
(Mode::Robust, 240, 0.421),
(Mode::Standard, 202, 0.500),
(Mode::Express, 134, 0.754),
];
for (mode, expected_ch, expected_rate) in cases {
assert_eq!(mode.ch_bits_per_block(), expected_ch);
let rate = K_INFO as f32 / expected_ch as f32;
assert!(
(rate - expected_rate).abs() < 0.005,
"{mode:?}: rate {rate:.3} ≠ {expected_rate}",
);
}
}
fn boxmuller(state: &mut u64) -> f32 {
let mut u = || {
*state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
((((*state) >> 32) & 0xFFFF_FFFF) as f32 + 1.0) / 4_294_967_297.0
};
let u1: f32 = u();
let u2: f32 = u();
(-2.0 * u1.ln()).sqrt() * (2.0 * std::f32::consts::PI * u2).cos()
}
fn awgn_sweep(keep: &[usize], eb_n0_db: f32, n_trials: usize) -> (usize, usize) {
use crate::core::{FecCodec, FecOpts};
use crate::fec::Ldpc240_101;
let fec = Ldpc240_101;
let mut bp_ok = 0usize;
let mut osd_ok = 0usize;
let rate = K_INFO as f32 / keep.len() as f32;
let eb_n0_linear = 10f32.powf(eb_n0_db / 10.0);
let sigma_sq = 1.0 / (2.0 * rate * eb_n0_linear);
let sigma = sigma_sq.sqrt();
for trial in 0..n_trials {
let mut info_state = (trial as u64).wrapping_mul(0x9E37_79B1_5BF0_3F39);
info_state = info_state.wrapping_add(0x1234_5678_DEAD_BEEF);
let mut info = vec![0u8; K_INFO];
for b in info.iter_mut() {
info_state = info_state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
*b = ((info_state >> 33) & 1) as u8;
}
let mut codeword = vec![0u8; N];
fec.encode(&info, &mut codeword);
let mut noise_state = (trial as u64)
.wrapping_mul(0xBF58_476D_1CE4_E5B9)
.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut llrs_full = vec![0.0f32; N];
for &j in keep {
let bit = codeword[j];
let signal = if bit == 1 { 1.0_f32 } else { -1.0 };
let noise = boxmuller(&mut noise_state) * sigma;
let received = signal + noise;
llrs_full[j] = 2.0 * received / sigma_sq;
}
let bp_opts = FecOpts {
bp_max_iter: 50,
osd_depth: 0,
ap_mask: None,
verify_info: None,
};
if let Some(r) = fec.decode_soft(&llrs_full, &bp_opts)
&& r.info == info
{
bp_ok += 1;
osd_ok += 1;
continue;
}
let osd_opts = FecOpts {
bp_max_iter: 50,
osd_depth: 2,
ap_mask: None,
verify_info: None,
};
if let Some(r) = fec.decode_soft(&llrs_full, &osd_opts)
&& r.info == info
{
osd_ok += 1;
}
}
(bp_ok, osd_ok)
}
#[test]
fn modes_decode_at_high_snr() {
let n = 30;
for mode in ALL_MODES {
let unif = keep_indices_uniform(mode);
let greedy = keep_indices_kSR_greedy(mode);
let (u_bp, u_osd) = awgn_sweep(&unif, 12.0, n);
let (g_bp, g_osd) = awgn_sweep(&greedy, 12.0, n);
eprintln!(
"{mode:?} @12dB uniform: BP {u_bp}/{n}, OSD {u_osd}/{n}; \
kSR-greedy: BP {g_bp}/{n}, OSD {g_osd}/{n}"
);
assert!(u_osd >= n * 9 / 10);
assert!(g_osd >= n * 9 / 10);
}
}
#[test]
#[ignore = "slow: AWGN PER sweep across modes × selectors × Eb/N0; run with --ignored"]
#[allow(non_snake_case)]
fn modes_awgn_sweep_uniform_vs_kSR() {
let n = 200;
for mode in ALL_MODES {
let unif = keep_indices_uniform(mode);
let greedy = keep_indices_kSR_greedy(mode);
for eb_n0_db in [-1.0, 0.0, 1.0, 2.0, 3.0, 5.0] {
let (u_bp, u_osd) = awgn_sweep(&unif, eb_n0_db, n);
let (g_bp, g_osd) = awgn_sweep(&greedy, eb_n0_db, n);
eprintln!(
"{mode:?} Eb/N0={eb_n0_db:+.0}dB uniform: BP {u_bp:3}/{n} OSD {u_osd:3}/{n} \
kSR-greedy: BP {g_bp:3}/{n} OSD {g_osd:3}/{n}"
);
}
}
}
#[test]
#[ignore = "slow: experimental rate-3/4 puncturing; run with --ignored"]
fn experimental_rate_3_4() {
let n = 200;
let target_punctures = 106;
let n_keep_parity = N_PARITY - target_punctures;
let mut unif_keep: Vec<usize> = (0..K_INFO).collect();
for p in 0..N_PARITY {
let cur = ((p + 1) * n_keep_parity) / N_PARITY;
let prev = (p * n_keep_parity) / N_PARITY;
if cur > prev {
unif_keep.push(K_INFO + p);
}
}
let greedy_keep = keep_indices_kSR_greedy_with_count(target_punctures);
for eb_n0_db in [3.0, 5.0, 8.0, 12.0] {
let (u_bp, u_osd) = awgn_sweep(&unif_keep, eb_n0_db, n);
let (g_bp, g_osd) = awgn_sweep(&greedy_keep, eb_n0_db, n);
eprintln!(
"rate 3/4 ({target_punctures} punctures) Eb/N0={eb_n0_db:+.0}dB \
uniform: BP {u_bp:3}/{n} OSD {u_osd:3}/{n} \
kSR-greedy: BP {g_bp:3}/{n} OSD {g_osd:3}/{n}"
);
}
}
#[test]
fn header_code_roundtrip() {
for mode in ALL_MODES {
let code = mode.header_code();
assert!(code < 4);
assert_eq!(Mode::from_header_code(code), Some(mode));
}
for code in 4u8..=255 {
assert_eq!(
Mode::from_header_code(code),
None,
"invalid code {code} decoded successfully",
);
}
}
}