Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ Gates: `hpx_hf_rungs_survive_fade` (fade), `goodput_gate` (the clean-channel cou

**The rate ladder's SNR scales are per-waveform-family by physical necessity — do NOT try to unify them.** `rx_snr_db` dispatches to each plugin's `estimate_snr_db`, and they report different quantities: single-carrier PSK (BPSK, post-#934) reads ~true channel SNR; OFDM/SC-FDMA read a *saturation-bounded plugin-domain* SNR that flattens near ~16 dB (ZF noise-enhancement on faded subcarriers) and **physically cannot** report the 20–30 dB the dense rungs run at. So `hpx_hf`'s SL2–SL6 floors are true channel SNR and SL7–SL14 floors are plugin-domain — two scales, one ladder. This looks like a bug; it is not. Forcing OFDM onto a true-SNR scale (or "unifying the estimators") would put the top rungs' floors above anything the estimate can read → the SNR climb never reaches them → the exact v0.14.0 "AWGN-scale floors never clear" stall. The **evidence-based climb** (#934) is what makes two scales safe: it advances on decode success where SNR saturates. Gate: `snr_scale_boundary` — it fails if OFDM starts tracking true SNR without the floors being re-derived in the same change (the two are one decision). Full single-carrier→true-SNR unification of QPSK/8PSK was scoped and DECLINED: high-risk (floor recalibration across 5 profiles) for churn-reduction only, since the evidence climb already self-corrects the mismatch.

**LLRs already carry `1/σ²` — do not weight them by it again.** `openpulse_dsp::constellation::symbol_llrs` divides every distance by `noise_var`, so a calibrated plugin (SC-FDMA, OFDM) emits true log-likelihood ratios whose magnitude is ∝ 1/σ². For repeated observations of the same bits, LLRs **add**: `combine_llrs_map` is the MAP combine and *is* inverse-noise weighting. The engine used to re-weight that sum by a `1 / mean(|LLR|)` proxy — a second 1/σ² — costing 0.75 dB on graded HARQ attempt sets (fixed in PR #686). `combine_llrs_weighted` is only for LLRs with a noise-blind scale (the ±1.0 trait default). Every shipped plugin is now calibrated (PR #687) **in the weak sense only, and the word hides two different bars**: `llr_calibration.rs` checks that `mean(|LLR|)` GROWS with SNR by a per-plugin floor (BPSK ≥ 8×, QPSK ≥ 1.1×, 8PSK ≥ 1.4×, ideal 15.8×) — that a σ² is divided by at all. The strong bar, that a bit carrying `|L|` is wrong about `1/(1+e^{|L|})` of the time within 4×, is checked only by the five `llr_reliability.rs` files (64QAM, MFSK16, OFDM, PILOT, SC-FDMA). **bpsk, qpsk and psk8 meet the weak bar and are untested against the strong one** — measured 2026-09-13, QPSK250 and 8PSK500 are in fact within ~1.25× at the SNRs they are used at, but nothing holds them there. Before citing "calibrated", say which bar. **Choosing the noise estimator is the hard part**: a demodulator's residual is not all thermal noise — pulse-shaping ISI and equalizer misadjustment vary the symbol *amplitude* with no SNR dependence, so a moment (M2/M4) or distance-to-nearest-point estimator stops tracking SNR entirely. Use the component *orthogonal* to the hard decision (`psk_symbol_noise_var`), or for a differential detector the quadrature companion, where the amplitude cancels exactly (`differential_llr_scale`).
**LLRs already carry `1/σ²` — do not weight them by it again.** `openpulse_dsp::constellation::symbol_llrs` divides every distance by `noise_var`, so a calibrated plugin (SC-FDMA, OFDM) emits true log-likelihood ratios whose magnitude is ∝ 1/σ². For repeated observations of the same bits, LLRs **add**: `combine_llrs_map` is the MAP combine and *is* inverse-noise weighting. The engine used to re-weight that sum by a `1 / mean(|LLR|)` proxy — a second 1/σ² — costing 0.75 dB on graded HARQ attempt sets (fixed in PR #686). `combine_llrs_weighted` is only for LLRs with a noise-blind scale (the ±1.0 trait default). Every shipped plugin is now calibrated (PR #687) **in the weak sense only, and the word hides two different bars**: `llr_calibration.rs` checks that `mean(|LLR|)` GROWS with SNR by a per-plugin floor (BPSK ≥ 8×, QPSK ≥ 1.1×, 8PSK ≥ 1.4×, ideal 15.8×) — that a σ² is divided by at all. The strong bar, that a bit carrying `|L|` is wrong about `1/(1+e^{|L|})` of the time within 4×, is checked only by the five `llr_reliability.rs` files (64QAM, MFSK16, OFDM, PILOT, SC-FDMA). **bpsk, qpsk and psk8 meet the weak bar and are untested against the strong one** — measured 2026-09-13, QPSK250 and 8PSK500 are in fact within ~1.25× at the SNRs they are used at, but nothing holds them there. Before citing "calibrated", say which bar. **Choosing the noise estimator is the hard part**: a demodulator's residual is not all thermal noise — pulse-shaping ISI and equalizer misadjustment vary the symbol *amplitude* with no SNR dependence, so a moment (M2/M4) or distance-to-nearest-point estimator stops tracking SNR entirely. Use the component *orthogonal* to the hard decision (`psk_symbol_noise_var`), or for a differential detector the quadrature companion, where the amplitude cancels exactly (`differential_llr_scale`). **Corrected 2026-09-21 (#1364): `1/σ²` is the right target only where the symbol amplitude is KNOWN.** For a blind differential detector it is the high-SNR *limit* of the true slope `2A²/var(dot)`, and an estimator that holds it all the way down keeps voting at full confidence on an attempt carrying no signal — measured, a noise-only attempt emitted LLRs of std 1.41 at every σ from 0.1 to 2.0, and against the true pairwise DBPSK LLR that is 8× over-confident at −12 dB. That matters only where LLRs are SUMMED across attempts, which is `hpx_hf` SL2–SL5.

**Acquire on the normalised correlation, not the unnormalised score.** `IqMatchedFilter::search`'s argmax favours high *energy*, on the reasoning that "a deep-fade low-energy window cannot win". When the **preamble** is the faded part that is exactly backwards — SC-FDMA lost frames to data-region windows 4896 samples later that merely shared the pilot comb (ρ = 0.994 at the true offset with energy 19.4, versus ρ = 0.657 with energy 83.0). Use `search_normalized` with an energy floor (ρ is meaningless on a silent window). This looked like "fade dynamics" for a release and was slated as a channel-estimate fix; **ablating** `smooth_ce` entirely (temporarily, to test the hypothesis) left the numbers bit-identical (PR #689) — the sync was the mechanism. `smooth_ce` itself stays in the SC-FDMA demod (an EMA channel-estimate smoother, live at 6 call sites in `plugins/scfdma/src/demodulate.rs`); it just wasn't the thing that was broken.

Expand Down
4 changes: 3 additions & 1 deletion crates/openpulse-core/src/fec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,9 @@ impl Default for SoftCombiner {
///
/// # When this is the wrong function
///
/// A true log-likelihood ratio `log P(b=0|y) / P(b=1|y)` already carries `1/σ²` — see
/// A true log-likelihood ratio `log P(b=0|y) / P(b=1|y)` already carries its own noise scaling —
/// `1/σ²` where the amplitude is known, and the vanishing slope `2A²/var(dot)` where it is blind
/// and must be estimated (#1364). See
/// `openpulse_dsp::constellation::symbol_llrs`, which divides every distance by `noise_var`. For
/// independent observations of the same bit, the MAP combine of true LLRs is their plain **sum**:
/// use [`combine_llrs_map`]. Passing `noise_var = σ²` here on top of already-calibrated LLRs applies
Expand Down
Loading
Loading