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.
78 lines
2.3 KiB
Elixir
78 lines
2.3 KiB
Elixir
defmodule Microwaveprop.SpaceWeather do
|
|
@moduledoc """
|
|
Context for NOAA SWPC space-weather observations: planetary K-index,
|
|
10.7 cm solar flux, and GOES X-ray flux. Used by the propagation
|
|
scorer for HF MUF prediction (F10.7 → SSN proxy), geomagnetic storm
|
|
effects (Kp), and short-wave fade detection (GOES X-ray flares).
|
|
"""
|
|
|
|
import Ecto.Query
|
|
|
|
alias Microwaveprop.Repo
|
|
alias Microwaveprop.SpaceWeather.GeomagneticObservation
|
|
alias Microwaveprop.SpaceWeather.SolarFluxObservation
|
|
alias Microwaveprop.SpaceWeather.SolarXrayObservation
|
|
|
|
# ---------- Kp ----------
|
|
|
|
@spec upsert_kp([map()]) :: {:ok, non_neg_integer()}
|
|
def upsert_kp([]), do: {:ok, 0}
|
|
|
|
def upsert_kp(rows) when is_list(rows) do
|
|
stamp_upsert(GeomagneticObservation, rows, conflict_target: :valid_time)
|
|
end
|
|
|
|
@spec latest_kp() :: GeomagneticObservation.t() | nil
|
|
def latest_kp do
|
|
Repo.one(from o in GeomagneticObservation, order_by: [desc: o.valid_time], limit: 1)
|
|
end
|
|
|
|
# ---------- F10.7 ----------
|
|
|
|
@spec upsert_solar_flux([map()]) :: {:ok, non_neg_integer()}
|
|
def upsert_solar_flux([]), do: {:ok, 0}
|
|
|
|
def upsert_solar_flux(rows) when is_list(rows) do
|
|
stamp_upsert(SolarFluxObservation, rows, conflict_target: :valid_time)
|
|
end
|
|
|
|
@spec latest_f107() :: SolarFluxObservation.t() | nil
|
|
def latest_f107 do
|
|
Repo.one(from o in SolarFluxObservation, order_by: [desc: o.valid_time], limit: 1)
|
|
end
|
|
|
|
# ---------- GOES X-ray ----------
|
|
|
|
@spec upsert_xray([map()]) :: {:ok, non_neg_integer()}
|
|
def upsert_xray([]), do: {:ok, 0}
|
|
|
|
def upsert_xray(rows) when is_list(rows) do
|
|
stamp_upsert(SolarXrayObservation, rows, conflict_target: [:valid_time, :energy_band])
|
|
end
|
|
|
|
@spec latest_xray() :: SolarXrayObservation.t() | nil
|
|
def latest_xray do
|
|
Repo.one(from o in SolarXrayObservation, order_by: [desc: o.valid_time], limit: 1)
|
|
end
|
|
|
|
# ---------- Shared ----------
|
|
|
|
defp stamp_upsert(schema, rows, opts) do
|
|
now = DateTime.truncate(DateTime.utc_now(), :second)
|
|
|
|
stamped =
|
|
Enum.map(rows, fn row ->
|
|
row
|
|
|> Map.put(:inserted_at, now)
|
|
|> Map.put(:updated_at, now)
|
|
end)
|
|
|
|
{count, _} =
|
|
Repo.insert_all(schema, stamped,
|
|
on_conflict: {:replace_all_except, [:id, :inserted_at]},
|
|
conflict_target: Keyword.fetch!(opts, :conflict_target)
|
|
)
|
|
|
|
{:ok, count}
|
|
end
|
|
end
|