defmodule Microwaveprop.Propagation do @moduledoc false import Ecto.Query alias Microwaveprop.Propagation.BandConfig alias Microwaveprop.Propagation.GridScore alias Microwaveprop.Propagation.Scorer alias Microwaveprop.Repo alias Microwaveprop.Weather.SoundingParams @doc """ Score a single grid point across all bands using HRRR profile data. Returns a list of %{band_mhz, score, factors} maps. """ def score_grid_point(hrrr_profile, valid_time) do derived = derive_from_hrrr(hrrr_profile) temp_c = hrrr_profile.surface_temp_c dewpoint_c = hrrr_profile.surface_dewpoint_c # Skip points with missing surface data (ocean/outside-domain) if is_nil(temp_c) or is_nil(dewpoint_c) do [] else score_grid_point_with_data(hrrr_profile, valid_time, temp_c, dewpoint_c, derived) end end defp score_grid_point_with_data(hrrr_profile, valid_time, temp_c, dewpoint_c, derived) do temp_f = Scorer.c_to_f(temp_c) dewpoint_f = Scorer.c_to_f(dewpoint_c) conditions = %{ abs_humidity: Scorer.absolute_humidity(temp_c, dewpoint_c), temp_f: temp_f, dewpoint_f: dewpoint_f, wind_speed_kts: Scorer.wind_speed_kts(hrrr_profile[:wind_u], hrrr_profile[:wind_v]), sky_cover_pct: hrrr_profile[:cloud_cover_pct], utc_hour: valid_time.hour, utc_minute: valid_time.minute, month: valid_time.month, pressure_mb: hrrr_profile.surface_pressure_mb, prev_pressure_mb: nil, rain_rate_mmhr: Scorer.precip_to_rate_mmhr(hrrr_profile[:precip_mm]), min_refractivity_gradient: derived[:min_refractivity_gradient], bl_depth_m: hrrr_profile[:hpbl_m] } Enum.map(BandConfig.all_bands(), fn band_config -> result = Scorer.composite_score(conditions, band_config) Map.put(result, :band_mhz, band_config.freq_mhz) end) end @doc "Upsert propagation scores in batches." def upsert_scores(scores) do now = DateTime.truncate(DateTime.utc_now(), :second) entries = Enum.map(scores, fn s -> %{ id: Ecto.UUID.generate(), lat: s.lat, lon: s.lon, valid_time: s.valid_time, band_mhz: s.band_mhz, score: s.score, factors: s.factors, inserted_at: now, updated_at: now } end) total = entries |> Enum.chunk_every(500) |> Enum.reduce(0, fn chunk, acc -> {count, _} = Repo.insert_all(GridScore, chunk, on_conflict: {:replace, [:score, :factors, :updated_at]}, conflict_target: [:lat, :lon, :valid_time, :band_mhz] ) acc + count end) {:ok, total} end @doc "Get the latest scores for a band." def latest_scores(band_mhz) do latest_time_query = from(gs in GridScore, where: gs.band_mhz == ^band_mhz, select: max(gs.valid_time) ) case Repo.one(latest_time_query) do nil -> [] latest_time -> Repo.all( from(gs in GridScore, where: gs.band_mhz == ^band_mhz and gs.valid_time == ^latest_time, select: %{lat: gs.lat, lon: gs.lon, score: gs.score} ) ) end end @doc "Get the latest valid_time across all scores." def latest_valid_time do Repo.one(from(gs in GridScore, select: max(gs.valid_time))) end defp derive_from_hrrr(%{profile: profile}) when is_list(profile) and length(profile) >= 3 do case SoundingParams.derive(profile) do nil -> %{} derived -> %{min_refractivity_gradient: derived.min_refractivity_gradient} end end defp derive_from_hrrr(_), do: %{} end