use proptest::prelude::*;
use viterbi::{
BranchMetric, CodeParams, CodedBlock, DePuncturer, HardHamming, PunctureError, PunctureMatrix,
PunctureOrder, PuncturedRate, Puncturer, SoftLlr, ViterbiDecoder, ViterbiEncoder,
};
#[test]
fn new_rejects_no_rows_as_empty_pattern() {
let err = PunctureMatrix::new(Vec::new(), PunctureOrder::ByColumn).unwrap_err();
assert_eq!(err, PunctureError::EmptyPattern);
}
#[test]
fn new_rejects_zero_length_rows_as_empty_pattern() {
let err =
PunctureMatrix::new(vec![Vec::new(), Vec::new()], PunctureOrder::ByColumn).unwrap_err();
assert_eq!(err, PunctureError::EmptyPattern);
}
#[test]
fn new_rejects_ragged_rows() {
let err = PunctureMatrix::new(vec![vec![true, true], vec![true]], PunctureOrder::ByColumn)
.unwrap_err();
assert_eq!(err, PunctureError::RaggedRows);
}
#[test]
fn new_rejects_more_than_three_rows() {
let err = PunctureMatrix::new(vec![vec![true]; 4], PunctureOrder::ByColumn).unwrap_err();
assert_eq!(err, PunctureError::RowCountMismatch { rows: 4, n: 3 });
}
#[test]
fn new_rejects_period_above_max() {
let period = viterbi::puncture::MAX_PERIOD + 1;
let err = PunctureMatrix::new(vec![vec![true; period]], PunctureOrder::ByColumn).unwrap_err();
assert_eq!(
err,
PunctureError::PeriodTooLarge {
period,
cap: viterbi::puncture::MAX_PERIOD,
}
);
}
#[test]
fn new_rejects_all_void_column() {
let err = PunctureMatrix::new(
vec![vec![true, false], vec![true, false]],
PunctureOrder::ByColumn,
)
.unwrap_err();
assert_eq!(err, PunctureError::AllVoidPeriod);
}
#[test]
fn new_accepts_valid_pattern_and_caches_metadata() {
let m = PunctureMatrix::new(
vec![vec![true, true], vec![true, false]],
PunctureOrder::ByColumn,
)
.unwrap();
assert_eq!(m.n(), 2);
assert_eq!(m.period(), 2);
assert_eq!(m.kept_per_period(), 3);
assert_eq!(m.order(), PunctureOrder::ByColumn);
}
#[test]
fn ccsds_rate_r3_4_has_expected_shape() {
let m = PunctureMatrix::ccsds_rate(2, PuncturedRate::R3_4).unwrap();
assert_eq!(m.n(), 2);
assert_eq!(m.period(), 3);
assert_eq!(m.kept_per_period(), 4);
assert_eq!(m.order(), PunctureOrder::Interleaved);
assert_eq!(m.code_rate(), (3, 4));
}
#[test]
fn ccsds_rate_all_supported_rates_match_yasuda() {
let cases = [
(PuncturedRate::R2_3, 2usize, 3usize, (2usize, 3usize)),
(PuncturedRate::R3_4, 3, 4, (3, 4)),
(PuncturedRate::R5_6, 5, 6, (5, 6)),
(PuncturedRate::R7_8, 7, 8, (7, 8)),
];
for (rate, period, kept, code_rate) in cases {
let m = PunctureMatrix::ccsds_rate(2, rate).unwrap();
assert_eq!(m.n(), 2);
assert_eq!(m.period(), period);
assert_eq!(m.kept_per_period(), kept);
assert_eq!(m.code_rate(), code_rate);
assert_eq!(m.order(), PunctureOrder::Interleaved);
}
}
#[test]
fn ccsds_rate_rejects_n1() {
let err = PunctureMatrix::ccsds_rate(1, PuncturedRate::R2_3).unwrap_err();
assert_eq!(err, PunctureError::UnsupportedRate);
}
#[test]
fn ccsds_rate_rejects_n3() {
let err = PunctureMatrix::ccsds_rate(3, PuncturedRate::R3_4).unwrap_err();
assert_eq!(err, PunctureError::UnsupportedRate);
}
#[test]
fn ccsds_rate_rejects_n4() {
let err = PunctureMatrix::ccsds_rate(4, PuncturedRate::R2_3).unwrap_err();
assert_eq!(err, PunctureError::UnsupportedRate);
}
#[test]
fn puncturer_rejects_catastrophic_n2() {
let m = PunctureMatrix::new(
vec![vec![true, true], vec![false, false]],
PunctureOrder::ByColumn,
)
.unwrap();
assert_eq!(
Puncturer::new(m, &CodeParams::ccsds_r1_2()).unwrap_err(),
PunctureError::Catastrophic
);
}
#[test]
fn puncturer_rejects_catastrophic_n3() {
let m = PunctureMatrix::new(
vec![vec![true, true], vec![false, false], vec![false, false]],
PunctureOrder::ByColumn,
)
.unwrap();
assert_eq!(
Puncturer::new(m, &CodeParams::ccsds_r1_3()).unwrap_err(),
PunctureError::Catastrophic
);
}
#[test]
fn puncturer_rejects_catastrophic_period3() {
let m = PunctureMatrix::new(
vec![vec![true, true, true], vec![false, false, false]],
PunctureOrder::ByColumn,
)
.unwrap();
assert_eq!(
Puncturer::new(m, &CodeParams::ccsds_r1_2()).unwrap_err(),
PunctureError::Catastrophic
);
}
#[test]
fn puncturer_accepts_known_good_2_3() {
let m = PunctureMatrix::ccsds_rate(2, PuncturedRate::R2_3).unwrap();
assert!(Puncturer::new(m, &CodeParams::ccsds_r1_2()).is_ok());
}
#[test]
fn puncturer_accepts_known_good_n3() {
let m = PunctureMatrix::new(
vec![vec![true, true], vec![true, false], vec![false, true]],
PunctureOrder::ByColumn,
)
.unwrap();
assert!(Puncturer::new(m, &CodeParams::ccsds_r1_3()).is_ok());
}
#[test]
fn puncturer_rejects_catastrophic_phase_interaction() {
let m = PunctureMatrix::new(
vec![vec![false, false, true], vec![true, true, false]],
PunctureOrder::ByColumn,
)
.unwrap();
assert_eq!(
Puncturer::new(m, &CodeParams::ccsds_r1_2()).unwrap_err(),
PunctureError::Catastrophic
);
}
#[test]
fn puncturer_new_rejects_n_mismatch() {
let m = PunctureMatrix::ccsds_rate(2, PuncturedRate::R2_3).unwrap();
assert_eq!(
Puncturer::new(m, &CodeParams::ccsds_r1_3()).unwrap_err(),
PunctureError::NMismatch {
matrix_n: 2,
params_n: 3,
}
);
}
#[test]
fn puncture_deletes_body_and_keeps_tail_length() {
let enc = ViterbiEncoder::new(CodeParams::ccsds_r1_2()).unwrap();
let coded = enc.encode(&[0xB2]).unwrap(); assert_eq!(coded.nbits, 28);
let p = Puncturer::new(
PunctureMatrix::ccsds_rate(2, PuncturedRate::R2_3).unwrap(),
&CodeParams::ccsds_r1_2(),
)
.unwrap();
let punctured = p.puncture(&coded, 8).unwrap();
assert_eq!(punctured.nbits, 24);
}
#[test]
fn puncture_rejects_inconsistent_nbits() {
let enc = ViterbiEncoder::new(CodeParams::ccsds_r1_2()).unwrap();
let coded = enc.encode(&[0xB2]).unwrap();
let p = Puncturer::new(
PunctureMatrix::ccsds_rate(2, PuncturedRate::R2_3).unwrap(),
&CodeParams::ccsds_r1_2(),
)
.unwrap();
assert_eq!(
p.puncture(&coded, 7).unwrap_err(),
PunctureError::MisalignedInput {
got: 28,
expected: 26,
}
);
}
#[test]
fn puncture_rejects_near_usize_max_nbits_without_overflow() {
let enc = ViterbiEncoder::new(CodeParams::ccsds_r1_2()).unwrap();
let coded = enc.encode(&[0xB2]).unwrap();
let p = Puncturer::new(
PunctureMatrix::ccsds_rate(2, PuncturedRate::R2_3).unwrap(),
&CodeParams::ccsds_r1_2(),
)
.unwrap();
assert!(matches!(
p.puncture(&coded, usize::MAX - 1),
Err(PunctureError::PayloadTooLarge { .. })
));
}
#[test]
fn puncture_rejects_short_coded_bytes_without_panic() {
let p = Puncturer::new(
PunctureMatrix::ccsds_rate(2, PuncturedRate::R3_4).unwrap(),
&CodeParams::ccsds_r1_2(),
)
.unwrap();
assert_eq!(28, (8 + 6) * 2); let malformed = CodedBlock {
bytes: vec![0u8; 1], nbits: 28,
};
assert!(matches!(
p.puncture(&malformed, 8),
Err(PunctureError::MisalignedInput { .. })
));
}
#[test]
fn depuncture_inserts_erasures_at_body_and_copies_tail_verbatim() {
let matrix = PunctureMatrix::new(
vec![vec![true, true], vec![true, false]],
PunctureOrder::Interleaved,
)
.unwrap();
let dp = DePuncturer::<SoftLlr>::new(matrix, &CodeParams::ccsds_r1_2()).unwrap();
assert_eq!(dp.expected_punctured_len(2), Some(15));
let received: Vec<i8> = vec![
10, 11, 12, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, ];
assert_eq!(received.len(), 15);
let full = dp.depuncture(&received, 2).unwrap();
assert_eq!(full.len(), 16);
assert_eq!(&full[0..3], &[10, 11, 12]);
assert_eq!(full[3], SoftLlr::erasure()); assert_eq!(&full[4..16], &received[3..15]);
}
#[test]
fn depuncture_rejects_wrong_length_input() {
let matrix = PunctureMatrix::new(
vec![vec![true, true], vec![true, false]],
PunctureOrder::ByColumn,
)
.unwrap();
let dp = DePuncturer::<SoftLlr>::new(matrix, &CodeParams::ccsds_r1_2()).unwrap();
let received = vec![0i8; 14];
assert_eq!(
dp.depuncture(&received, 2).unwrap_err(),
PunctureError::MisalignedInput {
got: 14,
expected: 15,
}
);
}
#[test]
fn depuncture_new_rejects_n_mismatch() {
let m = PunctureMatrix::ccsds_rate(2, PuncturedRate::R2_3).unwrap();
assert_eq!(
DePuncturer::<SoftLlr>::new(m, &CodeParams::ccsds_r1_3()).unwrap_err(),
PunctureError::NMismatch {
matrix_n: 2,
params_n: 3,
}
);
}
#[test]
fn depuncture_new_rejects_catastrophic() {
let m = PunctureMatrix::new(
vec![vec![true, true], vec![false, false]],
PunctureOrder::ByColumn,
)
.unwrap();
assert_eq!(
DePuncturer::<SoftLlr>::new(m, &CodeParams::ccsds_r1_2()).unwrap_err(),
PunctureError::Catastrophic
);
}
#[test]
fn expected_punctured_len_strictly_increasing_over_aligned_nbits() {
let matrix = PunctureMatrix::ccsds_rate(2, PuncturedRate::R2_3).unwrap(); let dp = DePuncturer::<SoftLlr>::new(matrix, &CodeParams::ccsds_r1_2()).unwrap();
let l1 = dp.expected_punctured_len(2).unwrap();
let l2 = dp.expected_punctured_len(4).unwrap();
let l3 = dp.expected_punctured_len(6).unwrap();
assert!(
l1 < l2 && l2 < l3,
"lengths must be strictly increasing: {l1} {l2} {l3}"
);
assert_eq!((l1, l2, l3), (15, 18, 21));
}
#[test]
fn depuncture_near_usize_max_nbits_fails_gracefully() {
let matrix = PunctureMatrix::ccsds_rate(2, PuncturedRate::R2_3).unwrap();
let dp = DePuncturer::<SoftLlr>::new(matrix, &CodeParams::ccsds_r1_2()).unwrap();
assert!(dp.expected_punctured_len(usize::MAX).is_none());
assert!(matches!(
dp.depuncture(&[], usize::MAX),
Err(PunctureError::PayloadTooLarge { .. })
));
}
fn coded_to_hard(coded: &CodedBlock) -> Vec<u8> {
(0..coded.nbits)
.map(|i| (coded.bytes[i / 8] >> (7 - (i % 8))) & 1)
.collect()
}
fn coded_to_soft(coded: &CodedBlock) -> Vec<i8> {
coded_to_hard(coded)
.into_iter()
.map(|b| if b == 0 { 64 } else { -64 })
.collect()
}
fn first_bits(bytes: &[u8], nbits: usize) -> Vec<u8> {
(0..nbits)
.map(|i| (bytes[i / 8] >> (7 - (i % 8))) & 1)
.collect()
}
fn expected_depunctured<S: Copy>(
orig: &[S],
keep: &[Vec<bool>],
n: usize,
nbits: usize,
erasure: S,
) -> Vec<S> {
let period = keep[0].len();
let mut out = Vec::with_capacity(orig.len());
for t in 0..nbits {
let phase = t % period;
for j in 0..n {
if keep[j][phase] {
out.push(orig[t * n + j]);
} else {
out.push(erasure);
}
}
}
for &s in &orig[nbits * n..] {
out.push(s);
}
out
}
fn yasuda_keep(sel: usize) -> Vec<Vec<bool>> {
let rows: &[&[u8]] = match sel {
0 => &[&[1, 1], &[1, 0]],
1 => &[&[1, 0, 1], &[1, 1, 0]],
2 => &[&[1, 0, 1, 0, 1], &[1, 1, 0, 1, 0]],
_ => &[&[1, 0, 0, 0, 1, 0, 1], &[1, 1, 1, 1, 0, 1, 0]],
};
rows.iter()
.map(|row| row.iter().map(|&b| b == 1).collect())
.collect()
}
fn n3_keep() -> Vec<Vec<bool>> {
vec![vec![true, true], vec![true, false], vec![false, true]]
}
fn check_punctured_roundtrip(
params: &CodeParams,
keep: Vec<Vec<bool>>,
data: &[u8],
nbits: usize,
) -> Result<(), TestCaseError> {
let n = params.n();
let matrix = PunctureMatrix::new(keep.clone(), PunctureOrder::Interleaved).unwrap();
let enc = ViterbiEncoder::new(params.clone()).unwrap();
let coded = enc.encode_bits(data, nbits).unwrap();
let punc = Puncturer::new(matrix.clone(), params).unwrap();
let punctured = punc.puncture(&coded, nbits).unwrap();
let dp_h = DePuncturer::<HardHamming>::new(matrix.clone(), params).unwrap();
let full_h = dp_h.depuncture(&coded_to_hard(&punctured), nbits).unwrap();
let exp_h = expected_depunctured(
&coded_to_hard(&coded),
&keep,
n,
nbits,
HardHamming::erasure(),
);
prop_assert_eq!(
&full_h,
&exp_h,
"hard depuncture must restore non-punctured positions"
);
let mut dec_h = ViterbiDecoder::<64, HardHamming>::new(params.clone(), nbits).unwrap();
let out_h = dec_h.decode(&full_h, nbits).unwrap();
prop_assert_eq!(
first_bits(&out_h.bytes, nbits),
first_bits(data, nbits),
"hard clean round-trip must recover the payload"
);
let dp_s = DePuncturer::<SoftLlr>::new(matrix, params).unwrap();
let full_s = dp_s.depuncture(&coded_to_soft(&punctured), nbits).unwrap();
let exp_s = expected_depunctured(&coded_to_soft(&coded), &keep, n, nbits, SoftLlr::erasure());
prop_assert_eq!(
&full_s,
&exp_s,
"soft depuncture must restore non-punctured positions"
);
let mut dec_s = ViterbiDecoder::<64, SoftLlr>::new(params.clone(), nbits).unwrap();
let out_s = dec_s.decode(&full_s, nbits).unwrap();
prop_assert_eq!(
first_bits(&out_s.bytes, nbits),
first_bits(data, nbits),
"soft clean round-trip must recover the payload"
);
Ok(())
}
fn data_and_nbits() -> impl Strategy<Value = (Vec<u8>, usize)> {
(1usize..=160).prop_flat_map(|nbits| {
(
proptest::collection::vec(any::<u8>(), nbits.div_ceil(8)),
Just(nbits),
)
})
}
proptest! {
#[test]
fn punctured_roundtrip_n2_restores_and_decodes(
(data, nbits) in data_and_nbits(),
sel in 0usize..4,
) {
check_punctured_roundtrip(&CodeParams::ccsds_r1_2(), yasuda_keep(sel), &data, nbits)?;
}
#[test]
fn punctured_roundtrip_n3_restores_and_decodes(
(data, nbits) in data_and_nbits(),
) {
check_punctured_roundtrip(&CodeParams::ccsds_r1_3(), n3_keep(), &data, nbits)?;
}
}
#[test]
fn depuncture_tail_exemption_hand_vector_n2_non_aligned() {
let matrix = PunctureMatrix::new(
vec![vec![true, false, true], vec![true, true, false]],
PunctureOrder::Interleaved,
)
.unwrap();
let dp = DePuncturer::<SoftLlr>::new(matrix, &CodeParams::ccsds_r1_2()).unwrap();
assert_eq!(dp.expected_punctured_len(4), Some(18));
let on_air: Vec<i8> = vec![
1, 2, 3, 4, 5, 6, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, ];
assert_eq!(on_air.len(), 18);
let full = dp.depuncture(&on_air, 4).unwrap();
assert_eq!(full.len(), 20);
#[rustfmt::skip]
let expected: Vec<i8> = vec![
1, 2, 0, 3, 4, 0, 5, 6, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, ];
assert_eq!(full, expected);
assert_eq!(&full[8..20], &on_air[6..18]);
}
#[test]
fn depuncture_tail_exemption_hand_vector_n3_non_aligned() {
let matrix = PunctureMatrix::new(n3_keep(), PunctureOrder::Interleaved).unwrap();
let dp = DePuncturer::<SoftLlr>::new(matrix, &CodeParams::ccsds_r1_3()).unwrap();
assert_eq!(dp.expected_punctured_len(3), Some(24));
let mut on_air: Vec<i8> = vec![1, 2, 3, 4, 5, 6]; on_air.extend(101..=118); assert_eq!(on_air.len(), 24);
let full = dp.depuncture(&on_air, 3).unwrap();
assert_eq!(full.len(), 27);
let mut expected: Vec<i8> = vec![
1, 2, 0, 3, 0, 4, 5, 6, 0, ];
expected.extend(101..=118); assert_eq!(full, expected);
assert_eq!(&full[9..27], &on_air[6..24]);
}
#[test]
fn bycolumn_puncture_depuncture_decode_round_trips() {
let params = CodeParams::ccsds_r1_2();
let keep = yasuda_keep(1); let data = vec![0xB4u8, 0x2Cu8];
let nbits = 13; let n = params.n();
let matrix = PunctureMatrix::new(keep.clone(), PunctureOrder::ByColumn).unwrap();
let enc = ViterbiEncoder::new(params.clone()).unwrap();
let coded = enc.encode_bits(&data, nbits).unwrap();
let punc = Puncturer::new(matrix.clone(), ¶ms).unwrap();
let punctured = punc.puncture(&coded, nbits).unwrap();
let dp = DePuncturer::<HardHamming>::new(matrix, ¶ms).unwrap();
let full = dp.depuncture(&coded_to_hard(&punctured), nbits).unwrap();
let expected = expected_depunctured(
&coded_to_hard(&coded),
&keep,
n,
nbits,
HardHamming::erasure(),
);
assert_eq!(
full, expected,
"ByColumn depuncture must restore the interleaved t*n+j stream"
);
let mut dec = ViterbiDecoder::<64, HardHamming>::new(params.clone(), nbits).unwrap();
let out = dec.decode(&full, nbits).unwrap();
assert_eq!(
first_bits(&out.bytes, nbits),
first_bits(&data, nbits),
"ByColumn clean round-trip must recover the payload"
);
}
#[test]
fn bycolumn_and_interleaved_produce_different_on_air_order() {
let params = CodeParams::ccsds_r1_2();
let keep = yasuda_keep(1); let data = vec![0xB4u8, 0x2Cu8];
let nbits = 12;
let enc = ViterbiEncoder::new(params.clone()).unwrap();
let coded = enc.encode_bits(&data, nbits).unwrap();
let m_col = PunctureMatrix::new(keep.clone(), PunctureOrder::ByColumn).unwrap();
let m_int = PunctureMatrix::new(keep, PunctureOrder::Interleaved).unwrap();
let out_col = Puncturer::new(m_col, ¶ms)
.unwrap()
.puncture(&coded, nbits)
.unwrap();
let out_int = Puncturer::new(m_int, ¶ms)
.unwrap()
.puncture(&coded, nbits)
.unwrap();
assert_eq!(
out_col.nbits, out_int.nbits,
"same keep-pattern keeps the same bit count"
);
assert_ne!(
out_col.bytes, out_int.bytes,
"ByColumn must reorder the on-air bits vs Interleaved"
);
}
#[test]
fn expected_punctured_len_strictly_increasing_over_small_n_period_pairs() {
let cases: Vec<(CodeParams, PunctureMatrix)> = vec![
(
CodeParams::ccsds_r1_2(),
PunctureMatrix::ccsds_rate(2, PuncturedRate::R2_3).unwrap(),
),
(
CodeParams::ccsds_r1_2(),
PunctureMatrix::ccsds_rate(2, PuncturedRate::R3_4).unwrap(),
),
(
CodeParams::ccsds_r1_2(),
PunctureMatrix::ccsds_rate(2, PuncturedRate::R5_6).unwrap(),
),
(
CodeParams::ccsds_r1_2(),
PunctureMatrix::ccsds_rate(2, PuncturedRate::R7_8).unwrap(),
),
(
CodeParams::ccsds_r1_3(),
PunctureMatrix::new(n3_keep(), PunctureOrder::Interleaved).unwrap(),
),
];
for (params, matrix) in cases {
let period = matrix.period();
let dp = DePuncturer::<SoftLlr>::new(matrix, ¶ms).unwrap();
let mut prev: Option<usize> = None;
for k in 1..=20 {
let nbits = k * period;
let len = dp
.expected_punctured_len(nbits)
.expect("small aligned nbits never overflows");
if let Some(p) = prev {
assert!(
len > p,
"expected_punctured_len must strictly increase: {p} -> {len} at nbits={nbits}"
);
}
prev = Some(len);
}
}
}
#[test]
fn depuncture_wrong_nbits_is_flagged_misaligned() {
let matrix = PunctureMatrix::ccsds_rate(2, PuncturedRate::R3_4).unwrap();
let dp = DePuncturer::<SoftLlr>::new(matrix, &CodeParams::ccsds_r1_2()).unwrap();
let nbits = 6;
let len = dp.expected_punctured_len(nbits).unwrap();
let on_air = vec![1i8; len];
assert!(dp.depuncture(&on_air, nbits).is_ok());
let len_next = dp.expected_punctured_len(nbits + 1).unwrap();
assert_ne!(len, len_next, "distinct nbits must give distinct lengths");
assert_eq!(
dp.depuncture(&on_air, nbits + 1).unwrap_err(),
PunctureError::MisalignedInput {
got: len,
expected: len_next,
}
);
}