- Fix score_pressure crash on nil pressure_mb (coastal HRRR points) - Set 10-min timeout on grid score upsert transaction (was :infinity) - Single DELETE for prune_old_scores instead of N queries in a loop - Remove dead load_hrrr_refractivity that loaded 95k rows into nil map - Pass selected_time to point_detail to skip latest_valid_time sub-query - Batch station existence checks (1 query per path point, not per station) - Batch solar index upserts via insert_all in chunks of 500 - Batch backfill_distances via single UPDATE FROM VALUES statement - Add is_grid_point boolean + partial index to hrrr_profiles (replaces non-sargable modular arithmetic filter on every weather map query) - Add partial index on contacts(qso_timestamp) WHERE pos1 IS NOT NULL - Move backfill enqueue to Oban worker so UI returns immediately
198 lines
6.1 KiB
Elixir
198 lines
6.1 KiB
Elixir
defmodule Microwaveprop.Workers.AsosAdjustmentWorker do
|
|
@moduledoc """
|
|
Runs every 10 minutes to fetch the latest ASOS observations and re-score
|
|
grid points near reporting stations. This provides fresher data between
|
|
the hourly HRRR grid computations.
|
|
|
|
For each grid point, finds the nearest ASOS station within 75 km.
|
|
If the station reported within the last 30 minutes, re-scores using
|
|
the ASOS observation for temp/dewpoint/wind/sky/pressure/precip and
|
|
the HRRR refractivity gradient from the last grid computation.
|
|
"""
|
|
|
|
use Oban.Worker, queue: :propagation, max_attempts: 3
|
|
|
|
alias Microwaveprop.Propagation
|
|
alias Microwaveprop.Propagation.BandConfig
|
|
alias Microwaveprop.Propagation.Grid
|
|
alias Microwaveprop.Propagation.Scorer
|
|
alias Microwaveprop.Radio
|
|
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
|
|
)
|
|
|
|
# Only adjust grid points within this distance of an ASOS station
|
|
@max_station_distance_km 75
|
|
# Only use observations newer than this
|
|
@max_obs_age_seconds 1800
|
|
|
|
@impl Oban.Worker
|
|
def perform(%Oban.Job{}) do
|
|
now = DateTime.utc_now()
|
|
cutoff = DateTime.add(now, -@max_obs_age_seconds, :second)
|
|
|
|
Logger.info("AsosAdjustment: fetching current ASOS observations")
|
|
|
|
with {:ok, observations} <- IemClient.fetch_current_asos(@state_networks) do
|
|
# Filter to recent observations
|
|
fresh =
|
|
Enum.filter(observations, fn obs ->
|
|
obs.utc_valid && DateTime.after?(obs.utc_valid, cutoff)
|
|
end)
|
|
|
|
Logger.info("AsosAdjustment: #{length(fresh)} fresh observations from #{length(observations)} total")
|
|
|
|
if fresh == [] do
|
|
:ok
|
|
else
|
|
valid_time = DateTime.truncate(now, :second)
|
|
scores = score_grid_from_asos(fresh, valid_time)
|
|
|
|
Logger.info("AsosAdjustment: computed #{length(scores)} adjusted scores")
|
|
|
|
case Propagation.upsert_scores(scores) do
|
|
{:ok, count} ->
|
|
Logger.info("AsosAdjustment: upserted #{count} scores")
|
|
|
|
Phoenix.PubSub.broadcast(
|
|
Microwaveprop.PubSub,
|
|
"propagation:updated",
|
|
{:propagation_updated, valid_time}
|
|
)
|
|
|
|
:ok
|
|
|
|
error ->
|
|
Logger.error("AsosAdjustment: upsert failed: #{inspect(error)}")
|
|
error
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
defp score_grid_from_asos(observations, valid_time) do
|
|
# Build a spatial index of observations
|
|
obs_list =
|
|
Enum.map(observations, fn obs ->
|
|
%{
|
|
lat: obs.lat,
|
|
lon: obs.lon,
|
|
temp_f: obs.temp_f,
|
|
dewpoint_f: obs.dewpoint_f,
|
|
wind_speed_kts: obs.wind_speed_kts,
|
|
sky_condition: obs.sky_condition,
|
|
sea_level_pressure_mb: pressure_from_obs(obs),
|
|
precip_1h_in: obs.precip_1h_in
|
|
}
|
|
end)
|
|
|
|
# For each grid point, find nearest station and score
|
|
Grid.conus_points()
|
|
|> Task.async_stream(
|
|
fn {lat, lon} = point ->
|
|
nearest = find_nearest_station(obs_list, lat, lon)
|
|
|
|
if nearest do
|
|
score_point_from_obs(point, nearest, valid_time)
|
|
end
|
|
end,
|
|
max_concurrency: System.schedulers_online() * 2,
|
|
timeout: 10_000
|
|
)
|
|
|> Enum.flat_map(fn
|
|
{:ok, nil} -> []
|
|
{:ok, scores} -> scores
|
|
{:exit, _} -> []
|
|
end)
|
|
end
|
|
|
|
defp find_nearest_station(obs_list, lat, lon) do
|
|
obs_list
|
|
|> Enum.map(fn obs ->
|
|
dist = Radio.haversine_km(lat, lon, obs.lat, obs.lon)
|
|
{dist, obs}
|
|
end)
|
|
|> Enum.filter(fn {dist, _} -> dist <= @max_station_distance_km end)
|
|
|> Enum.min_by(fn {dist, _} -> dist end, fn -> nil end)
|
|
|> case do
|
|
nil -> nil
|
|
{_dist, obs} -> obs
|
|
end
|
|
end
|
|
|
|
defp score_point_from_obs({lat, lon}, obs, valid_time) do
|
|
temp_c = Scorer.f_to_c(obs.temp_f)
|
|
dewpoint_c = Scorer.f_to_c(obs.dewpoint_f)
|
|
|
|
if is_nil(temp_c) or is_nil(dewpoint_c) do
|
|
[]
|
|
else
|
|
sky_pct = sky_condition_to_pct(obs.sky_condition)
|
|
rain_rate = precip_to_rate(obs.precip_1h_in)
|
|
|
|
conditions = %{
|
|
abs_humidity: Scorer.absolute_humidity(temp_c, dewpoint_c),
|
|
temp_f: obs.temp_f,
|
|
dewpoint_f: obs.dewpoint_f,
|
|
wind_speed_kts: obs.wind_speed_kts,
|
|
sky_cover_pct: sky_pct,
|
|
utc_hour: valid_time.hour,
|
|
utc_minute: valid_time.minute,
|
|
month: valid_time.month,
|
|
longitude: lon,
|
|
pressure_mb: obs.sea_level_pressure_mb,
|
|
prev_pressure_mb: nil,
|
|
rain_rate_mmhr: rain_rate,
|
|
min_refractivity_gradient: nil,
|
|
bl_depth_m: nil
|
|
}
|
|
|
|
Enum.map(BandConfig.all_bands(), fn band ->
|
|
result = Scorer.composite_score(conditions, band)
|
|
|
|
%{
|
|
lat: lat,
|
|
lon: lon,
|
|
valid_time: valid_time,
|
|
band_mhz: band.freq_mhz,
|
|
score: result.score,
|
|
factors: result.factors
|
|
}
|
|
end)
|
|
end
|
|
end
|
|
|
|
defp sky_condition_to_pct(nil), do: nil
|
|
defp sky_condition_to_pct("CLR"), do: 0.0
|
|
defp sky_condition_to_pct("SKC"), do: 0.0
|
|
defp sky_condition_to_pct("FEW"), do: 18.0
|
|
defp sky_condition_to_pct("SCT"), do: 40.0
|
|
defp sky_condition_to_pct("BKN"), do: 75.0
|
|
defp sky_condition_to_pct("OVC"), do: 100.0
|
|
defp sky_condition_to_pct("VV"), do: 100.0
|
|
defp sky_condition_to_pct(_), do: nil
|
|
|
|
defp pressure_from_obs(%{sea_level_pressure_mb: mslp}) when is_number(mslp), do: mslp
|
|
|
|
defp pressure_from_obs(%{altimeter_setting: alti}) when is_number(alti) do
|
|
# Convert altimeter setting (inHg) to millibars
|
|
alti * 33.8639
|
|
end
|
|
|
|
defp pressure_from_obs(_), do: nil
|
|
|
|
# Convert hourly precip accumulation (inches) to rain rate (mm/hr)
|
|
defp precip_to_rate(nil), do: 0.0
|
|
defp precip_to_rate(inches) when inches <= 0, do: 0.0
|
|
defp precip_to_rate(inches), do: inches * 25.4
|
|
end
|