20260914 - Learn which Doppler bins are a tone, and refuse them tracks - #33
Conversation
Three quarters of the tracks at both interfered nodes are built on the node's own CW interference: 32 of 42 at fairforest B, 343 of 490 at jn1. They are kinematically flawless, with spans of 26, 20 and 18 km and delay rate agreeing with Doppler rate to 1%, because a tone puts a detection at every delay in every frame, so the filter always finds one exactly where it predicted and the track manufactures its own confirming evidence. Nothing downstream of initiation has grounds to reject them, and every live metric computed today is computed over a population that is three quarters artefact. The two nodes are interfered at different frequencies, +27.9 Hz against a pair around -60 Hz, which is why this is learned rather than shipped: a notch tuned on either misses the other, and a third node has nothing to notch at all. Two statistics per Doppler bin over a rolling window, and a bin has to be high on both. In what share of recent frames it held a detection, which has to be gathered across frames rather than within one because CFAR thins a continuous ridge to one to three peaks and a per-frame test caught 14.4% where this catches 45%. And how far its detections scatter in delay once the drift its own Doppler mandates is removed. That second condition is the one protecting a real target, and removing the drift is what makes it work. A target sitting in one Doppler bin has a near-constant bistatic range rate by definition, so its delay marches steadily and its raw extent grows without limit: measured over a shipped window, 18 resolution cells for an ordinary 45 m/s. Take out the drift its Doppler mandates and an aircraft collapses to under a cell, while a tone, whose peaks land wherever the ridge was brightest that frame, stays scattered across the axis. The bins come from blah2's capture config rather than from a threshold here. c/fs by 1/cpi is the cell blah2's detector reports in, --blah2-config already reads that file for fc, and the node already passes it. Deriving them also settles the two constants Phase 2 needs for R. Suppression is off. Marking is not, so a node records what a suppressing tracker would have refused alongside the ADS-B labels that say whether refusing it would have cost an aircraft, and interference_fraction on the track reports it the way shadow_fraction already does. That gate - zero labelled aircraft lost - is the one this has to pass before it is turned on anywhere, and it cannot be answered from synthetic data. Nothing is judged until the window is full, so a restarted tracker suppresses nothing for its first minute. The cost is one window's worth of artefact tracks per restart; the alternative is convicting a bin on a partial window, where every bin it has seen reads as fully occupied. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @Purple10101's task in 3m 39s —— View job Claude finished @Purple10101's task
|
The drift the map takes out of each Doppler bin is a rate times an elapsed time. Across a clock resync that elapsed time is fiction, so every bin's scatter reads inflated and the one condition protecting a real target is the condition that fails. Clearing the window costs the usual first minute of not judging, which is what a resync should cost. Also from the review of #33: --blah2-config's help still described it as reading fc alone, and is_interfering() narrowed to Python floats where observe() accepts anything finite, so a numpy scalar would have been marked on the way in and read as clean on the way out. Found by claude-review on #33. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @Purple10101's task in 7m 26s —— View job Code review completeTask list:
Ran the Findings1. Promotion event omits 2. 3. 4. A run of 1-2 backward timestamps (below the resync threshold) still gets folded into the occupancy window before any clear 5. SummaryFindings #1 and #2 are the ones I'd block on — both make |
|
Fixed in f64afe6, all three notes. The resync one turned out to be worth a code change rather than a docs line, though not quite for the reason given: the spread is invariant to which sample is Help string updated, and 291 passed / 1 xfailed, ruff, format and the dead-code gate clean. |

Phase 1 of today's tracker plan: make the output and the metrics honest.
Three quarters of the tracks at both interfered nodes are built on the node's own CW interference. 32 of 42 at fairforest B, 343 of 490 at jn1. They are kinematically flawless, with spans of 26, 20 and 18 km and delay rate agreeing with Doppler rate to 1%, because a tone puts a detection at every delay in every frame: the filter always finds one exactly where it predicted, and the track manufactures its own confirming evidence. Nothing downstream of initiation has grounds to reject them, which is why this acts at initiation and nowhere else.
The consequence is that every live metric today is computed over a population that is three quarters artefact, and artefacts score well on kinematic consistency. The 8.7% impossible-track rate is not trustworthy until this is separated.
Why it has to be learned
Two frequencies, so a fixed notch tuned on either one misses the other, and owl-ded9 has nothing to notch at all. What is shared between the sites is the shape of the evidence, not its frequency, so the map is learned at runtime and is a no-op where there is nothing to learn.
What is in here
DopplerOccupancykeeps a rolling window and two statistics per Doppler bin. A bin has to be high on both:The drift removal is what makes the second condition usable, and it is the part I would most like reviewed. A target sitting in one Doppler bin has a near-constant bistatic range rate by definition, so its delay marches steadily and its raw extent grows without limit: over a shipped window that is 18 resolution cells for an ordinary 45 m/s, which plain extent would convict. Removing the drift that the bin's own Doppler implies collapses an aircraft to under a cell (a target drifting right across the bin leaves only a curvature term, a fraction of a cell) while leaving a tone scattered across the whole axis. The spread itself is a 10-90 percentile range rather than max-minus-min, so one stray detection in a bin an aircraft owns cannot convict it.
Also here:
c/fsby1/cpinow comes from blah2's capture config rather than being configured a second time next to it.--blah2-configalready read that file forfcand the node already passes it, sofsandcpicost no new plumbing. This is the same derivation Phase 2 needs forR.Suppression is off
interference.suppressdefaults to false. Marking does not, so a node records what a suppressing tracker would have refused, alongside the ADS-B labels that say whether refusing it would have cost an aircraft, andinterference_fractionreports it per track the wayshadow_fractionalready does. One capture then answers both the pre-change baseline and the validation, instead of needing two.The gate that matters is zero ADS-B-labelled aircraft lost at either node, and it cannot be answered from synthetic data. Nothing here should be turned on until it has been.
Verification
ruff check(E, F, W) clean,ruff format --checkclean,pre-commit run --all-filesclean including the dead-code gate, 290 passed / 1 xfailed.tests/test_interference.pyreproduces all three nodes' measured profiles - how often the bin was busy, how far it spread across the delay axis - and asserts that one set of constants convicts fairforest B at +27.9 Hz, convicts jn1 at both -60 and +60 Hz, and acquits owl-ded9 and a sky of twelve real aircraft crossing the same bins. A later change to either threshold has to answer to all three.Worst case cost, measured: 0.05 ms per frame at a realistic detection rate, 25 ms per 500 ms frame at the pathological limit where every Doppler bin is busy in every frame.
Known and deliberate
Relation to the other open PRs
Independent of #31 and #32, both of which are Phase 0. This branches off main and merges in any order.
🤖 Generated with Claude Code