diff --git a/lib/microwaveprop/weather.ex b/lib/microwaveprop/weather.ex index 0bc8adc0..0fb6465a 100644 --- a/lib/microwaveprop/weather.ex +++ b/lib/microwaveprop/weather.ex @@ -3,17 +3,17 @@ defmodule Microwaveprop.Weather do import Ecto.Query - require Logger - alias Microwaveprop.Repo - alias Microwaveprop.Weather.IemClient alias Microwaveprop.Weather.HrrrProfile + alias Microwaveprop.Weather.IemClient alias Microwaveprop.Weather.IemreObservation alias Microwaveprop.Weather.SolarIndex alias Microwaveprop.Weather.Sounding alias Microwaveprop.Weather.Station alias Microwaveprop.Weather.SurfaceObservation + require Logger + # Approximate km per degree latitude @km_per_deg_lat 111.0 @@ -97,7 +97,10 @@ defmodule Microwaveprop.Weather do end def sync_stations! do - asos = for s <- ~w(AK AL AR AZ CA CO CT DE FL GA HI IA ID IL IN KS KY LA MA MD ME MI MN MO MS MT NC ND NE NH NJ NM NV NY OH OK OR PA RI SC SD TN TX UT VA VT WA WI WV WY), do: "#{s}_ASOS" + asos = + for s <- + ~w(AK AL AR AZ CA CO CT DE FL GA HI IA ID IL IN KS KY LA MA MD ME MI MN MO MS MT NC ND NE NH NJ NM NV NY OH OK OR PA RI SC SD TN TX UT VA VT WA WI WV WY), + do: "#{s}_ASOS" for network <- asos ++ ["RAOB"] do type = if String.contains?(network, "ASOS"), do: "asos", else: "sounding" @@ -198,6 +201,72 @@ defmodule Microwaveprop.Weather do %{surface_observations: surface_observations, soundings: soundings} end + def latest_grid_valid_time do + Repo.one( + from(h in HrrrProfile, + where: + fragment("(? * 1000)::int % 125 = 0", h.lat) and + fragment("(? * 1000)::int % 125 = 0", h.lon), + select: max(h.valid_time) + ) + ) + end + + def latest_weather_grid(bounds) do + case latest_grid_valid_time() do + nil -> + [] + + latest_vt -> + Repo.all( + from(h in HrrrProfile, + where: + h.valid_time == ^latest_vt and + h.lat >= ^bounds["south"] and h.lat <= ^bounds["north"] and + h.lon >= ^bounds["west"] and h.lon <= ^bounds["east"] and + fragment("(? * 1000)::int % 125 = 0", h.lat) and + fragment("(? * 1000)::int % 125 = 0", h.lon), + select: %{ + lat: h.lat, + lon: h.lon, + valid_time: h.valid_time, + temperature: h.surface_temp_c, + dewpoint_depression: fragment("? - ?", h.surface_temp_c, h.surface_dewpoint_c), + bl_height: h.hpbl_m, + pwat: h.pwat_mm, + refractivity_gradient: h.min_refractivity_gradient, + ducting: h.ducting_detected, + surface_pressure_mb: h.surface_pressure_mb + } + ) + ) + end + end + + def weather_point_detail(lat, lon, valid_time) do + step = 0.125 + snapped_lat = Float.round(Float.round(lat / step) * step, 3) + snapped_lon = Float.round(Float.round(lon / step) * step, 3) + + Repo.one( + from(h in HrrrProfile, + where: h.lat == ^snapped_lat and h.lon == ^snapped_lon and h.valid_time == ^valid_time, + select: %{ + lat: h.lat, + lon: h.lon, + valid_time: h.valid_time, + temperature: h.surface_temp_c, + dewpoint_depression: fragment("? - ?", h.surface_temp_c, h.surface_dewpoint_c), + bl_height: h.hpbl_m, + pwat: h.pwat_mm, + refractivity_gradient: h.min_refractivity_gradient, + ducting: h.ducting_detected, + surface_pressure_mb: h.surface_pressure_mb + } + ) + ) + end + def upsert_hrrr_profile(attrs) do %HrrrProfile{} |> HrrrProfile.changeset(attrs) @@ -210,7 +279,9 @@ defmodule Microwaveprop.Weather do def upsert_hrrr_profiles_batch(profiles, opts \\ []) do now = DateTime.truncate(DateTime.utc_now(), :second) - on_conflict = if Keyword.get(opts, :skip_existing, false), do: :nothing, else: {:replace_all_except, [:id, :inserted_at]} + + on_conflict = + if Keyword.get(opts, :skip_existing, false), do: :nothing, else: {:replace_all_except, [:id, :inserted_at]} profiles |> Enum.chunk_every(500) diff --git a/test/microwaveprop/weather_grid_test.exs b/test/microwaveprop/weather_grid_test.exs new file mode 100644 index 00000000..c225e57f --- /dev/null +++ b/test/microwaveprop/weather_grid_test.exs @@ -0,0 +1,169 @@ +defmodule Microwaveprop.WeatherGridTest do + use Microwaveprop.DataCase, async: true + + alias Microwaveprop.Repo + alias Microwaveprop.Weather + + describe "latest_weather_grid/1" do + test "returns grid data for latest valid_time within bounds" do + now = DateTime.truncate(DateTime.utc_now(), :second) + + insert_hrrr_profile(%{ + lat: 32.125, + lon: -97.375, + valid_time: now, + surface_temp_c: 25.0, + surface_dewpoint_c: 18.0, + surface_pressure_mb: 1013.0, + hpbl_m: 500.0, + pwat_mm: 30.0, + min_refractivity_gradient: -280.0, + ducting_detected: true + }) + + insert_hrrr_profile(%{ + lat: 32.250, + lon: -97.375, + valid_time: now, + surface_temp_c: 26.0, + surface_dewpoint_c: 19.0, + surface_pressure_mb: 1012.0, + hpbl_m: 600.0, + pwat_mm: 32.0, + min_refractivity_gradient: -150.0, + ducting_detected: false + }) + + bounds = %{"south" => 32.0, "north" => 33.0, "west" => -98.0, "east" => -97.0} + result = Weather.latest_weather_grid(bounds) + + assert length(result) == 2 + point = Enum.find(result, &(&1.lat == 32.125)) + assert point.temperature == 25.0 + assert point.dewpoint_depression == 7.0 + assert point.bl_height == 500.0 + assert point.pwat == 30.0 + assert point.refractivity_gradient == -280.0 + assert point.ducting == true + end + + test "returns empty list when no data exists" do + bounds = %{"south" => 32.0, "north" => 33.0, "west" => -98.0, "east" => -97.0} + assert Weather.latest_weather_grid(bounds) == [] + end + + test "only returns points on 0.125 grid" do + now = DateTime.truncate(DateTime.utc_now(), :second) + + insert_hrrr_profile(%{ + lat: 32.125, + lon: -97.375, + valid_time: now, + surface_temp_c: 25.0, + surface_dewpoint_c: 18.0, + surface_pressure_mb: 1013.0, + hpbl_m: 500.0, + pwat_mm: 30.0 + }) + + insert_hrrr_profile(%{ + lat: 32.1337, + lon: -97.3821, + valid_time: now, + surface_temp_c: 25.0, + surface_dewpoint_c: 18.0, + surface_pressure_mb: 1013.0, + hpbl_m: 500.0, + pwat_mm: 30.0 + }) + + bounds = %{"south" => 32.0, "north" => 33.0, "west" => -98.0, "east" => -97.0} + result = Weather.latest_weather_grid(bounds) + + assert length(result) == 1 + assert hd(result).lat == 32.125 + end + + test "uses most recent valid_time only" do + old = DateTime.utc_now() |> DateTime.add(-7200, :second) |> DateTime.truncate(:second) + new = DateTime.truncate(DateTime.utc_now(), :second) + + insert_hrrr_profile(%{ + lat: 32.125, + lon: -97.375, + valid_time: old, + surface_temp_c: 20.0, + surface_dewpoint_c: 15.0, + surface_pressure_mb: 1010.0, + hpbl_m: 400.0, + pwat_mm: 25.0 + }) + + insert_hrrr_profile(%{ + lat: 32.125, + lon: -97.375, + valid_time: new, + surface_temp_c: 28.0, + surface_dewpoint_c: 20.0, + surface_pressure_mb: 1015.0, + hpbl_m: 700.0, + pwat_mm: 35.0 + }) + + bounds = %{"south" => 32.0, "north" => 33.0, "west" => -98.0, "east" => -97.0} + result = Weather.latest_weather_grid(bounds) + + assert length(result) == 1 + assert hd(result).temperature == 28.0 + end + end + + describe "weather_point_detail/3" do + test "returns all fields for a single grid point" do + now = DateTime.truncate(DateTime.utc_now(), :second) + + insert_hrrr_profile(%{ + lat: 32.125, + lon: -97.375, + valid_time: now, + surface_temp_c: 25.0, + surface_dewpoint_c: 18.0, + surface_pressure_mb: 1013.0, + hpbl_m: 500.0, + pwat_mm: 30.0, + min_refractivity_gradient: -280.0, + ducting_detected: true + }) + + result = Weather.weather_point_detail(32.15, -97.40, now) + + assert result.lat == 32.125 + assert result.temperature == 25.0 + assert result.surface_pressure_mb == 1013.0 + end + + test "returns nil when no data at point" do + now = DateTime.truncate(DateTime.utc_now(), :second) + assert Weather.weather_point_detail(32.15, -97.40, now) == nil + end + end + + defp insert_hrrr_profile(attrs) do + now = DateTime.truncate(DateTime.utc_now(), :second) + + defaults = %{ + id: Ecto.UUID.bingenerate(), + run_time: DateTime.add(attrs.valid_time, -3600, :second), + profile: [], + surface_refractivity: nil, + min_refractivity_gradient: nil, + ducting_detected: nil, + duct_characteristics: nil, + inserted_at: now, + updated_at: now + } + + entry = Map.merge(defaults, attrs) + Repo.insert_all("hrrr_profiles", [entry]) + end +end