fix(bpsk): BPSK's SNR estimate reads the channel at the lock it actually gets (#1438 PR1) - #1447
Merged
Merged
Conversation
…O-aware fit (#1438) BPSK's timing search locks a quarter symbol early whenever the frame starts that far into the slice, and the SNR estimator read the crossfade-cancelled stream, correct only on the exact boundary: at the production lock it read a near-constant (slope 0.09 dB/dB), so ClimbOnSnr could not fire on hpx_hf SL2-SL5 on air. estimate_snr_db now reads the uncancelled stream (the arm best sampled at that lock), removes one frame-global residual carrier frequency (the AFC discards sub-2 Hz corrections by design), and fits each 8-symbol window with the two neighbour taps (openpulse_dsp::constellation:: {remove_residual_frequency, isi_aware_snr_db_windowed}); MATCHED_FILTER_LOSS_DB 7.1 -> 4.4. The timing search is unchanged (reachability is PR2). Gates: DSP primitives, the inverted stream pin, forced-phase AWGN tracking over 4 rungs x 5 phases x 0/1/2 Hz, the BPSK250 fade slope, the one-decision-error and flip-heavy frequency pins, and a controller harness with the frame placed off the symbol grid; each watched failing under its own sabotage. The #1439 pin and ledger entry are relabelled; review-1435 gets a scoping banner; additive_snr_db_windowed is recorded DORMANT. Chain and results: docs/dev/project/traceability.md, 2026-09-25. Review: docs/dev/reviews/review-1438-snr-estimator.md. Implements: REQ-FUN-06 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0188ATCj6DZ9aRVQ2vSirua6
The daemon's only feed of the OTA rate controller (ota_decode_and_ack_inner) passes None on every failed decode since #1142: the estimate is taken only on a decoded span. The link simulator, whose comment says it mirrors the real software, still passed the whole-buffer reading on a failure, so its FastDownshift fired where the daemon's cannot. #1438's livelier estimate exposed it: psk_ladder_climbs_off_the_entry_rung_on_a_fade ended at avg_level 2.8 / SL2. A decision trace shows the downshifts fired on SL6 (QPSK250-D) failures using the QPSK estimator's readings, and in an SL1<->SL2 loop where every BPSK31 frame after an MFSK16 frame failed (19/19, unexplained, filed). With the failure-path reading removed the test passes and the linksim suite is 19/19. The FastDownshift branch in ota_rate.rs gets a note: it has no on-air consumer. Verification-objective: the link simulator feeds the rate controller what the daemon feeds it Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0188ATCj6DZ9aRVQ2vSirua6
…eadings measured Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0188ATCj6DZ9aRVQ2vSirua6
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#1438 PR1 — BPSK's SNR estimate reads the channel at the lock it actually gets
The defect. BPSK's timing search locks a quarter symbol early whenever the frame starts that far
into the slice, which is nearly every real burst. The SNR estimator read the crossfade-CANCELLED
stream, which is correct only on the exact symbol boundary. At the production lock it read ≈ 3 dB at
every true SNR from 10 to 30 dB (BPSK250, −0.28 of a symbol: slope 0.09 dB/dB on AWGN and on
moderate_f1). The rate controller'sClimbOnSnrpath therefore could not fire onhpx_hfSL2–SL5 on air.
The change.
estimate_snr_dbreads the uncancelled stream, the arm that is best sampled at that early lock.corrections by design.
a·d_{k−1} + b·d_k + c·d_{k+1}, so the neighbour taps stop countingas noise.
MATCHED_FILTER_LOSS_DBis refitted from 7.1 to 4.4.openpulse_dsp::constellation(remove_residual_frequency,isi_aware_snr_db_windowed).What it changes on air:
ClimbOnSnrfor decoded frames — every rung on AWGN, SL5 onmoderate_f1from ≈ 11 dB. A failed decode still passes no reading (engine.rs:3195), so thefast-downshift is untouched in the daemon. The linksim passed a reading on failures too, so its
fast-downshift could move where the daemon's cannot — fixed in the second commit, below.
last_rx_snr_db(panel, ADIF RST, QSY scan) changesfrom a phase-dependent constant to a tracking reading.
Evidence (details and every number: the 2026-09-25 entry in
docs/dev/project/traceability.md).uses the decisions (the first version of that test passed with the decision index sabotaged);
assert on the controller at a non-zero onset all sat on a symbol-period multiple, the one phase
where the old estimator was right. There a 15 dB frame reads 12.5–15.1 dB and climbs; with the old
estimator the first such frame reads 7.81 dB and holds.
to 11.4 dB.
residual, up to 1.74 dB low. It is not the frequency estimate (an exact derotation reads within
0.6 dB), and the mechanism is not established. The bound there is one-sided, reasoned in the test.
below SL4's 7.0 dB ceiling. BPSK31 and BPSK63 read flat on
moderate_f1(≈ −10.2 and −2.2 … −1.3 dBacross true 5–30 dB), better than the old estimator's −13.6 and ≈ −6.9 but still floored by the fade
(hpx_hf is pinned at the entry rung on a fade (~5 bps vs ~300-1200): the rate ladder can only climb on an SNR estimate, and no estimator is informative at 31 baud through a 1 Hz fade #934's low-baud limit). The daemon cannot demote on a decoded frame, but the panel and ADIF will
show those low numbers.
Second commit: the link simulator is aligned with the daemon. The daemon's rate controller gets
no SNR reading on a failed decode (#1142). The linksim, which says it mirrors the real software,
still passed one, and the new estimate exposed it:
psk_ladder_climbs_off_the_entry_rung_on_a_fadeended at SL2. A decision trace showsClimbOnSnrfrom SL5 into SL6, then fast-downshifts driven by the QPSK estimator's failure readings, then an
SL1↔SL2 loop in which every BPSK31 frame after an MFSK16 frame failed (19/19, unexplained, filed).
With
decode_ok.then_some(snr)the test passes and the linksim suite is 19/19.mainpassed withoutthis: at the linksim's lead-0 lock the old estimate on the fade (5.3–5.6 dB at true 15–25) sits under
SL5's 9 dB ceiling, so SL6 was reached by evidence only, and less often. That is inferred from the
lead-0 gate readings;
main's run was not traced.The controller's
FastDownshiftbranch now notes it has no on-air consumer.scripts/slow-tests.sh ota(CAP-33) on this HEAD:SLOW-TESTS: PASS— 3 passed, 0 failed, 2763 s.GATE: PASS 0f092df72038899efa7d84cd5f9550b4f930e90b clean 20260925T084909Z— 2596 passed, 0 failed. (The first gate, on an earlier commit, failed six steps —clippy ×3 on one lint, the reachability ratchet, the requirements-trailer lint, and the linksim test above; all
are addressed here.)
Corrections made in this PR. #1439's pin labels and its ledger entry: lead 32 → 24 is the
objective's early lock, not a range defect; the reachability defect shows at lead 0.
review-1435gains a banner scoping its round-1 estimator remark.
Out of scope, to be filed:
nulls, inside the AFC's 2 Hz deadband);
receive_with_ack_hintestimating on the whole buffer;scripts/slow-tests.shignoringCARGO_TARGET_DIR;Implements: REQ-FUN-06
Review: docs/dev/reviews/review-1438-snr-estimator.md
Refs #1438.
🤖 Generated with Claude Code
https://claude.ai/code/session_0188ATCj6DZ9aRVQ2vSirua6