Rank illuminators by expected bistatic detection area, with diversity, sweep calibration and fleet feedback - #30
Conversation
Direct-path received power, the previous rank key, rewards the towers a passive-radar node struggles with: a 1 MW transmitter 5 km away arrives at -3 dBm, overloads the front end and leaves a residual after cancellation that swamps echoes, while the same transmitter 50 km away gives several times the detectable area. Free-space loss also has no horizon, so a low mast 100 km out ranked on EIRP alone. services/tower_scoring.py models the receiver's disk as a grid of cells at a nominal target altitude and counts the area over which a 10 m^2 echo clears the effective noise floor (thermal plus direct-path residual after cancellation) by 13 dB after coherent processing, at the best surveillance azimuth, with a radio-horizon loss from the FCC antenna height. The bistatic angle cut excludes the baseline region. The model is vectorised with numpy: 200 towers score in about 10 ms. Every tower now carries expected_area_km2, best_azimuth_deg and horizon_km; nothing existing is renamed, retyped or dropped, so retina-gui and retina-spectrum keep reading the response as before. The shipped sort_order leads with expected_area_km2 and the band tier becomes a soft ranking.band_offset_db prior inside the model. A runtime overlay still carrying one of the old shipped defaults is upgraded in memory with a warning, the file untouched, following the distance-class precedent; a deliberately different overlay is left alone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Nothing flowed back from nodes: Auto-Calibrate's per-tower outcomes were discarded at process exit and every server-side metric is keyed by node with no tower attached. POST /api/feedback/tower-outcome takes those outcomes (and, later, archive-derived aggregates) behind a fail-closed TOWER_FINDER_FEEDBACK_TOKEN, distinct from the admin token because every node will hold it, and stores them in SQLite on the runtime volume. apply_feedback() turns rows from receivers within 30 km of the request into a log-multiplier per tower, shrunk towards 1 by n/(n+k) so one calibration run nudges a tower and a week of a live node moves it, and multiplies expected_area_km2 before the sort. It never raises into the request path. GET /api/feedback/summary shows the admin what has been learned. The archive residual is provisional and documented as such. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The rank is now built on expected_area_km2, so the number sits beside the rank in the table and in the top-pick card, with the best surveillance azimuth as a Point column. Towers past their radio horizon stay listed but read muted. Every new field is optional so an older backend renders exactly as before. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…weep A node uses one centre frequency at a time and Auto-Calibrate tries at most three candidates from the top of this list, so fourteen Sutro Tower channels in the top fourteen wasted every slot. Towers are now clustered into sites and ordered by maximal marginal relevance: each pick is the candidate with the largest area after a penalty for how similar it is to what has already been picked, in site, bearing, distance and band. Nothing is removed; the same-mast channels move down the list and every row carries site_id, site_channels and the diversity_penalty it was picked under. query.ranking names the order. On POST the node's sweep is the best direct-path measurement available and terrain is the largest error in the free-space model. The pilot powers are dBFS with no calibration, so the median gap between measured and modelled power over the matched TV towers becomes a per-sweep offset, and each measured tower's direct path is then the measured value. The analyser score, which is on different scales for FM and TV upstream, stops being compared across towers and becomes a quality multiplier on the area instead. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Claude finished @jehanazad's task in 3m 55s —— View job Claude finished the review
|
…ower-outcome The fleet feedback ingest from the expected-area ranking branch, on its own so it can merge and start collecting rows while that ranking is reviewed. The ranking does not read the store yet; apply_feedback ships here and the ranking branch wires it in. Checked against retina-gui's calibrator rather than designed in the abstract: a row is a flattening of one entry of a run's history, the outcome vocabulary is the calibrator's verbatim, callsign's limit is the node's own TX_NAME_MAX_LENGTH, and a not_reached entry (no final gains) validates. Two additions over the ranking branch's shape: - run_id, and a partial unique index on (node_id, run_id, tower_key). A node that times out on the post and retries would otherwise land its run twice, and every duplicate doubles that run's weight. Retries now answer 200 with the rows under `ignored`, so the node knows to stop. - device_error, which the calibrator sets when the SDR wedged rather than reported a clean overload; kept apart from outcome so a later fit can weight the two differently. Own bearer secret (TOWER_FINDER_FEEDBACK_TOKEN), fail-closed like the admin one and deliberately not interchangeable with it: every node holds this one. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Reconciles the ranking redesign with what landed on main since it branched: - The fleet feedback ingest merged separately as #32, with run_id dedupe and device_error on top of what this branch carried. Its files are taken from main wholesale; this branch keeps the ranking-side wiring (apply_feedback before the sort) and the README paragraph on how rows enter the ranking, folded into the newer section. - #33's terrestrial path loss and under-beam derating now shape received_power_dbm, which the sweep calibration reads. tower_scoring still models its own direct path with free space plus the horizon term, so the two direct-path figures differ; pointing scoring at path_loss/underbeam_loss for the direct path (and keeping free space for the target echo) is the follow-up #33 named. - Address lookup (#31) and the marker stacking fix (#29) merge cleanly apart from adjacent lines in app.py, the test helpers and the README API table. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Claude finished @jehanazad's task in 3m 46s —— View job Claude finished the review
|

Implements the ranking redesign from the design note (https://claude.ai/code/artifact/ce5a71b5-0be4-4084-8052-af60ccfff2a8). Four commits, each independently reviewable.
Compatibility with the nodes
retina-gui and retina-spectrum read this API. No existing field is renamed, retyped or removed;
rankstays contiguous 1..n;queryandcountkeep every existing key. A contract test pins the pre-existing field set and types on both routes. Everything below is additive.New per-tower fields:
expected_area_km2,best_azimuth_deg,horizon_km,feedback_factor,feedback_n,site_id,site_channels,diversity_penalty,direct_power_source,direct_power_dbm_override,measurement_quality.New
querykeys:ranking(both routes),calibration_offset_dbandcalibrated_towers(POST).What changes
services/tower_scoring.py). Bistatic radar equation over a 2 km grid of the receiver's 80 km disk at 3 km target altitude: echo against thermal noise plus the direct-path residual after 50 dB cancellation, 13 dB margin after coherent processing (6 MHz × 0.5 s for ATSC, 100 kHz × 1 s for FM), baseline region cut at 150° bistatic angle, radio-horizon loss from the FCC antenna height, best surveillance azimuth over a 42° Yagi with 20 dB front-to-back. numpy-vectorised: 200 towers in ~10 ms. Every knob is in a newscoringconfig section.ranking.band_offset_db(shipped as zeros) is added to EIRP inside the model. The physical differences between bands (path loss, processing gain, resolution) are already in the model; the offsets are for what is left and should be fitted from fleet data.ranking.diversityconfig (λ = 0.7 shipped). Same-mast channels move down the list rather than being removed.score(different scales for FM and TV upstream) becomes a quality multiplier instead of a sort key.POST /api/feedback/tower-outcomebehind a fail-closedTOWER_FINDER_FEEDBACK_TOKEN(separate from the admin token because every node will hold it), SQLite store on the runtime volume, shrinkage modelexp(n/(n+5)·r̄)over rows from receivers within 30 km, applied before the sort.GET /api/feedback/summaryfor admins. Never raises into the request path.Default sort and migration
Shipped
sort_orderis now[expected_area_km2 desc, received_power_dbm desc]. A runtime overlay still carrying one of the old shipped defaults is upgraded in memory with a warning and the file left untouched, following the distance-class precedent; a deliberately different overlay is left alone. Deployed volumes therefore pick up the new ranking on deploy without a config PUT, unless an operator had changed the sort.Verification
lat=37.78&lon=-122.41&radius_km=120&limit=200, 2.9 s end to end (FCC fetch dominates):The 19 Sutro Tower channels, previously ranks 1 to 19 at −3 dBm, now start at rank 27: too strong for the front end, too close for geometry. Ordering is stable across 40 to 60 dB of cancellation and 1 to 100 m² targets (sensitivity runs in the design note).
Follow-ups, not in this PR
band_offset_dband per-region residuals once feedback data exists; the archive residual is provisional and marked so.alternateswas deliberately not built: no rows are removed, so there is nothing to show yet.🤖 Generated with Claude Code