use super::fading_tables::{GAUSS_LEN, GAUSS_TAPS, LORENTZ_LEN, LORENTZ_TAPS};
use super::pdmath;
use super::{QraCode, QraCodeType};
const TS_QRA64: f32 = 0.576;
const MAX_SPREAD_HZ: f32 = 240.0;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum FadingModel {
Gaussian,
Lorentzian,
}
#[derive(Clone, Debug)]
pub struct FastFadingState {
pub es_no_metric: f32,
pub noise_var: f32,
pub n_bins_per_tone: usize,
pub n_bins_per_symbol: usize,
pub weights: Vec<f32>,
}
fn validate_submode(submode: u8) -> u8 {
assert!(
submode <= 4,
"fast-fading submode must be 0..=4 (got {submode})"
);
submode
}
fn bins_per_tone(submode: u8) -> usize {
1usize << validate_submode(submode)
}
fn table_index(b90_ts: f32) -> usize {
let b90 = b90_ts / TS_QRA64;
let raw = (b90.ln() / 1.09_f32.ln() - 0.499).floor() as i32;
raw.clamp(0, 63) as usize
}
fn punctured_codeword_len(code: &QraCode) -> usize {
match code.code_type {
QraCodeType::Normal | QraCodeType::Crc => code.N,
QraCodeType::CrcPunctured => code.N - 1,
QraCodeType::CrcPunctured2 => code.N - 2,
}
}
pub fn intrinsics_fast_fading(
code: &QraCode,
intrinsics: &mut [f32],
energies_wide: &[f32],
submode: u8,
b90_ts: f32,
model: FadingModel,
decoder_es_no_metric: f32,
) -> FastFadingState {
let big_m = code.M;
let n_n = punctured_codeword_len(code);
let bpt = bins_per_tone(submode);
let bins_per_symbol = big_m * (2 + bpt);
let total_bins = n_n * bins_per_symbol;
assert_eq!(
intrinsics.len(),
big_m * n_n,
"intrinsics_fast_fading: intrinsics buffer must be M*N"
);
assert_eq!(
energies_wide.len(),
total_bins,
"intrinsics_fast_fading: energies_wide must be N*M*(2+nBinsPerTone)"
);
let hidx = table_index(b90_ts);
let (hlen, taps) = match model {
FadingModel::Gaussian => (GAUSS_LEN[hidx], GAUSS_TAPS[hidx]),
FadingModel::Lorentzian => (LORENTZ_LEN[hidx], LORENTZ_TAPS[hidx]),
};
debug_assert_eq!(taps.len(), hlen, "fading table length mismatch");
let b90 = b90_ts / TS_QRA64;
let degrade_db = 8.0 * b90.ln() / MAX_SPREAD_HZ.ln();
let es_no_metric = decoder_es_no_metric * 10_f32.powf(degrade_db / 10.0);
let mut noise_var = energies_wide.iter().sum::<f32>() / total_bins as f32;
noise_var /= 1.0 + es_no_metric / bins_per_symbol as f32;
let mut weights = vec![0.0_f32; hlen];
for k in 0..hlen {
let t = taps[k] * es_no_metric;
weights[k] = t / (1.0 + t) / noise_var;
}
let hhsz = hlen - 1; let hlast = 2 * hhsz;
let mut sym_offset = big_m; let mut ix_offset = 0usize;
for _ in 0..n_n {
let first_bin = sym_offset.wrapping_sub(hlen - 1); let row = &mut intrinsics[ix_offset..ix_offset + big_m];
let mut max_logp = 0.0_f32;
for k in 0..big_m {
let centre = first_bin + k * bpt;
let mut acc = 0.0_f32;
for j in 0..hhsz {
acc += weights[j] * (energies_wide[centre + j] + energies_wide[centre + hlast - j]);
}
acc += weights[hhsz] * energies_wide[centre + hhsz];
row[k] = acc;
if acc > max_logp {
max_logp = acc;
}
}
let mut sum = 0.0_f32;
for v in row.iter_mut() {
let x = (*v - max_logp).clamp(-85.0, 85.0);
let e = x.exp();
*v = e;
sum += e;
}
let inv = if sum > 0.0 { 1.0 / sum } else { 0.0 };
for v in row.iter_mut() {
*v *= inv;
}
pdmath::norm(row);
sym_offset += bins_per_symbol;
ix_offset += big_m;
}
FastFadingState {
es_no_metric,
noise_var,
n_bins_per_tone: bpt,
n_bins_per_symbol: bins_per_symbol,
weights,
}
}
pub fn esnodb_fast_fading(
state: &FastFadingState,
code: &QraCode,
decoded_codeword: &[i32],
energies_wide: &[f32],
) -> f32 {
let big_m = code.M;
let n_n = punctured_codeword_len(code);
let bpt = state.n_bins_per_tone;
let bins_per_symbol = state.n_bins_per_symbol;
let n_weights = state.weights.len();
let n_tot_weights = 2 * n_weights - 1;
assert_eq!(
decoded_codeword.len(),
n_n,
"esnodb_fast_fading: decoded codeword must be of punctured length"
);
assert_eq!(
energies_wide.len(),
n_n * bins_per_symbol,
"esnodb_fast_fading: energies_wide length"
);
let mut es_plus_w_no = 0.0_f32;
let mut sym_offset = big_m; for &y in decoded_codeword {
let centre = sym_offset + (y as usize) * bpt;
let first_bin = centre - (n_weights - 1);
for j in 0..n_tot_weights {
es_plus_w_no += energies_wide[first_bin + j];
}
sym_offset += bins_per_symbol;
}
es_plus_w_no /= n_n as f32;
let u = es_plus_w_no / (state.noise_var * (1.0 + state.es_no_metric / bins_per_symbol as f32));
let u_floor = n_tot_weights as f32 + 0.316;
let u = u.max(u_floor);
let es_no = (u - n_tot_weights as f32) / (1.0 - u / bins_per_symbol as f32);
10.0 * es_no.log10()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fec::qra15_65_64::QRA15_65_64_IRR_E23;
#[test]
fn table_index_clamps_to_range() {
assert_eq!(table_index(1e-6), 0);
assert_eq!(table_index(1e6), 63);
}
#[test]
fn table_index_matches_c_reference_pivot_points() {
assert_eq!(table_index(TS_QRA64), 0);
let log109 = 1.09_f32.ln();
let b90_for_5 = (5.0 * log109).exp(); assert_eq!(table_index(TS_QRA64 * b90_for_5), 4);
}
#[test]
fn fading_tables_have_nonzero_lengths_and_match() {
assert_eq!(GAUSS_LEN.len(), 64);
assert_eq!(LORENTZ_LEN.len(), 64);
for i in 0..64 {
assert!(GAUSS_LEN[i] >= 2, "GAUSS_LEN[{i}] suspiciously small");
assert_eq!(
GAUSS_TAPS[i].len(),
GAUSS_LEN[i],
"GAUSS shape mismatch at {i}"
);
assert_eq!(
LORENTZ_TAPS[i].len(),
LORENTZ_LEN[i],
"LORENTZ shape mismatch at {i}"
);
for &tap in GAUSS_TAPS[i] {
assert!(tap.is_finite() && tap >= 0.0, "GAUSS tap negative or NaN");
}
for &tap in LORENTZ_TAPS[i] {
assert!(tap.is_finite() && tap >= 0.0, "LORENTZ tap negative or NaN");
}
}
assert_eq!(GAUSS_LEN[63], 65);
assert_eq!(LORENTZ_LEN[63], 65);
}
fn synthetic_wide_energies(channel: &[i32], submode: u8, peak: f32, eps: f32) -> Vec<f32> {
let big_m = 64;
let bpt = 1usize << submode;
let bins_per_symbol = big_m * (2 + bpt);
let n_n = channel.len();
let mut e = vec![eps; n_n * bins_per_symbol];
for (k, &sym) in channel.iter().enumerate() {
let sym_offset = k * bins_per_symbol + big_m;
let centre = sym_offset + (sym as usize) * bpt;
e[centre] = peak;
}
e
}
#[test]
fn intrinsics_concentrate_on_correct_tone_for_clean_input() {
let code = &QRA15_65_64_IRR_E23;
let n_n = 63usize;
let big_m = 64usize;
let channel: Vec<i32> = (0..n_n as i32).map(|i| (i * 7 + 3) % 64).collect();
let energies = synthetic_wide_energies(&channel, 0, 100.0, 0.01);
let mut intr = vec![0.0_f32; big_m * n_n];
let _state = intrinsics_fast_fading(
code,
&mut intr,
&energies,
0,
0.05,
FadingModel::Gaussian,
1.5,
);
for k in 0..n_n {
let row = &intr[big_m * k..big_m * (k + 1)];
let argmax = row
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
.unwrap()
.0 as i32;
assert_eq!(argmax, channel[k], "row {k} argmax wrong");
let s: f32 = row.iter().sum();
assert!((s - 1.0).abs() < 1e-3, "row {k} sum = {s}, not normalised");
}
}
#[test]
fn intrinsics_normalise_each_row_to_unit_sum() {
let code = &QRA15_65_64_IRR_E23;
let n_n = 63usize;
let big_m = 64usize;
let bpt = 4usize; let bins_per_symbol = big_m * (2 + bpt);
let energies: Vec<f32> = (0..n_n * bins_per_symbol)
.map(|i| (i % 17) as f32 + 0.1)
.collect();
let mut intr = vec![0.0_f32; big_m * n_n];
intrinsics_fast_fading(
code,
&mut intr,
&energies,
2,
0.5,
FadingModel::Gaussian,
1.5,
);
for k in 0..n_n {
let row = &intr[big_m * k..big_m * (k + 1)];
let s: f32 = row.iter().sum();
assert!((s - 1.0).abs() < 1e-3, "row {k} not normalised: sum = {s}");
for &p in row {
assert!(p >= 0.0 && p.is_finite(), "row {k} bad value {p}");
}
}
}
#[test]
fn esnodb_returns_finite_value_above_floor() {
let code = &QRA15_65_64_IRR_E23;
let n_n = 63usize;
let big_m = 64usize;
let channel: Vec<i32> = (0..n_n as i32).map(|i| (i * 11 + 5) % 64).collect();
let energies = synthetic_wide_energies(&channel, 1, 100.0, 0.05);
let mut intr = vec![0.0_f32; big_m * n_n];
let state = intrinsics_fast_fading(
code,
&mut intr,
&energies,
1,
0.3,
FadingModel::Gaussian,
1.5,
);
let snr_db = esnodb_fast_fading(&state, code, &channel, &energies);
assert!(snr_db.is_finite(), "esnodb returned non-finite: {snr_db}");
assert!(snr_db > -10.0, "esnodb absurdly low: {snr_db}");
}
}