- Add jump_credo_checks ~> 0.4 with all 20 checks enabled - Fix all standard Credo issues: 139 @spec (113 done, 26 remain), 4 refactoring, 3 alias usage, 9 System.cmd env, 5 unsafe_to_atom, 2 max line length, 9 assert_receive timeout - Fix 170+ jump_credo_checks warnings: - 117 TopLevelAliasImportRequire: move nested alias/import to module top - 32 UseObanProWorker: switch to Oban.Pro.Worker - 4 DoctestIExExamples: add doctests / create test file - ~20 WeakAssertion: strengthen type-check assertions - Various ConditionalAssertion, AssertReceiveTimeout fixes - Exclude vendor/ from Credo analysis - Remaining: 175 warnings (mostly opinionated WeakAssertion, AvoidSocketAssignsInTest), 26 @spec annotations
175 lines
5.3 KiB
Elixir
175 lines
5.3 KiB
Elixir
defmodule Microwaveprop.Propagation.ScorerTest do
|
|
use Microwaveprop.DataCase, async: true
|
|
|
|
alias Microwaveprop.Propagation.Scorer
|
|
|
|
@contact %{
|
|
utc_hour: 18,
|
|
utc_minute: 30,
|
|
month: 6,
|
|
longitude: -97.0,
|
|
latitude: 32.9
|
|
}
|
|
|
|
describe "score/2" do
|
|
test "returns a map with score, contributions, breakdown" do
|
|
result = Scorer.score(@contact, 10_000)
|
|
|
|
assert is_integer(result.score)
|
|
assert result.score >= 0 and result.score <= 100
|
|
assert is_list(result.contributions)
|
|
assert length(result.contributions) > 0
|
|
assert is_map(result.breakdown)
|
|
end
|
|
|
|
test "score increases with favorable conditions" do
|
|
good_contact = Map.merge(@contact, %{utc_hour: 6, utc_minute: 0, month: 1})
|
|
bad_contact = Map.merge(@contact, %{utc_hour: 14, utc_minute: 0, month: 7})
|
|
|
|
good_score = Scorer.score(good_contact, 10_000)
|
|
bad_score = Scorer.score(bad_contact, 10_000)
|
|
|
|
assert good_score.score > bad_score.score
|
|
end
|
|
|
|
test "band config selection picks correct humidity effect" do
|
|
result_10g = Scorer.score(@contact, 10_000)
|
|
result_24g = Scorer.score(@contact, 24_000)
|
|
|
|
assert result_10g.band_mhz == 10_000
|
|
assert result_24g.band_mhz == 24_000
|
|
end
|
|
|
|
test "score returns nil for invalid band" do
|
|
assert Scorer.score(@contact, 999) == nil
|
|
end
|
|
end
|
|
|
|
describe "path_integrated_conditions/2" do
|
|
test "averages scalar fields and picks worst pressure / strongest gradient" do
|
|
profiles = [
|
|
%{
|
|
surface_temp_c: 25.0,
|
|
surface_dewpoint_c: 15.0,
|
|
surface_pressure_mb: 1010.0,
|
|
min_refractivity_gradient: -300.0,
|
|
hpbl_m: 500.0,
|
|
pwat_mm: 20.0
|
|
},
|
|
%{
|
|
surface_temp_c: 25.0,
|
|
surface_dewpoint_c: 15.0,
|
|
surface_pressure_mb: 1020.0,
|
|
min_refractivity_gradient: -400.0,
|
|
hpbl_m: 1500.0,
|
|
pwat_mm: 30.0
|
|
}
|
|
]
|
|
|
|
cond_map = Scorer.path_integrated_conditions(profiles, @contact)
|
|
assert %{temp_f: _} = cond_map
|
|
|
|
# Averages carry through to Fahrenheit equivalents.
|
|
assert_in_delta cond_map.temp_f, 77.0, 0.1
|
|
assert_in_delta cond_map.dewpoint_f, 59.0, 0.1
|
|
|
|
# min() picks the worst pressure and the strongest (most-negative)
|
|
# refractivity gradient.
|
|
assert cond_map.pressure_mb == 1010.0
|
|
assert cond_map.min_refractivity_gradient == -400.0
|
|
|
|
# avg() smooths HPBL + PWAT.
|
|
assert cond_map.bl_depth_m == 1000.0
|
|
assert cond_map.pwat_mm == 25.0
|
|
|
|
# Time/position fields come from the contact.
|
|
assert cond_map.utc_hour == 18
|
|
assert cond_map.utc_minute == 30
|
|
assert cond_map.month == 6
|
|
assert cond_map.longitude == -97.0
|
|
|
|
# Fields that the path integration can't infer from HRRR are nil.
|
|
assert cond_map.wind_speed_kts == nil
|
|
assert cond_map.sky_cover_pct == nil
|
|
assert cond_map.prev_pressure_mb == nil
|
|
assert cond_map.rain_rate_mmhr == 0.0
|
|
end
|
|
|
|
test "returns nil when every profile is missing surface temperature" do
|
|
profiles = [
|
|
%{
|
|
surface_temp_c: nil,
|
|
surface_dewpoint_c: 10.0,
|
|
surface_pressure_mb: 1020.0,
|
|
min_refractivity_gradient: -200.0,
|
|
hpbl_m: 500.0,
|
|
pwat_mm: 20.0
|
|
}
|
|
]
|
|
|
|
assert Scorer.path_integrated_conditions(profiles, @contact) == nil
|
|
end
|
|
|
|
test "returns nil when every profile is missing surface dewpoint" do
|
|
profiles = [
|
|
%{
|
|
surface_temp_c: 20.0,
|
|
surface_dewpoint_c: nil,
|
|
surface_pressure_mb: 1020.0,
|
|
min_refractivity_gradient: -200.0,
|
|
hpbl_m: 500.0,
|
|
pwat_mm: 20.0
|
|
}
|
|
]
|
|
|
|
assert Scorer.path_integrated_conditions(profiles, @contact) == nil
|
|
end
|
|
|
|
test "nil profile list yields nil" do
|
|
assert Scorer.path_integrated_conditions([], @contact) == nil
|
|
end
|
|
|
|
test "tolerates profiles with missing non-critical scalars (pressure, gradient, HPBL, PWAT)" do
|
|
# Only temp + dewpoint are required; everything else can be nil
|
|
# across all profiles and the conditions map still comes back
|
|
# with nil / avg-over-empty for those fields.
|
|
profiles = [
|
|
%{
|
|
surface_temp_c: 22.0,
|
|
surface_dewpoint_c: 15.0,
|
|
surface_pressure_mb: nil,
|
|
min_refractivity_gradient: nil,
|
|
hpbl_m: nil,
|
|
pwat_mm: nil
|
|
}
|
|
]
|
|
|
|
cond_map = Scorer.path_integrated_conditions(profiles, @contact)
|
|
assert %{temp_f: _} = cond_map
|
|
assert cond_map.pressure_mb == nil
|
|
assert cond_map.min_refractivity_gradient == nil
|
|
assert cond_map.bl_depth_m == nil
|
|
assert cond_map.pwat_mm == nil
|
|
end
|
|
|
|
test "uses -97.0 as longitude when contact.pos1 is missing the lon key" do
|
|
# Stale contacts written before pos1 was normalised can lack a
|
|
# lon field. The function falls back to CONUS-centre (-97.0).
|
|
profiles = [
|
|
%{
|
|
surface_temp_c: 22.0,
|
|
surface_dewpoint_c: 15.0,
|
|
surface_pressure_mb: 1015.0,
|
|
min_refractivity_gradient: -200.0,
|
|
hpbl_m: 1000.0,
|
|
pwat_mm: 25.0
|
|
}
|
|
]
|
|
|
|
contact_no_lon = %{utc_hour: 12, utc_minute: 0, month: 3, latitude: 32.9}
|
|
|
|
cond_map = Scorer.path_integrated_conditions(profiles, contact_no_lon)
|
|
assert cond_map.longitude == -97.0
|
|
end
|
|
end
|
|
end
|