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
34 changes: 34 additions & 0 deletions docs/dev/project/traceability.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,40 @@ and the actually-observed results per change.

---

## 2026-09-24 — #1435 refuted; BPSK's timing search locks early (#1438); #1437's OTA gain corrected

**Change.** Test-only, plus this correction. `plugins/bpsk/src/demodulate.rs` gains module
`snr_decision_discriminator`: two `#[ignore]`d measurements (decisions on a fixed span; a lead-in sweep)
and one default-run characterisation pin, `the_timing_search_locks_early_when_a_lead_makes_it_reachable`,
which asserts the measured early lock at lead 16 (objective defect) and lead 32 (range defect) and is
expected to fail when either is fixed. Sabotage-verified.

**#1435 refuted.** On a fixed span, decisions cost ≤ 0.6 dB on `moderate_f1` even for the worst frames
(taken at the symbol boundary). #1435's −2.97 dB median came from where the decoded span started:
re-measuring 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 was read at a
phase-2 AFC correction.

**The defect (#1438), two in one function.** `find_timing_offset_with_expected`'s objective peaks
before the symbol boundary (computed from the two window definitions: 1.190 at d = −9 against 1.000 at
0), and measured it locks at d = −8 wherever that is reachable; and it scans only `0..n`, so for leads
≥ n the boundary is never visited. At lead 0 neither shows. The SNR estimate at the early lock is
capped: 3.05 / 3.50 / 3.55 / 3.55 dB at a true 10 / 20 / 30 / 40 dB. Through `ota_decode_burst` on a
clean 10 dB channel it read 3.0 dB at best, identically in a variant-0-only build, except at j = 20, where only the union decodes
from the earlier 4000 span (−6.0 dB against the variant-0 build's 3.0 dB from 4032) — it predates the union. BPSK's fast SNR climb cannot fire on the OTA path: `hpx_hf`'s
BPSK ceilings are 6.0–9.0 dB.

**Correction to the 2026-09-23 #1428 entry.** "+18/96 via `ota_decode_burst`" is confounded, in an unknown
direction: 17 of the 18 union-only frames were decoded from a span one symbol before the frame, where
the lock is 8 samples early; whether they decode at the boundary is unmeasured. "+10/48 via
`receive_with_fec_mode`" (lead 0) stands.

**Tests → results.** `cargo test -p bpsk-plugin --no-default-features --lib snr_decision_discriminator`:
1 passed, 2 ignored. Both pin assertions were watched failing under sabotage, each naming its defect.
Workspace gate: quoted in the PR.

---

## 2026-09-23 — #1428 the union: both crossfade arms, adjudicated by the FEC

**Requirement/change.** #1363 / #1428: BPSK's crossfade-ISI cancellation wins AWGN decisively and
Expand Down
76 changes: 76 additions & 0 deletions docs/dev/reviews/review-1435-timing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
project: openpulsehf
doc: docs/dev/reviews/review-1435-timing.md
status: resolved
last_updated: 2026-09-24
---

# Adversarial review — #1435's discriminating test, and the timing defect it found (#1438)

Three rounds by Fable. The first reviewed the measurements and conclusions; the second and third
reviewed the actual text of the issue, the #1435 closing comment, the #1437 correction and the ledger
entry before any of it was posted.

## Consumer

- `plugins/bpsk/src/demodulate.rs` — `find_timing_offset_with_expected`, called by
`symbol_stream_parts_with_expected` (every BPSK decode and `estimate_snr_db`), by
`estimate_afc_hz`, and by the #1062 candidate-vetting column.
- `crates/openpulse-modem/src/engine.rs` — `ota_decode_and_ack_inner` feeds the resulting SNR to
`OtaRateController::on_rx_frame` and to `EngineEvent::OtaRateDecision`, which the panel displays.

## Prior art

#1142 fixed a lock that becomes a noise argmax when a long lead-in pushes the preamble out of the
correlation window, by measuring on the decoded span; its engine comment documents the misalignment
curve. `additive_snr_db_windowed`'s doc already states that a decision-directed estimate "saturates
once symbol errors are common". Neither covers a deterministic early lock with the preamble inside
the window.

## Twins

The GPU path has its own timing search, `timing_offset_search_gpu` (#1083; #1433's shape). The #1062
vetting column calls the same function with a candidate `expected`, so its numbers will move with any
fix. Every BPSK rung shares the geometry.

---

## Prompt

**Round 1 — findings.** Sent the fixed-span decisions measurement, the lead-in sweep, the production
lead sweep with logged spans in both builds, the re-measurement of #1435's fixture, and the claim that
the SNR gates use lead-0 fixtures. Asked whether "true 10 dB" was the right reference, for an
explanation of the unexplained sub-symbol mis-lock from the code, why the scan's first successful onset
sits one symbol early, whether the defect predates the union, whether #1142's fix was incomplete, and
which fix direction is right — without implementing.

**Rounds 2 and 3 — the text.** Sent the four texts and the pin, asking first about provenance
(results stated as mine that were the reviewer's), then scope, arithmetic, and whether the pin could
fail on a fix. Round 3 checked only that round 2's corrections were applied faithfully and that the
rewrite introduced nothing new.

## Verdict

**Round 1.** My mechanism ("an odd whole-symbol lead lands the preamble on a zero-correlation lag")
was wrong in a way that changes the fix. The search's objective peaks before the symbol boundary
because the half-Hann window is not matched to the full-Hann pulse. I independently reproduced that
gain table from the code's window definitions. The unexplained sub-symbol mis-lock was the whole
mechanism. My "true 10 dB" reference was confirmed. The decode runs mis-locked too, so passing the
decode's timing to the estimator is a no-op, and a data-aided estimator would not help. It also asked
for the direct test of span causality, which I then ran: 7 of 8 paired differences were reproduced.

**Round 2.** It found two things that changed sentences everywhere. The lock is measured at −8, not
the model's −9. The production case is a RANGE defect as well as an objective defect: the search
scans only `0..n`, so at lead ≥ n the boundary is never visited. My pin's `assert_ne!(off32, 0)` could
not fail on any fix; it was rewritten to assert the measured lock at a reachable boundary (lead 16) and
past it (lead 32). My #1437 correction claimed the +18 was inflated, when 18/96 is already below 10/48;
the direction is unknown. Seed 22's anomaly was explained from my own log (a phase-2 AFC correction).
The review also caught `hpx500`'s ceilings having been cited as `hpx_hf`'s. The cap was measured
rather than derived before posting.

**Round 3.** Corrections applied faithfully, with no meaning inverted. New issues: "within 0.01 dB" is
0.02 on two seeds; "RS absorbs" an inferred mechanism stated as measured; the pin's values quoted from
the 8-seed sweep rather than the pin's own run; and one misworded exception. All applied.

**The pattern, again:** every round found errors in claims about what the code or the data *is*, not in
the code, and round 2 found them in text written to correct earlier errors.
Loading
Loading