LonghaoWang commited on
Commit
41b767b
·
verified ·
1 Parent(s): b4cbf7c

Switch to the 2026-10-25 CAS-Canglong SST model: 12-month mean, spread and quintile probabilities; held-out skill and hindcast tab; Oct 2026 IRI plume; atmosphere 2026-10-22

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +26 -0
  2. README.md +18 -12
  3. app.py +217 -99
  4. assets/maps/atm_msl_w1.png +2 -2
  5. assets/maps/atm_msl_w2.png +2 -2
  6. assets/maps/atm_msl_w3.png +2 -2
  7. assets/maps/atm_msl_w4.png +2 -2
  8. assets/maps/atm_msl_w5.png +2 -2
  9. assets/maps/atm_msl_w6.png +2 -2
  10. assets/maps/atm_t2m_w1.png +2 -2
  11. assets/maps/atm_t2m_w2.png +2 -2
  12. assets/maps/atm_t2m_w3.png +2 -2
  13. assets/maps/atm_t2m_w4.png +2 -2
  14. assets/maps/atm_t2m_w5.png +2 -2
  15. assets/maps/atm_t2m_w6.png +2 -2
  16. assets/maps/atm_tp_w1.png +2 -2
  17. assets/maps/atm_tp_w2.png +2 -2
  18. assets/maps/atm_tp_w3.png +2 -2
  19. assets/maps/atm_tp_w4.png +2 -2
  20. assets/maps/atm_tp_w5.png +2 -2
  21. assets/maps/atm_tp_w6.png +2 -2
  22. assets/maps/atm_z500_w1.png +2 -2
  23. assets/maps/atm_z500_w2.png +2 -2
  24. assets/maps/atm_z500_w3.png +2 -2
  25. assets/maps/atm_z500_w4.png +2 -2
  26. assets/maps/atm_z500_w5.png +2 -2
  27. assets/maps/atm_z500_w6.png +2 -2
  28. assets/maps/{sst_t13.png → pupper_t01.png} +2 -2
  29. assets/maps/{sst_t14.png → pupper_t02.png} +2 -2
  30. assets/maps/{sst_t15.png → pupper_t03.png} +2 -2
  31. assets/maps/{sst_t16.png → pupper_t04.png} +2 -2
  32. assets/maps/pupper_t05.png +3 -0
  33. assets/maps/pupper_t06.png +3 -0
  34. assets/maps/pupper_t07.png +3 -0
  35. assets/maps/pupper_t08.png +3 -0
  36. assets/maps/pupper_t09.png +3 -0
  37. assets/maps/pupper_t10.png +3 -0
  38. assets/maps/pupper_t11.png +3 -0
  39. assets/maps/pupper_t12.png +3 -0
  40. assets/maps/sigma_t01.png +3 -0
  41. assets/maps/sigma_t02.png +3 -0
  42. assets/maps/sigma_t03.png +3 -0
  43. assets/maps/sigma_t04.png +3 -0
  44. assets/maps/sigma_t05.png +3 -0
  45. assets/maps/sigma_t06.png +3 -0
  46. assets/maps/sigma_t07.png +3 -0
  47. assets/maps/sigma_t08.png +3 -0
  48. assets/maps/sigma_t09.png +3 -0
  49. assets/maps/sigma_t10.png +3 -0
  50. assets/maps/sigma_t11.png +3 -0
.gitattributes CHANGED
@@ -93,3 +93,29 @@ assets/maps/ssta_t15.png filter=lfs diff=lfs merge=lfs -text
93
  assets/maps/ssta_t16.png filter=lfs diff=lfs merge=lfs -text
94
  data/canglong_atmos_start20261008.nc filter=lfs diff=lfs merge=lfs -text
95
  data/canglong_sst_init202609.nc filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  assets/maps/ssta_t16.png filter=lfs diff=lfs merge=lfs -text
94
  data/canglong_atmos_start20261008.nc filter=lfs diff=lfs merge=lfs -text
95
  data/canglong_sst_init202609.nc filter=lfs diff=lfs merge=lfs -text
96
+ assets/maps/pupper_t01.png filter=lfs diff=lfs merge=lfs -text
97
+ assets/maps/pupper_t02.png filter=lfs diff=lfs merge=lfs -text
98
+ assets/maps/pupper_t03.png filter=lfs diff=lfs merge=lfs -text
99
+ assets/maps/pupper_t04.png filter=lfs diff=lfs merge=lfs -text
100
+ assets/maps/pupper_t05.png filter=lfs diff=lfs merge=lfs -text
101
+ assets/maps/pupper_t06.png filter=lfs diff=lfs merge=lfs -text
102
+ assets/maps/pupper_t07.png filter=lfs diff=lfs merge=lfs -text
103
+ assets/maps/pupper_t08.png filter=lfs diff=lfs merge=lfs -text
104
+ assets/maps/pupper_t09.png filter=lfs diff=lfs merge=lfs -text
105
+ assets/maps/pupper_t10.png filter=lfs diff=lfs merge=lfs -text
106
+ assets/maps/pupper_t11.png filter=lfs diff=lfs merge=lfs -text
107
+ assets/maps/pupper_t12.png filter=lfs diff=lfs merge=lfs -text
108
+ assets/maps/sigma_t01.png filter=lfs diff=lfs merge=lfs -text
109
+ assets/maps/sigma_t02.png filter=lfs diff=lfs merge=lfs -text
110
+ assets/maps/sigma_t03.png filter=lfs diff=lfs merge=lfs -text
111
+ assets/maps/sigma_t04.png filter=lfs diff=lfs merge=lfs -text
112
+ assets/maps/sigma_t05.png filter=lfs diff=lfs merge=lfs -text
113
+ assets/maps/sigma_t06.png filter=lfs diff=lfs merge=lfs -text
114
+ assets/maps/sigma_t07.png filter=lfs diff=lfs merge=lfs -text
115
+ assets/maps/sigma_t08.png filter=lfs diff=lfs merge=lfs -text
116
+ assets/maps/sigma_t09.png filter=lfs diff=lfs merge=lfs -text
117
+ assets/maps/sigma_t10.png filter=lfs diff=lfs merge=lfs -text
118
+ assets/maps/sigma_t11.png filter=lfs diff=lfs merge=lfs -text
119
+ assets/maps/sigma_t12.png filter=lfs diff=lfs merge=lfs -text
120
+ data/canglong_atmos_start20261022.nc filter=lfs diff=lfs merge=lfs -text
121
+ data/canglong_sst_round5_init202609.nc filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -9,7 +9,11 @@ python_version: "3.12"
9
  app_file: app.py
10
  pinned: true
11
  license: mit
12
- short_description: Live 16-month global SST and ENSO forecast from CAS-Canglong
 
 
 
 
13
  tags:
14
  - weather
15
  - climate
@@ -22,19 +26,21 @@ tags:
22
 
23
  # 🐉 CAS-Canglong · global SST & ENSO forecast
24
 
25
- The current operational run of **CAS-Canglong**, a 3-D Earth-specific transformer for sub-seasonal to seasonal prediction
26
- of global sea-surface temperature ([*Advanced Science* 2026](https://doi.org/10.1002/advs.78271) ·
27
- [code](https://github.com/GISWLH/CAS-Canglong) · live portal [canglong.earth](https://canglong.earth)).
 
 
28
 
29
- * **Ocean & ENSO** — 16 monthly SST and SST-anomaly maps (1° model grid and 0.25° refined field) and the Niño 3.4
30
- outlook against the IRI ENSO plume.
31
  * **Regional outlook** — Niño regions, the Indian Ocean Dipole, the tropical North Atlantic, the Kuroshio, the
32
- Mediterranean and more, month by month.
 
33
  * **Atmosphere · 6 weeks (experimental)** — weekly 2 m temperature, precipitation, sea-level pressure and 500 hPa height
34
  from the CAS-Canglong atmospheric run, read with [WeatherPre](https://github.com/GISWLH/WeatherPre).
35
- * **Data** — the NetCDF and CSV files behind every map.
36
 
37
- The fields are identical to those on canglong.earth. They come from re-running the frozen checkpoints on a GPU
38
- workstation; the Space only displays them. Assets are rebuilt with
39
- [`hf_space/scripts/build_assets.py`](https://github.com/GISWLH/CAS-Canglong/tree/main/hf_space).
40
- This is a research forecast, not an official warning product; see the *About* tab for limitations.
 
9
  app_file: app.py
10
  pinned: true
11
  license: mit
12
+ short_description: 12-month global SST & ENSO outlook with probabilities
13
+ models:
14
+ - LonghaoWang/CAS-Canglong-SST
15
+ datasets:
16
+ - LonghaoWang/SST-Hindcast-2016-2022-1deg-lead1-12
17
  tags:
18
  - weather
19
  - climate
 
26
 
27
  # 🐉 CAS-Canglong · global SST & ENSO forecast
28
 
29
+ The latest run of the **2026-10-25 release of the CAS-Canglong 1° SST model**
30
+ ([weights and code](https://huggingface.co/LonghaoWang/CAS-Canglong-SST) ·
31
+ [2016–2022 hindcast](https://huggingface.co/datasets/LonghaoWang/SST-Hindcast-2016-2022-1deg-lead1-12) ·
32
+ [paper, *Advanced Science* 2026](https://doi.org/10.1002/advs.78271) · [code](https://github.com/GISWLH/CAS-Canglong) ·
33
+ live portal [canglong.earth](https://canglong.earth)).
34
 
35
+ * **Ocean & ENSO** — 12 monthly maps of SST, SST anomaly, the chance of a top-quintile month and the forecast spread, and
36
+ the Niño 3.4 outlook against the IRI ENSO plume.
37
  * **Regional outlook** — Niño regions, the Indian Ocean Dipole, the tropical North Atlantic, the Kuroshio, the
38
+ Mediterranean and more, with the last 12 observed months.
39
+ * **Skill & hindcasts** — a held-out test on 2023–2026 targets and the 2016–2022 Niño 3.4 hindcast, lead by lead.
40
  * **Atmosphere · 6 weeks (experimental)** — weekly 2 m temperature, precipitation, sea-level pressure and 500 hPa height
41
  from the CAS-Canglong atmospheric run, read with [WeatherPre](https://github.com/GISWLH/WeatherPre).
42
+ * **Model & data** — the NetCDF and CSV files behind every map, and code to load the model and the hindcast.
43
 
44
+ The Space only displays precomputed fields; they come from the frozen model run on a GPU workstation and are rebuilt with
45
+ [`hf_space/scripts/build_assets.py`](https://github.com/GISWLH/CAS-Canglong/tree/main/hf_space). This is a research
46
+ forecast, not an official warning product; see the *About* tab for its limits, in particular for ENSO.
 
app.py CHANGED
@@ -1,6 +1,6 @@
1
  """CAS-Canglong forecast showcase (Hugging Face Space).
2
 
3
- Everything shown is read from `data/` and `assets/`, written by `scripts/build_assets.py` from one forecast run.
4
  """
5
  from __future__ import annotations
6
 
@@ -18,16 +18,22 @@ DATA, MAPS = ROOT / "data", ROOT / "assets" / "maps"
18
  RUN = json.loads((DATA / "run.json").read_text())
19
  SST = xr.open_dataset(DATA / RUN["sst_file"]).load()
20
  IRI = json.loads((DATA / "iri_plume.json").read_text())
 
 
 
 
21
 
22
  SURFACE, GRIDLINE, BASELINE, INK, INK2, MUTED = "#fcfcfb", "#e1e0d9", "#c3c2b7", "#0b0b0b", "#52514e", "#898781"
23
  CAS_BLUE, DYN_ORANGE, STAT_AQUA, CONTEXT = "#2a78d6", "#eb6834", "#1baf7a", "#c3c2b7"
24
  FONT = "system-ui, -apple-system, Segoe UI, sans-serif"
25
 
26
- FRAMES = RUN["frames"] # index 0 = observed initial month, 1..16 = forecast leads
27
  WEEKS = RUN["weeks"]
28
  MONTHS = pd.to_datetime(SST.time.values)
29
  OBS_MONTH = pd.Timestamp(SST.obs_time.values[-1])
30
- NINO = SST.nino34.values.astype(float)
 
 
31
 
32
  # lat/lon boxes on the 0..360 grid; DMI is west minus east
33
  REGIONS = {
@@ -44,6 +50,18 @@ REGIONS = {
44
  "Mediterranean (30°N–46°N, 0°–36°E)": [(30, 46, 0, 36)],
45
  "Global ocean (60°S–60°N)": [(-60, 60, 0, 360)],
46
  }
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
 
49
  def box_mean(field: xr.DataArray, boxes) -> np.ndarray:
@@ -56,7 +74,8 @@ def box_mean(field: xr.DataArray, boxes) -> np.ndarray:
56
 
57
 
58
  def region_series(name: str) -> np.ndarray:
59
- fields = xr.concat([SST.observed_ssta.expand_dims(time=[np.datetime64(OBS_MONTH, "ns")]), SST.ssta], "time")
 
60
  spec = REGIONS[name]
61
  if spec == "dmi":
62
  return box_mean(fields, [(-10, 10, 50, 70)]) - box_mean(fields, [(-10, 0, 90, 110)])
@@ -89,18 +108,8 @@ def _center(season: str) -> pd.Timestamp | None:
89
 
90
  def plume_figure() -> go.Figure:
91
  fig = go.Figure()
92
- obs = [(_center(o["season"]), o["data"], o["label"]) for o in IRI["observed"]]
93
- iri_anchor = max(obs, key=lambda o: o[0])
94
-
95
- def connect_observation(anchor, x, y, color, width, group):
96
- # A visual bridge to the first forecast; no extra forecast values are introduced.
97
- fig.add_trace(go.Scatter(x=[anchor[0], x[0]], y=[anchor[1], y[0]], mode="lines",
98
- line=dict(color=color, width=width, dash="dot"),
99
- legendgroup=group, showlegend=False, hoverinfo="skip"))
100
-
101
  for k, m in enumerate(IRI["models"]):
102
  x = [_center(s) for s in m["seasons"]]
103
- connect_observation(iri_anchor, x, m["data"], CONTEXT, 1, "iri")
104
  fig.add_trace(go.Scatter(x=x, y=m["data"], mode="lines", line=dict(color=CONTEXT, width=1),
105
  name="IRI plume models", legendgroup="iri", showlegend=k == 0, opacity=0.9,
106
  hovertemplate=f"{m['model']} ({m['type'].lower()}): %{{y:+.2f}} °C<extra></extra>"))
@@ -109,26 +118,20 @@ def plume_figure() -> go.Figure:
109
  if avg["model"] not in colors: continue
110
  x = [_center(s) for s in avg["seasons"]]
111
  name = f"IRI {avg['model']} mean"
112
- connect_observation(iri_anchor, x, avg["data"], colors[avg["model"]], 2, name)
113
- fig.add_trace(go.Scatter(x=x, y=avg["data"], mode="lines+markers", name=name, legendgroup=name,
114
  line=dict(color=colors[avg["model"]], width=2), marker=dict(size=8, line=dict(color=SURFACE, width=2)),
115
  hovertemplate=f"{name}: %{{y:+.2f}} °C<extra></extra>"))
116
- run = pd.Series(NINO).rolling(3, center=True).mean().values[1:-1]
 
 
117
  xs = MONTHS[1:-1]
118
- cas_anchor = (OBS_MONTH, float(SST.observed_nino34.values[-1]))
119
- connect_observation(cas_anchor, xs, run, CAS_BLUE, 3, "cas")
120
  fig.add_trace(go.Scatter(x=xs, y=run, mode="lines+markers", name="CAS-Canglong (3-month mean)",
121
- legendgroup="cas",
122
  line=dict(color=CAS_BLUE, width=3), marker=dict(size=8, line=dict(color=SURFACE, width=2)),
123
  hovertemplate="CAS-Canglong: %{y:+.2f} °C<extra></extra>"))
124
  fig.add_annotation(x=xs[-1], y=run[-1], text="CAS-Canglong", xanchor="left", xshift=8, showarrow=False,
125
  font=dict(color=INK, size=12))
126
- fig.add_trace(go.Scatter(x=[cas_anchor[0]], y=[cas_anchor[1]], mode="markers",
127
- name=f"Observed (ERA5, {OBS_MONTH:%b %Y})",
128
- marker=dict(color=CAS_BLUE, size=10, symbol="diamond", line=dict(color=SURFACE, width=2)),
129
- hovertemplate="ERA5 observed %{x|%b %Y}: %{y:+.2f} °C<extra></extra>"))
130
- fig.add_trace(go.Scatter(x=[o[0] for o in obs], y=[o[1] for o in obs], mode="lines+markers", name="Observed (IRI)",
131
- line=dict(color=INK, width=2),
132
  marker=dict(color=INK, size=9, symbol="diamond", line=dict(color=SURFACE, width=2)),
133
  text=[o[2] for o in obs], hovertemplate="%{text}: %{y:+.2f} °C<extra></extra>"))
134
  for y, label in ((0.5, "El Niño +0.5 °C"), (-0.5, "La Niña −0.5 °C")):
@@ -136,38 +139,48 @@ def plume_figure() -> go.Figure:
136
  fig.add_annotation(x=0, xref="paper", y=y, text=label, xanchor="left", yanchor="bottom", xshift=4,
137
  showarrow=False, font=dict(color=MUTED, size=11))
138
  fig.update_xaxes(dtick="M2", tickformat="%b<br>%Y", tickangle=0)
139
- return _layout(fig, "Niño 3.4 SST anomaly (°C)", 460)
140
 
141
 
142
  def region_figure(name: str) -> go.Figure:
143
  y = region_series(name)
144
- x = [OBS_MONTH, *MONTHS]
 
145
  fig = go.Figure()
146
- fig.add_trace(go.Scatter(x=x, y=y, mode="lines+markers", name="CAS-Canglong forecast",
147
- line=dict(color=CAS_BLUE, width=2),
148
- marker=dict(size=[0] + [8] * len(MONTHS), line=dict(color=SURFACE, width=2)),
149
- text=["ERA5 observed"] + ["CAS-Canglong forecast"] * len(MONTHS),
150
- hovertemplate="%{text} %{x|%b %Y}: %{y:+.2f} °C<extra></extra>"))
151
- fig.add_trace(go.Scatter(x=x[:1], y=y[:1], mode="markers", name=f"ERA5 observed {OBS_MONTH:%b %Y}",
152
- marker=dict(color=INK, size=10, symbol="diamond", line=dict(color=SURFACE, width=2)),
153
  hovertemplate="observed %{x|%b %Y}: %{y:+.2f} °C<extra></extra>"))
 
 
 
 
 
154
  if name.startswith("Niño"):
155
  for v in (0.5, -0.5):
156
  fig.add_hline(y=v, line=dict(color=MUTED, width=1, dash="dot"))
157
  fig.update_xaxes(dtick="M2", tickformat="%b<br>%Y", tickangle=0)
158
- return _layout(fig, "SST anomaly (°C)", 400)
159
 
160
 
161
  def region_table(name: str) -> pd.DataFrame:
162
- y = region_series(name)
163
  labels = [f"{OBS_MONTH:%b %Y} (observed)"] + [f"{t:%b %Y}" for t in MONTHS]
164
  return pd.DataFrame({"Month": labels, "Lead (months)": ["0"] + [str(i) for i in range(1, len(MONTHS) + 1)],
165
  "SST anomaly (°C)": [f"{v:+.2f}" for v in y]})
166
 
167
 
168
  def map_path(field: str, index: int) -> str:
169
- key = FRAMES[int(index)]["key"]
170
- return str(MAPS / f"{'ssta' if field.startswith('SST anomaly') else 'sst'}_{key}.png")
 
 
 
 
 
 
 
 
 
171
 
172
 
173
  def atmos_path(var: str, week: int) -> str:
@@ -176,80 +189,152 @@ def atmos_path(var: str, week: int) -> str:
176
  return str(MAPS / f"atm_{code}_w{int(week)}.png")
177
 
178
 
179
- def frame_label(index: int) -> str:
180
- return f"**{FRAMES[int(index)]['label']}**"
181
-
182
-
183
  def week_label(week: int) -> str:
184
  return f"**{WEEKS[int(week) - 1]['label']}**"
185
 
186
 
187
- above = [t for t, v in zip(MONTHS, NINO) if v > 0.5]
188
- peak_i = int(np.argmax(NINO))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  glob = region_series("Global ocean (60°S–60°N)")
190
  KPIS = [
191
- ("Niño 3.4 · " + f"{MONTHS[0]:%b %Y}", f"{NINO[0]:+.2f} °C", "first forecast month"),
192
- ("Niño 3.4 · " + f"{MONTHS[3]:%b %Y}", f"{NINO[3]:+.2f} °C", "boreal winter, 4-month lead"),
193
- ("El Niño (> +0.5 °C)", f"{above[0]:%b %Y} – {above[-1]:%b %Y}" if above else "–", f"{len(above)} of {len(NINO)} forecast months"),
194
- ("Global ocean 60°S–60°N", f"{glob[1]:+.2f} °C", f"area-mean anomaly, {MONTHS[0]:%b %Y}"),
 
195
  ]
196
  kpi_html = "".join(f'<div class="kpi"><div class="kpi-label">{a}</div><div class="kpi-value">{b}</div>'
197
  f'<div class="kpi-note">{c}</div></div>' for a, b, c in KPIS)
198
 
199
  HEADER = f"""
200
  <div class="hero">
201
- <div class="hero-kicker">CAS-Canglong · live research forecast</div>
202
  <h1>🐉 Global sea-surface temperature &amp; ENSO outlook</h1>
203
- <p>Input: ERA5 monthly means through <b>{OBS_MONTH:%B %Y}</b> → forecasts for <b>{MONTHS[0]:%b %Y} – {MONTHS[-1]:%b %Y}</b>
204
- (16 months) · six-week atmosphere from <b>{RUN['atmos_start']}</b></p>
205
  <div class="chips">
 
 
206
  <a href="https://doi.org/10.1002/advs.78271" target="_blank">📄 Paper · Advanced Science</a>
207
  <a href="https://github.com/GISWLH/CAS-Canglong" target="_blank">⭐ GitHub</a>
208
  <a href="https://canglong.earth" target="_blank">🌍 canglong.earth</a>
209
- <a href="https://github.com/GISWLH/WeatherAI" target="_blank">🧩 WeatherAI (PyTorch port)</a>
210
  <a href="https://github.com/GISWLH/WeatherPre" target="_blank">📊 WeatherPre</a>
211
  </div>
212
  </div>
213
  <div class="kpis">{kpi_html}</div>
214
  """
215
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  ABOUT = f"""
217
  ### What this is
218
- CAS-Canglong is a 3-D Earth-specific transformer for sub-seasonal to seasonal prediction of global sea-surface
219
- temperature (SST) ([Wang et al., *Advanced Science* 2026](https://doi.org/10.1002/advs.78271);
220
- preprint [arXiv:2409.05369](https://arxiv.org/abs/2409.05369)). This Space shows the **current operational run**, the same
221
- forecast that [canglong.earth](https://canglong.earth) serves:
222
-
223
- * **Ocean (16 months)** — a one-pass Swin-transformer SST model on a 1° grid reads 16 months of eight ERA5 monthly
224
- fields (SST, MSLP, latent heat flux, 10 m winds, 500/850 hPa geopotential, net solar radiation) and predicts the next
225
- 16 monthly SST fields in one forward pass; a learned refiner adds 0.25° detail without changing the 1° block means.
 
 
 
 
 
 
 
 
226
  * **Atmosphere (6 weeks, experimental)** — CAS-Canglong V3.5 weekly means published on canglong.earth, read through
227
  [WeatherPre](https://github.com/GISWLH/WeatherPre) (`wp.forecast("canglong", "t2m", "6w")`).
228
 
229
  ### How to read the maps
230
- * **SST anomaly** maps are the native 1° model output minus the ERA5 1959–2016 monthly climatology (the model's training
231
- period, the same reference as its Niño 3.4 index). On the 1991–2020 base that IRI uses, Niño 3.4 values would be
232
- about 0.1 °C lower (0.04–0.18 °C, depending on the month).
233
- * **SST** maps are the 0.25° field. Within about 2° of land it is blended with the latest ERA5 SST for display (8 grid
234
- rings replaced, 6 rings faded), exactly as on canglong.earth. That coastal belt is a visual repair, not model skill.
235
- * The **ENSO plume** compares CAS-Canglong's 3-month running mean with the
236
- [IRI ENSO plume]({IRI['source']}) published {IRI['published']}. The plume is redrawn from IRI's own figure.
 
 
237
 
238
  ### Limits — please read
239
- * A research forecast, not an official or operational warning product. No probability is implied: this is one
240
- deterministic run.
241
- * Checkpoint acceptance for the operational ocean model used agreement with the historical NWP multi-model mean and
242
- with the current IRI plume, so this run is **not an independent test of skill**. Published hindcast skill is in the paper.
243
- * The six-week atmospheric run is **not drift-corrected**. Against the ERA5 1990–2017 weekly climatology, its
244
- area-mean 2 m temperature poleward of 60°N is about +5 °C in week 1 and +14 °C in week 6, while 60°S–60°N stays within
245
- ±1 °C. Treat high-latitude fields as model climate, not as anomalies; anomaly maps are therefore not shown.
 
 
 
 
 
246
 
247
  ### Provenance of this run
248
- * Ocean run: {RUN['run_time_utc'][:16].replace('T', ' ')} UTC, re-run from the frozen checkpoints in an isolated
249
- directory; identical to the operational product (max |ΔSST| = {RUN['check_vs_operational']['max_abs_sst_diff_K']} K,
250
- max |ΔNiño 3.4| = {RUN['check_vs_operational']['max_abs_nino34_diff_K']} K).
251
- * Checkpoint SHA-256: 1° `{RUN['checkpoint_sha256']['lowres_checkpoint'][:16]}…`, 0.25° refiner
252
- `{RUN['checkpoint_sha256']['highres_checkpoint'][:16]}…`; input cache `{RUN['input_cache_sha256'][:16]}…`.
253
  * Atmosphere: {RUN['atmos_source']}.
254
 
255
  ### Cite
@@ -275,7 +360,7 @@ CSS = """
275
  .hero .chips a {text-decoration: none !important; padding: 5px 12px; border-radius: 999px; font-size: .9rem;
276
  border: 1px solid var(--border-color-primary); color: var(--body-text-color) !important; background: var(--block-background-fill);}
277
  .hero .chips a:hover {border-color: var(--color-accent);}
278
- .kpis {display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 10px; margin: 14px 0 4px;}
279
  .kpi {padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border-color-primary); background: var(--block-background-fill);}
280
  .kpi-label {font-size: .8rem; color: var(--body-text-color-subdued);}
281
  .kpi-value {font-size: 1.55rem; font-weight: 650; margin: 2px 0;}
@@ -287,26 +372,43 @@ with gr.Blocks(title="CAS-Canglong · SST & ENSO forecast") as demo:
287
  with gr.Tabs():
288
  with gr.Tab("🌊 Ocean & ENSO"):
289
  with gr.Row(equal_height=True):
290
- field = gr.Radio(["SST anomaly (1°)", "SST (0.25°)"], value="SST anomaly (1°)", label="Field", scale=1)
291
  lead = gr.Slider(0, len(FRAMES) - 1, value=1, step=1, label="Month (0 = last observed input month)", scale=3)
292
- caption = gr.Markdown(frame_label(1))
293
  image = gr.Image(map_path("SST anomaly", 1), show_label=False, interactive=False, buttons=["download", "fullscreen"])
294
  gr.Markdown("#### Niño 3.4 outlook vs the IRI ENSO plume")
295
  gr.Plot(plume_figure(), show_label=False)
296
- gr.Markdown("Dotted lines connect each forecast curve to its latest available monthly observation "
297
- "(IRI for the IRI models; ERA5 for CAS-Canglong). These are visual guides from monthly "
298
- "observations to 3-month forecast means; diamonds mark observations.")
299
- gr.Markdown("Monthly CAS-Canglong Niño 3.4 (°C): " + ", ".join(
300
- f"{t:%b %Y} {v:+.2f}" for t, v in zip(MONTHS, NINO)) + ".")
301
- lead.change(lambda f, i: (map_path(f, i), frame_label(i)), [field, lead], [image, caption])
302
- field.change(lambda f, i: map_path(f, i), [field, lead], image)
303
  with gr.Tab("📍 Regional outlook"):
304
  region = gr.Dropdown(list(REGIONS), value=list(REGIONS)[0], label="Region")
305
  rplot = gr.Plot(region_figure(list(REGIONS)[0]), show_label=False)
306
- gr.Markdown(f"Box means of the native 1° anomaly field (cos-latitude weighted, ocean cells only). The headline "
307
- f"Niño 3.4 ({NINO[0]:+.2f} °C for {MONTHS[0]:%b %Y}) is the operational index from the 0.25° field.")
308
  rtable = gr.Dataframe(region_table(list(REGIONS)[0]), interactive=False)
309
  region.change(lambda r: (region_figure(r), region_table(r)), region, [rplot, rtable])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
310
  with gr.Tab("🌦️ Atmosphere · 6 weeks (experimental)"):
311
  gr.Markdown("Weekly-mean fields of the CAS-Canglong V3.5 atmospheric run published on canglong.earth. "
312
  "**Not drift-corrected** — high-latitude temperatures drift warm with lead time (see *About*).")
@@ -318,15 +420,31 @@ with gr.Blocks(title="CAS-Canglong · SST & ENSO forecast") as demo:
318
  aimg = gr.Image(atmos_path("2 m temperature", 1), show_label=False, interactive=False, buttons=["download", "fullscreen"])
319
  week.change(lambda v, w: (atmos_path(v, w), week_label(w)), [avar, week], [aimg, wcap])
320
  avar.change(lambda v, w: atmos_path(v, w), [avar, week], aimg)
321
- with gr.Tab("📦 Data"):
322
- gr.Markdown("NetCDF and CSV files behind every map and chart on this page.")
323
- gr.File([str(DATA / RUN["sst_file"]), str(DATA / RUN["atmos_file"]), str(DATA / "nino34.csv"), str(DATA / "run.json")],
 
 
 
 
 
324
  file_count="multiple", label="Downloads", interactive=False)
325
  gr.Code(f"""import xarray as xr
326
- from huggingface_hub import hf_hub_download
327
- path = hf_hub_download("LonghaoWang/CAS-Canglong", "data/{RUN['sst_file']}", repo_type="space")
328
- ds = xr.open_dataset(path) # ssta / sst (1°), sst_025 (0.25°), nino34, observed fields
329
- print(ds.nino34.to_series())""", language="python", label="Load in Python")
 
 
 
 
 
 
 
 
 
 
 
330
  with gr.Tab("ℹ️ About"):
331
  gr.Markdown(ABOUT)
332
 
 
1
  """CAS-Canglong forecast showcase (Hugging Face Space).
2
 
3
+ Everything shown is read from `data/` and `assets/`, written by `scripts/build_assets.py` from one forecast release.
4
  """
5
  from __future__ import annotations
6
 
 
18
  RUN = json.loads((DATA / "run.json").read_text())
19
  SST = xr.open_dataset(DATA / RUN["sst_file"]).load()
20
  IRI = json.loads((DATA / "iri_plume.json").read_text())
21
+ HINDCAST = pd.read_csv(DATA / "hindcast_nino34.csv", parse_dates=["target_month"])
22
+ HINDCAST_SKILL = pd.read_csv(DATA / "hindcast_nino34_skill.csv")
23
+ HELDOUT = pd.read_csv(DATA / "round6_heldout_metrics.csv")
24
+ MODEL_REPO, HINDCAST_REPO = RUN["model_repo"], RUN["hindcast_dataset"]
25
 
26
  SURFACE, GRIDLINE, BASELINE, INK, INK2, MUTED = "#fcfcfb", "#e1e0d9", "#c3c2b7", "#0b0b0b", "#52514e", "#898781"
27
  CAS_BLUE, DYN_ORANGE, STAT_AQUA, CONTEXT = "#2a78d6", "#eb6834", "#1baf7a", "#c3c2b7"
28
  FONT = "system-ui, -apple-system, Segoe UI, sans-serif"
29
 
30
+ FRAMES = RUN["frames"] # index 0 = observed last input month, 1..12 = forecast leads
31
  WEEKS = RUN["weeks"]
32
  MONTHS = pd.to_datetime(SST.time.values)
33
  OBS_MONTH = pd.Timestamp(SST.obs_time.values[-1])
34
+ HIST = pd.to_datetime(SST.hist_time.values) # last 12 observed months, ending with OBS_MONTH
35
+ NINO = SST.nino34_9120.values.astype(float)
36
+ NINO_HIST = SST.nino34_history_9120.values.astype(float)
37
 
38
  # lat/lon boxes on the 0..360 grid; DMI is west minus east
39
  REGIONS = {
 
50
  "Mediterranean (30°N–46°N, 0°–36°E)": [(30, 46, 0, 36)],
51
  "Global ocean (60°S–60°N)": [(-60, 60, 0, 360)],
52
  }
53
+ FIELDS = {"SST anomaly": "ssta", "SST": "sst", "Chance of a top-quintile month": "pupper", "Forecast uncertainty (σ)": "sigma"}
54
+ HELDOUT_TRACKS = {
55
+ "12 common start months, Jul 2023–Jun 2024 (8 models)": "all_available_models",
56
+ "25 common start months, Jul 2023–Jul 2025 (7 models)": "longer_common_period",
57
+ "34 fixed target months, Dec 2023–Sep 2026 (own models only)": "own_fixed_target_months",
58
+ }
59
+ HELDOUT_REGIONS = {"Global": "Global", "Niño 3.4": "Nino3.4", "Tropical Indo-Pacific (20°S–20°N)": "WP",
60
+ "North Pacific (20°N–70°N)": "PDO", "Arctic (70°N–90°N)": "AO", "South Indian (60°S–40°S)": "SIO",
61
+ "South Pacific (60°S–40°S)": "SPO", "South Atlantic (60°S–40°S)": "SAO"}
62
+ HELDOUT_LABELS = {"Cutoff-2022 model": "CAS-Canglong method, refit with data to 2022",
63
+ "Seasonal baseline": "frozen seasonal model (before calibration)",
64
+ "Original baseline": "original head", "Surface baseline": "surface head", "Joint baseline": "joint head"}
65
 
66
 
67
  def box_mean(field: xr.DataArray, boxes) -> np.ndarray:
 
74
 
75
 
76
  def region_series(name: str) -> np.ndarray:
77
+ """12 observed months (ending with the last input month) followed by the 12 forecast months, 1991-2020 base."""
78
+ fields = xr.concat([SST.history_ssta_9120.rename(hist_time="t"), SST.ssta_9120.rename(time="t")], "t")
79
  spec = REGIONS[name]
80
  if spec == "dmi":
81
  return box_mean(fields, [(-10, 10, 50, 70)]) - box_mean(fields, [(-10, 0, 90, 110)])
 
108
 
109
  def plume_figure() -> go.Figure:
110
  fig = go.Figure()
 
 
 
 
 
 
 
 
 
111
  for k, m in enumerate(IRI["models"]):
112
  x = [_center(s) for s in m["seasons"]]
 
113
  fig.add_trace(go.Scatter(x=x, y=m["data"], mode="lines", line=dict(color=CONTEXT, width=1),
114
  name="IRI plume models", legendgroup="iri", showlegend=k == 0, opacity=0.9,
115
  hovertemplate=f"{m['model']} ({m['type'].lower()}): %{{y:+.2f}} °C<extra></extra>"))
 
118
  if avg["model"] not in colors: continue
119
  x = [_center(s) for s in avg["seasons"]]
120
  name = f"IRI {avg['model']} mean"
121
+ fig.add_trace(go.Scatter(x=x, y=avg["data"], mode="lines+markers", name=name,
 
122
  line=dict(color=colors[avg["model"]], width=2), marker=dict(size=8, line=dict(color=SURFACE, width=2)),
123
  hovertemplate=f"{name}: %{{y:+.2f}} °C<extra></extra>"))
124
+ fig.add_trace(go.Scatter(x=HIST, y=NINO_HIST, mode="lines", name="ERA5 observed (monthly)",
125
+ line=dict(color=INK, width=1.5), hovertemplate="ERA5 %{x|%b %Y}: %{y:+.2f} °C<extra></extra>"))
126
+ run = pd.Series(NINO).rolling(3, center=True).mean().values[1:-1] # forecast-only seasons, OND .. JAS
127
  xs = MONTHS[1:-1]
 
 
128
  fig.add_trace(go.Scatter(x=xs, y=run, mode="lines+markers", name="CAS-Canglong (3-month mean)",
 
129
  line=dict(color=CAS_BLUE, width=3), marker=dict(size=8, line=dict(color=SURFACE, width=2)),
130
  hovertemplate="CAS-Canglong: %{y:+.2f} °C<extra></extra>"))
131
  fig.add_annotation(x=xs[-1], y=run[-1], text="CAS-Canglong", xanchor="left", xshift=8, showarrow=False,
132
  font=dict(color=INK, size=12))
133
+ obs = [(_center(o["season"]), o["data"], o["label"]) for o in IRI["observed"]]
134
+ fig.add_trace(go.Scatter(x=[o[0] for o in obs], y=[o[1] for o in obs], mode="markers", name="Observed (IRI)",
 
 
 
 
135
  marker=dict(color=INK, size=9, symbol="diamond", line=dict(color=SURFACE, width=2)),
136
  text=[o[2] for o in obs], hovertemplate="%{text}: %{y:+.2f} °C<extra></extra>"))
137
  for y, label in ((0.5, "El Niño +0.5 °C"), (-0.5, "La Niña −0.5 °C")):
 
139
  fig.add_annotation(x=0, xref="paper", y=y, text=label, xanchor="left", yanchor="bottom", xshift=4,
140
  showarrow=False, font=dict(color=MUTED, size=11))
141
  fig.update_xaxes(dtick="M2", tickformat="%b<br>%Y", tickangle=0)
142
+ return _layout(fig, "Niño 3.4 SST anomaly (°C, 1991–2020 base)", 460)
143
 
144
 
145
  def region_figure(name: str) -> go.Figure:
146
  y = region_series(name)
147
+ x = [*HIST, *MONTHS]
148
+ n = len(HIST)
149
  fig = go.Figure()
150
+ fig.add_trace(go.Scatter(x=x[:n], y=y[:n], mode="lines+markers", name="ERA5 observed",
151
+ line=dict(color=INK, width=1.5), marker=dict(size=6),
 
 
 
 
 
152
  hovertemplate="observed %{x|%b %Y}: %{y:+.2f} °C<extra></extra>"))
153
+ fig.add_trace(go.Scatter(x=x[n - 1:n + 1], y=y[n - 1:n + 1], mode="lines", line=dict(color=CAS_BLUE, width=2, dash="dot"),
154
+ showlegend=False, hoverinfo="skip"))
155
+ fig.add_trace(go.Scatter(x=x[n:], y=y[n:], mode="lines+markers", name="CAS-Canglong forecast",
156
+ line=dict(color=CAS_BLUE, width=2), marker=dict(size=8, line=dict(color=SURFACE, width=2)),
157
+ hovertemplate="%{x|%b %Y}: %{y:+.2f} °C<extra></extra>"))
158
  if name.startswith("Niño"):
159
  for v in (0.5, -0.5):
160
  fig.add_hline(y=v, line=dict(color=MUTED, width=1, dash="dot"))
161
  fig.update_xaxes(dtick="M2", tickformat="%b<br>%Y", tickangle=0)
162
+ return _layout(fig, "SST anomaly (°C, 1991–2020 base)", 400)
163
 
164
 
165
  def region_table(name: str) -> pd.DataFrame:
166
+ y = region_series(name)[len(HIST) - 1:]
167
  labels = [f"{OBS_MONTH:%b %Y} (observed)"] + [f"{t:%b %Y}" for t in MONTHS]
168
  return pd.DataFrame({"Month": labels, "Lead (months)": ["0"] + [str(i) for i in range(1, len(MONTHS) + 1)],
169
  "SST anomaly (°C)": [f"{v:+.2f}" for v in y]})
170
 
171
 
172
  def map_path(field: str, index: int) -> str:
173
+ code, i = FIELDS[field], int(index)
174
+ if code in ("pupper", "sigma"):
175
+ i = max(i, 1) # probability and spread exist for forecast months only
176
+ return str(MAPS / f"{code}_{FRAMES[i]['key']}.png")
177
+
178
+
179
+ def frame_label(field: str, index: int) -> str:
180
+ i = int(index)
181
+ if FIELDS[field] in ("pupper", "sigma") and i == 0:
182
+ return f"**{FRAMES[1]['label']}** — probability and spread maps start at lead 1"
183
+ return f"**{FRAMES[i]['label']}**"
184
 
185
 
186
  def atmos_path(var: str, week: int) -> str:
 
189
  return str(MAPS / f"atm_{code}_w{int(week)}.png")
190
 
191
 
 
 
 
 
192
  def week_label(week: int) -> str:
193
  return f"**{WEEKS[int(week) - 1]['label']}**"
194
 
195
 
196
+ def heldout_figure(track: str, region: str, metric: str) -> go.Figure:
197
+ col = "acc_spatial" if metric.startswith("Spatial ACC") else "rmse_K"
198
+ d = HELDOUT[(HELDOUT.track == HELDOUT_TRACKS[track]) & (HELDOUT.region == HELDOUT_REGIONS[region])]
199
+ fig = go.Figure()
200
+ greys = iter(["#898781", "#a9a79f", "#c3c2b7", "#d9d8d2"])
201
+ nwp = iter([DYN_ORANGE, "#b8421b", "#f3a5a1"])
202
+ for model, g in sorted(d.groupby("model"), key=lambda kv: (kv[0] != "Cutoff-2022 model", "baseline" not in kv[0], kv[0])):
203
+ g = g.sort_values("lead")
204
+ main = model == "Cutoff-2022 model"
205
+ color = CAS_BLUE if main else next(greys) if "baseline" in model else next(nwp)
206
+ fig.add_trace(go.Scatter(x=g.lead, y=g[col], mode="lines+markers", name=HELDOUT_LABELS.get(model, f"{model} (raw archive)"),
207
+ line=dict(color=color, width=3 if main else 1.6, dash=None if main or "baseline" in model else "dash"),
208
+ marker=dict(size=7 if main else 5),
209
+ hovertemplate=f"{model} · lead %{{x}}: %{{y:.3f}}<extra></extra>"))
210
+ fig.update_xaxes(title="Lead (months)", dtick=1)
211
+ fig = _layout(fig, "Spatial ACC" if col == "acc_spatial" else "RMSE (°C)", 480)
212
+ fig.update_layout(hovermode="closest", margin=dict(r=40, b=110), legend=dict(y=-0.2))
213
+ return fig
214
+
215
+
216
+ def heldout_table(track: str, region: str) -> pd.DataFrame:
217
+ d = HELDOUT[(HELDOUT.track == HELDOUT_TRACKS[track]) & (HELDOUT.region == HELDOUT_REGIONS[region])]
218
+ t = d.groupby("model").agg(acc=("acc_spatial", "mean"), rmse=("rmse_K", "mean"), months=("n_months", "first"),
219
+ first=("first_valid", "min"), last=("last_valid", "max")).sort_values("acc", ascending=False)
220
+ return pd.DataFrame({"Model": [HELDOUT_LABELS.get(m, f"{m} (raw archive)") for m in t.index],
221
+ "Mean spatial ACC (leads 1–12)": t.acc.round(3).values,
222
+ "Mean RMSE °C (leads 1–12)": t.rmse.round(3).values, "Samples per lead": t.months.values,
223
+ "Target months": [f"{a} – {b}" for a, b in zip(t["first"], t["last"])]})
224
+
225
+
226
+ def hindcast_figure(lead: int) -> go.Figure:
227
+ fig = go.Figure()
228
+ fig.add_trace(go.Scatter(x=HINDCAST.target_month, y=HINDCAST.obs, mode="lines", name="ERA5 observed",
229
+ line=dict(color=INK, width=2), hovertemplate="observed %{x|%b %Y}: %{y:+.2f} °C<extra></extra>"))
230
+ fig.add_trace(go.Scatter(x=HINDCAST.target_month, y=HINDCAST[f"lead{int(lead)}"], mode="lines",
231
+ name=f"CAS-Canglong hindcast, lead {int(lead)}", line=dict(color=CAS_BLUE, width=2),
232
+ hovertemplate="hindcast %{x|%b %Y}: %{y:+.2f} °C<extra></extra>"))
233
+ for v in (0.5, -0.5):
234
+ fig.add_hline(y=v, line=dict(color=MUTED, width=1, dash="dot"))
235
+ fig.update_xaxes(dtick="M6", tickformat="%b<br>%Y", tickangle=0)
236
+ return _layout(fig, "Niño 3.4 anomaly (°C, 1959–2009 base)", 400)
237
+
238
+
239
+ def hindcast_caption(lead: int) -> str:
240
+ r = HINDCAST_SKILL[HINDCAST_SKILL.lead == int(lead)].iloc[0]
241
+ return (f"Lead {int(lead)}: correlation **{r['corr']:.2f}**, RMSE **{r['rmse_K']:.2f} °C** over {int(r['n'])} target months "
242
+ "(2016–2022). These targets were used to calibrate the released model, so the scores are in-sample.")
243
+
244
+
245
+ above = [t for t, v in zip(MONTHS, NINO) if v >= 0.5]
246
  glob = region_series("Global ocean (60°S–60°N)")
247
  KPIS = [
248
+ (f"Niño 3.4 · {OBS_MONTH:%b %Y}", f"{NINO_HIST[-1]:+.2f} °C", "observed (ERA5), last input month"),
249
+ (f"Niño 3.4 · {MONTHS[0]:%b %Y}", f"{NINO[0]:+.2f} °C", "forecast, 1-month lead"),
250
+ (f"Niño 3.4 · {MONTHS[3]:%b %Y}", f"{NINO[3]:+.2f} °C", "forecast, 4-month lead"),
251
+ ("El Niño (≥ +0.5 °C)", f"{above[0]:%b %Y} – {above[-1]:%b %Y}" if above else "–", f"{len(above)} of {len(NINO)} forecast months"),
252
+ ("Global ocean 60°S–60°N", f"{glob[len(HIST)]:+.2f} °C", f"area-mean anomaly, {MONTHS[0]:%b %Y}"),
253
  ]
254
  kpi_html = "".join(f'<div class="kpi"><div class="kpi-label">{a}</div><div class="kpi-value">{b}</div>'
255
  f'<div class="kpi-note">{c}</div></div>' for a, b, c in KPIS)
256
 
257
  HEADER = f"""
258
  <div class="hero">
259
+ <div class="hero-kicker">CAS-Canglong · research forecast · model release 2026-10-25</div>
260
  <h1>🐉 Global sea-surface temperature &amp; ENSO outlook</h1>
261
+ <p>Input: ERA5 monthly means through <b>{OBS_MONTH:%B %Y}</b> → mean, spread and calibrated quintile probabilities for
262
+ <b>{MONTHS[0]:%b %Y} – {MONTHS[-1]:%b %Y}</b> (12 months) · six-week atmosphere from <b>{RUN['atmos_start']}</b></p>
263
  <div class="chips">
264
+ <a href="https://huggingface.co/{MODEL_REPO}" target="_blank">🧠 Model weights</a>
265
+ <a href="https://huggingface.co/datasets/{HINDCAST_REPO}" target="_blank">🗂️ Hindcast 2016–2022</a>
266
  <a href="https://doi.org/10.1002/advs.78271" target="_blank">📄 Paper · Advanced Science</a>
267
  <a href="https://github.com/GISWLH/CAS-Canglong" target="_blank">⭐ GitHub</a>
268
  <a href="https://canglong.earth" target="_blank">🌍 canglong.earth</a>
 
269
  <a href="https://github.com/GISWLH/WeatherPre" target="_blank">📊 WeatherPre</a>
270
  </div>
271
  </div>
272
  <div class="kpis">{kpi_html}</div>
273
  """
274
 
275
+ HELDOUT_NOTE = """
276
+ **What was held out.** These curves come from a copy of the released method that was re-fitted with targets up to
277
+ December 2022 only (calibration settings chosen on 2021–2022) and then scored on 2023-01 to 2026-09 targets it never saw.
278
+ The four base heads were trained on targets up to August 2009 and validated up to April 2016. The published weights themselves were calibrated with
279
+ targets through September 2026, so they have no held-out score of their own. The method design was shaped by earlier
280
+ looks at the post-2022 period, so this is a retrospective out-of-time check, not a pristine blind test.
281
+ Spatial ACC is the area-weighted anomaly correlation of each map (not the grid-point temporal correlation). The three
282
+ dynamical systems are raw ensemble-mean archives with no drift or bias correction, which inflates their RMSE. 12–34
283
+ monthly samples per lead are serially correlated; no significance test was made. **Niño 3.4 is the weak spot:** there the
284
+ refit's ACC is below that of its own uncalibrated seasonal model at every lead.
285
+ """
286
+
287
  ABOUT = f"""
288
  ### What this is
289
+ CAS-Canglong learns the coupled ocean–atmosphere system from ERA5 to predict global sea-surface temperature (SST)
290
+ ([Wang et al., *Advanced Science* 2026](https://doi.org/10.1002/advs.78271); preprint
291
+ [arXiv:2409.05369](https://arxiv.org/abs/2409.05369)). This Space shows the **2026-10-25 release of the 1° monthly SST
292
+ model** ([weights and code on Hugging Face](https://huggingface.co/{MODEL_REPO})):
293
+
294
+ * **Inputs** — the last 120 months of eight ERA5 monthly fields on a 1° grid: SST, mean sea-level pressure, surface latent
295
+ heat flux, 10 m u and v wind, 500 and 850 hPa geopotential, net surface solar radiation.
296
+ * **Core** — four frozen heads built on a 1° CAS-Canglong network: the original network/EOF blend, a slow-background
297
+ branch, a joint ocean–atmosphere branch and a seasonal-state branch whose response depends on the calendar month.
298
+ * **Calibration** — a per-grid-cell ridge correction (α = 0.05, 19 coefficients per lead and cell, using the four head
299
+ forecasts, the observed SST anomaly of the last 1, 3 and 12 months, annual/semi-annual harmonics, a time trend and a
300
+ few seasonal interactions),
301
+ mixed 10 % with 90 % of the seasonal model; a CRPS-fitted Gaussian spread; and quintile probabilities calibrated in the
302
+ spirit of the probability-based correction of [Guan et al., *Nature* 2026](https://www.nature.com/articles/s41586-026-11126-x).
303
+ * **Output** — 12 monthly leads on 40,701 ocean cells (nominal 1°): mean SST, Gaussian σ and quantiles, and the
304
+ probability of each fifth of the 1959–2009 climate. Leads 13–16 are not produced.
305
  * **Atmosphere (6 weeks, experimental)** — CAS-Canglong V3.5 weekly means published on canglong.earth, read through
306
  [WeatherPre](https://github.com/GISWLH/WeatherPre) (`wp.forecast("canglong", "t2m", "6w")`).
307
 
308
  ### How to read the maps
309
+ * **SST anomaly** maps and all regional and Niño 3.4 series here use the WMO 1991–2020 normal, computed from the same
310
+ ERA5 record as the inputs, so they can be compared with the [IRI ENSO plume]({IRI['source']}) (published
311
+ {IRI['published']}, redrawn). The model's own reference is 1959-01–2009-08; on that base Niño 3.4 is 0.04–0.23 °C higher,
312
+ depending on the month. Both are in the NetCDF.
313
+ * **Chance of a top-quintile month** is the calibrated probability that the monthly SST falls in the warmest fifth of the
314
+ 1959–2009 climate for that calendar month (20 % = climatological odds). Because the ocean has warmed since then, high
315
+ values are widespread.
316
+ * **Forecast uncertainty (σ)** is a per-cell Gaussian spread. The probabilities are marginal per grid cell, not a coherent
317
+ ensemble, so they cannot be summed into regional or Niño 3.4 uncertainties.
318
 
319
  ### Limits — please read
320
+ * A research forecast, not an official or operational warning product.
321
+ * The released weights were calibrated with targets through 2026-09, and the calibration settings were selected on
322
+ year-blocked diagnostics of the same years. Every historical score of these exact weights, including the published
323
+ 2016–2022 hindcast, is therefore in-sample. The *Skill & hindcasts* tab shows the held-out check of a refit copy.
324
+ * **ENSO:** the forecast damps the 2026 El Niño quickly. Niño 3.4 drops from {NINO_HIST[-1]:+.2f} °C observed in
325
+ {OBS_MONTH:%B} to {NINO[0]:+.2f} °C in {MONTHS[0]:%B}, well below the IRI dynamical models. The damping is already in
326
+ the four frozen base heads (lead 1: +1.5 to +1.8 °C on the model base), and Niño 3.4 is the region where the held-out
327
+ check found this method weakest. Read the Niño 3.4 outlook with that in mind. canglong.earth still serves the previous
328
+ 16-month Public-Swin run, whose Niño 3.4 stays at +3.2 °C or more through December 2026 (on its 1959–2016 base).
329
+ * The six-week atmospheric run is **not drift-corrected**; high-latitude 2 m temperature drifts warm with lead time
330
+ (about +5 °C in week 1 to +14 °C in week 6 poleward of 60°N in earlier runs). Read those fields as model output; anomaly
331
+ maps are not shown.
332
 
333
  ### Provenance of this run
334
+ * Forecast file: `{RUN['sst_file']}`, derived from the release NetCDF (SHA-256 `{(RUN['source_forecast_sha256'] or '')[:16]}…`)
335
+ without changing any value. Checks: SST − anomaly reproduces the stored climatology to
336
+ {RUN['checks']['forecast_sst_minus_ssta_vs_climatology_max_abs_K']:.1e} K, and the observed {OBS_MONTH:%B} anomaly
337
+ equals the model's own input context (max |Δ| = {RUN['checks']['observed_ssta_vs_model_context_max_abs_K']} K).
 
338
  * Atmosphere: {RUN['atmos_source']}.
339
 
340
  ### Cite
 
360
  .hero .chips a {text-decoration: none !important; padding: 5px 12px; border-radius: 999px; font-size: .9rem;
361
  border: 1px solid var(--border-color-primary); color: var(--body-text-color) !important; background: var(--block-background-fill);}
362
  .hero .chips a:hover {border-color: var(--color-accent);}
363
+ .kpis {display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 10px; margin: 14px 0 4px;}
364
  .kpi {padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border-color-primary); background: var(--block-background-fill);}
365
  .kpi-label {font-size: .8rem; color: var(--body-text-color-subdued);}
366
  .kpi-value {font-size: 1.55rem; font-weight: 650; margin: 2px 0;}
 
372
  with gr.Tabs():
373
  with gr.Tab("🌊 Ocean & ENSO"):
374
  with gr.Row(equal_height=True):
375
+ field = gr.Radio(list(FIELDS), value="SST anomaly", label="Field", scale=2)
376
  lead = gr.Slider(0, len(FRAMES) - 1, value=1, step=1, label="Month (0 = last observed input month)", scale=3)
377
+ caption = gr.Markdown(frame_label("SST anomaly", 1))
378
  image = gr.Image(map_path("SST anomaly", 1), show_label=False, interactive=False, buttons=["download", "fullscreen"])
379
  gr.Markdown("#### Niño 3.4 outlook vs the IRI ENSO plume")
380
  gr.Plot(plume_figure(), show_label=False)
381
+ gr.Markdown("Monthly CAS-Canglong Niño 3.4 (°C, 1991–2020 base): " + ", ".join(
382
+ f"{t:%b %Y} {v:+.2f}" for t, v in zip(MONTHS, NINO)) + ". The forecast damps the observed El Niño "
383
+ "much faster than the IRI dynamical models; see *About* and *Skill & hindcasts*.")
384
+ lead.change(lambda f, i: (map_path(f, i), frame_label(f, i)), [field, lead], [image, caption])
385
+ field.change(lambda f, i: (map_path(f, i), frame_label(f, i)), [field, lead], [image, caption])
 
 
386
  with gr.Tab("📍 Regional outlook"):
387
  region = gr.Dropdown(list(REGIONS), value=list(REGIONS)[0], label="Region")
388
  rplot = gr.Plot(region_figure(list(REGIONS)[0]), show_label=False)
389
+ gr.Markdown("Box means of the native 1° anomaly field (cos-latitude weighted, model ocean cells only), "
390
+ "relative to ERA5 1991–2020, with the last 12 observed months for context.")
391
  rtable = gr.Dataframe(region_table(list(REGIONS)[0]), interactive=False)
392
  region.change(lambda r: (region_figure(r), region_table(r)), region, [rplot, rtable])
393
+ with gr.Tab("📈 Skill & hindcasts"):
394
+ gr.Markdown("#### Held-out test on 2023–2026")
395
+ with gr.Row():
396
+ htrack = gr.Dropdown(list(HELDOUT_TRACKS), value=list(HELDOUT_TRACKS)[0], label="Comparison", scale=3)
397
+ hregion = gr.Dropdown(list(HELDOUT_REGIONS), value="Global", label="Region", scale=2)
398
+ hmetric = gr.Radio(["Spatial ACC (higher is better)", "RMSE (lower is better)"],
399
+ value="Spatial ACC (higher is better)", label="Metric", scale=2)
400
+ hplot = gr.Plot(heldout_figure(list(HELDOUT_TRACKS)[0], "Global", "Spatial ACC"), show_label=False)
401
+ htable = gr.Dataframe(heldout_table(list(HELDOUT_TRACKS)[0], "Global"), interactive=False)
402
+ gr.Markdown(HELDOUT_NOTE)
403
+ for c in (htrack, hregion, hmetric):
404
+ c.change(lambda t, r, m: (heldout_figure(t, r, m), heldout_table(t, r)), [htrack, hregion, hmetric], [hplot, htable])
405
+ gr.Markdown(f"#### Hindcast 2016–2022 · [dataset](https://huggingface.co/datasets/{HINDCAST_REPO})")
406
+ hlead = gr.Slider(1, 12, value=3, step=1, label="Lead (months)")
407
+ hcplot = gr.Plot(hindcast_figure(3), show_label=False)
408
+ hccap = gr.Markdown(hindcast_caption(3))
409
+ hlead.change(lambda k: (hindcast_figure(k), hindcast_caption(k)), hlead, [hcplot, hccap])
410
+ gr.Dataframe(HINDCAST_SKILL.rename(columns={"lead": "Lead", "n": "Months", "corr": "Correlation", "rmse_K": "RMSE (°C)"}),
411
+ interactive=False, label="Niño 3.4 hindcast scores by lead (in-sample)")
412
  with gr.Tab("🌦️ Atmosphere · 6 weeks (experimental)"):
413
  gr.Markdown("Weekly-mean fields of the CAS-Canglong V3.5 atmospheric run published on canglong.earth. "
414
  "**Not drift-corrected** — high-latitude temperatures drift warm with lead time (see *About*).")
 
420
  aimg = gr.Image(atmos_path("2 m temperature", 1), show_label=False, interactive=False, buttons=["download", "fullscreen"])
421
  week.change(lambda v, w: (atmos_path(v, w), week_label(w)), [avar, week], [aimg, wcap])
422
  avar.change(lambda v, w: atmos_path(v, w), [avar, week], aimg)
423
+ with gr.Tab("📦 Model & data"):
424
+ gr.Markdown(f"* **Model** — frozen weights, calibration states, inference code and a CPU example: "
425
+ f"[{MODEL_REPO}](https://huggingface.co/{MODEL_REPO})\n"
426
+ f"* **Hindcast** — observed and lead 1–12 SST for every target month of 2016–2022: "
427
+ f"[{HINDCAST_REPO}](https://huggingface.co/datasets/{HINDCAST_REPO})\n"
428
+ "* **This run** — the files behind every map and chart on this page:")
429
+ gr.File([str(DATA / RUN["sst_file"]), str(DATA / "nino34.csv"), str(DATA / RUN["atmos_file"]),
430
+ str(DATA / "hindcast_nino34.csv"), str(DATA / "round6_heldout_metrics.csv"), str(DATA / "run.json")],
431
  file_count="multiple", label="Downloads", interactive=False)
432
  gr.Code(f"""import xarray as xr
433
+ from huggingface_hub import hf_hub_download, snapshot_download
434
+
435
+ # this run: mean, anomaly (both bases), sigma, 10/90 % quantiles, quintile probabilities, Nino 3.4
436
+ ds = xr.open_dataset(hf_hub_download("LonghaoWang/CAS-Canglong", "data/{RUN['sst_file']}", repo_type="space"))
437
+ print(ds.nino34_9120.to_series().round(2))
438
+
439
+ # the model: verify the frozen package and replay the October 2026 forecast on a CPU
440
+ pkg = snapshot_download("{MODEL_REPO}", allow_patterns=["frozen_model_20261025_v1/*"])
441
+ # cd <pkg>/frozen_model_20261025_v1 && python -B run_frozen.py --verify-only
442
+ # python -B run_frozen.py --prepared-input examples/input_only_202610.npz --output /tmp/forecast_202610.nc
443
+
444
+ # the 2016-2022 hindcast (obs + lead1..lead12 per target month)
445
+ hc = xr.open_dataset(hf_hub_download("{HINDCAST_REPO}",
446
+ "hindcast_2016_2022_v1/sst_hindcast_201601_202212_lead01_12.nc", repo_type="dataset"))""",
447
+ language="python", label="Load in Python")
448
  with gr.Tab("ℹ️ About"):
449
  gr.Markdown(ABOUT)
450
 
assets/maps/atm_msl_w1.png CHANGED

Git LFS Details

  • SHA256: 1b9d7ab6c05a01517dc111ec96f5560e101ba7d00d6160326ae95b6947a70bf5
  • Pointer size: 131 Bytes
  • Size of remote file: 189 kB

Git LFS Details

  • SHA256: 95adb1f4fa5fe607cddd452d005340124937b89cf6cbead2f87f437cc1b8d09f
  • Pointer size: 131 Bytes
  • Size of remote file: 212 kB
assets/maps/atm_msl_w2.png CHANGED

Git LFS Details

  • SHA256: e6a5b246b817f7cfcda1aaea51a5075d76b4510c24f7ad3f9d3c664f12a02778
  • Pointer size: 131 Bytes
  • Size of remote file: 190 kB

Git LFS Details

  • SHA256: efd33befd545eb3fcfc550513d20580cc600c0a6a078a24f9bbba662ef0db74e
  • Pointer size: 131 Bytes
  • Size of remote file: 210 kB
assets/maps/atm_msl_w3.png CHANGED

Git LFS Details

  • SHA256: 3d82c94a97cc5a6571a616c8e811e8a686f2d8940384c5c7032889aa63163ec3
  • Pointer size: 131 Bytes
  • Size of remote file: 189 kB

Git LFS Details

  • SHA256: ebac83607bb5ecd9c6d8d8541546bab88e63f8b73982b9db1cc7357e1851c5e9
  • Pointer size: 131 Bytes
  • Size of remote file: 211 kB
assets/maps/atm_msl_w4.png CHANGED

Git LFS Details

  • SHA256: 8604aed199f2ff082cca8be7e37aef32e5e19e52590646bb48dd33ffdd2f2e0e
  • Pointer size: 131 Bytes
  • Size of remote file: 188 kB

Git LFS Details

  • SHA256: 7c4a34f2048fbbbfbe20741f3bbdf178a2100249c8776fd4b4d8f912cce05087
  • Pointer size: 131 Bytes
  • Size of remote file: 208 kB
assets/maps/atm_msl_w5.png CHANGED

Git LFS Details

  • SHA256: afced3c4a917ea44c91b13fff481d850c0bbaaf01d1a8252b52c59df6360fe84
  • Pointer size: 131 Bytes
  • Size of remote file: 188 kB

Git LFS Details

  • SHA256: 3fe56bda0602fb5daf2508b43f40584ae9e8b43b62c5146d50622229af9154ef
  • Pointer size: 131 Bytes
  • Size of remote file: 209 kB
assets/maps/atm_msl_w6.png CHANGED

Git LFS Details

  • SHA256: 83bb2fae6e5a7156075a995ed42b0c1acea866b43ade155fcca99c8b80532f0b
  • Pointer size: 131 Bytes
  • Size of remote file: 187 kB

Git LFS Details

  • SHA256: af35aee284b08b3e96ab755147bc046d301f95df9844e886a2600c5e8e338e64
  • Pointer size: 131 Bytes
  • Size of remote file: 210 kB
assets/maps/atm_t2m_w1.png CHANGED

Git LFS Details

  • SHA256: 816b0452a86bb4056f2843d328b421b9084aa06c47cd951b5448a22ff4dbfb83
  • Pointer size: 131 Bytes
  • Size of remote file: 181 kB

Git LFS Details

  • SHA256: 62504f9084a5361522c6c62fde33443e608092b2d0265268577dd329d8a20cb7
  • Pointer size: 131 Bytes
  • Size of remote file: 198 kB
assets/maps/atm_t2m_w2.png CHANGED

Git LFS Details

  • SHA256: 7f946e3691b495ff183e51c432534286374e03882901520ada63b83c6a0a88f6
  • Pointer size: 131 Bytes
  • Size of remote file: 180 kB

Git LFS Details

  • SHA256: 154543c7167569e7fd73ed8449bb259f7e04e1906b6c27dc11a438b1199024d9
  • Pointer size: 131 Bytes
  • Size of remote file: 196 kB
assets/maps/atm_t2m_w3.png CHANGED

Git LFS Details

  • SHA256: debf72b2fb9edd8b03852cbae5b7a0f6f8f5c3c3bb8bc1bf165928d4bcbe28ed
  • Pointer size: 131 Bytes
  • Size of remote file: 182 kB

Git LFS Details

  • SHA256: 81bba40464353aedc83f20cacda7fff5e28cc7cb603c70a0709dd421170ba264
  • Pointer size: 131 Bytes
  • Size of remote file: 197 kB
assets/maps/atm_t2m_w4.png CHANGED

Git LFS Details

  • SHA256: 104c6af35985d79d5694c54648621dab239e38b4366cb05108c1fe55ccfdd49d
  • Pointer size: 131 Bytes
  • Size of remote file: 184 kB

Git LFS Details

  • SHA256: b6322351bd4a5969d5b67d62c6598ae1da929025687d1d0305987821eb34fc38
  • Pointer size: 131 Bytes
  • Size of remote file: 196 kB
assets/maps/atm_t2m_w5.png CHANGED

Git LFS Details

  • SHA256: 5e733f1ad08e322671a7af014e3a1b4981f3aae632bd5c4343d14fca5334e3c6
  • Pointer size: 131 Bytes
  • Size of remote file: 184 kB

Git LFS Details

  • SHA256: 20a7b89a2732e0332cc4edaa41e9050b011e803f3e48ff18191750dfff7055e6
  • Pointer size: 131 Bytes
  • Size of remote file: 198 kB
assets/maps/atm_t2m_w6.png CHANGED

Git LFS Details

  • SHA256: 4b1b58d72965ec383f69659a1240ed83b8f751cdc601cd656e7867d4e871e3dc
  • Pointer size: 131 Bytes
  • Size of remote file: 184 kB

Git LFS Details

  • SHA256: 2ed633bbae61ff0319511bcf1538183222c31659010906d38d607d4dc4da355b
  • Pointer size: 131 Bytes
  • Size of remote file: 199 kB
assets/maps/atm_tp_w1.png CHANGED

Git LFS Details

  • SHA256: f9c63cc021aeaf17379aa9f21a326e11e7362d242ec4f4c0b820f661c5780113
  • Pointer size: 131 Bytes
  • Size of remote file: 196 kB

Git LFS Details

  • SHA256: cf73535ccb9bae956796940a7ff936215e16d7842caec03f7609c69fdf11e947
  • Pointer size: 131 Bytes
  • Size of remote file: 214 kB
assets/maps/atm_tp_w2.png CHANGED

Git LFS Details

  • SHA256: 5adca71672db7d8c06685ed52c30a08b6c0a0123e60326cab6ddca6c05191aec
  • Pointer size: 131 Bytes
  • Size of remote file: 195 kB

Git LFS Details

  • SHA256: 41e75065e77f7ae09d8da3086694e253b704ea02496e3f4fcefcf978833668d9
  • Pointer size: 131 Bytes
  • Size of remote file: 212 kB
assets/maps/atm_tp_w3.png CHANGED

Git LFS Details

  • SHA256: 9e7b0504db36ff3a7b172bc73384170d378efa7c71c31e3d7b4d343dc685d1e3
  • Pointer size: 131 Bytes
  • Size of remote file: 195 kB

Git LFS Details

  • SHA256: 60d1abf59be6c57d1288a5f8208cb49f0cdc34af274e167197c32bc7010ed2f6
  • Pointer size: 131 Bytes
  • Size of remote file: 211 kB
assets/maps/atm_tp_w4.png CHANGED

Git LFS Details

  • SHA256: c251125cef78f151f8ef086c47f3336b371dfdb12c626be52205c354df65377f
  • Pointer size: 131 Bytes
  • Size of remote file: 196 kB

Git LFS Details

  • SHA256: 946f7aae5843c9eef89b6273fa8faa6b288b58f7bc09fbdd8fe5fe012dc2dcea
  • Pointer size: 131 Bytes
  • Size of remote file: 209 kB
assets/maps/atm_tp_w5.png CHANGED

Git LFS Details

  • SHA256: 07395bd3eec105f07813d72bd039708412ad97de07c4c8ec2d01fcc08cbe6311
  • Pointer size: 131 Bytes
  • Size of remote file: 196 kB

Git LFS Details

  • SHA256: 8e84a4b4bb5592c0a984dc662cea5eca31f60aad14b29a07c07766faa5700aaf
  • Pointer size: 131 Bytes
  • Size of remote file: 210 kB
assets/maps/atm_tp_w6.png CHANGED

Git LFS Details

  • SHA256: dc27b2309007167fd303ec27c35a1d67176698909bd063b15be525df1e3f7a72
  • Pointer size: 131 Bytes
  • Size of remote file: 194 kB

Git LFS Details

  • SHA256: 1c09293298562b3b0adb428177d371ca9ff38d581dbf219a26056bd4a8bae080
  • Pointer size: 131 Bytes
  • Size of remote file: 211 kB
assets/maps/atm_z500_w1.png CHANGED

Git LFS Details

  • SHA256: 95c472daf723343bf9c752aed0699222bde7930232a45fb337bcb2ac76bda0ef
  • Pointer size: 131 Bytes
  • Size of remote file: 193 kB

Git LFS Details

  • SHA256: 010348fc26fad5e5200fa031a8fb155304a94a93670591c031178284927bab1f
  • Pointer size: 131 Bytes
  • Size of remote file: 214 kB
assets/maps/atm_z500_w2.png CHANGED

Git LFS Details

  • SHA256: 540b4ffd2a4214354106425ff112eb6dc9e784ba761199486de08115a7eed759
  • Pointer size: 131 Bytes
  • Size of remote file: 195 kB

Git LFS Details

  • SHA256: 5a2d81b2176d91ee9152d987cc0a448f8137bbb418b45fa19ad38f002586379b
  • Pointer size: 131 Bytes
  • Size of remote file: 216 kB
assets/maps/atm_z500_w3.png CHANGED

Git LFS Details

  • SHA256: 448df5e3cc1b7dd0404947cadb4c7d1ca4c1239e6d93e1d02634ea62461afc5b
  • Pointer size: 131 Bytes
  • Size of remote file: 198 kB

Git LFS Details

  • SHA256: 335714e219fa7db5fb80351a6e647e18c813df1ec75019be88081b5277f610ba
  • Pointer size: 131 Bytes
  • Size of remote file: 217 kB
assets/maps/atm_z500_w4.png CHANGED

Git LFS Details

  • SHA256: 9a4161b141f77697490bb8e147e9e49e45b263bd36ce8af5ea3e68e398cae598
  • Pointer size: 131 Bytes
  • Size of remote file: 200 kB

Git LFS Details

  • SHA256: f225cdbdb1c6146fd7ca97caef579b271ebbc3807405cf9cbcc46846b27b7e50
  • Pointer size: 131 Bytes
  • Size of remote file: 218 kB
assets/maps/atm_z500_w5.png CHANGED

Git LFS Details

  • SHA256: a439df8ea9a411e3dcf6716c92deab824bcb1488079bf2fccd9339d0892a1a9e
  • Pointer size: 131 Bytes
  • Size of remote file: 201 kB

Git LFS Details

  • SHA256: 3b24e91ec49c7aafbf26e07672731eb440c986afd24d785dd835c065713364d8
  • Pointer size: 131 Bytes
  • Size of remote file: 219 kB
assets/maps/atm_z500_w6.png CHANGED

Git LFS Details

  • SHA256: 81e9e830d2f63e015dd60f5164e273a754c303fc343c9ccd6a8c9b8f48f848e9
  • Pointer size: 131 Bytes
  • Size of remote file: 201 kB

Git LFS Details

  • SHA256: 2a019be8497f6df539faa7c0c5c6a1af87b4b73ca474dff94cc2f87a0554cf74
  • Pointer size: 131 Bytes
  • Size of remote file: 220 kB
assets/maps/{sst_t13.png → pupper_t01.png} RENAMED
File without changes
assets/maps/{sst_t14.png → pupper_t02.png} RENAMED
File without changes
assets/maps/{sst_t15.png → pupper_t03.png} RENAMED
File without changes
assets/maps/{sst_t16.png → pupper_t04.png} RENAMED
File without changes
assets/maps/pupper_t05.png ADDED

Git LFS Details

  • SHA256: 3c66d1d0b49970ad59a4d28616429d0e146f3a743fbab1ebfcf50059494e6b45
  • Pointer size: 131 Bytes
  • Size of remote file: 256 kB
assets/maps/pupper_t06.png ADDED

Git LFS Details

  • SHA256: 0c99cf312c7b29107ee89c488166377b008bd869ec5235af3fe4aeeddabeeb54
  • Pointer size: 131 Bytes
  • Size of remote file: 257 kB
assets/maps/pupper_t07.png ADDED

Git LFS Details

  • SHA256: fd161f799d39747f31f71a6eb3b991f9316b5f5bb0931e9e83dc630fb2b73e7f
  • Pointer size: 131 Bytes
  • Size of remote file: 256 kB
assets/maps/pupper_t08.png ADDED

Git LFS Details

  • SHA256: 6f3f821943fb78e3da8d9be342aae77fd0aa1fc9f8790f9589a4fdaea294abd2
  • Pointer size: 131 Bytes
  • Size of remote file: 256 kB
assets/maps/pupper_t09.png ADDED

Git LFS Details

  • SHA256: ec062183b6487b779a06623243c787aac1d46bcd271cf3738a8aa2d1cfb02d1e
  • Pointer size: 131 Bytes
  • Size of remote file: 255 kB
assets/maps/pupper_t10.png ADDED

Git LFS Details

  • SHA256: c7468ae585585a03a7ad96a43fe600972669015b569b81efff68e4f3bae6a255
  • Pointer size: 131 Bytes
  • Size of remote file: 255 kB
assets/maps/pupper_t11.png ADDED

Git LFS Details

  • SHA256: fb9d776d26a35c2194fdb8bf886fba828fbfebdadc5cba54e8536181da4bbb66
  • Pointer size: 131 Bytes
  • Size of remote file: 255 kB
assets/maps/pupper_t12.png ADDED

Git LFS Details

  • SHA256: ae119c738d103f2659b4ed08f89e1d7bd6455dfa86071dc36a046a704fe3b8b8
  • Pointer size: 131 Bytes
  • Size of remote file: 254 kB
assets/maps/sigma_t01.png ADDED

Git LFS Details

  • SHA256: a84ad156e77b1f95db47c01fd13f62f972e24fad38d6670757723b5eb5558067
  • Pointer size: 131 Bytes
  • Size of remote file: 221 kB
assets/maps/sigma_t02.png ADDED

Git LFS Details

  • SHA256: 9d2903a39c2120031071d61d65bd71412621e970f025f890f74cd1fa237b5399
  • Pointer size: 131 Bytes
  • Size of remote file: 222 kB
assets/maps/sigma_t03.png ADDED

Git LFS Details

  • SHA256: 68ff366295d80488524f46d7522fe9aaf7bbe5b27a36ec1da4009ca05c5cec49
  • Pointer size: 131 Bytes
  • Size of remote file: 222 kB
assets/maps/sigma_t04.png ADDED

Git LFS Details

  • SHA256: 0a8519f545afbccfbc8bdbd48ea24823095d62a313003d1a515ec7a3fa8b74ae
  • Pointer size: 131 Bytes
  • Size of remote file: 221 kB
assets/maps/sigma_t05.png ADDED

Git LFS Details

  • SHA256: 2ae8f6ac18dc8daaa48af42e3871c4ad2dc8770d95e91a0950f0b856ff67f54d
  • Pointer size: 131 Bytes
  • Size of remote file: 222 kB
assets/maps/sigma_t06.png ADDED

Git LFS Details

  • SHA256: 19f7542caddca12dbd7fb6bbb0df1e6e9915e2b72af6183ba1b10b69ddc77409
  • Pointer size: 131 Bytes
  • Size of remote file: 222 kB
assets/maps/sigma_t07.png ADDED

Git LFS Details

  • SHA256: eb07c3a43e9951464a4f67325a8e4c5321ecd74003fad2555098eb8854bfc33f
  • Pointer size: 131 Bytes
  • Size of remote file: 221 kB
assets/maps/sigma_t08.png ADDED

Git LFS Details

  • SHA256: b0396d01adca6001342be72039e98d1a89d1754ab34ada83ed8a5d0a148f994c
  • Pointer size: 131 Bytes
  • Size of remote file: 223 kB
assets/maps/sigma_t09.png ADDED

Git LFS Details

  • SHA256: 53ffb6b050f33d025c76b83876af7a08c51cc1ed4d39eaaadd34c581a7b45a1e
  • Pointer size: 131 Bytes
  • Size of remote file: 221 kB
assets/maps/sigma_t10.png ADDED

Git LFS Details

  • SHA256: 8e0b0afc6cedcac19381d3574bdb6735df764987e7cd23f2c248f59e9f73bd5a
  • Pointer size: 131 Bytes
  • Size of remote file: 220 kB
assets/maps/sigma_t11.png ADDED

Git LFS Details

  • SHA256: 789fffb64bd63f3742794d050162277a59c1da28563e1596a1e5cbdacbe04fef
  • Pointer size: 131 Bytes
  • Size of remote file: 222 kB