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
|
||||
|
||||
@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
|
||||
def find_nearest_hrrr(lat, lon, timestamp) do
|
||||
dlat = 0.07
|
||||
|
|
|
|||
|
|
@ -265,6 +265,49 @@ defmodule Microwaveprop.WeatherTest do
|
|||
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
|
||||
test "returns set of dates already in the database" do
|
||||
Weather.upsert_solar_index(%{date: ~D[2024-06-15], sfi: 150.2})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue