Skip to content

Publish a synthetic node's declared cone as its detection area - #32

Merged
jehanazad merged 1 commit into
feat/evidence-only-coveragefrom
feat/declared-wedge-polygon
Sep 15, 2026
Merged

jehanazad merged 1 commit into
feat/evidence-only-coveragefrom
feat/declared-wedge-polygon

Conversation

@jehanazad

Copy link
Copy Markdown
Contributor

Summary

Adds EmpiricalCoverageState.declared_wedge_polygon() and a per-node declared_geometry_is_truth flag on NodeAnalyticsManager.register_node, so a node whose declared cone is its detection area (a simulator node) is published as that cone instead of as its accumulated evidence. Real nodes are unchanged: evidence-only polygon, now tagged polygon_source: "evidence" ("learned" under FOV shadow/active).

Stacked on #28 (base branch is its head); merge #28 first and GitHub retargets this one to main.

Why

The simulator only emits a detection for an aircraft inside a node's declared cone (retina_simulation/world.py::_aircraft_in_detection_cone), so for a synthetic node the wedge is ground truth. The calibration points that feed the evidence polygon come from ADS-B hexes bound to tracks, and a steady share of those binds are the wrong aircraft. Measured on the test deployment 2026-09-13 from the persisted bins: one 42° node held 3,037 points of which 47 % lay outside its wedge (34 % ignoring the two edge bins); 55 out-of-wedge bins had reached FOV_OPEN_MIN_POINTS, and the published polygon covered 71 of 72 bearings. Across an eight-node sample 5–41 % of points were fully outside the wedge, yet even a 5 % node opened 15+ stray bearings, because three points open a bin.

Changes

  • declared_wedge_polygon(step_deg=2.5): apex, both wedge edges exactly plus every step_deg between, apex — radius per bearing is _reach_at (the bistatic ellipse when TX + differential limit are declared, else the monostatic circle: the simulator's own range rule), no clamp multiplier, no bins read. Omni prior → closed ring, no apex.
  • register_node(..., *, declared_geometry_is_truth=False): membership in _declared_truth asserted per registration, dropped on retire / reset / loss of geometry; a membership change invalidates the summaries cache.
  • get_node_summary: declared nodes publish the wedge with polygon_source: "declared" regardless of n_points, and skip the evidence-derived FOV diagnostics; everyone else gains polygon_source.

Test coverage

  • tests/test_declared_wedge.py: 26 new tests — vertex bearings within half-width and ranges at _reach_at (±1 %), both edges present, closure, omni ring, elliptical vs circular reach, manager publication for flagged / unflagged nodes, zero-point publication, flag drop on re-register and retire, cache invalidation.
  • Full suite: 510 passed. pre-commit run --all-files: ruff, ruff-format, vulture, ruff-config all pass.
  • Independent cross-check (not in the repo): the wedge built from the real persisted bins of the 42° node above agrees with SimulationWorld._aircraft_in_detection_cone on 25,919 / 25,921 grid points; both disagreements sit on the boundary.

Review notes

  • Vertices are placed with the flat offset_latlon step every polygon in the file uses, so reading a vertex back spherically shows a ~0.2° convergence term at 50 km; the tests measure spherically on purpose and carry a documented 0.25° slack.
  • The flag is not persisted: it describes who registered the node and is re-asserted on every registration; detection areas are not persisted either, and a summary needs one before any polygon is published.
  • The same wrong-aircraft bind rate inflates real nodes' evidence polygons; that is a calibration-attribution follow-up, deliberately not touched here.

🤖 Generated with Claude Code

get_node_summary publishes to_polygon(evidence_only=True) for every node.
That is right for real hardware — a real node's beam_azimuth_deg and
beam_width_deg are unsurveyed configuration, so drawing them would claim
coverage nobody measured — but it is wrong for a simulator node.  The
simulator emits a detection only for an aircraft inside the node's declared
cone (retina_simulation/world.py::_aircraft_in_detection_cone), so for those
nodes the cone IS the detection area by definition, and the evidence is the
unreliable half: the calibration points come from ADS-B hexes bound to tracks
and roughly a third of those binds are to the wrong aircraft.

Measured on test 2026-09-13, synth-GVL-SCAT-0032 (42 deg beam) held 3,037
calibration points of which 47% lay outside its wedge (34% ignoring the two
edge bins), 55 out-of-wedge bins had opened, and the published polygon covered
71 of 72 bearings.  Every synthetic node on the test map draws as a disc.

So: EmpiricalCoverageState.declared_wedge_polygon() draws the prior azimuth
and width at _reach_at on each bearing — already the bistatic ellipse when a
differential limit and the TX are known, else the circle, which is exactly the
simulator's own range rule — with no clamp, no bins and no min-points gate.
An omni prior gets a full ring; a directional one an apex-closed wedge with
both edges exact.

NodeAnalyticsManager.register_node grows a keyword-only
declared_geometry_is_truth flag (default False, asserted per registration, so
a re-registration without it drops the node again), and get_node_summary
publishes the declared wedge for a flagged node and the evidence-only shape
for everyone else.  The FOV diagnostics block is skipped for a flagged node —
it describes the learned wedge, which such a node does not publish — while
n_points/n_filled_bins stay in the payload: the evidence is still accumulated
and still worth reporting, it just is not drawn.  Every summary now names its
"polygon_source" (declared / evidence / learned) so the map can say which it
is showing rather than guess.

A flag change invalidates the 60 s summaries cache the same way a rebuilt
detection area does; retire_node, _reset_for_tests and the "cannot place this
node" path all clear it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@jehanazad
jehanazad merged commit 41d2004 into feat/evidence-only-coverage Sep 15, 2026
1 check passed
jehanazad added a commit that referenced this pull request Sep 15, 2026
Publish a synthetic node's declared cone as its detection area (#32 onto main)
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.

1 participant