Skip to content

Stamp solver inputs with the node pool they were clustered from - #29

Merged
jehanazad merged 1 commit into
mainfrom
feat/pool-n-nodes
Sep 7, 2026
Merged

jehanazad merged 1 commit into
mainfrom
feat/pool-n-nodes

Conversation

@jehanazad

Copy link
Copy Markdown
Contributor

Why

When exactly three nodes detect a dark aircraft, the published solve uses only two of them for about a third of that time, and nothing we record can say why. Three explanations are indistinguishable after the fact:

  1. the third node never formed a pairing at all,
  2. it formed one that the position clustering did not merge into the same solver input,
  3. it was merged and then dropped downstream.

(3) is already ruled out — no published record carries more source tracks than nodes — so the measurement that separates (1) from (2) is the one worth adding. This PR adds that measurement. It is not a fix; no clustering behaviour changes.

What

format_track_pairs_for_solver now runs a second union-find over the same round's pairings, joined by shared (node_id, track_id) instead of by position (_shared_track_pools). A node's tracker gives one track per aircraft, so pairings sharing a track are describing one aircraft by construction — no distance threshold, no velocity test, nothing tunable. The node set of that component is the widest solve the round could have produced for the aircraft.

Every emitted solver input is stamped with it:

  • pool_n_nodes — number of distinct node ids across the shared-track component(s) its pairings belong to
  • pool_node_ids — the sorted ids

So an input that used {A, B} while a pairing (A, C) existed in the same round reports n_nodes=2, pool_n_nodes=3. n_nodes < pool_n_nodes is a solve the round had the measurements for and did not make; n_nodes == pool_n_nodes had nothing left to take. _solver_input takes the pool as a second argument, defaulting to None (stamped as None — "not measured", rather than claiming the pool equals what was used).

Cost is O(n α(n)) over the round's pairings, on top of the O(n²) position matrix the same function already builds.

Tests

tests/test_track_association.py::TestSharedTrackPool — three pairings of one aircraft where (A,C) sits 55 km off: two inputs, both pool_n_nodes=3, one with n_nodes=2; a clean 3-node cluster with pool == n_nodes; and two different aircraft at the same position, which must report pools of 2 each rather than the round's 3 nodes.

tests/test_track_association.py: 41 passed. test_association.py test_cross_node.py test_track_claiming.py test_detection_association.py: 94 passed. ruff check / ruff format --check on src and tests: clean.

The consumer side (history record + /api/test/solver-stats pool block) is offworldlabs/retina-server.

🤖 Generated with Claude Code

When exactly three nodes detect a dark aircraft on the test droplet, the
published solve uses only two of them about a third of the time, and no
counter we have can say why.  Three explanations are indistinguishable
after the fact: the third node never formed a pairing at all, it formed
one that the position clustering did not merge, or it was merged and
then dropped downstream.  The last is already ruled out (no published
record carries more source tracks than nodes), so the measurement that
separates the first two is the one worth adding.

format_track_pairs_for_solver now runs a second union-find over the same
round's pairings, joined by shared (node_id, track_id) instead of by
position.  A node's tracker gives one track per aircraft, so pairings
sharing a track are describing one aircraft by construction — no
distance threshold, no velocity test, nothing tunable.  The node set of
that component is therefore the widest solve the round could have made
for the aircraft, and every emitted solver input is stamped with it as
pool_n_nodes / pool_node_ids.

An input whose n_nodes is below its pool_n_nodes is a solve the round
had the measurements for and did not make; one whose pool equals its
n_nodes had nothing left to take.  That is exactly the distinction the
position clustering cannot report on itself, because merge_dist_km, the
velocity-consistency test and the sub-cluster diameter bound are all
free to leave a genuine third node in an input of its own.

This is measurement only — no clustering behaviour changes.  Cost is
O(n alpha(n)) over the round's pairings, on top of the O(n^2) position
matrix already built in the same function.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@jehanazad
jehanazad merged commit 65dcb9a into main Sep 7, 2026
1 check 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.

1 participant