viterbi 0.2.0

Pure-Rust, no-unsafe convolutional encoder + Viterbi (MLSE) decoder — CCSDS inner FEC: hard & soft-decision (LLR), rate 1/2 & 1/3, puncturing (2/3–7/8), generic constraint length K<=9.
Documentation
// Author: BolivarTech
// Version: 1.0.0
// Date: 2026-07-05
use viterbi::{CcsdsSoftDecoder, CodeParams, CodedBlock, ViterbiEncoder};

fn hard_bit_to_llr(bit: u8) -> i8 {
    if bit == 0 {
        64
    } else {
        -64
    }
} // λ>0 ⇒ bit 0

/// Map every MSB-first coded bit of `coded` to a confident ±64 LLR (a clean, hard-quantized channel).
fn coded_to_llrs(coded: &CodedBlock) -> Vec<i8> {
    (0..coded.nbits)
        .map(|i| hard_bit_to_llr((coded.bytes[i / 8] >> (7 - (i % 8))) & 1))
        .collect()
}

/// Spacing (in coded bits) between injected errors so each sits beyond the constraint span
/// (~K = 7 info bits ⇒ ~14 coded bits) and is therefore independently correctable.
const SPACED_ERROR_STRIDE: usize = 40;

#[test]
fn soft_round_trip_clean_channel() {
    let enc = ViterbiEncoder::new(CodeParams::ccsds_r1_2()).unwrap();
    let data = b"soft!";
    let coded = enc.encode(data).unwrap();
    let mut llrs = Vec::new();
    for i in 0..coded.nbits {
        let bit = (coded.bytes[i / 8] >> (7 - (i % 8))) & 1;
        llrs.push(hard_bit_to_llr(bit));
    }
    let mut dec = CcsdsSoftDecoder::new(CodeParams::ccsds_r1_2(), 1_000).unwrap();
    let out = dec.decode(&llrs, data.len() * 8).unwrap();
    assert_eq!(&out.bytes[..data.len()], data);
}

/// Soft decoding is deterministic (R10): decoding the same LLRs twice yields bit-identical output.
#[test]
fn soft_decode_is_deterministic() {
    let enc = ViterbiEncoder::new(CodeParams::ccsds_r1_2()).unwrap();
    let data = b"determinism";
    let coded = enc.encode(data).unwrap();
    let llrs = coded_to_llrs(&coded);
    let mut dec = CcsdsSoftDecoder::new(CodeParams::ccsds_r1_2(), 1_000).unwrap();
    let a = dec.decode(&llrs, data.len() * 8).unwrap();
    let b = dec.decode(&llrs, data.len() * 8).unwrap();
    assert_eq!(a, b);
}

/// Long-stream (≥ 10_000 info bits) soft round-trip so the every-stage subtract-min renormalization
/// and its `min == 0 && max ≤ B` spread invariant are exercised on the `SoftLlr` path (the ACS
/// `debug_assert!` is active in the test build), completing without overflow.
#[test]
fn soft_long_stream_round_trip_exercises_renorm() {
    let enc = ViterbiEncoder::new(CodeParams::ccsds_r1_2()).unwrap();
    // 1_500 bytes = 12_000 info bits (> 10_000), a spread-independent-of-length stress of the renorm.
    let raw: Vec<u8> = (0..1_500u32).map(|i| (i as u8).wrapping_mul(37)).collect();
    let coded = enc.encode(&raw).unwrap();
    let llrs = coded_to_llrs(&coded);
    let mut dec = CcsdsSoftDecoder::new(CodeParams::ccsds_r1_2(), 20_000).unwrap();
    let out = dec.decode(&llrs, raw.len() * 8).unwrap();
    assert_eq!(out.bytes, raw);
}

proptest::proptest! {
    /// Bounded-error soft channel: inverting the LLR sign of one coded bit per `SPACED_ERROR_STRIDE`
    /// window (a confident-but-wrong sample) keeps each error isolated beyond the constraint span, so
    /// the soft decoder still recovers the exact payload.
    #[test]
    fn soft_recovers_under_spaced_errors(
        raw in proptest::collection::vec(proptest::num::u8::ANY, 1..128)
    ) {
        let enc = ViterbiEncoder::new(CodeParams::ccsds_r1_2()).unwrap();
        let coded = enc.encode(&raw).unwrap();
        let mut llrs = coded_to_llrs(&coded);
        let mut i = SPACED_ERROR_STRIDE / 2;
        while i < coded.nbits {
            llrs[i] = -llrs[i]; // flip the sign ⇒ confident wrong bit (isolated, correctable)
            i += SPACED_ERROR_STRIDE;
        }
        let mut dec = CcsdsSoftDecoder::new(CodeParams::ccsds_r1_2(), 1_024).unwrap();
        let out = dec.decode(&llrs, raw.len() * 8).unwrap();
        proptest::prop_assert_eq!(out.bytes, raw);
    }
}