Files
trx-rs/src/decoders/trx-ftx/src/common/osd.rs
T
sjg ba48de2d30
Sync docs to Wiki / wiki (push) Has been cancelled
Initial commit
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-05-17 23:25:14 +02:00

923 lines
27 KiB
Rust

// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
//
// SPDX-License-Identifier: GPL-2.0-or-later
//! OSD-1/OSD-2 CRC-guided bit-flip decoder for the (174,91) LDPC code.
//!
//! This is a port of `ft2_ldpc.c` which implements Ordered Statistics Decoding
//! with configurable depth (ndeep 0-6). The decoder first runs iterative
//! belief-propagation (BP), then falls back to OSD refinement using the
//! accumulated LLR sums from BP iterations.
//!
//! The OSD algorithm works by:
//! 1. Sorting codeword bits by LLR reliability
//! 2. Gaussian elimination to put the generator matrix in systematic form
//! (with respect to the most reliable bits)
//! 3. Exhaustive search over bit-flip patterns of increasing weight
//! 4. Pattern hashing (OSD-2) to efficiently search two-bit-flip corrections
use std::sync::OnceLock;
use super::constants::{FTX_LDPC_GENERATOR, FTX_LDPC_MN, FTX_LDPC_NM, FTX_LDPC_NUM_ROWS};
use super::crc::{ftx_compute_crc, ftx_extract_crc};
use super::decode::pack_bits;
use super::encode::parity8;
use super::ldpc::ldpc_check;
use super::protocol::{FTX_LDPC_K, FTX_LDPC_K_BYTES, FTX_LDPC_M, FTX_LDPC_N};
/// Piecewise linear approximation of `atanh(x)` used in BP message passing.
fn platanh(x: f32) -> f32 {
let isign: f32 = if x < 0.0 { -1.0 } else { 1.0 };
let z = x.abs();
if z <= 0.664 {
return x / 0.83;
}
if z <= 0.9217 {
return isign * ((z - 0.4064) / 0.322);
}
if z <= 0.9951 {
return isign * ((z - 0.8378) / 0.0524);
}
if z <= 0.9998 {
return isign * ((z - 0.9914) / 0.0012);
}
isign * 7.0
}
/// Check CRC of a 91-bit message (in bit array form).
fn check_crc91(plain91: &[u8]) -> bool {
let mut a91 = [0u8; FTX_LDPC_K_BYTES];
pack_bits(plain91, FTX_LDPC_K, &mut a91);
let crc_extracted = ftx_extract_crc(&a91);
a91[9] &= 0xF8;
a91[10] = 0x00;
let crc_calculated = ftx_compute_crc(&a91, 96 - 14);
crc_extracted == crc_calculated
}
/// Encode a 91-bit message (bit array) into a 174-bit codeword without CRC computation.
fn encode174_91_nocrc_bits(message91: &[u8], codeword: &mut [u8; FTX_LDPC_N]) {
let mut packed = [0u8; FTX_LDPC_K_BYTES];
pack_bits(message91, FTX_LDPC_K, &mut packed);
// Systematic bits
for i in 0..FTX_LDPC_K {
codeword[i] = message91[i] & 0x01;
}
// Parity bits from generator matrix
for i in 0..FTX_LDPC_M {
let mut nsum: u8 = 0;
for j in 0..FTX_LDPC_K_BYTES {
nsum ^= parity8(packed[j] & FTX_LDPC_GENERATOR[i][j]);
}
codeword[FTX_LDPC_K + i] = nsum & 0x01;
}
}
/// Matrix-vector multiply for re-encoding in OSD.
fn mrbencode91(me: &[u8], codeword: &mut [u8], g2: &[u8], n: usize, k: usize) {
codeword[..n].fill(0);
for i in 0..k {
if me[i] == 0 {
continue;
}
codeword[..n]
.iter_mut()
.enumerate()
.for_each(|(j, c)| *c ^= g2[j * k + i]);
}
}
/// Generate next bit-flip pattern of given order.
fn nextpat91(mi: &mut [u8], k: usize, iorder: usize, iflag: &mut i32) {
let mut ind: i32 = -1;
for i in 0..k.saturating_sub(1) {
if mi[i] == 0 && mi[i + 1] == 1 {
ind = i as i32;
}
}
if ind < 0 {
*iflag = -1;
return;
}
// Build new pattern in-place: zero out after ind, set the swap, pack remaining 1s at end
let ind_u = ind as usize;
mi[(ind_u + 1)..k].fill(0);
mi[ind_u] = 1;
let mut nz = iorder as i32;
for &v in mi.iter().take(k) {
nz -= v as i32;
}
if nz > 0 {
mi[(k - nz as usize)..k].fill(1);
}
*iflag = -1;
for (i, &v) in mi.iter().enumerate().take(k) {
if v == 1 {
*iflag = i as i32;
break;
}
}
}
/// Pattern hash table for OSD-2 optimization.
struct OsdBox {
head: Vec<i32>,
next: Vec<i32>,
pairs: Vec<[i32; 2]>,
capacity: usize,
count: usize,
last_pattern: i32,
next_index: i32,
}
impl OsdBox {
fn new(ntau: usize) -> Option<Self> {
let size = 1 << ntau;
let capacity = 5000;
Some(Self {
head: vec![-1; size],
next: vec![-1; capacity],
pairs: vec![[-1, -1]; capacity],
capacity,
count: 0,
last_pattern: -1,
next_index: -1,
})
}
fn boxit(&mut self, e2: &[u8], ntau: usize, i1: i32, i2: i32) {
if self.count >= self.capacity {
return;
}
let idx = self.count;
self.count += 1;
self.pairs[idx] = [i1, i2];
let ipat = pattern_hash(e2, ntau);
let ip = self.head[ipat];
if ip == -1 {
self.head[ipat] = idx as i32;
} else {
let mut cur = ip;
while self.next[cur as usize] != -1 {
cur = self.next[cur as usize];
}
self.next[cur as usize] = idx as i32;
}
}
fn fetchit(&mut self, e2: &[u8], ntau: usize) -> (i32, i32) {
let ipat = pattern_hash(e2, ntau);
let index = self.head[ipat];
if self.last_pattern != ipat as i32 && index >= 0 {
let i1 = self.pairs[index as usize][0];
let i2 = self.pairs[index as usize][1];
self.next_index = self.next[index as usize];
self.last_pattern = ipat as i32;
(i1, i2)
} else if self.last_pattern == ipat as i32 && self.next_index >= 0 {
let ni = self.next_index as usize;
let i1 = self.pairs[ni][0];
let i2 = self.pairs[ni][1];
self.next_index = self.next[ni];
(i1, i2)
} else {
self.next_index = -1;
self.last_pattern = ipat as i32;
(-1, -1)
}
}
}
/// Compute hash of a bit pattern for OSD-2 lookup.
fn pattern_hash(e2: &[u8], ntau: usize) -> usize {
let mut ipat = 0usize;
for (i, &v) in e2.iter().enumerate().take(ntau) {
if v != 0 {
ipat |= 1 << (ntau - i - 1);
}
}
ipat
}
/// Ordered Statistics Decoding with configurable depth.
///
/// `llr`: log-likelihood ratios for 174 bits (modified internally).
/// `k`: number of systematic bits (91).
/// `apmask`: a priori mask (which bits are known).
/// `ndeep`: search depth (0-6).
/// `message91`: output 91-bit message.
/// `cw`: output 174-bit codeword.
/// `nhardmin`: output minimum hard errors.
/// `dmin`: output minimum distance.
#[allow(clippy::too_many_arguments)]
pub fn osd174_91(
llr: &mut [f32; FTX_LDPC_N],
k: usize,
apmask: &[u8; FTX_LDPC_N],
ndeep: usize,
message91: &mut [u8; FTX_LDPC_K],
cw: &mut [u8; FTX_LDPC_N],
nhardmin: &mut i32,
dmin: &mut f32,
) {
let n = FTX_LDPC_N;
let ndeep = ndeep.min(6);
// Cached per-bit generator matrix (each row i generates codeword from
// unit vector e_i)
let gen = generator_matrix();
// Stack-allocated working buffers (k=91, n=174, n-k=83).
let mut genmrb = [0u8; FTX_LDPC_K * FTX_LDPC_N];
let mut g2 = [0u8; FTX_LDPC_N * FTX_LDPC_K];
let mut m0 = [0u8; FTX_LDPC_K];
let mut me = [0u8; FTX_LDPC_K];
let mut mi = [0u8; FTX_LDPC_K];
let mut misub = [0u8; FTX_LDPC_K];
let mut e2sub = [0u8; FTX_LDPC_M];
let mut e2 = [0u8; FTX_LDPC_M];
let mut ui = [0u8; FTX_LDPC_M];
let mut r2pat = [0u8; FTX_LDPC_M];
let mut hdec = [0u8; FTX_LDPC_N];
let mut c0 = [0u8; FTX_LDPC_N];
let mut ce = [0u8; FTX_LDPC_N];
let mut nxor = [0u8; FTX_LDPC_N];
let mut apmaskr = [0u8; FTX_LDPC_N];
let mut rx = [0.0f32; FTX_LDPC_N];
let mut absrx = [0.0f32; FTX_LDPC_N];
let mut indices = [0usize; FTX_LDPC_N];
// Sort bits by reliability (descending)
let mut rel_indices = [0usize; FTX_LDPC_N];
let mut rel_abs = [0.0f32; FTX_LDPC_N];
for i in 0..n {
rel_indices[i] = i;
rel_abs[i] = llr[i].abs();
}
rel_indices[..n].sort_by(|&a, &b| {
rel_abs[b]
.partial_cmp(&rel_abs[a])
.unwrap_or(std::cmp::Ordering::Equal)
});
for i in 0..n {
rx[i] = llr[i];
apmaskr[i] = apmask[i];
hdec[i] = if rx[i] >= 0.0 { 1 } else { 0 };
absrx[i] = rx[i].abs();
}
// Reorder by reliability
for i in 0..n {
indices[i] = rel_indices[i];
for row in 0..k {
genmrb[row * n + i] = gen[row][indices[i]];
}
}
// Gaussian elimination to systematic form
for id in 0..k {
let max_col = (k + 20).min(n);
for col in id..max_col {
if genmrb[id * n + col] == 0 {
continue;
}
// Swap columns id and col
if col != id {
for row in 0..k {
genmrb.swap(row * n + id, row * n + col);
}
indices.swap(id, col);
}
// Eliminate column id from all other rows
for row in 0..k {
if row != id && genmrb[row * n + id] == 1 {
for c in 0..n {
genmrb[row * n + c] ^= genmrb[id * n + c];
}
}
}
break;
}
}
// Transpose to column-major g2
for row in 0..k {
for col in 0..n {
g2[col * k + row] = genmrb[row * n + col];
}
}
// Reorder LLRs and hard decisions by reliability
for i in 0..n {
hdec[i] = if rx[indices[i]] >= 0.0 { 1 } else { 0 };
absrx[i] = rx[indices[i]].abs();
rx[i] = llr[indices[i]];
apmaskr[i] = apmask[indices[i]];
}
m0[..k].copy_from_slice(&hdec[..k]);
// Initial encode
mrbencode91(&m0, &mut c0, &g2, n, k);
for i in 0..n {
nxor[i] = c0[i] ^ hdec[i];
}
*nhardmin = 0;
*dmin = 0.0;
for i in 0..n {
*nhardmin += nxor[i] as i32;
if nxor[i] != 0 {
*dmin += absrx[i];
}
}
cw.copy_from_slice(&c0[..n]);
if ndeep == 0 {
reorder_result(cw, &indices, message91, nhardmin, dmin, llr);
return;
}
// Configure search parameters based on depth
let (nord, npre1, npre2, nt, ntheta, ntau) = match ndeep {
1 => (1, 0, 0, 40, 12, 0),
2 => (1, 1, 0, 40, 10, 0),
3 => (1, 1, 1, 40, 12, 14),
4 => (2, 1, 1, 40, 12, 17),
5 => (3, 1, 1, 40, 12, 15),
_ => (4, 1, 1, 95, 12, 15),
};
// OSD-1: exhaustive search over bit patterns of increasing order
for iorder in 1..=nord {
misub.iter_mut().for_each(|v| *v = 0);
misub[(k - iorder)..k].fill(1);
let mut iflag = (k - iorder) as i32;
while iflag >= 0 {
let iend = if iorder == nord && npre1 == 0 {
iflag as usize
} else {
0
};
let mut d1 = 0.0f32;
let mut n1 = iflag;
while n1 >= iend as i32 {
mi[..k].copy_from_slice(&misub[..k]);
mi[n1 as usize] = 1;
// Check if any masked bit would be flipped
let masked = (0..k).any(|i| apmaskr[i] != 0 && mi[i] != 0);
if masked {
n1 -= 1;
continue;
}
for i in 0..k {
me[i] = m0[i] ^ mi[i];
}
if n1 == iflag {
mrbencode91(&me, &mut ce, &g2, n, k);
for i in 0..(n - k) {
e2sub[i] = ce[k + i] ^ hdec[k + i];
e2[i] = e2sub[i];
}
let mut nd1kpt = 1;
for &v in e2sub.iter().take(nt.min(n - k)) {
nd1kpt += v as i32;
}
d1 = 0.0;
for i in 0..k {
if (me[i] ^ hdec[i]) != 0 {
d1 += absrx[i];
}
}
if nd1kpt <= ntheta {
let mut dd = d1;
for i in 0..(n - k) {
if e2sub[i] != 0 {
dd += absrx[k + i];
}
}
if dd < *dmin {
*dmin = dd;
cw[..n].copy_from_slice(&ce[..n]);
*nhardmin = 0;
for i in 0..n {
*nhardmin += (ce[i] ^ hdec[i]) as i32;
}
}
}
} else {
for i in 0..(n - k) {
e2[i] = e2sub[i] ^ g2[(k + i) * k + n1 as usize];
}
let mut nd1kpt = 2;
for &v in e2.iter().take(nt.min(n - k)) {
nd1kpt += v as i32;
}
if nd1kpt <= ntheta {
mrbencode91(&me, &mut ce, &g2, n, k);
let mut dd = d1
+ if (ce[n1 as usize] ^ hdec[n1 as usize]) != 0 {
absrx[n1 as usize]
} else {
0.0
};
for i in 0..(n - k) {
if e2[i] != 0 {
dd += absrx[k + i];
}
}
if dd < *dmin {
*dmin = dd;
cw[..n].copy_from_slice(&ce[..n]);
*nhardmin = 0;
for i in 0..n {
*nhardmin += (ce[i] ^ hdec[i]) as i32;
}
}
}
}
n1 -= 1;
}
nextpat91(&mut misub, k, iorder, &mut iflag);
}
}
// OSD-2: pattern-hashed two-bit-flip search
if npre2 == 1 {
if let Some(mut osd_box) = OsdBox::new(ntau) {
// Build hash table of all column pairs
for i1 in (0..k as i32).rev() {
for i2 in (0..i1).rev() {
for i in 0..ntau {
mi[i] = g2[(k + i) * k + i1 as usize] ^ g2[(k + i) * k + i2 as usize];
}
osd_box.boxit(&mi, ntau, i1, i2);
}
}
// Search using base patterns
misub.iter_mut().for_each(|v| *v = 0);
misub[(k - nord)..k].fill(1);
let mut iflag = (k - nord) as i32;
while iflag >= 0 {
for i in 0..k {
me[i] = m0[i] ^ misub[i];
}
mrbencode91(&me, &mut ce, &g2, n, k);
for i in 0..(n - k) {
e2sub[i] = ce[k + i] ^ hdec[k + i];
}
for i2 in 0..=ntau {
ui.iter_mut().for_each(|v| *v = 0);
if i2 > 0 {
ui[i2 - 1] = 1;
}
for i in 0..ntau {
r2pat[i] = e2sub[i] ^ ui[i];
}
osd_box.last_pattern = -1;
osd_box.next_index = -1;
loop {
let (in1, in2) = osd_box.fetchit(&r2pat, ntau);
if in1 < 0 || in2 < 0 {
break;
}
mi[..k].copy_from_slice(&misub[..k]);
mi[in1 as usize] = 1;
mi[in2 as usize] = 1;
let mut w = 0;
let mut masked = false;
for i in 0..k {
w += mi[i] as usize;
if apmaskr[i] != 0 && mi[i] != 0 {
masked = true;
}
}
if w < nord + npre1 + npre2 || masked {
continue;
}
for i in 0..k {
me[i] = m0[i] ^ mi[i];
}
mrbencode91(&me, &mut ce, &g2, n, k);
let mut dd = 0.0f32;
let mut nh = 0i32;
for i in 0..n {
let diff = ce[i] ^ hdec[i];
nh += diff as i32;
if diff != 0 {
dd += absrx[i];
}
}
if dd < *dmin {
*dmin = dd;
cw[..n].copy_from_slice(&ce[..n]);
*nhardmin = nh;
}
}
}
nextpat91(&mut misub, k, nord, &mut iflag);
}
}
}
reorder_result(cw, &indices, message91, nhardmin, dmin, llr);
}
/// Reorder codeword back to original bit ordering and verify CRC.
fn reorder_result(
cw: &mut [u8; FTX_LDPC_N],
indices: &[usize],
message91: &mut [u8; FTX_LDPC_K],
nhardmin: &mut i32,
_dmin: &mut f32,
_llr: &[f32; FTX_LDPC_N],
) {
let mut reordered = [0u8; FTX_LDPC_N];
for i in 0..FTX_LDPC_N {
reordered[indices[i]] = cw[i];
}
cw.copy_from_slice(&reordered);
message91.copy_from_slice(&cw[..FTX_LDPC_K]);
if !check_crc91(message91) {
*nhardmin = -*nhardmin;
}
}
/// Get a reference to the cached generator matrix.
/// The matrix is computed once on first call and reused thereafter.
fn generator_matrix() -> &'static [[u8; FTX_LDPC_N]; FTX_LDPC_K] {
static GEN: OnceLock<Box<[[u8; FTX_LDPC_N]; FTX_LDPC_K]>> = OnceLock::new();
GEN.get_or_init(|| {
let mut gen = Box::new([[0u8; FTX_LDPC_N]; FTX_LDPC_K]);
for i in 0..FTX_LDPC_K {
let mut msg = [0u8; FTX_LDPC_K];
msg[i] = 1;
if i < 77 {
msg[77..FTX_LDPC_K].fill(0);
}
encode174_91_nocrc_bits(&msg, &mut gen[i]);
}
gen
})
}
/// Full iterative BP decoder with OSD refinement.
///
/// Runs belief-propagation for up to `maxiterations` iterations, saving
/// accumulated LLR sums. If BP does not converge, falls back to OSD
/// using the saved sums.
///
/// `llr`: input log-likelihood ratios (174 values).
/// `keff`: effective K (must be 91).
/// `maxosd`: maximum number of OSD passes (0-3).
/// `norder`: OSD depth parameter.
/// `apmask`: a priori mask.
/// `message91`: output decoded 91-bit message.
/// `cw`: output 174-bit codeword.
/// `ntype`: output decode type (0=fail, 1=BP, 2=OSD).
/// `nharderror`: output number of hard errors.
/// `dmin`: output minimum distance.
#[allow(clippy::too_many_arguments)]
pub fn ft2_decode174_91_osd(
llr: &mut [f32; FTX_LDPC_N],
keff: usize,
maxosd: usize,
norder: usize,
apmask: &mut [u8; FTX_LDPC_N],
message91: &mut [u8; FTX_LDPC_K],
cw: &mut [u8; FTX_LDPC_N],
ntype: &mut i32,
nharderror: &mut i32,
dmin: &mut f32,
) {
*ntype = 0;
*nharderror = -1;
*dmin = 0.0;
if keff != FTX_LDPC_K {
return;
}
let maxiterations = 30;
let maxosd = maxosd.min(3);
let nosd = if maxosd == 0 { 1 } else { maxosd };
let mut zsave = [[0.0f32; FTX_LDPC_N]; 3];
if maxosd == 0 {
zsave[0].copy_from_slice(llr);
}
let mut tov = [[0.0f32; 3]; FTX_LDPC_N];
let mut toc = [[0.0f32; 7]; FTX_LDPC_M];
let mut zsum = [0.0f32; FTX_LDPC_N];
let mut hdec = [0u8; FTX_LDPC_N];
let mut best_cw = [0u8; FTX_LDPC_N];
let mut ncnt = 0;
let mut nclast = 0;
for iter in 0..=maxiterations {
// Compute beliefs
let mut zn = [0.0f32; FTX_LDPC_N];
for i in 0..FTX_LDPC_N {
zn[i] = llr[i];
if apmask[i] != 1 {
zn[i] += tov[i][0] + tov[i][1] + tov[i][2];
}
zsum[i] += zn[i];
}
if iter > 0 && iter <= maxosd {
zsave[iter - 1].copy_from_slice(&zsum);
}
// Hard decisions
for i in 0..FTX_LDPC_N {
best_cw[i] = if zn[i] > 0.0 { 1 } else { 0 };
}
let ncheck = ldpc_check(&best_cw);
if ncheck == 0 && check_crc91(&best_cw) {
message91.copy_from_slice(&best_cw[..FTX_LDPC_K]);
cw.copy_from_slice(&best_cw);
for i in 0..FTX_LDPC_N {
hdec[i] = if llr[i] >= 0.0 { 1 } else { 0 };
}
*nharderror = 0;
*dmin = 0.0;
for i in 0..FTX_LDPC_N {
let diff = hdec[i] ^ best_cw[i];
*nharderror += diff as i32;
if diff != 0 {
*dmin += llr[i].abs();
}
}
*ntype = 1;
return;
}
// Early termination
if iter > 0 {
let nd = ncheck - nclast;
ncnt = if nd < 0 { 0 } else { ncnt + 1 };
if ncnt >= 5 && iter >= 10 && ncheck > 15 {
*nharderror = -1;
break;
}
}
nclast = ncheck;
// Check-to-variable messages
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;
if n >= FTX_LDPC_N {
continue;
}
toc[m][n_idx] = zn[n];
for kk in 0..3 {
if (FTX_LDPC_MN[n][kk] as usize).wrapping_sub(1) == m {
toc[m][n_idx] -= tov[n][kk];
}
}
}
}
// Variable-to-check messages
for m in 0..FTX_LDPC_M {
let num_rows = FTX_LDPC_NUM_ROWS[m] as usize;
let mut tanhtoc = [0.0f32; 7];
for i in 0..num_rows.min(7) {
tanhtoc[i] = (-toc[m][i] / 2.0).tanh();
}
for &nm_val in FTX_LDPC_NM[m].iter().take(num_rows) {
let n = nm_val as usize - 1;
if n >= FTX_LDPC_N {
continue;
}
let mut tmn = 1.0f32;
for n_idx in 0..num_rows {
if FTX_LDPC_NM[m][n_idx] as usize - 1 != n {
tmn *= tanhtoc[n_idx];
}
}
for kk in 0..3 {
if (FTX_LDPC_MN[n][kk] as usize).wrapping_sub(1) == m {
tov[n][kk] = 2.0 * platanh(-tmn);
}
}
}
}
}
// OSD fallback
for i in 0..nosd {
if i >= zsave.len() {
break;
}
let mut osd_llr = [0.0f32; FTX_LDPC_N];
osd_llr.copy_from_slice(&zsave[i]);
let mut osd_harderror: i32 = -1;
let mut osd_dmin: f32 = 0.0;
osd174_91(
&mut osd_llr,
keff,
apmask,
norder,
message91,
cw,
&mut osd_harderror,
&mut osd_dmin,
);
if osd_harderror > 0 {
*nharderror = osd_harderror;
*dmin = 0.0;
for j in 0..FTX_LDPC_N {
hdec[j] = if llr[j] >= 0.0 { 1 } else { 0 };
if (hdec[j] ^ cw[j]) != 0 {
*dmin += llr[j].abs();
}
}
*ntype = 2;
return;
}
}
*ntype = 0;
*nharderror = -1;
*dmin = 0.0;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common::ldpc::fast_atanh;
#[test]
fn ldpc_check_all_zeros() {
let cw = [0u8; FTX_LDPC_N];
assert_eq!(ldpc_check(&cw), 0);
}
#[test]
fn ldpc_check_single_bit_error() {
let mut cw = [0u8; FTX_LDPC_N];
cw[0] = 1;
assert!(ldpc_check(&cw) > 0);
}
#[test]
fn fast_atanh_zero() {
assert!(fast_atanh(0.0).abs() < 1e-6);
}
#[test]
fn 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 platanh_small() {
let result = platanh(0.5);
assert!(result > 0.0);
assert!(result.is_finite());
}
#[test]
fn platanh_large() {
let result = platanh(0.9999);
assert!(result > 0.0);
assert!(result.is_finite());
}
#[test]
fn platanh_negative() {
let pos = platanh(0.5);
let neg = platanh(-0.5);
assert!((pos + neg).abs() < 1e-6, "platanh should be odd");
}
#[test]
fn shared_pack_bits_basic() {
let mut bits = [0u8; FTX_LDPC_K];
bits[0] = 1;
bits[7] = 1;
let mut packed = [0u8; FTX_LDPC_K_BYTES];
pack_bits(&bits, FTX_LDPC_K, &mut packed);
assert_eq!(packed[0], 0x81);
}
#[test]
fn check_crc91_all_zeros() {
// All-zero message likely fails CRC
let bits = [0u8; FTX_LDPC_K];
// CRC check result depends on specific polynomial behavior
let _result = check_crc91(&bits);
// Just verify it doesn't panic
}
#[test]
fn shared_parity8_basic() {
assert_eq!(parity8(0x00), 0);
assert_eq!(parity8(0x01), 1);
assert_eq!(parity8(0x03), 0);
assert_eq!(parity8(0xFF), 0);
}
#[test]
fn pattern_hash_basic() {
let e2 = [1u8, 0, 1, 0];
assert_eq!(pattern_hash(&e2, 4), 0b1010);
}
#[test]
fn pattern_hash_all_zeros() {
let e2 = [0u8; 16];
assert_eq!(pattern_hash(&e2, 16), 0);
}
#[test]
fn nextpat91_basic() {
let k = 5;
let mut mi = vec![0u8; k];
mi[4] = 1;
let mut iflag = 4i32;
nextpat91(&mut mi, k, 1, &mut iflag);
// After one step, the pattern should shift
assert!(iflag >= -1);
}
#[test]
fn generator_matrix_row_zero() {
let gen = generator_matrix();
// Row 0 should encode unit vector e_0
assert_eq!(gen[0][0], 1);
// Some parity bits should be non-zero
let parity_nonzero = gen[0][FTX_LDPC_K..FTX_LDPC_N].iter().any(|&b| b != 0);
assert!(parity_nonzero);
}
#[test]
fn encode174_91_nocrc_all_zeros() {
let msg = [0u8; FTX_LDPC_K];
let mut cw = [0u8; FTX_LDPC_N];
encode174_91_nocrc_bits(&msg, &mut cw);
for &b in &cw {
assert_eq!(b, 0);
}
}
#[test]
fn osd_box_basic() {
let mut b = OsdBox::new(4).unwrap();
let pattern = [1u8, 0, 1, 0];
b.boxit(&pattern, 4, 5, 3);
let (i1, i2) = b.fetchit(&pattern, 4);
assert_eq!(i1, 5);
assert_eq!(i2, 3);
}
#[test]
fn osd_box_empty_fetch() {
let mut b = OsdBox::new(4).unwrap();
let pattern = [0u8; 4];
let (i1, i2) = b.fetchit(&pattern, 4);
assert_eq!(i1, -1);
assert_eq!(i2, -1);
}
}