prop/test/microwaveprop/space_weather/swpc_client_test.exs
Graham McIntire 923c8a468d
SpaceWeather: SWPC JSON ingestion (Kp, F10.7, GOES X-ray)
Adds the second ionospheric data source layer. Polls NOAA SWPC's free
public JSON services every 5 minutes for three products:

* Planetary K-index (1-min cadence) → geomagnetic_observations
* 10.7 cm solar flux (hourly) → solar_flux_observations
* GOES X-ray flux (1-min, 0.1-0.8nm long-wave band only) →
  solar_xray_observations

All three products feed HF / Es scoring inputs that the propagation
engine will start using in a follow-up commit:

* Kp drives HF absorption and Es damping (memory notes Kp is inversely
  correlated with tropo/Es stability).
* F10.7 is the SSN proxy for ITU-R P.533 HF MUF prediction.
* GOES X-ray long-wave flux is the short-wave fade (SWF) / D-layer
  absorption proxy — a C-class flare starts attenuating HF within
  seconds of the X-ray peak.

New context `Microwaveprop.SpaceWeather` exposes `upsert_*/1` and
`latest_*/0` per product; `SpaceWeatherFetchWorker` pulls all three
independently so one endpoint's outage doesn't block the others.
Fixtures captured from live SWPC endpoints on 2026-04-15 for the
parser tests.

Not yet wired into scoring — that's a separate commit.
2026-04-15 14:55:24 -05:00

63 lines
2.1 KiB
Elixir
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

defmodule Microwaveprop.SpaceWeather.SwpcClientTest do
use ExUnit.Case, async: true
alias Microwaveprop.SpaceWeather.SwpcClient
describe "parse_kp/1" do
test "decodes the SWPC planetary_k_index_1m fixture" do
body = File.read!("test/fixtures/swpc/kp_sample.json")
assert {:ok, rows} = SwpcClient.parse_kp(body)
assert length(rows) == 10
first = hd(rows)
assert first.valid_time == ~U[2026-04-15 13:49:00Z]
assert first.kp_index == 0
assert_in_delta first.estimated_kp, 0.33, 0.001
end
test "returns {:error, _} for malformed bodies" do
assert {:error, _} = SwpcClient.parse_kp("not json")
assert {:error, _} = SwpcClient.parse_kp(~s({"not": "an array"}))
end
end
describe "parse_f107/1" do
test "decodes the SWPC f107_cm_flux fixture" do
body = File.read!("test/fixtures/swpc/f107_sample.json")
assert {:ok, rows} = SwpcClient.parse_f107(body)
assert length(rows) == 10
first = hd(rows)
assert first.valid_time == ~U[2026-04-15 17:00:00Z]
assert first.frequency_mhz == 2800
assert first.flux_sfu == 112.0
assert first.reporting_schedule == "Morning"
end
test "keeps nullable historical stats as nil when absent" do
body = File.read!("test/fixtures/swpc/f107_sample.json")
{:ok, [first | _]} = SwpcClient.parse_f107(body)
assert first.ninety_day_mean == nil
end
end
describe "parse_xrays/1" do
test "decodes the GOES xrays-1-day fixture, keeping the 0.1-0.8nm band only" do
body = File.read!("test/fixtures/swpc/xrays_sample.json")
assert {:ok, rows} = SwpcClient.parse_xrays(body)
# Fixture has 12 records (6 timestamps × 2 bands). We keep the
# long-wavelength band (0.1-0.8nm) which is the standard SWF /
# D-layer absorption proxy.
assert length(rows) == 6
assert Enum.all?(rows, fn r -> r.energy_band == "0.1-0.8nm" end)
first = hd(rows)
assert first.valid_time == ~U[2026-04-14 19:49:00Z]
assert first.satellite == 18
assert_in_delta first.flux_wm2, 3.523e-07, 1.0e-09
end
end
end