Adds Microwaveprop.Propagation.AsosNudge: a pure IDW bias-field module that takes ASOS observations + HRRR profiles and returns re-scored grid rows for every cell within 250km of a reporting station. Upper-air fields (min_refractivity_gradient, pwat_mm, hpbl_m, profile, duct metadata) pass through unchanged so HRRR's signal isn't clobbered. The old AsosAdjustmentWorker was unwired and buggy — nil'd out ~22% of the scoring weight and wrote orphan timestamps. Replaced with a slim worker that queries the latest HRRR valid_time, fetches live ASOS currents, calls AsosNudge.compute/3, and upserts onto (lat, lon, valid_time, band_mhz) so nudged values overwrite the HRRR hour cleanly instead of polluting available_valid_times. After each upsert it warms ScoreCache and broadcasts propagation:updated so live /map clients refresh. Cron hooked up every 10 minutes in config.exs and dev.exs. Also cleaned up the stale "dev has propagation disabled" note in CLAUDE.md. 13 new AsosNudge unit tests cover: residual computation (co-located, out-of-grid, nil fields), IDW weighting (single station, far station, two equidistant stations, nil component handling), upper-air preservation, and the compute/3 entry point's shape and radius filter. Drive-by Styler formatting touched a handful of unrelated files from `mix format`.
150 lines
4.8 KiB
Elixir
150 lines
4.8 KiB
Elixir
defmodule Microwaveprop.Workers.AsosAdjustmentWorker do
|
|
@moduledoc """
|
|
Nudge the propagation score grid with fresh ASOS observations between
|
|
hourly HRRR runs.
|
|
|
|
On each cron tick (every 10 minutes) this worker:
|
|
|
|
1. Finds the most recent `valid_time` that actually has HRRR profiles
|
|
persisted.
|
|
2. Pulls current ASOS observations from all state networks via
|
|
`Microwaveprop.Weather.IemClient.fetch_current_asos/1`, keeps only
|
|
those within 30 minutes of now.
|
|
3. Loads every `hrrr_profiles` row on the 0.125° propagation grid for
|
|
that valid_time into memory as plain maps.
|
|
4. Hands both lists to `Microwaveprop.Propagation.AsosNudge.compute/3`
|
|
— pure function, IDW bias field, upper-air fields preserved.
|
|
5. Upserts the nudged scores onto `(lat, lon, valid_time, band_mhz)`,
|
|
overwriting the HRRR-only values for grid cells within 250 km of a
|
|
reporting station. Cells outside that radius are left alone.
|
|
6. Warms the `ScoreCache` and broadcasts `propagation:updated` so every
|
|
connected `/map` client refreshes.
|
|
|
|
This worker owns the I/O. `AsosNudge` stays pure so its tests don't need
|
|
Repo or network access.
|
|
"""
|
|
|
|
use Oban.Worker, queue: :propagation, max_attempts: 3
|
|
|
|
import Ecto.Query, only: [from: 2]
|
|
|
|
alias Microwaveprop.Propagation
|
|
alias Microwaveprop.Propagation.AsosNudge
|
|
alias Microwaveprop.Propagation.BandConfig
|
|
alias Microwaveprop.Repo
|
|
alias Microwaveprop.Weather.HrrrProfile
|
|
alias Microwaveprop.Weather.IemClient
|
|
|
|
require Logger
|
|
|
|
@state_networks ~w(
|
|
AL_ASOS AK_ASOS AZ_ASOS AR_ASOS CA_ASOS CO_ASOS CT_ASOS DE_ASOS
|
|
FL_ASOS GA_ASOS HI_ASOS ID_ASOS IL_ASOS IN_ASOS IA_ASOS KS_ASOS
|
|
KY_ASOS LA_ASOS ME_ASOS MD_ASOS MA_ASOS MI_ASOS MN_ASOS MS_ASOS
|
|
MO_ASOS MT_ASOS NE_ASOS NV_ASOS NH_ASOS NJ_ASOS NM_ASOS NY_ASOS
|
|
NC_ASOS ND_ASOS OH_ASOS OK_ASOS OR_ASOS PA_ASOS RI_ASOS SC_ASOS
|
|
SD_ASOS TN_ASOS TX_ASOS UT_ASOS VT_ASOS VA_ASOS WA_ASOS WV_ASOS
|
|
WI_ASOS WY_ASOS DC_ASOS
|
|
)
|
|
|
|
@max_obs_age_seconds 1800
|
|
|
|
@impl Oban.Worker
|
|
def perform(%Oban.Job{}) do
|
|
with {:ok, valid_time} <- latest_hrrr_valid_time(),
|
|
{:ok, observations} <- IemClient.fetch_current_asos(@state_networks),
|
|
[_ | _] = fresh <- filter_fresh(observations),
|
|
[_ | _] = profiles <- load_hrrr_profiles(valid_time) do
|
|
apply_nudge(fresh, profiles, valid_time)
|
|
else
|
|
:no_hrrr ->
|
|
Logger.info("AsosAdjustment: no HRRR profiles yet, skipping")
|
|
:ok
|
|
|
|
[] ->
|
|
Logger.info("AsosAdjustment: no fresh ASOS data, skipping")
|
|
:ok
|
|
|
|
{:error, reason} ->
|
|
Logger.warning("AsosAdjustment: aborted — #{inspect(reason)}")
|
|
:ok
|
|
end
|
|
end
|
|
|
|
defp apply_nudge(observations, profiles, valid_time) do
|
|
scores = AsosNudge.compute(observations, valid_time, profiles)
|
|
|
|
if scores == [] do
|
|
Logger.info("AsosAdjustment: #{length(observations)} fresh obs but no grid cells within range")
|
|
|
|
:ok
|
|
else
|
|
case Propagation.upsert_scores(scores, prune: false) do
|
|
{:ok, count} ->
|
|
Logger.info("AsosAdjustment: nudged #{count} scores from #{length(observations)} obs at #{valid_time}")
|
|
|
|
warm_and_broadcast(valid_time)
|
|
:ok
|
|
|
|
error ->
|
|
Logger.error("AsosAdjustment: upsert failed — #{inspect(error)}")
|
|
error
|
|
end
|
|
end
|
|
end
|
|
|
|
defp warm_and_broadcast(valid_time) do
|
|
Enum.each(BandConfig.all_bands(), fn band ->
|
|
Propagation.warm_cache_and_broadcast(band.freq_mhz, valid_time)
|
|
end)
|
|
|
|
Phoenix.PubSub.broadcast(
|
|
Microwaveprop.PubSub,
|
|
"propagation:updated",
|
|
{:propagation_updated, valid_time}
|
|
)
|
|
end
|
|
|
|
defp latest_hrrr_valid_time do
|
|
case Repo.one(
|
|
from(h in HrrrProfile,
|
|
where: h.is_grid_point == true,
|
|
select: max(h.valid_time)
|
|
)
|
|
) do
|
|
nil -> :no_hrrr
|
|
vt -> {:ok, vt}
|
|
end
|
|
end
|
|
|
|
defp filter_fresh(observations) do
|
|
cutoff = DateTime.add(DateTime.utc_now(), -@max_obs_age_seconds, :second)
|
|
|
|
Enum.filter(observations, fn obs ->
|
|
obs.utc_valid && DateTime.after?(obs.utc_valid, cutoff)
|
|
end)
|
|
end
|
|
|
|
defp load_hrrr_profiles(valid_time) do
|
|
Repo.all(
|
|
from(h in HrrrProfile,
|
|
where: h.valid_time == ^valid_time and h.is_grid_point == true,
|
|
select: %{
|
|
lat: h.lat,
|
|
lon: h.lon,
|
|
valid_time: h.valid_time,
|
|
surface_temp_c: h.surface_temp_c,
|
|
surface_dewpoint_c: h.surface_dewpoint_c,
|
|
surface_pressure_mb: h.surface_pressure_mb,
|
|
surface_refractivity: h.surface_refractivity,
|
|
min_refractivity_gradient: h.min_refractivity_gradient,
|
|
hpbl_m: h.hpbl_m,
|
|
pwat_mm: h.pwat_mm,
|
|
ducting_detected: h.ducting_detected,
|
|
duct_characteristics: h.duct_characteristics,
|
|
profile: h.profile
|
|
}
|
|
)
|
|
)
|
|
end
|
|
end
|