Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
6644628
Gate overlap zones on node world, and report the skipped pairs
claude Sep 5, 2026
b488400
Measure candidate contamination in the association bench
jehanazad Sep 5, 2026
a1395ca
solver: SOLVER_ALT_MODE=free, and ship only the configs a candidate uses
claude Sep 5, 2026
a82e6b2
Satisfy the lint gate on the bench contamination scorer
jehanazad Sep 5, 2026
1c9e5de
Make the dark lane's blind spots readable
jehanazad Sep 5, 2026
3a69d2d
Claim a re-solve slot on publication, not on admission
jehanazad Sep 5, 2026
964029e
Score contamination at the publish point as well as the candidate point
jehanazad Sep 5, 2026
ed1a023
Report superseded and cluster_splits as live association counters
jehanazad Sep 5, 2026
ca748c3
Bump retina-analytics: bound the cluster diameter
jehanazad Sep 5, 2026
3702fec
Exclude stale coasting tracks and align measurement epochs
jehanazad Sep 5, 2026
cb38610
solver: one free-altitude start by default, SOLVER_FREE_ALT_STARTS fo…
jehanazad Sep 5, 2026
fcccd03
Test stale-track exclusion and measurement epoch alignment
jehanazad Sep 5, 2026
c66b55f
Bump retina-analytics: merge distance down to one grid step
jehanazad Sep 5, 2026
6f84343
Bump retina-analytics: lint fix
jehanazad Sep 5, 2026
a6cff9f
Tighten multinode DR budget and refresh the selected track's solve dots
jehanazad Sep 5, 2026
3f7f974
Reject non-PSD solve covariance before it poisons the display filter
jehanazad Sep 5, 2026
4119765
Follow established dark tracks top-down (DARK_FOLLOW_MODE)
jehanazad Sep 5, 2026
9e3fd15
Mirror the tightened disc-growth horizon on the backend helper
jehanazad Sep 5, 2026
9ec77e6
Add regression tests for the non-PSD covariance failure
jehanazad Sep 5, 2026
60486a0
Test the dark-follow lane, and dead-reckon a follow input's anchor
jehanazad Sep 5, 2026
376c07d
Extend the solver-stats payload pins to the follow lane
jehanazad Sep 5, 2026
0ad8d81
Sweep and snapshot multinode_tracks under the solver's track lock
jehanazad Sep 5, 2026
1787b7a
Let the dark-follow lane own the keys it follows
jehanazad Sep 5, 2026
22ebf69
Keep altitude-separated multinode entries apart in feed dedup
jehanazad Sep 5, 2026
7ce3c72
Tighten supersession: 4 km base, altitude gate, bounded merge
jehanazad Sep 5, 2026
d011de1
Merge feat/assoc-world-gate-overlap into feat/free-altitude-solve
claude Sep 6, 2026
32fcd0e
Merge feat/free-altitude-solve (stacked on #291) into feat/dark-solve…
claude Sep 6, 2026
fcccb1f
Merge feat/dark-solver-observability (stacked on #291, #292) into fix…
claude Sep 6, 2026
1cdb647
geolocator: point the submodule at main now that free-altitude (#24) …
claude Sep 6, 2026
ded647b
Merge feat/free-altitude-solve (stacked on #291) into feat/dark-solve…
claude Sep 6, 2026
d09dd12
Merge feat/dark-solver-observability (stacked on #291, #292) into fix…
claude Sep 6, 2026
c950b42
Merge fix/resolve-slot-claim-on-publish (stacked on #291-#293) into f…
claude Sep 6, 2026
85eed51
Merge fix/cluster-contamination (stacked on #291-#294) into feat/meas…
claude Sep 6, 2026
9e15934
Merge feat/measurement-epochs (stacked on #291-#295) into fix/mlat-do…
claude Sep 6, 2026
3565480
Merge fix/mlat-dots-and-dr-budget (stacked on #291-#296) into fix/kf-…
claude Sep 6, 2026
86d2394
Merge fix/kf-covariance-psd (stacked on #291-#297) into feat/dark-tra…
claude Sep 6, 2026
0bb06c0
mlat-history: the lane list knows dark_follow
claude Sep 6, 2026
768e577
Merge feat/dark-track-following (stacked on #291-#298) into fix/feed-…
claude Sep 6, 2026
d7e2ecd
Merge fix/feed-sweep-under-tracks-lock (stacked on #291-#299) into fi…
claude Sep 6, 2026
3fbedaa
Merge fix/feed-dedup-multinode-split (stacked on #291-#300) into fix/…
claude Sep 6, 2026
72c16a6
test: chain-302 + origin/main (#304)
claude Sep 6, 2026
7846a63
test overlay: the dark-solver configuration the overnight captures me…
claude Sep 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -121,17 +121,69 @@ MENDER_PAT=
# SOLVER_RESOLVE_INTERVAL_S is how long an aircraft is not re-solved for.
# Association is rate-limited per node, so every node that sees an aircraft
# emits its own candidate for it and most arrivals are duplicates; this is the
# window that suppresses them. Do not exceed the 60 s multinode_tracks expiry —
# window that suppresses them. Do not exceed the multinode_tracks expiry
# (MN_DARK_EXPIRY_S below for dark tracks, 60 s for ADS-B-assisted ones) —
# tracks would lapse between solves. 0 turns the suppression off. See
# services/tasks/solver.py and docs/runbook.md's solver_latency_high entry.
#
# SOLVER_MAX_DISPLACEMENT_KM_DARK is how far a DARK (no-ADS-B) solve may land
# from its association guess before it is rejected; the default is 6 km = 2 x
# the 3 km association grid step, since a dark guess is a quantised grid point
# rather than an ADS-B fix. ADS-B-anchored solves keep the fixed 2 km cap.
#
# MN_DR_CAP_S is how far a multinode entry is dead-reckoned past its last
# solve before it holds position, and MN_DARK_EXPIRY_S is when a DARK
# (mn-dark-*) entry is dropped from the feed entirely. Both are position-error
# budgets: measured against ground truth, dark entries run 1.5 km median error
# at 8-15 s of solve age but 2.0 km at 15-30 s and 4.0 km at 30-60 s. ADS-B-
# assisted (mn-adsb-*) entries keep the 60 s expiry — a transponder hex anchors
# them. Raise both to trade position accuracy for track continuity.
#
# SOLVER_ALT_MODE is how an n>=3 solve gets its altitude. sweep (default)
# solves once per fixed altitude layer and keeps the lowest rms_delay — six
# process-pool round trips per candidate, and an altitude quantised to a
# ladder whose 2 km spacing puts up to 1 km of error straight into the
# residual the reject gate reads. free makes one pool call to the geolocator's
# multi-start helper, which solves altitude as a sixth unknown. Both modes
# stamp altitude_mode on the solve-history record, so /api/test/mlat-history
# can compare them across a deploy of each. See docs/solverflow.md.
#
# SOLVER_FREE_ALT_STARTS is how many start altitudes free mode gives that
# helper, clamped to the number of layers. 1 (the default) starts at the layer
# nearest the association guess, or at the guess altitude itself when it comes
# from ADS-B; more is a window around it, at one LM run each. Measured on test
# over 1019 free solves, three starts changed rms_delay by more than 0.1 us in
# 13 of them, so the extra runs are off by default and worth turning on only
# where the geometry sends a single start to the wrong side of an ellipse.
# SOLVER_WORKERS=2
# SOLVER_RESOLVE_INTERVAL_S=12
# SOLVER_MAX_DISPLACEMENT_KM_DARK=6.0
# MN_DR_CAP_S=15.0
# MN_DARK_EXPIRY_S=30.0
# SOLVER_ALT_MODE=sweep
# SOLVER_FREE_ALT_STARTS=1

# Dark track following (services/dark_follow.py, docs/solverflow.md 3d). The
# known lane's top-down claim applied to aircraft with no transponder: an
# established mn-dark-* track's Kalman state predicts delay/Doppler at each
# node, matching detections are claimed against it, and the resulting solve
# carries the followed key as its anchor so it lands back on the SAME track.
#
# DARK_FOLLOW_MODE: off | shadow | binding. shadow (the default) claims,
# solves and records but leaves the detections in the dark pool and publishes
# nothing; binding strips them and publishes through the normal dark gates.
#
# The rest are the eligibility and guard thresholds. A key is followable while
# its last solve is under MAX_AGE_S old with at least MIN_SOLVES solves and a
# filter velocity sigma under MAX_VEL_SIGMA_MS; it is dropped for COOLDOWN_S
# after two consecutive rejected follow-solves, which is what stops a ghost
# locking itself onto the map. INTERVAL_S rate-limits follow-solves per key.
# DARK_FOLLOW_MODE=shadow
# DARK_FOLLOW_MAX_AGE_S=20
# DARK_FOLLOW_MIN_SOLVES=3
# DARK_FOLLOW_MAX_VEL_SIGMA_MS=60
# DARK_FOLLOW_INTERVAL_S=2.0
# DARK_FOLLOW_COOLDOWN_S=30

# Detection mirror. Production only. Every accepted v1 detection frame is
# forwarded to another environment's /api/radar/detections/bulk, batched once a
Expand Down
47 changes: 43 additions & 4 deletions backend/config/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,25 @@ def as_num(v) -> float:
N2_CONFIRM_MIN_EPOCHS = 4 # Floor on samples; span is the real gate
N2_TRACK_HISTORY_MAX = 20 # Per-node track samples fed to the fit

# How old a track's newest REAL detection may be before the track stops being
# offered to association (see services/frame_processor.confirmed_track_views).
# A COASTING track is kept alive for N_DELETE=10 frames after its last
# association, and at the fleet's 0.74-1 Hz per-node cadence that is up to ~13 s
# of dead reckoning. confirmed_track_views hands association the track's last
# real sample, and association hands the solver that sample as if it were
# current — so an aircraft that has flown out of a node's beam keeps
# contributing a seconds-old delay to n>=3 solves. Measured on the test
# droplet: 230 out of-cone nodes survived into published dark solves in 20 min,
# median 4 deg outside the beam edge (p90 22 deg) and 1.8 km beyond max range,
# and they are the nodes the rms trim then throws away. 0 disables the filter.
TRACK_MAX_STALE_S = float(os.getenv("TRACK_MAX_STALE_S", "3.0"))

# A 2-node track needs this many solves before it renders a plane; 1
# disables the gate. One-shot n=2 solves were the dominant ghost source.
MN_N2_MIN_SOLVES = int(os.getenv("MN_N2_MIN_SOLVES", "2"))
# One-shot display lifetime for n>=3 solves, seconds. A track confirmed by
# a second solve gets the normal 60 s entry expiry / 30 s DR cap.
# a second solve gets the normal MN_DARK_EXPIRY_S/60 s entry expiry and the
# MN_DR_CAP_S dead-reckoning cap below.
#
# The window has to outlive the wait for the confirmation it is waiting for,
# or it is not a preview window — it is a guaranteed disappearance. A second
Expand All @@ -97,11 +111,36 @@ def as_num(v) -> float:
# cadence is a 9 s median and a 25 s p90. At 5 s the great majority of
# genuine n>=3 one-shots blinked out before the round that would have
# confirmed them ever ran, which reads on the map as flicker, not as caution.
# 15 s covers the median and most of the p90 while still being well short of
# the 60 s entry expiry, so an unconfirmed one-shot is still withdrawn long
# before a confirmed track would be.
# 15 s covers the median and most of the p90 while still being short of the
# entry expiry (MN_DARK_EXPIRY_S 30 s dark, 60 s assisted), so an unconfirmed
# one-shot is still withdrawn before a confirmed track would be.
MN_ONESHOT_TTL_S = float(os.getenv("MN_ONESHOT_TTL_S", "15.0"))

# How far a multinode entry may be dead-reckoned past its last solve, seconds.
# Beyond this it holds its last dead-reckoned point until the entry expires.
#
# The cap is a position-error budget, not a cadence allowance. Measured on
# the test droplet over 20 minutes (dark multinode feed entries vs ground
# truth): median error 1.05 km under 3 s of solve age, 1.21 km at 3–8 s,
# 1.50 km at 8–15 s, then 2.02 km at 15–30 s (7% of entries more than 5 km
# off). The knee is at 15 s, which is where the KF's learned velocity error
# starts to dominate the solve error it is extrapolating. The old 30 s cap
# was set when a dark aircraft was re-solved every 12 s and the extra window
# bought coverage; dark solves now land every 1–3 s, so a 15 s gap is a lost
# track rather than a cadence gap, and extrapolating it only invents motion.
MN_DR_CAP_S = float(os.getenv("MN_DR_CAP_S", "15.0"))

# Entry expiry for DARK multinode tracks (mn-dark-*), seconds. ADS-B-assisted
# entries (mn-adsb-*) keep the 60 s expiry: they are anchored to a transponder
# fix, so a gap there is the ADS-B feed breathing rather than a lost target.
#
# Same 20-minute capture: the 30–60 s age band was 12% of all displayed dark
# entries, with a 3.99 km median error and 32% more than 5 km off — an icon
# that reads as a live target while sitting kilometres from any aircraft. At
# the current 1–3 s dark solve cadence an entry that has not re-solved in 30 s
# is a lost track, and withdrawing it is more honest than holding it.
MN_DARK_EXPIRY_S = float(os.getenv("MN_DARK_EXPIRY_S", "30.0"))

# Quality gate for adopting the constant-velocity fit's velocity into a
# published solve, in place of the single-epoch Doppler solution (see
# solver.py's _resolve_cv_fit / velocity adoption in _process_solver_item).
Expand Down
Loading
Loading