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