clippy needless_range_loop (rust 1.97) flagged the loops in ldpc_check and ldpc_decode that use a range only to index one array. Replace them with iterator/enumerate forms. The belief-propagation loops that index several arrays by the same variable are left as-is (not flagged). Behaviour is unchanged; the transformations are index-for-index equivalent. Verified the lib compiles and is clippy-clean; the crate's LDPC tests run in the CI test job (they need a dev-dependency not available in the local offline sandbox). Assisted-By: Claude Code (claude-opus-4) Claude-Session: https://claude.ai/code/session_01NFpGtGTWUEYXLwZeZs2RAV Signed-off-by: Stan Grams <sjg@haxx.space>
292 lines
8.8 KiB
Rust
292 lines
8.8 KiB
Rust
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
|
//
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
//! Pure Rust LDPC decoder for FTx protocols.
|
|
//!
|
|
//! This is a port of the sum-product and belief-propagation LDPC decoders
|
|
//! from ft8_lib's `ldpc.c`. Given a 174-bit codeword as an array of
|
|
//! log-likelihood ratios (log(P(x=0)/P(x=1))), returns a corrected 174-bit
|
|
//! codeword. The last 87 bits are the systematic plain-text.
|
|
|
|
use super::constants::{FTX_LDPC_MN, FTX_LDPC_NM, FTX_LDPC_NUM_ROWS};
|
|
use super::protocol::{FTX_LDPC_M, FTX_LDPC_N};
|
|
|
|
/// Fast rational approximation of `tanh(x)`, clamped at +/-4.97.
|
|
pub(crate) fn fast_tanh(x: f32) -> f32 {
|
|
if x < -4.97f32 {
|
|
return -1.0f32;
|
|
}
|
|
if x > 4.97f32 {
|
|
return 1.0f32;
|
|
}
|
|
let x2 = x * x;
|
|
let a = x * (945.0f32 + x2 * (105.0f32 + x2));
|
|
let b = 945.0f32 + x2 * (420.0f32 + x2 * 15.0f32);
|
|
a / b
|
|
}
|
|
|
|
/// Fast rational approximation of `atanh(x)`.
|
|
pub(crate) fn fast_atanh(x: f32) -> f32 {
|
|
let x2 = x * x;
|
|
let a = x * (945.0f32 + x2 * (-735.0f32 + x2 * 64.0f32));
|
|
let b = 945.0f32 + x2 * (-1050.0f32 + x2 * 225.0f32);
|
|
a / b
|
|
}
|
|
|
|
/// Count the number of LDPC parity errors in a 174-bit codeword.
|
|
///
|
|
/// Returns 0 if all parity checks pass (valid codeword).
|
|
pub(crate) fn ldpc_check(codeword: &[u8; FTX_LDPC_N]) -> i32 {
|
|
let mut errors = 0i32;
|
|
for m in 0..FTX_LDPC_M {
|
|
let mut x: u8 = 0;
|
|
let num_rows = FTX_LDPC_NUM_ROWS[m] as usize;
|
|
for &nm in FTX_LDPC_NM[m].iter().take(num_rows) {
|
|
x ^= codeword[nm as usize - 1];
|
|
}
|
|
if x != 0 {
|
|
errors += 1;
|
|
}
|
|
}
|
|
errors
|
|
}
|
|
|
|
/// Sum-product LDPC decoder.
|
|
///
|
|
/// `codeword` contains 174 log-likelihood ratios (modified in place during
|
|
/// decoding). `plain` receives the decoded 174-bit hard decisions (0 or 1).
|
|
/// `max_iters` controls how many iterations to attempt.
|
|
///
|
|
/// Returns the number of remaining parity errors (0 = success).
|
|
#[cfg(test)]
|
|
pub fn ldpc_decode(
|
|
codeword: &mut [f32; FTX_LDPC_N],
|
|
max_iters: usize,
|
|
plain: &mut [u8; FTX_LDPC_N],
|
|
) -> i32 {
|
|
// Flat arrays for m[][] and e[][] (~57 kB each, ~114 kB total on stack).
|
|
let mut m_matrix = [0.0f32; FTX_LDPC_M * FTX_LDPC_N];
|
|
let mut e_matrix = [0.0f32; FTX_LDPC_M * FTX_LDPC_N];
|
|
|
|
// Initialize m[][] with the channel LLRs.
|
|
for j in 0..FTX_LDPC_M {
|
|
m_matrix[j * FTX_LDPC_N..][..FTX_LDPC_N].copy_from_slice(codeword);
|
|
}
|
|
|
|
let mut min_errors = FTX_LDPC_M as i32;
|
|
|
|
for _iter in 0..max_iters {
|
|
// Update e[][] from m[][]
|
|
for j in 0..FTX_LDPC_M {
|
|
let num_rows = FTX_LDPC_NUM_ROWS[j] as usize;
|
|
let m_row = j * FTX_LDPC_N;
|
|
for &nm1 in FTX_LDPC_NM[j].iter().take(num_rows) {
|
|
let i1 = nm1 as usize - 1;
|
|
let mut a = 1.0f32;
|
|
for &nm2 in FTX_LDPC_NM[j].iter().take(num_rows) {
|
|
let i2 = nm2 as usize - 1;
|
|
if i2 != i1 {
|
|
a *= fast_tanh(-m_matrix[m_row + i2] / 2.0f32);
|
|
}
|
|
}
|
|
e_matrix[j * FTX_LDPC_N + i1] = -2.0f32 * fast_atanh(a);
|
|
}
|
|
}
|
|
|
|
// Hard decisions
|
|
for i in 0..FTX_LDPC_N {
|
|
let mut l = codeword[i];
|
|
for &mn in FTX_LDPC_MN[i].iter().take(3) {
|
|
l += e_matrix[(mn as usize - 1) * FTX_LDPC_N + i];
|
|
}
|
|
plain[i] = if l > 0.0 { 1 } else { 0 };
|
|
}
|
|
|
|
let errors = ldpc_check(plain);
|
|
if errors < min_errors {
|
|
min_errors = errors;
|
|
if errors == 0 {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Update m[][] from e[][]
|
|
for i in 0..FTX_LDPC_N {
|
|
for (ji1, &mn1) in FTX_LDPC_MN[i].iter().enumerate().take(3) {
|
|
let j1 = mn1 as usize - 1;
|
|
let mut l = codeword[i];
|
|
for (ji2, &mn2) in FTX_LDPC_MN[i].iter().enumerate().take(3) {
|
|
if ji1 != ji2 {
|
|
let j2 = mn2 as usize - 1;
|
|
l += e_matrix[j2 * FTX_LDPC_N + i];
|
|
}
|
|
}
|
|
m_matrix[j1 * FTX_LDPC_N + i] = l;
|
|
}
|
|
}
|
|
}
|
|
|
|
min_errors
|
|
}
|
|
|
|
/// Belief-propagation LDPC decoder.
|
|
///
|
|
/// `codeword` contains 174 log-likelihood ratios. `plain` receives the
|
|
/// decoded 174-bit hard decisions (0 or 1). `max_iters` controls how many
|
|
/// iterations to attempt.
|
|
///
|
|
/// Returns the number of remaining parity errors (0 = success).
|
|
pub fn bp_decode(
|
|
codeword: &[f32; FTX_LDPC_N],
|
|
max_iters: usize,
|
|
plain: &mut [u8; FTX_LDPC_N],
|
|
) -> i32 {
|
|
let mut tov = [[0.0f32; 3]; FTX_LDPC_N];
|
|
let mut toc = [[0.0f32; 7]; FTX_LDPC_M];
|
|
|
|
let mut min_errors = FTX_LDPC_M as i32;
|
|
|
|
for _iter in 0..max_iters {
|
|
// Hard decision guess (tov=0 in iter 0)
|
|
let mut plain_sum = 0u32;
|
|
for n in 0..FTX_LDPC_N {
|
|
let sum = codeword[n] + tov[n][0] + tov[n][1] + tov[n][2];
|
|
plain[n] = if sum > 0.0 { 1 } else { 0 };
|
|
plain_sum += plain[n] as u32;
|
|
}
|
|
|
|
if plain_sum == 0 {
|
|
// Message converged to all-zeros, which is prohibited.
|
|
break;
|
|
}
|
|
|
|
let errors = ldpc_check(plain);
|
|
if errors < min_errors {
|
|
min_errors = errors;
|
|
if errors == 0 {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Send messages from bits to check nodes
|
|
for m in 0..FTX_LDPC_M {
|
|
let num_rows = FTX_LDPC_NUM_ROWS[m] as usize;
|
|
for n_idx in 0..num_rows {
|
|
let n = FTX_LDPC_NM[m][n_idx] as usize - 1;
|
|
let mut tnm = codeword[n];
|
|
for m_idx in 0..3 {
|
|
if (FTX_LDPC_MN[n][m_idx] as usize - 1) != m {
|
|
tnm += tov[n][m_idx];
|
|
}
|
|
}
|
|
toc[m][n_idx] = fast_tanh(-tnm / 2.0);
|
|
}
|
|
}
|
|
|
|
// Send messages from check nodes to variable nodes
|
|
for n in 0..FTX_LDPC_N {
|
|
for m_idx in 0..3 {
|
|
let m = FTX_LDPC_MN[n][m_idx] as usize - 1;
|
|
let num_rows = FTX_LDPC_NUM_ROWS[m] as usize;
|
|
let mut tmn = 1.0f32;
|
|
for n_idx in 0..num_rows {
|
|
if (FTX_LDPC_NM[m][n_idx] as usize - 1) != n {
|
|
tmn *= toc[m][n_idx];
|
|
}
|
|
}
|
|
tov[n][m_idx] = -2.0 * fast_atanh(tmn);
|
|
}
|
|
}
|
|
}
|
|
|
|
min_errors
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_fast_tanh_clamp() {
|
|
assert_eq!(fast_tanh(-5.0), -1.0);
|
|
assert_eq!(fast_tanh(5.0), 1.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fast_tanh_zero() {
|
|
assert!((fast_tanh(0.0)).abs() < 1e-6);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fast_tanh_approximation() {
|
|
for &x in &[-3.0f32, -1.0, -0.5, 0.5, 1.0, 3.0] {
|
|
let approx = fast_tanh(x);
|
|
let exact = x.tanh();
|
|
assert!(
|
|
(approx - exact).abs() < 0.01,
|
|
"fast_tanh({}) = {}, expected ~{}",
|
|
x,
|
|
approx,
|
|
exact
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_fast_atanh_zero() {
|
|
assert!((fast_atanh(0.0)).abs() < 1e-6);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fast_atanh_approximation() {
|
|
for &x in &[-0.5f32, -0.25, 0.25, 0.5] {
|
|
let approx = fast_atanh(x);
|
|
let exact = x.atanh();
|
|
assert!(
|
|
(approx - exact).abs() < 0.05,
|
|
"fast_atanh({}) = {}, expected ~{}",
|
|
x,
|
|
approx,
|
|
exact
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_ldpc_check_all_zeros() {
|
|
// All-zero codeword should pass all parity checks.
|
|
let codeword = [0u8; FTX_LDPC_N];
|
|
assert_eq!(ldpc_check(&codeword), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_ldpc_check_single_bit_error() {
|
|
// Flipping one bit should cause parity errors.
|
|
let mut codeword = [0u8; FTX_LDPC_N];
|
|
codeword[0] = 1;
|
|
assert!(ldpc_check(&codeword) > 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_ldpc_decode_all_zeros() {
|
|
// Negative LLRs → hard decision 0 for all bits.
|
|
// The all-zeros codeword satisfies all LDPC parity checks.
|
|
let mut codeword = [-10.0f32; FTX_LDPC_N];
|
|
let mut plain = [0u8; FTX_LDPC_N];
|
|
let errors = ldpc_decode(&mut codeword, 20, &mut plain);
|
|
assert_eq!(errors, 0);
|
|
assert!(plain.iter().all(|&b| b == 0));
|
|
}
|
|
|
|
#[test]
|
|
fn test_bp_decode_all_ones() {
|
|
// Positive LLRs → hard decision 1 for all bits.
|
|
// All-ones is not a valid codeword, so bp_decode should report errors.
|
|
let codeword = [10.0f32; FTX_LDPC_N];
|
|
let mut plain = [0u8; FTX_LDPC_N];
|
|
let errors = bp_decode(&codeword, 20, &mut plain);
|
|
assert!(errors > 0);
|
|
}
|
|
}
|