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 ---------- @kp_update_fields ~w(kp_index estimated_kp updated_at)a @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, update_fields: @kp_update_fields ) 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 ---------- @solar_flux_update_fields ~w(frequency_mhz flux_sfu reporting_schedule ninety_day_mean updated_at)a @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, update_fields: @solar_flux_update_fields ) 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 ---------- @xray_update_fields ~w(satellite flux_wm2 electron_contaminated updated_at)a @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], update_fields: @xray_update_fields ) 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, Keyword.fetch!(opts, :update_fields)}, conflict_target: Keyword.fetch!(opts, :conflict_target) ) {:ok, count} end end