Download predict.py from dClimate/persistence-1deg: direct link, hf CLI and curl.
- Browser
- Download file 4.54 kB
-
https://huggingface.co/dClimate/persistence-1deg/resolve/main/predict.py
- Command line
-
hf download hf://dClimate/persistence-1deg/predict.py
-
curl -L -o predict.py https://huggingface.co/dClimate/persistence-1deg/resolve/main/predict.py
4.54 kB
| """Persistence on the 1 degree grid. | |
| The same forecast as the 0.25 degree baseline -- copy the analysis forward | |
| unchanged -- sampled onto the coarse grid the platform also accepts. | |
| Two jobs, neither of them "another baseline for its own sake": | |
| 1. **It exercises the 1 degree path end to end.** The contract allows 0.25 and | |
| 1.0, but until something actually runs at 1.0 the coarse path is only | |
| proven by tests. Grid shape, sampler indexing and the board's grid label are | |
| all resolution-dependent, and all three fail quietly rather than loudly: a | |
| coarse forecast sampled as though it were fine still produces numbers that | |
| look like weather. | |
| 2. **It isolates the cost of resolution alone.** Its skill differs from the | |
| 0.25 degree baseline's by exactly one thing -- the grid -- because the | |
| forecast itself is identical. Any gap between the two rows on the board is | |
| the resolution penalty at the stations, measured rather than assumed. That | |
| is a genuinely useful number to have on the board when reading a coarse | |
| model's score against a fine one's. | |
| ## Why subsampling is honest here, where regridding would not be | |
| The IC is 721x1440 at 0.25 degrees. The 1 degree grid is 181x360, and every | |
| one of its points falls EXACTLY on a 0.25 degree point -- 90 to -90 in steps of | |
| 1.0 is a strict subset of 90 to -90 in steps of 0.25, and likewise for | |
| longitude. So `[::4]` selects real analysis values and invents nothing. | |
| Verified rather than assumed: subsampling the 0.25 degree axes by 4 reproduces | |
| the 1 degree axes with a maximum error of exactly 0.0 on both. | |
| Averaging each 4x4 block instead would be defensible as a physical coarsening, | |
| but it would make this model's forecast DIFFERENT from the fine baseline's, and | |
| then the gap between the two rows would measure "subsample versus average" | |
| rather than the resolution penalty. Picking the collocated point keeps the | |
| comparison about one variable. | |
| """ | |
| from __future__ import annotations | |
| import numpy as np | |
| # The unit the returned arrays are in. | |
| # | |
| # The harness converts once, at the boundary. This returns a slice of the IC | |
| # untouched, and the IC stores 2m temperature in Kelvin -- so Kelvin is what | |
| # this says. Claiming deg_C here would score every value about 273 from the | |
| # observation, which looks like a catastrophically bad model rather than like a | |
| # units bug. | |
| OUTPUT_UNITS = {"temperature_2m": "K"} | |
| # Index of T+0 on the IC's time axis. | |
| # | |
| # The IC carries T-12h, T-6h and T+0 in that order. Persistence forecasts from | |
| # the most recent analysis, so it is the last element, not the first. Index 0 | |
| # would forecast from twelve hours before the init and silently lose that much | |
| # skill at every lead. | |
| T_ZERO = -1 | |
| # 0.25 -> 1.0 degrees. Both axes, because the grid is square in degrees even | |
| # though it is not in kilometres. | |
| STRIDE = 4 | |
| def run(inputs, declaration, init_time): | |
| """Subsample the analysis to 1 degree and hold it at every lead. | |
| Args: | |
| inputs: IC fields as numpy arrays, keyed by ERA5 name. Surface fields | |
| are (time, lat, lon) on the 0.25 degree grid. | |
| 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} on the declared grid. | |
| """ | |
| analysis = np.asarray(inputs["2m_temperature"])[T_ZERO] | |
| rows, cols = declaration["grid_shape"] | |
| coarse = analysis[::STRIDE, ::STRIDE] | |
| # Checked rather than trusted. If the IC's resolution ever changes, `[::4]` | |
| # silently produces a differently-shaped array, and the failure would | |
| # otherwise surface as a harness error naming the shape but not the cause. | |
| if coarse.shape != (rows, cols): | |
| raise ValueError( | |
| f"subsampling the {analysis.shape} IC by {STRIDE} gave {coarse.shape}, " | |
| f"but the declaration says {(rows, cols)}. This model assumes a 0.25 " | |
| "degree IC exactly four times finer than its own grid." | |
| ) | |
| leads = len(declaration["lead_hours"]) | |
| # `broadcast_to` is a read-only view over one grid rather than N copies; | |
| # `np.array` then materialises the single contiguous array the harness | |
| # writes. At 1 degree that is only ~7 MB, but the idiom matches the fine | |
| # baseline so the two read the same. | |
| return { | |
| "temperature_2m": np.array( | |
| np.broadcast_to(coarse, (leads, rows, cols)), dtype="float32" | |
| ) | |
| } | |