Skip to content

Physics tab: live ADS-B traffic from adsb.retina.fm with its own dark share - #356

Merged
jehanazad merged 2 commits into
mainfrom
feat/live-adsb-seed
Sep 10, 2026
Merged

jehanazad merged 2 commits into
mainfrom
feat/live-adsb-seed

Conversation

@jehanazad

Copy link
Copy Markdown
Contributor

Summary

Pairs with offworldlabs/retina-simulation#14 (pinned at adbc713). The synthetic fleet now pulls the real aircraft over its metro from adsb.retina.fm (/v2/point/{lat}/{lon}/{radius_nm}) into the simulated world, so the synthetic nodes echo real trajectories — and the Physics tab gets a slider for how many of those fly dark, independent of the synthetic knobs.

  • Fleet entrypoint: FLEET_LIVE_ADSB_URL (default https://adsb.retina.fm; empty = off) and FLEET_LIVE_ADSB_INTERVAL--live-adsb-url / --live-adsb-interval. Production is opted out (FLEET_LIVE_ADSB_URL= in docker-compose.prod.yml): its real hardware nodes claim the same real hexes in the "real" world, and a simulated fleet mirroring those aircraft would push the same keys into the one ADS-B cache as simulated-world traffic. Test and staging get it by default.
  • Simulation config: frac_live_dark (share of the live aircraft mirrored without their transponder — outside the frac_* sum, like frac_adsb_outage) and live_adsb_enabled (pauses the pull, removing the live aircraft). Both PUT-able, snapshot-restored, and applied in-process at the fleet's next 5 s poll — the simulator re-casts the aircraft already in the air (stable per-hex hash: raising the slider only ever adds dark aircraft).
  • Ground truth: the push carries source: live|sim; GET /api/simulation/config counts live / live_adsb / live_dark alongside the type buckets (overlapping them, like adsb_silent); /api/simulation/ground-truth exposes has_adsb and source.
  • Physics tab: a "Live ADS-B traffic" card (feed toggle, live/dark badge, "Dark share of live aircraft" slider), deliberately outside the composition bar — the feed sets the live headcount, so it is not a share of the synthetic mix. The objects-target card now says it governs synthetic aircraft only. GT map: dark aircraft coloured by has_adsb (the "dark" object_type it keyed on never arrives), live aircraft drawn dashed.
  • Docs: docs/simulation.md "Live ADS-B seeding" replaces the stale adsb.lol section.

The two populations are adjusted independently: min/max_aircraft + frac_dark govern synthetic spawns (spawning counts synthetic aircraft only), frac_live_dark governs the live ones.

Test plan

  • backend: tests/test_sim_ingest.py (+5: counts by cast, defaults, PUT/echo, validation, outside-the-sum), test_state_snapshot.py, test_sim_ingest_mount.py — all green
  • frontend: PhysicsSettings.test.tsx (+2: fallbacks + Apply payload, toggle disables the slider and sends the flag) — 7/7; tsc --noEmit and vite build clean
  • ruff on the touched backend files; sh -n on the entrypoint
  • live on the test droplet: fleet log shows Live ADS-B: N aircraft in world (…dark…), config counts report live, slider re-casts within a poll

🤖 Generated with Claude Code

… share

Pins retina-simulation feat/live-adsb-seed (adbc713): the fleet pulls the
real aircraft over its metro from an adsb.lol-shaped feed INTO the simulated
world, so the synthetic nodes echo real trajectories.

- deploy/fleet-entrypoint.sh: FLEET_LIVE_ADSB_URL (default
  https://adsb.retina.fm; empty = off) and FLEET_LIVE_ADSB_INTERVAL feed
  --live-adsb-url / --live-adsb-interval. Production sets the URL empty:
  its real hardware nodes claim the same real hexes in the "real" world,
  and the simulated fleet would push those keys into the one ADS-B cache
  as simulated-world traffic.
- simulation config: frac_live_dark (share of the LIVE aircraft mirrored
  without their transponder; outside the frac_* sum like frac_adsb_outage)
  and live_adsb_enabled (pauses the pull). Both PUT-able, snapshot-restored,
  and applied in-process at the fleet's next config poll — the simulator
  re-casts the aircraft already in the air.
- ground truth: the push carries source=live|sim, the config counts report
  live / live_adsb / live_dark alongside the type buckets (overlapping them,
  like adsb_silent), and /api/simulation/ground-truth exposes has_adsb and
  source.
- Physics tab: a "Live ADS-B traffic" card with the feed toggle, a live/dark
  badge and a "Dark share of live aircraft" slider, deliberately separate
  from the composition bar (the feed sets the live headcount, so it is not
  a share of the synthetic mix); the objects-target card says it governs
  synthetic aircraft only. GT map: dark aircraft coloured by has_adsb (the
  "dark" object_type it keyed on never arrives), live aircraft dashed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@claude

This comment has been minimized.

@jehanazad

Copy link
Copy Markdown
Contributor Author

Live on the test droplet (detached checkout of 85f2478, both containers rebuilt 20:00 UTC; rollback tag deploy-20260910-live-adsb-rollback = previous main f5810a0):

  • fleet log: Live ADS-B seeding started (feed=https://adsb.retina.fm, 1 area(s), interval=5s)Live ADS-B: 55 aircraft in world (4 dark, frac_live_dark=0.15)
  • GET /api/simulation/config counts: live 58 · live_adsb 53 · live_dark 5 alongside 25 synthetic (21 ADS-B + 4 dark at the droplet's frac_dark=0.25, min/max 20–40 untouched)
  • slider re-cast within one poll: PUT 0.5 → 26/61 live aircraft dark; PUT 0.0 → 0/61; PUT 0.15 → 5/64
  • the synthetic nodes are solving the real trajectories: 18 of 27 multinode tracks link to live-feed ground truth (incl. a dark-cast live-a4c83e solved by 10 nodes); real hexes with callsigns (DAL2279, AAL2282, …) in aircraft.json
  • Physics tab renders the new card (toggle, 64 live · 5 dark badge, 15 % slider); no console errors beyond the usual storage-context ones.

Left deployed on test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@claude

claude Bot commented Sep 10, 2026

Copy link
Copy Markdown

Claude finished @jehanazad's task in 9m 58s —— View job


Code Review

Todo list

  • Gather context (PR diff, changed files, CLAUDE.md)
  • Re-run /code-review:code-review against current HEAD (973ee03)
  • Verify carry-over findings from the prior review against the current code
  • Check for new issues introduced since the prior review
  • Post findings

Only commit since the last review is the submodule pin (973ee03, libs/retina-simulationadbc713) — no retina-server source changed. The three findings from the prior review are all still present and unaddressed:

1. frac_live_dark (and every other frac_* key) still accepts booleans — unfixed

backend/routes/test.py:589-591

elif k.startswith("frac_"):
    if not isinstance(v, (int, float)) or not (0.0 <= v <= 1.0):
        raise HTTPException(400, detail=f"{k} must be 0.0–1.0")

isinstance(True, (int, float)) is True and 0.0 <= True <= 1.0 holds, so PUT /api/simulation/config {"frac_live_dark": true} is silently stored as 1.0 instead of 400ing. The adjacent live_adsb_enabled branch (line 587-588) explicitly guards isinstance(v, bool) — the same guard was never extended to the frac_ branch, so it's still missing for frac_anomalous/frac_drone/frac_dark/frac_adsb_outage/frac_live_dark alike.

Fix this →

2. docs/simulation.md staging table still not reconciled with the new "Live ADS-B seeding" section

docs/simulation.md:129 still reads:

| `FLEET_MODE` | `adsb` | Merge the real ADS-B feed |

directly above the new "Live ADS-B seeding (LiveAdsbClient)" section (line ~144), whose prose ("merges the result into the world") describes a different mechanism (FLEET_LIVE_ADSB_URL/LiveAdsbClient) than the --mode adsb orchestrator flag this table row refers to. A reader could reasonably conclude FLEET_MODE=adsb is another live-feed toggle, or the same knob as FLEET_LIVE_ADSB_URL.

Fix this →

3. New tests still leave state.simulation_config fractions mutated for later tests

backend/tests/test_sim_ingest.py:245-254, test_frac_live_dark_is_outside_the_frac_sum_constraint PUTs frac_anomalous=0.1, frac_drone=0.1, frac_dark=0.8 alongside frac_live_dark=1.0, but only resets frac_live_dark afterward (line 254). state.simulation_config is a process-global dict, and the module's _clean_state autouse fixture (lines 30-40) only wipes ground-truth/anomaly state, not simulation_config. So frac_anomalous/frac_drone/frac_dark are left at 0.1/0.1/0.8 for every test that runs after this one in the module — masked today only because the sibling test_adsb_outage_is_outside_the_frac_sum_constraint (line 270-279) happens to re-set the same three values to the same numbers. Fragile: a future test relying on the 0.0 defaults, added after this one, will fail depending on collection order.

Fix this →

4. New: the "Dark Aircraft" card badge conflates synthetic and live-dark counts (design nuance, likely low severity)

frontend/src/components/PhysicsSettings.tsx:592-593 renders {counts[countKey] ?? 0} live per type card, and for the dark card countKey is "dark". Backend ground_truth_counts["dark"] (backend/routes/test.py, counts loop) intentionally overlaps synthetic frac_dark spawns with live aircraft cast dark via frac_live_dark — confirmed by test_counts_split_live_feed_aircraft_by_cast (backend/tests/test_sim_ingest.py:204-223), which asserts counts["dark"] == 2 for one synthetic-dark + one live-dark aircraft. This overlap is deliberate and tested (mirrors how adsb_silent overlaps type buckets), but it means the Physics tab's "Dark Aircraft" card — which sits next to the frac_dark slider that the PR's own description says "governs synthetic aircraft only" — shows a badge that moves when the independent frac_live_dark/live-feed population changes, with no synthetic/live breakdown. An operator watching that badge to confirm frac_dark took effect could misread a live-feed change as a synthetic one. Given the PR explicitly designed the Live ADS-B card to sit "outside the composition bar" for this exact reason, the Dark Aircraft card's badge arguably deserves the same separation (e.g. a synthetic-only count, or live_dark broken out alongside).


Confirmed live-droplet verification in the thread (55→ aircraft, slider re-cast, GT map dashed live markers) exercises the runtime path but wouldn't surface any of the four issues above (validation edge case, doc wording, test isolation, and a UI count-composition nuance).

@jehanazad
jehanazad merged commit 2f0173b into main Sep 10, 2026
16 checks passed
@jehanazad
jehanazad deleted the feat/live-adsb-seed branch September 10, 2026 21:31
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