Stamp solver inputs with the node pool they were clustered from - #29
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
(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_solvernow 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 topool_node_ids— the sorted idsSo an input that used
{A, B}while a pairing(A, C)existed in the same round reportsn_nodes=2, pool_n_nodes=3.n_nodes < pool_n_nodesis a solve the round had the measurements for and did not make;n_nodes == pool_n_nodeshad nothing left to take._solver_inputtakes the pool as a second argument, defaulting toNone(stamped asNone— "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, bothpool_n_nodes=3, one withn_nodes=2; a clean 3-node cluster withpool == 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 --checkonsrcandtests: clean.The consumer side (history record +
/api/test/solver-statspool block) is offworldlabs/retina-server.🤖 Generated with Claude Code