use crate::K as CCSDS_K;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParamError {
ZeroGenerator,
GeneratorOutOfRange {
g: u32,
k: u8,
},
KOutOfRange {
k: u8,
},
KUnsupported {
k: u8,
},
RateUnsupported {
n: usize,
},
InconsistentLengths,
Catastrophic,
StateCountMismatch {
states: usize,
k: u8,
},
}
impl core::fmt::Display for ParamError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{self:?}")
}
}
impl std::error::Error for ParamError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CodeProfile {
CcsdsR1_2,
CcsdsR1_3,
K9R1_2,
}
impl CodeProfile {
#[must_use]
pub fn params(self) -> CodeParams {
match self {
CodeProfile::CcsdsR1_2 => CodeParams::ccsds_r1_2(),
CodeProfile::CcsdsR1_3 => CodeParams::ccsds_r1_3(),
CodeProfile::K9R1_2 => {
let p = CodeParams {
k: 9,
generators: vec![0o561, 0o753],
invert_outputs: vec![false, false],
};
debug_assert!(p.validate().is_ok(), "K9R1_2 preset must be valid");
p
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CodeParams {
pub(crate) k: u8,
pub(crate) generators: Vec<u32>,
pub(crate) invert_outputs: Vec<bool>,
}
impl CodeParams {
#[must_use]
pub fn k(&self) -> u8 {
self.k
}
#[must_use]
pub fn generators(&self) -> &[u32] {
&self.generators
}
#[must_use]
pub fn invert_outputs(&self) -> &[bool] {
&self.invert_outputs
}
#[must_use]
pub fn n(&self) -> usize {
self.generators.len()
}
pub fn new(k: u8, generators: Vec<u32>, invert_outputs: Vec<bool>) -> Result<Self, ParamError> {
let p = Self {
k,
generators,
invert_outputs,
};
p.validate()?;
Ok(p)
}
pub(crate) fn validate(&self) -> Result<(), ParamError> {
if !(1..=9).contains(&self.k) {
return Err(ParamError::KOutOfRange { k: self.k });
}
if self.generators.is_empty() || self.invert_outputs.len() != self.generators.len() {
return Err(ParamError::InconsistentLengths);
}
let limit = 1u32 << self.k;
for &g in &self.generators {
if g == 0 {
return Err(ParamError::ZeroGenerator);
}
if g >= limit {
return Err(ParamError::GeneratorOutOfRange { g, k: self.k });
}
}
if is_catastrophic(&self.generators) {
return Err(ParamError::Catastrophic);
}
Ok(())
}
#[must_use]
pub fn ccsds_r1_2() -> Self {
let p = Self {
k: CCSDS_K,
generators: vec![0o171, 0o133],
invert_outputs: vec![false, true],
};
debug_assert!(p.validate().is_ok(), "CCSDS preset must be valid");
p
}
#[must_use]
pub fn ccsds_r1_3() -> Self {
let p = Self {
k: CCSDS_K,
generators: vec![0o133, 0o171, 0o145],
invert_outputs: vec![false, false, false],
};
debug_assert!(p.validate().is_ok(), "CCSDS rate-1/3 preset must be valid");
p
}
}
fn is_catastrophic(generators: &[u32]) -> bool {
let mut g = 0u32;
for &x in generators {
g = gcd_gf2(g, x);
}
g.count_ones() != 1
}
fn gcd_gf2(mut a: u32, mut b: u32) -> u32 {
while a != 0 && b != 0 {
if a.leading_zeros() > b.leading_zeros() {
core::mem::swap(&mut a, &mut b);
}
let shift = b.leading_zeros() - a.leading_zeros();
a ^= b << shift;
}
a | b }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ccsds_preset_is_valid_and_correct() {
let p = CodeParams::ccsds_r1_2();
assert_eq!(p.k, 7);
assert_eq!(p.generators, vec![0o171, 0o133]);
assert_eq!(p.invert_outputs, vec![false, true]);
}
#[test]
fn rejects_zero_generator() {
assert_eq!(
CodeParams::new(7, vec![0o171, 0], vec![false, true]),
Err(ParamError::ZeroGenerator)
);
}
#[test]
fn rejects_k_out_of_range() {
assert_eq!(
CodeParams::new(10, vec![1, 1], vec![false, false]),
Err(ParamError::KOutOfRange { k: 10 })
);
}
#[test]
fn rejects_generator_out_of_range() {
assert_eq!(
CodeParams::new(3, vec![0b1000, 0b101], vec![false, false]),
Err(ParamError::GeneratorOutOfRange { g: 0b1000, k: 3 })
);
}
#[test]
fn rejects_inconsistent_lengths() {
assert_eq!(
CodeParams::new(7, vec![0o171, 0o133], vec![false]),
Err(ParamError::InconsistentLengths)
);
}
#[test]
fn rejects_catastrophic_equal_generators() {
assert_eq!(
CodeParams::new(7, vec![0o171, 0o171], vec![false, false]),
Err(ParamError::Catastrophic)
);
}
#[test]
fn accepts_ccsds_generators() {
assert!(CodeParams::new(7, vec![0o171, 0o133], vec![false, true]).is_ok());
}
#[test]
fn ccsds_preset_equals_validated_new() {
assert_eq!(
CodeParams::ccsds_r1_2(),
CodeParams::new(7, vec![0o171, 0o133], vec![false, true]).unwrap()
);
}
#[test]
fn rejects_catastrophic_shared_factor() {
assert_eq!(
CodeParams::new(3, vec![0b011, 0b101], vec![false, false]),
Err(ParamError::Catastrophic)
);
}
#[test]
fn accepts_monomial_gcd_non_catastrophic() {
assert!(CodeParams::new(3, vec![0b110, 0b100], vec![false, false]).is_ok());
}
#[test]
fn catastrophic_check_handles_multistep_gcd() {
assert_eq!(
CodeParams::new(4, vec![0b1001, 0b0101], vec![false, false]),
Err(ParamError::Catastrophic)
);
}
#[test]
fn ccsds_r1_3_preset_is_valid_and_correct() {
let p = CodeParams::ccsds_r1_3();
assert_eq!(p.k, 7);
assert_eq!(p.generators, vec![0o133, 0o171, 0o145]);
assert_eq!(p.invert_outputs, vec![false, false, false]);
assert!(p.validate().is_ok());
}
#[test]
fn code_profile_ccsds_r1_3_resolves_to_ccsds_r1_3_params() {
assert_eq!(CodeProfile::CcsdsR1_3.params(), CodeParams::ccsds_r1_3());
}
#[test]
fn code_profile_ccsds_r1_2_resolves_to_ccsds_r1_2_params() {
assert_eq!(CodeProfile::CcsdsR1_2.params(), CodeParams::ccsds_r1_2());
}
#[test]
fn n_reports_generator_count() {
assert_eq!(CodeParams::ccsds_r1_2().n(), 2);
assert_eq!(CodeParams::ccsds_r1_3().n(), 3);
}
#[test]
fn every_code_profile_params_is_valid() {
for p in [
CodeProfile::CcsdsR1_2,
CodeProfile::CcsdsR1_3,
CodeProfile::K9R1_2,
] {
assert!(
p.params().validate().is_ok(),
"profile {p:?} must produce valid CodeParams"
);
}
}
}