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-02
//! Reference-vector conformance tests (R6).
//!
//! Validates the production [`ViterbiEncoder`] against two independent CCSDS
//! K=7 R=1/2 references:
//! 1. An impulse response traced by hand from the CCSDS definition.
//! 2. A deliberately-naive, independently-written encoder oracle.
//!
//! # Provenance of the impulse response `[0xBA, 0x48]`
//!
//! The impulse vector is **derived directly from the published CCSDS 131.0-B
//! generator polynomials**, not copied from a black-box fixture:
//! - `G1 = 0o171 = 1 + D + D² + D³ + D⁶`
//! - `G2 = 0o133 = 1 + D² + D³ + D⁵ + D⁶` (its output is **inverted** per CCSDS)
//! - output order `C1(1), C2(1), C1(2), C2(2), …`, MSB-first packing.
//!
//! Feeding a single `1` followed by the 6-bit zero tail through those polynomials
//! yields the 14-bit impulse response `[0xBA, 0x48]`. It is **independently
//! cross-validated** by the [`naive_ccsds_encode`] oracle below, which computes the
//! parity of `window & G` directly (a different code path from the production
//! trellis), so a bug shared with the production encoder cannot make both agree.
//! An external `libfec` (Karn) sourced fixture is a CI / follow-up cross-check.

use viterbi::encoder::ViterbiEncoder;
use viterbi::params::CodeParams;

/// Number of CCSDS tail bits (memory `m = K - 1 = 6`).
const TAIL_BITS: usize = 6;
/// G1 generator polynomial (`0o171`).
const G1: u32 = 0o171;
/// G2 generator polynomial (`0o133`); its output is inverted per CCSDS.
const G2: u32 = 0o133;

#[test]
fn encoder_matches_traced_ccsds_impulse() {
    let enc = ViterbiEncoder::new(CodeParams::ccsds_r1_2()).unwrap();
    // Impulse: one info bit `1` (MSB of 0x80) + 6-bit zero tail -> 7 stages, 14 coded bits.
    assert_eq!(
        enc.encode_bits(&[0x80], 1).unwrap(),
        viterbi::packing::CodedBlock {
            bytes: vec![0xBA, 0x48],
            nbits: 14,
        }
    );
    // All-zero one-byte payload: 14 stages; the (state,input=0) branch defines every symbol.
    assert_eq!(enc.encode(&[0x00]).unwrap().nbits, 28);
}

/// Independent, deliberately-naive CCSDS K=7 R=1/2 encoder — a conformance **oracle** written
/// differently from the production path (direct parity of `window & G`, G2 inverted, MSB-first pack).
fn naive_ccsds_encode(data: &[u8], nbits: usize) -> Vec<u8> {
    let mut reg: u32 = 0; // 7-bit window: current input at bit6, 6-bit history in bits5..0
    let mut bits: Vec<u8> = Vec::new();
    let step = |input: u8, bits: &mut Vec<u8>, reg: &mut u32| {
        let g = ((input as u32) << 6) | *reg;
        bits.push(((g & G1).count_ones() & 1) as u8);
        bits.push((((g & G2).count_ones() & 1) ^ 1) as u8); // G2 inverted
        *reg = (g >> 1) & 0x3F;
    };
    for i in 0..nbits {
        step((data[i / 8] >> (7 - (i % 8))) & 1, &mut bits, &mut reg);
    }
    for _ in 0..TAIL_BITS {
        step(0, &mut bits, &mut reg); // zero-tail flush
    }
    let mut out = vec![0u8; bits.len().div_ceil(8)];
    for (i, &b) in bits.iter().enumerate() {
        if b == 1 {
            out[i / 8] |= 1 << (7 - (i % 8));
        }
    }
    out
}

#[test]
fn encoder_matches_independent_oracle() {
    let enc = ViterbiEncoder::new(CodeParams::ccsds_r1_2()).unwrap();
    for data in [
        &[0x00u8][..],
        &[0xFF],
        &[0x80],
        &[0x01, 0x23, 0x45, 0x67],
        b"CCSDS",
    ] {
        assert_eq!(
            enc.encode(data).unwrap().bytes,
            naive_ccsds_encode(data, data.len() * 8),
            "production encoder must match the independent oracle for {data:?}"
        );
    }
}