Let the /path forecast chart start at the current analysis hour
point_forecast used a strict \`>= now\` cutoff, so the leftmost "now" sample was always dropped — the HRRR publishing lag means the newest analysis file is typically 30–60 min behind the wall clock, which made the filter evict the very hour the chart most wanted to anchor on. If the chain ever fell behind by even one step the whole forecast went empty and the chart disappeared. Use the same window as available_valid_times: keep everything from one hour before now onward, and fall back to the single newest file when nothing is fresh enough. Both the cache-path and store-path flows go through a shared forecast_window helper so the behavior stays in lockstep with the map timeline.
This commit is contained in:
parent
30b4baf792
commit
e227978f2a
2 changed files with 86 additions and 6 deletions
|
|
@ -333,7 +333,7 @@ defmodule Microwaveprop.Propagation do
|
|||
defp point_forecast_from_store(band_mhz, lat, lon, now) do
|
||||
band_mhz
|
||||
|> ScoresFile.list_valid_times()
|
||||
|> Enum.filter(fn t -> DateTime.compare(t, now) != :lt end)
|
||||
|> forecast_window(now)
|
||||
|> Enum.map(&point_forecast_entry(&1, band_mhz, lat, lon))
|
||||
|> Enum.reject(&is_nil/1)
|
||||
end
|
||||
|
|
@ -347,7 +347,7 @@ defmodule Microwaveprop.Propagation do
|
|||
|
||||
defp point_forecast_from_cache(band_mhz, lat, lon, now, cached_times) do
|
||||
cached_times
|
||||
|> Enum.filter(fn t -> DateTime.compare(t, now) != :lt end)
|
||||
|> forecast_window(now)
|
||||
|> Enum.map(fn t ->
|
||||
case ScoreCache.fetch_point(band_mhz, t, lat, lon) do
|
||||
{:ok, score} -> %{valid_time: t, score: score}
|
||||
|
|
@ -357,6 +357,20 @@ defmodule Microwaveprop.Propagation do
|
|||
|> Enum.reject(&is_nil/1)
|
||||
end
|
||||
|
||||
# Select the set of valid_times the forecast chart should render.
|
||||
# Mirrors `available_valid_times`: keep everything from one hour
|
||||
# before now onward so the most recent analysis hour (typically
|
||||
# ~30–60 min behind wall clock due to HRRR publishing lag) sits at
|
||||
# the left edge of the chart as "now". When every hour on disk is
|
||||
# older than that cutoff, fall back to just the newest entry so the
|
||||
# chart can still render a single data point.
|
||||
defp forecast_window([], _now), do: []
|
||||
|
||||
defp forecast_window(times, now) do
|
||||
cutoff = DateTime.add(now, -3600, :second)
|
||||
filter_or_latest(times, cutoff)
|
||||
end
|
||||
|
||||
defp snap_to_grid(lat, lon) do
|
||||
step = Grid.step()
|
||||
{Float.round(Float.round(lat / step) * step, 3), Float.round(Float.round(lon / step) * step, 3)}
|
||||
|
|
|
|||
|
|
@ -318,15 +318,37 @@ defmodule Microwaveprop.PropagationTest do
|
|||
] = result
|
||||
end
|
||||
|
||||
test "filters past valid_times out" do
|
||||
past = DateTime.utc_now() |> DateTime.add(-3600, :second) |> DateTime.truncate(:second)
|
||||
test "filters valid_times older than one hour but keeps recent past as the 'now' anchor" do
|
||||
# Two hours old: dropped. Thirty minutes old: kept as the
|
||||
# leftmost "now" point. One hour future: kept.
|
||||
stale = DateTime.utc_now() |> DateTime.add(-2 * 3600, :second) |> DateTime.truncate(:second)
|
||||
recent = DateTime.utc_now() |> DateTime.add(-1800, :second) |> DateTime.truncate(:second)
|
||||
future = DateTime.utc_now() |> DateTime.add(3600, :second) |> DateTime.truncate(:second)
|
||||
|
||||
ScoreCache.put(10_000, past, [%{lat: 32.875, lon: -97.0, score: 60}])
|
||||
ScoreCache.put(10_000, stale, [%{lat: 32.875, lon: -97.0, score: 40}])
|
||||
ScoreCache.put(10_000, recent, [%{lat: 32.875, lon: -97.0, score: 60}])
|
||||
ScoreCache.put(10_000, future, [%{lat: 32.875, lon: -97.0, score: 80}])
|
||||
|
||||
result = Propagation.point_forecast(10_000, 32.875, -97.0)
|
||||
assert [%{valid_time: ^future, score: 80}] = result
|
||||
|
||||
assert [
|
||||
%{valid_time: ^recent, score: 60},
|
||||
%{valid_time: ^future, score: 80}
|
||||
] = result
|
||||
end
|
||||
|
||||
test "falls back to the single latest valid_time when everything is older than one hour" do
|
||||
# HRRR just cut out (nothing fresh); we still want the chart to
|
||||
# render one point at the newest time we have, matching
|
||||
# available_valid_times semantics.
|
||||
older = DateTime.utc_now() |> DateTime.add(-6 * 3600, :second) |> DateTime.truncate(:second)
|
||||
newer = DateTime.utc_now() |> DateTime.add(-3 * 3600, :second) |> DateTime.truncate(:second)
|
||||
|
||||
ScoreCache.put(10_000, older, [%{lat: 32.875, lon: -97.0, score: 30}])
|
||||
ScoreCache.put(10_000, newer, [%{lat: 32.875, lon: -97.0, score: 45}])
|
||||
|
||||
result = Propagation.point_forecast(10_000, 32.875, -97.0)
|
||||
assert [%{valid_time: ^newer, score: 45}] = result
|
||||
end
|
||||
|
||||
test "snaps coordinates to the nearest grid point" do
|
||||
|
|
@ -343,6 +365,50 @@ defmodule Microwaveprop.PropagationTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "point_forecast/3 from store" do
|
||||
test "includes recent past valid_times and falls back to the latest when stale" do
|
||||
# Three .ntms files on disk: one 2h old (dropped by cutoff),
|
||||
# one 30min old (kept as "now"), one 1h future (kept).
|
||||
stale = DateTime.utc_now() |> DateTime.add(-2 * 3600, :second) |> DateTime.truncate(:second)
|
||||
recent = DateTime.utc_now() |> DateTime.add(-1800, :second) |> DateTime.truncate(:second)
|
||||
future = DateTime.utc_now() |> DateTime.add(3600, :second) |> DateTime.truncate(:second)
|
||||
|
||||
Enum.each([{stale, 40}, {recent, 60}, {future, 80}], fn {t, score} ->
|
||||
Propagation.replace_scores(
|
||||
[%{lat: 32.875, lon: -97.0, valid_time: t, band_mhz: 10_000, score: score, factors: nil}],
|
||||
t
|
||||
)
|
||||
end)
|
||||
|
||||
# Bypass the cache so we go through the store-backed path.
|
||||
ScoreCache.clear()
|
||||
|
||||
result = Propagation.point_forecast(10_000, 32.875, -97.0)
|
||||
|
||||
assert [
|
||||
%{valid_time: ^recent, score: 60},
|
||||
%{valid_time: ^future, score: 80}
|
||||
] = result
|
||||
end
|
||||
|
||||
test "returns just the latest store entry when every hour is stale" do
|
||||
older = DateTime.utc_now() |> DateTime.add(-6 * 3600, :second) |> DateTime.truncate(:second)
|
||||
newer = DateTime.utc_now() |> DateTime.add(-3 * 3600, :second) |> DateTime.truncate(:second)
|
||||
|
||||
Enum.each([{older, 30}, {newer, 45}], fn {t, score} ->
|
||||
Propagation.replace_scores(
|
||||
[%{lat: 32.875, lon: -97.0, valid_time: t, band_mhz: 10_000, score: score, factors: nil}],
|
||||
t
|
||||
)
|
||||
end)
|
||||
|
||||
ScoreCache.clear()
|
||||
|
||||
result = Propagation.point_forecast(10_000, 32.875, -97.0)
|
||||
assert [%{valid_time: ^newer, score: 45}] = result
|
||||
end
|
||||
end
|
||||
|
||||
describe "available_valid_times/1" do
|
||||
test "reads from the on-disk .ntms store" do
|
||||
valid_time = DateTime.truncate(DateTime.utc_now(), :second)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue