From 9cbf8b91f48bf06e98a820771f520c4ecf9e350e Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Fri, 3 Apr 2026 16:52:01 -0500 Subject: [PATCH] Derive upper-air weather layers from HRRR profiles in weather grid queries --- lib/microwaveprop/weather.ex | 96 +++++++++++++++--------- test/microwaveprop/weather_grid_test.exs | 94 +++++++++++++++++++++++ 2 files changed, 153 insertions(+), 37 deletions(-) diff --git a/lib/microwaveprop/weather.ex b/lib/microwaveprop/weather.ex index 0fb6465a..ac57ddf6 100644 --- a/lib/microwaveprop/weather.ex +++ b/lib/microwaveprop/weather.ex @@ -11,6 +11,7 @@ defmodule Microwaveprop.Weather do alias Microwaveprop.Weather.Sounding alias Microwaveprop.Weather.Station alias Microwaveprop.Weather.SurfaceObservation + alias Microwaveprop.Weather.WeatherLayers require Logger @@ -218,28 +219,33 @@ defmodule Microwaveprop.Weather do [] 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 - } - ) + 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, + surface_temp_c: h.surface_temp_c, + surface_dewpoint_c: h.surface_dewpoint_c, + surface_refractivity: h.surface_refractivity, + profile: h.profile, + duct_characteristics: h.duct_characteristics + } ) + |> Repo.all() + |> Enum.map(&derive_and_clean/1) end end @@ -248,23 +254,39 @@ defmodule Microwaveprop.Weather do 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 - } - ) + 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, + surface_temp_c: h.surface_temp_c, + surface_dewpoint_c: h.surface_dewpoint_c, + surface_refractivity: h.surface_refractivity, + profile: h.profile, + duct_characteristics: h.duct_characteristics + } ) + |> Repo.one() + |> then(fn + nil -> nil + row -> derive_and_clean(row) + end) + end + + defp derive_and_clean(row) do + derived = WeatherLayers.derive(row) + + row + |> Map.merge(derived) + |> Map.drop([:profile, :duct_characteristics, :surface_temp_c, :surface_dewpoint_c]) end def upsert_hrrr_profile(attrs) do diff --git a/test/microwaveprop/weather_grid_test.exs b/test/microwaveprop/weather_grid_test.exs index c225e57f..8a383c7f 100644 --- a/test/microwaveprop/weather_grid_test.exs +++ b/test/microwaveprop/weather_grid_test.exs @@ -116,6 +116,57 @@ defmodule Microwaveprop.WeatherGridTest do assert length(result) == 1 assert hd(result).temperature == 28.0 end + + test "includes derived upper-air fields and strips raw profile data" do + now = DateTime.truncate(DateTime.utc_now(), :second) + + profile = [ + %{"pres" => 1000.0, "tmpc" => 25.0, "dwpc" => 18.0, "hght" => 100.0}, + %{"pres" => 925.0, "tmpc" => 20.0, "dwpc" => 14.0, "hght" => 800.0}, + %{"pres" => 850.0, "tmpc" => 15.0, "dwpc" => 10.0, "hght" => 1500.0}, + %{"pres" => 700.0, "tmpc" => 5.0, "dwpc" => -2.0, "hght" => 3100.0}, + %{"pres" => 500.0, "tmpc" => -15.0, "dwpc" => -25.0, "hght" => 5600.0} + ] + + duct_chars = [ + %{"base" => 200.0, "strength" => 50.0}, + %{"base" => 800.0, "strength" => 30.0} + ] + + 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, + surface_refractivity: 320.0, + hpbl_m: 500.0, + pwat_mm: 30.0, + min_refractivity_gradient: -280.0, + ducting_detected: true, + profile: profile, + duct_characteristics: duct_chars + }) + + bounds = %{"south" => 32.0, "north" => 33.0, "west" => -98.0, "east" => -97.0} + [point] = Weather.latest_weather_grid(bounds) + + # Derived fields should be present + assert is_float(point.surface_rh) + assert point.surface_refractivity == 320.0 + assert point.temp_850mb == 15.0 + assert point.dewpoint_850mb == 10.0 + assert is_float(point.lapse_rate) + assert point.duct_base_m == 200.0 + assert point.duct_strength == 50.0 + + # Raw bulky fields should be stripped + refute Map.has_key?(point, :profile) + refute Map.has_key?(point, :duct_characteristics) + refute Map.has_key?(point, :surface_temp_c) + refute Map.has_key?(point, :surface_dewpoint_c) + end end describe "weather_point_detail/3" do @@ -146,6 +197,49 @@ defmodule Microwaveprop.WeatherGridTest do now = DateTime.truncate(DateTime.utc_now(), :second) assert Weather.weather_point_detail(32.15, -97.40, now) == nil end + + test "includes derived upper-air fields and strips raw profile data" do + now = DateTime.truncate(DateTime.utc_now(), :second) + + profile = [ + %{"pres" => 1000.0, "tmpc" => 25.0, "dwpc" => 18.0, "hght" => 100.0}, + %{"pres" => 850.0, "tmpc" => 15.0, "dwpc" => 10.0, "hght" => 1500.0}, + %{"pres" => 500.0, "tmpc" => -15.0, "dwpc" => -25.0, "hght" => 5600.0} + ] + + duct_chars = [%{"base" => 300.0, "strength" => 40.0}] + + 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, + surface_refractivity: 320.0, + hpbl_m: 500.0, + pwat_mm: 30.0, + min_refractivity_gradient: -280.0, + ducting_detected: true, + profile: profile, + duct_characteristics: duct_chars + }) + + result = Weather.weather_point_detail(32.15, -97.40, now) + + # Derived fields + assert is_float(result.surface_rh) + assert result.surface_refractivity == 320.0 + assert result.temp_850mb == 15.0 + assert result.duct_base_m == 300.0 + assert result.duct_strength == 40.0 + + # Raw fields stripped + refute Map.has_key?(result, :profile) + refute Map.has_key?(result, :duct_characteristics) + refute Map.has_key?(result, :surface_temp_c) + refute Map.has_key?(result, :surface_dewpoint_c) + end end defp insert_hrrr_profile(attrs) do