prop/lib/microwaveprop/ionosphere/observation.ex
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

39 lines
1.2 KiB
Elixir

defmodule Microwaveprop.Ionosphere.Observation do
@moduledoc """
A single ionosonde measurement for one station at one time. Populated
from GIRO's DIDBase scaled-characteristic feed and used by the
propagation scorer for sporadic-E and HF MUF predictions.
"""
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "ionosonde_observations" do
field :station_code, :string
field :valid_time, :utc_datetime
field :confidence_score, :integer
field :fo_f2_mhz, :float
field :fo_e_mhz, :float
field :fo_es_mhz, :float
field :hm_f2_km, :float
field :mufd_mhz, :float
timestamps(type: :utc_datetime)
end
@type t :: %__MODULE__{}
@required_fields ~w(station_code valid_time)a
@optional_fields ~w(confidence_score fo_f2_mhz fo_e_mhz fo_es_mhz hm_f2_km mufd_mhz)a
@spec changeset(t() | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t()
def changeset(observation, attrs) do
observation
|> cast(attrs, @required_fields ++ @optional_fields)
|> validate_required(@required_fields)
|> unique_constraint([:station_code, :valid_time])
end
end