Fix nonsense signal/SNR for HF paths longer than 7000 km#137
Merged
Conversation
For any path >= 7000 km, predict() called _combine_short_and_long with a
long_pred produced by _evaluate_long_model -- which is a stub:
def _evaluate_long_model(self, frequency: float) -> Prediction:
\"\"\"Evaluate long path model (not fully implemented).\"\"\"
# Simplified implementation - would need two Reflectrix objects
# For now, return a basic prediction
return Prediction()
A default Prediction() has signal.power_dbw = 0.0, power10 = 0.0, etc.
_combine_short_and_long happily mixed those zeros into the result:
* dist >= 10000 km: returned long_pred unchanged (all-zero signal),
back-deriving total_loss_db = tx_power_dbw - 0 = ~20 dB.
* 7000 km <= dist < 10000 km: smooth-interp pulled power_dbw toward the
long path's 0 dBW. With a typical short_pwr10 of -120 dBW, the math
collapses to power_dbw ~= 10*log10(r) ~= -0.7 dBW, giving the constant
signal_dbw the dashboard was rendering.
The dashboard reported this as flat -0.7 dBW signal and ~+155 dB SNR for
every region beyond 7000 km, with all 24 hours identical and reliability
pinned at 100%. Closer regions had the opposite failure mode -- short
predictions at 28+ MHz that legitimately couldn't propagate produced
total_loss values bounded by the muf-penalty term, which then surfaced
as -200 to -270 dB SNR via the same combine step writing back through
power_dbw - tx_power_dbw.
Until a real long-path model exists, skip the combine step entirely and
use the short prediction for every distance. Short-mode VOACAP results
are already valid out past 10000 km; the F2 multi-hop tracking just gets
less accurate, which is preferable to the engine inventing signal.
Also fix a secondary bug: tx_power_dbw was stamped onto current_antenna
before the per-frequency loop, where current_antenna still points at the
isotropic default. select_antenna() then swapped in a user-added antenna
whose tx_power_dbw came from its constructor (typically a placeholder
like 10 dBW for the dashboard). Move the assignment inside the loop,
after select_antenna(), so the real params.tx_power reaches whichever
antenna gets used.
Add regression tests asserting that:
* signal.power_dbw < -50 dBW, snr_db < +100 dB, total_loss_db > 80 dB
for paths at 4500 / 8500 / 11000 km against a 100 W TX with isotropic
antennas (each clearly violated by the stub-mixing bug),
* params.tx_power flows through to a user-added antenna, surviving its
placeholder tx_power_dbw.
https://claude.ai/code/session_01BLfWHK9hf8k4aBBaQjfQa8
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.
Summary
Fix two bugs in
PredictionEnginethat together produced the constant-0.7 dBWsignal levels and absurd ±150–270 dB SNRs the user observed in the dashboard for every region beyond 7000 km.Bug 1: long-path stub leaks into result
_evaluate_long_model()is a documented stub (# Simplified implementation - would need two Reflectrix objects. For now, return a basic prediction.) that returns an emptyPrediction(). For any path ≥ 7000 km,predict()was calling_combine_short_and_long(short_pred, long_pred)with that stub.A default
Prediction()hassignal.power_dbw = 0.0._combine_short_and_longmixed the zero into the result:long_preddirectly → all-zero signal,total_loss_dbback-derived totx_power_dbw - 0 ≈ 20 dB.short_pwr10of −120 dBW, the math collapses topower_dbw ≈ 10·log10(r) ≈ −0.7 dBW— exactly the constant signal level the dashboard rendered.Short paths weren't fixed by this either:
current_conditionsshowed regions like CA/EU/UK/SA at 10 m with SNRs of −223 / −267 / −234 dB, where short-model predictions on a frequency far above MUF produced largetotal_loss_dbvalues that propagated through correctly into the short pred but then got clobbered by the same combine-step writeback for any portion crossing 7000 km.Until a real long-path model exists, this PR skips the combine step entirely and always uses the short prediction. The short model is already valid out past 10000 km; the F2 multi-hop tracking just gets less accurate, which is strictly preferable to the engine inventing signal.
Bug 2: TX power doesn't reach user-added antennas
predict()settx_power_dbwoncurrent_antennaonce, before the per-frequency loop whereselect_antenna()swaps in the right antenna. At that early pointcurrent_antennais still the isotropic default, so the user's vertical/yagi/dipole kept whatever placeholder its constructor set (the dashboard usestx_power_dbw=10.0, comment says "Will be set from engine.params.tx_power" — but nothing ever did). Net effect: a configured 100 W (20 dBW) TX was being treated as 10 W on every dashboard prediction.Move the assignment inside the per-frequency loop, after
select_antenna().Verified output
For Halifax → Africa equivalent (~9500 km) at 14.1 MHz, 100 W, vertical antennas:
total_loss_dbpower_dbwsnr_dbreliability(SNR is now a normal-looking +4 dB, marginal-copy regime, as you'd expect for a 9500 km daytime path on 20 m.)
Test plan
test_voacap_validation.pystill produces sensible numbers (loss 157.3 dB, SNR 26.2 dB to London at 4689 km)validate_dvoacap.pyreportsVALIDATION SUCCESSFULtests/test_prediction_engine.py:power_dbw < −50 dBW,snr_db < +100 dB,total_loss_db > 80 dB(every assertion was clearly violated by the stub-mixing bug)params.tx_powerpropagates through to a user-added antenna whose constructor used a placeholder value/api/data, confirmsignal_dbwnow varies hour-to-hour and regions, and reliability is no longer pinned at 100% / 0%Out of scope for this PR
_evaluate_long_model. The stub is preserved as-is; only its consumption is bypassed.https://claude.ai/code/session_01BLfWHK9hf8k4aBBaQjfQa8
Generated by Claude Code