Adopt pool nodes a dark solve can vouch for - #325
Conversation
The association round routinely pairs more nodes for an aircraft than the input the solver is handed uses. pool_n_nodes (the node set of the shared-track component the input was clustered out of) made that measurable, and 21 minutes of live instrumentation put 72% of published dark solves below their pool, mean shortfall 2.27 nodes: published n=2 with pool>=3 was 28 of 45, published n=3 with pool>=4 was 125 of 169. The expensive half is at the bottom — 390 of 481 rejected n=2 candidates had a pool of 3 or more, and an n=3 candidate publishes 81% of the time against 6% for n=2. That is the largest single block of detections this pipeline throws away. Merging harder upstream is not the fix: sweeping the position merge radius to 6 km raised cross-aircraft contamination from 25% to 32%, because at that radius two aircraft are as close together as one aircraft's own pairings. So the solve itself vouches for the extra node instead. _adopt_pool_nodes predicts, through the association stage's own forward model (predict_observation), what each pool node should have measured at the position and velocity just solved, adopts the ones whose measured delay agrees within 6.0 µs and Doppler within 60 Hz, re-solves wider from the first solve's position, and keeps the wider solve only if it passes the usual rms_delay gate and has not walked more than 5 km from the narrow one. With two or more adopted nodes a failed rms gets one retry without the worst-residual adoption, because the rms is a sum over both and a single contaminated adoption should not sink a widening the other node carried; beyond that the narrow solve stands. Cost is bounded at two extra LM solves per eligible candidate. The 6.0 µs default is derived, not tuned: an n=2 dark solve sits ~2 km from truth and 2 km of range error is 2/c ≈ 6.7 µs of bistatic delay, so the gate admits a genuine node at the accuracy this stage actually has while an unrelated target is normally tens of µs away. A node with no registered geometry, or a pool measurement whose node cannot be predicted for, is not adopted rather than adopted unchecked. It runs immediately after the first successful solve and BEFORE trimming and before every gate, so a widened candidate is judged as an n=3 solve throughout — including by the n=2 confirmation gate, which no longer applies to it. That is the point rather than a side effect: the gate exists because two nodes cannot corroborate each other's identity, and a third node whose measured delay matches what the two-node solve predicts for it is exactly the corroboration it was demanding. Only bottom-up dark inputs qualify; anchored and known-lane inputs carry an identity and were never clustered, so they have no pool. Every history record carries adopt_meta (pool size, candidates, adopted node ids, outcome) and n_nodes_pre_adopt, so adoption's true and false positives can be read from the dump rather than inferred from four counters; the counters (solver_adopt_eligible / _widened / _nodes_added / _rejected) are exposed in the solver-stats block. SOLVER_ADOPT_POOL=0 disables the stage outright. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit d7a941f)
configs_for_solver_input trims the queued configs to the measurement nodes, so an adopted pool node reached the re-solve with no config and was dropped by the epoch alignment and the solver's NodeSetups. Live on the test droplet 120 of 309 candidates marked widened had solved on their original two nodes (contributing_node_ids unchanged, rms 0, jump 0). A pool adds 0-6 configs, not the 50 the trim exists to keep out. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit a319c08)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Claude finished @jehanazad's task in 3m 31s —— View job Review: PR #325 — Adopt pool nodes a dark solve can vouch for
SummarySolid, well-contained change. The adoption stage runs in the right place (right after the first successful solve, before trimming and every gate), the Two things worth a look, neither blocking:
Everything else checked out:
Not verified: this checkout has no |
Summary
Widen a dark solve with the nodes the association round already paired for it, when the solve itself vouches for them. Clean re-cut against main of the work first landed on the overnight integration branch (#323, absorbed there) plus the config fix it needed.
Why
pool_n_nodes(#319) showed 72–89% of published dark solves are narrower than the round's shared-track pool (mean shortfall 2.3–3.3 nodes), and 390 of 481 rejected n=2 candidates in one window had a pool of 3 or more — a third node was available and the candidate could have been an n=3 solve (which publishes ~80% of the time instead of ~6%). Widening the merge radius was ruled out earlier (contamination rises).Changes
pool_measurements— the pool nodes' own measurements that the clustering left out._adopt_pool_nodes: after the first successful solve, predict each pool node's delay/Doppler at the solved state (predict_observation), adopt those withinSOLVER_ADOPT_DELAY_GATE_US(6.0) /SOLVER_ADOPT_DOPPLER_GATE_HZ(60), re-solve wider, keep the wide solve if rms_delay ≤ 3 µs and it moved ≤SOLVER_ADOPT_MAX_JUMP_KM(5); one retry dropping the worst adopted node. Runs before trimming and before every gate, so a 2-node candidate that adopts a third node is judged as n=3. Kill switchSOLVER_ADOPT_POOL=0.configs_for_solver_inputalso ships the pool nodes' configs — without them the re-solve silently dropped the adopted node (120 of 309 "widened" candidates stayed at two nodes in the first live run).solver_adopt_{eligible,widened,nodes_added,rejected};adopt_meta/n_nodes_pre_adopton every history record; pin bump.Measured (test droplet, 20-min captures vs ground truth)
Test coverage
tests/test_solver_pool_adoption.py(10 cases: widen, gate reject, rms reject, jump reject, anchored skip, env off, meta/counters) and aconfigs_for_solver_inputcase; lib tests in retina-analytics#30.Review notes
Gate defaults are first-cut values from the n=2 error budget (2 km ≈ 6.7 µs); they are env-tunable and worth a sweep once the by-outcome
adopt_metaaccumulates.🤖 Generated with Claude Code