187 lines
5.8 KiB
Elixir
187 lines
5.8 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,
|
|
sky_cover_pct: 25.0,
|
|
wind_speed_kts: 5.0,
|
|
pressure_mb: 1013.0,
|
|
prev_pressure_mb: 1015.0,
|
|
abs_humidity: 10.0,
|
|
temp_f: 80.0,
|
|
dewpoint_f: 60.0,
|
|
min_refractivity_gradient: -150.0,
|
|
bl_depth_m: 500.0,
|
|
rain_rate_mmhr: 0.0
|
|
}
|
|
|
|
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 result.contributions != []
|
|
assert Map.has_key?(result, :breakdown)
|
|
end
|
|
|
|
test "score increases with favorable conditions" do
|
|
# Same month, same location — dawn inversion peak scores higher than afternoon mixing.
|
|
# At longitude -97.0 the solar offset is ~-6.5 h, so 13 UTC ≈ 6:30 local (peak inversion)
|
|
# and 21 UTC ≈ 14:30 local (deep convective mixing).
|
|
good_contact = Map.merge(@contact, %{utc_hour: 13, utc_minute: 0})
|
|
bad_contact = Map.merge(@contact, %{utc_hour: 21, utc_minute: 0})
|
|
|
|
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
|