Model the direct path with Okumura-Hata and derate towers under the beam - #33
Conversation
received_power_dbm was licensed EIRP plus 6 dBi minus free-space loss: what a receiver in a vacuum would hear. Against a real installation it ran 20 to 50 dB optimistic, and the part of that which is the same for every node has a textbook model. Two terms, both in a new `propagation` config section: - Okumura-Hata loss in excess of free space (never negative) for the mast height, the receiver height and an environment knob shipped as suburban. Inputs outside the formula's validity are clamped to its edge, so FM and tall masts get the edge-of-range figure rather than free space. - An under-beam derating: a broadcast beam is a few degrees tall and tilted down about a degree, so a receiver 3 km from a 300 m mast sits 6 degrees under it. Parabolic main lobe per band, capped at 20 dB for null fill. Low-power FCC records often carry antennaHeight 0 while the full-power station on the same mast carries the real figure, so a record without a height borrows the tallest one within the channel-sharing radius. Without that the LD channels at a shared mast escape the derating the full-power record gets. Each tower now also carries path_loss_db, excess_path_loss_db and underbeam_loss_db, so a reader can see why a tower 3 km away is not the loudest thing on the list. No existing field changes shape. An overlay written before the section existed gets the terrestrial model, not free space: a config PUT is not the trigger for the change. `model: "free_space"` restores the old figure. Against the home-installation measurements the mean over-prediction fell from 24 to 3 dB on FM, 44 to 19 on VHF and 53 to 25 on UHF; the remainder is that installation's whip, polarisation and walls, which the node has to measure. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Claude finished @jehanazad's task in 2m 49s —— View job Reviewing PR #33
|
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>

Follows from the home-installation PSD comparison:
received_power_dbmwas EIRP + 6 dBi − free-space loss, and against measurements it ran 20 to 50 dB high. The part of that gap that is the same for every node has a textbook model; the part that is this installation's whip, polarisation and walls does not, and stays the node's to measure (sweep calibration in #30, feedback rows via #32).What changes
Two terms on the direct path, both in a new
propagationconfig section (shipped intower_config.json, documented in the README):rx_height_m, shipped 5 m) andenvironment(open/suburban/urban, shippedsuburban). Never negative, so a tower in plain view is still free space away. FM, masts over 200 m and ranges under 1 km are clamped to the formula's edge rather than refused.model: "free_space"is the old behaviour.12·(angle/beamwidth)², per-bandvertical_beamwidth_deg(FM 8°, VHF 4°, UHF 2°), capped atmax_underbeam_loss_db(20) for null fill. Height is above ground, so a hilltop mast is derated less than it should be, never more.A record with no antenna height (common on FCC low-power records, which carry 0) borrows the tallest height within the 200 m channel-sharing radius, so the LD channels on a shared mast get the same derating as the full-power record. Indexed on a 0.01° grid; the radius decides, the grid only narrows the search.
Additive fields per tower:
path_loss_db,excess_path_loss_db,underbeam_loss_db. Nothing existing changes shape. Overlays that predate the section get the terrestrial model without a config PUT.Against the measurements
Live query for the home site, model versus the sheet's tinySA and SDRConnect reference readings (mean over-prediction, dB):
Per tower, WGGB-TV at 3.6 km goes from −4.8 to −35.6 dBm (measured −54): 11 dB of Hata excess, 20 dB under the beam. WSHM-LD on the same mast, height 0 on record, now borrows WGGB's 320 m and moves from rank 4 to 12. The TV remainder is consistent with a vertical whip receiving horizontally polarised ATSC indoors, which no site-independent model should try to absorb.
Verification
test_propagation.py), ruff check and format clean. The existing ranking tests pass unchanged: Hata is monotonic in distance and its excess is near flat across TV bands, so the orderings they pin still hold.process_and_rank660 ms end to end (unchanged code dominates), the mast index plus 1500 lookups 4 ms.Not in this PR
tower_scoringcomputes its own direct-path power with free space; when it rebases it should callpath_lossandunderbeam_lossfor the direct path and keep free space for the target echo, which is air-to-ground.🤖 Generated with Claude Code