"""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" ) }