Add Microwaveprop.Backtest: a feature-evaluation framework that runs a (lat, lon, valid_time) -> float function over the historical QSO corpus and a matched random-time baseline, reporting distribution statistics, distance-binned lift, and band-stratified lift. Adds four baseline feature wrappers around the current scorer inputs (naive_gradient, td_depression, time_of_day, pressure), a mix backtest CLI, and the first set of baseline reports under priv/backtest_reports so downstream phases have a frozen reference point to compare against.
84 lines
2.8 KiB
Elixir
84 lines
2.8 KiB
Elixir
defmodule Microwaveprop.Backtest.Features do
|
|
@moduledoc """
|
|
Named feature functions for use with `Microwaveprop.Backtest`.
|
|
|
|
Every feature has the shape `(lat, lon, valid_time) -> float | nil`
|
|
and is named after the physical quantity it represents. These are
|
|
the "known baselines" the plan refers to: wrappers around the
|
|
existing scorer's inputs so we can measure the lift of new features
|
|
against them on an apples-to-apples basis.
|
|
|
|
## Contract
|
|
|
|
- Return a `float` when the underlying data is available.
|
|
- Return `nil` when there's no HRRR profile within the usual match
|
|
window (`Weather.find_nearest_hrrr/3` returning nil).
|
|
- Never raise — bad inputs should produce `nil`, not crashes. The
|
|
backtest harness runs these across tens of thousands of calls and
|
|
a raise on one point kills the whole report.
|
|
"""
|
|
|
|
alias Microwaveprop.Weather
|
|
|
|
@doc """
|
|
Minimum refractivity gradient from the nearest HRRR profile.
|
|
|
|
This is the scalar the current scorer uses. More negative is better
|
|
(stronger ducting potential). We return the raw gradient; the
|
|
backtest harness handles binning and summarizing.
|
|
"""
|
|
@spec naive_gradient(float, float, DateTime.t()) :: float | nil
|
|
def naive_gradient(lat, lon, valid_time) do
|
|
with %{min_refractivity_gradient: grad} when is_float(grad) <-
|
|
Weather.find_nearest_hrrr(lat, lon, valid_time) do
|
|
grad
|
|
else
|
|
_ -> nil
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Dewpoint depression (T - Td) at the surface, in °C.
|
|
|
|
Lower depression means higher relative humidity. For 10 GHz the
|
|
existing scorer treats this as beneficial; for 24+ GHz it's harmful.
|
|
"""
|
|
@spec td_depression(float, float, DateTime.t()) :: float | nil
|
|
def td_depression(lat, lon, valid_time) do
|
|
with %{surface_temp_c: t, surface_dewpoint_c: td} when is_float(t) and is_float(td) <-
|
|
Weather.find_nearest_hrrr(lat, lon, valid_time) do
|
|
t - td
|
|
else
|
|
_ -> nil
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Time-of-day feature: hours since midnight UTC, as a float in [0, 24).
|
|
|
|
A flat-by-time baseline against which diurnal lift is measured. The
|
|
existing scorer collapses this to a band-dependent shape; the
|
|
backtest treats it as raw UTC hour so we can see the shape directly
|
|
in the distribution.
|
|
"""
|
|
@spec time_of_day(float, float, DateTime.t()) :: float
|
|
def time_of_day(_lat, _lon, valid_time) do
|
|
valid_time.hour + valid_time.minute / 60.0
|
|
end
|
|
|
|
@doc """
|
|
Surface pressure in hPa from the nearest HRRR profile.
|
|
|
|
Used as the baseline the plan predicts `ParallelToFront` (Phase 5)
|
|
will replace.
|
|
"""
|
|
@spec pressure(float, float, DateTime.t()) :: float | nil
|
|
def pressure(lat, lon, valid_time) do
|
|
with %{surface_pressure_mb: p} when is_float(p) <-
|
|
Weather.find_nearest_hrrr(lat, lon, valid_time) do
|
|
p
|
|
else
|
|
_ -> nil
|
|
end
|
|
end
|
|
end
|