feat(weather): nearest_sounding_to helper for path calculator
Spatial + temporal nearest-RAOB lookup (defaults to 300 km / ±3 h) joining weather_stations → soundings. Returns the full sounding row so callers can read the derived ducting_detected / min_refractivity_ gradient fields that HRRR's pressure-level product systematically under-reads for thin surface ducts. Prep for the path-calculator enrichment: a sounding within range at the path midpoint gives an independent duct signal to cross-check the nine HRRR samples already taken along the path.
This commit is contained in:
parent
622edee180
commit
18d16b0ad6
2 changed files with 91 additions and 0 deletions
|
|
@ -795,6 +795,54 @@ defmodule Microwaveprop.Weather do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Nearest sounding to (`lat`, `lon`) within `radius_km` km and a ±3-hour
|
||||||
|
window around `timestamp`. Joins `weather_stations` to `soundings` so
|
||||||
|
the caller gets the raw sounding row (station_id set, derived duct
|
||||||
|
fields populated) back.
|
||||||
|
|
||||||
|
Returns `{:ok, sounding}` or `{:error, :not_found}`. Use this in the
|
||||||
|
path calculator to surface the nearest RAOB's `ducting_detected` flag
|
||||||
|
as an independent check on HRRR's pressure-level duct signal, which
|
||||||
|
under-reads thin surface ducts.
|
||||||
|
"""
|
||||||
|
@spec nearest_sounding_to(float(), float(), DateTime.t(), keyword()) ::
|
||||||
|
{:ok, Sounding.t()} | {:error, :not_found}
|
||||||
|
def nearest_sounding_to(lat, lon, timestamp, opts \\ []) do
|
||||||
|
radius_km = Keyword.get(opts, :radius_km, 300)
|
||||||
|
hours = Keyword.get(opts, :hours, 3)
|
||||||
|
|
||||||
|
# 1 deg lat ≈ 111 km; lon scaled by cos(lat).
|
||||||
|
dlat = radius_km / 111.0
|
||||||
|
dlon = radius_km / (111.0 * max(0.1, :math.cos(lat * :math.pi() / 180.0)))
|
||||||
|
time_start = DateTime.add(timestamp, -hours * 3600, :second)
|
||||||
|
time_end = DateTime.add(timestamp, hours * 3600, :second)
|
||||||
|
|
||||||
|
from(s in Sounding,
|
||||||
|
join: station in assoc(s, :station),
|
||||||
|
where:
|
||||||
|
station.lat >= ^(lat - dlat) and station.lat <= ^(lat + dlat) and
|
||||||
|
station.lon >= ^(lon - dlon) and station.lon <= ^(lon + dlon) and
|
||||||
|
s.observed_at >= ^time_start and s.observed_at <= ^time_end,
|
||||||
|
order_by:
|
||||||
|
fragment(
|
||||||
|
"SQRT(POW(? - ?, 2) + POW(? - ?, 2)) + ABS(EXTRACT(EPOCH FROM ? - ?)) / 86400.0",
|
||||||
|
station.lat,
|
||||||
|
^lat,
|
||||||
|
station.lon,
|
||||||
|
^lon,
|
||||||
|
s.observed_at,
|
||||||
|
^timestamp
|
||||||
|
),
|
||||||
|
limit: 1
|
||||||
|
)
|
||||||
|
|> Repo.one()
|
||||||
|
|> case do
|
||||||
|
nil -> {:error, :not_found}
|
||||||
|
sounding -> {:ok, sounding}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@spec find_nearest_hrrr(float(), float(), DateTime.t()) :: HrrrProfile.t() | nil
|
@spec find_nearest_hrrr(float(), float(), DateTime.t()) :: HrrrProfile.t() | nil
|
||||||
def find_nearest_hrrr(lat, lon, timestamp) do
|
def find_nearest_hrrr(lat, lon, timestamp) do
|
||||||
dlat = 0.07
|
dlat = 0.07
|
||||||
|
|
|
||||||
|
|
@ -265,6 +265,49 @@ defmodule Microwaveprop.WeatherTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "nearest_sounding_to/3" do
|
||||||
|
@profile [
|
||||||
|
%{"pres" => 1013.0, "hght" => 171, "tmpc" => 25.0, "dwpc" => 15.0},
|
||||||
|
%{"pres" => 925.0, "hght" => 800, "tmpc" => 20.0, "dwpc" => 12.0}
|
||||||
|
]
|
||||||
|
|
||||||
|
setup do
|
||||||
|
{:ok, station} = Weather.find_or_create_station(@sounding_station_attrs)
|
||||||
|
|
||||||
|
{:ok, sounding} =
|
||||||
|
Weather.upsert_sounding(station, %{
|
||||||
|
observed_at: ~U[2026-03-28 12:00:00Z],
|
||||||
|
profile: @profile,
|
||||||
|
level_count: 2,
|
||||||
|
ducting_detected: true,
|
||||||
|
min_refractivity_gradient: -250.0
|
||||||
|
})
|
||||||
|
|
||||||
|
%{station: station, sounding: sounding}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns the closest sounding within spatial + temporal window", %{sounding: sounding} do
|
||||||
|
# Roughly 30 km north of Fort Worth sounding station, 1h after the obs.
|
||||||
|
assert {:ok, found} =
|
||||||
|
Weather.nearest_sounding_to(33.10, -97.30, ~U[2026-03-28 13:00:00Z])
|
||||||
|
|
||||||
|
assert found.id == sounding.id
|
||||||
|
assert found.ducting_detected == true
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns {:error, :not_found} when no sounding is in range" do
|
||||||
|
# Far outside the default 300 km / ±3h window.
|
||||||
|
assert {:error, :not_found} =
|
||||||
|
Weather.nearest_sounding_to(45.0, -80.0, ~U[2026-03-28 12:00:00Z])
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns {:error, :not_found} when the sounding is outside the time window" do
|
||||||
|
# Same location as the sounding, 8 hours later — outside ±3h.
|
||||||
|
assert {:error, :not_found} =
|
||||||
|
Weather.nearest_sounding_to(32.83, -97.30, ~U[2026-03-28 20:00:00Z])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "existing_solar_dates/0" do
|
describe "existing_solar_dates/0" do
|
||||||
test "returns set of dates already in the database" do
|
test "returns set of dates already in the database" do
|
||||||
Weather.upsert_solar_index(%{date: ~D[2024-06-15], sfi: 150.2})
|
Weather.upsert_solar_index(%{date: ~D[2024-06-15], sfi: 150.2})
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue