prop/test/microwaveprop/ionosphere/ionosphere_test.exs
Graham McIntire 361b9dec5a
Ionosphere: GIRO ionosonde ingestion (foF2/foEs/hmF2/MUFD)
First step toward physics-based HF / sporadic-E scoring. Polls GIRO's
DIDBase tabular endpoint every 10 minutes for two US ionosondes
(Millstone Hill MHJ45, Alpena AL945), parses the plain-text response
into observation rows, and upserts into a new ionosonde_observations
table keyed on (station_code, valid_time).

This gets foEs (E-layer sporadic critical frequency) into the database
as a direct measurement — the key input for predicting 144 MHz Es
openings via ITU-R P.534-6. foF2 + MUFD are the F2-layer inputs for
HF MUF predictions.

Not yet wired into scoring. Boulder/Wallops/Austin/Idaho/Point Arguello
are in the GIRO catalog but were silent when probed — add them back
if/when they come online.

Next steps: SWPC JSON (Kp, F10.7, sunspot), GOES X-ray flux, D-RAP text,
and the P.534-6 Es scoring factor that uses foEs at midpoint for the
144/440 band configs.
2026-04-15 14:37:43 -05:00

83 lines
2.5 KiB
Elixir

defmodule Microwaveprop.IonosphereTest do
use Microwaveprop.DataCase, async: true
alias Microwaveprop.Ionosphere
alias Microwaveprop.Ionosphere.Observation
@valid_attrs %{
station_code: "MHJ45",
valid_time: ~U[2026-04-15 18:00:00Z],
confidence_score: 80,
fo_f2_mhz: 7.113,
fo_e_mhz: 3.10,
fo_es_mhz: 3.08,
hm_f2_km: 242.0,
mufd_mhz: 23.00
}
describe "upsert_observations/2" do
test "inserts new observations and returns the inserted count" do
assert {:ok, 2} =
Ionosphere.upsert_observations("MHJ45", [
Map.delete(@valid_attrs, :station_code),
@valid_attrs
|> Map.delete(:station_code)
|> Map.put(:valid_time, ~U[2026-04-15 18:07:30Z])
])
assert Repo.aggregate(Observation, :count, :id) == 2
end
test "re-upserting the same (station, valid_time) replaces fields and does not duplicate" do
{:ok, 1} = Ionosphere.upsert_observations("MHJ45", [Map.delete(@valid_attrs, :station_code)])
updated =
@valid_attrs
|> Map.delete(:station_code)
|> Map.put(:fo_es_mhz, 9.5)
assert {:ok, 1} = Ionosphere.upsert_observations("MHJ45", [updated])
assert Repo.aggregate(Observation, :count, :id) == 1
[row] = Repo.all(Observation)
assert row.fo_es_mhz == 9.5
end
test "stamps station_code on each observation" do
{:ok, 1} =
Ionosphere.upsert_observations("AL945", [
Map.delete(@valid_attrs, :station_code)
])
[row] = Repo.all(Observation)
assert row.station_code == "AL945"
end
test "returns 0 and does nothing for an empty list" do
assert {:ok, 0} = Ionosphere.upsert_observations("MHJ45", [])
assert Repo.aggregate(Observation, :count, :id) == 0
end
end
describe "latest_observation/1" do
test "returns the most recent observation for a station" do
older = Map.delete(@valid_attrs, :station_code)
newer =
@valid_attrs
|> Map.delete(:station_code)
|> Map.put(:valid_time, ~U[2026-04-15 19:00:00Z])
|> Map.put(:fo_es_mhz, 10.0)
{:ok, _} = Ionosphere.upsert_observations("MHJ45", [older, newer])
row = Ionosphere.latest_observation("MHJ45")
assert row.valid_time == ~U[2026-04-15 19:00:00Z]
assert row.fo_es_mhz == 10.0
end
test "returns nil when no observations exist for the station" do
assert Ionosphere.latest_observation("MHJ45") == nil
end
end
end