#[cfg(test)]
use alloc::vec;
use alloc::vec::Vec;
#[cfg(not(feature = "std"))]
use num_traits::Float;
pub const POLY1: u32 = 0xf2d0_5351;
pub const POLY2: u32 = 0xe461_3c47;
pub const K_CONSTRAINT: usize = 32;
#[inline]
pub fn encode_step(encstate: u32) -> u32 {
let p1 = {
let mut t = encstate & POLY1;
t ^= t >> 16;
t ^= t >> 8;
(t & 0xff).count_ones() & 1
};
let p2 = {
let mut t = encstate & POLY2;
t ^= t >> 16;
t ^= t >> 8;
(t & 0xff).count_ones() & 1
};
(p1 << 1) | p2
}
pub fn conv_encode(data: &[u8], nbits: usize, out: &mut [u8]) {
assert!(out.len() >= 2 * nbits, "output buffer too small");
let mut encstate: u32 = 0;
for i in 0..nbits {
let bit = (data[i / 8] >> (7 - (i % 8))) & 1;
encstate = (encstate << 1) | bit as u32;
let sym = encode_step(encstate);
out[2 * i] = ((sym >> 1) & 1) as u8;
out[2 * i + 1] = (sym & 1) as u8;
}
}
pub fn build_branch_metrics(llrs: &[f32], bias: f32, scale: f32) -> Vec<[i32; 2]> {
llrs.iter()
.map(|&l| {
let m0 = l * 0.5 - bias;
let m1 = -l * 0.5 - bias;
[(m0 * scale).round() as i32, (m1 * scale).round() as i32]
})
.collect()
}
pub fn build_branch_metrics_wsprd(llrs: &[f32], bias: f32) -> Vec<[i32; 2]> {
use super::metric_table::WSPRD_METRIC_TABLE as T;
wsprd_normalised_symbols(llrs)
.map(|v| {
let sym = ((v + 128.0) as usize).min(255);
let m0 = (10.0 * (T[sym] - bias)).round() as i32;
let m1 = (10.0 * (T[255 - sym] - bias)).round() as i32;
[m0, m1]
})
.collect()
}
const SYMFAC: f32 = 50.0;
pub fn wsprd_normalised_symbols(llrs: &[f32]) -> impl Iterator<Item = f32> + '_ {
let n = llrs.len() as f32;
let fsum: f32 = llrs.iter().map(|l| -l).sum::<f32>() / n;
let f2sum: f32 = llrs.iter().map(|l| l * l).sum::<f32>() / n;
let fac = (f2sum - fsum * fsum).max(0.0).sqrt();
llrs.iter().map(move |&l| {
if fac > 0.0 {
(SYMFAC * -l / fac).clamp(-128.0, 127.0)
} else {
0.0
}
})
}
pub fn wsprd_soft_symbol_rms(llrs: &[f32]) -> f32 {
let n = llrs.len() as f32;
(wsprd_normalised_symbols(llrs).map(|v| v * v).sum::<f32>() / n).sqrt()
}
pub const WSPRD_MIN_RMS: f32 = 52.0 * (SYMFAC / 64.0);
#[derive(Default)]
struct Node {
encstate: u32,
gamma: i64, metrics: [i32; 4],
tm: [i32; 2],
i: u8, }
pub struct FanoDecodeResult {
pub data: Vec<u8>,
pub metric: i64,
pub cycles: u64,
pub max_np: usize,
pub converged: bool,
}
pub fn fano_decode(
branch_metrics: &[[i32; 2]],
nbits: usize,
delta: i32,
max_cycles_per_bit: u64,
) -> FanoDecodeResult {
let mut scratch = FanoScratch::new();
fano_decode_with_scratch(
&mut scratch,
branch_metrics,
nbits,
delta,
max_cycles_per_bit,
)
}
#[derive(Default)]
pub struct FanoScratch {
nodes: Vec<Node>,
}
impl FanoScratch {
pub fn new() -> Self {
Self { nodes: Vec::new() }
}
}
pub fn fano_decode_with_scratch(
scratch: &mut FanoScratch,
branch_metrics: &[[i32; 2]],
nbits: usize,
delta: i32,
max_cycles_per_bit: u64,
) -> FanoDecodeResult {
assert_eq!(
branch_metrics.len(),
2 * nbits,
"branch_metrics length mismatch"
);
if scratch.nodes.len() != nbits + 1 {
scratch.nodes = (0..=nbits).map(|_| Node::default()).collect();
}
let nodes = &mut scratch.nodes;
for (k, node) in nodes.iter_mut().take(nbits).enumerate() {
let a = branch_metrics[2 * k];
let b = branch_metrics[2 * k + 1];
node.metrics[0] = a[0] + b[0]; node.metrics[1] = a[0] + b[1]; node.metrics[2] = a[1] + b[0]; node.metrics[3] = a[1] + b[1]; }
let last_idx = nbits.saturating_sub(1);
let tail_idx = nbits.saturating_sub(K_CONSTRAINT - 1);
{
let lsym = encode_step(0) as usize;
let m0 = nodes[0].metrics[lsym];
let m1 = nodes[0].metrics[3 ^ lsym];
if m0 > m1 {
nodes[0].tm = [m0, m1];
} else {
nodes[0].tm = [m1, m0];
nodes[0].encstate |= 1;
}
nodes[0].i = 0;
nodes[0].gamma = 0;
}
let max_cycles = max_cycles_per_bit * nbits as u64;
let mut np: usize = 0;
let mut t: i32 = 0;
let mut max_np: usize = 0;
let mut cycles: u64 = 0;
let mut converged = false;
while cycles < max_cycles {
cycles += 1;
if np > max_np {
max_np = np;
}
let ngamma = nodes[np].gamma + nodes[np].tm[nodes[np].i as usize] as i64;
if ngamma >= t as i64 {
if nodes[np].gamma < (t as i64) + delta as i64 {
while ngamma >= (t as i64) + delta as i64 {
t += delta;
}
}
let new_state = nodes[np].encstate << 1;
let new_idx = np + 1;
nodes[new_idx].gamma = ngamma;
nodes[new_idx].encstate = new_state;
np = new_idx;
if np > last_idx {
converged = true;
break;
}
let lsym = encode_step(nodes[np].encstate) as usize;
if np >= tail_idx {
nodes[np].tm[0] = nodes[np].metrics[lsym];
nodes[np].tm[1] = i32::MIN / 2; } else {
let m0 = nodes[np].metrics[lsym];
let m1 = nodes[np].metrics[3 ^ lsym];
if m0 > m1 {
nodes[np].tm = [m0, m1];
} else {
nodes[np].tm = [m1, m0];
nodes[np].encstate |= 1;
}
}
nodes[np].i = 0;
continue;
}
loop {
if np == 0 || nodes[np - 1].gamma < t as i64 {
t -= delta;
if nodes[np].i != 0 {
nodes[np].i = 0;
nodes[np].encstate ^= 1;
}
break;
}
np -= 1;
if np < tail_idx && nodes[np].i == 0 {
nodes[np].i = 1;
nodes[np].encstate ^= 1;
break;
}
}
}
let nbytes = nbits / 8;
let mut data = Vec::with_capacity(nbytes);
for i in 0..nbytes {
data.push(nodes[8 * i + 7].encstate as u8);
}
let final_metric = nodes[np.min(nbits)].gamma;
instrument::record(cycles, max_cycles, converged);
FanoDecodeResult {
data,
metric: final_metric,
cycles,
max_np,
converged,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encode_then_decode_noise_free() {
let nbits = 81;
let mut data = [0u8; 11]; for i in 0..50 {
if i % 3 == 0 {
data[i / 8] |= 1 << (7 - (i % 8));
}
}
let mut coded = vec![0u8; 2 * nbits];
conv_encode(&data, nbits, &mut coded);
let llrs: Vec<f32> = coded
.iter()
.map(|&b| if b == 0 { 8.0 } else { -8.0 })
.collect();
let bm = build_branch_metrics(&llrs, 0.0, 16.0);
let res = fano_decode(&bm, nbits, 17, 10_000);
assert!(res.converged, "fano should converge on perfect LLRs");
for i in 0..50 {
let orig = (data[i / 8] >> (7 - (i % 8))) & 1;
let got = (res.data[i / 8] >> (7 - (i % 8))) & 1;
assert_eq!(got, orig, "bit {} mismatch", i);
}
}
#[test]
fn encoder_symmetry_poly1_poly2_odd() {
for state in [0u32, 1, 0xaaaa_aaaa, 0x5555_5555, 0xdead_beef] {
let a = encode_step(state);
let b = encode_step(state ^ 1);
assert_eq!(a ^ b, 0b11, "not complementary for state {:#x}", state);
}
}
}
pub mod instrument {
use core::sync::atomic::{AtomicU32, Ordering};
pub const CYCLE_SUM_SHIFT: u32 = 10;
pub static BUDGET: AtomicU32 = AtomicU32::new(0);
pub static OK_COUNT: AtomicU32 = AtomicU32::new(0);
pub static OK_CYCLES_SUM: AtomicU32 = AtomicU32::new(0);
pub static OK_CYCLES_MAX: AtomicU32 = AtomicU32::new(0);
pub static FAIL_COUNT: AtomicU32 = AtomicU32::new(0);
pub static FAIL_CYCLES_SUM: AtomicU32 = AtomicU32::new(0);
pub static OK_DECILE: [AtomicU32; 10] = [const { AtomicU32::new(0) }; 10];
pub static FAIL_DECILE: [AtomicU32; 10] = [const { AtomicU32::new(0) }; 10];
pub(super) fn record(cycles: u64, budget: u64, converged: bool) {
let cycles32 = u32::try_from(cycles).unwrap_or(u32::MAX);
let budget32 = u32::try_from(budget).unwrap_or(u32::MAX);
BUDGET.store(budget32, Ordering::Relaxed);
let decile = (cycles * 10)
.checked_div(budget)
.map_or(0, |d| (d as usize).min(9));
let scaled = cycles32 >> CYCLE_SUM_SHIFT;
if converged {
OK_COUNT.fetch_add(1, Ordering::Relaxed);
OK_CYCLES_SUM.fetch_add(scaled, Ordering::Relaxed);
OK_CYCLES_MAX.fetch_max(cycles32, Ordering::Relaxed);
OK_DECILE[decile].fetch_add(1, Ordering::Relaxed);
} else {
FAIL_COUNT.fetch_add(1, Ordering::Relaxed);
FAIL_CYCLES_SUM.fetch_add(scaled, Ordering::Relaxed);
FAIL_DECILE[decile].fetch_add(1, Ordering::Relaxed);
}
}
pub fn reset() {
for c in [
&BUDGET,
&OK_COUNT,
&OK_CYCLES_SUM,
&OK_CYCLES_MAX,
&FAIL_COUNT,
&FAIL_CYCLES_SUM,
] {
c.store(0, Ordering::Relaxed);
}
for b in OK_DECILE.iter().chain(FAIL_DECILE.iter()) {
b.store(0, Ordering::Relaxed);
}
}
#[must_use]
pub fn snapshot() -> Snapshot {
let mut ok = [0u32; 10];
let mut fail = [0u32; 10];
for i in 0..10 {
ok[i] = OK_DECILE[i].load(Ordering::Relaxed);
fail[i] = FAIL_DECILE[i].load(Ordering::Relaxed);
}
Snapshot {
budget: BUDGET.load(Ordering::Relaxed),
ok_count: OK_COUNT.load(Ordering::Relaxed),
ok_cycles_sum: OK_CYCLES_SUM.load(Ordering::Relaxed),
ok_cycles_max: OK_CYCLES_MAX.load(Ordering::Relaxed),
fail_count: FAIL_COUNT.load(Ordering::Relaxed),
fail_cycles_sum: FAIL_CYCLES_SUM.load(Ordering::Relaxed),
ok_decile: ok,
fail_decile: fail,
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Snapshot {
pub budget: u32,
pub ok_count: u32,
pub ok_cycles_sum: u32,
pub ok_cycles_max: u32,
pub fail_count: u32,
pub fail_cycles_sum: u32,
pub ok_decile: [u32; 10],
pub fail_decile: [u32; 10],
}
impl Snapshot {
#[must_use]
pub fn since(self, e: Snapshot) -> Snapshot {
let mut ok = [0u32; 10];
let mut fail = [0u32; 10];
for i in 0..10 {
ok[i] = self.ok_decile[i].saturating_sub(e.ok_decile[i]);
fail[i] = self.fail_decile[i].saturating_sub(e.fail_decile[i]);
}
Snapshot {
budget: self.budget,
ok_count: self.ok_count.saturating_sub(e.ok_count),
ok_cycles_sum: self.ok_cycles_sum.saturating_sub(e.ok_cycles_sum),
ok_cycles_max: self.ok_cycles_max,
fail_count: self.fail_count.saturating_sub(e.fail_count),
fail_cycles_sum: self.fail_cycles_sum.saturating_sub(e.fail_cycles_sum),
ok_decile: ok,
fail_decile: fail,
}
}
#[must_use]
pub fn ok_cycles_mean(self) -> u32 {
self.ok_cycles_sum.checked_div(self.ok_count).unwrap_or(0) << CYCLE_SUM_SHIFT
}
#[must_use]
pub fn fail_cycles_mean(self) -> u32 {
self.fail_cycles_sum
.checked_div(self.fail_count)
.unwrap_or(0)
<< CYCLE_SUM_SHIFT
}
}
}