Skip to content

pipeline: detection freshness, ADS-B field coercion, and monotonic blah2 frames (tranche 1) - #235

Merged
jehanazad merged 5 commits into
mainfrom
fix/pipeline-accuracy-tranche1
Aug 26, 2026
Merged

jehanazad merged 5 commits into
mainfrom
fix/pipeline-accuracy-tranche1

Conversation

@Babissimo

Copy link
Copy Markdown
Contributor

Tranche 1 of the pipeline accuracy audit. Parent: 86cb9bp4q.

Tickets: 86cb9bqkh, 86cb9bqgf, 86cb9bqd9, 86cb9ut1w.

Independent of the tracker PR (retina-tracker#24); the submodule pointer is untouched here and its bump is a separate PR.

Four changes

Only forward blah2 timestamps are accepted (86cb9bqkh). The guard was ts_ms != last_ts, which catches an exact repeat but passes an older frame straight through to the tracker. Nothing between state.frame_queue and Tracker.process_frame re-orders or checks monotonicity.

> is safe here only because _convert_frame's staleness gate already clamps every accepted timestamp to within STALE_THRESHOLD_S of our own clock, bounding a backwards clock step to a ~20 s self-healing stall rather than a permanent one. That coupling is recorded at the call site, because 86cb9bqj3 edits the line directly above it.

This reduces one source of out-of-order frames; it does not eliminate them. FRAME_WORKERS runs several unkeyed workers over one queue, so two frames from the same node can still reach the same tracker out of order. That is the per-node executor work, deliberately out of this tranche.

The newest detection's delay is published, not the oldest (86cb9bqgf). Track.get_recent_detections builds its result newest-first then reverses, so it returns oldest-first. A comment asserted the opposite and a 2026-08-06 commit changed a correct [-1] to [0] on the strength of it. Three other places in the estate already treat the same call as oldest-first.

latest_delay_us builds the ambiguity arc, and for single-node tracks track_gates replaces the icon position with that arc's midpoint, so the aircraft was drawn up to a full window of differential range behind itself, of order 6 km.

Corrected expectation, which differs from the audit's: the accuracy endpoints will not simply improve. _record_accuracy_sample and _refresh_node_verification both score solver_lat/solver_lon, captured before the arc-midpoint override, so they never saw the biased position. What moves is delay matching, since truth association gates on abs(measured - expected) < 15 µs. Expect n_matched to rise, and mean error possibly with it as tracks that previously failed to match rejoin the sample.

ADS-B fields are coerced before arithmetic (86cb9bqd9). tar1090 reports alt_baro as the literal string "ground"; readsb sends a null gs. A bare multiply raises, nothing between _run_geolocation and frame_loop's catch-all wraps it, and the record keeps its freshness stamp, so one such aircraft costs every frame until it ages out. Already recorded twice in this codebase as having taken out /api/test/mlat-accuracy and, on retina-test, the whole map.

The audit found three sites. There were eight, and one of its three was already fixed by #233 the day after it was written. The two it missed that matter most hand the raw value to retina-geolocator, which multiplies it bare outside the solver's try, so it kills the frame from inside the library; one of those is reachable precisely when the obvious site is not. A seventh used float() and so raises ValueError, which a grep shaped on the known TypeError misses entirely.

Grounded aircraft are dropped from the altitude truth term (86cb9ut1w). Coercing "ground" to 0.0 is right for a solver seed and wrong for a truth comparison: "ground" means on the surface, not 0 m MSL, about 313 m at Atlanta field elevation. It also silently changed the sample population, since these aircraft were previously excluded by crashing. They are now excluded deliberately from altitude while still counted for position and velocity.

services/blah2_bridge.py also comes off the coverage omit list, where it was excluded as needing live hardware. It now reports 89%.

Verification

Full suite exit 0 under randomised order, coverage 82.5%, pre-commit run --all-files green, node contract confirmed current. Every production change is mutation-tested: revert it and a named test fails, reproducing the original TypeError or ValueError.

blah2_bridge_task had no coverage at all before this (it is imported only by main.py), so the timestamp guard's behaviour was unobservable in either direction.

Known follow-ups, ticketed not folded in

86cb9t72j the geolocator still multiplies raw fields and is fragile for its other callers, guarded here at the boundary. 86cb9v3jt a third grounded-aircraft altitude site remains in the MLAT verification path, and it is the one the map UI renders. 86cb9t7c4 gs/track arithmetic elsewhere in the backend, no evidence it is live.

Note for reviewers

backend/config/constants.py overlaps with the ADS-B truth query-regions branch. My hunk is additive apart from one module-docstring line; happy to be the one who rebases.

🤖 Generated with Claude Code

@claude

claude Bot commented Aug 25, 2026

Copy link
Copy Markdown

Claude finished @Babissimo's task in 16m 51s —— View job


Claude finished reviewing PR #235

Todo list

  • Gather context (PR description, diff, changed files)
  • Understand the request (code review via /code-review:code-review)
  • Run multi-agent code review against origin/main...HEAD
  • Verify candidate findings against the actual diff
  • Post review feedback

Summary

All four fixes described in the PR body check out against the code:

  • blah2_bridge.py:294-310 — timestamp guard is now ts_ms > last_ts, last_ts starts at 0, and the docstring's coupling to _convert_frame's staleness gate is accurate.
  • passive_radar.py:511-512 / 567 — both latest_delay_us/latest_doppler_hz sites now correctly read [-1] under the oldest-first ordering.
  • _coerced_adsb() (new, passive_radar.py:350-365) and its two call sites, plus state.py's _adsb_for_seeding, known_claiming.py's claim_known_targets, and routes/test.py's validate_ground_truth all correctly route through the new as_num/is_num helpers.
  • _refresh_node_verification (analytics_refresh.py:893-899) and POST /api/test/validate (routes/test.py:307-352) both now null out altitude truth for non-numeric ("ground") readings instead of scoring against a fabricated 0.0, and correctly separate the altitude sample denominator from the position one.

Finding

backend/services/tasks/analytics_refresh.py:1221-1224 — inconsistent coercion introduced within this PR's own edit to _refresh_mlat_verification. This diff converts the function's external-ADS-B truth pool to as_num() (line 1250, same hunk), but the live-ADS-B truth pool three lines above is left on the old pattern:

_gs_raw = entry.get("gs", 0) or 0
gs_ms = (_gs_raw if isinstance(_gs_raw, (int, float)) else 0.0) * 0.514444

isinstance(v, (int, float)) accepts float('nan'), which is exactly what is_num()/as_num() were built to reject (per their docstrings in constants.py). A NaN gs/alt_baro from the live feed — the same class of malformed field this PR spends its whole "ADS-B fields are coerced" commit fixing — would silently poison gs_ms/alt_m for this truth candidate instead of being dropped, feeding a NaN into /api/test/mlat-accuracy's comparisons. Since this is the same function, same commit, and the sibling pool 15 lines below was already converted, this reads like an oversight rather than a deferred scope cut. Fix this →

Minor nit

backend/services/known_claiming.py:124-125_tag_velocity's docstring says it applies "the same conversion state._adsb_for_seeding applies," but this PR's own state.py edit moved _adsb_for_seeding onto as_num() (which rejects NaN) while _tag_velocity still uses (tag.get("gs", 0) or 0). Not a functional bug — the c3e1b27 commit message notes the only non-numeric gs/track in the repo is a test fixture — but the docstring is now inaccurate about the two functions being equivalent.

Already covered by disclosed follow-ups (verified, not re-flagged)

The multi-agent pass also surfaced several additional un-migrated gs/track/altitude sites (passive_radar.py:606-607 & 644-645, known_claiming.py:126-128, blah2_bridge.py:268-270, and a third grounded-aircraft altitude site in _refresh_mlat_verification's match-scoring at line 1498). All of these fall cleanly under the PR body's own "Known follow-ups" tickets (86cb9t72j, 86cb9t7c4, 86cb9v3jt) — including the map-UI detail, which the PR body states correctly. No action needed beyond what's already ticketed.

@claude

claude Bot commented Aug 25, 2026

Copy link
Copy Markdown

Claude finished @Babissimo's task in 2m 23s —— View job


Claude finished reviewing PR #235

Todo list

  • Gather context (PR description, diff, changed files)
  • Understand the request (re-run code review via /code-review:code-review per custom instructions)
  • Confirm previously-flagged issues are addressed by the new 4838bdf commit
  • Run full review against origin/main...HEAD
  • Verify candidate findings against the actual diff
  • Post review feedback

Summary

This sync added one commit, 4838bdfanalytics: use one coercion for both MLAT truth pools — since the last review pass. It directly fixes both items raised previously:

  • backend/services/tasks/analytics_refresh.py:1218-1221 — the live-ADS-B truth pool in _refresh_mlat_verification now goes through as_num() (same as the external pool at line 1247), replacing the hand-rolled isinstance(v, (int, float)) check that admitted NaN/inf. A new regression test, test_non_finite_feed_values_do_not_poison_the_truth_pool in test_mlat_verification.py, seeds a NaN alt_baro / inf gs and asserts the four published error figures stay finite — it fails against the old isinstance form, matching the mutation-testing standard the rest of the PR holds itself to.
  • backend/services/known_claiming.py:124-128_tag_velocity's docstring no longer claims parity with state._adsb_for_seeding (which moved to as_num earlier in the branch); it now states plainly that node-tag gs/track are left uncoerced and points at the tracking ticket (86cb9t7c4) for closing that gap.

Both are exactly what my last pass asked for, nothing more, nothing less.

Fresh pass over the full diff

I re-read every changed source file against origin/main (constants.py, state.py, passive_radar.py, blah2_bridge.py, known_claiming.py, routes/test.py, analytics_refresh.py, pyproject.toml) rather than relying on the earlier pass. Findings:

  • Timestamp guard (blah2_bridge.py:294,310): last_ts = 0 initial value, ts_ms > last_ts guard, update-on-accept — correct forward-only semantics, and TestFrameOrdering's four cases (first frame, repeat, older, newer-after-older) exercise exactly the behavior described.
  • Delay/doppler ordering fix: all four sites now consistently read [-1] for "newest" against oldest-first get_recent_detections()/event["detections"] output (passive_radar.py:511-512, 567), and the n=1 get_recent_detections call at line 571 is unaffected by ordering since a single-element list makes [0]/[-1] equivalent — correctly left alone.
  • ADS-B coercion: _coerced_adsb() (passive_radar.py:350-365) coerces only the four numeric keys and explicitly avoids adding keys the geolocator branches on ("gs" in adsb) — a subtlety called out in its own docstring and easy to get wrong. All eight sites named in the PR body check out against the code.
  • Grounded-aircraft altitude truth: _refresh_node_verification (analytics_refresh.py:893-902) and POST /api/test/validate (routes/test.py:307-352) correctly null out altitude on "ground"/non-numeric rather than substituting 0.0, and separate the altitude sample denominator from the position one (n_altitude_samples, None medians when empty rather than a fabricated 0).
  • The third disclosed grounded-aircraft site (analytics_refresh.py's match-scoring path, still feeding raw as_num-coerced 0.0 altitude into the _MLAT_ALT_GATE_M gate and alt_err around line 1495) is unchanged by this PR and remains correctly tracked under 86cb9v3jt, not silently left in ambiguous territory.

No new issues found. I wasn't able to execute pytest in this sandbox (Bash requires interactive approval not available in this run), so this pass is a static read against the diff and the new tests' logic, not a live run — consistent with what the PR body reports CI already confirmed (full suite green, 82.5% coverage, mutation-tested).

@Babissimo

Copy link
Copy Markdown
Contributor Author

Both review findings were real, and both were inconsistencies this branch introduced. Fixed in 4838bdf.

The MLAT live truth pool now uses as_num like its sibling. The review is right that this reads as an oversight rather than a scope cut: the external pool twenty lines below was converted in this same branch, and leaving the live one on a hand-rolled isinstance split one function across two coercion idioms.

It also matters more than a consistency nit. isinstance(v, (int, float)) admits NaN, and json.loads parses a bare NaN literal by default, so a node emitting non-standard JSON puts a genuine float NaN into state.adsb_aircraft — which the live pool reads directly. A NaN there does not raise: it propagates through every comparison as NaN, so the aircraft still matches, still publishes, and the altitude and velocity figures on /api/test/mlat-verification go quietly non-numeric. Confirmed isinstance(float("nan"), (int, float)) is True and json.loads('{"gs": NaN}') yields a float NaN.

Added a regression test asserting the four published figures stay finite; it fails against the isinstance form and passes with as_num. Verified the only behavioural change is for NaN and infinity, both to the safe value: numeric, absent, zero and "ground" inputs are unaffected.

The _tag_velocity docstring is corrected. It claimed equivalence with state._adsb_for_seeding, and moving that function onto as_num earlier in this branch made that false. It now says what it actually does and points at 86cb9t7c4, which tracks closing the gap. Behaviour deliberately unchanged, per that ticket.

While in the block I also dropped the incident narration from the comment above it, keeping only the constraint that still binds. The workspace rules say to delete a passage describing a fixed fault rather than keep it.

Full suite green (82.54%), pre-commit run --all-files green.

Thanks for checking the follow-up tickets rather than re-flagging what they cover — that is exactly the right call on the sites under 86cb9t72j, 86cb9t7c4 and 86cb9v3jt.

@Babissimo
Babissimo marked this pull request as draft August 25, 2026 16:53
@Babissimo
Babissimo marked this pull request as ready for review August 25, 2026 16:55
@claude

claude Bot commented Aug 25, 2026

Copy link
Copy Markdown

Claude finished @Babissimo's task in 2m 49s —— View job


Claude finished reviewing PR #235

Todo list

  • Gather context (PR description, diff, changed files, prior review comments)
  • Confirm current HEAD and whether anything changed since the last review pass
  • Run full review against origin/main...HEAD
  • Verify candidate findings against the actual diff
  • Post review feedback

Summary

HEAD is still 4838bdf, identical to what the previous review pass (16:46 UTC) already covered, and both findings from that pass are confirmed fixed:

  • analytics_refresh.py:1218-1221 — live-ADS-B truth pool now uses as_num(), matching the external pool at line 1247, and is covered by the new test_non_finite_feed_values_do_not_poison_the_truth_pool.
  • known_claiming.py:124-128_tag_velocity's docstring no longer claims parity with state._adsb_for_seeding.

I re-verified all four headline fixes against the code directly rather than re-trusting the prior passes: the blah2_bridge.py:294-310 forward-only timestamp guard, the passive_radar.py delay/doppler ordering fix (all four sites correctly read [-1] against oldest-first get_recent_detections()), the _coerced_adsb() helper and its call sites, and the grounded-aircraft altitude-truth nulling in analytics_refresh.py:893-902 and routes/test.py:307-352. All check out as described.

New finding

backend/pipeline/passive_radar.py:606-607 and :644-645 — inconsistent coercion within a block this PR itself edited. In _run_geolocation, both sites read adsb = _state.adsb_aircraft.get(adsb_hex) and then:

_gs_ms = (adsb.get("gs", 0) or 0) * 0.514444
_trk = math.radians(adsb.get("track", 0) or 0)
existing.alt_m = as_num(adsb.get("alt_baro")) * FT_TO_M   # <- fixed by this PR

alt_baro was switched to as_num() in this same commit, but gs/track on the two lines directly above were left on the old (v or 0) pattern. That pattern only guards against None (readsb's null gs) — it does not guard against NaN, and this PR's own regression test (test_non_finite_feed_values_do_not_poison_the_truth_pool, added for the sibling fix in analytics_refresh.py) proves state.adsb_aircraft can carry a real float NaN, since json.loads parses a bare NaN literal and frame_processor.py:448-457 / tcp_handler.py:580-589 write _ae.get("gs", 0) straight into state.adsb_aircraft with no coercion at write time — the exact same dict the just-fixed live truth pool reads.

NaN or 0 evaluates to NaN (truthy), so a NaN gs or track here doesn't raise — it silently produces NaN vel_east/vel_north on the live GeolocatedTrack (the object that feeds frontend dead-reckoning per the comment at line 600-602), not just an internal accuracy metric. This is a live path: _state.adsb_aircraft is populated from raw per-frame node JSON on every frame with an ADS-B tag, not a dead branch.

This one, unlike the sibling sites your review labeled "no evidence it is live" (86cb9t7c4), is live — same dict, same malformed-JSON vector the PR just fixed and tested for one line above. The two existing tests targeting this exact code (test_refresh_between_solves_coerces_to_zero, test_adsb_bootstrap_after_solver_failure_coerces_to_zero) only assert the alt_baro coercion; test_null_ground_speed_does_not_kill_the_frame only covers None, not NaN, so the gap is untested as well as unfixed. Fix this →

Everything else re-checked, no issues

constants.py's is_num/as_num helpers, state.py's _adsb_for_seeding, routes/test.py's validate_ground_truth denominator split, and pyproject.toml's coverage-omit removal all match the PR description exactly. known_claiming.py:126-128's _tag_velocity remains deliberately uncoerced per its corrected docstring and 86cb9t7c4 — that one's fine as disclosed, since it reads a per-tag value, not state.adsb_aircraft directly, and the NaN vector demonstrated above hasn't been shown against it specifically.
· branch fix/pipeline-accuracy-tranche1

Babissimo and others added 5 commits August 26, 2026 10:51
The duplicate guard tested `ts_ms != last_ts`, which catches an exact
repeat but passes an older frame straight through to the tracker. Nothing
between `state.frame_queue` and `Tracker.process_frame` re-orders or
checks monotonicity, and `process_frame` computes dt by bare subtraction,
so an older frame yields a negative dt -- the head of the chain the
tracker-side fixes close.

`>` is safe here only because `_convert_frame`'s staleness gate already
clamps every accepted timestamp to within STALE_THRESHOLD_S of our own
clock, which bounds a backwards clock step to a stall of twice that
rather than a permanent one. That coupling is not obvious from the call
site, so it is recorded there: the gate has to stay above this guard.

blah2's timestamps are POSIX epoch from system_clock, not uptime, so a
blah2 restart cannot regress them; and at a 0.5 s CPI against 1 ms
resolution two distinct frames cannot share a timestamp, so `>` discards
nothing that `!=` kept.

`blah2_bridge_task` had no test coverage at all -- it is imported only by
main.py -- so the guard's behaviour was unobservable in either direction.
Four cases now pin it, including that a rejected out-of-order frame does
not wedge the node against the next good one.

This reduces one source of out-of-order frames; it does not eliminate
them. FRAME_WORKERS runs several unkeyed workers over one queue, so two
frames from the same node can still reach the same tracker out of order.
That is the per-node executor work, deliberately out of this tranche.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…bqgf)

`Track.get_recent_detections` builds its result newest-first and then
reverses before returning, so it hands back oldest-first. Nothing on the
path from tracker to solver re-sorts: the event writer stores the list
verbatim, GeoTrack keeps the same object, and the geolocator only reads
it. So `[0]` is the OLDEST detection in the window, not the newest.

A comment above the site asserted the opposite and a 2026-08-06 commit
changed a correct `[-1]` to `[0]` on the strength of it. Three other
places in the estate already treat the same call as oldest-first
(frame_processor's `hist[-N:]`, analytics' `hist[-1]`, aircraft_feed's
`reversed(meas)`), so the convention was never in doubt anywhere else.

`latest_delay_us` builds the ambiguity arc, and for single-node tracks
track_gates replaces the icon position with that arc's midpoint. Reading
the oldest detection therefore drew the aircraft up to a full window of
differential range behind itself, of order 6 km at ~1 µs/s over a
20-detection window.

Expected effect, corrected from the audit's framing: the accuracy
endpoints will NOT simply improve. `_record_accuracy_sample` and
`_refresh_node_verification` both score `solver_lat`/`solver_lon`, which
track_gates captures BEFORE the arc-midpoint override, so they never saw
the biased position. What does move is delay matching -- truth
association gates on `abs(measured - expected) < 15 µs`, and a stale
delay drifts outside that window -- so expect n_matched to rise, and
mean error possibly with it as tracks that previously failed to match
rejoin the sample. The arc is still anchored at the oldest epoch inside
the solver itself (lm_solver_track's t0), which is a separate ticket.
The fresh-fix injection writes the current position onto that same
oldest epoch, so its guess carries a window of lag. Left as it stands,
since LM refines the guess, with the ordering recorded in a comment so
the site stops reading as though `[0]` meant newest.

The test fixtures asserted the inverted convention deliberately, so they
had to be rebuilt rather than adjusted: the module docstring stated it as
a finding, `_event` took its timestamp from `detections[0]`, and the one
ordering-sensitive fixture was written descending. Two cases are added
that a single-detection event could not distinguish -- one for the
multi-detection materialised path, which is what would have caught the
second site, and one for the ADS-B identity evidence derived from the
same value, since that authorises calibration points and its derivation
was previously uncovered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…(86cb9bqd9)

tar1090 reports alt_baro as the literal string "ground" for aircraft on
the ground, and readsb sends a null gs for aircraft reporting no
velocity. A bare multiply on either raises, and nothing between
`_run_geolocation` and `frame_loop`'s catch-all wraps it, so one such
aircraft costs the entire frame: track association, the
`state.adsb_aircraft` refresh and the archive append are all skipped, and
`frames_processed` never advances. The record keeps its freshness stamp,
so it stays inside the 60 s window and every subsequent frame dies the
same way until it ages out. This is the failure already recorded at
analytics_refresh.py and frame_processor.py, which took out
/api/test/mlat-accuracy and, on retina-test, the whole map.

The audit found two sites. There are four, and the two it missed are the
ones that matter, because both hand the value to the geolocator, which
multiplies alt_baro, gs and track bare and outside the solver's try:

- the fresh-fix injection, which overwrites detection[0]'s tag;
- every other detection's inline tag, which is what reaches the solver
  when the injection does not fire -- no live fix, one older than 60 s,
  or a hex whose case does not match, since the three writers normalise
  the key while the tracker carries `adsb["hex"]` raw.

Coercing at both boundaries rather than in the geolocator: that is a
separate submodule, and this repo should not hand a library a value the
library cannot use. The library stays fragile for its other callers,
which is a ticket of its own.

The tag coercion rewrites only keys already present. The geolocator
branches on `"gs" in adsb` to choose its velocity path, so adding a key
would change behaviour rather than preserve it.

Uses `_as_num` from the seeding path rather than the isinstance guard the
audit prescribed, and lifts it to config.constants as `as_num` so there
is one definition beside FT_TO_M instead of a private helper reached
through a deferred import. It rejects NaN as well as strings; the
prescribed variant passes NaN through, which would poison alt_m silently
rather than loudly. The audit's third site, in core/state.py, was fixed
upstream by e48cde1 the day after the audit was written.

`_coerced_adsb` guards on `isinstance(tag, dict)`, not truthiness. A node
can put anything in `frame["adsb"][i]` -- passive_radar copies it onto the
detection with no type check, and `DetectionRequest.frames` is `list[dict]`
with extras allowed, so nothing validates the entries inside a frame;
`tcp_handler` already skips non-dicts on the same list. Unpacking a truthy
non-dict raises `'str' object is not a mapping` from `_geolocate_track_event`,
which sits outside both trys in the file, so the coercion would have caused
the very frame loss it exists to prevent. Passing the value through restores
what happened before this helper existed: the geolocator's `"lat" not in adsb`
is a valid membership test on a string or a list, and yields no ADS-B guess.

At the verification refresh the original was `float()`, not a bare multiply,
so `as_num` narrows the domain as well as fixing it: a numeric-string altitude
now reads as 0 rather than converting. That is deliberate. Every other reader
of `state.adsb_aircraft` already resolves a numeric string to 0, so this makes
the published truth agree with the altitude the pipeline itself used; and the
alternative, a try/except around `float()`, would readmit NaN and inf into
`altitude_error_m` silently, which is the failure mode as_num exists to avoid.

The ticket's criterion is the whole backend, not the frame path, so the
sweep was re-run across it and three more raw multiplies came up. The
node-supplied tag in the known lane fails open since e48cde1, so a
grounded aircraft there degrades that lane silently and ticks
`known_claims_errors` rather than killing the frame. The truth candidate
in the per-node verification refresh goes through `float()`, which raises
ValueError rather than TypeError, and the caller's blanket except then
leaves that node with no verification payload at all. `POST
/api/test/validate` guards on truthiness, which "ground" passes. A
fourth, the external-cache fallback in `_refresh_mlat_verification`, is
coerced for the grep's sake only: that cache is written solely by
periodic.py, whose entries carry alt_m and no alt_baro key at all, so
there is no failure for a test to reproduce. The geolocator's own
multiplies are not in the backend and stay on their own ticket.

The two `or 0` sites on gs and track in `_tag_velocity` keep it. Every
comment in the estate names alt_baro alone, that matches the readsb
schema where the string sentinel is an altitude convention, and the only
non-numeric gs or track anywhere in the repo is an invented test
fixture. What is different at the two
sites changed here is that the value crosses into code that cannot guard
itself.

`services/blah2_bridge.py` comes off the coverage omit list: it was
excluded as needing live blah2 hardware, which the tests added with the
timestamp guard show is no longer true. It now reports 89%.

Every test here reproduces the production failure when its guard is
removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…cb9ut1w)

tar1090 reports alt_baro as "ground" for an aircraft on the surface. That
is a statement about position, not altitude: the aircraft is at field
elevation, roughly 313 m at Atlanta and 0 m almost nowhere. Coercing the
sentinel to 0.0 is sound for a solver seed, where the value is a starting
point the solve moves off, and wrong at the two sites that score the
solver against it, where it charges the whole of the field elevation to
altitude error and drags the published mean down with it.

Before this branch, float("ground") raised and the caller's blanket except
dropped the node's entire verification payload, so a grounded aircraft
never reached altitude_error_m at all. Coercing the sentinel silently
admitted a class of aircraft the accuracy sample had never contained. This
restores that exclusion deliberately, and narrows it to the altitude term
alone: a grounded aircraft is good position and velocity truth, so it
stays in those samples, and only the altitude comparison skips it.

The predicate is is_num() on the raw field, not on as_num()'s output,
which flattens absent, non-numeric and a genuine 0 ft to the same 0.0. It
is not keyed on the literal "ground": under tar1090's schema a present
alt_baro that is not a finite number is the sentinel, and for a truth
comparison dropping any other non-numeric value is the conservative
reading. as_num() is now defined through is_num() so the two cannot drift.

Exclusion is null in both payloads, matching the else-None arm truth_alt_m
already carried. POST /api/test/validate gains n_altitude_samples, since
its mean now runs over a denominator smaller than the match count, and its
altitude aggregates report null rather than 0 when nothing carried
altitude truth: 0 m of error would read as a perfect altitude solve.

The frontend is unaffected. Its one altitude_error_m reader takes the MLAT
payload, built by a separate emitter from its own truth pool.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`_refresh_mlat_verification` builds its truth pool from two sources. The
external one was moved onto `as_num` earlier in this branch; the live one,
twenty lines above it, was left on a hand-rolled isinstance test. That
split was introduced here, so it is closed here.

The two are not equivalent. `isinstance(v, (int, float))` admits NaN and
infinity, which is exactly what `as_num` exists to reject and the reason
this branch chose it over the guard the audit prescribed. `json.loads`
parses a bare `NaN` literal by default, so a node emitting non-standard
JSON puts a genuine float NaN into `state.adsb_aircraft`, and the live
pool reads that store directly.

A NaN in the truth pool does not raise. It propagates through every
comparison as NaN, so the aircraft still matches, still publishes, and
the altitude and velocity errors on /api/test/mlat-verification go
quietly non-numeric. The regression test asserts the four published
figures stay finite, and fails against the isinstance form.

Also drops the incident narration from the comment above it, keeping only
the constraint that still binds: the sentinel arrives, and an isinstance
test lets NaN through. The history is in git.

`_tag_velocity`'s docstring claimed it applied the same conversion as
`state._adsb_for_seeding`. Moving that function onto `as_num` earlier in
this branch made the claim false, so it now says what it actually does
and points at the ticket that would close the gap.

Both found by the automated review on the PR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Babissimo
Babissimo force-pushed the fix/pipeline-accuracy-tranche1 branch from 4838bdf to d3da82f Compare August 26, 2026 09:56
@claude

claude Bot commented Aug 26, 2026

Copy link
Copy Markdown

Claude finished @Babissimo's task in 1m 47s —— View job


Claude finished reviewing PR #235

Todo list

  • Gather context (PR description, diff, prior review comments, current HEAD)
  • Confirm what changed since the last review pass
  • Run full review against origin/main...HEAD
  • Verify candidate findings against the actual diff
  • Post review feedback

Summary

HEAD (d3da82f) carries the identical tree to 4838bdf, the commit the last two review passes already covered (same 13 files, same +640/-62 diff stat). This sync is a rebase onto the newly-merged #242 (Joseph-form covariance fix), not new content — no commits were added or changed since the 16:55 UTC pass.

Re-verified independently rather than trusting that:

  • blah2_bridge.py:294-310 — forward-only (ts_ms > last_ts, last_ts = 0 initial) timestamp guard, matches the ticket and its own tests.
  • passive_radar.py delay/doppler ordering — all four sites (511-512, 567) correctly read [-1] against oldest-first get_recent_detections().
  • _coerced_adsb() (passive_radar.py:350-365) and its call sites, state.py's _adsb_for_seeding, routes/test.py's validate_ground_truth — all route through the new constants.is_num/as_num helpers as described.
  • Grounded-aircraft altitude truth nulling — analytics_refresh.py:893-902 and routes/test.py:307-352 correctly separate the altitude denominator from the position one.
  • Both items raised in the 16:46 UTC pass (analytics_refresh.py's live-ADS-B truth pool now on as_num, known_claiming.py's _tag_velocity docstring correction) remain fixed, with the new test_non_finite_feed_values_do_not_poison_the_truth_pool regression test in place.

Outstanding finding (raised in the previous pass, still unaddressed)

backend/pipeline/passive_radar.py:605-607 and :643-645 — inconsistent coercion within a block this PR itself edited, still present at current HEAD:

_gs_ms = (adsb.get("gs", 0) or 0) * 0.514444
_trk = math.radians(adsb.get("track", 0) or 0)
existing.alt_m = as_num(adsb.get("alt_baro")) * FT_TO_M   # <- this line was fixed by this PR

(the second site at 643-645, in the solver-failure ADS-B fallback path, has the same gs/track pattern without a neighboring as_num line to contrast against, but is fed from the same adsb dict).

alt_baro was switched to as_num() in this PR's own commit, but gs/track on the surrounding lines were left on (v or 0), which only guards None — not NaN. This PR's own new regression test (test_non_finite_feed_values_do_not_poison_the_truth_pool) demonstrates that state.adsb_aircraft can carry a real float NaN (a node emitting a bare NaN JSON literal parses as one via json.loads), and that's the exact same dict both of these sites read via _state.adsb_aircraft.get(adsb_hex). NaN or 0 evaluates to NaN (truthy), so this doesn't raise — it silently produces NaN vel_east/vel_north on the live GeolocatedTrack, which the comment at 598-600 says feeds frontend dead-reckoning. That's a live map-rendering path, not just an accuracy metric.

Existing tests only cover the None case (test_null_ground_speed_does_not_kill_the_frame) and the alt_baro coercion (test_refresh_between_solves_coerces_to_zero, test_adsb_bootstrap_after_solver_failure_coerces_to_zero) — there's no test for gs/track NaN at either site.

Fix this →

Everything else re-checked, no issues

constants.py's is_num/as_num, pyproject.toml's coverage-omit removal, and the rest of the test additions match the PR description exactly. known_claiming.py:126-128's _tag_velocity remains deliberately uncoerced per its corrected docstring and 86cb9t7c4 — fine as disclosed.
· branch fix/pipeline-accuracy-tranche1

@jehanazad
jehanazad merged commit ec2afb7 into main Aug 26, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants