defmodule Microwaveprop.Pskr.CalibrationSample do @moduledoc """ One sample for the PSKR-driven recalibration corpus: a (hour, band, midpoint cell) bucket joining aggregate PSKR spot density with the HRRR atmospheric state and SWPC geomagnetic state at that hour. ## Why we don't store predicted scores The corpus exists so the recalibrator can learn weights from (atmospheric features → observed spot density). Predicted scores are derived quantities that change every time the algorithm changes; storing them locks the corpus to a single algorithm version. Features are immutable — they describe what the atmosphere actually was — so the same sample row stays useful across every weight refit. ## Cell granularity `midpoint_lat`/`midpoint_lon` are snapped to the propagation grid (currently 0.125°) by `Pskr.CalibrationSampler` before insert. Two PSKR paths whose midpoints land in the same grid cell collapse into a single row — the same logic that already drives `pskr_spots_hourly` aggregation, applied at the spatial dimension. ## Forward-only PSK Reporter has no historical archive — the firehose is real-time only. The corpus grows from when the feed first started and never backfills further. Recalibration weight refits should only be attempted once enough days have accumulated to cover diurnal and synoptic variability (heuristic: ≥ 30 d, ≥ 10k samples). """ use Ecto.Schema import Ecto.Changeset @primary_key {:id, :binary_id, autogenerate: true} @foreign_key_type :binary_id schema "pskr_calibration_samples" do field :hour_utc, :utc_datetime field :band, :string field :midpoint_lat, :float field :midpoint_lon, :float field :spot_count, :integer, default: 0 field :distinct_paths, :integer, default: 0 field :median_distance_km, :float field :modes, {:array, :string}, default: [] field :surface_temp_c, :float field :surface_dewpoint_c, :float field :pwat_mm, :float field :surface_pressure_mb, :float field :min_refractivity_gradient, :float field :hpbl_m, :float field :hrrr_ducting_detected, :boolean field :kp_index, :integer timestamps(type: :utc_datetime) end @type t :: %__MODULE__{} @cast_fields ~w(hour_utc band midpoint_lat midpoint_lon spot_count distinct_paths median_distance_km modes surface_temp_c surface_dewpoint_c pwat_mm surface_pressure_mb min_refractivity_gradient hpbl_m hrrr_ducting_detected kp_index)a @required_fields ~w(hour_utc band midpoint_lat midpoint_lon spot_count)a @spec changeset(t(), map()) :: Ecto.Changeset.t() def changeset(record, attrs) do record |> cast(attrs, @cast_fields) |> validate_required(@required_fields) |> unique_constraint([:hour_utc, :band, :midpoint_lat, :midpoint_lon]) end end