Skip to content

Known lane: gate publishes on the loci's geometry, not on receiver sites - #671

Merged
jehanazad merged 1 commit into
mainfrom
fix/known-lane-geometry-gate
Oct 15, 2026
Merged

jehanazad merged 1 commit into
mainfrom
fix/known-lane-geometry-gate

Conversation

@jehanazad

@jehanazad jehanazad commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

#663 withheld every known-lane solve whose receivers shared a site. That reads the wrong variable: two nodes on one roof cross at half the angle their transmitters subtend at the aircraft, so the same pair is a good fix for one aircraft and none for the next. This replaces the site count with a per-solve geometry figure, the dilution of precision of the delay loci, which holds at any node count, so three or four receivers on one roof are judged by the same number as two.

Why

Each node's delay puts the aircraft on a curve whose normal is the sum of the unit vectors from its transmitter and from its receiver. Co-sited receivers share the receiver term, which leaves the towers to decide the crossing. Sharing a tower with receivers apart is the mirror case, and receivers far apart are no fix either when the aircraft is distant enough that every focus lies one way.

On test, 2026-10-02, the one real n=2 pair (receivers co-sited, towers 18 km apart, 981 known-lane solves in 26 min):

km of position per µs of delay solves ghosts (>2 km) median error
under 2 264 3% 0.61 km
2 to 3 167 8% 0.68 km
3 to 4 96 33% 1.03 km
4 to 5 98 38% 1.35 km
5 to 8 130 42% 1.65 km
over 8 226 61% 2.96 km

The site rule withheld every row alike. Median error over median figure is 0.28 µs in each row from 2 to 8, so the error is the geometry. At the 3.0 default the gate keeps 59% of this pair's truth_match solves, with a 5% ghost rate among the attempts it passes.

Changes

  • services/solve_geometry.py (new): delay_dop_km_per_us, sqrt(trace((Σ g gᵀ)⁻¹)) over the nodes' horizontal bistatic-range gradients at a pinned altitude, in km per µs. At n=2 it is the crossing angle in another unit. None when a node's geometry is unknown, inf for parallel loci.
  • Known lane publish gate: weak_geometry in place of single_site, KNOWN_PUBLISH_MAX_DELAY_DOP_KM_PER_US (3.0, 0 disables), counter known_lane_publish_weak_geometry. Evaluated at the transponder's position, not the solve's. Checked before the rms gate. Binding only; label, accuracy sample and history outcome are unchanged.
  • Records: history records and accuracy samples carry delay_dop_km_per_us in place of n_sites. solver-stats reports publish_weak_geometry in place of publish_single_site.
  • Re-anchoring: ghosts from a geometry the gate would withhold never re-anchor; a co-sited pair on separate towers now can.
  • Removed: node_sites.count_sites and its tests, which had no other caller.
  • Docs: docs/solverflow.md gains the gate in the 3c flowchart, prose and constants table; backend/.env.example gains the key.

Test coverage

  • tests/test_solve_geometry.py (new, 19 tests): the n=2 closed form, the half-angle rule for co-sited receivers, the shared-tower mirror case, three and four receivers on one roof (towers spread, one tower farm, two farms), altitude handling, and the unknown cases.
  • tests/test_known_lane.py: TestSingleSiteGate becomes TestGeometryGate. Co-sited on separate towers publishes; co-sited on one farm is withheld at n=2 and n=3; three co-sited on spread towers publish; three separate sites seen from 200 km are withheld; gate precedence, the env ceiling, zero-disables, unknown config, shadow mode, no-converge records and re-anchoring.
  • Run locally: ruff, vulture, contract check and pyright clean. Full backend suite 5760 passed, 3 failed, all three in tests/test_deploy_scripts.py, which this change does not touch; they fail on the droplet's own environment (an extra line in the rendered .env).
  • Not committed, run by hand: noisy delays (0.3 µs) through the repo's solve_multinode for 2, 3 and 4 receivers on one roof. Median error tracks the figure at every node count, and under 3 km/µs at most 2% of solves in any set land over 2 km.

Greenville replay (2026-10-06)

The four co-sited Greenville nodes are the live case. Three afternoon hours of their detections from prod's public archive, replayed through the real solver as every subset, with the two untagged nodes' detections claimed offline against the ADS-B truth the two tagging nodes carry (receiver position self-surveyed from the tags; the two surveys agree to 68 m):

set (MHz) towers solves median DOP pass at 3.0 ghosts, all ghosts, passing median error
183+201 28 km apart 477 0.87 100% 0% 0% 0.09 km
201+213 24 km apart 1351 1.29 99% 1% 1% 0.24 km
183+213 34 km apart, same bearing 479 2.67 67% 5% 0% 0.49 km
183+195 one tower farm, 200 m 1 90 0% — — —
183+201+213 423 0.85 100% 0% 0% 0.09 km

Two-node solves by the figure: ghosts 0% under 1, 1% at 1–2, 0% at 2–3, 6% at 3–5, 17% at 5–10, 48% over 10. The site-count rule would have withheld every row of this table. The quad never formed: the 195 MHz node claimed 37 aircraft detections in three hours against 937 for the 183 MHz node on the same tower, so the four-receiver case rests on the solver Monte Carlo above.

Review notes

  • Dark lane is untouched. It has the same defect (Known lane: count co-sited receivers as one site, withhold single-site solves #663 noted eleven one-site dark ghosts 119–169 km off) and no ADS-B truth to catch it, but its altitude is free, so it needs a 3-D figure and its own calibration. Follow-up.
  • Calibration: the 3.0 threshold rests on the Atlanta pair over 26 minutes and the Greenville replay above (three co-sited pairs and one triple over three hours), plus the synthetic fleet, where n=2, n=3 and n≥4 solves show similar error at the same figure. No real four-receiver set was observed.
  • Prod and staging run the known lane in shadow, so this changes the map on test only, and only after a redeploy; test is still on a 2026-09-28 build.
  • Renamed counter and report key: anything reading publish_single_site or n_sites from solver-stats or history needs the new names. Nothing in the dashboard does.

🤖 Generated with Claude Code

#663 withheld every known-lane solve whose receivers shared a site. That
reads the wrong variable. A node's delay puts the aircraft on a curve whose
normal is the sum of the unit vectors from its transmitter and from its
receiver, so two nodes on one roof cross at half the angle their
TRANSMITTERS subtend at the aircraft: a good pair with towers either side
of it, none with towers on one farm, and which of the two changes from one
aircraft to the next. Receivers far apart are no fix either when the
aircraft is distant enough that every focus lies one way, and the site rule
published those.

On test, 2026-10-02, the one real n=2 pair (receivers co-sited, towers
18 km apart, 981 solves in 26 min) was 3% ghosts where a microsecond of
delay error is under 2 km of position, 8% from 2 to 3, 33% from 3 to 4 and
61% over 8. The site rule withheld all of them alike.

- services/solve_geometry.delay_dop_km_per_us: the dilution of precision of
  the delay loci at a pinned altitude, sqrt(trace((sum g g^T)^-1)) over the
  nodes' horizontal bistatic-range gradients, in km per microsecond. It is
  the crossing angle's generalisation to any node count, so three or four
  receivers on one roof are judged by the same number as two. None when a
  node's geometry is unknown, which never demotes a solve.
- Binding publish gate "weak_geometry" (KNOWN_PUBLISH_MAX_DELAY_DOP_KM_PER_US,
  3.0, 0 disables; counter known_lane_publish_weak_geometry) in place of
  "single_site", evaluated at the transponder's position rather than the
  solve's. Checked before the rms gate. Measurement, label and sample are
  unchanged.
- History records and accuracy samples carry delay_dop_km_per_us in place
  of n_sites. node_sites.count_sites had no other caller and goes.
- Ghosts from a geometry the gate would withhold never re-anchor; a co-sited
  pair on separate towers now can.

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

claude Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Claude finished @jehanazad's task in 4m 36s —— View job


Code review

  • Gather context (PR diff, changed files, CLAUDE.md)
  • Understand the request
  • Review changes for bugs and CLAUDE.md compliance
  • Validate findings
  • Post final review

No issues found. Checked for bugs and CLAUDE.md compliance.

Notes from the review (not issues, just what I checked):

  • Verified the DOP math in backend/services/solve_geometry.py: trace((Σ gᵢgᵢᵀ)⁻¹) is the correct position-error covariance trace for both the exact n=2 case and the least-squares n>2 case, the n=2 closed-form reduction (sqrt(g1²+g2²)/(g1·g2·sinθ)) checks out algebraically, and the singular-ratio comment (sin²θ/4 at unit gradients) matches the code.
  • Traced the altitude handling: _gradient defaults a missing tx_alt_ft/rx_alt_ft to 0.0 rather than the terrain defaults (node_config.ALTITUDE_DEFAULT_FT, 900/1200 ft) that node_config.py says must be applied "at every door into geometry." This looks like it could be the exact cross-subsystem-defaulting bug that module explicitly warns about — but I confirmed the only production caller (known_lane._delay_dop via frame_processor.get_node_configs()) already resolves altitudes before node_cfgs reaches solve_geometry, so this isn't reachable today. Flagging only as context, not as a finding.
  • Confirmed no leftover references anywhere in the repo to the removed count_sites, n_sites, or publish_single_site names (grepped the full tree).
  • Spot-checked backend/tests/test_solve_geometry.py and the new TestGeometryGate class in backend/tests/test_known_lane.py against the implementation — gate precedence (geometry before rms), zero-disables, unknown-config passthrough, and the re-anchoring-requires-sound-geometry change all match what the code does.
  • backend/core/state.py, backend/services/solver_report.py, backend/tests/test_solver_stats.py, and backend/.env.example renames are consistent and complete.

CLAUDE.md items (contract regen, uv.lock, .env.example keys) don't apply here — no /v1/nodes routes or dependency changes, and the new KNOWN_PUBLISH_MAX_DELAY_DOP_KM_PER_US key is already added to backend/.env.example.

@jehanazad
jehanazad merged commit 20faa39 into main Oct 15, 2026
40 checks passed
@jehanazad
jehanazad deleted the fix/known-lane-geometry-gate branch October 15, 2026 21:33
jehanazad added a commit that referenced this pull request Oct 18, 2026
Shadow was the measuring stage for the known lane: it claimed, calibrated
and solved but published nothing. The measurements are in. On production,
2026-10-01, the single-node arcs the map draws instead sat a median 1.49 km
(p95 31.5 km) from the aircraft, while the lane's withheld truth_match
solves measured 0.49 km (p95 1.72 km); since #671 a solve whose loci barely
cross is withheld as well.

Staging mirrors production's real nodes, so binding there first shows what
app.retina.fm will show before production's own overlay moves. The parity
check gets a staging-scoped allowance for KNOWN_LANE_MODE alone, marked
temporary: the change that binds production removes it, and the fallback
flag stays compared throughout. Binding is lane-wide, so claims the
adsb-service fallback makes for nodes that send no ADS-B of their own are
drawn at the service's fix.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
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