You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
BPSK's timing search locks 8 samples early whenever a lead makes that reachable — the SNR estimate is capped near 3.5 dB and BPSK's fast climb can never fire on the OTA path #1438
BPSK's timing search, find_timing_offset_with_expected (plugins/bpsk/src/demodulate.rs), picks
the sub-symbol offset that maximises the preamble correlation of symbols produced by demodulate_iq's half-Hann window. It is wrong in two independent ways.
1. Its objective peaks before the symbol boundary. The window is not matched to the modulator's
pulse (modulate.rs: each symbol is a full 64-sample Hann spanning two slots). Computed from those two
window definitions, the current symbol's gain against where the window starts, relative to the
boundary:
window start d (samples)
−12
−9
−8
−4
−1
0
+2
+4
gain of the current symbol
1.166
1.190
1.188
1.130
1.038
1.000
0.914
0.818
coefficient of the next symbol
0.041
0.078
0.094
0.184
0.282
0.320
0.406
0.502
coefficient of the previous one
0.113
0.052
0.037
0.006
0.000
0.000
0.000
0.000
(0.320 is the discrete sum; the code's cancellation uses β = 1/3.) The model peaks at d = −9; measured, the search locks at d = −8 wherever that is reachable — lead 16 locks at offset 8,
though offset 16 is in range.
2. Its range is one symbol. It scans offsets 0..n from the start of the buffer. For a lead of
n or more the boundary is never visited — at lead 32 it locks at 24, and a correct objective alone
would lock at 31.
At lead 0 neither defect can show: the early optimum is unreachable and the boundary is offset 0.
Every buffer-is-the-frame test hands the search lead 0, which is why none of them could see this.
What it costs — measured
The SNR estimate is capped at the d = −8 lock, and lower at other misalignments. BPSK250, 200 B,
AWGN, 8 seeds, plugin level (a 16- or 32-sample lead reads identically):
true SNR
10 dB
20 dB
30 dB
40 dB
estimate, lead 0
11.10
19.29
22.81
23.38
estimate, 16- or 32-sample lead
3.05
3.50
3.55
3.55
(Lead 0 flattening near 23 dB is the estimator's own documented EVM floor, not this defect.) The
cap's attribution is derived, not ablated: at d = −8 the next-symbol coefficient is 0.09 while cancel_crossfade_isi subtracts 1/3, leaving ~0.24 of the next symbol as injected ISI.
It reaches production. Through ota_decode_burst on a clean 10 dB channel, frame at 4032 + j in
AWGN, 6 seeds, every frame decoded: the SNR fed to the rate controller was 3.0 dB at best and
−1.3 dB at j = 16, identically in a variant-0-only build — so the cap predates the #1428 union.
Logged spans show why: the onset scan steps 32 samples from the burst start, and its first successful
onset is typically one symbol BEFORE the frame (4000), giving the estimator a lead of 32 + j — the
range defect, where the boundary is never reachable. At j = 20 the cancelled arm no longer decodes
at 4000: the variant-0 build decodes at 4032 and reads 3.0 dB, while the union's second arm decodes
at 4000 and reports −6.0 dB. The union extends the early lock's reach; it did not create it.
The decode runs mis-locked too — a code read.decode_attempt → receive_from_samples_with_fec_inner → stage_demodulate_variants → demodulate_variants → symbol_stream_parts_with_expected reaches the same search on the same slice, with no onset
refinement between (refine_onset runs only on the receive_with_timeout path). On a clean 10 dB
channel the cancelled arm (with Rs) still decodes from a span up to 48 samples before the frame and
not from 52 — so a decode is not evidence of a correct lock. That RS is what absorbs it is inferred,
not ablated.
Blast radius
The fast SNR climb cannot fire for BPSK on the OTA path.hpx_hf's BPSK rungs have SNR
ceilings of 6.0 / 6.5 / 7.0 / 9.0 dB (SL2–SL5); a reading capped near 3.5 dB clears none of them.
BPSK climbs on decode evidence only. Demotion is unaffected: it runs on Failed, where no span
exists and the SNR is None.
Operators see it in the panel's rate-decision log line (OtaRateDecision.snr_db); the TUI
drops the field.
Where frames decode, on fades. On moderate_f1, 8 of BPSK's SNR estimate reads low on frames the FEC had to correct — decision-directed on the losing arm's decisions #1435's 96 frames failed the cancelled arm
at the early lock and decoded at the boundary; with the union they are delivered from the early lock
instead, with the capped reading. Whether the second arm's 17 early-lock decodes of frames the
variant-0 build lost would also decode at the boundary is unmeasured.
AFC, unmeasured. After a phase-1 success the scan commits an AFC estimate on the winning slice
(update_afc_estimate), and estimate_afc_hz uses the same timing search, so the estimate is taken
from the early lock; rx_snr_db then runs at a centre frequency the decode did not use. There is one
logged instance of the reading moving with AFC state: seed 22 below. Seed 94 (−19.3 dB in production,
+0.27 dB on the same span on a fresh engine, no settle) is consistent with it but not yet shown.
Not fixable in the estimator. Passing the decode's timing to the estimator is a no-op (the same
search on the same slice). A data-aided estimate would still measure at the early lock.
Direction — a design decision, for review before code
Both defects need fixing. A review of these measurements suggested (i) correlating the preamble
against a filter matched to the actual pulse — which should peak at the boundary, by symmetry;
untested — while keeping the half-Hann window and β cancellation for the data demod, and (ii) widening
the search to ≥ 2n offsets so the boundary is reachable for leads in [n, 2n). Wire-neutral and
plugin-local, but it changes the numbers of the #1062 vetting column (the same function, with a
candidate expected) and has a GPU twin with its own timing search (timing_offset_search_gpu; #1083,
and #1433's shape). Cost to measure: every mode makes ~129 scan attempts per burst (1 + 4 × 32);
BPSK31's per-attempt cost is the driver, at 2n = 512 offsets over 32 symbols of 256 samples.
Pinned
plugins/bpsk/src/demodulate.rs → the_timing_search_locks_early_when_a_lead_makes_it_reachable, a
default-run characterisation test at a strong 30 dB, one deterministic seed: lead 0 locks at 0 and
reads > 20 dB (22.89 in its own run); lead 16 locks at 8 (the objective defect) and lead 32 at 24 (the
range defect), both reading < 5 dB (3.55 and 3.53). Each assertion names the defect it sees and is expected to fail when that one is
fixed. Sabotage-verified.
The defect — two, in one function
BPSK's timing search,
find_timing_offset_with_expected(plugins/bpsk/src/demodulate.rs), picksthe sub-symbol offset that maximises the preamble correlation of symbols produced by
demodulate_iq's half-Hann window. It is wrong in two independent ways.1. Its objective peaks before the symbol boundary. The window is not matched to the modulator's
pulse (
modulate.rs: each symbol is a full 64-sample Hann spanning two slots). Computed from those twowindow definitions, the current symbol's gain against where the window starts, relative to the
boundary:
(0.320 is the discrete sum; the code's cancellation uses β = 1/3.) The model peaks at d = −9;
measured, the search locks at d = −8 wherever that is reachable — lead 16 locks at offset 8,
though offset 16 is in range.
2. Its range is one symbol. It scans offsets
0..nfrom the start of the buffer. For a lead ofn or more the boundary is never visited — at lead 32 it locks at 24, and a correct objective alone
would lock at 31.
At lead 0 neither defect can show: the early optimum is unreachable and the boundary is offset 0.
Every buffer-is-the-frame test hands the search lead 0, which is why none of them could see this.
What it costs — measured
The SNR estimate is capped at the d = −8 lock, and lower at other misalignments. BPSK250, 200 B,
AWGN, 8 seeds, plugin level (a 16- or 32-sample lead reads identically):
(Lead 0 flattening near 23 dB is the estimator's own documented EVM floor, not this defect.) The
cap's attribution is derived, not ablated: at d = −8 the next-symbol coefficient is 0.09 while
cancel_crossfade_isisubtracts 1/3, leaving ~0.24 of the next symbol as injected ISI.It reaches production. Through
ota_decode_burston a clean 10 dB channel, frame at 4032 + j inAWGN, 6 seeds, every frame decoded: the SNR fed to the rate controller was 3.0 dB at best and
−1.3 dB at j = 16, identically in a variant-0-only build — so the cap predates the #1428 union.
Logged spans show why: the onset scan steps 32 samples from the burst start, and its first successful
onset is typically one symbol BEFORE the frame (4000), giving the estimator a lead of 32 + j — the
range defect, where the boundary is never reachable. At j = 20 the cancelled arm no longer decodes
at 4000: the variant-0 build decodes at 4032 and reads 3.0 dB, while the union's second arm decodes
at 4000 and reports −6.0 dB. The union extends the early lock's reach; it did not create it.
The decode runs mis-locked too — a code read.
decode_attempt→receive_from_samples_with_fec_inner→stage_demodulate_variants→demodulate_variants→symbol_stream_parts_with_expectedreaches the same search on the same slice, with no onsetrefinement between (
refine_onsetruns only on thereceive_with_timeoutpath). On a clean 10 dBchannel the cancelled arm (with
Rs) still decodes from a span up to 48 samples before the frame andnot from 52 — so a decode is not evidence of a correct lock. That RS is what absorbs it is inferred,
not ablated.
Blast radius
hpx_hf's BPSK rungs have SNRceilings of 6.0 / 6.5 / 7.0 / 9.0 dB (SL2–SL5); a reading capped near 3.5 dB clears none of them.
BPSK climbs on decode evidence only. Demotion is unaffected: it runs on
Failed, where no spanexists and the SNR is
None.OtaRateDecision.snr_db); the TUIdrops the field.
moderate_f1, 8 of BPSK's SNR estimate reads low on frames the FEC had to correct — decision-directed on the losing arm's decisions #1435's 96 frames failed the cancelled armat the early lock and decoded at the boundary; with the union they are delivered from the early lock
instead, with the capped reading. Whether the second arm's 17 early-lock decodes of frames the
variant-0 build lost would also decode at the boundary is unmeasured.
(
update_afc_estimate), andestimate_afc_hzuses the same timing search, so the estimate is takenfrom the early lock;
rx_snr_dbthen runs at a centre frequency the decode did not use. There is onelogged instance of the reading moving with AFC state: seed 22 below. Seed 94 (−19.3 dB in production,
+0.27 dB on the same span on a fresh engine, no settle) is consistent with it but not yet shown.
What this is not
moderate_f1even forthe worst frames — measured at the boundary, where the cancelled arm is the good one; decisions at
the early lock are worse and unmeasured. Re-measuring BPSK's SNR estimate reads low on frames the FEC had to correct — decision-directed on the losing arm's decisions #1435's fixture on the same channel
realisation with a fresh engine, changing only the span start (4000 against 4032), reproduced 7 of its
8 paired differences to within 0.02 dB. The eighth (seed 22: −0.70 paired, −3.11 by span alone) is the
variant-0 build's reading being taken at a phase-2 AFC correction (
settles=33in its log; the other15 runs settled zero times): 0.96 dB, against 3.37 dB for the same span at zero correction.
of the correlation window, by measuring on the decoded span. Here the preamble is inside the window
and the lock is deterministic (readings flat to ±0.05 dB across noise seeds); the decoded span itself
carries the short lead.
search on the same slice). A data-aided estimate would still measure at the early lock.
Direction — a design decision, for review before code
Both defects need fixing. A review of these measurements suggested (i) correlating the preamble
against a filter matched to the actual pulse — which should peak at the boundary, by symmetry;
untested — while keeping the half-Hann window and β cancellation for the data demod, and (ii) widening
the search to ≥ 2n offsets so the boundary is reachable for leads in [n, 2n). Wire-neutral and
plugin-local, but it changes the numbers of the #1062 vetting column (the same function, with a
candidate
expected) and has a GPU twin with its own timing search (timing_offset_search_gpu; #1083,and #1433's shape). Cost to measure: every mode makes ~129 scan attempts per burst (1 + 4 × 32);
BPSK31's per-attempt cost is the driver, at 2n = 512 offsets over 32 symbols of 256 samples.
Pinned
plugins/bpsk/src/demodulate.rs→the_timing_search_locks_early_when_a_lead_makes_it_reachable, adefault-run characterisation test at a strong 30 dB, one deterministic seed: lead 0 locks at 0 and
reads > 20 dB (22.89 in its own run); lead 16 locks at 8 (the objective defect) and lead 32 at 24 (the
range defect), both reading < 5 dB (3.55 and 3.53). Each assertion names the defect it sees and is expected to fail when that one is
fixed. Sabotage-verified.
Refs #1435, #1437, #1142, #1062, #1083, #1433.