Skip to content

fix(dsp): a signal-free HARQ attempt voted at full strength (#1364) - #1426

Merged
dc0sk merged 1 commit into
mainfrom
fix/1364-differential-llr-scale
Sep 21, 2026
Merged

dc0sk merged 1 commit into
mainfrom
fix/1364-differential-llr-scale

Conversation

@dc0sk

@dc0sk dc0sk commented Sep 21, 2026

Copy link
Copy Markdown
Owner

Closes #1364.

differential_llr_scale returned 2·mean|dot|/var(cross). On an attempt carrying no signal that
emits LLRs of std 1.41 at every σ from 0.1 to 2.0 — measured through the shipped function.
combine_llrs_map SUMS attempts and the OTA arm retains failed bursts, so a worthless attempt did
not merely fail to help: it outvoted the attempts carrying the frame. hpx_hf SL2–SL5 run this
path.

My diagnosis was wrong twice, and review corrected both

  • At A = 0 the old estimator returns exactly the 1/σ² its contract promises — √2 is that
    contract honoured, not violated. The defect is the contract: 1/σ² is the high-SNR limit of
    the true DBPSK LLR slope, which vanishes with the signal. Against the exact pairwise LLR the old
    formula is 8× over-confident at −12 dB (slope 0.94 against 0.11).
  • My "target 0.04" was a category error. σ₁²/σ₂² is a scale ratio; the gate's 0.21 is a
    magnitude ratio, and under exact 1/σ² the magnitude ratio really is ≈0.20.

The fix

² = √(max(0, ⟨dot²⟩ − ⟨cross²⟩)), scale 2²/⟨cross²⟩ — the Gaussian-approximation LLR with an
unbiased fourth-moment amplitude, tending to 1/σ² at high SNR and to 0 as the signal vanishes.
For iid circular noise E[dot²] − E[cross²] = A⁴ exactly, since the A²v and v²/2 terms are
common to both. f64 accumulators (the subtraction is catastrophic cancellation by construction) and
a SCALE_FLOOR so a zero estimate cannot emit −0.0, which an l < 0.0 consumer reads as bit 0.

Premise pinned, not assumed

The identity holds only for noise uncorrelated at lag 1. cancel_crossfade_isi induces ρ = −1/3,
under which A = 0 yields 2ρ²v² and the defect returns. differential_llr_scale_assumes_iid_noise
makes #1361's open question a failing test rather than a silent regression in HARQ weighting.

Why nothing caught it

The old unit test claimed this exact property and passed, because it synthesised dots/crosses
from the high-SNR asymptotic model
— omitting the n·conj(n) term that causes the floor — and
swept only 10/20 dB. Its expected = 1/σ² was the wrong target anyway. Rebuilt from actual complex
symbols, swept to A = 0, with the old formula retained inside as a control asserting it votes ≈ √2.

A limit that is fundamental, not a shortfall

At A = 0 the score for A² equals the score for v, so the Fisher information is singular — the
known blind-SNR degeneracy — and any blind estimate is sampling-noise limited at N^(−1/4). The
tracking sweep is therefore asserted only where ² is resolvable to the stated tolerance,
derived as ≳ −6 dB at N = 20 000 after −12 dB read 2.01× and −9 dB read 1.36×.

Measured, both arms

old new
failing seed triples (16) 7/16 1/16
mean delta 0.656 dB 0.156 dB

a_deeply_faded_extra_attempt_does_not_hurt still passes — #832's gate is improved, not changed.
(#1364 recorded 3/16 on its own smaller triple set; this is a different set, not a reproduction.)

No fade regression, which was the review's live risk (Jensen: √E[A⁴] ≥ E[A²], ×1.41 on a
Rayleigh envelope). moderate_f1, 32 seeds, 1 clean + 1 faded: 19/23/27 new against 20/22/27 old
at 6/8/10 dB; the 2-clean rows are identical. No fade benefit either — the gain is AWGN-specific,
and saying so is the honest scope.

Sabotage, both directions

The rebuilt tests fail on the old estimator (v=0.02: signal-free vote 1.439 exceeds derived bound 0.464) and pass on the new one.

Test results

scripts/gate.sh on the branch tip, clean tree:

suites=339 tests_passed=2576 tests_failed=0
GATE: PASS 6eb55213fab35373179945fb0071110898eb7478 clean 20260921T100143Z

All 13 steps ok. Test count is 2576 against main's 2574: three new tests replacing the one whose fixture could not exhibit the defect.

Filed separately, not folded in

#1425 — this is a class. Every blind scale-invariant calibrator votes at a σ-independent
magnitude on pure noise; 8PSK500 does so at 0.44 of a good attempt against BPSK250's 0.009. The
fix here is specific to a differential detector's dot/cross pair and does not generalise to the
coherent plugins.

The wrong contract was written in five places and all were swept: constellation.rs's doc, the two
bpsk_demodulate_soft comments, llr_calibration.rs's header, fec.rs's combine_llrs_map doc,
and CLAUDE.md's "LLRs already carry 1/σ²" edge.

Refactors: CAP-20
Refactors: CAP-12
Verification-objective: a HARQ attempt's LLR magnitude must fall with its signal, not merely with
its noise power, so a signal-free attempt cannot outvote a good one

Review: docs/dev/reviews/review-1364-differential-llr-scale.md

🤖 Generated with Claude Code

https://claude.ai/code/session_0188ATCj6DZ9aRVQ2vSirua6

`differential_llr_scale` returned `2*mean|dot|/var(cross)`. On an attempt carrying NO
signal that emits LLRs of std 1.41 at EVERY sigma from 0.1 to 2.0 — measured through
the shipped function. `combine_llrs_map` sums attempts and the OTA arm retains failed
bursts, so a worthless attempt did not merely fail to help: it outvoted the attempts
carrying the frame. hpx_hf SL2-SL5 run this path.

MY DIAGNOSIS WAS WRONG TWICE AND REVIEW CORRECTED BOTH. At A=0 the old estimator
returns EXACTLY the 1/sigma^2 its contract promises — sqrt(2) is that contract
honoured. The defect is the CONTRACT: 1/sigma^2 is the high-SNR LIMIT of the true
DBPSK LLR slope, which vanishes with the signal; against the exact pairwise LLR the old
formula is 8x over-confident at -12 dB. And my "target 0.04" was a category error —
sigma1^2/sigma2^2 is a SCALE ratio while the gate's 0.21 is a MAGNITUDE ratio, and
under exact 1/sigma^2 the magnitude ratio really is ~0.20.

Implementation: A^2 = sqrt(max(0, <dot^2> - <cross^2>)), scale = 2A^2/<cross^2> — the
Gaussian-approximation LLR with an unbiased fourth-moment amplitude, tending to
1/sigma^2 at high SNR and to 0 as the signal vanishes. f64 accumulators (the
subtraction is catastrophic cancellation by construction) and a SCALE_FLOOR so a zero
estimate cannot emit -0.0, which an `l < 0.0` consumer reads as bit 0.

PREMISE PINNED, NOT ASSUMED: the identity holds only for noise uncorrelated at lag 1.
`cancel_crossfade_isi` induces rho = -1/3, under which A=0 yields 2*rho^2*v^2 and the
defect returns. A test makes #1361's open question a failing test rather than a silent
regression in HARQ weighting.

WHY NOTHING CAUGHT IT: the old unit test claimed this exact property and passed,
because it SYNTHESISED dots/crosses from the high-SNR asymptotic model — omitting the
n*conj(n) term that causes the floor — and swept only 10/20 dB. Its `expected =
1/sigma2` was the wrong target anyway. Rebuilt from actual complex symbols, swept to
A=0, with the old formula retained as a control asserting it votes ~sqrt(2).

A FUNDAMENTAL LIMIT, not a shortfall: at A=0 the score for A^2 equals the score for v,
so the Fisher information is singular and any blind estimate is sampling-noise limited
at N^(-1/4). The tracking sweep is asserted only where A^2 is resolvable TO THE STATED
TOLERANCE — derived as >= -6 dB at N=20000, after -12 dB read 2.01x and -9 dB 1.36x.

Measured, both arms, 16 seed triples: old 7/16 failing (mean delta 0.656 dB) -> new
1/16 (0.156 dB). `a_deeply_faded_extra_attempt_does_not_hurt` still passes. NO FADE
REGRESSION, which was the review's live risk (Jensen, sqrt(E[A^4]) >= E[A^2]):
moderate_f1, 32 seeds, 1 clean + 1 faded gives 19/23/27 new against 20/22/27 old at
6/8/10 dB. No fade BENEFIT either — the gain is AWGN-specific.

The wrong contract was written in five places; all swept. Filed separately as #1425:
the property is a CLASS — 8PSK500 votes at 0.44 of a good attempt on pure noise against
BPSK250's 0.009 — and this fix is specific to a differential detector's dot/cross pair.

Refactors: CAP-20
Refactors: CAP-12
Verification-objective: a HARQ attempt's LLR magnitude must fall with its signal, not
merely with its noise power, so a signal-free attempt cannot outvote a good one

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0188ATCj6DZ9aRVQ2vSirua6
@dc0sk
dc0sk merged commit 92539fd into main Sep 21, 2026
11 checks passed
@dc0sk
dc0sk deleted the fix/1364-differential-llr-scale branch September 21, 2026 12:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant