#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct FecDecoded {
pub info: u16,
pub errors: u8,
}
const GOLAY_23_12_GEN: [u32; 12] = [
0b100000000000_11000111010,
0b010000000000_01100011101,
0b001000000000_11110110100,
0b000100000000_01111011010,
0b000010000000_00111101101,
0b000001000000_11011001100,
0b000000100000_01101100110,
0b000000010000_00110110011,
0b000000001000_11011100011,
0b000000000100_10101001011,
0b000000000010_10010011111,
0b000000000001_10001110101,
];
pub fn golay_23_12_encode(info: u16) -> u32 {
let mut cw = 0u32;
let info = u32::from(info) & 0xFFF;
for i in 0..12 {
if (info >> (11 - i)) & 1 == 1 {
cw ^= GOLAY_23_12_GEN[i];
}
}
cw
}
fn golay_23_12_syndrome(codeword: u32) -> u16 {
let cw = codeword & 0x7F_FFFF;
let info = (cw >> 11) & 0xFFF;
let received_parity = cw & 0x7FF;
let mut reconstructed_parity = 0u32;
for i in 0..12 {
if (info >> (11 - i)) & 1 == 1 {
reconstructed_parity ^= GOLAY_23_12_GEN[i] & 0x7FF;
}
}
(received_parity ^ reconstructed_parity) as u16
}
fn golay_23_12_syndrome_table() -> &'static [u32; 2048] {
use std::sync::OnceLock;
static TABLE: OnceLock<[u32; 2048]> = OnceLock::new();
TABLE.get_or_init(|| {
let mut table = [0u32; 2048];
for a in 0..23 {
let pat = 1u32 << a;
table[golay_23_12_syndrome(pat) as usize] = pat;
}
for a in 0..23 {
for b in (a + 1)..23 {
let pat = (1u32 << a) | (1u32 << b);
table[golay_23_12_syndrome(pat) as usize] = pat;
}
}
for a in 0..23 {
for b in (a + 1)..23 {
for c in (b + 1)..23 {
let pat = (1u32 << a) | (1u32 << b) | (1u32 << c);
table[golay_23_12_syndrome(pat) as usize] = pat;
}
}
}
table
})
}
pub fn golay_23_12_decode(codeword: u32) -> FecDecoded {
let cw = codeword & 0x7F_FFFF;
let syndrome = golay_23_12_syndrome(cw);
let pattern = golay_23_12_syndrome_table()[syndrome as usize];
let corrected = cw ^ pattern;
FecDecoded {
info: ((corrected >> 11) & 0xFFF) as u16,
errors: pattern.count_ones() as u8,
}
}
pub fn golay_24_12_encode(info: u16) -> u32 {
let cw23 = golay_23_12_encode(info);
let parity = cw23.count_ones() & 1;
(cw23 << 1) | parity
}
pub fn golay_24_12_decode(codeword: u32) -> FecDecoded {
let cw = codeword & 0xFF_FFFF;
let cw23 = (cw >> 1) & 0x7F_FFFF;
let received_parity = cw & 1;
let d23 = golay_23_12_decode(cw23);
let corrected23 = golay_23_12_encode(d23.info);
let reconstructed_parity = corrected23.count_ones() & 1;
let parity_matches = reconstructed_parity == received_parity;
let errors = if d23.errors == 3 && !parity_matches {
u8::MAX
} else if parity_matches {
d23.errors
} else {
d23.errors + 1
};
FecDecoded { info: d23.info, errors }
}
const HAMMING_15_11_PARITY: [u8; 11] = [
0b1111, 0b1110, 0b1101, 0b1100, 0b1011, 0b1010, 0b1001, 0b0111, 0b0110,
0b0101, 0b0011,
];
pub fn hamming_15_11_encode(info: u16) -> u16 {
let info = info & 0x7FF;
let mut parity = 0u16;
for i in 0..11 {
if (info >> (10 - i)) & 1 == 1 {
parity ^= u16::from(HAMMING_15_11_PARITY[i]);
}
}
(info << 4) | (parity & 0xF)
}
pub fn hamming_15_11_decode(codeword: u16) -> FecDecoded {
let cw = codeword & 0x7FFF;
let info = (cw >> 4) & 0x7FF;
let received_parity = cw & 0xF;
let mut reconstructed_parity = 0u16;
for i in 0..11 {
if (info >> (10 - i)) & 1 == 1 {
reconstructed_parity ^= u16::from(HAMMING_15_11_PARITY[i]);
}
}
let syndrome = (received_parity ^ reconstructed_parity) & 0xF;
if syndrome == 0 {
return FecDecoded { info, errors: 0 };
}
for i in 0..11 {
if u16::from(HAMMING_15_11_PARITY[i]) == syndrome {
let corrected = info ^ (1 << (10 - i));
return FecDecoded { info: corrected, errors: 1 };
}
}
FecDecoded { info, errors: 1 }
}
const CHASE_GOLAY_DEPTH: usize = 6;
const CHASE_HAMMING_DEPTH: usize = 3;
pub fn golay_23_12_decode_soft(soft: &[i8; 23]) -> FecDecoded {
let weak = least_reliable_positions::<23>(soft, CHASE_GOLAY_DEPTH);
let hard_cw = soft_to_codeword_u32::<23>(soft);
chase_decode_u32::<23, 3>(soft, hard_cw, &weak, |cw| {
let d = golay_23_12_decode(cw);
let candidate_cw = golay_23_12_encode(d.info);
(d, candidate_cw)
})
}
pub fn golay_24_12_decode_soft(soft: &[i8; 24]) -> FecDecoded {
let weak = least_reliable_positions::<24>(soft, CHASE_GOLAY_DEPTH);
let hard_cw = soft_to_codeword_u32::<24>(soft);
chase_decode_u32::<24, 3>(soft, hard_cw, &weak, |cw| {
let d = golay_24_12_decode(cw);
let candidate_cw = golay_24_12_encode(d.info);
(d, candidate_cw)
})
}
pub fn hamming_15_11_decode_soft(soft: &[i8; 15]) -> FecDecoded {
let weak = least_reliable_positions::<15>(soft, CHASE_HAMMING_DEPTH);
let hard_cw = soft_to_codeword_u16::<15>(soft);
chase_decode_u16::<15, 1>(soft, hard_cw, &weak, |cw| {
let d = hamming_15_11_decode(cw);
let candidate_cw = hamming_15_11_encode(d.info);
(d, candidate_cw)
})
}
fn least_reliable_positions<const N: usize>(soft: &[i8; N], k: usize) -> Vec<usize> {
let mut ranked: [(usize, u8); N] = [(0, 0); N];
for (i, &s) in soft.iter().enumerate() {
ranked[i] = (i, s.unsigned_abs());
}
ranked.sort_unstable_by_key(|&(_, conf)| conf);
ranked.iter().take(k.min(N)).map(|&(pos, _)| pos).collect()
}
fn soft_to_codeword_u32<const N: usize>(soft: &[i8; N]) -> u32 {
let mut cw = 0u32;
for &s in soft {
cw = (cw << 1) | u32::from(s > 0);
}
cw
}
fn soft_to_codeword_u16<const N: usize>(soft: &[i8; N]) -> u16 {
let mut cw = 0u16;
for &s in soft {
cw = (cw << 1) | u16::from(s > 0);
}
cw
}
fn soft_distance_u32<const N: usize>(soft: &[i8; N], candidate: u32) -> u32 {
let mut dist = 0u32;
for (i, &s) in soft.iter().enumerate() {
let cb = ((candidate >> (N - 1 - i)) & 1) as i8;
let hard = i8::from(s > 0);
if cb != hard {
dist += u32::from(s.unsigned_abs());
}
}
dist
}
fn soft_distance_u16<const N: usize>(soft: &[i8; N], candidate: u16) -> u32 {
let mut dist = 0u32;
for (i, &s) in soft.iter().enumerate() {
let cb = ((candidate >> (N - 1 - i)) & 1) as i8;
let hard = i8::from(s > 0);
if cb != hard {
dist += u32::from(s.unsigned_abs());
}
}
dist
}
fn chase_decode_u32<const N: usize, const T: u32>(
soft: &[i8; N],
hard_cw: u32,
weak: &[usize],
hard_decode: impl Fn(u32) -> (FecDecoded, u32),
) -> FecDecoded {
let mut best: Option<(FecDecoded, u32)> = None;
let num_patterns = 1u32 << weak.len();
for pattern in 0..num_patterns {
if pattern.count_ones() > T {
continue;
}
let mut candidate = hard_cw;
for (bit_idx, &pos) in weak.iter().enumerate() {
if (pattern >> bit_idx) & 1 == 1 {
candidate ^= 1u32 << (N - 1 - pos);
}
}
let (decoded, reencoded) = hard_decode(candidate);
let dist = soft_distance_u32::<N>(soft, reencoded);
match best {
Some((_, best_dist)) if dist >= best_dist => {}
_ => best = Some((decoded, dist)),
}
}
best.map(|(d, _)| d).expect("Chase search emits at least the zero-flip candidate")
}
fn chase_decode_u16<const N: usize, const T: u32>(
soft: &[i8; N],
hard_cw: u16,
weak: &[usize],
hard_decode: impl Fn(u16) -> (FecDecoded, u16),
) -> FecDecoded {
let mut best: Option<(FecDecoded, u32)> = None;
let num_patterns = 1u32 << weak.len();
for pattern in 0..num_patterns {
if pattern.count_ones() > T {
continue;
}
let mut candidate = hard_cw;
for (bit_idx, &pos) in weak.iter().enumerate() {
if (pattern >> bit_idx) & 1 == 1 {
candidate ^= 1u16 << (N - 1 - pos);
}
}
let (decoded, reencoded) = hard_decode(candidate);
let dist = soft_distance_u16::<N>(soft, reencoded);
match best {
Some((_, best_dist)) if dist >= best_dist => {}
_ => best = Some((decoded, dist)),
}
}
best.map(|(d, _)| d).expect("Chase search emits at least the zero-flip candidate")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn golay_roundtrip_all_info_words() {
for info in 0u16..(1 << 12) {
let cw = golay_23_12_encode(info);
assert!(cw >> 23 == 0, "encoded word must fit in 23 bits");
let d = golay_23_12_decode(cw);
assert_eq!(d.info, info);
assert_eq!(d.errors, 0);
}
}
#[test]
fn golay_minimum_distance_is_seven() {
for info in 1u16..(1 << 12) {
let cw = golay_23_12_encode(info);
assert!(
cw.count_ones() >= 7,
"codeword for info 0x{:03x} has weight {}",
info,
cw.count_ones()
);
}
}
#[test]
fn golay_corrects_up_to_three_errors() {
for info in [0x000u16, 0x001, 0x555, 0xAAA, 0xFFF] {
let cw = golay_23_12_encode(info);
for a in 0..23 {
for b in a..23 {
for c in b..23 {
let mut err = 1u32 << a;
if b != a {
err |= 1u32 << b;
}
if c != b {
err |= 1u32 << c;
}
let received = cw ^ err;
let d = golay_23_12_decode(received);
assert_eq!(d.info, info, "info={info:03x} err={err:07x}");
assert_eq!(d.errors, err.count_ones() as u8);
}
}
}
}
}
#[test]
fn hamming_roundtrip_all_info_words() {
for info in 0u16..(1 << 11) {
let cw = hamming_15_11_encode(info);
assert!(cw >> 15 == 0);
let d = hamming_15_11_decode(cw);
assert_eq!(d.info, info);
assert_eq!(d.errors, 0);
}
}
#[test]
fn hamming_minimum_distance_is_three() {
for info in 1u16..(1 << 11) {
let cw = hamming_15_11_encode(info);
assert!(cw.count_ones() >= 3);
}
}
#[test]
fn hamming_corrects_any_single_error() {
for info in [0x000u16, 0x001, 0x555, 0x2AA, 0x7FF] {
let cw = hamming_15_11_encode(info);
for bit in 0..15 {
let received = cw ^ (1u16 << bit);
let d = hamming_15_11_decode(received);
assert_eq!(d.info, info, "info={info:03x} bit={bit}");
assert_eq!(d.errors, 1);
}
}
}
#[test]
fn golay_24_12_roundtrip_all_info_words() {
for info in 0u16..(1 << 12) {
let cw = golay_24_12_encode(info);
assert!(cw >> 24 == 0, "encoded word must fit in 24 bits");
let d = golay_24_12_decode(cw);
assert_eq!(d.info, info);
assert_eq!(d.errors, 0);
}
}
#[test]
fn golay_24_12_minimum_distance_is_eight() {
for info in 1u16..(1 << 12) {
let cw = golay_24_12_encode(info);
assert!(
cw.count_ones() >= 8,
"info 0x{info:03x}: weight {}",
cw.count_ones()
);
}
}
#[test]
fn golay_24_12_corrects_single_and_triple_errors() {
let info = 0x555u16;
let cw = golay_24_12_encode(info);
for bit in 0..24 {
let d = golay_24_12_decode(cw ^ (1 << bit));
assert_eq!(d.info, info, "single bit {bit}");
assert_eq!(d.errors, 1);
}
let e3 = (1u32 << 0) | (1u32 << 5) | (1u32 << 18);
let d = golay_24_12_decode(cw ^ e3);
assert_eq!(d.info, info);
assert_eq!(d.errors, 3);
}
#[test]
fn golay_24_12_detects_some_four_error_patterns() {
let info = 0xABCu16;
let cw = golay_24_12_encode(info);
let mut detected = 0usize;
let mut total = 0usize;
for a in 0..24 {
for b in (a + 1)..24 {
for c in (b + 1)..24 {
for d_bit in (c + 1)..24 {
let err =
(1u32 << a) | (1u32 << b) | (1u32 << c) | (1u32 << d_bit);
let res = golay_24_12_decode(cw ^ err);
total += 1;
if res.errors == u8::MAX {
detected += 1;
}
}
}
}
}
assert!(
detected * 2 > total,
"detected only {detected}/{total} four-error patterns"
);
}
#[test]
fn golay_decodes_every_syndrome() {
let zero_cw = golay_23_12_encode(0);
for syndrome_bits in 0u32..(1 << 11) {
let received = zero_cw ^ syndrome_bits;
let d = golay_23_12_decode(received);
assert!(d.errors <= 3);
}
}
fn codeword_u32_to_soft<const N: usize>(cw: u32, confidence: i8) -> [i8; N] {
let mut soft = [0i8; N];
for i in 0..N {
let bit = (cw >> (N - 1 - i)) & 1;
soft[i] = if bit == 1 { confidence } else { -confidence };
}
soft
}
fn codeword_u16_to_soft<const N: usize>(cw: u16, confidence: i8) -> [i8; N] {
let mut soft = [0i8; N];
for i in 0..N {
let bit = (cw >> (N - 1 - i)) & 1;
soft[i] = if bit == 1 { confidence } else { -confidence };
}
soft
}
#[test]
fn soft_golay_23_roundtrip_high_confidence() {
for info in 0u16..(1 << 12) {
let cw = golay_23_12_encode(info);
let soft: [i8; 23] = codeword_u32_to_soft(cw, 120);
let d = golay_23_12_decode_soft(&soft);
assert_eq!(d.info, info);
assert_eq!(d.errors, 0);
}
}
#[test]
fn soft_golay_23_corrects_weak_errors_beyond_hard_capacity() {
let info = 0b110011001100u16;
let cw = golay_23_12_encode(info);
let mut soft: [i8; 23] = codeword_u32_to_soft(cw, 120);
for pos in [0, 5, 10, 15] {
soft[pos] = -soft[pos].signum() * 4;
}
let d = golay_23_12_decode_soft(&soft);
assert_eq!(d.info, info);
}
#[test]
fn soft_golay_23_matches_hard_on_strong_input() {
let info = 0b101101010010u16;
let cw = golay_23_12_encode(info);
let soft: [i8; 23] = codeword_u32_to_soft(cw, 127);
let soft_d = golay_23_12_decode_soft(&soft);
let hard_d = golay_23_12_decode(cw);
assert_eq!(soft_d.info, hard_d.info);
assert_eq!(soft_d.errors, 0);
}
#[test]
fn soft_golay_24_roundtrip_high_confidence() {
for info in [0x000u16, 0x001, 0x555, 0xAAA, 0xFFF, 0xABC] {
let cw = golay_24_12_encode(info);
let soft: [i8; 24] = codeword_u32_to_soft(cw, 120);
let d = golay_24_12_decode_soft(&soft);
assert_eq!(d.info, info);
assert_eq!(d.errors, 0);
}
}
#[test]
fn soft_hamming_15_roundtrip_high_confidence() {
for info in 0u16..(1 << 11) {
let cw = hamming_15_11_encode(info);
let soft: [i8; 15] = codeword_u16_to_soft(cw, 120);
let d = hamming_15_11_decode_soft(&soft);
assert_eq!(d.info, info);
assert_eq!(d.errors, 0);
}
}
#[test]
fn soft_hamming_15_corrects_single_error() {
let info = 0x2AAu16;
let cw = hamming_15_11_encode(info);
for bit in 0..15 {
let mut soft: [i8; 15] = codeword_u16_to_soft(cw, 120);
soft[bit] = -soft[bit];
let d = hamming_15_11_decode_soft(&soft);
assert_eq!(d.info, info, "bit {bit}");
assert_eq!(d.errors, 1);
}
}
#[test]
fn soft_hamming_15_prefers_weak_flip_over_miscorrection() {
let info = 0x555u16;
let cw = hamming_15_11_encode(info);
let mut soft: [i8; 15] = codeword_u16_to_soft(cw, 127);
soft[10] = -soft[10].signum() * 3;
let d = hamming_15_11_decode_soft(&soft);
assert_eq!(d.info, info);
assert_eq!(d.errors, 1);
}
}