use crate::raptorq::gf256::{Gf256, gf256_addmul_slice};
use crate::raptorq::proof::{
DecodeConfig, DecodeProof, EliminationTrace, FailureReason, InactivationStrategy, PeelingTrace,
ReceivedSummary,
};
use crate::raptorq::systematic::{ConstraintMatrix, SystematicParams};
use crate::raptorq::{decision_contract, decision_contract::GovernanceSnapshot};
use crate::types::ObjectId;
use std::collections::{BTreeSet, VecDeque};
use std::hash::{Hash, Hasher};
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct ReceivedSymbol {
pub esi: u32,
pub is_source: bool,
pub columns: Vec<usize>,
pub coefficients: Vec<Gf256>,
pub data: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DecodeError {
InsufficientSymbols {
received: usize,
required: usize,
},
SingularMatrix {
row: usize,
},
SymbolSizeMismatch {
expected: usize,
actual: usize,
},
SymbolEquationArityMismatch {
esi: u32,
columns: usize,
coefficients: usize,
},
ColumnIndexOutOfRange {
esi: u32,
column: usize,
max_valid: usize,
},
SourceEsiOutOfRange {
esi: u32,
max_valid: usize,
},
InvalidSourceSymbolEquation {
esi: u32,
expected_column: usize,
},
CorruptDecodedOutput {
esi: u32,
byte_index: usize,
expected: u8,
actual: u8,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DecodeFailureClass {
Recoverable,
Unrecoverable,
}
impl DecodeError {
#[must_use]
#[inline]
pub const fn failure_class(&self) -> DecodeFailureClass {
match self {
Self::InsufficientSymbols { .. } | Self::SingularMatrix { .. } => {
DecodeFailureClass::Recoverable
}
Self::SymbolSizeMismatch { .. }
| Self::SymbolEquationArityMismatch { .. }
| Self::ColumnIndexOutOfRange { .. }
| Self::SourceEsiOutOfRange { .. }
| Self::InvalidSourceSymbolEquation { .. }
| Self::CorruptDecodedOutput { .. } => DecodeFailureClass::Unrecoverable,
}
}
#[must_use]
#[inline]
pub const fn is_recoverable(&self) -> bool {
matches!(self.failure_class(), DecodeFailureClass::Recoverable)
}
#[must_use]
#[inline]
pub const fn is_unrecoverable(&self) -> bool {
matches!(self.failure_class(), DecodeFailureClass::Unrecoverable)
}
}
#[derive(Debug, Clone, Default)]
pub struct DecodeStats {
pub peeled: usize,
pub inactivated: usize,
pub gauss_ops: usize,
pub pivots_selected: usize,
pub hard_regime_activated: bool,
pub markowitz_pivots: usize,
pub hard_regime_fallbacks: usize,
pub hard_regime_branch: Option<&'static str>,
pub hard_regime_conservative_fallback_reason: Option<&'static str>,
pub peel_queue_pushes: usize,
pub peel_queue_pops: usize,
pub peel_frontier_peak: usize,
pub dense_core_rows: usize,
pub dense_core_cols: usize,
pub dense_core_dropped_rows: usize,
pub peeling_fallback_reason: Option<&'static str>,
pub policy_mode: Option<&'static str>,
pub policy_reason: Option<&'static str>,
pub policy_replay_ref: Option<&'static str>,
pub governance: Option<decision_contract::GovernanceTelemetry>,
pub policy_density_permille: usize,
pub policy_rank_deficit_permille: usize,
pub policy_inactivation_pressure_permille: usize,
pub policy_overhead_ratio_permille: usize,
pub policy_budget_exhausted: bool,
pub policy_baseline_loss: u32,
pub policy_high_support_loss: u32,
pub policy_block_schur_loss: u32,
pub factor_cache_hits: usize,
pub factor_cache_misses: usize,
pub factor_cache_inserts: usize,
pub factor_cache_evictions: usize,
pub factor_cache_lookup_collisions: usize,
pub factor_cache_last_key: Option<u64>,
pub factor_cache_last_reason: Option<&'static str>,
pub factor_cache_last_reuse_eligible: Option<bool>,
pub factor_cache_entries: usize,
pub factor_cache_capacity: usize,
pub wavefront_active: bool,
pub wavefront_batches: usize,
pub wavefront_overlap_peeled: usize,
pub wavefront_batch_size: usize,
}
#[derive(Debug)]
pub struct DecodeResult {
pub intermediate: Vec<Vec<u8>>,
pub source: Vec<Vec<u8>>,
pub stats: DecodeStats,
}
#[derive(Debug)]
pub struct DecodeResultWithProof {
pub result: DecodeResult,
pub proof: DecodeProof,
}
struct DecoderState {
params: SystematicParams,
equations: Vec<Equation>,
rhs: Vec<Vec<u8>>,
solved: Vec<Option<Vec<u8>>>,
active_cols: BTreeSet<usize>,
inactive_cols: BTreeSet<usize>,
stats: DecodeStats,
}
const DENSE_FACTOR_CACHE_CAPACITY: usize = 16;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DenseFactorCacheResult {
Hit,
MissInserted,
MissEvicted,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum DenseFactorCacheLookup {
Hit(Arc<DenseFactorArtifact>),
MissNoEntry,
MissFingerprintCollision,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct DenseFactorArtifact {
dense_cols: Vec<usize>,
col_to_dense: DenseColIndexMap,
}
impl DenseFactorArtifact {
fn new(dense_cols: Vec<usize>) -> Self {
let col_to_dense = build_dense_col_index_map(&dense_cols);
Self {
dense_cols,
col_to_dense,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum DenseColIndexMap {
Direct(Vec<usize>),
SortedPairs(Vec<(usize, usize)>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct DenseFactorSignature {
fingerprint: u64,
unsolved: Vec<usize>,
row_offsets: Vec<usize>,
row_terms_flat: Vec<(usize, u8)>,
}
impl DenseFactorSignature {
fn from_equations(equations: &[Equation], dense_rows: &[usize], unsolved: &[usize]) -> Self {
let mut row_offsets = Vec::with_capacity(dense_rows.len());
let row_terms_capacity = dense_rows
.iter()
.map(|&eq_idx| equations[eq_idx].terms.len())
.sum();
let mut row_terms_flat = Vec::with_capacity(row_terms_capacity);
for &eq_idx in dense_rows {
let mut unsolved_cursor = 0usize;
for &(col, coef) in &equations[eq_idx].terms {
if coef.is_zero() {
continue;
}
while unsolved_cursor < unsolved.len() && unsolved[unsolved_cursor] < col {
unsolved_cursor += 1;
}
if unsolved_cursor >= unsolved.len() {
break;
}
if unsolved[unsolved_cursor] == col {
row_terms_flat.push((col, coef.raw()));
}
}
row_offsets.push(row_terms_flat.len());
}
let mut hasher = crate::util::DetHasher::default();
unsolved.hash(&mut hasher);
row_offsets.hash(&mut hasher);
row_terms_flat.hash(&mut hasher);
let fingerprint = hasher.finish();
Self {
fingerprint,
unsolved: unsolved.to_vec(),
row_offsets,
row_terms_flat,
}
}
}
#[derive(Debug, Clone)]
struct DenseFactorCacheEntry {
signature: DenseFactorSignature,
artifact: Arc<DenseFactorArtifact>,
}
#[derive(Debug, Default)]
struct DenseFactorCache {
entries: VecDeque<DenseFactorCacheEntry>,
}
impl DenseFactorCache {
fn lookup(&self, signature: &DenseFactorSignature) -> DenseFactorCacheLookup {
let mut saw_fingerprint_collision = false;
for entry in &self.entries {
if entry.signature.fingerprint != signature.fingerprint {
continue;
}
if entry.signature == *signature {
return DenseFactorCacheLookup::Hit(entry.artifact.clone());
}
saw_fingerprint_collision = true;
}
if saw_fingerprint_collision {
DenseFactorCacheLookup::MissFingerprintCollision
} else {
DenseFactorCacheLookup::MissNoEntry
}
}
fn insert(
&mut self,
signature: DenseFactorSignature,
artifact: Arc<DenseFactorArtifact>,
) -> DenseFactorCacheResult {
if let Some(existing) = self
.entries
.iter_mut()
.find(|entry| entry.signature == signature)
{
existing.artifact = artifact;
return DenseFactorCacheResult::MissInserted;
}
let result = if self.entries.len() >= DENSE_FACTOR_CACHE_CAPACITY {
let _ = self.entries.pop_front();
DenseFactorCacheResult::MissEvicted
} else {
DenseFactorCacheResult::MissInserted
};
self.entries.push_back(DenseFactorCacheEntry {
signature,
artifact,
});
result
}
#[inline]
fn len(&self) -> usize {
self.entries.len()
}
}
#[derive(Debug, Clone)]
struct Equation {
terms: Vec<(usize, Gf256)>,
used: bool,
}
impl Equation {
fn new(columns: Vec<usize>, coefficients: Vec<Gf256>) -> Self {
let mut terms: Vec<_> = columns.into_iter().zip(coefficients).collect();
terms.sort_by_key(|(col, _)| *col);
let mut merged = Vec::with_capacity(terms.len());
for (col, coef) in terms {
if let Some((last_col, last_coef)) = merged.last_mut() {
if *last_col == col {
*last_coef += coef;
continue;
}
}
merged.push((col, coef));
}
merged.retain(|(_, coef)| !coef.is_zero());
Self {
terms: merged,
used: false,
}
}
#[inline]
fn degree(&self) -> usize {
self.terms.len()
}
#[inline]
fn coef(&self, col: usize) -> Gf256 {
self.terms
.binary_search_by_key(&col, |(c, _)| *c)
.map_or(Gf256::ZERO, |idx| self.terms[idx].1)
}
}
#[inline]
fn original_col_for_dense(unsolved: &[usize], dense_col: usize) -> usize {
unsolved.get(dense_col).copied().unwrap_or(dense_col)
}
#[inline]
fn singular_matrix_error(unsolved: &[usize], dense_col: usize) -> DecodeError {
DecodeError::SingularMatrix {
row: original_col_for_dense(unsolved, dense_col),
}
}
#[inline]
fn inconsistent_matrix_error(unused_eqs: &[usize], dense_row: usize) -> DecodeError {
DecodeError::SingularMatrix {
row: unused_eqs.get(dense_row).copied().unwrap_or(dense_row),
}
}
fn first_inconsistent_dense_row(
a: &[Gf256],
n_rows: usize,
n_cols: usize,
b: &[Vec<u8>],
) -> Option<usize> {
(0..n_rows).find(|&row| {
let row_off = row * n_cols;
a[row_off..row_off + n_cols]
.iter()
.all(|coef| coef.is_zero())
&& b[row].iter().any(|&byte| byte != 0)
})
}
#[inline]
fn active_degree_one_col(state: &DecoderState, eq: &Equation) -> Option<usize> {
if eq.used || eq.degree() != 1 {
return None;
}
let col = eq.terms[0].0;
if state.active_cols.contains(&col) && state.solved[col].is_none() {
Some(col)
} else {
None
}
}
fn build_dense_core_rows(
state: &DecoderState,
unused_eqs: &[usize],
unsolved: &[usize],
) -> Result<(Vec<usize>, usize), DecodeError> {
let mut unsolved_mask = vec![false; state.params.l];
for &col in unsolved {
unsolved_mask[col] = true;
}
let mut dense_rows = Vec::with_capacity(unused_eqs.len());
let mut dropped_zero_rows = 0usize;
for &eq_idx in unused_eqs {
let has_unsolved_term = state.equations[eq_idx]
.terms
.iter()
.any(|(col, coef)| unsolved_mask[*col] && !coef.is_zero());
if has_unsolved_term {
dense_rows.push(eq_idx);
continue;
}
if state.rhs[eq_idx].iter().any(|&byte| byte != 0) {
return Err(DecodeError::SingularMatrix { row: eq_idx });
}
dropped_zero_rows += 1;
}
Ok((dense_rows, dropped_zero_rows))
}
fn validate_dense_core_rhs_widths(
state: &DecoderState,
dense_rows: &[usize],
symbol_size: usize,
) -> Result<(), DecodeError> {
for &eq_idx in dense_rows {
let actual = state.rhs[eq_idx].len();
if actual != symbol_size {
return Err(DecodeError::SymbolSizeMismatch {
expected: symbol_size,
actual,
});
}
}
Ok(())
}
const DENSE_COL_ABSENT: usize = usize::MAX;
const DENSE_COL_DIRECT_MAP_RANGE_RATIO: usize = 8;
#[inline]
fn build_dense_col_index_map(unsolved: &[usize]) -> DenseColIndexMap {
let Some(max_col) = unsolved.iter().copied().max() else {
return DenseColIndexMap::Direct(Vec::new());
};
let direct_map_max_col = unsolved
.len()
.saturating_mul(DENSE_COL_DIRECT_MAP_RANGE_RATIO);
if max_col <= direct_map_max_col {
let mut col_to_dense = vec![DENSE_COL_ABSENT; max_col.saturating_add(1)];
for (dense_col, &col) in unsolved.iter().enumerate() {
col_to_dense[col] = dense_col;
}
DenseColIndexMap::Direct(col_to_dense)
} else {
let mut pairs: Vec<(usize, usize)> = unsolved
.iter()
.copied()
.enumerate()
.map(|(dense_col, col)| (col, dense_col))
.collect();
pairs.sort_by_key(|(col, _)| *col);
DenseColIndexMap::SortedPairs(pairs)
}
}
#[inline]
fn dense_col_index_from_direct(map: &[usize], col: usize) -> Option<usize> {
let dense_col = *map.get(col)?;
if dense_col == DENSE_COL_ABSENT {
return None;
}
Some(dense_col)
}
#[inline]
fn dense_col_index_from_sorted_pairs(pairs: &[(usize, usize)], col: usize) -> Option<usize> {
let idx = pairs
.binary_search_by_key(&col, |(candidate_col, _)| *candidate_col)
.ok()?;
Some(pairs[idx].1)
}
#[inline]
fn dense_col_index(col_to_dense: &DenseColIndexMap, col: usize) -> Option<usize> {
match col_to_dense {
DenseColIndexMap::Direct(map) => dense_col_index_from_direct(map, col),
DenseColIndexMap::SortedPairs(pairs) => dense_col_index_from_sorted_pairs(pairs, col),
}
}
fn sparse_first_dense_columns(
equations: &[Equation],
dense_rows: &[usize],
unsolved: &[usize],
) -> Vec<usize> {
if unsolved.len() < 2 {
return unsolved.to_vec();
}
let mut support = vec![0usize; unsolved.len()];
if unsolved.windows(2).all(|w| w[0] <= w[1]) {
for &eq_idx in dense_rows {
let mut unsolved_cursor = 0usize;
for &(col, coef) in &equations[eq_idx].terms {
if coef.is_zero() {
continue;
}
while unsolved_cursor < unsolved.len() && unsolved[unsolved_cursor] < col {
unsolved_cursor += 1;
}
if unsolved_cursor >= unsolved.len() {
break;
}
if unsolved[unsolved_cursor] == col {
support[unsolved_cursor] += 1;
}
}
}
} else {
let col_to_dense = build_dense_col_index_map(unsolved);
for &eq_idx in dense_rows {
for &(col, coef) in &equations[eq_idx].terms {
if coef.is_zero() {
continue;
}
if let Some(dense_col) = dense_col_index(&col_to_dense, col) {
support[dense_col] += 1;
}
}
}
}
let mut ordered: Vec<(usize, usize)> = unsolved
.iter()
.copied()
.enumerate()
.map(|(dense_col, col)| (col, support[dense_col]))
.collect();
ordered.sort_by(|(col_a, support_a), (col_b, support_b)| {
support_a.cmp(support_b).then_with(|| col_a.cmp(col_b))
});
ordered.into_iter().map(|(col, _)| col).collect()
}
fn failure_reason_with_trace(err: &DecodeError, elimination: &EliminationTrace) -> FailureReason {
match err {
DecodeError::SingularMatrix { row } => FailureReason::SingularMatrix {
row: *row,
attempted_cols: elimination.pivot_events.iter().map(|ev| ev.col).collect(),
},
_ => FailureReason::from(err),
}
}
const HARD_REGIME_MIN_COLS: usize = 8;
const HARD_REGIME_DENSITY_PERCENT: usize = 35;
const HARD_REGIME_NEAR_SQUARE_EXTRA_ROWS: usize = 2;
const BLOCK_SCHUR_MIN_COLS: usize = 12;
const BLOCK_SCHUR_MIN_DENSITY_PERCENT: usize = 45;
const BLOCK_SCHUR_TRAILING_COLS: usize = 4;
const HYBRID_SPARSE_COST_NUMERATOR: usize = 3;
const HYBRID_SPARSE_COST_DENOMINATOR: usize = 5;
const SMALL_ROW_DENSE_FASTPATH_COLS: usize = 4;
const POLICY_FEATURE_BUDGET_CELLS: usize = 4096;
const POLICY_REPLAY_REF: &str = "replay:rq-track-f-runtime-policy-v1";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct DecoderPolicyFeatures {
density_permille: usize,
rank_deficit_permille: usize,
inactivation_pressure_permille: usize,
overhead_ratio_permille: usize,
budget_exhausted: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DecoderPolicyMode {
ConservativeBaseline,
HighSupportFirst,
BlockSchurLowRank,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct DecoderPolicyDecision {
mode: DecoderPolicyMode,
features: DecoderPolicyFeatures,
baseline_loss: u32,
high_support_loss: u32,
block_schur_loss: u32,
reason: &'static str,
governance: Option<decision_contract::GovernanceTelemetry>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum HardRegimePlan {
Markowitz,
BlockSchurLowRank { split_col: usize },
}
impl HardRegimePlan {
const fn label(self) -> &'static str {
match self {
Self::Markowitz => "markowitz",
Self::BlockSchurLowRank { .. } => "block_schur_low_rank",
}
}
const fn strategy(self) -> InactivationStrategy {
match self {
Self::Markowitz => InactivationStrategy::HighSupportFirst,
Self::BlockSchurLowRank { .. } => InactivationStrategy::BlockSchurLowRank,
}
}
}
fn matrix_nonzero_count(a: &[Gf256]) -> usize {
a.iter().filter(|coef| !coef.is_zero()).count()
}
fn clamp_usize_to_u32(value: usize) -> u32 {
u32::try_from(value).unwrap_or(u32::MAX)
}
fn compute_decoder_policy_features(
n_rows: usize,
n_cols: usize,
dense_nonzeros: usize,
unsupported_cols: usize,
inactivation_pressure_permille: usize,
) -> DecoderPolicyFeatures {
if n_rows == 0 || n_cols == 0 {
return DecoderPolicyFeatures {
density_permille: 0,
rank_deficit_permille: 0,
inactivation_pressure_permille,
overhead_ratio_permille: 0,
budget_exhausted: false,
};
}
let total_cells = n_rows.saturating_mul(n_cols);
let density_permille = dense_nonzeros.saturating_mul(1000) / total_cells.max(1);
let rank_deficit_permille = unsupported_cols.saturating_mul(1000) / n_cols;
let overhead_ratio_permille = n_rows.saturating_sub(n_cols).saturating_mul(1000) / n_cols;
DecoderPolicyFeatures {
density_permille,
rank_deficit_permille,
inactivation_pressure_permille,
overhead_ratio_permille,
budget_exhausted: total_cells > POLICY_FEATURE_BUDGET_CELLS,
}
}
fn policy_losses(features: DecoderPolicyFeatures, n_cols: usize) -> (u32, u32, u32) {
let density = clamp_usize_to_u32(features.density_permille);
let rank_deficit = clamp_usize_to_u32(features.rank_deficit_permille);
let inactivation_pressure = clamp_usize_to_u32(features.inactivation_pressure_permille);
let overhead = clamp_usize_to_u32(features.overhead_ratio_permille);
let baseline_loss = 400u32
.saturating_add(density.saturating_mul(3))
.saturating_add(rank_deficit.saturating_mul(4))
.saturating_add(inactivation_pressure.saturating_mul(2))
.saturating_add(overhead);
let high_support_loss = 700u32
.saturating_add(density)
.saturating_add(rank_deficit.saturating_mul(3))
.saturating_add(inactivation_pressure)
.saturating_add(overhead / 2);
let block_schur_loss = if n_cols < BLOCK_SCHUR_MIN_COLS {
u32::MAX
} else {
750u32
.saturating_add(density / 2)
.saturating_add(rank_deficit.saturating_mul(2))
.saturating_add(inactivation_pressure)
.saturating_add(overhead / 3)
};
(baseline_loss, high_support_loss, block_schur_loss)
}
#[cfg(test)]
thread_local! {
static TEST_BYPASS_GOVERNANCE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
#[cfg(test)]
fn set_test_bypass_governance(bypass: bool) {
TEST_BYPASS_GOVERNANCE.with(|cell| cell.set(bypass));
}
fn bypass_governance() -> bool {
#[cfg(test)]
{
TEST_BYPASS_GOVERNANCE.with(std::cell::Cell::get)
}
#[cfg(not(test))]
{
false
}
}
fn choose_runtime_decoder_policy(
n_rows: usize,
n_cols: usize,
dense_nonzeros: usize,
unsupported_cols: usize,
inactivation_pressure_permille: usize,
) -> DecoderPolicyDecision {
let features = compute_decoder_policy_features(
n_rows,
n_cols,
dense_nonzeros,
unsupported_cols,
inactivation_pressure_permille,
);
let mut decision = choose_low_level_decoder_policy(features, n_rows, n_cols);
let governance = decision_contract::evaluate_governance(&GovernanceSnapshot {
n_rows,
n_cols,
density_permille: features.density_permille,
rank_deficit_permille: features.rank_deficit_permille,
inactivation_pressure_permille: features.inactivation_pressure_permille,
overhead_ratio_permille: features.overhead_ratio_permille,
budget_exhausted: features.budget_exhausted,
baseline_loss: decision.baseline_loss,
high_support_loss: decision.high_support_loss,
block_schur_loss: decision.block_schur_loss,
});
if !bypass_governance() {
match governance.chosen_action {
"canary_hold" if matches!(decision.mode, DecoderPolicyMode::BlockSchurLowRank) => {
decision.mode = DecoderPolicyMode::HighSupportFirst;
decision.reason = "g7_expected_loss_canary_hold";
}
"rollback" if !matches!(decision.mode, DecoderPolicyMode::ConservativeBaseline) => {
decision.mode = DecoderPolicyMode::ConservativeBaseline;
decision.reason = "g7_expected_loss_rollback";
}
"fallback" if !matches!(decision.mode, DecoderPolicyMode::ConservativeBaseline) => {
decision.mode = DecoderPolicyMode::ConservativeBaseline;
decision.reason = "g7_deterministic_fallback_trigger";
}
_ => {}
}
}
decision.governance = Some(governance);
decision
}
fn choose_low_level_decoder_policy(
features: DecoderPolicyFeatures,
n_rows: usize,
n_cols: usize,
) -> DecoderPolicyDecision {
let (baseline_loss, high_support_loss, mut block_schur_loss) = policy_losses(features, n_cols);
if features.budget_exhausted {
return DecoderPolicyDecision {
mode: DecoderPolicyMode::ConservativeBaseline,
features,
baseline_loss,
high_support_loss,
block_schur_loss,
reason: "policy_budget_exhausted_conservative",
governance: None,
};
}
let hard_gate = n_cols >= HARD_REGIME_MIN_COLS
&& (features.density_permille >= HARD_REGIME_DENSITY_PERCENT.saturating_mul(10)
|| n_rows <= n_cols.saturating_add(HARD_REGIME_NEAR_SQUARE_EXTRA_ROWS));
if !hard_gate {
return DecoderPolicyDecision {
mode: DecoderPolicyMode::ConservativeBaseline,
features,
baseline_loss,
high_support_loss,
block_schur_loss,
reason: "expected_loss_conservative_gate",
governance: None,
};
}
let block_gate = n_cols >= BLOCK_SCHUR_MIN_COLS
&& features.density_permille >= BLOCK_SCHUR_MIN_DENSITY_PERCENT.saturating_mul(10)
&& n_cols > BLOCK_SCHUR_TRAILING_COLS;
if !block_gate {
block_schur_loss = u32::MAX;
}
let mode = if block_schur_loss < high_support_loss {
DecoderPolicyMode::BlockSchurLowRank
} else {
DecoderPolicyMode::HighSupportFirst
};
DecoderPolicyDecision {
mode,
features,
baseline_loss,
high_support_loss,
block_schur_loss,
reason: "expected_loss_minimum",
governance: None,
}
}
const fn decoder_policy_mode_label(mode: DecoderPolicyMode) -> &'static str {
match mode {
DecoderPolicyMode::ConservativeBaseline => "conservative_baseline",
DecoderPolicyMode::HighSupportFirst => "high_support_first",
DecoderPolicyMode::BlockSchurLowRank => "block_schur_low_rank",
}
}
fn apply_policy_decision_to_stats(stats: &mut DecodeStats, decision: DecoderPolicyDecision) {
stats.policy_mode = Some(decoder_policy_mode_label(decision.mode));
stats.policy_reason = Some(decision.reason);
stats.policy_replay_ref = Some(POLICY_REPLAY_REF);
stats.governance = decision.governance;
stats.policy_density_permille = decision.features.density_permille;
stats.policy_rank_deficit_permille = decision.features.rank_deficit_permille;
stats.policy_inactivation_pressure_permille = decision.features.inactivation_pressure_permille;
stats.policy_overhead_ratio_permille = decision.features.overhead_ratio_permille;
stats.policy_budget_exhausted = decision.features.budget_exhausted;
stats.policy_baseline_loss = decision.baseline_loss;
stats.policy_high_support_loss = decision.high_support_loss;
stats.policy_block_schur_loss = decision.block_schur_loss;
}
#[derive(Debug, Clone, Copy)]
struct DenseFactorCacheObservation {
key: u64,
result: DenseFactorCacheResult,
reason: &'static str,
reuse_eligible: bool,
fingerprint_collision: bool,
cache_entries: usize,
cache_capacity: usize,
}
fn apply_dense_factor_cache_observation(
stats: &mut DecodeStats,
observation: DenseFactorCacheObservation,
) {
stats.factor_cache_last_key = Some(observation.key);
stats.factor_cache_last_reason = Some(observation.reason);
stats.factor_cache_last_reuse_eligible = Some(observation.reuse_eligible);
stats.factor_cache_entries = observation.cache_entries;
stats.factor_cache_capacity = observation.cache_capacity;
if observation.fingerprint_collision {
stats.factor_cache_lookup_collisions += 1;
}
match observation.result {
DenseFactorCacheResult::Hit => {
stats.factor_cache_hits += 1;
}
DenseFactorCacheResult::MissInserted => {
stats.factor_cache_misses += 1;
stats.factor_cache_inserts += 1;
}
DenseFactorCacheResult::MissEvicted => {
stats.factor_cache_misses += 1;
stats.factor_cache_inserts += 1;
stats.factor_cache_evictions += 1;
}
}
}
fn row_nonzero_count(a: &[Gf256], n_cols: usize, row: usize) -> usize {
let row_off = row * n_cols;
a[row_off..row_off + n_cols]
.iter()
.filter(|coef| !coef.is_zero())
.count()
}
fn sparse_update_columns_if_beneficial(pivot_row: &[Gf256], n_cols: usize) -> Option<Vec<usize>> {
if n_cols == 0 {
return None;
}
let threshold =
n_cols.saturating_mul(HYBRID_SPARSE_COST_NUMERATOR) / HYBRID_SPARSE_COST_DENOMINATOR;
if n_cols <= SMALL_ROW_DENSE_FASTPATH_COLS {
let mut sparse_nnz = 0usize;
for coef in pivot_row.iter().take(n_cols) {
if coef.is_zero() {
continue;
}
sparse_nnz += 1;
if sparse_nnz > threshold {
return None;
}
}
let mut cols = Vec::with_capacity(sparse_nnz.max(1));
for (idx, coef) in pivot_row.iter().take(n_cols).enumerate() {
if !coef.is_zero() {
cols.push(idx);
}
}
return Some(cols);
}
let mut seen = 0usize;
let mut cols = Vec::with_capacity((threshold + 1).min(n_cols).max(1));
for (idx, coef) in pivot_row.iter().take(n_cols).enumerate() {
if coef.is_zero() {
continue;
}
seen += 1;
if seen > threshold {
return None;
}
cols.push(idx);
}
Some(cols)
}
fn select_hard_regime_plan(n_rows: usize, n_cols: usize, a: &[Gf256]) -> HardRegimePlan {
let total_cells = n_rows.saturating_mul(n_cols);
if n_cols < BLOCK_SCHUR_MIN_COLS || total_cells == 0 {
return HardRegimePlan::Markowitz;
}
let nonzeros = matrix_nonzero_count(a);
let dense_enough =
nonzeros.saturating_mul(100) >= total_cells.saturating_mul(BLOCK_SCHUR_MIN_DENSITY_PERCENT);
if !dense_enough || n_cols <= BLOCK_SCHUR_TRAILING_COLS {
return HardRegimePlan::Markowitz;
}
let split_col = n_cols - BLOCK_SCHUR_TRAILING_COLS;
HardRegimePlan::BlockSchurLowRank { split_col }
}
fn row_cross_block_nnz(
a: &[Gf256],
n_cols: usize,
row: usize,
split_col: usize,
col: usize,
) -> usize {
let row_off = row * n_cols;
let row_slice = &a[row_off..row_off + n_cols];
if col < split_col {
row_slice[split_col..]
.iter()
.filter(|coef| !coef.is_zero())
.count()
} else {
row_slice[..split_col]
.iter()
.filter(|coef| !coef.is_zero())
.count()
}
}
fn select_pivot_row(
a: &[Gf256],
n_rows: usize,
n_cols: usize,
col: usize,
row_used: &[bool],
hard_regime: bool,
hard_plan: HardRegimePlan,
) -> Option<usize> {
if !hard_regime {
return (0..n_rows).find(|&row| !row_used[row] && !a[row * n_cols + col].is_zero());
}
let mut best: Option<(usize, usize, usize)> = None;
for row in 0..n_rows {
if row_used[row] || a[row * n_cols + col].is_zero() {
continue;
}
let cross_block_nnz = match hard_plan {
HardRegimePlan::Markowitz => 0,
HardRegimePlan::BlockSchurLowRank { split_col } => {
row_cross_block_nnz(a, n_cols, row, split_col, col)
}
};
let nnz = row_nonzero_count(a, n_cols, row);
match best {
None => best = Some((row, cross_block_nnz, nnz)),
Some((_best_row, best_cross, _best_nnz)) if cross_block_nnz < best_cross => {
best = Some((row, cross_block_nnz, nnz));
}
Some((_best_row, best_cross, best_nnz))
if cross_block_nnz == best_cross && nnz < best_nnz =>
{
best = Some((row, cross_block_nnz, nnz));
}
Some((best_row, best_cross, best_nnz))
if cross_block_nnz == best_cross && nnz == best_nnz && row < best_row =>
{
best = Some((row, cross_block_nnz, nnz));
}
_ => {}
}
}
best.map(|(row, _, _)| row)
}
pub struct InactivationDecoder {
params: SystematicParams,
seed: u64,
dense_factor_cache: parking_lot::Mutex<DenseFactorCache>,
}
impl InactivationDecoder {
#[must_use]
pub fn new(k: usize, symbol_size: usize, seed: u64) -> Self {
let params = SystematicParams::for_source_block(k, symbol_size);
Self {
params,
seed,
dense_factor_cache: parking_lot::Mutex::new(DenseFactorCache::default()),
}
}
#[must_use]
#[inline]
pub const fn params(&self) -> &SystematicParams {
&self.params
}
#[inline]
const fn implicit_padding_rows(&self) -> usize {
self.params.k_prime.saturating_sub(self.params.k)
}
#[inline]
const fn minimum_received_symbols(&self) -> usize {
self.params.l.saturating_sub(self.implicit_padding_rows())
}
fn validate_input(&self, symbols: &[ReceivedSymbol]) -> Result<(), DecodeError> {
let k = self.params.k;
let l = self.params.l;
let symbol_size = self.params.symbol_size;
let required = self.minimum_received_symbols();
if symbols.len() < required {
return Err(DecodeError::InsufficientSymbols {
received: symbols.len(),
required,
});
}
for sym in symbols {
if sym.data.len() != symbol_size {
return Err(DecodeError::SymbolSizeMismatch {
expected: symbol_size,
actual: sym.data.len(),
});
}
if sym.columns.len() != sym.coefficients.len() {
return Err(DecodeError::SymbolEquationArityMismatch {
esi: sym.esi,
columns: sym.columns.len(),
coefficients: sym.coefficients.len(),
});
}
for &column in &sym.columns {
if column >= l {
return Err(DecodeError::ColumnIndexOutOfRange {
esi: sym.esi,
column,
max_valid: l,
});
}
}
if sym.is_source {
let esi = sym.esi as usize;
if esi >= k {
return Err(DecodeError::SourceEsiOutOfRange {
esi: sym.esi,
max_valid: k,
});
}
if sym.columns.len() != 1
|| sym.coefficients.len() != 1
|| sym.columns[0] != esi
|| sym.coefficients[0] != Gf256::ONE
{
return Err(DecodeError::InvalidSourceSymbolEquation {
esi: sym.esi,
expected_column: esi,
});
}
}
}
Ok(())
}
fn verify_decoded_output(
&self,
symbols: &[ReceivedSymbol],
intermediate: &[Vec<u8>],
) -> Result<(), DecodeError> {
let symbol_size = self.params.symbol_size;
let mut reconstructed = vec![0u8; symbol_size];
for sym in symbols {
if sym.is_source
&& sym.columns.len() == 1
&& sym.coefficients.len() == 1
&& (sym.esi as usize) < self.params.k
&& sym.columns[0] == sym.esi as usize
&& sym.coefficients[0] == Gf256::ONE
{
let source_col = sym.columns[0];
let expected = &intermediate[source_col];
if let Some(byte_index) = first_mismatch_byte(expected, &sym.data) {
return Err(DecodeError::CorruptDecodedOutput {
esi: sym.esi,
byte_index,
expected: expected[byte_index],
actual: sym.data[byte_index],
});
}
continue;
}
reconstructed.fill(0);
for (&column, &coefficient) in sym.columns.iter().zip(sym.coefficients.iter()) {
if coefficient.is_zero() {
continue;
}
gf256_addmul_slice(&mut reconstructed, &intermediate[column], coefficient);
}
if let Some(byte_index) = first_mismatch_byte(&reconstructed, &sym.data) {
return Err(DecodeError::CorruptDecodedOutput {
esi: sym.esi,
byte_index,
expected: reconstructed[byte_index],
actual: sym.data[byte_index],
});
}
}
Ok(())
}
pub fn decode(&self, symbols: &[ReceivedSymbol]) -> Result<DecodeResult, DecodeError> {
let k = self.params.k;
let symbol_size = self.params.symbol_size;
self.validate_input(symbols)?;
let mut state = self.build_state(symbols);
Self::peel(&mut state);
self.inactivate_and_solve(&mut state)?;
let intermediate: Vec<Vec<u8>> = state
.solved
.into_iter()
.map(|opt| opt.unwrap_or_else(|| vec![0u8; symbol_size]))
.collect();
self.verify_decoded_output(symbols, &intermediate)?;
let source: Vec<Vec<u8>> = intermediate[..k].to_vec();
Ok(DecodeResult {
intermediate,
source,
stats: state.stats,
})
}
pub fn decode_wavefront(
&self,
symbols: &[ReceivedSymbol],
batch_size: usize,
) -> Result<DecodeResult, DecodeError> {
let k = self.params.k;
let symbol_size = self.params.symbol_size;
self.validate_input(symbols)?;
let effective_batch = if batch_size == 0 {
symbols.len()
} else {
batch_size
};
let mut state = self.build_state(&[]);
state.equations.reserve(symbols.len());
state.rhs.reserve(symbols.len());
state.stats.wavefront_active = true;
state.stats.wavefront_batch_size = effective_batch;
let mut total_overlap_peeled = 0usize;
let mut batch_count = 0usize;
let mut queue = VecDeque::new();
let mut queued = vec![false; state.equations.len()];
for (idx, queued_flag) in queued.iter_mut().enumerate() {
if !*queued_flag && active_degree_one_col(&state, &state.equations[idx]).is_some() {
queue.push_back(idx);
*queued_flag = true;
state.stats.peel_queue_pushes += 1;
}
}
state.stats.peel_frontier_peak = state.stats.peel_frontier_peak.max(queue.len());
Self::peel_from_queue(&mut state, &mut queue, &mut queued);
for chunk in symbols.chunks(effective_batch) {
let base_eq_idx = state.equations.len();
for sym in chunk {
let eq = Equation::new(sym.columns.clone(), sym.coefficients.clone());
state.equations.push(eq);
state.rhs.push(sym.data.clone());
}
queued.resize(state.equations.len(), false);
for idx in base_eq_idx..state.equations.len() {
let eq = &state.equations[idx];
let solved_terms: Vec<(usize, Gf256)> = eq
.terms
.iter()
.filter(|(col, coef)| !coef.is_zero() && state.solved[*col].is_some())
.copied()
.collect();
for (col, eq_coef) in &solved_terms {
let solution = state.solved[*col].as_ref().expect("solution must exist");
gf256_addmul_slice(&mut state.rhs[idx], solution, *eq_coef);
if let Ok(pos) = state.equations[idx]
.terms
.binary_search_by_key(col, |(c, _)| *c)
{
state.equations[idx].terms.remove(pos);
}
}
}
for (idx, queued_flag) in queued.iter_mut().enumerate().skip(base_eq_idx) {
if !*queued_flag && active_degree_one_col(&state, &state.equations[idx]).is_some() {
queue.push_back(idx);
*queued_flag = true;
state.stats.peel_queue_pushes += 1;
}
}
state.stats.peel_frontier_peak = state.stats.peel_frontier_peak.max(queue.len());
let peeled_before = state.stats.peeled;
Self::peel_from_queue(&mut state, &mut queue, &mut queued);
let peeled_this_batch = state.stats.peeled - peeled_before;
if batch_count > 0 {
total_overlap_peeled += peeled_this_batch;
}
batch_count += 1;
}
state.stats.wavefront_batches = batch_count;
state.stats.wavefront_overlap_peeled = total_overlap_peeled;
self.inactivate_and_solve(&mut state)?;
let intermediate: Vec<Vec<u8>> = state
.solved
.into_iter()
.map(|opt| opt.unwrap_or_else(|| vec![0u8; symbol_size]))
.collect();
self.verify_decoded_output(symbols, &intermediate)?;
let source: Vec<Vec<u8>> = intermediate[..k].to_vec();
Ok(DecodeResult {
intermediate,
source,
stats: state.stats,
})
}
fn peel_from_queue(state: &mut DecoderState, queue: &mut VecDeque<usize>, queued: &mut [bool]) {
while let Some(eq_idx) = queue.pop_front() {
state.stats.peel_queue_pops += 1;
queued[eq_idx] = false;
let Some(col) = active_degree_one_col(state, &state.equations[eq_idx]) else {
continue;
};
let (_col, coef) = state.equations[eq_idx].terms[0];
state.equations[eq_idx].used = true;
let mut solution = std::mem::take(&mut state.rhs[eq_idx]);
if coef != Gf256::ONE {
let inv = coef.inv();
crate::raptorq::gf256::gf256_mul_slice(&mut solution, inv);
}
state.active_cols.remove(&col);
state.stats.peeled += 1;
let active_cols = &state.active_cols;
let solved = &state.solved;
for (i, (eq, rhs)) in state
.equations
.iter_mut()
.zip(state.rhs.iter_mut())
.enumerate()
{
if eq.used {
continue;
}
let eq_coef = eq.coef(col);
if eq_coef.is_zero() {
continue;
}
gf256_addmul_slice(rhs, &solution, eq_coef);
if let Ok(pos) = eq.terms.binary_search_by_key(&col, |(c, _)| *c) {
eq.terms.remove(pos);
}
if !queued[i] && eq.degree() == 1 {
let next_col = eq.terms[0].0;
if active_cols.contains(&next_col) && solved[next_col].is_none() {
queue.push_back(i);
queued[i] = true;
state.stats.peel_queue_pushes += 1;
}
}
}
state.stats.peel_frontier_peak = state.stats.peel_frontier_peak.max(queue.len());
state.solved[col] = Some(solution);
}
}
#[allow(clippy::result_large_err)]
pub fn decode_with_proof(
&self,
symbols: &[ReceivedSymbol],
object_id: ObjectId,
sbn: u8,
) -> Result<DecodeResultWithProof, (DecodeError, DecodeProof)> {
let k = self.params.k;
let symbol_size = self.params.symbol_size;
let config = DecodeConfig {
object_id,
sbn,
k,
s: self.params.s,
h: self.params.h,
l: self.params.l,
symbol_size,
seed: self.seed,
};
let mut proof_builder = DecodeProof::builder(config);
let received = ReceivedSummary::from_received(symbols.iter().map(|s| (s.esi, s.is_source)));
proof_builder.set_received(received);
if let Err(err) = self.validate_input(symbols) {
proof_builder.set_failure(FailureReason::from(&err));
return Err((err, proof_builder.build()));
}
let mut state = self.build_state(symbols);
Self::peel_with_proof(&mut state, proof_builder.peeling_mut());
if let Err(err) =
self.inactivate_and_solve_with_proof(&mut state, proof_builder.elimination_mut())
{
let reason = failure_reason_with_trace(&err, proof_builder.elimination_mut());
proof_builder.set_failure(reason);
return Err((err, proof_builder.build()));
}
let intermediate: Vec<Vec<u8>> = state
.solved
.into_iter()
.map(|opt| opt.unwrap_or_else(|| vec![0u8; symbol_size]))
.collect();
if let Err(err) = self.verify_decoded_output(symbols, &intermediate) {
proof_builder.set_failure(FailureReason::from(&err));
return Err((err, proof_builder.build()));
}
let source: Vec<Vec<u8>> = intermediate[..k].to_vec();
proof_builder.set_success(&source);
Ok(DecodeResultWithProof {
result: DecodeResult {
intermediate,
source,
stats: state.stats,
},
proof: proof_builder.build(),
})
}
fn build_state(&self, symbols: &[ReceivedSymbol]) -> DecoderState {
let l = self.params.l;
let symbol_size = self.params.symbol_size;
let mut equations = Vec::with_capacity(symbols.len() + self.implicit_padding_rows());
let mut rhs = Vec::with_capacity(symbols.len() + self.implicit_padding_rows());
for sym in symbols {
let eq = Equation::new(sym.columns.clone(), sym.coefficients.clone());
equations.push(eq);
rhs.push(sym.data.clone());
}
for column in self.params.k..self.params.k_prime {
equations.push(Equation::new(vec![column], vec![Gf256::ONE]));
rhs.push(vec![0u8; symbol_size]);
}
let active_cols: BTreeSet<usize> = (0..l).collect();
DecoderState {
params: self.params.clone(),
equations,
rhs,
solved: vec![None; l],
active_cols,
inactive_cols: BTreeSet::new(),
stats: DecodeStats::default(),
}
}
fn dense_factor_with_cache(
&self,
equations: &[Equation],
dense_rows: &[usize],
unsolved: &[usize],
) -> (Arc<DenseFactorArtifact>, DenseFactorCacheObservation) {
let signature = DenseFactorSignature::from_equations(equations, dense_rows, unsolved);
let cache_key = signature.fingerprint;
let (lookup, cache_entries_at_lookup) = {
let cache = self.dense_factor_cache.lock();
(cache.lookup(&signature), cache.len())
};
if let DenseFactorCacheLookup::Hit(artifact) = lookup {
return (
artifact,
DenseFactorCacheObservation {
key: cache_key,
result: DenseFactorCacheResult::Hit,
reason: "signature_match_reuse",
reuse_eligible: true,
fingerprint_collision: false,
cache_entries: cache_entries_at_lookup,
cache_capacity: DENSE_FACTOR_CACHE_CAPACITY,
},
);
}
let saw_fingerprint_collision =
matches!(lookup, DenseFactorCacheLookup::MissFingerprintCollision);
let artifact = Arc::new(DenseFactorArtifact::new(sparse_first_dense_columns(
equations, dense_rows, unsolved,
)));
let (result, cache_entries) = {
let mut cache = self.dense_factor_cache.lock();
let result = cache.insert(signature, Arc::clone(&artifact));
(result, cache.len())
};
let reason = if saw_fingerprint_collision {
"fingerprint_collision_rebuild"
} else {
match result {
DenseFactorCacheResult::Hit => "signature_match_reuse",
DenseFactorCacheResult::MissInserted => "cache_miss_rebuild",
DenseFactorCacheResult::MissEvicted => "cache_miss_evicted_oldest",
}
};
(
artifact,
DenseFactorCacheObservation {
key: cache_key,
result,
reason,
reuse_eligible: false,
fingerprint_collision: saw_fingerprint_collision,
cache_entries,
cache_capacity: DENSE_FACTOR_CACHE_CAPACITY,
},
)
}
#[must_use]
pub fn constraint_symbols(&self) -> Vec<ReceivedSymbol> {
let s = self.params.s;
let h = self.params.h;
let symbol_size = self.params.symbol_size;
let base_rows = s + h;
let constraints = ConstraintMatrix::build(&self.params, self.seed);
let mut result = Vec::with_capacity(base_rows);
for row in 0..base_rows {
let (columns, coefficients) = Self::constraint_row_equation(&constraints, row);
result.push(ReceivedSymbol {
esi: row as u32,
is_source: false,
columns,
coefficients,
data: vec![0u8; symbol_size],
});
}
result
}
fn constraint_row_equation(
constraints: &ConstraintMatrix,
row: usize,
) -> (Vec<usize>, Vec<Gf256>) {
let mut columns = Vec::new();
let mut coefficients = Vec::new();
for col in 0..constraints.cols {
let coeff = constraints.get(row, col);
if !coeff.is_zero() {
columns.push(col);
coefficients.push(coeff);
}
}
(columns, coefficients)
}
fn peel(state: &mut DecoderState) {
Self::peel_impl(state, |_| {});
}
fn peel_with_proof(state: &mut DecoderState, trace: &mut PeelingTrace) {
Self::peel_impl(state, |col| {
trace.record_solved(col);
});
}
fn peel_impl<F>(state: &mut DecoderState, mut on_solved: F)
where
F: FnMut(usize),
{
let mut queue = VecDeque::new();
let mut queued = vec![false; state.equations.len()];
for (idx, eq) in state.equations.iter().enumerate() {
if active_degree_one_col(state, eq).is_some() {
queue.push_back(idx);
queued[idx] = true;
state.stats.peel_queue_pushes += 1;
}
}
state.stats.peel_frontier_peak = state.stats.peel_frontier_peak.max(queue.len());
while let Some(eq_idx) = queue.pop_front() {
state.stats.peel_queue_pops += 1;
queued[eq_idx] = false;
let Some(col) = active_degree_one_col(state, &state.equations[eq_idx]) else {
continue;
};
let (_col, coef) = state.equations[eq_idx].terms[0];
state.equations[eq_idx].used = true;
let mut solution = std::mem::take(&mut state.rhs[eq_idx]);
if coef != Gf256::ONE {
let inv = coef.inv();
crate::raptorq::gf256::gf256_mul_slice(&mut solution, inv);
}
state.active_cols.remove(&col);
state.stats.peeled += 1;
on_solved(col);
let active_cols = &state.active_cols;
let solved = &state.solved;
for (i, eq) in state.equations.iter_mut().enumerate() {
if eq.used {
continue;
}
let eq_coef = eq.coef(col);
if eq_coef.is_zero() {
continue;
}
gf256_addmul_slice(&mut state.rhs[i], &solution, eq_coef);
if let Ok(pos) = eq.terms.binary_search_by_key(&col, |(c, _)| *c) {
eq.terms.remove(pos);
}
if !queued[i] && !eq.used && eq.degree() == 1 {
let next_col = eq.terms[0].0;
if active_cols.contains(&next_col) && solved[next_col].is_none() {
queue.push_back(i);
queued[i] = true;
state.stats.peel_queue_pushes += 1;
}
}
}
state.stats.peel_frontier_peak = state.stats.peel_frontier_peak.max(queue.len());
state.solved[col] = Some(solution);
}
}
#[allow(clippy::too_many_lines)]
fn inactivate_and_solve(&self, state: &mut DecoderState) -> Result<(), DecodeError> {
let symbol_size = self.params.symbol_size;
let unsolved: Vec<usize> = state
.active_cols
.iter()
.filter(|&&col| state.solved[col].is_none())
.copied()
.collect();
if unsolved.is_empty() {
return Ok(());
}
state.stats.peeling_fallback_reason = Some("peeling_exhausted_to_dense_core");
let unused_eqs: Vec<usize> = state
.equations
.iter()
.enumerate()
.filter_map(|(i, eq)| if eq.used { None } else { Some(i) })
.collect();
let (dense_rows, dropped_zero_rows) = build_dense_core_rows(state, &unused_eqs, &unsolved)?;
state.stats.dense_core_dropped_rows += dropped_zero_rows;
validate_dense_core_rhs_widths(state, &dense_rows, symbol_size)?;
for &col in &unsolved {
state.inactive_cols.insert(col);
state.active_cols.remove(&col);
state.stats.inactivated += 1;
}
let (dense_factor, cache_observation) =
self.dense_factor_with_cache(&state.equations, &dense_rows, &unsolved);
apply_dense_factor_cache_observation(&mut state.stats, cache_observation);
let dense_cols = &dense_factor.dense_cols;
let col_to_dense = &dense_factor.col_to_dense;
let n_rows = dense_rows.len();
let n_cols = dense_cols.len();
let inactivation_pressure_permille =
unsolved.len().saturating_mul(1000) / state.params.l.max(1);
state.stats.dense_core_rows = n_rows;
state.stats.dense_core_cols = n_cols;
let mut b: Vec<Vec<u8>> = Vec::with_capacity(n_rows);
if n_rows < n_cols {
reactivate_unsolved_columns(state, &unsolved);
return Err(singular_matrix_error(&unsolved, n_rows));
}
let total_cells = match n_rows.checked_mul(n_cols) {
Some(total_cells) => total_cells,
None => {
let err = DecodeError::InsufficientSymbols {
received: n_rows,
required: n_cols,
};
reactivate_unsolved_columns(state, &unsolved);
return Err(err);
}
};
let mut a = vec![Gf256::ZERO; total_cells];
let mut dense_nonzeros = 0usize;
let mut dense_col_support = vec![0usize; n_cols];
for (row, &eq_idx) in dense_rows.iter().enumerate() {
let row_off = row * n_cols;
for &(col, coef) in &state.equations[eq_idx].terms {
if let Some(dense_col) = dense_col_index(col_to_dense, col) {
a[row_off + dense_col] = coef;
if !coef.is_zero() {
dense_nonzeros += 1;
dense_col_support[dense_col] += 1;
}
}
}
b.push(std::mem::take(&mut state.rhs[eq_idx]));
}
let unsupported_cols = dense_col_support
.iter()
.filter(|&&support| support == 0)
.count();
let dense_rhs_snapshot = snapshot_dense_rhs(&b, symbol_size);
let decision = choose_runtime_decoder_policy(
n_rows,
n_cols,
dense_nonzeros,
unsupported_cols,
inactivation_pressure_permille,
);
apply_policy_decision_to_stats(&mut state.stats, decision);
let mut hard_regime = !matches!(decision.mode, DecoderPolicyMode::ConservativeBaseline);
let mut hard_plan = match decision.mode {
DecoderPolicyMode::ConservativeBaseline | DecoderPolicyMode::HighSupportFirst => {
HardRegimePlan::Markowitz
}
DecoderPolicyMode::BlockSchurLowRank => select_hard_regime_plan(n_rows, n_cols, &a),
};
if hard_regime {
state.stats.hard_regime_activated = true;
state.stats.hard_regime_branch = Some(hard_plan.label());
} else if decision.reason == "policy_budget_exhausted_conservative" {
state.stats.hard_regime_conservative_fallback_reason = Some(decision.reason);
}
let mut pivot_row = vec![usize::MAX; n_cols];
loop {
pivot_row.fill(usize::MAX);
let mut row_used = vec![false; n_rows];
let mut pivot_buf = vec![Gf256::ZERO; n_cols];
let mut pivot_rhs = vec![0u8; symbol_size];
let mut gauss_ops = 0usize;
let mut pivots_selected = 0usize;
let mut markowitz_pivots = 0usize;
let mut elimination_error = None;
for col in 0..n_cols {
let pivot =
select_pivot_row(&a, n_rows, n_cols, col, &row_used, hard_regime, hard_plan);
let Some(prow) = pivot else {
elimination_error = Some(singular_matrix_error(dense_cols, col));
break;
};
pivot_row[col] = prow;
row_used[prow] = true;
pivots_selected += 1;
if hard_regime && matches!(hard_plan, HardRegimePlan::Markowitz) {
markowitz_pivots += 1;
}
let prow_off = prow * n_cols;
let pivot_coef = a[prow_off + col];
let inv = pivot_coef.inv();
for value in &mut a[prow_off..prow_off + n_cols] {
*value *= inv;
}
crate::raptorq::gf256::gf256_mul_slice(&mut b[prow], inv);
pivot_buf[..n_cols].copy_from_slice(&a[prow_off..prow_off + n_cols]);
pivot_rhs[..symbol_size].copy_from_slice(&b[prow]);
let sparse_cols = sparse_update_columns_if_beneficial(&pivot_buf[..n_cols], n_cols);
for (row, rhs) in b.iter_mut().enumerate().take(n_rows) {
if row == prow {
continue;
}
let row_off = row * n_cols;
let factor = a[row_off + col];
if factor.is_zero() {
continue;
}
if let Some(cols) = sparse_cols.as_ref() {
for &c in cols {
a[row_off + c] += factor * pivot_buf[c];
}
} else {
for c in 0..n_cols {
a[row_off + c] += factor * pivot_buf[c];
}
}
gf256_addmul_slice(rhs, &pivot_rhs[..symbol_size], factor);
gauss_ops += 1;
}
}
if elimination_error.is_none() {
if let Some(row) = first_inconsistent_dense_row(&a, n_rows, n_cols, &b) {
elimination_error = Some(inconsistent_matrix_error(&dense_rows, row));
}
}
state.stats.pivots_selected += pivots_selected;
state.stats.markowitz_pivots += markowitz_pivots;
state.stats.gauss_ops += gauss_ops;
if let Some(err) = elimination_error {
if !hard_regime {
hard_regime = true;
state.stats.hard_regime_activated = true;
state.stats.hard_regime_fallbacks += 1;
state.stats.hard_regime_conservative_fallback_reason =
Some("fallback_after_baseline_failure");
rebuild_dense_matrix_from_equations(
&state.equations,
&dense_rows,
col_to_dense,
n_cols,
&mut a,
);
restore_dense_rhs(&mut b, &dense_rhs_snapshot, symbol_size);
hard_plan = select_hard_regime_plan(n_rows, n_cols, &a);
state.stats.hard_regime_branch = Some(hard_plan.label());
continue;
}
if matches!(hard_plan, HardRegimePlan::BlockSchurLowRank { .. }) {
hard_plan = HardRegimePlan::Markowitz;
state.stats.hard_regime_fallbacks += 1;
state.stats.hard_regime_conservative_fallback_reason =
Some("block_schur_failed_to_converge");
rebuild_dense_matrix_from_equations(
&state.equations,
&dense_rows,
col_to_dense,
n_cols,
&mut a,
);
restore_dense_rhs(&mut b, &dense_rhs_snapshot, symbol_size);
continue;
}
restore_dense_rows_into_state(state, &dense_rows, &dense_rhs_snapshot, symbol_size);
reactivate_unsolved_columns(state, &unsolved);
return Err(err);
}
break;
}
for (dense_col, &col) in dense_cols.iter().enumerate() {
let prow = pivot_row[dense_col];
if prow < n_rows {
state.solved[col] = Some(std::mem::take(&mut b[prow]));
} else {
state.solved[col] = Some(vec![0u8; symbol_size]);
}
}
Ok(())
}
#[allow(clippy::too_many_lines)]
fn inactivate_and_solve_with_proof(
&self,
state: &mut DecoderState,
trace: &mut EliminationTrace,
) -> Result<(), DecodeError> {
*trace = EliminationTrace::default();
let symbol_size = self.params.symbol_size;
let unsolved: Vec<usize> = state
.active_cols
.iter()
.filter(|&&col| state.solved[col].is_none())
.copied()
.collect();
if unsolved.is_empty() {
return Ok(());
}
state.stats.peeling_fallback_reason = Some("peeling_exhausted_to_dense_core");
let unused_eqs: Vec<usize> = state
.equations
.iter()
.enumerate()
.filter_map(|(i, eq)| if eq.used { None } else { Some(i) })
.collect();
let (dense_rows, dropped_zero_rows) = build_dense_core_rows(state, &unused_eqs, &unsolved)?;
state.stats.dense_core_dropped_rows += dropped_zero_rows;
validate_dense_core_rhs_widths(state, &dense_rows, symbol_size)?;
for &col in &unsolved {
state.inactive_cols.insert(col);
state.active_cols.remove(&col);
state.stats.inactivated += 1;
trace.record_inactivation(col);
}
let (dense_factor, cache_observation) =
self.dense_factor_with_cache(&state.equations, &dense_rows, &unsolved);
apply_dense_factor_cache_observation(&mut state.stats, cache_observation);
let dense_cols = &dense_factor.dense_cols;
let col_to_dense = &dense_factor.col_to_dense;
let n_rows = dense_rows.len();
let n_cols = dense_cols.len();
let inactivation_pressure_permille =
unsolved.len().saturating_mul(1000) / state.params.l.max(1);
state.stats.dense_core_rows = n_rows;
state.stats.dense_core_cols = n_cols;
let mut b: Vec<Vec<u8>> = Vec::with_capacity(n_rows);
if n_rows < n_cols {
reactivate_unsolved_columns(state, &unsolved);
return Err(singular_matrix_error(&unsolved, n_rows));
}
let total_cells = match n_rows.checked_mul(n_cols) {
Some(total_cells) => total_cells,
None => {
let err = DecodeError::InsufficientSymbols {
received: n_rows,
required: n_cols,
};
reactivate_unsolved_columns(state, &unsolved);
return Err(err);
}
};
let mut a = vec![Gf256::ZERO; total_cells];
let mut dense_nonzeros = 0usize;
let mut dense_col_support = vec![0usize; n_cols];
for (row, &eq_idx) in dense_rows.iter().enumerate() {
let row_off = row * n_cols;
for &(col, coef) in &state.equations[eq_idx].terms {
if let Some(dense_col) = dense_col_index(col_to_dense, col) {
a[row_off + dense_col] = coef;
if !coef.is_zero() {
dense_nonzeros += 1;
dense_col_support[dense_col] += 1;
}
}
}
b.push(std::mem::take(&mut state.rhs[eq_idx]));
}
let unsupported_cols = dense_col_support
.iter()
.filter(|&&support| support == 0)
.count();
let dense_rhs_snapshot = snapshot_dense_rhs(&b, symbol_size);
trace.set_strategy(InactivationStrategy::AllAtOnce);
let decision = choose_runtime_decoder_policy(
n_rows,
n_cols,
dense_nonzeros,
unsupported_cols,
inactivation_pressure_permille,
);
apply_policy_decision_to_stats(&mut state.stats, decision);
let mut hard_regime = !matches!(decision.mode, DecoderPolicyMode::ConservativeBaseline);
let mut hard_plan = match decision.mode {
DecoderPolicyMode::ConservativeBaseline | DecoderPolicyMode::HighSupportFirst => {
HardRegimePlan::Markowitz
}
DecoderPolicyMode::BlockSchurLowRank => select_hard_regime_plan(n_rows, n_cols, &a),
};
if hard_regime {
state.stats.hard_regime_activated = true;
state.stats.hard_regime_branch = Some(hard_plan.label());
trace.record_strategy_transition(
InactivationStrategy::AllAtOnce,
hard_plan.strategy(),
"dense_or_near_square",
);
} else if decision.reason == "policy_budget_exhausted_conservative" {
state.stats.hard_regime_conservative_fallback_reason = Some(decision.reason);
}
let mut pivot_row = vec![usize::MAX; n_cols];
loop {
pivot_row.fill(usize::MAX);
let mut row_used = vec![false; n_rows];
let mut pivot_buf = vec![Gf256::ZERO; n_cols];
let mut pivot_rhs = vec![0u8; symbol_size];
let mut gauss_ops = 0usize;
let mut pivots_selected = 0usize;
let mut markowitz_pivots = 0usize;
let mut elimination_error = None;
for col in 0..n_cols {
let pivot =
select_pivot_row(&a, n_rows, n_cols, col, &row_used, hard_regime, hard_plan);
let Some(prow) = pivot else {
elimination_error = Some(singular_matrix_error(dense_cols, col));
break;
};
pivot_row[col] = prow;
row_used[prow] = true;
pivots_selected += 1;
if hard_regime && matches!(hard_plan, HardRegimePlan::Markowitz) {
markowitz_pivots += 1;
}
trace.record_pivot(dense_cols[col], prow);
let prow_off = prow * n_cols;
let pivot_coef = a[prow_off + col];
let inv = pivot_coef.inv();
for value in &mut a[prow_off..prow_off + n_cols] {
*value *= inv;
}
crate::raptorq::gf256::gf256_mul_slice(&mut b[prow], inv);
pivot_buf[..n_cols].copy_from_slice(&a[prow_off..prow_off + n_cols]);
pivot_rhs[..symbol_size].copy_from_slice(&b[prow]);
let sparse_cols = sparse_update_columns_if_beneficial(&pivot_buf[..n_cols], n_cols);
for (row, rhs) in b.iter_mut().enumerate().take(n_rows) {
if row == prow {
continue;
}
let row_off = row * n_cols;
let factor = a[row_off + col];
if factor.is_zero() {
continue;
}
if let Some(cols) = sparse_cols.as_ref() {
for &c in cols {
a[row_off + c] += factor * pivot_buf[c];
}
} else {
for c in 0..n_cols {
a[row_off + c] += factor * pivot_buf[c];
}
}
gf256_addmul_slice(rhs, &pivot_rhs[..symbol_size], factor);
gauss_ops += 1;
trace.record_row_op();
}
}
if elimination_error.is_none() {
if let Some(row) = first_inconsistent_dense_row(&a, n_rows, n_cols, &b) {
elimination_error = Some(inconsistent_matrix_error(&dense_rows, row));
}
}
state.stats.pivots_selected += pivots_selected;
state.stats.markowitz_pivots += markowitz_pivots;
state.stats.gauss_ops += gauss_ops;
if let Some(err) = elimination_error {
if !hard_regime {
hard_regime = true;
state.stats.hard_regime_activated = true;
state.stats.hard_regime_fallbacks += 1;
state.stats.hard_regime_conservative_fallback_reason =
Some("fallback_after_baseline_failure");
rebuild_dense_matrix_from_equations(
&state.equations,
&dense_rows,
col_to_dense,
n_cols,
&mut a,
);
restore_dense_rhs(&mut b, &dense_rhs_snapshot, symbol_size);
hard_plan = select_hard_regime_plan(n_rows, n_cols, &a);
state.stats.hard_regime_branch = Some(hard_plan.label());
trace.record_strategy_transition(
InactivationStrategy::AllAtOnce,
hard_plan.strategy(),
"fallback_after_baseline_failure",
);
trace.pivots = 0;
trace.pivot_events.clear();
trace.row_ops = 0;
trace.pivot_events_truncated = false;
continue;
}
if matches!(hard_plan, HardRegimePlan::BlockSchurLowRank { .. }) {
hard_plan = HardRegimePlan::Markowitz;
state.stats.hard_regime_fallbacks += 1;
state.stats.hard_regime_conservative_fallback_reason =
Some("block_schur_failed_to_converge");
trace.record_strategy_transition(
InactivationStrategy::BlockSchurLowRank,
InactivationStrategy::HighSupportFirst,
"block_schur_failed_to_converge",
);
trace.pivots = 0;
trace.pivot_events.clear();
trace.row_ops = 0;
trace.pivot_events_truncated = false;
rebuild_dense_matrix_from_equations(
&state.equations,
&dense_rows,
col_to_dense,
n_cols,
&mut a,
);
restore_dense_rhs(&mut b, &dense_rhs_snapshot, symbol_size);
continue;
}
restore_dense_rows_into_state(state, &dense_rows, &dense_rhs_snapshot, symbol_size);
reactivate_unsolved_columns(state, &unsolved);
return Err(err);
}
break;
}
for (dense_col, &col) in dense_cols.iter().enumerate() {
let prow = pivot_row[dense_col];
if prow < n_rows {
state.solved[col] = Some(std::mem::take(&mut b[prow]));
} else {
state.solved[col] = Some(vec![0u8; symbol_size]);
}
}
Ok(())
}
#[must_use]
pub fn repair_equation(&self, esi: u32) -> (Vec<usize>, Vec<Gf256>) {
self.params.rfc_repair_equation(esi)
}
#[must_use]
pub fn repair_equation_rfc6330(&self, esi: u32) -> (Vec<usize>, Vec<Gf256>) {
self.repair_equation(esi)
}
#[must_use]
pub fn all_source_equations(&self) -> Vec<(Vec<usize>, Vec<Gf256>)> {
let k = self.params.k;
(0..k).map(|i| (vec![i], vec![Gf256::ONE])).collect()
}
#[must_use]
pub fn source_equation(&self, esi: u32) -> (Vec<usize>, Vec<Gf256>) {
assert!((esi as usize) < self.params.k, "source ESI must be < K");
(vec![esi as usize], vec![Gf256::ONE])
}
}
fn first_mismatch_byte(expected: &[u8], actual: &[u8]) -> Option<usize> {
expected
.iter()
.zip(actual.iter())
.position(|(expected, actual)| expected != actual)
}
fn rebuild_dense_matrix_from_equations(
equations: &[Equation],
dense_rows: &[usize],
col_to_dense: &DenseColIndexMap,
n_cols: usize,
a: &mut [Gf256],
) {
a.fill(Gf256::ZERO);
for (row, &eq_idx) in dense_rows.iter().enumerate() {
let row_off = row * n_cols;
for &(col, coef) in &equations[eq_idx].terms {
if let Some(dense_col) = dense_col_index(col_to_dense, col) {
a[row_off + dense_col] = coef;
}
}
}
}
fn snapshot_dense_rhs(rows: &[Vec<u8>], symbol_size: usize) -> Vec<u8> {
let mut snapshot = vec![0u8; rows.len().saturating_mul(symbol_size)];
for (row_idx, row) in rows.iter().enumerate() {
debug_assert_eq!(row.len(), symbol_size);
let off = row_idx * symbol_size;
snapshot[off..off + symbol_size].copy_from_slice(row);
}
snapshot
}
fn restore_dense_rhs(rows: &mut [Vec<u8>], snapshot: &[u8], symbol_size: usize) {
debug_assert_eq!(snapshot.len(), rows.len().saturating_mul(symbol_size));
for (row_idx, row) in rows.iter_mut().enumerate() {
debug_assert_eq!(row.len(), symbol_size);
let off = row_idx * symbol_size;
row.copy_from_slice(&snapshot[off..off + symbol_size]);
}
}
fn restore_dense_rows_into_state(
state: &mut DecoderState,
dense_rows: &[usize],
snapshot: &[u8],
symbol_size: usize,
) {
debug_assert_eq!(snapshot.len(), dense_rows.len().saturating_mul(symbol_size));
for (row_idx, &eq_idx) in dense_rows.iter().enumerate() {
let off = row_idx * symbol_size;
state.rhs[eq_idx] = snapshot[off..off + symbol_size].to_vec();
}
}
fn reactivate_unsolved_columns(state: &mut DecoderState, unsolved: &[usize]) {
for &col in unsolved {
state.inactive_cols.remove(&col);
state.active_cols.insert(col);
}
}
impl ReceivedSymbol {
#[must_use]
pub fn source(esi: u32, data: Vec<u8>) -> Self {
Self {
esi,
is_source: true,
columns: vec![esi as usize],
coefficients: vec![Gf256::ONE],
data,
}
}
#[must_use]
pub fn repair(esi: u32, columns: Vec<usize>, coefficients: Vec<Gf256>, data: Vec<u8>) -> Self {
Self {
esi,
is_source: false,
columns,
coefficients,
data,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use insta::assert_json_snapshot;
use serde_json::json;
fn should_use_sparse_row_update(pivot_nnz: usize, n_cols: usize) -> bool {
if n_cols == 0 {
return false;
}
pivot_nnz.saturating_mul(HYBRID_SPARSE_COST_DENOMINATOR)
<= n_cols.saturating_mul(HYBRID_SPARSE_COST_NUMERATOR)
}
fn pivot_nonzero_columns(pivot_row: &[Gf256], n_cols: usize) -> Vec<usize> {
let mut cols = Vec::with_capacity(n_cols.min(32));
for (idx, coef) in pivot_row.iter().take(n_cols).enumerate() {
if !coef.is_zero() {
cols.push(idx);
}
}
cols
}
use crate::raptorq::systematic::SystematicEncoder;
use crate::raptorq::test_log_schema::{
UnitDecodeStats, UnitGovernanceDecision, UnitLogEntry, validate_unit_log_json,
};
fn rfc_eq_context(
scenario_id: &str,
seed: u64,
k: usize,
symbol_size: usize,
loss_pattern: &str,
outcome: &str,
) -> String {
format!(
"scenario_id={scenario_id} seed={seed} k={k} symbol_size={symbol_size} \
loss_pattern={loss_pattern} outcome={outcome} \
artifact_path=artifacts/raptorq_b2_tuple_scenarios_v1.json \
fixture_ref=RQ-B2-TUPLE-V1 \
repro_cmd='rch exec -- cargo test -p asupersync --lib \
repair_equation_rfc6330 -- --nocapture'"
)
}
fn to_unit_decode_stats(k: usize, dropped: usize, stats: &DecodeStats) -> UnitDecodeStats {
UnitDecodeStats {
k,
loss_pct: dropped.saturating_mul(100) / k.max(1),
dropped,
peeled: stats.peeled,
inactivated: stats.inactivated,
gauss_ops: stats.gauss_ops,
pivots: stats.pivots_selected,
peel_queue_pushes: stats.peel_queue_pushes,
peel_queue_pops: stats.peel_queue_pops,
peel_frontier_peak: stats.peel_frontier_peak,
dense_core_rows: stats.dense_core_rows,
dense_core_cols: stats.dense_core_cols,
dense_core_dropped_rows: stats.dense_core_dropped_rows,
fallback_reason: stats
.hard_regime_conservative_fallback_reason
.or(stats.peeling_fallback_reason)
.unwrap_or("none")
.to_string(),
hard_regime_activated: stats.hard_regime_activated,
hard_regime_branch: stats.hard_regime_branch.unwrap_or("none").to_string(),
hard_regime_fallbacks: stats.hard_regime_fallbacks,
conservative_fallback_reason: stats
.hard_regime_conservative_fallback_reason
.unwrap_or("none")
.to_string(),
governance: stats.governance.as_ref().map(UnitGovernanceDecision::from),
}
}
fn emit_decoder_unit_log(
scenario_id: &str,
seed: u64,
parameter_set: &str,
outcome: &str,
repro_command: &str,
stats: Option<UnitDecodeStats>,
) -> String {
let mut entry = UnitLogEntry::new(
scenario_id,
seed,
parameter_set,
"replay:rq-track-c-decoder-unit-v1",
repro_command,
outcome,
)
.with_artifact_path("artifacts/raptorq_track_c_decoder_unit_v1.json");
if let Some(stats) = stats {
entry = entry.with_decode_stats(stats);
}
let json = entry.to_json().expect("serialize decoder unit log entry");
let violations = validate_unit_log_json(&json);
let context = entry.to_context_string();
assert!(
violations.is_empty(),
"{context}: unit log schema violations: {violations:?}"
);
json
}
fn decode_stats_snapshot(
scenario: &str,
k: usize,
symbol_size: usize,
seed: u64,
dropped: usize,
received_symbols: usize,
result: &DecodeResult,
) -> serde_json::Value {
json!({
"scenario": scenario,
"k": k,
"symbol_size": symbol_size,
"seed": seed,
"received_symbols": received_symbols,
"decoded_source_symbols": result.source.len(),
"intermediate_symbols": result.intermediate.len(),
"stats": serde_json::to_value(to_unit_decode_stats(k, dropped, &result.stats))
.expect("serialize decode stats snapshot"),
})
}
#[test]
fn dense_col_index_map_handles_sparse_columns() {
let unsolved = vec![2, 7, 11];
let col_to_dense = build_dense_col_index_map(&unsolved);
assert_eq!(dense_col_index(&col_to_dense, 2), Some(0));
assert_eq!(dense_col_index(&col_to_dense, 7), Some(1));
assert_eq!(dense_col_index(&col_to_dense, 11), Some(2));
assert_eq!(dense_col_index(&col_to_dense, 3), None);
assert_eq!(dense_col_index(&col_to_dense, 99), None);
}
#[test]
fn sparse_first_dense_columns_orders_by_support_then_column() {
let equations = vec![
Equation::new(vec![7, 11], vec![Gf256::ONE, Gf256::ONE]),
Equation::new(vec![2, 7], vec![Gf256::ONE, Gf256::ONE]),
Equation::new(vec![7], vec![Gf256::ONE]),
Equation::new(vec![2], vec![Gf256::ONE]),
];
let dense_rows = vec![0, 1, 2, 3];
let unsolved = vec![7, 2, 11];
let ordered = sparse_first_dense_columns(&equations, &dense_rows, &unsolved);
assert_eq!(ordered, vec![11, 2, 7]);
}
#[test]
fn sparse_first_dense_columns_sorted_fast_path_matches_expected() {
let equations = vec![
Equation::new(vec![7, 11], vec![Gf256::ONE, Gf256::ONE]),
Equation::new(vec![2, 7], vec![Gf256::ONE, Gf256::ONE]),
Equation::new(vec![7], vec![Gf256::ONE]),
Equation::new(vec![2], vec![Gf256::ONE]),
];
let dense_rows = vec![0, 1, 2, 3];
let unsolved = vec![2, 7, 11];
let ordered = sparse_first_dense_columns(&equations, &dense_rows, &unsolved);
assert_eq!(ordered, vec![11, 2, 7]);
}
#[test]
fn dense_col_index_map_uses_direct_representation_for_compact_range() {
let unsolved = vec![1, 2, 4];
let map = build_dense_col_index_map(&unsolved);
assert!(matches!(map, DenseColIndexMap::Direct(_)));
assert_eq!(dense_col_index(&map, 1), Some(0));
assert_eq!(dense_col_index(&map, 2), Some(1));
assert_eq!(dense_col_index(&map, 4), Some(2));
assert_eq!(dense_col_index(&map, 3), None);
}
#[test]
fn dense_col_index_map_uses_sorted_pairs_for_sparse_high_columns() {
let unsolved = vec![2, 7, 10_000];
let map = build_dense_col_index_map(&unsolved);
assert!(matches!(map, DenseColIndexMap::SortedPairs(_)));
assert_eq!(dense_col_index(&map, 2), Some(0));
assert_eq!(dense_col_index(&map, 7), Some(1));
assert_eq!(dense_col_index(&map, 10_000), Some(2));
assert_eq!(dense_col_index(&map, 9_999), None);
}
#[test]
fn dense_factor_signature_detects_equation_changes() {
let equations_a = vec![Equation::new(vec![0, 1], vec![Gf256::ONE, Gf256::new(7)])];
let equations_b = vec![Equation::new(vec![0, 1], vec![Gf256::ONE, Gf256::new(9)])];
let dense_rows = vec![0];
let unsolved = vec![0, 1];
let sig_a = DenseFactorSignature::from_equations(&equations_a, &dense_rows, &unsolved);
let sig_b = DenseFactorSignature::from_equations(&equations_b, &dense_rows, &unsolved);
assert_ne!(sig_a, sig_b);
}
#[test]
fn dense_factor_cache_requires_strict_signature_match() {
let equations_a = vec![Equation::new(vec![0, 1], vec![Gf256::ONE, Gf256::new(7)])];
let equations_b = vec![Equation::new(vec![0, 1], vec![Gf256::ONE, Gf256::new(9)])];
let dense_rows = vec![0];
let unsolved = vec![0, 1];
let sig_a = DenseFactorSignature::from_equations(&equations_a, &dense_rows, &unsolved);
let sig_b = DenseFactorSignature::from_equations(&equations_b, &dense_rows, &unsolved);
let mut cache = DenseFactorCache::default();
assert_eq!(
cache.insert(
sig_a.clone(),
Arc::new(DenseFactorArtifact::new(vec![1, 0]))
),
DenseFactorCacheResult::MissInserted
);
assert_eq!(
cache.lookup(&sig_a),
DenseFactorCacheLookup::Hit(Arc::new(DenseFactorArtifact::new(vec![1, 0])))
);
assert_eq!(cache.lookup(&sig_b), DenseFactorCacheLookup::MissNoEntry);
}
#[test]
fn dense_factor_cache_detects_fingerprint_collision() {
let equations_a = vec![Equation::new(vec![0, 1], vec![Gf256::ONE, Gf256::new(7)])];
let equations_b = vec![Equation::new(vec![0, 1], vec![Gf256::ONE, Gf256::new(9)])];
let dense_rows = vec![0];
let unsolved = vec![0, 1];
let sig_a = DenseFactorSignature::from_equations(&equations_a, &dense_rows, &unsolved);
let mut sig_b = DenseFactorSignature::from_equations(&equations_b, &dense_rows, &unsolved);
sig_b.fingerprint = sig_a.fingerprint;
let mut cache = DenseFactorCache::default();
assert_eq!(
cache.insert(sig_a, Arc::new(DenseFactorArtifact::new(vec![1, 0]))),
DenseFactorCacheResult::MissInserted
);
assert_eq!(
cache.lookup(&sig_b),
DenseFactorCacheLookup::MissFingerprintCollision
);
}
#[test]
fn dense_factor_cache_evicts_oldest_entry_at_capacity() {
let mut cache = DenseFactorCache::default();
let mut first_signature = None;
for idx in 0..=DENSE_FACTOR_CACHE_CAPACITY {
let signature = DenseFactorSignature {
fingerprint: idx as u64,
unsolved: vec![idx],
row_offsets: vec![1],
row_terms_flat: vec![(idx, 1)],
};
if idx == 0 {
first_signature = Some(signature.clone());
}
let expected = if idx + 1 > DENSE_FACTOR_CACHE_CAPACITY {
DenseFactorCacheResult::MissEvicted
} else {
DenseFactorCacheResult::MissInserted
};
assert_eq!(
cache.insert(signature, Arc::new(DenseFactorArtifact::new(vec![idx]))),
expected
);
}
assert_eq!(cache.len(), DENSE_FACTOR_CACHE_CAPACITY);
assert_eq!(
cache.lookup(&first_signature.expect("first signature recorded")),
DenseFactorCacheLookup::MissNoEntry
);
}
#[test]
fn hybrid_cost_model_prefers_sparse_for_low_support() {
assert!(should_use_sparse_row_update(3, 8));
assert!(should_use_sparse_row_update(6, 10));
assert!(!should_use_sparse_row_update(7, 10));
assert!(!should_use_sparse_row_update(1, 0));
}
#[test]
fn pivot_nonzero_columns_returns_stable_sorted_positions() {
let row = vec![
Gf256::ZERO,
Gf256::ONE,
Gf256::ZERO,
Gf256::ONE,
Gf256::ONE,
Gf256::ZERO,
];
let cols = pivot_nonzero_columns(&row, row.len());
assert_eq!(cols, vec![1, 3, 4]);
}
#[test]
fn sparse_update_columns_if_beneficial_matches_threshold() {
let row_sparse = vec![
Gf256::ONE,
Gf256::ONE,
Gf256::ZERO,
Gf256::ONE,
Gf256::ZERO,
Gf256::ONE,
Gf256::ONE,
Gf256::ONE,
Gf256::ZERO,
Gf256::ZERO,
];
let row_dense = vec![
Gf256::ONE,
Gf256::ONE,
Gf256::ONE,
Gf256::ONE,
Gf256::ONE,
Gf256::ONE,
Gf256::ONE,
Gf256::ZERO,
Gf256::ZERO,
Gf256::ZERO,
];
let sparse_cols = sparse_update_columns_if_beneficial(&row_sparse, 10)
.expect("row_sparse should take sparse update path");
assert_eq!(sparse_cols, vec![0, 1, 3, 5, 6, 7]);
assert!(sparse_update_columns_if_beneficial(&row_dense, 10).is_none());
}
fn make_source_data(k: usize, symbol_size: usize) -> Vec<Vec<u8>> {
(0..k)
.map(|i| {
(0..symbol_size)
.map(|j| ((i * 37 + j * 13 + 7) % 256) as u8)
.collect()
})
.collect()
}
fn make_received_source(
decoder: &InactivationDecoder,
source: &[Vec<u8>],
) -> Vec<ReceivedSymbol> {
let source_eqs = decoder.all_source_equations();
source
.iter()
.enumerate()
.map(|(i, data)| {
let (cols, coefs) = source_eqs[i].clone();
ReceivedSymbol {
esi: i as u32,
is_source: true,
columns: cols,
coefficients: coefs,
data: data.clone(),
}
})
.collect()
}
fn build_repair_from_intermediate(
encoder: &SystematicEncoder,
columns: &[usize],
symbol_size: usize,
) -> Vec<u8> {
let mut out = vec![0u8; symbol_size];
for &col in columns {
for (dst, src) in out.iter_mut().zip(encoder.intermediate_symbol(col)) {
*dst ^= *src;
}
}
out
}
#[test]
fn decode_all_source_symbols() {
let k = 8;
let symbol_size = 32;
let seed = 42u64;
let source = make_source_data(k, symbol_size);
let decoder = InactivationDecoder::new(k, symbol_size, seed);
let mut received = decoder.constraint_symbols();
received.extend(make_received_source(&decoder, &source));
let encoder = SystematicEncoder::new(&source, symbol_size, seed).unwrap();
let l = decoder.params().l;
for esi in (k as u32)..(l as u32) {
let (cols, coefs) = decoder.repair_equation(esi);
let repair_data = encoder.repair_symbol(esi);
received.push(ReceivedSymbol::repair(esi, cols, coefs, repair_data));
}
let result = decoder.decode(&received).expect("decode should succeed");
for (i, original) in source.iter().enumerate() {
assert_eq!(&result.source[i], original, "source symbol {i} mismatch");
}
}
#[test]
fn decode_mixed_source_and_repair() {
let k = 8;
let symbol_size = 32;
let seed = 42u64;
let source = make_source_data(k, symbol_size);
let encoder = SystematicEncoder::new(&source, symbol_size, seed).unwrap();
let decoder = InactivationDecoder::new(k, symbol_size, seed);
let l = decoder.params().l;
let mut received = decoder.constraint_symbols();
let source_eqs = decoder.all_source_equations();
for i in 0..(k / 2) {
let (cols, coefs) = source_eqs[i].clone();
received.push(ReceivedSymbol {
esi: i as u32,
is_source: true,
columns: cols,
coefficients: coefs,
data: source[i].clone(),
});
}
for esi in (k as u32)..(l as u32 + k as u32 / 2) {
let (cols, coefs) = decoder.repair_equation(esi);
let repair_data = encoder.repair_symbol(esi);
received.push(ReceivedSymbol::repair(esi, cols, coefs, repair_data));
}
let result = decoder.decode(&received).expect("decode should succeed");
for (i, original) in source.iter().enumerate() {
assert_eq!(&result.source[i], original, "source symbol {i} mismatch");
}
}
#[test]
fn decode_repair_only() {
let k = 4;
let symbol_size = 16;
let seed = 99u64;
let source = make_source_data(k, symbol_size);
let encoder = SystematicEncoder::new(&source, symbol_size, seed).unwrap();
let decoder = InactivationDecoder::new(k, symbol_size, seed);
let l = decoder.params().l;
let mut received = decoder.constraint_symbols();
for esi in (k as u32)..(k as u32 + l as u32) {
let (cols, coefs) = decoder.repair_equation(esi);
let repair_data = encoder.repair_symbol(esi);
received.push(ReceivedSymbol::repair(esi, cols, coefs, repair_data));
}
let result = decoder.decode(&received).expect("decode should succeed");
for (i, original) in source.iter().enumerate() {
assert_eq!(&result.source[i], original, "source symbol {i} mismatch");
}
}
#[test]
fn decode_repair_only_hits_dense_factor_cache_on_second_run() {
let k = 4;
let symbol_size = 16;
let seed = 99u64;
let source = make_source_data(k, symbol_size);
let encoder = SystematicEncoder::new(&source, symbol_size, seed).unwrap();
let decoder = InactivationDecoder::new(k, symbol_size, seed);
let l = decoder.params().l;
let mut received = decoder.constraint_symbols();
for esi in (k as u32)..(k as u32 + l as u32) {
let (cols, coefs) = decoder.repair_equation(esi);
let repair_data = encoder.repair_symbol(esi);
received.push(ReceivedSymbol::repair(esi, cols, coefs, repair_data));
}
let first = decoder
.decode(&received)
.expect("first decode should succeed");
let second = decoder
.decode(&received)
.expect("second decode should succeed");
assert!(
first.stats.factor_cache_misses >= 1,
"first decode should populate dense-factor cache"
);
assert!(
second.stats.factor_cache_hits >= 1,
"second decode should hit dense-factor cache"
);
assert_eq!(
first.stats.factor_cache_last_reason,
Some("cache_miss_rebuild")
);
assert_eq!(
second.stats.factor_cache_last_reason,
Some("signature_match_reuse")
);
assert_eq!(first.stats.factor_cache_last_reuse_eligible, Some(false));
assert_eq!(second.stats.factor_cache_last_reuse_eligible, Some(true));
assert_eq!(
first.stats.factor_cache_last_key, second.stats.factor_cache_last_key,
"repeated burst decode should probe the same structural cache key",
);
assert_eq!(
second.stats.factor_cache_capacity,
DENSE_FACTOR_CACHE_CAPACITY
);
assert!(
second.stats.factor_cache_entries <= second.stats.factor_cache_capacity,
"cache occupancy must remain bounded by configured capacity"
);
}
#[test]
fn decode_burst_loss_payload_recovers_with_repair_overhead() {
let k = 8;
let symbol_size = 32;
let seed = 2026u64;
let source = make_source_data(k, symbol_size);
let encoder = SystematicEncoder::new(&source, symbol_size, seed).unwrap();
let decoder = InactivationDecoder::new(k, symbol_size, seed);
let l = decoder.params().l;
let mut payload = make_received_source(&decoder, &source);
for esi in (k as u32)..((k + l + 8) as u32) {
let (cols, coefs) = decoder.repair_equation(esi);
let repair_data = encoder.repair_symbol(esi);
payload.push(ReceivedSymbol::repair(esi, cols, coefs, repair_data));
}
payload.drain(3..7);
let mut received = decoder.constraint_symbols();
received.extend(payload);
assert!(
received.len() >= l,
"burst-loss scenario must still provide at least L equations"
);
let first = decoder
.decode(&received)
.expect("burst-loss decode should recover source symbols");
let second = InactivationDecoder::new(k, symbol_size, seed)
.decode(&received)
.expect("burst-loss replay decode should recover source symbols");
assert_eq!(first.source, source);
assert_eq!(second.source, source);
assert_eq!(
first.source, second.source,
"replay should be deterministic"
);
assert_eq!(first.stats.peeled, second.stats.peeled);
assert_eq!(first.stats.inactivated, second.stats.inactivated);
}
#[test]
fn decode_statistics_output_scrubbed() {
let k = 8;
let symbol_size = 32;
let happy_seed = 42u64;
let happy_source = make_source_data(k, symbol_size);
let happy_encoder = SystematicEncoder::new(&happy_source, symbol_size, happy_seed).unwrap();
let happy_decoder = InactivationDecoder::new(k, symbol_size, happy_seed);
let happy_l = happy_decoder.params().l;
let mut happy_received = happy_decoder.constraint_symbols();
happy_received.extend(make_received_source(&happy_decoder, &happy_source));
for esi in (k as u32)..(happy_l as u32) {
let (cols, coefs) = happy_decoder.repair_equation(esi);
let repair_data = happy_encoder.repair_symbol(esi);
happy_received.push(ReceivedSymbol::repair(esi, cols, coefs, repair_data));
}
let happy_result = happy_decoder
.decode(&happy_received)
.expect("happy-path decode should succeed");
let degraded_seed = 2026u64;
let degraded_source = make_source_data(k, symbol_size);
let degraded_encoder =
SystematicEncoder::new(°raded_source, symbol_size, degraded_seed).unwrap();
let degraded_decoder = InactivationDecoder::new(k, symbol_size, degraded_seed);
let degraded_l = degraded_decoder.params().l;
let mut degraded_payload = make_received_source(°raded_decoder, °raded_source);
for esi in (k as u32)..((k + degraded_l + 8) as u32) {
let (cols, coefs) = degraded_decoder.repair_equation(esi);
let repair_data = degraded_encoder.repair_symbol(esi);
degraded_payload.push(ReceivedSymbol::repair(esi, cols, coefs, repair_data));
}
degraded_payload.drain(3..7);
let mut degraded_received = degraded_decoder.constraint_symbols();
degraded_received.extend(degraded_payload);
let degraded_result = degraded_decoder
.decode(°raded_received)
.expect("degraded decode should recover source symbols");
assert_json_snapshot!(
"decode_statistics_output_scrubbed",
json!({
"happy_path": decode_stats_snapshot(
"happy_path",
k,
symbol_size,
happy_seed,
0,
happy_received.len(),
&happy_result,
),
"degraded_burst_loss": decode_stats_snapshot(
"degraded_burst_loss",
k,
symbol_size,
degraded_seed,
4,
degraded_received.len(),
°raded_result,
),
})
);
}
#[test]
fn decode_corrupted_repair_symbol_reports_corrupt_output() {
let k = 8;
let symbol_size = 32;
let seed = 0u64;
let source = make_source_data(k, symbol_size);
let encoder = SystematicEncoder::new(&source, symbol_size, seed).unwrap();
let decoder = InactivationDecoder::new(k, symbol_size, seed);
let l = decoder.params().l;
let mut received = decoder.constraint_symbols();
received.extend(make_received_source(&decoder, &source));
for esi in (k as u32)..(l as u32) {
let (cols, coefs) = decoder.repair_equation(esi);
let repair_data = encoder.repair_symbol(esi);
received.push(ReceivedSymbol::repair(esi, cols, coefs, repair_data));
}
let tampered = received
.iter_mut()
.find(|sym| !sym.is_source && sym.esi >= k as u32)
.expect("must include at least one repair symbol");
tampered.data[0] ^= 0x5A;
let err = decoder
.decode(&received)
.expect_err("corrupted repair symbol must fail");
assert!(
matches!(err, DecodeError::SingularMatrix { .. })
|| matches!(err, DecodeError::CorruptDecodedOutput { .. }),
"expected corruption or inconsistency, got: {err:?}"
);
}
#[test]
fn decode_with_proof_corrupted_repair_symbol_reports_failure_reason() {
let k = 8;
let symbol_size = 32;
let seed = 0u64;
let source = make_source_data(k, symbol_size);
let encoder = SystematicEncoder::new(&source, symbol_size, seed).unwrap();
let decoder = InactivationDecoder::new(k, symbol_size, seed);
let l = decoder.params().l;
let mut received = decoder.constraint_symbols();
received.extend(make_received_source(&decoder, &source));
for esi in (k as u32)..(l as u32) {
let (cols, coefs) = decoder.repair_equation(esi);
let repair_data = encoder.repair_symbol(esi);
received.push(ReceivedSymbol::repair(esi, cols, coefs, repair_data));
}
let tampered = received
.iter_mut()
.find(|sym| !sym.is_source && sym.esi >= k as u32)
.expect("must include at least one repair symbol");
tampered.data[0] ^= 0xA5;
let (err, proof) = decoder
.decode_with_proof(&received, ObjectId::new_for_test(9090), 0)
.expect_err("corrupted repair symbol should fail with proof witness");
assert!(
matches!(err, DecodeError::SingularMatrix { .. })
|| matches!(err, DecodeError::CorruptDecodedOutput { .. }),
"expected corruption or inconsistency, got: {err:?}"
);
assert!(
matches!(
proof.outcome,
crate::raptorq::proof::ProofOutcome::Failure {
reason: FailureReason::SingularMatrix { .. }
}
) || matches!(
proof.outcome,
crate::raptorq::proof::ProofOutcome::Failure {
reason: FailureReason::CorruptDecodedOutput { .. }
}
),
"expected corruption or inconsistency in proof"
);
}
#[test]
fn decode_wavefront_corrupted_repair_symbol_matches_direct_and_proof_failure() {
let k = 8;
let symbol_size = 32;
let seed = 1u64;
let source = make_source_data(k, symbol_size);
let encoder = SystematicEncoder::new(&source, symbol_size, seed).unwrap();
let decoder = InactivationDecoder::new(k, symbol_size, seed);
let l = decoder.params().l;
let mut received = decoder.constraint_symbols();
received.extend(make_received_source(&decoder, &source));
for esi in (k as u32)..(l as u32) {
let (cols, coefs) = decoder.repair_equation(esi);
let repair_data = encoder.repair_symbol(esi);
received.push(ReceivedSymbol::repair(esi, cols, coefs, repair_data));
}
let tampered = received
.iter_mut()
.find(|sym| !sym.is_source && sym.esi >= k as u32)
.expect("must include at least one repair symbol");
tampered.data[0] ^= 0x3C;
let direct = decoder
.decode(&received)
.expect_err("corrupted repair symbol must fail in direct decode");
let wavefront = decoder
.decode_wavefront(&received, 3)
.expect_err("corrupted repair symbol must fail in wavefront decode");
let (proof_err, proof) = decoder
.decode_with_proof(&received, ObjectId::new_for_test(9191), 0)
.expect_err("corrupted repair symbol must fail in proof decode");
assert_eq!(
direct, wavefront,
"wavefront decode must report the same failure as direct decode"
);
assert_eq!(
direct, proof_err,
"proof decode must report the same failure as direct decode"
);
assert!(
matches!(direct, DecodeError::SingularMatrix { .. })
|| matches!(direct, DecodeError::CorruptDecodedOutput { .. }),
"expected corruption or inconsistency, got: {direct:?}"
);
assert!(
matches!(
proof.outcome,
crate::raptorq::proof::ProofOutcome::Failure {
reason: FailureReason::SingularMatrix { .. }
}
) || matches!(
proof.outcome,
crate::raptorq::proof::ProofOutcome::Failure {
reason: FailureReason::CorruptDecodedOutput { .. }
}
),
"expected corruption or inconsistency in proof"
);
}
#[test]
fn decode_insufficient_symbols_fails() {
let k = 8;
let symbol_size = 32;
let seed = 42u64;
let source = make_source_data(k, symbol_size);
let decoder = InactivationDecoder::new(k, symbol_size, seed);
let source_eqs = decoder.all_source_equations();
let received: Vec<ReceivedSymbol> = (0..2)
.map(|i| {
let (cols, coefs) = source_eqs[i].clone();
ReceivedSymbol {
esi: i as u32,
is_source: true,
columns: cols,
coefficients: coefs,
data: source[i].clone(),
}
})
.collect();
let err = decoder.decode(&received).unwrap_err();
assert!(matches!(err, DecodeError::InsufficientSymbols { .. }));
let dropped = k.saturating_sub(received.len());
let parameter_set = format!("k={k},symbol_size={symbol_size},dropped={dropped}");
let log_json = emit_decoder_unit_log(
"RQ-C-LOG-FAIL-INSUFFICIENT-001",
seed,
¶meter_set,
"decode_failure",
"rch exec -- cargo test -p asupersync --lib raptorq::decoder::tests::decode_insufficient_symbols_fails -- --nocapture",
None,
);
assert!(
log_json.contains("\"scenario_id\":\"RQ-C-LOG-FAIL-INSUFFICIENT-001\""),
"failure log must retain deterministic scenario id"
);
assert!(
log_json
.contains("\"artifact_path\":\"artifacts/raptorq_track_c_decoder_unit_v1.json\""),
"failure log must include artifact pointer"
);
}
#[test]
fn decode_sources_only_auto_synthesizes_padded_lt_rows() {
let k = 50;
let symbol_size = 32;
let seed = 77u64;
let source = make_source_data(k, symbol_size);
let decoder = InactivationDecoder::new(k, symbol_size, seed);
assert!(
decoder.params().k_prime > k,
"test requires a padded systematic parameter set"
);
let mut received = decoder.constraint_symbols();
received.extend(make_received_source(&decoder, &source));
let result = decoder
.decode(&received)
.expect("decoder should synthesize K..K' zero LT rows");
for (idx, original) in source.iter().enumerate() {
assert_eq!(
&result.source[idx], original,
"source symbol {idx} mismatch after synthesized padding rows"
);
}
}
#[test]
fn wavefront_decode_sources_only_auto_synthesizes_padded_lt_rows() {
let k = 50;
let symbol_size = 32;
let seed = 79u64;
let source = make_source_data(k, symbol_size);
let decoder = InactivationDecoder::new(k, symbol_size, seed);
assert!(
decoder.params().k_prime > k,
"test requires a padded systematic parameter set"
);
let mut received = decoder.constraint_symbols();
received.extend(make_received_source(&decoder, &source));
let sequential = decoder
.decode(&received)
.expect("sequential decoder should synthesize K..K' zero LT rows");
for &batch_size in &[1, 7, received.len()] {
let wavefront = decoder
.decode_wavefront(&received, batch_size)
.unwrap_or_else(|_| panic!("wavefront decode batch_size={batch_size}"));
assert_eq!(
wavefront.source, sequential.source,
"wavefront decode must match sequential decode for padded K..K' rows"
);
}
}
#[test]
fn systematic_index_table_sources_only_decode_succeeds_across_representative_k_values() {
let symbol_size = 8;
let representative_k_values = [1usize, 2, 3, 4, 5, 8, 16, 31, 32, 33, 63, 64];
for &k in &representative_k_values {
let seed = 10_000u64 + k as u64;
let source = make_source_data(k, symbol_size);
let decoder = InactivationDecoder::new(k, symbol_size, seed);
let mut received = decoder.constraint_symbols();
received.extend(make_received_source(&decoder, &source));
let decoded = decoder
.decode(&received)
.unwrap_or_else(|_| panic!("sources-only decode must succeed for k={k}"));
assert_eq!(
decoded.source, source,
"systematic-table decode mismatch for k={k}"
);
}
}
#[test]
fn extra_repair_symbols_do_not_reduce_decode_success_rate() {
let k = 12;
let symbol_size = 16;
let seed = 0x5EED_0204u64;
let source = make_source_data(k, symbol_size);
let encoder = SystematicEncoder::new(&source, symbol_size, seed).unwrap();
let decoder = InactivationDecoder::new(k, symbol_size, seed);
let l = decoder.params().l;
let mut baseline = decoder.constraint_symbols();
for esi in (k as u32)..(k as u32 + l as u32) {
let (cols, coefs) = decoder.repair_equation(esi);
let repair_data = encoder.repair_symbol(esi);
baseline.push(ReceivedSymbol::repair(esi, cols, coefs, repair_data));
}
let baseline_decoded = decoder
.decode(&baseline)
.expect("repair-only baseline decode must succeed");
let mut with_extra_repair = baseline.clone();
for esi in (k as u32 + l as u32)..(k as u32 + l as u32 + 4) {
let (cols, coefs) = decoder.repair_equation(esi);
let repair_data = encoder.repair_symbol(esi);
with_extra_repair.push(ReceivedSymbol::repair(esi, cols, coefs, repair_data));
}
let extra_decoded = decoder
.decode(&with_extra_repair)
.expect("adding valid repair symbols must not break decode success");
assert_eq!(
extra_decoded.source, baseline_decoded.source,
"extra repair symbols must not change recovered source output"
);
let wavefront_extra = decoder
.decode_wavefront(&with_extra_repair, 3)
.expect("wavefront decode must remain successful with extra repair symbols");
assert_eq!(
wavefront_extra.source, baseline_decoded.source,
"wavefront decode must recover the same output after extra repair symbols are added"
);
}
#[test]
fn decode_rejects_source_esi_in_padded_range() {
let k = 50;
let symbol_size = 32;
let seed = 91u64;
let source = make_source_data(k, symbol_size);
let decoder = InactivationDecoder::new(k, symbol_size, seed);
assert!(
decoder.params().k_prime > k,
"test requires a padded systematic parameter set"
);
let mut received = decoder.constraint_symbols();
received.extend(make_received_source(&decoder, &source));
received.push(ReceivedSymbol::source(k as u32, vec![0xA5; symbol_size]));
let err = decoder.decode(&received).unwrap_err();
assert_eq!(
err,
DecodeError::SourceEsiOutOfRange {
esi: k as u32,
max_valid: k,
}
);
assert!(err.is_unrecoverable());
}
#[test]
fn decode_symbol_equation_arity_mismatch_fails() {
let k = 8;
let symbol_size = 32;
let seed = 42u64;
let source = make_source_data(k, symbol_size);
let encoder = SystematicEncoder::new(&source, symbol_size, seed).unwrap();
let decoder = InactivationDecoder::new(k, symbol_size, seed);
let l = decoder.params().l;
let mut received = decoder.constraint_symbols();
received.extend(make_received_source(&decoder, &source));
for esi in (k as u32)..(l as u32) {
let (cols, coefs) = decoder.repair_equation(esi);
let repair_data = encoder.repair_symbol(esi);
received.push(ReceivedSymbol::repair(esi, cols, coefs, repair_data));
}
received[0].columns.push(0);
let esi = received[0].esi;
let columns = received[0].columns.len();
let coefficients = received[0].coefficients.len();
let err = decoder.decode(&received).unwrap_err();
assert_eq!(
err,
DecodeError::SymbolEquationArityMismatch {
esi,
columns,
coefficients
}
);
}
#[test]
fn decode_with_proof_symbol_equation_arity_mismatch_reports_failure_reason() {
let k = 8;
let symbol_size = 32;
let seed = 43u64;
let source = make_source_data(k, symbol_size);
let encoder = SystematicEncoder::new(&source, symbol_size, seed).unwrap();
let decoder = InactivationDecoder::new(k, symbol_size, seed);
let l = decoder.params().l;
let mut received = decoder.constraint_symbols();
received.extend(make_received_source(&decoder, &source));
for esi in (k as u32)..(l as u32) {
let (cols, coefs) = decoder.repair_equation(esi);
let repair_data = encoder.repair_symbol(esi);
received.push(ReceivedSymbol::repair(esi, cols, coefs, repair_data));
}
received[0].columns.push(0);
let esi = received[0].esi;
let columns = received[0].columns.len();
let coefficients = received[0].coefficients.len();
let (err, proof) = decoder
.decode_with_proof(&received, ObjectId::new_for_test(4242), 0)
.unwrap_err();
assert_eq!(
err,
DecodeError::SymbolEquationArityMismatch {
esi,
columns,
coefficients
}
);
assert!(matches!(
proof.outcome,
crate::raptorq::proof::ProofOutcome::Failure {
reason: FailureReason::SymbolEquationArityMismatch {
esi: e,
columns: c,
coefficients: coef_count
}
} if e == esi && c == columns && coef_count == coefficients
));
}
#[test]
fn decode_column_index_out_of_range_fails_unrecoverably() {
let k = 8;
let symbol_size = 32;
let seed = 44u64;
let source = make_source_data(k, symbol_size);
let encoder = SystematicEncoder::new(&source, symbol_size, seed).unwrap();
let decoder = InactivationDecoder::new(k, symbol_size, seed);
let l = decoder.params().l;
let mut received = decoder.constraint_symbols();
received.extend(make_received_source(&decoder, &source));
for esi in (k as u32)..(l as u32) {
let (cols, coefs) = decoder.repair_equation(esi);
let repair_data = encoder.repair_symbol(esi);
received.push(ReceivedSymbol::repair(esi, cols, coefs, repair_data));
}
let esi = received[0].esi;
let invalid_column = l;
received[0].columns[0] = invalid_column;
let err = decoder.decode(&received).unwrap_err();
assert_eq!(
err,
DecodeError::ColumnIndexOutOfRange {
esi,
column: invalid_column,
max_valid: l
}
);
assert!(err.is_unrecoverable());
assert!(!err.is_recoverable());
}
#[test]
fn decode_with_proof_column_index_out_of_range_reports_failure_reason() {
let k = 8;
let symbol_size = 32;
let seed = 45u64;
let source = make_source_data(k, symbol_size);
let encoder = SystematicEncoder::new(&source, symbol_size, seed).unwrap();
let decoder = InactivationDecoder::new(k, symbol_size, seed);
let l = decoder.params().l;
let mut received = decoder.constraint_symbols();
received.extend(make_received_source(&decoder, &source));
for esi in (k as u32)..(l as u32) {
let (cols, coefs) = decoder.repair_equation(esi);
let repair_data = encoder.repair_symbol(esi);
received.push(ReceivedSymbol::repair(esi, cols, coefs, repair_data));
}
let esi = received[1].esi;
let invalid_column = l + 2;
received[1].columns[0] = invalid_column;
let (err, proof) = decoder
.decode_with_proof(&received, ObjectId::new_for_test(5252), 0)
.unwrap_err();
assert_eq!(
err,
DecodeError::ColumnIndexOutOfRange {
esi,
column: invalid_column,
max_valid: l
}
);
assert!(matches!(
proof.outcome,
crate::raptorq::proof::ProofOutcome::Failure {
reason: FailureReason::ColumnIndexOutOfRange {
esi: e,
column,
max_valid
}
} if e == esi && column == invalid_column && max_valid == l
));
}
#[test]
fn decode_deterministic() {
let k = 6;
let symbol_size = 24;
let seed = 77u64;
let source = make_source_data(k, symbol_size);
let encoder = SystematicEncoder::new(&source, symbol_size, seed).unwrap();
let decoder = InactivationDecoder::new(k, symbol_size, seed);
let l = decoder.params().l;
let mut received = decoder.constraint_symbols();
received.extend(make_received_source(&decoder, &source));
for esi in (k as u32)..(l as u32) {
let (cols, coefs) = decoder.repair_equation(esi);
let repair_data = encoder.repair_symbol(esi);
received.push(ReceivedSymbol::repair(esi, cols, coefs, repair_data));
}
let result1 = decoder.decode(&received).unwrap();
let result2 = decoder.decode(&received).unwrap();
assert_eq!(result1.source, result2.source);
assert_eq!(result1.stats.peeled, result2.stats.peeled);
assert_eq!(result1.stats.inactivated, result2.stats.inactivated);
assert_eq!(
result1.stats.peel_queue_pushes, result2.stats.peel_queue_pushes,
"peel queue push accounting must be deterministic"
);
assert_eq!(
result1.stats.peel_queue_pops, result2.stats.peel_queue_pops,
"peel queue pop accounting must be deterministic"
);
assert_eq!(
result1.stats.dense_core_rows, result2.stats.dense_core_rows,
"dense-core row extraction must be deterministic"
);
assert_eq!(
result1.stats.dense_core_cols, result2.stats.dense_core_cols,
"dense-core column extraction must be deterministic"
);
let parameter_set = format!("k={k},symbol_size={symbol_size},dropped=0");
let log_json = emit_decoder_unit_log(
"RQ-C-LOG-SUCCESS-DET-001",
seed,
¶meter_set,
"ok",
"rch exec -- cargo test -p asupersync --lib raptorq::decoder::tests::decode_deterministic -- --nocapture",
Some(to_unit_decode_stats(k, 0, &result1.stats)),
);
assert!(
log_json.contains("\"outcome\":\"ok\""),
"success log should preserve deterministic outcome marker"
);
assert!(
log_json.contains("\"repro_command\":\"rch exec --"),
"success log must keep remote replay command"
);
}
#[test]
fn stats_track_peeling_and_inactivation() {
let k = 8;
let symbol_size = 32;
let seed = 42u64;
let source = make_source_data(k, symbol_size);
let encoder = SystematicEncoder::new(&source, symbol_size, seed).unwrap();
let decoder = InactivationDecoder::new(k, symbol_size, seed);
let l = decoder.params().l;
let mut received = decoder.constraint_symbols();
received.extend(make_received_source(&decoder, &source));
for esi in (k as u32)..(l as u32 + 2) {
let (cols, coefs) = decoder.repair_equation(esi);
let repair_data = encoder.repair_symbol(esi);
received.push(ReceivedSymbol::repair(esi, cols, coefs, repair_data));
}
let result = decoder.decode(&received).unwrap();
assert!(
result.stats.peeled > 0 || result.stats.inactivated > 0,
"expected some peeling or inactivation"
);
assert!(
result.stats.peel_queue_pushes >= result.stats.peel_queue_pops,
"queue pushes should dominate or equal pops"
);
assert!(
result.stats.peel_frontier_peak > 0,
"peeling queue should observe non-zero frontier depth"
);
if result.stats.inactivated > 0 {
assert!(
result.stats.dense_core_cols > 0,
"dense core should contain unsolved columns when inactivation occurs"
);
assert_eq!(
result.stats.peeling_fallback_reason,
Some("peeling_exhausted_to_dense_core"),
"fallback reason should be explicit when we transition to dense core"
);
}
}
#[test]
fn repair_equation_rfc6330_deterministic() {
let k = 8;
let symbol_size = 32;
let seed = 42u64;
let decoder = InactivationDecoder::new(k, symbol_size, seed);
let (c1, k1) = decoder.repair_equation_rfc6330(17);
let (c2, k2) = decoder.repair_equation_rfc6330(17);
let context = rfc_eq_context(
"RQ-B2-DECODER-EQ-DET-001",
seed,
k,
symbol_size,
"none",
"deterministic_replay",
);
assert_eq!(c1, c2, "{context} column replay mismatch");
assert_eq!(k1, k2, "{context} coefficient replay mismatch");
}
#[test]
fn repair_equation_rfc6330_indices_within_bounds() {
let k = 10;
let symbol_size = 32;
let seed = 7u64;
let decoder = InactivationDecoder::new(k, symbol_size, seed);
let params = decoder.params();
let upper = params.w + params.p;
let context = rfc_eq_context(
"RQ-B2-DECODER-EQ-BOUNDS-001",
seed,
k,
symbol_size,
"none",
"index_bounds",
);
for esi in 0..32u32 {
let (cols, coefs) = decoder.repair_equation_rfc6330(esi);
assert_eq!(
cols.len(),
coefs.len(),
"{context} len mismatch for esi={esi}"
);
assert!(!cols.is_empty(), "{context} empty row for esi={esi}");
assert!(
cols.iter().all(|col| *col < upper),
"{context} out-of-range column for esi={esi}"
);
}
}
#[test]
fn repair_equation_rfc6330_includes_pi_domain_entries() {
let k = 12;
let symbol_size = 64;
let seed = 99u64;
let decoder = InactivationDecoder::new(k, symbol_size, seed);
let params = decoder.params();
let w = params.w;
let mut saw_pi = false;
for esi in 0..128u32 {
let (cols, _) = decoder.repair_equation_rfc6330(esi);
if cols.iter().any(|c| *c >= w) {
saw_pi = true;
break;
}
}
let context = rfc_eq_context(
"RQ-B2-DECODER-EQ-PI-001",
seed,
k,
symbol_size,
"none",
"pi_domain_coverage",
);
assert!(saw_pi, "{context} expected PI-domain index in sample");
}
#[test]
fn repair_equation_rfc6330_matches_systematic_params_helper() {
let scenarios = [
("RQ-C1-PARITY-001", 8usize, 32usize, 42u64),
("RQ-C1-PARITY-002", 16usize, 64usize, 77u64),
("RQ-C1-PARITY-003", 32usize, 128usize, 1234u64),
];
for (scenario_id, k, symbol_size, seed) in scenarios {
let decoder = InactivationDecoder::new(k, symbol_size, seed);
let params = SystematicParams::for_source_block(k, symbol_size);
for esi in 0..64u32 {
let decoder_eq = decoder.repair_equation_rfc6330(esi);
let shared_eq = params.rfc_repair_equation(esi);
let context = rfc_eq_context(
scenario_id,
seed,
k,
symbol_size,
"none",
"decoder_params_parity",
);
assert_eq!(
decoder_eq, shared_eq,
"{context} decoder/params equation mismatch for esi={esi}"
);
}
}
}
#[test]
fn decode_roundtrip_with_rfc_tuple_repair_equations() {
let k = 8;
let symbol_size = 32;
let seed = 42u64;
let source = make_source_data(k, symbol_size);
let encoder = SystematicEncoder::new(&source, symbol_size, seed)
.expect("RQ-C1-E2E-001 encoder setup should succeed");
let decoder = InactivationDecoder::new(k, symbol_size, seed);
let l = decoder.params().l;
let mut received = decoder.constraint_symbols();
received.extend(make_received_source(&decoder, &source));
for esi in (k as u32)..(l as u32) {
let (columns, coefficients) = decoder.repair_equation_rfc6330(esi);
let repair_data = build_repair_from_intermediate(&encoder, &columns, symbol_size);
received.push(ReceivedSymbol::repair(
esi,
columns,
coefficients,
repair_data,
));
}
let result = decoder.decode(&received).unwrap_or_else(|err| {
let context = rfc_eq_context(
"RQ-C1-E2E-001",
seed,
k,
symbol_size,
"none",
"decode_failed",
);
panic!("{context} unexpected decode failure: {err:?}");
});
for (i, original) in source.iter().enumerate() {
let context = rfc_eq_context(
"RQ-C1-E2E-001",
seed,
k,
symbol_size,
"none",
"roundtrip_compare",
);
assert_eq!(
&result.source[i], original,
"{context} source symbol mismatch at index {i}"
);
}
}
#[test]
fn verify_decoded_output_detects_corruption_witness() {
let k = 6;
let symbol_size = 16;
let seed = 46u64;
let decoder = InactivationDecoder::new(k, symbol_size, seed);
let source = make_source_data(k, symbol_size);
let received = make_received_source(&decoder, &source);
let mut intermediate = vec![vec![0u8; symbol_size]; decoder.params().l];
for (idx, src) in source.iter().enumerate() {
intermediate[idx] = src.clone();
}
intermediate[0][0] ^= 0xA5;
let err = decoder
.verify_decoded_output(&received, &intermediate)
.expect_err("corruption guard should reject inconsistent reconstruction");
assert!(matches!(
err,
DecodeError::CorruptDecodedOutput {
esi: 0,
byte_index: 0,
..
}
));
assert!(err.is_unrecoverable());
}
#[test]
fn failure_classification_is_explicit() {
assert!(
DecodeError::InsufficientSymbols {
received: 1,
required: 2
}
.is_recoverable()
);
assert!(DecodeError::SingularMatrix { row: 3 }.is_recoverable());
assert!(
DecodeError::SymbolSizeMismatch {
expected: 8,
actual: 7
}
.is_unrecoverable()
);
assert!(
DecodeError::ColumnIndexOutOfRange {
esi: 1,
column: 99,
max_valid: 12
}
.is_unrecoverable()
);
assert!(
DecodeError::SourceEsiOutOfRange {
esi: 12,
max_valid: 8
}
.is_unrecoverable()
);
assert!(
DecodeError::InvalidSourceSymbolEquation {
esi: 3,
expected_column: 3
}
.is_unrecoverable()
);
assert!(
DecodeError::CorruptDecodedOutput {
esi: 1,
byte_index: 0,
expected: 1,
actual: 2
}
.is_unrecoverable()
);
}
fn make_rank_deficient_state(
params: &SystematicParams,
symbol_size: usize,
left_col: usize,
right_col: usize,
) -> DecoderState {
let equation = Equation::new(vec![left_col, right_col], vec![Gf256::ONE, Gf256::ONE]);
let active_cols = [left_col, right_col].into_iter().collect();
DecoderState {
params: params.clone(),
equations: vec![equation.clone(), equation],
rhs: vec![vec![0x11; symbol_size], vec![0x22; symbol_size]],
solved: vec![None; params.l],
active_cols,
inactive_cols: BTreeSet::new(),
stats: DecodeStats::default(),
}
}
fn make_underdetermined_dense_core_state(
params: &SystematicParams,
symbol_size: usize,
left_col: usize,
right_col: usize,
) -> DecoderState {
let equation = Equation::new(vec![left_col, right_col], vec![Gf256::ONE, Gf256::ONE]);
let active_cols = [left_col, right_col].into_iter().collect();
DecoderState {
params: params.clone(),
equations: vec![equation],
rhs: vec![vec![0x11; symbol_size]],
solved: vec![None; params.l],
active_cols,
inactive_cols: BTreeSet::new(),
stats: DecodeStats::default(),
}
}
fn make_reordered_underdetermined_dense_core_state(
params: &SystematicParams,
symbol_size: usize,
left_col: usize,
middle_col: usize,
right_col: usize,
) -> DecoderState {
let eq_left_middle =
Equation::new(vec![left_col, middle_col], vec![Gf256::ONE, Gf256::ONE]);
let eq_middle_right =
Equation::new(vec![middle_col, right_col], vec![Gf256::ONE, Gf256::ONE]);
let active_cols = [left_col, middle_col, right_col].into_iter().collect();
DecoderState {
params: params.clone(),
equations: vec![eq_left_middle, eq_middle_right],
rhs: vec![vec![0x11; symbol_size], vec![0x22; symbol_size]],
solved: vec![None; params.l],
active_cols,
inactive_cols: BTreeSet::new(),
stats: DecodeStats::default(),
}
}
fn make_pivot_tie_break_state(
params: &SystematicParams,
symbol_size: usize,
left_col: usize,
right_col: usize,
) -> DecoderState {
let eq_left = Equation::new(vec![left_col], vec![Gf256::ONE]);
let eq_mix = Equation::new(vec![left_col, right_col], vec![Gf256::ONE, Gf256::ONE]);
let eq_right = Equation::new(vec![right_col], vec![Gf256::ONE]);
let active_cols = [left_col, right_col].into_iter().collect();
DecoderState {
params: params.clone(),
equations: vec![eq_left, eq_mix, eq_right],
rhs: vec![
vec![0x10; symbol_size],
vec![0x30; symbol_size],
vec![0x20; symbol_size],
],
solved: vec![None; params.l],
active_cols,
inactive_cols: BTreeSet::new(),
stats: DecodeStats::default(),
}
}
fn make_inconsistent_overdetermined_state(
params: &SystematicParams,
symbol_size: usize,
left_col: usize,
right_col: usize,
) -> DecoderState {
let eq_left = Equation::new(vec![left_col], vec![Gf256::ONE]);
let eq_right = Equation::new(vec![right_col], vec![Gf256::ONE]);
let eq_mix = Equation::new(vec![left_col, right_col], vec![Gf256::ONE, Gf256::ONE]);
let active_cols = [left_col, right_col].into_iter().collect();
DecoderState {
params: params.clone(),
equations: vec![eq_left, eq_right, eq_mix],
rhs: vec![
vec![0x10; symbol_size],
vec![0x20; symbol_size],
vec![0x31; symbol_size], ],
solved: vec![None; params.l],
active_cols,
inactive_cols: BTreeSet::new(),
stats: DecodeStats::default(),
}
}
fn make_dense_core_prunable_state(
params: &SystematicParams,
symbol_size: usize,
left_col: usize,
right_col: usize,
empty_rhs_byte: u8,
) -> DecoderState {
let eq_left = Equation::new(vec![left_col], vec![Gf256::ONE]);
let eq_right = Equation::new(vec![right_col], vec![Gf256::ONE]);
let eq_empty = Equation {
terms: Vec::new(),
used: false,
};
let active_cols = [left_col, right_col].into_iter().collect();
DecoderState {
params: params.clone(),
equations: vec![eq_left, eq_right, eq_empty],
rhs: vec![
vec![0x10; symbol_size],
vec![0x20; symbol_size],
vec![empty_rhs_byte; symbol_size],
],
solved: vec![None; params.l],
active_cols,
inactive_cols: BTreeSet::new(),
stats: DecodeStats::default(),
}
}
fn make_hard_regime_dense_state(
params: &SystematicParams,
symbol_size: usize,
start_col: usize,
width: usize,
) -> DecoderState {
let cols: Vec<usize> = (start_col..start_col + width).collect();
let mut equations = Vec::with_capacity(width);
let mut rhs = Vec::with_capacity(width);
for i in 0..width {
let row_cols = cols[i..].to_vec();
let row_coefs = vec![Gf256::ONE; row_cols.len()];
equations.push(Equation::new(row_cols, row_coefs));
rhs.push(vec![(i as u8) + 1; symbol_size]);
}
DecoderState {
params: params.clone(),
equations,
rhs,
solved: vec![None; params.l],
active_cols: cols.into_iter().collect(),
inactive_cols: BTreeSet::new(),
stats: DecodeStats::default(),
}
}
fn make_block_schur_rank_deficient_state(
params: &SystematicParams,
symbol_size: usize,
start_col: usize,
width: usize,
) -> DecoderState {
let cols: Vec<usize> = (start_col..start_col + width).collect();
let mut equations = Vec::with_capacity(width);
let mut rhs = Vec::with_capacity(width);
for i in 0..width {
equations.push(Equation::new(cols.clone(), vec![Gf256::ONE; cols.len()]));
let rhs_byte = ((i % 255) as u8).saturating_add(1);
rhs.push(vec![rhs_byte; symbol_size]);
}
DecoderState {
params: params.clone(),
equations,
rhs,
solved: vec![None; params.l],
active_cols: cols.into_iter().collect(),
inactive_cols: BTreeSet::new(),
stats: DecodeStats::default(),
}
}
#[test]
fn singular_matrix_reports_original_column_id() {
let decoder = InactivationDecoder::new(8, 16, 123);
let params = decoder.params().clone();
let mut state = make_rank_deficient_state(¶ms, 16, 3, 7);
let err = decoder.inactivate_and_solve(&mut state).unwrap_err();
assert_eq!(
err,
DecodeError::SingularMatrix { row: 7 },
"rank-deficient failure should report original unsolved column id"
);
}
#[test]
fn underdetermined_dense_core_reports_singular_error() {
let decoder = InactivationDecoder::new(8, 16, 512);
let params = decoder.params().clone();
let mut state = make_underdetermined_dense_core_state(¶ms, 16, 3, 7);
let err = decoder.inactivate_and_solve(&mut state).unwrap_err();
assert_eq!(
err,
DecodeError::SingularMatrix { row: 7 },
"dense-core underdetermined systems should be classified as rank-deficient"
);
}
#[test]
fn underdetermined_dense_core_reports_witness_in_natural_unsolved_order() {
let decoder = InactivationDecoder::new(8, 16, 5121);
let params = decoder.params().clone();
let mut state = make_reordered_underdetermined_dense_core_state(¶ms, 16, 3, 7, 9);
let err = decoder.inactivate_and_solve(&mut state).unwrap_err();
assert_eq!(
err,
DecodeError::SingularMatrix { row: 9 },
"early underdetermined failure should report a witness the proof trace can explain"
);
}
#[test]
fn singular_matrix_with_proof_keeps_deterministic_attempt_history() {
let decoder = InactivationDecoder::new(8, 16, 321);
let params = decoder.params().clone();
let mut state = make_rank_deficient_state(¶ms, 16, 3, 7);
let mut trace = EliminationTrace::default();
let err = decoder
.inactivate_and_solve_with_proof(&mut state, &mut trace)
.unwrap_err();
assert_eq!(err, DecodeError::SingularMatrix { row: 7 });
assert_eq!(
trace
.pivot_events
.iter()
.map(|ev| ev.col)
.collect::<Vec<_>>(),
vec![3],
"pivot history should be deterministic across rank-deficient failure"
);
}
#[test]
fn underdetermined_dense_core_with_proof_reports_singular_error() {
let decoder = InactivationDecoder::new(8, 16, 513);
let params = decoder.params().clone();
let mut state = make_underdetermined_dense_core_state(¶ms, 16, 3, 7);
let mut trace = EliminationTrace::default();
let err = decoder
.inactivate_and_solve_with_proof(&mut state, &mut trace)
.unwrap_err();
assert_eq!(
err,
DecodeError::SingularMatrix { row: 7 },
"proof path should keep the same rank-deficiency classification"
);
assert!(
trace.pivot_events.is_empty(),
"no pivots should be recorded when the dense core is underdetermined up front"
);
assert_eq!(
trace.inactive_cols,
vec![3, 7],
"proof trace should preserve deterministic inactivation order"
);
assert_eq!(trace.inactivated, 2);
}
#[test]
fn underdetermined_dense_core_with_proof_keeps_witness_in_inactive_col_order() {
let decoder = InactivationDecoder::new(8, 16, 5131);
let params = decoder.params().clone();
let mut state = make_reordered_underdetermined_dense_core_state(¶ms, 16, 3, 7, 9);
let mut trace = EliminationTrace::default();
let err = decoder
.inactivate_and_solve_with_proof(&mut state, &mut trace)
.unwrap_err();
assert_eq!(err, DecodeError::SingularMatrix { row: 9 });
assert_eq!(
trace.inactive_cols,
vec![3, 7, 9],
"proof trace should expose the same column ordering used for the underdetermined witness"
);
assert!(trace.pivot_events.is_empty());
}
#[test]
fn underdetermined_dense_core_with_proof_resets_stale_trace_state() {
let decoder = InactivationDecoder::new(8, 16, 514);
let params = decoder.params().clone();
let mut state = make_underdetermined_dense_core_state(¶ms, 16, 3, 7);
let mut trace = EliminationTrace::default();
trace.set_strategy(InactivationStrategy::BlockSchurLowRank);
trace.record_inactivation(99);
trace.record_pivot(88, 0);
trace.record_row_op();
let err = decoder
.inactivate_and_solve_with_proof(&mut state, &mut trace)
.unwrap_err();
assert_eq!(err, DecodeError::SingularMatrix { row: 7 });
assert_eq!(
trace.strategy,
InactivationStrategy::AllAtOnce,
"proof helper should not leak prior strategy state into a new decode"
);
assert_eq!(
trace.inactive_cols,
vec![3, 7],
"only the current decode's inactivations should remain in the trace"
);
assert_eq!(trace.inactivated, 2);
assert!(
trace.pivot_events.is_empty(),
"stale pivot events must be cleared before recording a new decode"
);
assert_eq!(trace.row_ops, 0);
assert!(
trace.strategy_transitions.is_empty(),
"stale strategy transitions must not leak across proof invocations"
);
assert!(!trace.inactive_cols_truncated);
assert!(!trace.pivot_events_truncated);
assert!(!trace.strategy_transitions_truncated);
}
#[test]
fn proof_and_non_proof_dense_core_fallback_stats_stay_aligned() {
let decoder = InactivationDecoder::new(8, 16, 514);
let params = decoder.params().clone();
let mut plain_state = make_underdetermined_dense_core_state(¶ms, 16, 3, 7);
let plain_err = decoder.inactivate_and_solve(&mut plain_state).unwrap_err();
let mut proof_state = make_underdetermined_dense_core_state(¶ms, 16, 3, 7);
let mut trace = EliminationTrace::default();
let proof_err = decoder
.inactivate_and_solve_with_proof(&mut proof_state, &mut trace)
.unwrap_err();
assert_eq!(plain_err, proof_err);
assert_eq!(
plain_state.stats.peeling_fallback_reason,
Some("peeling_exhausted_to_dense_core")
);
assert_eq!(
proof_state.stats.peeling_fallback_reason, plain_state.stats.peeling_fallback_reason,
"proof capture must not change dense-core fallback telemetry"
);
}
#[test]
fn underdetermined_dense_failure_restores_decoder_state_before_rhs_take() {
let decoder = InactivationDecoder::new(8, 16, 3200);
let params = decoder.params().clone();
let mut state = make_underdetermined_dense_core_state(¶ms, 16, 3, 7);
let initial_rhs = state.rhs.clone();
let initial_active = state.active_cols.clone();
let err = decoder.inactivate_and_solve(&mut state).unwrap_err();
assert_eq!(err, DecodeError::SingularMatrix { row: 7 });
assert_eq!(state.rhs, initial_rhs);
assert_eq!(state.active_cols, initial_active);
assert!(state.inactive_cols.is_empty());
}
#[test]
fn underdetermined_dense_failure_with_proof_restores_decoder_state_before_rhs_take() {
let decoder = InactivationDecoder::new(8, 16, 3200);
let params = decoder.params().clone();
let mut state = make_underdetermined_dense_core_state(¶ms, 16, 3, 7);
let initial_rhs = state.rhs.clone();
let initial_active = state.active_cols.clone();
let mut trace = EliminationTrace::default();
let err = decoder
.inactivate_and_solve_with_proof(&mut state, &mut trace)
.unwrap_err();
assert_eq!(err, DecodeError::SingularMatrix { row: 7 });
assert_eq!(state.rhs, initial_rhs);
assert_eq!(state.active_cols, initial_active);
assert!(state.inactive_cols.is_empty());
assert_eq!(trace.inactive_cols, vec![3, 7]);
}
#[test]
fn dense_failure_restores_decoder_state_after_early_termination() {
let decoder = InactivationDecoder::new(8, 16, 3201);
let params = decoder.params().clone();
let mut state = make_rank_deficient_state(¶ms, 16, 3, 7);
let initial_rhs = state.rhs.clone();
let initial_active = state.active_cols.clone();
let err = decoder.inactivate_and_solve(&mut state).unwrap_err();
assert_eq!(err, DecodeError::SingularMatrix { row: 7 });
assert_eq!(
state.rhs, initial_rhs,
"dense-failure cleanup must restore RHS rows taken into the dense core"
);
assert_eq!(
state.active_cols, initial_active,
"dense-failure cleanup must reactivate unsolved columns for postmortem inspection"
);
assert!(
state.inactive_cols.is_empty(),
"dense-failure cleanup must not leak inactive-column bookkeeping"
);
}
#[test]
fn dense_failure_with_proof_restores_decoder_state_after_early_termination() {
let decoder = InactivationDecoder::new(8, 16, 3202);
let params = decoder.params().clone();
let mut state = make_rank_deficient_state(¶ms, 16, 3, 7);
let initial_rhs = state.rhs.clone();
let initial_active = state.active_cols.clone();
let mut trace = EliminationTrace::default();
let err = decoder
.inactivate_and_solve_with_proof(&mut state, &mut trace)
.unwrap_err();
assert_eq!(err, DecodeError::SingularMatrix { row: 7 });
assert_eq!(
state.rhs, initial_rhs,
"proof dense-failure cleanup must restore RHS rows taken into the dense core"
);
assert_eq!(
state.active_cols, initial_active,
"proof dense-failure cleanup must reactivate unsolved columns for postmortem inspection"
);
assert!(
state.inactive_cols.is_empty(),
"proof dense-failure cleanup must not leak inactive-column bookkeeping"
);
}
#[test]
fn dense_core_rhs_width_drift_fails_closed_before_plain_snapshot() {
let decoder = InactivationDecoder::new(8, 16, 3203);
let params = decoder.params().clone();
let mut state = make_rank_deficient_state(¶ms, 16, 3, 7);
let initial_rhs = state.rhs.clone();
let initial_active = state.active_cols.clone();
state.rhs[0].truncate(15);
let err = decoder.inactivate_and_solve(&mut state).unwrap_err();
assert_eq!(
err,
DecodeError::SymbolSizeMismatch {
expected: 16,
actual: 15,
},
"dense-core RHS width drift must fail closed instead of panicking during snapshot"
);
assert_eq!(state.rhs[0].len(), 15);
assert_eq!(state.rhs[1], initial_rhs[1]);
assert_eq!(state.active_cols, initial_active);
assert!(state.inactive_cols.is_empty());
}
#[test]
fn dense_core_rhs_width_drift_fails_closed_before_proof_snapshot() {
let decoder = InactivationDecoder::new(8, 16, 3204);
let params = decoder.params().clone();
let mut state = make_rank_deficient_state(¶ms, 16, 3, 7);
let initial_rhs = state.rhs.clone();
let initial_active = state.active_cols.clone();
let mut trace = EliminationTrace::default();
state.rhs[0].truncate(15);
let err = decoder
.inactivate_and_solve_with_proof(&mut state, &mut trace)
.unwrap_err();
assert_eq!(
err,
DecodeError::SymbolSizeMismatch {
expected: 16,
actual: 15,
},
"proof dense-core RHS width drift must fail closed instead of panicking during snapshot"
);
assert_eq!(state.rhs[0].len(), 15);
assert_eq!(state.rhs[1], initial_rhs[1]);
assert_eq!(state.active_cols, initial_active);
assert!(state.inactive_cols.is_empty());
assert_eq!(trace.inactive_cols, vec![3, 7]);
assert_eq!(trace.inactivated, 2);
assert!(trace.pivot_events.is_empty());
}
#[test]
fn failure_reason_captures_attempted_pivot_columns() {
let mut elimination = EliminationTrace::default();
elimination.record_pivot(3, 0);
elimination.record_pivot(9, 1);
let reason =
failure_reason_with_trace(&DecodeError::SingularMatrix { row: 11 }, &elimination);
assert_eq!(
reason,
FailureReason::SingularMatrix {
row: 11,
attempted_cols: vec![3, 9],
}
);
}
#[test]
fn pivot_tie_break_prefers_lowest_available_row_deterministically() {
let decoder = InactivationDecoder::new(8, 1, 999);
let params = decoder.params().clone();
let mut state_one = make_pivot_tie_break_state(¶ms, 1, 3, 7);
let mut trace_one = EliminationTrace::default();
decoder
.inactivate_and_solve_with_proof(&mut state_one, &mut trace_one)
.expect("tie-break test state should be solvable");
assert_eq!(
trace_one
.pivot_events
.iter()
.map(|ev| (ev.col, ev.row))
.collect::<Vec<_>>(),
vec![(3, 0), (7, 1)],
"pivot order should be deterministic and prefer lowest available row"
);
assert_eq!(state_one.solved[3], Some(vec![0x10]));
assert_eq!(state_one.solved[7], Some(vec![0x20]));
let mut state_two = make_pivot_tie_break_state(¶ms, 1, 3, 7);
let mut trace_two = EliminationTrace::default();
decoder
.inactivate_and_solve_with_proof(&mut state_two, &mut trace_two)
.expect("second solve should match first solve");
assert_eq!(
trace_one.pivot_events, trace_two.pivot_events,
"pivot trace should be stable across repeated runs"
);
}
#[test]
fn inconsistent_overdetermined_system_reports_singular_error() {
let decoder = InactivationDecoder::new(8, 16, 111);
let params = decoder.params().clone();
let mut state = make_inconsistent_overdetermined_state(¶ms, 16, 3, 7);
let err = decoder.inactivate_and_solve(&mut state).unwrap_err();
assert_eq!(
err,
DecodeError::SingularMatrix { row: 2 },
"contradictory overdetermined system should fail deterministically at witness row"
);
}
#[test]
fn inconsistent_overdetermined_with_proof_preserves_attempt_history() {
let decoder = InactivationDecoder::new(8, 16, 222);
let params = decoder.params().clone();
let mut state = make_inconsistent_overdetermined_state(¶ms, 16, 3, 7);
let mut trace = EliminationTrace::default();
let err = decoder
.inactivate_and_solve_with_proof(&mut state, &mut trace)
.unwrap_err();
assert_eq!(err, DecodeError::SingularMatrix { row: 2 });
assert_eq!(
trace
.pivot_events
.iter()
.map(|ev| ev.col)
.collect::<Vec<_>>(),
vec![3, 7],
"inconsistent-system witness should preserve full pivot-attempt history"
);
}
#[test]
fn dense_core_extraction_drops_redundant_zero_rows() {
let decoder = InactivationDecoder::new(8, 16, 6060);
let params = decoder.params().clone();
let mut state = make_dense_core_prunable_state(¶ms, 16, 3, 7, 0x00);
decoder
.inactivate_and_solve(&mut state)
.expect("state with redundant zero row should be solvable");
assert_eq!(
state.stats.dense_core_rows, 2,
"dense core should only include rows with unsolved-column signal"
);
assert_eq!(
state.stats.dense_core_cols, 2,
"dense core should preserve unsolved column width"
);
assert_eq!(
state.stats.dense_core_dropped_rows, 1,
"one redundant zero-information row should be dropped"
);
}
#[test]
fn dense_core_inconsistent_constant_row_reports_equation_witness() {
let decoder = InactivationDecoder::new(8, 16, 6161);
let params = decoder.params().clone();
let mut state = make_dense_core_prunable_state(¶ms, 16, 3, 7, 0x01);
let err = decoder.inactivate_and_solve(&mut state).unwrap_err();
assert_eq!(
err,
DecodeError::SingularMatrix { row: 2 },
"inconsistent constant row should report deterministic original equation index"
);
}
#[test]
fn baseline_failure_triggers_deterministic_hard_regime_fallback() {
let decoder = InactivationDecoder::new(8, 1, 4242);
let params = decoder.params().clone();
let mut state = make_rank_deficient_state(¶ms, 1, 3, 7);
let err = decoder.inactivate_and_solve(&mut state).unwrap_err();
assert_eq!(err, DecodeError::SingularMatrix { row: 7 });
assert!(
state.stats.hard_regime_activated,
"fallback should activate hard regime deterministically"
);
assert_eq!(
state.stats.hard_regime_fallbacks, 1,
"exactly one fallback transition is expected"
);
assert!(
state.stats.markowitz_pivots <= state.stats.pivots_selected,
"hard-regime pivot accounting should remain internally consistent"
);
}
#[test]
fn proof_trace_records_fallback_transition_reason() {
let decoder = InactivationDecoder::new(8, 1, 4343);
let params = decoder.params().clone();
let mut state = make_rank_deficient_state(¶ms, 1, 3, 7);
let mut trace = EliminationTrace::default();
let err = decoder
.inactivate_and_solve_with_proof(&mut state, &mut trace)
.unwrap_err();
assert_eq!(err, DecodeError::SingularMatrix { row: 7 });
assert_eq!(
trace.strategy,
InactivationStrategy::HighSupportFirst,
"proof trace should expose fallback strategy"
);
assert_eq!(
trace.strategy_transitions.len(),
1,
"fallback should record one strategy transition"
);
assert_eq!(
trace.strategy_transitions[0].reason, "fallback_after_baseline_failure",
"transition reason should be deterministic and triage-friendly"
);
assert_eq!(
trace
.pivot_events
.iter()
.map(|ev| ev.col)
.collect::<Vec<_>>(),
vec![3],
"fallback proof should preserve the deterministic pivot-attempt witness"
);
}
#[test]
fn hard_regime_activation_is_deterministic_and_observable() {
set_test_bypass_governance(true);
let decoder = InactivationDecoder::new(8, 1, 3030);
let params = decoder.params().clone();
let mut state_one = make_hard_regime_dense_state(¶ms, 1, 4, 8);
let mut trace_one = EliminationTrace::default();
decoder
.inactivate_and_solve_with_proof(&mut state_one, &mut trace_one)
.expect("hard regime state should be solvable");
assert!(
state_one.stats.hard_regime_activated,
"hard-regime transition should be observable in decode stats"
);
assert_eq!(
state_one.stats.markowitz_pivots, 8,
"all hard-regime pivots should use deterministic Markowitz selector"
);
assert_eq!(
trace_one.strategy,
InactivationStrategy::HighSupportFirst,
"proof trace must expose hard-regime strategy"
);
assert_eq!(
trace_one.strategy_transitions.len(),
1,
"hard regime should record a single strategy transition"
);
assert_eq!(
trace_one.strategy_transitions[0].reason, "dense_or_near_square",
"transition reason should be deterministic and triage-friendly"
);
let mut state_two = make_hard_regime_dense_state(¶ms, 1, 4, 8);
let mut trace_two = EliminationTrace::default();
decoder
.inactivate_and_solve_with_proof(&mut state_two, &mut trace_two)
.expect("repeated hard regime solve should succeed");
assert_eq!(
state_one.stats.markowitz_pivots, state_two.stats.markowitz_pivots,
"hard-regime pivot counts should be stable across runs"
);
assert_eq!(
trace_one.pivot_events, trace_two.pivot_events,
"hard-regime pivot event ordering must be deterministic"
);
assert_eq!(
trace_one.strategy_transitions, trace_two.strategy_transitions,
"hard-regime strategy transition history must be deterministic"
);
}
#[test]
fn hard_regime_plan_selects_block_schur_for_dense_large_core() {
let n_rows = 12;
let n_cols = 12;
let dense = vec![Gf256::ONE; n_rows * n_cols];
let plan = select_hard_regime_plan(n_rows, n_cols, &dense);
assert_eq!(
plan,
HardRegimePlan::BlockSchurLowRank { split_col: 8 },
"dense 12x12 system should select deterministic block-schur plan"
);
}
#[test]
fn block_schur_failure_falls_back_to_markowitz_with_reason() {
set_test_bypass_governance(true);
let decoder = InactivationDecoder::new(32, 1, 7070);
let params = decoder.params().clone();
let mut state = make_block_schur_rank_deficient_state(¶ms, 1, 4, 12);
let mut trace = EliminationTrace::default();
let err = decoder
.inactivate_and_solve_with_proof(&mut state, &mut trace)
.expect_err("rank-deficient block-schur candidate should fail deterministically");
assert!(matches!(err, DecodeError::SingularMatrix { .. }));
assert!(
state.stats.hard_regime_activated,
"dense rank-deficient system should activate hard regime"
);
assert_eq!(
state.stats.hard_regime_branch,
Some("block_schur_low_rank"),
"stats should expose deterministic accelerated branch selection"
);
assert_eq!(
state.stats.hard_regime_conservative_fallback_reason,
Some("block_schur_failed_to_converge"),
"stats should expose deterministic conservative fallback reason"
);
assert_eq!(
state.stats.hard_regime_fallbacks, 1,
"block-schur attempt should perform exactly one conservative fallback"
);
assert!(
trace.strategy_transitions.iter().any(|transition| {
transition.from == InactivationStrategy::BlockSchurLowRank
&& transition.to == InactivationStrategy::HighSupportFirst
&& transition.reason == "block_schur_failed_to_converge"
}),
"proof trace should record deterministic branch fallback transition"
);
}
#[test]
fn block_schur_fallback_preserves_non_pivot_truncation_flags() {
set_test_bypass_governance(true);
let decoder = InactivationDecoder::new(300, 1, 7171);
let params = decoder.params().clone();
let mut state = make_block_schur_rank_deficient_state(
¶ms,
1,
4,
crate::raptorq::proof::MAX_PIVOT_EVENTS + 1,
);
let mut trace = EliminationTrace::default();
let err = decoder
.inactivate_and_solve_with_proof(&mut state, &mut trace)
.expect_err("wide rank-deficient block-schur candidate should fail deterministically");
assert!(matches!(err, DecodeError::SingularMatrix { .. }));
assert!(
trace.inactive_cols_truncated,
"large inactivation witness must stay marked truncated after fallback cleanup"
);
assert!(
!trace.pivot_events_truncated,
"clearing pivot history for retry should only reset the pivot-events truncation flag"
);
assert!(
!trace.strategy_transitions_truncated,
"single fallback transition should remain non-truncated"
);
}
#[test]
fn normal_regime_keeps_basic_pivot_strategy() {
let decoder = InactivationDecoder::new(8, 1, 99);
let params = decoder.params().clone();
let mut state = make_pivot_tie_break_state(¶ms, 1, 3, 7);
decoder
.inactivate_and_solve(&mut state)
.expect("normal regime test state should solve");
assert!(
!state.stats.hard_regime_activated,
"small systems should stay on the baseline inactivation strategy"
);
assert_eq!(
state.stats.markowitz_pivots, 0,
"baseline strategy should not report Markowitz pivots"
);
}
#[test]
fn normal_regime_proof_trace_keeps_all_at_once_strategy() {
let decoder = InactivationDecoder::new(8, 1, 100);
let params = decoder.params().clone();
let mut state = make_pivot_tie_break_state(¶ms, 1, 3, 7);
let mut trace = EliminationTrace::default();
decoder
.inactivate_and_solve_with_proof(&mut state, &mut trace)
.expect("normal regime proof solve should succeed");
assert_eq!(
trace.strategy,
InactivationStrategy::AllAtOnce,
"normal regime should stay on baseline strategy"
);
assert!(
trace.strategy_transitions.is_empty(),
"normal regime must not emit strategy transitions"
);
}
#[test]
fn policy_metadata_is_recorded_for_conservative_mode() {
let decoder = InactivationDecoder::new(8, 1, 101);
let params = decoder.params().clone();
let mut state = make_pivot_tie_break_state(¶ms, 1, 3, 7);
decoder
.inactivate_and_solve(&mut state)
.expect("conservative-mode state should solve");
assert_eq!(state.stats.policy_mode, Some("conservative_baseline"));
assert_eq!(
state.stats.policy_reason,
Some("expected_loss_conservative_gate")
);
assert_eq!(state.stats.policy_replay_ref, Some(POLICY_REPLAY_REF));
assert!(state.stats.policy_baseline_loss > 0);
assert!(state.stats.policy_high_support_loss > 0);
let governance = state
.stats
.governance
.expect("governance telemetry must be recorded");
assert_eq!(
governance.replay_ref,
decision_contract::G7_DECISION_REPLAY_REF
);
assert_eq!(
governance
.state_posterior_permille
.iter()
.map(|&value| u32::from(value))
.sum::<u32>(),
1000
);
}
#[test]
fn policy_metadata_is_recorded_for_aggressive_mode() {
set_test_bypass_governance(true);
let decoder = InactivationDecoder::new(32, 1, 8080);
let params = decoder.params().clone();
let mut state = make_block_schur_rank_deficient_state(¶ms, 1, 2, 15);
let _err = decoder.inactivate_and_solve(&mut state).unwrap_err();
assert!(
matches!(
state.stats.policy_mode,
Some("high_support_first" | "block_schur_low_rank")
),
"dense state should log an aggressive policy mode"
);
assert_eq!(state.stats.policy_reason, Some("expected_loss_minimum"));
assert_eq!(state.stats.policy_replay_ref, Some(POLICY_REPLAY_REF));
assert!(state.stats.policy_density_permille >= 350);
let governance = state
.stats
.governance
.expect("governance telemetry must be recorded");
assert!(matches!(
governance.chosen_action,
"continue" | "canary_hold" | "rollback" | "fallback"
));
assert!(governance.confidence_score <= 1000);
}
#[test]
fn decoder_policy_budget_exhaustion_forces_conservative_baseline() {
let n_rows = 65;
let n_cols = 65;
let dense = vec![Gf256::ONE; n_rows * n_cols];
let decision = choose_runtime_decoder_policy(n_rows, n_cols, dense.len(), 0, 700);
assert_eq!(decision.mode, DecoderPolicyMode::ConservativeBaseline);
assert_eq!(decision.reason, "policy_budget_exhausted_conservative");
assert!(decision.features.budget_exhausted);
assert!(
decision
.governance
.is_some_and(|telemetry| telemetry.deterministic_fallback_triggered)
);
}
#[test]
fn decoder_policy_prefers_aggressive_strategy_for_dense_high_pressure() {
set_test_bypass_governance(true);
let n_rows = 16;
let n_cols = 16;
let dense = vec![Gf256::ONE; n_rows * n_cols];
let decision = choose_runtime_decoder_policy(n_rows, n_cols, dense.len(), 0, 850);
assert!(
matches!(
decision.mode,
DecoderPolicyMode::HighSupportFirst | DecoderPolicyMode::BlockSchurLowRank
),
"dense/high-pressure matrix should avoid conservative baseline"
);
}
#[test]
fn decoder_policy_prefers_conservative_for_sparse_low_pressure() {
let n_rows = 24;
let n_cols = 16;
let mut sparse = vec![Gf256::ZERO; n_rows * n_cols];
for idx in 0..n_cols {
sparse[idx * n_cols + idx] = Gf256::ONE;
}
let one = choose_runtime_decoder_policy(n_rows, n_cols, n_cols, 0, 40);
let two = choose_runtime_decoder_policy(n_rows, n_cols, n_cols, 0, 40);
assert_eq!(one, two, "policy decision should be deterministic");
assert_eq!(one.mode, DecoderPolicyMode::ConservativeBaseline);
assert_eq!(one.reason, "expected_loss_conservative_gate");
assert!(
one.governance.is_some(),
"G7 governance telemetry must be present"
);
}
#[test]
fn all_source_equations_returns_identity_map() {
let k = 8;
let decoder = InactivationDecoder::new(k, 32, 42);
let equations = decoder.all_source_equations();
assert_eq!(equations.len(), k, "should return exactly K equations");
for (i, (cols, coefs)) in equations.iter().enumerate() {
assert_eq!(cols, &[i], "source equation {i} should map to column {i}");
assert_eq!(
coefs,
&[Gf256::ONE],
"source equation {i} should have unit coefficient"
);
}
}
#[test]
fn source_equation_matches_all_source_equations() {
let k = 12;
let decoder = InactivationDecoder::new(k, 16, 99);
let all = decoder.all_source_equations();
for esi in 0..k as u32 {
let single = decoder.source_equation(esi);
assert_eq!(
single, all[esi as usize],
"source_equation({esi}) must match all_source_equations()[{esi}]"
);
}
}
#[test]
#[should_panic(expected = "source ESI must be < K")]
fn source_equation_panics_on_esi_ge_k() {
let k = 4;
let decoder = InactivationDecoder::new(k, 16, 42);
let _ = decoder.source_equation(k as u32); }
#[test]
fn decode_with_duplicate_source_esi_produces_defined_outcome() {
let k = 8;
let symbol_size = 32;
let seed = 42u64;
let source: Vec<Vec<u8>> = (0..k)
.map(|i| {
(0..symbol_size)
.map(|j| ((i * 37 + j * 13 + 7) % 256) as u8)
.collect()
})
.collect();
let encoder = SystematicEncoder::new(&source, symbol_size, seed).unwrap();
let decoder = InactivationDecoder::new(k, symbol_size, seed);
let l = decoder.params().l;
let mut received = decoder.constraint_symbols();
for (i, data) in source.iter().enumerate() {
received.push(ReceivedSymbol::source(i as u32, data.clone()));
}
received.push(ReceivedSymbol::source(0, source[0].clone()));
for esi in (k as u32)..(l as u32) {
let (cols, coefs) = decoder.repair_equation(esi);
let repair_data = encoder.repair_symbol(esi);
received.push(ReceivedSymbol::repair(esi, cols, coefs, repair_data));
}
let result = decoder.decode(&received);
match result {
Ok(decoded_symbols) => {
assert_eq!(
decoded_symbols.source, source,
"decode with duplicate ESI should recover correct source"
);
}
Err(e) => {
assert!(
matches!(e, DecodeError::SingularMatrix { .. }),
"unexpected error type with duplicate ESI: {e:?}"
);
}
}
}
#[test]
fn decode_all_zeros_source_data() {
let k = 8;
let symbol_size = 32;
let seed = 42u64;
let source: Vec<Vec<u8>> = (0..k).map(|_| vec![0u8; symbol_size]).collect();
let encoder = SystematicEncoder::new(&source, symbol_size, seed).unwrap();
let decoder = InactivationDecoder::new(k, symbol_size, seed);
let l = decoder.params().l;
let mut received = decoder.constraint_symbols();
for (i, data) in source.iter().enumerate() {
received.push(ReceivedSymbol::source(i as u32, data.clone()));
}
for esi in (k as u32)..(l as u32) {
let (cols, coefs) = decoder.repair_equation(esi);
let repair_data = encoder.repair_symbol(esi);
received.push(ReceivedSymbol::repair(esi, cols, coefs, repair_data));
}
let result = decoder
.decode(&received)
.expect("all-zeros source should decode");
assert_eq!(result.source, source, "decoded all-zeros must match");
}
#[test]
fn intermediate_symbols_match_encoder_after_decode() {
let k = 8;
let symbol_size = 32;
let seed = 42u64;
let source: Vec<Vec<u8>> = (0..k)
.map(|i| {
(0..symbol_size)
.map(|j| ((i * 37 + j * 13 + 7) % 256) as u8)
.collect()
})
.collect();
let encoder = SystematicEncoder::new(&source, symbol_size, seed).unwrap();
let decoder = InactivationDecoder::new(k, symbol_size, seed);
let l = decoder.params().l;
let mut received = decoder.constraint_symbols();
for (i, data) in source.iter().enumerate() {
received.push(ReceivedSymbol::source(i as u32, data.clone()));
}
for esi in (k as u32)..(l as u32) {
let (cols, coefs) = decoder.repair_equation(esi);
let repair_data = encoder.repair_symbol(esi);
received.push(ReceivedSymbol::repair(esi, cols, coefs, repair_data));
}
let result = decoder.decode(&received).expect("decode should succeed");
assert_eq!(result.intermediate.len(), l);
for i in 0..l {
assert_eq!(
result.intermediate[i],
encoder.intermediate_symbol(i),
"intermediate symbol {i}/{l} must match encoder"
);
}
}
#[test]
fn stats_peeled_plus_inactivated_covers_all_columns() {
let k = 8;
let symbol_size = 32;
let seed = 42u64;
let source: Vec<Vec<u8>> = (0..k)
.map(|i| {
(0..symbol_size)
.map(|j| ((i * 37 + j * 13 + 7) % 256) as u8)
.collect()
})
.collect();
let encoder = SystematicEncoder::new(&source, symbol_size, seed).unwrap();
let decoder = InactivationDecoder::new(k, symbol_size, seed);
let l = decoder.params().l;
let mut received = decoder.constraint_symbols();
for (i, data) in source.iter().enumerate() {
received.push(ReceivedSymbol::source(i as u32, data.clone()));
}
for esi in (k as u32)..(l as u32) {
let (cols, coefs) = decoder.repair_equation(esi);
let repair_data = encoder.repair_symbol(esi);
received.push(ReceivedSymbol::repair(esi, cols, coefs, repair_data));
}
let result = decoder.decode(&received).expect("decode should succeed");
assert_eq!(
result.stats.peeled + result.stats.inactivated,
l,
"peeled + inactivated must equal L ({l})"
);
}
#[test]
fn wavefront_decode_matches_sequential() {
let k = 16;
let symbol_size = 64;
let seed = 0xF8_0001u64;
let source = make_source_data(k, symbol_size);
let encoder = SystematicEncoder::new(&source, symbol_size, seed).unwrap();
let decoder = InactivationDecoder::new(k, symbol_size, seed);
let mut received = decoder.constraint_symbols();
for esi in 0..(k as u32) {
received.push(ReceivedSymbol::source(esi, source[esi as usize].clone()));
}
for esi in (k as u32)..(k as u32 + 4) {
let (cols, coefs) = decoder.repair_equation(esi);
let repair_data = encoder.repair_symbol(esi);
received.push(ReceivedSymbol::repair(esi, cols, coefs, repair_data));
}
let sequential = decoder.decode(&received).expect("sequential decode");
for batch_size in [1, 2, 4, 8, 16, 0] {
let wavefront = decoder
.decode_wavefront(&received, batch_size)
.unwrap_or_else(|_| panic!("wavefront decode batch_size={batch_size}"));
for (i, (seq_sym, wf_sym)) in sequential
.source
.iter()
.zip(wavefront.source.iter())
.enumerate()
{
assert_eq!(
seq_sym, wf_sym,
"source symbol {i} mismatch at batch_size={batch_size}"
);
}
assert!(wavefront.stats.wavefront_active);
assert_eq!(
wavefront.stats.wavefront_batch_size,
if batch_size == 0 {
received.len()
} else {
batch_size
}
);
assert!(wavefront.stats.wavefront_batches > 0);
}
}
#[test]
fn wavefront_decode_with_loss_matches_sequential() {
let k = 8;
let symbol_size = 32;
let seed = 0xF8_0002u64;
let source = make_source_data(k, symbol_size);
let encoder = SystematicEncoder::new(&source, symbol_size, seed).unwrap();
let decoder = InactivationDecoder::new(k, symbol_size, seed);
let l = decoder.params().l;
let mut received = decoder.constraint_symbols();
for esi in (k as u32)..(k as u32 + l as u32) {
let (cols, coefs) = decoder.repair_equation(esi);
let repair_data = encoder.repair_symbol(esi);
received.push(ReceivedSymbol::repair(esi, cols, coefs, repair_data));
}
let sequential = decoder.decode(&received).expect("sequential decode");
for batch_size in [1, 4, 8] {
let wavefront = decoder
.decode_wavefront(&received, batch_size)
.unwrap_or_else(|_| panic!("wavefront batch_size={batch_size}"));
for (i, (seq_sym, wf_sym)) in sequential
.source
.iter()
.zip(wavefront.source.iter())
.enumerate()
{
assert_eq!(
seq_sym, wf_sym,
"source symbol {i} mismatch at batch_size={batch_size} (repair-only)"
);
}
}
}
#[test]
fn wavefront_overlap_peeling_is_tracked() {
let k = 16;
let symbol_size = 64;
let seed = 0xF8_0003u64;
let source = make_source_data(k, symbol_size);
let encoder = SystematicEncoder::new(&source, symbol_size, seed).unwrap();
let decoder = InactivationDecoder::new(k, symbol_size, seed);
let mut received = decoder.constraint_symbols();
for esi in 0..(k as u32) {
received.push(ReceivedSymbol::source(esi, source[esi as usize].clone()));
}
for esi in (k as u32)..(k as u32 + 4) {
let (cols, coefs) = decoder.repair_equation(esi);
let repair_data = encoder.repair_symbol(esi);
received.push(ReceivedSymbol::repair(esi, cols, coefs, repair_data));
}
let wavefront = decoder
.decode_wavefront(&received, 1)
.expect("wavefront batch_size=1");
assert!(wavefront.stats.wavefront_active);
assert_eq!(wavefront.stats.wavefront_batch_size, 1);
assert_eq!(wavefront.stats.wavefront_batches, received.len());
assert!(
wavefront.stats.peeled > 0,
"some symbols should peel in wavefront mode"
);
}
#[test]
fn wavefront_sequential_fallback_batch_zero() {
let k = 8;
let symbol_size = 32;
let seed = 0xF8_0004u64;
let source = make_source_data(k, symbol_size);
let encoder = SystematicEncoder::new(&source, symbol_size, seed).unwrap();
let decoder = InactivationDecoder::new(k, symbol_size, seed);
let mut received = decoder.constraint_symbols();
for esi in 0..(k as u32) {
received.push(ReceivedSymbol::source(esi, source[esi as usize].clone()));
}
for esi in (k as u32)..(k as u32 + 2) {
let (cols, coefs) = decoder.repair_equation(esi);
let repair_data = encoder.repair_symbol(esi);
received.push(ReceivedSymbol::repair(esi, cols, coefs, repair_data));
}
let sequential = decoder.decode(&received).expect("sequential");
let wavefront = decoder
.decode_wavefront(&received, 0)
.expect("wavefront batch_size=0");
assert_eq!(sequential.source, wavefront.source);
assert!(wavefront.stats.wavefront_active);
assert_eq!(wavefront.stats.wavefront_batches, 1);
assert_eq!(wavefront.stats.wavefront_batch_size, received.len());
}
}