File size: 3,748 Bytes
b2db41b 6b8b7a9 b2db41b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | """Persistence: tomorrow is the same as today.
The simplest forecast there is. It copies the T+0 initial condition forward,
unchanged, to every declared lead hour.
This is not a joke entry. Persistence is the standard reference forecast in
meteorology, and on this platform it does two jobs:
1. **A skill floor.** Any model worth ranking must beat it beyond the first step
or two. A submission that cannot is not forecasting, whatever it costs to
run.
2. **A pipeline oracle.** Its error curve is known in advance -- small at +6h,
climbing steadily, bad by +120h. That makes it the one model whose score can
be checked rather than merely believed, which is why it is what the platform
is proven with. Weather forecasts fail plausibly: a latitude flip, a
Kelvin/Celsius slip, or an off-by-one in lead hours all produce output that
still looks like weather. Against persistence those show up immediately:
flat error across leads -> lead hours are not being applied
error around 273 -> a unit conversion was missed
large error at +6h -> grid sampling is wrong (lat or lon)
beats AIFS at long leads -> scoring or window alignment is broken
Keeping it permanently on the leaderboard means any future regression of that
kind is visible on the board itself rather than needing a diagnostic run.
"""
from __future__ import annotations
import numpy as np
# The unit the arrays below are in.
#
# The harness reads this to convert once, at the boundary. Persistence returns a
# slice of the IC untouched, and the IC stores 2m temperature in Kelvin, so that
# is what this says. Getting it wrong is the "confident RMSE of 273" failure --
# which is precisely the failure this model exists to make visible, so it would
# be poor form to commit it here.
OUTPUT_UNITS = {"temperature_2m": "K"}
# Index of T+0 within the IC's time axis.
#
# The IC carries three timesteps, T-12h, T-6h and T+0, in that order
# (`INPUT_STEP_HOURS = (-12, -6, 0)` in the spec). Persistence forecasts from the
# most recent one, so it is the last, not the first. Using index 0 would
# forecast from twelve hours before the init and quietly lose that much skill at
# every lead.
T_ZERO = -1
def run(inputs, declaration, init_time):
"""Copy the analysis forward to every lead hour.
Args:
inputs: IC fields as numpy arrays, keyed by ERA5 name. Surface fields
are (time, lat, lon).
declaration: the parsed earthboi.yaml -- lead_hours, grid_shape, etc.
init_time: the init as an aware datetime (unused; persistence has no
time dependence beyond the analysis it starts from).
Returns:
{"temperature_2m": (lead, lat, lon) float32}
"""
analysis = np.asarray(inputs["2m_temperature"])[T_ZERO]
rows, cols = declaration["grid_shape"]
if analysis.shape != (rows, cols):
# The IC is 0.25 degrees; a 1 degree declaration would need subsampling.
# Persistence does not implement that -- a wrong-shaped array would be
# caught by the harness anyway, but failing here names the actual reason.
raise ValueError(
f"IC grid {analysis.shape} does not match the declared grid "
f"{(rows, cols)}. This baseline only runs at the IC's native resolution."
)
leads = len(declaration["lead_hours"])
# `broadcast_to` returns a read-only view over one grid rather than N copies;
# `np.array` then makes the one contiguous array the harness writes. At 0.25
# degrees and 28 leads that is ~116 MB, which is worth not doubling.
return {
"temperature_2m": np.array(
np.broadcast_to(analysis, (leads, rows, cols)), dtype="float32"
)
}
|