The suspected defect
BPSK's estimate_snr_db (plugins/bpsk/src/demodulate.rs) rebuilds the transmitted symbols from
the differential decisions of the cancelled stream by integrating flips, then fits a per-window
least-squares gain (additive_snr_db_windowed, 16-symbol windows).
An isolated wrong decision inverts the reconstruction from that point on. Later windows absorb that
through their own fitted gain, so the damage is confined to the window containing the error — where
the fitted gain shrinks toward zero and most of the window is booked as noise. The common case is a
paired error (one bad symbol flips two differential bits), which re-inverts after one symbol and
costs one symbol, not a window.
So the reading depends on how many isolated decisions the cancelled arm got wrong, not only on the
channel. A 16-symbol window is two bytes, so a frame whose RS block needed > 16 byte corrections has
at least 9 of its ~128 windows holding an error. If each reads as noise, the estimate is capped near
10·log10(119/9) ≈ 11 dB Es/N0 whatever the true SNR. That is a prediction the measurement below is
consistent with, not a demonstration.
Why this surfaced now
Not new — every frame RS had to correct already carries it. #1428's union makes it more frequent:
when the uncancelled arm produces a frame, the estimate still comes from the cancelled arm's
decisions, which on that attempt were bad enough to fail RS.
Measured through ota_decode_burst on moderate_f1 @ 8 dB, 200 B, paired against a
variant-0-only build on the same channel realisation: on the 8 seeds both builds decode, the union's
reading is lower on all 8 — median −2.97 dB, range −0.70 to −6.88. One frame only the union decodes
read −19.3 dB.
Two limits on that evidence. The spans were not controlled: where the union won at an earlier onset,
the slice and the decisions both differ between builds, so the 8 pairs confound them. And the
−19.3 dB reading is 11 dB below the lowest value the documented misalignment sweep reached at its one
operating point (−8.39 dB at a true 5.82, engine.rs ~3180), so misalignment alone is an unlikely
account of it, not an excluded one.
Blast radius
- The rate ladder — measured: none. A decoded frame is never answered with a demotion
(crates/openpulse-core/tests/success_based_climb.rs::a_decoded_frame_is_never_answered_with_a_demotion,
fed −20 dB). By code read, the evidence climb (crates/openpulse-core/src/ota_rate.rs,
proven_by_success) does not read the SNR; a low reading can only forgo the faster SNR climb for
that frame.
- QSY scan and ADIF — by code read: none. The hard arm records no SNR (
pending_snr is Some
only on the soft branch), so last_rx_snr_db() never sees it.
- Operators: yes. It is carried in
EngineEvent::OtaRateDecision.snr_db, which the daemon
forwards wholesale to control clients. An operator can be shown −19 dB for a frame that decoded.
Proposed direction — a design decision, so for review before code
After a successful decode the transmitted symbols are known exactly (re-encode the corrected wire).
Estimating SNR against those — data-aided rather than decision-directed — would remove the mechanism
rather than the symptom, if the mechanism above is the right one.
Discriminating measurement first (a bpsk unit test, since symbol_stream is private): on one
decoded frame, compute the estimate on the SAME span with (i) the shipped decisions and (ii) the
oracle symbols. The mechanism above predicts (i) ≪ (ii) ≈ the reading a variant-0 decode gives.
Holding the span fixed is what removes the onset confound above. Do not instead point the
estimator at the uncancelled stream: it keeps the +β·a_{k+1} residual, capping a clean channel's
Es/N0 near 9.5 dB — a bias of the same size.
Refs #1428, #1363.
The suspected defect
BPSK's
estimate_snr_db(plugins/bpsk/src/demodulate.rs) rebuilds the transmitted symbols fromthe differential decisions of the cancelled stream by integrating flips, then fits a per-window
least-squares gain (
additive_snr_db_windowed, 16-symbol windows).An isolated wrong decision inverts the reconstruction from that point on. Later windows absorb that
through their own fitted gain, so the damage is confined to the window containing the error — where
the fitted gain shrinks toward zero and most of the window is booked as noise. The common case is a
paired error (one bad symbol flips two differential bits), which re-inverts after one symbol and
costs one symbol, not a window.
So the reading depends on how many isolated decisions the cancelled arm got wrong, not only on the
channel. A 16-symbol window is two bytes, so a frame whose RS block needed > 16 byte corrections has
at least 9 of its ~128 windows holding an error. If each reads as noise, the estimate is capped near
10·log10(119/9) ≈ 11 dB Es/N0 whatever the true SNR. That is a prediction the measurement below is
consistent with, not a demonstration.
Why this surfaced now
Not new — every frame RS had to correct already carries it. #1428's union makes it more frequent:
when the uncancelled arm produces a frame, the estimate still comes from the cancelled arm's
decisions, which on that attempt were bad enough to fail RS.
Measured through
ota_decode_burstonmoderate_f1@ 8 dB, 200 B, paired against avariant-0-only build on the same channel realisation: on the 8 seeds both builds decode, the union's
reading is lower on all 8 — median −2.97 dB, range −0.70 to −6.88. One frame only the union decodes
read −19.3 dB.
Two limits on that evidence. The spans were not controlled: where the union won at an earlier onset,
the slice and the decisions both differ between builds, so the 8 pairs confound them. And the
−19.3 dB reading is 11 dB below the lowest value the documented misalignment sweep reached at its one
operating point (−8.39 dB at a true 5.82,
engine.rs~3180), so misalignment alone is an unlikelyaccount of it, not an excluded one.
Blast radius
(
crates/openpulse-core/tests/success_based_climb.rs::a_decoded_frame_is_never_answered_with_a_demotion,fed −20 dB). By code read, the evidence climb (
crates/openpulse-core/src/ota_rate.rs,proven_by_success) does not read the SNR; a low reading can only forgo the faster SNR climb forthat frame.
pending_snrisSomeonly on the soft branch), so
last_rx_snr_db()never sees it.EngineEvent::OtaRateDecision.snr_db, which the daemonforwards wholesale to control clients. An operator can be shown −19 dB for a frame that decoded.
Proposed direction — a design decision, so for review before code
After a successful decode the transmitted symbols are known exactly (re-encode the corrected wire).
Estimating SNR against those — data-aided rather than decision-directed — would remove the mechanism
rather than the symptom, if the mechanism above is the right one.
Discriminating measurement first (a bpsk unit test, since
symbol_streamis private): on onedecoded frame, compute the estimate on the SAME span with (i) the shipped decisions and (ii) the
oracle symbols. The mechanism above predicts (i) ≪ (ii) ≈ the reading a variant-0 decode gives.
Holding the span fixed is what removes the onset confound above. Do not instead point the
estimator at the uncancelled stream: it keeps the
+β·a_{k+1}residual, capping a clean channel'sEs/N0 near 9.5 dB — a bias of the same size.
Refs #1428, #1363.