Skip to content

Known lane: count co-sited receivers as one site, withhold single-site solves - #663

Merged
jehanazad merged 1 commit into
mainfrom
fix/known-lane-cosited-pairs
Oct 1, 2026
Merged

jehanazad merged 1 commit into
mainfrom
fix/known-lane-cosited-pairs

Conversation

@jehanazad

Copy link
Copy Markdown
Contributor

Problem

On test (2026-09-30), real aircraft a271b0 was claimed by two real nodes (213 MHz and 201 MHz) whose receivers are 56 m apart and whose transmitters differ. The known lane counted that as n=2 and ran free solves: 4.95 km and 2.32 km off (labelled ghost), then 1.90 km off, which the rms_delay publish gate withheld at 3.59 µs.

Two receivers on one roof share the receiver focus of every bistatic ellipse they measure. The normal of each delay locus bisects the directions to the transmitter and the receiver, so with a common receiver the two loci cross at half the angle the two transmitters subtend at the target. At both co-sited pairs on test the transmitters sit on the same side, which makes that angle shallow, and at a pinned altitude the loci can graze or miss each other entirely. Counting such a pair as two independent nodes overstates the geometry.

The comment at the rms gate also said n=2 solves "carry 0.0". That is false: this one carried 3.59 µs.

Evidence (live known-lane history on test, 21 min, 5 671 attempts)

Sites were grouped by the published node positions. The fuzz shares one point per site under the #359 rule, so this is exactly the site grouping.

attempts ghost median / p90 displacement published median / p90
n=2, two sites 1 879 5 % 0.12 / 1.05 km 0.11 / 0.72 km
n=2, one site 76 43 % 1.55 / 6.56 km 0.76 / 1.56 km
  • Affected: 81 of 5 671 attempts (1.4 %) had every claim at one site: 3 real sites, 9 hexes, including 3- and 4-node sets at one site. That is 44 of 5 427 publishes (0.8 %). One of those sites is two channels off the same tower into the same receiver, which is fully degenerate.
  • Not affected: 349 other attempts include a co-sited member but span at least two sites, and they are unchanged.

Change

  • node_sites.count_sites(positions): the public-geometry grouping (exact equality, then NODE_FUZZ_SITE_KM, default 150 m) over a given node set. It is pure: no snapshot and no DB. No new constant.
  • known_lane._count_sites: counts sites among the claim set from the solve's own node configs. An unplaced or unknown node counts as its own site, so the unknown case can never demote a solve.
  • New binding publish gate single_site: a solve whose claims come from fewer than two sites is still attempted, classified, sampled and recorded, but it is never published. The aircraft is left to the ADS-B display path, which a sibling PR on aircraft_feed.py covers.
    • This gate is checked before the rms gate, and only one counter is charged per withheld solve.
    • New counter known_lane_publish_single_site, reported as known_lane.publish_single_site in solver-stats.
    • n_sites is stamped on history records and accuracy samples.
  • Single-site ghosts never re-anchor. Re-anchoring trusts two self-consistent kf-seeded ghosts, but a degenerate geometry repeats its wrong answer just as consistently.
  • Rms gate comment rewritten. The gate already applied at every n, and a test now pins the n=2 case at 3.59 µs.

Unchanged:

  • The labels truth_match and ghost stay displacement classifications, so single-site samples keep their label; n_sites is there to bin them.
  • vel_untrusted still keys on node count.
  • The dark-follow pass is not touched.

Dark lane: same defect, not fixed here

The dark lane counts n_nodes = len(matched_node_ids) (retina-analytics association) with no site notion. In the same window, 3 725 of 4 481 dark-lane records were n=2 from a single site, almost all from one Atlanta pair, and mostly rejected (beam, n2_unconfirmed, rms). However, 11 were published: 4.2 % of all 264 dark publishes, every one 119–169 km from the nearest aircraft. This needs a follow-up in the dark lane or the association.

Tests

  • test_known_lane.py::TestSingleSiteGate, 10 tests: the co-sited pair is withheld under both the stub and the real LM solver; a receiver 200 m away counts as a second site; 3 nodes at 2 sites still publish; NODE_FUZZ_SITE_KM=0 narrows the gate to exact matches; an unplaced node counts as its own site; shadow mode never counts; the no-converge record carries n_sites; re-anchoring is blocked.
  • test_known_lane.py: new n=2 rms gate test.
  • test_node_sites.py::TestCountSites.
  • test_solver_stats.py: updated for the new key.

The known_lane, solver_report, node_sites and accuracy test files all pass (22 files), and pre-commit is clean.

🤖 Generated with Claude Code

…e solves

Two receivers within the co-location radius (node_sites, NODE_FUZZ_SITE_KM,
150 m) share the receiver focus of every bistatic ellipse they measure, so
their loci cross at half the angle their transmitters subtend at the
target. An n=2 from them is not the n=2 two sites give. On test, a271b0
(two receivers 56 m apart) solved 4.95, 2.32 and 1.90 km off. Over 21 min
of live known-lane history, co-sited n=2 attempts were ghosts 43% of the
time against 5% for two-site pairs, and the ones published sat a median
0.76 km off against 0.11 km.

- node_sites.count_sites: the public-geometry grouping over a given node
  set, pure (no snapshot, no DB).
- known_lane._count_sites: sites among the claim set, from the solve's own
  node configs; stamped as n_sites on history records and accuracy samples.
- Binding publish gate "single_site" (counter
  known_lane_publish_single_site, reported in solver-stats), checked before
  the rms gate. Measurement, label and sample are unchanged.
- Single-site ghosts never re-anchor: a degenerate geometry repeats its
  wrong answer, so self-agreement is no evidence.
- Fix the comment claiming n=2 solves always carry rms_delay 0.0; the rms
  gate applies at every n (test pins the 3.59 us n=2 case).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@claude

claude Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Claude finished @jehanazad's task in 2m 15s —— View job


Review complete

Summary

This is a well-scoped, well-tested fix. I read the full diff (node_sites.py, known_lane.py, solver_report.py, core/state.py) plus both new/updated test files and traced through the logic by hand.

Correctness

  • node_sites.count_sites() correctly reuses _cluster() — the same two-pass rule (exact equality, then NODE_FUZZ_SITE_KM proximity) the public map grouping already uses. It's pure (no snapshot/DB read), which is the right call for a per-solve hot path. Traced the 56 m co-sited pair through _cluster: neither node has an exact-equality match, so both land in the "singles" pass; the lower-id node becomes its own anchor first, the second joins it within the 150 m radius → correctly collapses to 1 site. The 200 m case correctly falls outside lat_gate/radius and stays 2 sites.
  • known_lane._count_sites() correctly treats a node missing from node_cfgs, or with a non-numeric rx_lat/rx_lon, as its own site via _site_of's _is_num check — matches the "unknown must never demote" guarantee and is tested (test_a_node_without_a_position_is_its_own_site, test_an_unplaced_node_is_a_site_of_its_own).
  • Gate ordering in _attempt: single_site is checked before rms_delay, only one counter is bumped per withheld solve (if/elif), and published correctly requires publishable and sites_ok and rms_ok. Verified against test_single_site_is_named_over_a_failing_residual.
  • Re-anchoring: gating _reanchor(...) behind n_sites >= 2 via short-circuit means a single-site ghost is skipped entirely rather than breaking or feeding the streak — it neither counts for nor against a later two-site re-anchor streak for the same hex. That matches the stated intent ("a degenerate geometry repeats its wrong answer as reliably as a good one") and is exercised by test_single_site_ghosts_never_reanchor. One minor, non-blocking observation: _reanchor_mem[hexn] from an earlier two-site ghost is left stale if all later ghosts for that hex become single-site (the entry is only cleaned by the TTL sweep, which only runs when any hex calls _reanchor again). Bounded (one entry per hex, TTL-bounded) and behaviorally harmless, so not worth a fix on its own.
  • state.known_lane_publish_single_site is a plain module-level int = 0 annotation, so it's automatically picked up by _COUNTER_ZEROS/_reset_for_tests() — no separate registration needed, confirmed by reading core/state.py:1218.
  • solver_report.py and test_solver_stats.py updates are consistent (new key added to both the snapshot and the funnel dict, and to the expected key sets in tests).

Repo-instruction gates (per CLAUDE.md)

  • No changes under /v1/nodes, its models, or node_config.py → no contracts/nodes-v1.openapi.yaml regen needed.
  • No backend/pyproject.toml change → no uv.lock relock needed.
  • Nothing here touches backend/.env/.env.example or host addresses/credentials.

Nit (non-blocking)

  • core/state.py's updated comment block (the one above known_lane_attempts) reads slightly awkwardly across the line wrap: "...residual gate, publish_single_site those it withheld..." — a semicolon or period between the two clauses would read more cleanly. Purely cosmetic.

Tests
Read through TestSingleSiteGate (10 cases) and TestCountSites (6 cases) — they cover the co-sited pair under both a stub and the real LM solver, the exact 150 m boundary, NODE_FUZZ_SITE_KM=0 narrowing to exact-match only, the unplaced-node case, shadow mode never counting, no_converge still carrying n_sites, and re-anchor suppression. This matches what the PR description claims, and I didn't find a gap in the scenarios covered. Didn't execute pytest in this sandbox (no venv/dependency install attempted), so this is a static read-through, not a run — but the logic checks out by hand-tracing the values through _cluster.

No correctness issues found. This looks ready to merge from a code standpoint.

@jehanazad
jehanazad merged commit 0eb3470 into main Oct 1, 2026
20 checks passed
@jehanazad
jehanazad deleted the fix/known-lane-cosited-pairs branch October 1, 2026 00:30
jehanazad added a commit that referenced this pull request Oct 15, 2026
…tes (#671)

#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>
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