Conversation
The dark lane's ghost frames (feed entries >5 km from any aircraft, 10-14% of dark entries on the test droplet) are not mostly bad births: 58% of them sit on keys that had already accumulated four-or-more-node solves, and what moved them was a two- or three-node solve joining by proximity. Those joins are wrong far more often than their node count suggests -- 25% of n=3 joins land >3 km off (36-62% at n=2) against 5% at n=4 and 2% at n>=5 -- and nothing at solve time separates a good one from a bad one, because a three-node free-altitude fit is exactly determined and its rms is ~0 either way. What can separate them is the track. A key with several four-plus-node solves behind it knows its position to a few hundred metres, and 250 m/s x 5 s is 1.25 km, so a 4 km innovation is not flight. Two env-flagged behaviours in services/track_filter.py, both defaulting to today's: - TRACK_KF_R_SOURCE=uncertainty takes _measurement_R's additive base term from solve_uncertainty.solve_sigma_m instead of the flat 1200 m, so the filter trusts a solve as much as the map's uncertainty disc claims to and prices an n=2 join differently from an n=5 one. Under the flat base an innovation must reach ~5 km to breach chi2 at all, which is already outside the solver's 6 km proximity gate -- so this is also what makes the gate able to see the 4 km population. - TRACK_KF_OUTLIER_MODE=hold, dark keys only: a gate breach on an ESTABLISHED entry (>=2 accepted updates) with |innovation| > 1500 m is held rather than re-anchored -- the state advances by the coast and the solve is published at the predicted position, so the key is refreshed but does not move. Three consecutive holds concede: the new position has been confirmed, the identity really changed. Every return path now stamps kf_action/kf_d2/kf_innov_m, solver.py copies them onto the history record, and dark-lane kf_held/kf_reanchored counters land in /api/test/solver-stats. With both flags at their defaults nothing changes but those three diagnostic fields, so a capture can measure what the other policy would have shown before anything is switched on. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Claude finished @jehanazad's task in 8m 47s —— View job Code ReviewTasks
Findings1. A "held" result still ships the outlier's altitude, velocity, and node-count-derived fields — held = dict(result)
held["lat"] = round(lat_h, 6)
held["lon"] = round(lon_h, 6)
held["smoother"] = "kf"
held["kf_pos_sigma_m"] = round(...)
return _stamp(held, "held", d2, innov_m)
The whole premise of None of the new tests ( 2. New env knobs aren't added to
3. Sigma formula and ENU→latlon conversion duplicated between held["kf_pos_sigma_m"] = round(math.sqrt(max(0.0, 0.5 * (p_pred[0, 0] + p_pred[2, 2]))), 1)
...
smoothed["kf_pos_sigma_m"] = round(math.sqrt(max(0.0, 0.5 * (p_upd[0, 0] + p_upd[2, 2]))), 1)plus the matching Everything else — the R-source calibration wiring, the gate/hold/streak state machine, the lock discipline, and the diagnostic stamping ( |
…old for cap20 Live measurement of PR #305 against the cap17/cap19 overlay. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Live measurement (cap20, test droplet, stack a351bdd, 00:32–00:52Z 2026-09-06, Thin fleet (4–7 dark aircraft vs 11–12 in cap17), 600 frames, 1141 GT-matched dark records.
KF action audit (per published solve, raw solve error vs published error):
The hold fires on the wrong side of the ledger: 9 of the 21 held solves with ground truth were correct solves whose published position became the coasted prediction 4 km off, and the 12 bad ones were published at the prediction, which was itself 6.6 km off (the "established" track that earned the hold was already wrong). Uncertainty-weighted R did not stop bad n=3 joins on established keys either (30 smoothed bad solves published at 5.6 km, unchanged from raw). Ghost causes are unchanged in shape: 73 % bad bottom-up solves (err>3 km), 13 % bad follow solves. Verdict: flags stay at their defaults ( 🤖 Generated with Claude Code |
|
Closing per the 2026-09-10 review against main 886a342. The cap20 measurement above stands (ghost rate 3.3% -> 22.8%, hold firing on correct solves). The premise is also gone: #310 retries a gate breach with manoeuvre process noise before re-anchoring (a 4 km innovation scores d2~4 against the 13.8 gate and is rescued, not re-anchored), #329 already prices dark n=2 solves at 2500 m by default, and the dark sigma floors this PR's calibration cites are now 2100/850/240 m. Merging the hold would either pre-empt #310 on real turns or sit downstream where it never fires. The per-record kf_action / kf_d2 / kf_innov_m diagnostics are being carried forward on a fresh branch. |
Why
Dark-lane feed entries more than 5 km from any aircraft ("ghosts") are 10–14% of dark entries
(test droplet, 20-min captures 15 and 17, 2026-09-05). Tracing each ghost frame to the solve that
governs it: 58% sit on keys that had already accumulated four-or-more-node solves, and what
moved them was a two- or three-node solve joining by proximity. Those joins are wrong far more
often than their node count suggests:
Nothing at solve time separates a good one from a bad one: a three-node free-altitude solve is
exactly determined (six measurements, six unknowns), so its rms is ~0 whether it is right or
wrong. What can separate them is the track — a key with several four-plus-node solves behind it
has a Kalman state good to a few hundred metres, and a 4 km innovation is not a manoeuvre
(250 m/s × 5 s = 1.25 km). Today
services/track_filter.pylets it through anyway:_measurement_Rtrusts every solve at a flat 1,200 m base regardless of node count, and a gate breach re-anchors
the whole filter at the outlier. Measured: a bad three-node join onto a four-plus key has raw
error 4.1 km and published (smoothed) error 3.3 km.
What
Two env-flagged behaviours in
services/track_filter.py, both defaulting to today's behaviour,both read per call like
TRACK_SMOOTHER:TRACK_KF_R_SOURCE=flat(default) |uncertainty.uncertaintytakes_measurement_R'sadditive base term from
solve_uncertainty.solve_sigma_m(result, dark=…)— the same calibratednumber the map draws its uncertainty disc from (floors 650/210/180 m at n=2/3/≥4, ×1.5 dark) —
instead of the flat 1,200 m. The inflated formal
cov_en_km2term, the PSD rejection and thefinal clamp are untouched; only
basechanges.TRACK_KF_OUTLIER_MODE=reanchor(default) |hold. Inhold, on dark keys only, agate breach (χ² > 13.8) on an established entry (≥
TRACK_KF_HOLD_MIN_UPDATES= 2 acceptedposition updates) whose innovation exceeds
TRACK_KF_HOLD_MIN_INNOV_M= 1500 m is held: theupdate is not applied, the anchor does not move, the state advances by the coast, and the solve
is published at the predicted position — the key is refreshed but does not move.
TRACK_KF_HOLD_MAX_STREAK= 3 consecutive holds concede and re-anchor (the new position keepsbeing confirmed, so the identity really changed). A good solve in between clears the streak.
A measured note that matters for how these ship: under the flat base an innovation must reach
~5 km before it breaches χ² at all, which is already outside the solver's 6 km proximity gate — so
with a flat R there is almost no in-gate bad join for
holdto act on (which is also why today'sbad joins arrive as smoothed 3.3 km errors rather than re-anchors). Tightening the base to the
calibrated per-solve sigma is what lets the gate see the 4 km population in the first place. The
test overlay will therefore run
hold+uncertaintytogether for a capture.Measurement
Every
_smooth_kfreturn path now stampskf_action(
init/passthrough/smoothed/held/reanchored),kf_d2(2 dp, None when no innovation wascomputed) and
kf_innov_m(1 dp).solver.py's_record_solve_historycopies all three onto thehistory record beside
kf_pos_sigma_m, and dark-lanekf_held/kf_reanchoredcounters are bumpedin the publish path and exposed in
/api/test/solver-stats'sfragmentationblock. With bothflags at their defaults nothing changes but those three diagnostic fields, so a capture can say
what each policy would have shown for the same solves before anything is switched on.
Verified
tests/test_track_filter.py— newTestRSource(8),TestHoldOutliers(9),TestHoldThroughTheSolver(2);all 29 pre-existing tests unchanged and green:
52 passed, 1 skipped.tests/test_solver_stats.py—56 passed(two key-set pins updated for the new fragmentationfields, plus a new counter test).
tests/test_mn_lifetime.py23 passed,tests/test_solver_anchor.py30 passed.2803 passed, 2 skipped in 144.46s.pre-commit run --all-files: all hooks pass (ruff check / format / vulture / config x2).Nothing is deployed and no docker-compose file is touched. Stacked on #298
(
fix/kf-covariance-psd).🤖 Generated with Claude Code