euler314 commited on
Commit
2025f05
·
verified ·
1 Parent(s): 87a6e36

Refresh complete model card, pin corrected pressure movie, and sync inference documentation

Browse files
README.md CHANGED
@@ -9,14 +9,28 @@ tags:
9
 
10
  # Trackformer 1.2
11
 
12
- **[Download 1.2: weights + code](https://github.com/yu314-coder/typhoon-predict/releases/download/trackformer-1.2/trackformer_1_2_field_20260929.tar.gz)** · **[Read the illustrated paper](paper/trackformer.pdf)** · **[Watch Fung-wong](https://yu314-coder.github.io/typhoon-predict/trackformer_1_2_fung_wong.mp4)** · **[Hugging Face model](https://huggingface.co/euler314/typhoon-predict)**
13
 
14
- Release status: **1.2 research release**, not operational certification. The package contains the released 1.2 weights, inference code, input contract, completed 270-storm evaluation and illustrated paper. The original 1.1 release remains separate.
 
 
15
 
16
  Trackformer 1.2 is a **research-only Western Pacific tropical-cyclone forecast model**. It evolves a sea-level-pressure (MSLP) field and a moving storm-centred pressure core every six hours through +120 h. A track and central-pressure estimate are extracted from the evolving core, not independently drawn on top of a pressure image. The prior [Trackformer 1.1 release](https://github.com/yu314-coder/typhoon-predict/releases/tag/trackformer-1.1) remains available and unchanged.
17
 
18
  **Not an operational warning system.** Do not use these forecasts for evacuation, aviation, maritime, or other safety-critical decisions. The public 1.2 name identifies one selected development checkpoint; a genuinely untouched storm-level holdout has not yet established generalization.
19
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  ## What changed from 1.1
21
 
22
  | | Trackformer 1.1 | Trackformer 1.2 |
@@ -37,7 +51,7 @@ The two models use different input pipelines. A direct chart is therefore a *for
37
  4. **Coupled outputs.** A local low-pressure association within 300 km identifies the forecast centre. Central pressure is sampled from the predicted core **at that centre**. Maximum wind is an auxiliary scalar, not a resolved wind field or wind radii.
38
  5. **Autoregression.** The transition runs 20 times for +6, +12, …, +120 h. Training combines basin-field, regional/core-field, track, central-pressure and auxiliary-wind losses, plus masked pressure-change and displacement losses. Future truth is a training/evaluation target only, never an inference input.
39
 
40
- The implementation is in [`models/trackformer_1_2_field/`](models/trackformer_1_2_field/); its [`manifest.json`](models/trackformer_1_2_field/manifest.json) records exact grids, normalization, architecture and SHA-256 provenance. Internally the selected checkpoint came from experiment version `1.2.73`, epoch 4. The public model is **Trackformer 1.2**; that internal identifier is retained only for reproducibility.
41
 
42
  ### Architecture at a glance
43
 
@@ -54,10 +68,12 @@ The implementation is in [`models/trackformer_1_2_field/`](models/trackformer_1_
54
 
55
  Grid spacing describes the representation, not independently demonstrated effective resolution. A mean of member centres is not necessarily the minimum of the displayed mean pressure field.
56
 
57
- ## Completed benchmark: 270 storms · 1,473 daily forecasts
58
 
59
  One typhoon on one UTC day is one case. Each issue forecasts **+6 to +120 h**. We average lead errors within each issue, daily scores within each storm, then the **270 storm scores equally**. This prevents long-lived storms dominating the benchmark. All 1,473 cases finished on **29 September 2026, 08:22 UTC**, with every saved forecast SHA-256 verified.
60
 
 
 
61
  | Equal-storm metric | 1.1 | 1.2 · mean of 50 | Preferred |
62
  | --- | ---: | ---: | --- |
63
  | Mean track error, +6 to +120 h | 798.4 km | **471.2 km** | Lower |
@@ -67,7 +83,7 @@ One typhoon on one UTC day is one case. Each issue forecasts **+6 to +120 h**. W
67
  | Geographic path similarity | 0.5345 | **0.6560** | Higher |
68
  | Fréchet distance | 1,658.1 km | **1,045.8 km** | Lower |
69
 
70
- ![Complete equal-storm benchmark: direction, route alignment, shape and position](evaluation/trackformer_1_2_vs_1_1_270_storms_bars.png)
71
 
72
  Mean track error is **41.0% lower** for 1.2 on this cohort. Direction, shape and geographic alignment remain separate measures, not a combined score. A shape score near one does not guarantee overlapping routes at matching times. The 1.2 forecasts are means of **50 distinct causal input perturbations**, not 50 independently trained networks. Forecast pipelines differ, so this is not an architecture ablation.
73
 
@@ -75,7 +91,7 @@ Mean track error is **41.0% lower** for 1.2 on this cohort. Direction, shape and
75
 
76
  **Limits:** the frozen cohort includes 40 recent storms and 230 historical storms from 1980–1999. It excludes this checkpoint's fitting/validation years, but has not been certified untouched across prior experiments. Historical hindcasts use retrospective analyses and a model trained on later years. Complete five-day labels are required, excluding short remaining lifetimes. No operational or no-overfitting claim follows.
77
 
78
- [Final report and all storm scores](evaluation/daily_storm_final.json) · [Protocol](docs/daily_storm_benchmark.md) · [Frozen issues](evaluation/release_data/daily_storm_cohort.json) · [Reproduce chart](release_tools/plot_daily_storm_final.py)
79
 
80
  **Reading route similarity:** direction error compares headings of common moving six-hour steps. Centred shape similarity removes translation and scale but preserves orientation; it does not establish geographic overlap. Path similarity also responds to displacement. Predicted and observed routes should align at the same valid times, so these metrics must be read alongside position error and unshifted route overlays.
81
 
@@ -95,13 +111,13 @@ The full frozen **270-storm / 1,473-day** plan was checked. The original 1.1 int
95
  | JMA pressure time-curve shape error | 0.293 | 0.288 | 40 storms / 134 days |
96
  | Wind six-hour trend-direction mismatch | 56.1% | 60.8% | 40 storms / 134 days |
97
 
98
- ![Wind and intensity magnitude errors and time-curve shape errors, with whole-storm uncertainty bars](evaluation/intensity/intensity_error_bars.png)
99
 
100
- ![Intensity MAE by forecast lead and six-hour trend-direction mismatch](evaluation/intensity/intensity_leads_and_trends.png)
101
 
102
  **Tradeoffs, not an overall win:** 1.2 has slightly lower mean pressure error, but its auxiliary wind is worse on magnitude and curve alignment. USA pressure at +120 h is also worse (**15.55 vs 13.52 hPa**), whereas JMA pressure at +120 h is better (**12.44 vs 15.95 hPa**). Agencies and forecast leads matter. The wind score is a diagnostic of 1.2's auxiliary scalar against a native one-minute reference, **not validation of a resolved or calibrated surface-wind product**; JMA's ten-minute wind is neither converted nor mixed into that score.
103
 
104
- The time-curve shape error is **(1 − centred cosine similarity) / 2**, lower is better, at exact common UTC leads. It removes mean and amplitude, so it must be read with MAE; it does not prove intensity levels align. It requires at least six valid points and non-flat curves in both forecasts and truth. Trend-direction mismatch compares increasing / flat / decreasing changes over **adjacent valid six-hour steps**; no gap is bridged. The [full report](evaluation/intensity/intensity_final.json) also includes six-hour tendency MAE, all per-storm scores, coverage and paired differences. Error bars are 2,000 fixed-seed **whole-storm** bootstrap replicates, not independent resampling of overlapping days. These are development diagnostics, not a newly certified untouched holdout or an architecture ablation; the curve protocol is a versioned post-hoc descriptive addition, not a preregistered hypothesis test.
105
 
106
  ### Radius errors — matched definitions, not a fabricated 1.2 head
107
 
@@ -114,123 +130,131 @@ Native 1.1 RMW and **NE / SE / SW / NW maximum-extent R34/R50/R64** predictions
114
  | R50 quadrants | 36.39 km | N/A — no radius head | 16 storms / 59 days |
115
  | R64 quadrants | 26.78 km | N/A — no radius head | 12 storms / 45 days |
116
 
117
- ![Native radius errors and time-curve shape errors: 1.1 scored against matching USA reports, 1.2 explicitly unavailable](evaluation/intensity/native_radius_error_bars.png)
118
 
119
- **N/A is not zero error or a model win.** The separate pressure-scaled radius candidate below is not inserted into these native model bars: its surface-wind height and averaging period are not validated, so its strict definition guard rejects an official-equivalent skill claim. USA RMW is not universally best-tracked. [Native field definitions](https://www.ncei.noaa.gov/sites/default/files/2025-09/IBTrACS_v04r01_column_documentation.pdf) · [Protocol and reproduction](docs/intensity_benchmark.md) · [Verification receipt](evaluation/intensity/verification.json).
120
 
121
  ### Four-storm wind, pressure and radius timelines
122
 
123
  The plots retain native **USA one-minute** and **JMA ten-minute** winds as separate curves, plus observed pressure, RMW and each radius quadrant. A separate experimental algorithm uses the exact issue-time structure and each of **50 actual model pressure members**, then averages diagnosed member values—not a diagnosis of the mean map. Dashed RMW is **issue-time persistence**, not a forecast of changing eyewall size. Reference gaps remain gaps; the radius candidate is visibly labeled experimental.
124
 
125
- ![Four selected storms: native wind references, auxiliary neural wind and separate pressure-scaled experimental wind](evaluation/storm_structure/four_storm_wind_candidate.png)
126
 
127
  <details>
128
  <summary>Fung-wong · issue 7 November 2025, 00 UTC</summary>
129
 
130
- ![Fung-wong exact-time wind, pressure and quadrant-radius comparisons](evaluation/storm_structure/fung_wong_wind_radius.png)
131
 
132
- [Definitions, member proof and source hashes](evaluation/storm_structure/fung_wong_structure.json). This is a separately saved fresh replay; its pressure output is **not substituted into the older Fung-wong MP4**.
133
 
134
  </details>
135
 
136
  <details>
137
  <summary>Soudelor · issue 5 August 2015, 00 UTC</summary>
138
 
139
- ![Soudelor wind, pressure and radius comparisons](evaluation/storm_structure/soudelor_wind_radius.png)
140
 
141
- [Definitions and member proof](evaluation/storm_structure/soudelor_structure.json).
142
 
143
  </details>
144
 
145
  <details>
146
  <summary>Mangkhut · issue 11 September 2018, 00 UTC</summary>
147
 
148
- ![Mangkhut wind, pressure and radius comparisons](evaluation/storm_structure/mangkhut_wind_radius.png)
149
 
150
- [Definitions and member proof](evaluation/storm_structure/mangkhut_structure.json).
151
 
152
  </details>
153
 
154
  <details>
155
  <summary>Meranti · issue 10 September 2016, 00 UTC</summary>
156
 
157
- ![Meranti wind, pressure and radius comparisons](evaluation/storm_structure/meranti_wind_radius.png)
158
 
159
- [Definitions and member proof](evaluation/storm_structure/meranti_structure.json).
160
 
161
  </details>
162
 
163
- [Experimental algorithm and limitations](release_tools/STORM_STRUCTURE_DIAGNOSTICS.md) · [Audit index](evaluation/storm_structure/index.json) · [History wind/intensity/radius graphs](https://trackformer-weatherlab.rudin-euler-8253.chatgpt.site/history). These selected examples are illustrations, not typical-skill or untouched-test claims. The History tab shows available observations for every browsable storm; forecasts and radius estimates are never invented to fill missing inputs.
164
 
165
  ## Fung-wong pressure forecast — MP4
166
 
167
- <video controls playsinline preload="metadata" width="100%" poster="https://huggingface.co/euler314/typhoon-predict/resolve/main/evaluation/trackformer_1_2_fung_wong_video_poster.png" src="https://huggingface.co/euler314/typhoon-predict/resolve/main/docs/trackformer_1_2_fung_wong.mp4"></video>
168
-
169
- [![Play the Trackformer 1.2 Fung-wong pressure forecast MP4](evaluation/trackformer_1_2_fung_wong_video_poster.png)](https://yu314-coder.github.io/typhoon-predict/trackformer_1_2_fung_wong.mp4)
170
 
171
- **[Play or download the Fung-wong MP4](https://yu314-coder.github.io/typhoon-predict/trackformer_1_2_fung_wong.mp4)** · 23 seconds · +6 to +120 h · **50-member mean**. Click the preview above to play it; inline video playback depends on the Markdown host.
172
 
173
- The forecast starts **7 November 2025 at 00 UTC**. The updated MP4 uses **blue for low pressure and red for high pressure**, with the unnecessary map grid and faint future-path overlays removed. Actual saved model-generated pressure with **4 hPa isobars** is shown alongside the mean forecast route and observed best track, at matching valid times and on the same geographic map. Neither route nor field is shifted or rescaled to improve alignment. The central-pressure timeline compares the forecast to **JMA best-track pressure from IBTrACS TOKYO_PRES**, not a JMA forecast.
174
 
175
  This is the user-selected **Fung-wong route-and-pressure example**, not a claim of typical or untouched-test performance. The curves follow a similar broad path but do not perfectly overlap; timing and position differences remain. Central-pressure MAE is **7.36 hPa** across 20 valid labels, with an imperfect intensification and weakening cycle. For context, mean geographic position error is 130.9 km and +120 h error is 142.3 km; those distances alone do not establish route overlap. These scores use exact observed coordinates and great-circle distances, separately from the saved local-coordinate benchmark scores above.
176
 
177
- Native-detail input history was unavailable; the regional pressure reconstruction uses 0.25° output sampling, not native resolution. **From +66 h the forecast centre is outside the fixed regional patch**, so only the saved coarse basin field is displayed there. The video labels this coverage limit; it does not invent an extended detailed core. Member-mean central pressure still comes from each model member's moving-core readout, not the minimum of the displayed mean map. [Data, source hashes and video provenance](evaluation/release_data/fung_wong_video.json).
178
 
179
  ## More historical pressure forecasts — MP4
180
 
181
  Each film shows a **Western Pacific pressure overview**, a larger **unshifted forecast-versus-observed route close-up**, and a central-pressure comparison with JMA best track. All three were forecast on the Mac GPU using the frozen **Trackformer 1.2** release and **50 distinct seeded input perturbations**. The displayed physical fields, routes and central pressures are separately averaged over all 50 members. Each film contains 20 genuine six-hour forecast states through **+120 h**, with blue lows and red highs; no interpolated forecast states. Soudelor and Meranti retain their original 23-second films. **Mangkhut is now 20 seconds, without the extra frozen ending**, and uses 2 hPa isobars with 4 hPa labels.
182
 
183
- **Mangkhut pressure repair:** its actual moving model cores are now registered geographically for every member before averaging, rather than losing detailed pressure when the storm leaves the old fixed regional patch. The reconstruction uses the frozen model equation—evolving basin plus its tapered moving anomaly—with no scalar-pressure insertion or shift to the observed/forecast route. Original routes, central pressures, inputs and weights are unchanged. [Common-grid physical pressure arrays](evaluation/release_data/pressure_core/mangkhut/common-pressure.npz) · [Core and replay verification](evaluation/release_data/pressure_core/mangkhut/verification.json) · [Playback audit](evaluation/release_data/mangkhut_playback_verification.json).
184
 
185
  ### Soudelor (2015) — issue 5 August 2015, 00 UTC
186
 
187
- <video controls playsinline preload="metadata" width="100%" poster="https://huggingface.co/euler314/typhoon-predict/resolve/main/evaluation/trackformer_1_2_soudelor_video_poster.png" src="https://huggingface.co/euler314/typhoon-predict/resolve/main/docs/trackformer_1_2_soudelor.mp4"></video>
188
 
189
- [Play / download Soudelor MP4](https://huggingface.co/euler314/typhoon-predict/resolve/main/docs/trackformer_1_2_soudelor.mp4) · [Provenance](evaluation/release_data/soudelor_video.json)
190
 
191
  ### Mangkhut (2018) — issue 11 September 2018, 00 UTC
192
 
193
- <video controls playsinline preload="metadata" width="100%" poster="https://huggingface.co/euler314/typhoon-predict/resolve/main/evaluation/trackformer_1_2_mangkhut_video_poster.png" src="https://huggingface.co/euler314/typhoon-predict/resolve/main/docs/trackformer_1_2_mangkhut.mp4"></video>
194
 
195
- [Play / download Mangkhut MP4](https://huggingface.co/euler314/typhoon-predict/resolve/main/docs/trackformer_1_2_mangkhut.mp4) · [Provenance](evaluation/release_data/mangkhut_video.json)
196
 
197
  ### Meranti (2016) — issue 10 September 2016, 00 UTC
198
 
199
- <video controls playsinline preload="metadata" width="100%" poster="https://huggingface.co/euler314/typhoon-predict/resolve/main/evaluation/trackformer_1_2_meranti_video_poster.png" src="https://huggingface.co/euler314/typhoon-predict/resolve/main/docs/trackformer_1_2_meranti.mp4"></video>
200
 
201
- [Play / download Meranti MP4](https://huggingface.co/euler314/typhoon-predict/resolve/main/docs/trackformer_1_2_meranti.mp4) · [Provenance](evaluation/release_data/meranti_video.json)
202
 
203
  **These are calendar-selected historical development examples, not untouched tests or representative skill claims.** Their dates were fixed before inference, without choosing the lowest-error starts, but may overlap the checkpoint's fitting/validation years. Inputs are nine exact six-hour retrospective NCEP analyses through issue time, current intensity and past motion. Future observations are comparison labels only. Native-detail input history is not supplied and remains masked. Soudelor and Meranti retain a fixed regional reconstruction; beyond its coverage they show the 2.5° basin field only. Mangkhut uses the actual moving-core reconstruction: 0.1° close-up and 0.25° overview are interpolation/sampling, not additional native information; the core's information spacing is 20 km. Meranti's issue-time wind was missing and remains masked, not filled from future data. No validated wind-radii forecast is implied.
204
 
205
- [50-member mean and MP4 audit](evaluation/release_data/historical_video50_verification.json) · [Mac GPU forecast preparation](release_tools/forecast_historical_video50_mac.py) · [Video renderer](release_tools/build_fung_wong_video.py)
206
 
207
  ### Public forecast and observation API
208
 
209
  [API guide](https://trackformer-weatherlab.rudin-euler-8253.chatgpt.site/history-api) · [Machine-readable resources](https://trackformer-weatherlab.rudin-euler-8253.chatgpt.site/api/history/v1/resources) · [All published data assets](https://trackformer-weatherlab.rudin-euler-8253.chatgpt.site/api/data/v1/catalog). Cross-origin, read-only JSON exposes routes, actual basin/core pressure fields, separate observed winds/radii and provenance for external pages. The original 32,230-issue Western Pacific archive and the still-progressing moving-core recovery have separate verification endpoints; missing core/wind/radius data remain unavailable, never zero. One-member historical issues are not relabelled 50-member ensembles.
210
 
 
 
 
 
 
 
 
 
 
 
211
  ### Illustrated technical paper
212
 
213
- The **[Trackformer 1.2 technical paper (PDF)](paper/trackformer.pdf)** explains the input tensors, multiscale attention, recurrent basin transport, moving pressure core, centre association, objectives and 50-member means. It includes a **model-structure diagram**, **1.2-versus-1.1 benchmark bars and lead-error curves**, and the selected **Fung-wong route, central-pressure timeline and model-generated isobar map**. Development-data limitations, short-lead regressions and pressure failures are reported alongside improvements. [Editable, self-contained LaTeX source](paper/trackformer.tex) · [Figure reproduction utility](release_tools/build_paper_figures.py). Historical material is recoverable from Git history and the [1.1 release](https://github.com/yu314-coder/typhoon-predict/releases/tag/trackformer-1.1).
214
 
215
  ## Recent pressure forecast with isobars
216
 
217
  The archived **Surigae forecast issued 27 September 2026 at 12:00 UTC** shows the model's regional pressure field at +6, +24 and +36 h. Thin lines are isobars every **4 hPa**, with selected labels every 12 hPa; magenta shows the forecast track and centre. Coastlines provide geographic context. These panels use the actual saved model values.
218
 
219
- ![Surigae model-generated pressure forecast with labelled isobars](evaluation/trackformer_1_2_surigae_isobars.png)
220
 
221
- This recent example is **one deterministic forecast**, separate from the benchmark's mean of 50. Its input uses nine GFS analyses ending at 06 UTC, six hours before issue time; issue-time JMA central pressure was provided, while wind, prior motion and native regional history were unavailable. This transfer from the training data contract is experimental. The fixed regional map ends at 144°E; the selected leads keep the forecast centre inside it. The [pressure-field provenance](evaluation/release_data/surigae_provenance.json) records the input limitations and checkpoint hash.
222
 
223
  ## Get the model and run it
224
 
225
  - [GitHub release and source](https://github.com/yu314-coder/typhoon-predict/releases/tag/trackformer-1.2)
226
  - [Hugging Face weights and model card](https://huggingface.co/euler314/typhoon-predict)
227
- - [Input schema and inference instructions](models/trackformer_1_2_field/README.md)
228
 
229
  The inference-only `weights.pt` is hosted in the release/Hugging Face model folder, **not committed to GitHub source**. The manifest records its SHA-256 and the original training-checkpoint SHA-256. The three source modules are copied unchanged from the verified training implementation. Use **`models/trackformer_1_2_field/predict.py`** as the inference entry point. The withdrawn route/scalar candidate and its incompatible example have been removed from the current source tree.
230
 
231
  ### Run one forecast
232
 
233
- Download and extract the complete package above, or obtain the source and place the [Hugging Face weights](https://huggingface.co/euler314/typhoon-predict/resolve/main/models/trackformer_1_2_field/weights.pt) at `models/trackformer_1_2_field/weights.pt`. In an environment with PyTorch and NumPy, prepare a normalized **causal issue packet** using the [input schema](models/trackformer_1_2_field/README.md), then run from the package root:
234
 
235
  ```bash
236
  python models/trackformer_1_2_field/predict.py causal_issue_packet.npz forecast.npz --device mps
@@ -246,10 +270,12 @@ Only issue-time and earlier analyses are permitted. The prediction wrapper rejec
246
 
247
  | Folder | Contents |
248
  | --- | --- |
249
- | [`models/trackformer_1_2_field/`](models/trackformer_1_2_field/) | Current 1.2 model, inference wrapper and input/provenance contract |
250
- | [`paper/`](paper/) | Illustrated architecture and development-evaluation paper: editable source and PDF |
251
- | [`evaluation/`](evaluation/) | Benchmark charts, selected forecast previews, metrics and saved plot data |
252
- | [`docs/`](docs/) | Evaluation protocols and published HTML/MP4 showcases |
253
- | [`release_tools/`](release_tools/) | Reproduction scripts and weight-export utility |
 
 
254
 
255
- The older 1.1 implementation, obsolete Colab notebook, withdrawn candidate, and superseded comparison assets are no longer mixed into the current source tree. Their history and the existing release tags remain intact; the current 1.1-versus-1.2 comparison data are retained. See the [cleanup record](docs/repository_layout.md).
 
9
 
10
  # Trackformer 1.2
11
 
12
+ **[Download 1.2: weights + code](https://github.com/yu314-coder/typhoon-predict/releases/download/trackformer-1.2/trackformer_1_2_field_20260929.tar.gz)** · **[Watch corrected Mangkhut · 20 s](https://huggingface.co/euler314/typhoon-predict/resolve/87a6e366b42bb4cc0edc95d2c50c55fca21a2c93/docs/trackformer_1_2_mangkhut.mp4)** · **[Live History](https://trackformer-weatherlab.rudin-euler-8253.chatgpt.site/history)** · **[Public data API](https://trackformer-weatherlab.rudin-euler-8253.chatgpt.site/history-api)** · **[Read the illustrated paper](https://huggingface.co/euler314/typhoon-predict/blob/main/paper/trackformer.pdf)**
13
 
14
+ **README revision: 1 October 2026 (Taipei).** The corrected Mangkhut preview and film below use revision-pinned media URLs. This is a documentation and pressure-display correction, **not new weights or improved forecast scores**. GitHub and the [Hugging Face model card](https://huggingface.co/euler314/typhoon-predict) share this documentation source; Hugging Face additionally provides inline video players.
15
+
16
+ Release status: **1.2 research release**, not operational certification. The downloadable package remains the 29 September release snapshot of weights, inference code, input contract, daily-issue evaluation and paper. The updated cards, corrected film and live API are linked separately; refreshing this README does not replace that package. The original 1.1 release remains separate.
17
 
18
  Trackformer 1.2 is a **research-only Western Pacific tropical-cyclone forecast model**. It evolves a sea-level-pressure (MSLP) field and a moving storm-centred pressure core every six hours through +120 h. A track and central-pressure estimate are extracted from the evolving core, not independently drawn on top of a pressure image. The prior [Trackformer 1.1 release](https://github.com/yu314-coder/typhoon-predict/releases/tag/trackformer-1.1) remains available and unchanged.
19
 
20
  **Not an operational warning system.** Do not use these forecasts for evacuation, aviation, maritime, or other safety-critical decisions. The public 1.2 name identifies one selected development checkpoint; a genuinely untouched storm-level holdout has not yet established generalization.
21
 
22
+ ## Corrected pressure forecast — Mangkhut (2018)
23
+
24
+ <video controls preload="metadata" width="100%" aria-label="Corrected Mangkhut moving-core pressure forecast — 1 October 2026 revision" poster="https://huggingface.co/euler314/typhoon-predict/resolve/87a6e366b42bb4cc0edc95d2c50c55fca21a2c93/evaluation/trackformer_1_2_mangkhut_video_poster.png" src="https://huggingface.co/euler314/typhoon-predict/resolve/87a6e366b42bb4cc0edc95d2c50c55fca21a2c93/docs/trackformer_1_2_mangkhut.mp4"></video>
25
+
26
+ **[Play the corrected Mangkhut MP4](https://huggingface.co/euler314/typhoon-predict/resolve/87a6e366b42bb4cc0edc95d2c50c55fca21a2c93/docs/trackformer_1_2_mangkhut.mp4)** · issue **11 September 2018, 00 UTC** · **50-member mean** · **20 seconds** · +6 to +120 h. Twenty distinct forecast states are shown for one second each; **no extra frozen ending** is appended. GitHub shows a clickable preview, while Hugging Face supports inline playback.
27
+
28
+ The corrected map uses the **actual evolving model pressure core**, registered on geographic coordinates separately for each of the 50 members before averaging. The Western Pacific overview and close-up use the same physical mean: basin pressure plus the model's tapered moving anomaly, never a vortex inserted from a pressure number. Blue is low pressure; red is high pressure. Isobars are **2 hPa**, with major labels every **4 hPa**. Routes, member central pressures, causal input histories and released weights are unchanged. Sampling at 0.1°/0.25° is interpolation, not new native resolution; the core information spacing is approximately 20 km.
29
+
30
+ [Pressure arrays and provenance](https://huggingface.co/euler314/typhoon-predict/blob/main/evaluation/release_data/pressure_core/mangkhut/common-pressure.npz) · [Core-field replay verification](https://huggingface.co/euler314/typhoon-predict/blob/main/evaluation/release_data/pressure_core/mangkhut/verification.json) · [20-state playback audit](https://huggingface.co/euler314/typhoon-predict/blob/main/evaluation/release_data/mangkhut_playback_verification.json). Video SHA-256: `220e09eea71af88c9622131f4248be9b9914adae16785c5caee349b1e10abc20`.
31
+
32
+ **Scope of this repair:** Mangkhut is the corrected 50-member film. Fung-wong, Soudelor and Meranti below retain their older fixed-patch films and 23-second playback; they are **not** presented as regenerated moving-core videos. The separate live History core recovery preserves existing historical routes and pressure readouts; those automatic historical issues are **one member**, not this 50-member example. [Original archive audit](https://trackformer-weatherlab.rudin-euler-8253.chatgpt.site/api/history/v1/verification) · [Continuing core-field recovery](https://trackformer-weatherlab.rudin-euler-8253.chatgpt.site/api/history/core-status).
33
+
34
  ## What changed from 1.1
35
 
36
  | | Trackformer 1.1 | Trackformer 1.2 |
 
51
  4. **Coupled outputs.** A local low-pressure association within 300 km identifies the forecast centre. Central pressure is sampled from the predicted core **at that centre**. Maximum wind is an auxiliary scalar, not a resolved wind field or wind radii.
52
  5. **Autoregression.** The transition runs 20 times for +6, +12, …, +120 h. Training combines basin-field, regional/core-field, track, central-pressure and auxiliary-wind losses, plus masked pressure-change and displacement losses. Future truth is a training/evaluation target only, never an inference input.
53
 
54
+ The implementation is in [`models/trackformer_1_2_field/`](https://huggingface.co/euler314/typhoon-predict/tree/main/models/trackformer_1_2_field/); its [`manifest.json`](https://huggingface.co/euler314/typhoon-predict/blob/main/models/trackformer_1_2_field/manifest.json) records exact grids, normalization, architecture and SHA-256 provenance. Internally the selected checkpoint came from experiment version `1.2.73`, epoch 4. The public model is **Trackformer 1.2**; that internal identifier is retained only for reproducibility.
55
 
56
  ### Architecture at a glance
57
 
 
68
 
69
  Grid spacing describes the representation, not independently demonstrated effective resolution. A mean of member centres is not necessarily the minimum of the displayed mean pressure field.
70
 
71
+ ## Daily-issue benchmark: 1,473 forecasts · 270 storm scores
72
 
73
  One typhoon on one UTC day is one case. Each issue forecasts **+6 to +120 h**. We average lead errors within each issue, daily scores within each storm, then the **270 storm scores equally**. This prevents long-lived storms dominating the benchmark. All 1,473 cases finished on **29 September 2026, 08:22 UTC**, with every saved forecast SHA-256 verified.
74
 
75
+ This is the completed **1,473-daily-issue** evaluation, not the superseded one-start-per-storm 270-case benchmark. The October pressure-display repair does not change these frozen predictions or scores.
76
+
77
  | Equal-storm metric | 1.1 | 1.2 · mean of 50 | Preferred |
78
  | --- | ---: | ---: | --- |
79
  | Mean track error, +6 to +120 h | 798.4 km | **471.2 km** | Lower |
 
83
  | Geographic path similarity | 0.5345 | **0.6560** | Higher |
84
  | Fréchet distance | 1,658.1 km | **1,045.8 km** | Lower |
85
 
86
+ ![Complete equal-storm benchmark: direction, route alignment, shape and position](https://huggingface.co/euler314/typhoon-predict/resolve/87a6e366b42bb4cc0edc95d2c50c55fca21a2c93/evaluation/trackformer_1_2_vs_1_1_270_storms_bars.png)
87
 
88
  Mean track error is **41.0% lower** for 1.2 on this cohort. Direction, shape and geographic alignment remain separate measures, not a combined score. A shape score near one does not guarantee overlapping routes at matching times. The 1.2 forecasts are means of **50 distinct causal input perturbations**, not 50 independently trained networks. Forecast pipelines differ, so this is not an architecture ablation.
89
 
 
91
 
92
  **Limits:** the frozen cohort includes 40 recent storms and 230 historical storms from 1980–1999. It excludes this checkpoint's fitting/validation years, but has not been certified untouched across prior experiments. Historical hindcasts use retrospective analyses and a model trained on later years. Complete five-day labels are required, excluding short remaining lifetimes. No operational or no-overfitting claim follows.
93
 
94
+ [Final report and all storm scores](https://huggingface.co/euler314/typhoon-predict/blob/main/evaluation/daily_storm_final.json) · [Protocol](https://huggingface.co/euler314/typhoon-predict/blob/main/docs/daily_storm_benchmark.md) · [Frozen issues](https://huggingface.co/euler314/typhoon-predict/blob/main/evaluation/release_data/daily_storm_cohort.json) · [Reproduce chart](https://huggingface.co/euler314/typhoon-predict/blob/main/release_tools/plot_daily_storm_final.py)
95
 
96
  **Reading route similarity:** direction error compares headings of common moving six-hour steps. Centred shape similarity removes translation and scale but preserves orientation; it does not establish geographic overlap. Path similarity also responds to displacement. Predicted and observed routes should align at the same valid times, so these metrics must be read alongside position error and unshifted route overlays.
97
 
 
111
  | JMA pressure time-curve shape error | 0.293 | 0.288 | 40 storms / 134 days |
112
  | Wind six-hour trend-direction mismatch | 56.1% | 60.8% | 40 storms / 134 days |
113
 
114
+ ![Wind and intensity magnitude errors and time-curve shape errors, with whole-storm uncertainty bars](https://huggingface.co/euler314/typhoon-predict/resolve/87a6e366b42bb4cc0edc95d2c50c55fca21a2c93/evaluation/intensity/intensity_error_bars.png)
115
 
116
+ ![Intensity MAE by forecast lead and six-hour trend-direction mismatch](https://huggingface.co/euler314/typhoon-predict/resolve/87a6e366b42bb4cc0edc95d2c50c55fca21a2c93/evaluation/intensity/intensity_leads_and_trends.png)
117
 
118
  **Tradeoffs, not an overall win:** 1.2 has slightly lower mean pressure error, but its auxiliary wind is worse on magnitude and curve alignment. USA pressure at +120 h is also worse (**15.55 vs 13.52 hPa**), whereas JMA pressure at +120 h is better (**12.44 vs 15.95 hPa**). Agencies and forecast leads matter. The wind score is a diagnostic of 1.2's auxiliary scalar against a native one-minute reference, **not validation of a resolved or calibrated surface-wind product**; JMA's ten-minute wind is neither converted nor mixed into that score.
119
 
120
+ The time-curve shape error is **(1 − centred cosine similarity) / 2**, lower is better, at exact common UTC leads. It removes mean and amplitude, so it must be read with MAE; it does not prove intensity levels align. It requires at least six valid points and non-flat curves in both forecasts and truth. Trend-direction mismatch compares increasing / flat / decreasing changes over **adjacent valid six-hour steps**; no gap is bridged. The [full report](https://huggingface.co/euler314/typhoon-predict/blob/main/evaluation/intensity/intensity_final.json) also includes six-hour tendency MAE, all per-storm scores, coverage and paired differences. Error bars are 2,000 fixed-seed **whole-storm** bootstrap replicates, not independent resampling of overlapping days. These are development diagnostics, not a newly certified untouched holdout or an architecture ablation; the curve protocol is a versioned post-hoc descriptive addition, not a preregistered hypothesis test.
121
 
122
  ### Radius errors — matched definitions, not a fabricated 1.2 head
123
 
 
130
  | R50 quadrants | 36.39 km | N/A — no radius head | 16 storms / 59 days |
131
  | R64 quadrants | 26.78 km | N/A — no radius head | 12 storms / 45 days |
132
 
133
+ ![Native radius errors and time-curve shape errors: 1.1 scored against matching USA reports, 1.2 explicitly unavailable](https://huggingface.co/euler314/typhoon-predict/resolve/87a6e366b42bb4cc0edc95d2c50c55fca21a2c93/evaluation/intensity/native_radius_error_bars.png)
134
 
135
+ **N/A is not zero error or a model win.** The separate pressure-scaled radius candidate below is not inserted into these native model bars: its surface-wind height and averaging period are not validated, so its strict definition guard rejects an official-equivalent skill claim. USA RMW is not universally best-tracked. [Native field definitions](https://www.ncei.noaa.gov/sites/default/files/2025-09/IBTrACS_v04r01_column_documentation.pdf) · [Protocol and reproduction](https://huggingface.co/euler314/typhoon-predict/blob/main/docs/intensity_benchmark.md) · [Verification receipt](https://huggingface.co/euler314/typhoon-predict/blob/main/evaluation/intensity/verification.json).
136
 
137
  ### Four-storm wind, pressure and radius timelines
138
 
139
  The plots retain native **USA one-minute** and **JMA ten-minute** winds as separate curves, plus observed pressure, RMW and each radius quadrant. A separate experimental algorithm uses the exact issue-time structure and each of **50 actual model pressure members**, then averages diagnosed member values—not a diagnosis of the mean map. Dashed RMW is **issue-time persistence**, not a forecast of changing eyewall size. Reference gaps remain gaps; the radius candidate is visibly labeled experimental.
140
 
141
+ ![Four selected storms: native wind references, auxiliary neural wind and separate pressure-scaled experimental wind](https://huggingface.co/euler314/typhoon-predict/resolve/87a6e366b42bb4cc0edc95d2c50c55fca21a2c93/evaluation/storm_structure/four_storm_wind_candidate.png)
142
 
143
  <details>
144
  <summary>Fung-wong · issue 7 November 2025, 00 UTC</summary>
145
 
146
+ ![Fung-wong exact-time wind, pressure and quadrant-radius comparisons](https://huggingface.co/euler314/typhoon-predict/resolve/87a6e366b42bb4cc0edc95d2c50c55fca21a2c93/evaluation/storm_structure/fung_wong_wind_radius.png)
147
 
148
+ [Definitions, member proof and source hashes](https://huggingface.co/euler314/typhoon-predict/blob/main/evaluation/storm_structure/fung_wong_structure.json). This is a separately saved fresh replay; its pressure output is **not substituted into the older Fung-wong MP4**.
149
 
150
  </details>
151
 
152
  <details>
153
  <summary>Soudelor · issue 5 August 2015, 00 UTC</summary>
154
 
155
+ ![Soudelor wind, pressure and radius comparisons](https://huggingface.co/euler314/typhoon-predict/resolve/87a6e366b42bb4cc0edc95d2c50c55fca21a2c93/evaluation/storm_structure/soudelor_wind_radius.png)
156
 
157
+ [Definitions and member proof](https://huggingface.co/euler314/typhoon-predict/blob/main/evaluation/storm_structure/soudelor_structure.json).
158
 
159
  </details>
160
 
161
  <details>
162
  <summary>Mangkhut · issue 11 September 2018, 00 UTC</summary>
163
 
164
+ ![Mangkhut wind, pressure and radius comparisons](https://huggingface.co/euler314/typhoon-predict/resolve/87a6e366b42bb4cc0edc95d2c50c55fca21a2c93/evaluation/storm_structure/mangkhut_wind_radius.png)
165
 
166
+ [Definitions and member proof](https://huggingface.co/euler314/typhoon-predict/blob/main/evaluation/storm_structure/mangkhut_structure.json).
167
 
168
  </details>
169
 
170
  <details>
171
  <summary>Meranti · issue 10 September 2016, 00 UTC</summary>
172
 
173
+ ![Meranti wind, pressure and radius comparisons](https://huggingface.co/euler314/typhoon-predict/resolve/87a6e366b42bb4cc0edc95d2c50c55fca21a2c93/evaluation/storm_structure/meranti_wind_radius.png)
174
 
175
+ [Definitions and member proof](https://huggingface.co/euler314/typhoon-predict/blob/main/evaluation/storm_structure/meranti_structure.json).
176
 
177
  </details>
178
 
179
+ [Experimental algorithm and limitations](https://huggingface.co/euler314/typhoon-predict/blob/main/release_tools/STORM_STRUCTURE_DIAGNOSTICS.md) · [Audit index](https://huggingface.co/euler314/typhoon-predict/blob/main/evaluation/storm_structure/index.json) · [History wind/intensity/radius graphs](https://trackformer-weatherlab.rudin-euler-8253.chatgpt.site/history). These selected examples are illustrations, not typical-skill or untouched-test claims. The History tab shows available observations for every browsable storm; forecasts and radius estimates are never invented to fill missing inputs.
180
 
181
  ## Fung-wong pressure forecast — MP4
182
 
183
+ <video controls preload="metadata" width="100%" aria-label="Play the Trackformer 1.2 Fung-wong pressure forecast MP4" poster="https://huggingface.co/euler314/typhoon-predict/resolve/87a6e366b42bb4cc0edc95d2c50c55fca21a2c93/evaluation/trackformer_1_2_fung_wong_video_poster.png" src="https://huggingface.co/euler314/typhoon-predict/resolve/87a6e366b42bb4cc0edc95d2c50c55fca21a2c93/docs/trackformer_1_2_fung_wong.mp4"></video>
 
 
184
 
185
+ **Original fixed-patch film — not regenerated in the October moving-core repair.** **[Play or download the Fung-wong MP4](https://huggingface.co/euler314/typhoon-predict/resolve/87a6e366b42bb4cc0edc95d2c50c55fca21a2c93/docs/trackformer_1_2_fung_wong.mp4)** · 23 seconds · +6 to +120 h · **50-member mean**. Click the preview above to play it; inline video playback depends on the Markdown host. Its older 23-second playback retains an additional held ending; use the corrected Mangkhut film above for the new 20-state playback.
186
 
187
+ The forecast starts **7 November 2025 at 00 UTC**. This earlier MP4 uses **blue for low pressure and red for high pressure**, with the unnecessary map grid and faint future-path overlays removed. Actual saved model-generated pressure with **4 hPa isobars** is shown alongside the mean forecast route and observed best track, at matching valid times and on the same geographic map. Neither route nor field is shifted or rescaled to improve alignment. The central-pressure timeline compares the forecast to **JMA best-track pressure from IBTrACS TOKYO_PRES**, not a JMA forecast.
188
 
189
  This is the user-selected **Fung-wong route-and-pressure example**, not a claim of typical or untouched-test performance. The curves follow a similar broad path but do not perfectly overlap; timing and position differences remain. Central-pressure MAE is **7.36 hPa** across 20 valid labels, with an imperfect intensification and weakening cycle. For context, mean geographic position error is 130.9 km and +120 h error is 142.3 km; those distances alone do not establish route overlap. These scores use exact observed coordinates and great-circle distances, separately from the saved local-coordinate benchmark scores above.
190
 
191
+ Native-detail input history was unavailable; the regional pressure reconstruction uses 0.25° output sampling, not native resolution. **From +66 h the forecast centre is outside the fixed regional patch**, so only the saved coarse basin field is displayed there. The video labels this coverage limit; it does not invent an extended detailed core. Member-mean central pressure still comes from each model member's moving-core readout, not the minimum of the displayed mean map. [Data, source hashes and video provenance](https://huggingface.co/euler314/typhoon-predict/blob/main/evaluation/release_data/fung_wong_video.json).
192
 
193
  ## More historical pressure forecasts — MP4
194
 
195
  Each film shows a **Western Pacific pressure overview**, a larger **unshifted forecast-versus-observed route close-up**, and a central-pressure comparison with JMA best track. All three were forecast on the Mac GPU using the frozen **Trackformer 1.2** release and **50 distinct seeded input perturbations**. The displayed physical fields, routes and central pressures are separately averaged over all 50 members. Each film contains 20 genuine six-hour forecast states through **+120 h**, with blue lows and red highs; no interpolated forecast states. Soudelor and Meranti retain their original 23-second films. **Mangkhut is now 20 seconds, without the extra frozen ending**, and uses 2 hPa isobars with 4 hPa labels.
196
 
197
+ **Mangkhut pressure repair:** its actual moving model cores are now registered geographically for every member before averaging, rather than losing detailed pressure when the storm leaves the old fixed regional patch. The reconstruction uses the frozen model equation—evolving basin plus its tapered moving anomaly—with no scalar-pressure insertion or shift to the observed/forecast route. Original routes, central pressures, inputs and weights are unchanged. [Common-grid physical pressure arrays](https://huggingface.co/euler314/typhoon-predict/blob/main/evaluation/release_data/pressure_core/mangkhut/common-pressure.npz) · [Core and replay verification](https://huggingface.co/euler314/typhoon-predict/blob/main/evaluation/release_data/pressure_core/mangkhut/verification.json) · [Playback audit](https://huggingface.co/euler314/typhoon-predict/blob/main/evaluation/release_data/mangkhut_playback_verification.json).
198
 
199
  ### Soudelor (2015) — issue 5 August 2015, 00 UTC
200
 
201
+ <video controls preload="metadata" width="100%" aria-label="Soudelor (2015) original fixed-patch film" poster="https://huggingface.co/euler314/typhoon-predict/resolve/87a6e366b42bb4cc0edc95d2c50c55fca21a2c93/evaluation/trackformer_1_2_soudelor_video_poster.png" src="https://huggingface.co/euler314/typhoon-predict/resolve/87a6e366b42bb4cc0edc95d2c50c55fca21a2c93/docs/trackformer_1_2_soudelor.mp4"></video>
202
 
203
+ [Play / download Soudelor MP4](https://huggingface.co/euler314/typhoon-predict/resolve/87a6e366b42bb4cc0edc95d2c50c55fca21a2c93/docs/trackformer_1_2_soudelor.mp4) · [Provenance](https://huggingface.co/euler314/typhoon-predict/blob/main/evaluation/release_data/soudelor_video.json)
204
 
205
  ### Mangkhut (2018) — issue 11 September 2018, 00 UTC
206
 
207
+ The corrected 20-second player appears near the top of this card; its moving-core repair and audit are described there.
208
 
209
+ [Play / download Mangkhut MP4](https://huggingface.co/euler314/typhoon-predict/resolve/87a6e366b42bb4cc0edc95d2c50c55fca21a2c93/docs/trackformer_1_2_mangkhut.mp4) · [Provenance](https://huggingface.co/euler314/typhoon-predict/blob/main/evaluation/release_data/mangkhut_video.json)
210
 
211
  ### Meranti (2016) — issue 10 September 2016, 00 UTC
212
 
213
+ <video controls preload="metadata" width="100%" aria-label="Meranti (2016) original fixed-patch film" poster="https://huggingface.co/euler314/typhoon-predict/resolve/87a6e366b42bb4cc0edc95d2c50c55fca21a2c93/evaluation/trackformer_1_2_meranti_video_poster.png" src="https://huggingface.co/euler314/typhoon-predict/resolve/87a6e366b42bb4cc0edc95d2c50c55fca21a2c93/docs/trackformer_1_2_meranti.mp4"></video>
214
 
215
+ [Play / download Meranti MP4](https://huggingface.co/euler314/typhoon-predict/resolve/87a6e366b42bb4cc0edc95d2c50c55fca21a2c93/docs/trackformer_1_2_meranti.mp4) · [Provenance](https://huggingface.co/euler314/typhoon-predict/blob/main/evaluation/release_data/meranti_video.json)
216
 
217
  **These are calendar-selected historical development examples, not untouched tests or representative skill claims.** Their dates were fixed before inference, without choosing the lowest-error starts, but may overlap the checkpoint's fitting/validation years. Inputs are nine exact six-hour retrospective NCEP analyses through issue time, current intensity and past motion. Future observations are comparison labels only. Native-detail input history is not supplied and remains masked. Soudelor and Meranti retain a fixed regional reconstruction; beyond its coverage they show the 2.5° basin field only. Mangkhut uses the actual moving-core reconstruction: 0.1° close-up and 0.25° overview are interpolation/sampling, not additional native information; the core's information spacing is 20 km. Meranti's issue-time wind was missing and remains masked, not filled from future data. No validated wind-radii forecast is implied.
218
 
219
+ [50-member mean and MP4 audit](https://huggingface.co/euler314/typhoon-predict/blob/main/evaluation/release_data/historical_video50_verification.json) · [Mac GPU forecast preparation](https://huggingface.co/euler314/typhoon-predict/blob/main/release_tools/forecast_historical_video50_mac.py) · [Video renderer](https://huggingface.co/euler314/typhoon-predict/blob/main/release_tools/build_fung_wong_video.py)
220
 
221
  ### Public forecast and observation API
222
 
223
  [API guide](https://trackformer-weatherlab.rudin-euler-8253.chatgpt.site/history-api) · [Machine-readable resources](https://trackformer-weatherlab.rudin-euler-8253.chatgpt.site/api/history/v1/resources) · [All published data assets](https://trackformer-weatherlab.rudin-euler-8253.chatgpt.site/api/data/v1/catalog). Cross-origin, read-only JSON exposes routes, actual basin/core pressure fields, separate observed winds/radii and provenance for external pages. The original 32,230-issue Western Pacific archive and the still-progressing moving-core recovery have separate verification endpoints; missing core/wind/radius data remain unavailable, never zero. One-member historical issues are not relabelled 50-member ensembles.
224
 
225
+ API contract **1.1** supports cross-origin GET/HEAD/OPTIONS without a token. Use `/api/history/v1/catalog` to discover exact issues, `/api/history/v1/issues/{id}` for forecast plus decoded physical fields, and `/api/history/v1/observations/storms/{storm_id}` for separately sourced observations. Field documents include `core_reconstruction` only when a matching export is available; `core_availability` records unavailable or pending cores. `/api/data/v1/catalog` provides paginated already-public assets and file URLs, with separate served/original SHA-256 values. No private credentials or server environment data are exposed.
226
+
227
+ | Archive product | Member count and coverage | Verification |
228
+ | --- | --- | --- |
229
+ | Original historical tick forecasts | One member; all 32,230 planned Western Pacific issues verified | [Original receipt](https://trackformer-weatherlab.rudin-euler-8253.chatgpt.site/api/history/v1/verification) |
230
+ | Moving-core field recovery | Same original issues and identities; **still partial**, not a completed second forecast archive | [Live status](https://trackformer-weatherlab.rudin-euler-8253.chatgpt.site/api/history/core-status) · [Core receipt](https://trackformer-weatherlab.rudin-euler-8253.chatgpt.site/api/history/v1/core-verification) |
231
+ | This corrected Mangkhut film | 50 distinct causal input-perturbed members on a common geographic grid | [Film and member audit](https://huggingface.co/euler314/typhoon-predict/blob/main/evaluation/release_data/historical_video50_verification.json) |
232
+
233
+ The historical plan covers six-hour starts from 1996 onward, Wayne's explicitly requested 1986 sequence, and older first issues since 1970—not all global storms or every pre-1996 tick. Reanalysis hindcasts are retrospective; later 2026 archived GFS analyses are explicitly experimental transfer. Automatic cloud inference and core recovery run in the existing GitHub repository, not on visitors' devices or this Mac. Follow the status endpoints for current counts rather than treating a README snapshot as a live progress meter.
234
+
235
  ### Illustrated technical paper
236
 
237
+ The **[Trackformer 1.2 technical paper (PDF)](https://huggingface.co/euler314/typhoon-predict/blob/main/paper/trackformer.pdf)** explains the input tensors, multiscale attention, recurrent basin transport, moving pressure core, centre association, objectives and 50-member means. It includes a **model-structure diagram**, **1.2-versus-1.1 benchmark bars and lead-error curves**, and the selected **Fung-wong route, central-pressure timeline and model-generated isobar map**. Development-data limitations, short-lead regressions and pressure failures are reported alongside improvements. [Editable, self-contained LaTeX source](https://huggingface.co/euler314/typhoon-predict/blob/main/paper/trackformer.tex) · [Figure reproduction utility](https://huggingface.co/euler314/typhoon-predict/blob/main/release_tools/build_paper_figures.py). Historical material is recoverable from Git history and the [1.1 release](https://github.com/yu314-coder/typhoon-predict/releases/tag/trackformer-1.1).
238
 
239
  ## Recent pressure forecast with isobars
240
 
241
  The archived **Surigae forecast issued 27 September 2026 at 12:00 UTC** shows the model's regional pressure field at +6, +24 and +36 h. Thin lines are isobars every **4 hPa**, with selected labels every 12 hPa; magenta shows the forecast track and centre. Coastlines provide geographic context. These panels use the actual saved model values.
242
 
243
+ ![Surigae model-generated pressure forecast with labelled isobars](https://huggingface.co/euler314/typhoon-predict/resolve/87a6e366b42bb4cc0edc95d2c50c55fca21a2c93/evaluation/trackformer_1_2_surigae_isobars.png)
244
 
245
+ This recent example is **one deterministic forecast**, separate from the benchmark's mean of 50. Its input uses nine GFS analyses ending at 06 UTC, six hours before issue time; issue-time JMA central pressure was provided, while wind, prior motion and native regional history were unavailable. This transfer from the training data contract is experimental. The fixed regional map ends at 144°E; the selected leads keep the forecast centre inside it. The [pressure-field provenance](https://huggingface.co/euler314/typhoon-predict/blob/main/evaluation/release_data/surigae_provenance.json) records the input limitations and checkpoint hash.
246
 
247
  ## Get the model and run it
248
 
249
  - [GitHub release and source](https://github.com/yu314-coder/typhoon-predict/releases/tag/trackformer-1.2)
250
  - [Hugging Face weights and model card](https://huggingface.co/euler314/typhoon-predict)
251
+ - [Input schema and inference instructions](https://huggingface.co/euler314/typhoon-predict/blob/main/models/trackformer_1_2_field/README.md)
252
 
253
  The inference-only `weights.pt` is hosted in the release/Hugging Face model folder, **not committed to GitHub source**. The manifest records its SHA-256 and the original training-checkpoint SHA-256. The three source modules are copied unchanged from the verified training implementation. Use **`models/trackformer_1_2_field/predict.py`** as the inference entry point. The withdrawn route/scalar candidate and its incompatible example have been removed from the current source tree.
254
 
255
  ### Run one forecast
256
 
257
+ Download and extract the complete package above, or obtain the source and place the [Hugging Face weights](https://huggingface.co/euler314/typhoon-predict/resolve/main/models/trackformer_1_2_field/weights.pt) at `models/trackformer_1_2_field/weights.pt`. In an environment with PyTorch and NumPy, prepare a normalized **causal issue packet** using the [input schema](https://huggingface.co/euler314/typhoon-predict/blob/main/models/trackformer_1_2_field/README.md), then run from the package root:
258
 
259
  ```bash
260
  python models/trackformer_1_2_field/predict.py causal_issue_packet.npz forecast.npz --device mps
 
270
 
271
  | Folder | Contents |
272
  | --- | --- |
273
+ | [`models/trackformer_1_2_field/`](https://huggingface.co/euler314/typhoon-predict/tree/main/models/trackformer_1_2_field/) | Current 1.2 model, inference wrapper and input/provenance contract |
274
+ | [`paper/`](https://huggingface.co/euler314/typhoon-predict/tree/main/paper/) | Illustrated architecture and development-evaluation paper: editable source and PDF |
275
+ | [`evaluation/`](https://huggingface.co/euler314/typhoon-predict/tree/main/evaluation/) | Benchmark charts, selected forecast previews, metrics and saved plot data |
276
+ | [`docs/`](https://huggingface.co/euler314/typhoon-predict/tree/main/docs/) | Evaluation protocols and published HTML/MP4 showcases |
277
+ | [`release_tools/`](https://huggingface.co/euler314/typhoon-predict/tree/main/release_tools/) | Reproduction scripts and weight-export utility |
278
+
279
+ The older 1.1 implementation, obsolete Colab notebook, withdrawn candidate, and superseded comparison assets are no longer mixed into the current source tree. Their history and the existing release tags remain intact; the current 1.1-versus-1.2 comparison data are retained. See the [cleanup record](https://huggingface.co/euler314/typhoon-predict/blob/main/docs/repository_layout.md).
280
 
281
+ [Shared README publisher](https://huggingface.co/euler314/typhoon-predict/blob/main/release_tools/sync_public_model_cards.py) · [Publication regression tests](https://huggingface.co/euler314/typhoon-predict/blob/main/release_tools/test_public_model_cards.py). The publisher renders the entire Hugging Face card from this README, preserves its metadata and four inline players, and checks the frozen weight and neural-source identities before and after upload. Documentation synchronization does not regenerate forecasts or modify scientific scores.
models/trackformer_1_2_field/README.md CHANGED
@@ -1,5 +1,7 @@
1
  # Trackformer 1.2 field-model inference
2
 
 
 
3
  This folder contains the exact source, input contract and inference-only weights for the research model in the [main README](../../README.md). `model.py`, `baseline_model.py` and `v165_base.py` are source-identical to the selected training implementation. The weight file is distributed via the [GitHub release](https://github.com/yu314-coder/typhoon-predict/releases/tag/trackformer-1.2) and [Hugging Face](https://huggingface.co/euler314/typhoon-predict/tree/main/models/trackformer_1_2_field), not Git.
4
 
5
  Use Python with PyTorch and NumPy. The export environment used PyTorch 2.13.0; the source was evaluated on macOS MPS. Example:
@@ -26,6 +28,21 @@ The `.npz` packet must contain exactly these arrays, with no object/pickle conte
26
 
27
  Output `.npz` contains `lead_hours`, `track_lat_lon`, `central_pressure_hpa`, `basin_mslp_hpa` and `regional_mslp_hpa`. Pressure arrays are physical hPa. The regional grid is issue-relative and must be located using the static coordinate channels, not interpreted as a fixed global map.
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  **Causality:** Never populate arrays from positive-lead analysis, later best track, agency forecast or future pressure field. The wrapper checks timestamp order and shapes, but cannot authenticate the upstream archive. Do not silently replace missing values with future data. This is not a raw-weather downloader or live warning service.
30
 
31
  The published 50-member mean is a separate evaluation policy: 50 deterministic seeds perturb only normalized historical basin/regional fields with smooth zero-centred noise, then average routes, central pressures and common-grid fields. `predict.py` computes one clean forecast; do not label it a 50-member mean.
 
 
 
1
  # Trackformer 1.2 field-model inference
2
 
3
+ **Documentation revision: 1 October 2026.** This describes the current GitHub/Hugging Face wrapper. The 29 September release tarball remains an earlier snapshot; its optional wind/radius diagnostics may differ. Released neural weights, architecture, causal input schema and route/pressure predictions are unchanged.
4
+
5
  This folder contains the exact source, input contract and inference-only weights for the research model in the [main README](../../README.md). `model.py`, `baseline_model.py` and `v165_base.py` are source-identical to the selected training implementation. The weight file is distributed via the [GitHub release](https://github.com/yu314-coder/typhoon-predict/releases/tag/trackformer-1.2) and [Hugging Face](https://huggingface.co/euler314/typhoon-predict/tree/main/models/trackformer_1_2_field), not Git.
6
 
7
  Use Python with PyTorch and NumPy. The export environment used PyTorch 2.13.0; the source was evaluated on macOS MPS. Example:
 
28
 
29
  Output `.npz` contains `lead_hours`, `track_lat_lon`, `central_pressure_hpa`, `basin_mslp_hpa` and `regional_mslp_hpa`. Pressure arrays are physical hPa. The regional grid is issue-relative and must be located using the static coordinate channels, not interpreted as a fixed global map.
30
 
31
+ It also exposes the existing `maximum_wind_auxiliary_kt` scalar and experimental
32
+ pressure-derived `pressure_wind_estimate_kt`, `rmw_estimate_km` and
33
+ `r34_estimate_km` / `r50_estimate_km` / `r64_estimate_km`. Every pressure-derived
34
+ array has a matching `_valid` mask: zero padding where the mask is false is
35
+ **not a zero wind or radius forecast**. `wind_estimation_json` is a JSON string
36
+ with assumptions, member counts and unavailable reasons, not pickled objects.
37
+ The auxiliary wind also has `maximum_wind_auxiliary_kt_valid`, a numerical/domain
38
+ validity mask, not a claim of calibrated wind skill.
39
+ These are uncalibrated circular-equivalent ocean estimates, not resolved
40
+ surface winds, agency-compatible sustained winds or quadrant radii. See
41
+ [the estimation method and validation requirements](WIND_ESTIMATION.md).
42
+ No model weights, learned architecture or existing benchmark results changed.
43
+
44
  **Causality:** Never populate arrays from positive-lead analysis, later best track, agency forecast or future pressure field. The wrapper checks timestamp order and shapes, but cannot authenticate the upstream archive. Do not silently replace missing values with future data. This is not a raw-weather downloader or live warning service.
45
 
46
  The published 50-member mean is a separate evaluation policy: 50 deterministic seeds perturb only normalized historical basin/regional fields with smooth zero-centred noise, then average routes, central pressures and common-grid fields. `predict.py` computes one clean forecast; do not label it a 50-member mean.
47
+
48
+ **Pressure-display coverage:** `regional_mslp_hpa` is the original fixed issue-relative composite, not an automatically extended moving-core map. When the forecast centre leaves that patch, a scalar central-pressure readout must not be inserted into the image to make it appear consistent. The corrected Mangkhut film separately exports the actual moving model cores and registers member fields before averaging; [the current model card](https://huggingface.co/euler314/typhoon-predict) links the verified pressure arrays and shows which other films retain their older fixed patches. The automatic History archive and its separate core recovery expose exact geography and coverage through [the public API](https://trackformer-weatherlab.rudin-euler-8253.chatgpt.site/history-api).
models/trackformer_1_2_field/WIND_ESTIMATION.md ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Experimental wind and circular-radius diagnostics
2
+
3
+ The frozen 1.2 weights and neural architecture are unchanged. Maximum wind is
4
+ the existing learned auxiliary `vmax` output, in knots, not a resolved wind map.
5
+ Its averaging period is not harmonized to JMA, and skill has not been established
6
+ by the route/pressure benchmark. Do not treat this product as operational advice.
7
+
8
+ `wind_estimation.py` optionally diagnoses each model-generated pressure member:
9
+
10
+ 1. Sample complete circular rings about that member's predicted centre, no more
11
+ finely than the source/reconstruction grid, out to at most 480 km.
12
+ 2. Compute the azimuthal-mean radial pressure gradient (hPa converted to Pa).
13
+ 3. Solve `Vg^2/r + |f| Vg = (1/rho) dp/dr`, with air density 1.15 kg/m3, then
14
+ apply a fixed 0.8 ocean surface-reduction assumption. This is uncalibrated;
15
+ motion, land friction, asymmetry, boundary-layer jets and averaging periods
16
+ are not represented. No future observations or official routes are inputs.
17
+ 4. Report a resolved gradient-wind peak/RMW and outward 34/50/64-kt crossings.
18
+ These are **circular equivalent radius estimates**, not quadrant radii and
19
+ not isobar radii. Coarse (>50 km) source grids, incomplete rings, unresolved
20
+ peaks and missing crossings produce nulls, not invented zeros or edge radii.
21
+ 5. Diagnose members individually, then summarize. A mean is supplied only when
22
+ every member has that estimate. Valid counts and p10/p90 describe spread;
23
+ they are not calibrated confidence intervals. Never diagnose a mean map and
24
+ call it a mean of 50 radius predictions.
25
+
26
+ The 20-km moving core is a learned reconstruction. A resampled 0.25-degree
27
+ regional image does not add resolved observations. Even an available estimate
28
+ therefore remains experimental. A single gridded basin mean cannot reconstruct
29
+ the missing member diagnostics. Legacy forecasts without saved member/core
30
+ outputs retain unavailable estimates instead of being rerun or relabelled.
31
+
32
+ For the saved 50-member regional composite, core origins were not archived.
33
+ That release's core spans +/-640 km and centre association is restricted to
34
+ 300 km about its origin. Consequently this read-only legacy exporter samples
35
+ only rings within 300 km of each predicted centre, conservatively inside the
36
+ core footprint; a missing crossing beyond that limit remains unavailable.
37
+ New forecasts use the actual moving-core coordinates and their valid masks.
38
+
39
+ Defaults are fixed physical assumptions, not fitted to showcase outcomes. Before
40
+ claiming skill, freeze eligible whole-storm held-out cases; score +6 to +120 h
41
+ maximum winds and agency-compatible wind radii separately, with coverage,
42
+ unresolved cases, land/ocean breakdowns and whole-storm uncertainty. No such
43
+ validation or calibration is claimed by this implementation.
44
+
45
+ References: [NOAA PaHM theory and limitations](https://noaa-ocs-modeling.github.io/PaHM/pahm_manual.pdf)
46
+ and [Chavas, Reed and Knaff (2017)](https://doi.org/10.1038/s41467-017-01546-9).
models/trackformer_1_2_field/predict.py CHANGED
@@ -48,6 +48,7 @@ def load_packet(path: Path) -> dict[str, np.ndarray]:
48
 
49
 
50
  def forecast(packet: Path, model_dir: Path, device: str = "cpu") -> dict[str, np.ndarray]:
 
51
  metadata = json.loads((model_dir / "manifest.json").read_text())
52
  if metadata["public_version"] != "1.2":
53
  raise ValueError("Unexpected public model version")
@@ -70,9 +71,19 @@ def forecast(packet: Path, model_dir: Path, device: str = "cpu") -> dict[str, np
70
  "central_pressure_hpa": torch.stack([o["pressure"][0] for o in outputs]).cpu().numpy(),
71
  "basin_mslp_hpa": torch.stack([o["global"][0, 0] for o in outputs]).cpu().numpy() * scale + offset,
72
  "regional_mslp_hpa": torch.stack([o["regional"][0, 0] for o in outputs]).cpu().numpy() * scale + offset,
 
73
  }
 
 
 
 
 
 
 
 
74
  if not all(np.isfinite(value).all() for value in result.values()):
75
  raise ValueError("Non-finite forecast")
 
76
  return result
77
 
78
 
 
48
 
49
 
50
  def forecast(packet: Path, model_dir: Path, device: str = "cpu") -> dict[str, np.ndarray]:
51
+ from wind_estimation import diagnose_outputs, summarize_members, KEYS
52
  metadata = json.loads((model_dir / "manifest.json").read_text())
53
  if metadata["public_version"] != "1.2":
54
  raise ValueError("Unexpected public model version")
 
71
  "central_pressure_hpa": torch.stack([o["pressure"][0] for o in outputs]).cpu().numpy(),
72
  "basin_mslp_hpa": torch.stack([o["global"][0, 0] for o in outputs]).cpu().numpy() * scale + offset,
73
  "regional_mslp_hpa": torch.stack([o["regional"][0, 0] for o in outputs]).cpu().numpy() * scale + offset,
74
+ "maximum_wind_auxiliary_kt": torch.stack([o["vmax"][0] for o in outputs]).cpu().numpy(),
75
  }
76
+ diagnostics = [summarize_members(diagnose_outputs(o, contract)) for o in outputs]
77
+ result['maximum_wind_auxiliary_kt_valid'] = np.asarray(
78
+ [d['estimates']['maximum_wind_auxiliary_kt']['mean'] is not None for d in diagnostics], dtype=bool)
79
+ for key in KEYS[1:]:
80
+ values = [d['estimates'][key]['mean'] for d in diagnostics]
81
+ # Numeric zero is storage padding only. Consumers MUST use this mask.
82
+ result[key] = np.asarray([0 if v is None else v for v in values], dtype=np.float32)
83
+ result[key + '_valid'] = np.asarray([v is not None for v in values], dtype=bool)
84
  if not all(np.isfinite(value).all() for value in result.values()):
85
  raise ValueError("Non-finite forecast")
86
+ result['wind_estimation_json'] = np.asarray(json.dumps(diagnostics, allow_nan=False))
87
  return result
88
 
89
 
models/trackformer_1_2_field/wind_estimation.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Uncalibrated pressure-gradient diagnostics, not new neural weights.
2
+
3
+ Diagnose each physical member before ensemble aggregation. These are circular,
4
+ ocean-assumption estimates, not resolved 10-m winds or official quadrant radii.
5
+ No observed future intensity/radii, fitted showcase corrections, or mean-field
6
+ substitutions are accepted. See WIND_ESTIMATION.md for assumptions/limitations.
7
+ """
8
+ import numpy as np
9
+
10
+ VERSION = 'pressure-gradient-experimental-v1'
11
+ KEYS = ('maximum_wind_auxiliary_kt', 'pressure_wind_estimate_kt',
12
+ 'rmw_estimate_km', 'r34_estimate_km', 'r50_estimate_km', 'r64_estimate_km')
13
+ ASSUMPTIONS = dict(air_density_kg_m3=1.15, ocean_surface_reduction=0.8,
14
+ maximum_radius_km=480, azimuth_samples=72,
15
+ radial_smoothing='three-point pressure average',
16
+ geometry='azimuthal mean; circular equivalent radii, not quadrants',
17
+ wind_averaging_period='not calibrated to an agency averaging period',
18
+ land_friction_asymmetry_and_translation='not modelled',
19
+ calibration='none; fixed physical assumptions, not fitted to benchmarks')
20
+
21
+
22
+ def unavailable(reason, auxiliary_wind=None):
23
+ result = {key: None for key in KEYS}
24
+ if auxiliary_wind is not None and np.isfinite(auxiliary_wind) and 0 <= auxiliary_wind < 250:
25
+ result[KEYS[0]] = float(auxiliary_wind)
26
+ return dict(result, status=reason, experimental=True, method=VERSION)
27
+
28
+
29
+ def diagnose_member(pressure_hpa, latitude, longitude, center, auxiliary_wind=None,
30
+ *, source_spacing_km, source='model pressure reconstruction',
31
+ sampling_radius_limit_km=None):
32
+ """Bilinear polar sampling on a rectilinear *physical member* pressure grid.
33
+
34
+ source_spacing_km is underlying information spacing, not upsampled pixel
35
+ spacing. Coarse (>50 km) fields cannot support these inner-core diagnostics.
36
+ Missing rings, domain exits, inner unresolved peaks and outer non-crossings
37
+ remain missing; the last sampled radius is never passed off as a crossing.
38
+ """
39
+ result = unavailable('unresolved', auxiliary_wind)
40
+ result.update(source=source, source_spacing_km=float(source_spacing_km))
41
+ p = np.asarray(pressure_hpa, dtype=float)
42
+ lat, lon = np.asarray(latitude, dtype=float), np.asarray(longitude, dtype=float)
43
+ c = np.asarray(center, dtype=float)
44
+ if p.shape != (len(lat), len(lon)) or len(lat) < 3 or len(lon) < 3:
45
+ raise ValueError('Expected a physical rectilinear pressure grid')
46
+ if not np.isfinite(source_spacing_km) or source_spacing_km <= 0:
47
+ raise ValueError('Underlying source spacing must be known')
48
+ if not np.isfinite(c).all() or not (0 < c[0] < 60 and 100 < c[1] < 180):
49
+ result['status'] = 'outside_model_domain'
50
+ result[KEYS[0]] = None
51
+ return result
52
+ if source_spacing_km > 50:
53
+ result['status'] = 'underlying_grid_too_coarse'
54
+ return result
55
+ if lat[1] < lat[0]: lat, p = lat[::-1], p[::-1]
56
+ if lon[1] < lon[0]: lon, p = lon[::-1], p[:, ::-1]
57
+ if not np.allclose(np.diff(lat), np.diff(lat)[0], rtol=1e-3, atol=1e-5) or not np.allclose(np.diff(lon), np.diff(lon)[0], rtol=1e-3, atol=1e-5):
58
+ raise ValueError('Expected a regular latitude/longitude grid')
59
+ step = max(20., source_spacing_km,
60
+ float(np.diff(lat)[0])*111.2,
61
+ float(np.diff(lon)[0])*111.2*np.cos(np.deg2rad(c[0])))
62
+ if step > 50:
63
+ result['status'] = 'sampling_grid_too_coarse'
64
+ return result
65
+ p = np.where(np.isfinite(p) & (p >= 800) & (p <= 1100), p, np.nan)
66
+ radius_limit = ASSUMPTIONS['maximum_radius_km']
67
+ if sampling_radius_limit_km is not None:
68
+ if not np.isfinite(sampling_radius_limit_km) or sampling_radius_limit_km <= 0:
69
+ raise ValueError('A positive supported sampling radius is required')
70
+ radius_limit = min(radius_limit, sampling_radius_limit_km)
71
+ result['supported_sampling_radius_km'] = float(radius_limit)
72
+ radii = np.arange(0., radius_limit + .1, step)
73
+ theta = np.arange(ASSUMPTIONS['azimuth_samples'])*2*np.pi/ASSUMPTIONS['azimuth_samples']
74
+ ys = c[0] + radii[:, None]*np.sin(theta)/111.2
75
+ xs = c[1] + radii[:, None]*np.cos(theta)/(111.2*np.cos(np.deg2rad(c[0])))
76
+ fy, fx = (ys-lat[0])/(lat[1]-lat[0]), (xs-lon[0])/(lon[1]-lon[0])
77
+ inside = (fy >= 0) & (fx >= 0) & (fy <= len(lat)-1) & (fx <= len(lon)-1)
78
+ iy = np.clip(np.floor(fy).astype(int), 0, len(lat)-2)
79
+ ix = np.clip(np.floor(fx).astype(int), 0, len(lon)-2)
80
+ dy, dx = fy-iy, fx-ix
81
+ samples = (p[iy, ix]*(1-dy)*(1-dx) + p[iy+1, ix]*dy*(1-dx) +
82
+ p[iy, ix+1]*(1-dy)*dx + p[iy+1, ix+1]*dy*dx)
83
+ usable = inside.all(1) & np.isfinite(samples).all(1)
84
+ missing = np.flatnonzero(~usable)
85
+ stop = int(missing[0]) if len(missing) else len(radii)
86
+ if stop < 7:
87
+ result['status'] = 'insufficient_complete_ring_coverage'
88
+ return result
89
+ radii, profile = radii[:stop], samples[:stop].mean(1)*100 # hPa -> Pa
90
+ smooth = profile.copy()
91
+ smooth[1:-1] = (profile[:-2] + profile[1:-1] + profile[2:])/3
92
+ # Vg^2/r + |f| Vg = (1/rho) dp/dr. Estimate ocean surface-equivalent speed.
93
+ derivative = np.gradient(smooth, radii*1000)
94
+ f = abs(2*7.292115e-5*np.sin(np.deg2rad(c[0])))
95
+ r = radii*1000
96
+ rho = ASSUMPTIONS['air_density_kg_m3']
97
+ reduction = ASSUMPTIONS['ocean_surface_reduction']
98
+ velocity = (np.sqrt((f*r/2)**2 + r*np.maximum(derivative, 0)/rho) - f*r/2)*reduction
99
+ knots = velocity/0.514444444444
100
+ # Discard the endpoint derivative; require >=2 real cells for a peak.
101
+ peak = int(np.argmax(knots[1:-1])) + 1
102
+ result.update(radial_step_km=float(step), complete_ring_limit_km=float(radii[-2]))
103
+ if peak < 2 or peak >= len(radii)-2 or knots[peak] < 1:
104
+ result['status'] = 'maximum_wind_not_resolved'
105
+ return result
106
+ result.update(pressure_wind_estimate_kt=float(knots[peak]),
107
+ rmw_estimate_km=float(radii[peak]), status='experimental_estimate')
108
+ for threshold in (34, 50, 64):
109
+ key = f'r{threshold}_estimate_km'
110
+ if knots[peak] < threshold:
111
+ continue # unresolved threshold, not a demonstrated zero-radius storm
112
+ crossing = next((i for i in range(peak+1, len(radii)-1)
113
+ if knots[i] < threshold <= knots[i-1]), None)
114
+ if crossing is not None:
115
+ fraction = (threshold-knots[crossing-1])/(knots[crossing]-knots[crossing-1])
116
+ result[key] = float(radii[crossing-1] + fraction*step)
117
+ return result
118
+
119
+
120
+ def summarize_members(members):
121
+ """Require all members for a labelled ensemble mean; disclose censoring."""
122
+ if not members:
123
+ raise ValueError('Actual member diagnostics are required')
124
+ summary = dict(method=VERSION, experimental=True, members=len(members),
125
+ aggregation='diagnose each member, then equal-weight mean; never diagnose the mean pressure',
126
+ assumptions=ASSUMPTIONS, estimates={},
127
+ status_counts={status: sum(m['status'] == status for m in members)
128
+ for status in sorted({m['status'] for m in members})})
129
+ for key in KEYS:
130
+ values = [m[key] for m in members if m[key] is not None and np.isfinite(m[key])]
131
+ complete = len(values) == len(members)
132
+ summary['estimates'][key] = dict(mean=float(np.mean(values)) if complete else None,
133
+ p10=float(np.quantile(values, .1)) if complete else None,
134
+ p90=float(np.quantile(values, .9)) if complete else None,
135
+ valid_members=len(values), total_members=len(members))
136
+ return summary
137
+
138
+
139
+ def diagnose_outputs(output, contract):
140
+ """Extract physical moving-core members without touching weights or state."""
141
+ array = lambda tensor: tensor.detach().cpu().numpy()
142
+ center, wind = array(output['center']), array(output['vmax'])
143
+ if not all(key in output for key in ('core', 'core_lat', 'core_lon')):
144
+ return [unavailable('member_core_not_saved', value) for value in wind]
145
+ pressure = array(output['core'])[:, 0]*contract['normalization']['std'][0]+contract['normalization']['mean'][0]
146
+ if 'core_valid' in output:
147
+ pressure = np.where(array(output['core_valid'])[:,0], pressure, np.nan)
148
+ lat, lon = array(output['core_lat']), array(output['core_lon'])
149
+ return [diagnose_member(pressure[i], lat[i,:,0], lon[i,0,:], center[i], wind[i],
150
+ source_spacing_km=20, source='physical 20-km learned moving-core reconstruction')
151
+ for i in range(len(center))]
release_tools/sync_public_model_cards.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Render the complete HF card from the canonical GitHub README.
2
+
3
+ Preserve remote metadata, four inline films and frozen neural weights. Default
4
+ mode only prepares artifacts on D; --publish explicitly updates the HF card,
5
+ matching existing inference wrapper/docs and this small reproduction utility.
6
+ No forecasts are rerun, no media are regenerated and no neural modules change.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import hashlib
12
+ import html
13
+ import json
14
+ import re
15
+ import subprocess
16
+ from pathlib import Path
17
+
18
+ ROOT = Path(__file__).resolve().parents[1]
19
+ REPO = 'euler314/typhoon-predict'
20
+ MEDIA_REVISION = '87a6e366b42bb4cc0edc95d2c50c55fca21a2c93'
21
+ CACHE = '/Volumes/D/typhoon_predict/.cache/huggingface-intensity'
22
+ HF = 'https://huggingface.co/' + REPO
23
+ GALLERY = '| Soudelor (2015) | Mangkhut (2018) | Meranti (2016) |\n'
24
+ FILMS = ('mangkhut', 'fung_wong', 'soudelor', 'meranti')
25
+ SYNC_FILES = (
26
+ 'models/trackformer_1_2_field/README.md',
27
+ 'models/trackformer_1_2_field/WIND_ESTIMATION.md',
28
+ 'models/trackformer_1_2_field/predict.py',
29
+ 'models/trackformer_1_2_field/wind_estimation.py',
30
+ 'release_tools/sync_public_model_cards.py',
31
+ 'release_tools/test_public_model_cards.py',
32
+ )
33
+
34
+
35
+ def sha(path):
36
+ with Path(path).open('rb') as stream:
37
+ return hashlib.file_digest(stream, 'sha256').hexdigest()
38
+
39
+
40
+ def media_url(stem, poster=False):
41
+ path = ('evaluation/trackformer_1_2_' + stem + '_video_poster.png'
42
+ if poster else 'docs/trackformer_1_2_' + stem + '.mp4')
43
+ return HF + '/resolve/' + MEDIA_REVISION + '/' + path
44
+
45
+
46
+ def player(stem, title):
47
+ return ('<video controls preload="metadata" width="100%" '
48
+ 'aria-label="' + html.escape(title, quote=True) + '" '
49
+ 'poster="' + media_url(stem, True) + '" '
50
+ 'src="' + media_url(stem) + '"></video>')
51
+
52
+
53
+ def render_card(original, github):
54
+ metadata = re.match(r'\A---\n.*?\n---\n', original, re.S)
55
+ if not metadata:
56
+ raise ValueError('Preserve existing HF YAML metadata; missing header')
57
+ if github.count('## Corrected pressure forecast — Mangkhut (2018)\n') != 1:
58
+ raise ValueError('Missing or ambiguous corrected-film section')
59
+ if github.count(GALLERY) != 1:
60
+ raise ValueError('Missing or ambiguous historical video gallery')
61
+ card = github
62
+ for stem, alt in (
63
+ ('mangkhut', 'Corrected Mangkhut moving-core pressure forecast — 1 October 2026 revision'),
64
+ ('fung_wong', 'Play the Trackformer 1.2 Fung-wong pressure forecast MP4'),
65
+ ):
66
+ old = '[![' + alt + '](' + media_url(stem, True) + ')](' + media_url(stem) + ')'
67
+ if card.count(old) != 1:
68
+ raise ValueError('Missing or ambiguous primary preview: ' + stem)
69
+ card = card.replace(old, player(stem, alt), 1)
70
+ start = card.index(GALLERY)
71
+ end = card.index('\n\n', start)
72
+ original_gallery = card[start:end]
73
+ if len(original_gallery.splitlines()) != 5:
74
+ raise ValueError('Unexpected gallery; preserve existing documentation')
75
+ replacements = []
76
+ for stem, name, issue in (
77
+ ('soudelor', 'Soudelor (2015)', '5 August 2015, 00 UTC'),
78
+ ('mangkhut', 'Mangkhut (2018)', '11 September 2018, 00 UTC'),
79
+ ('meranti', 'Meranti (2016)', '10 September 2016, 00 UTC'),
80
+ ):
81
+ replacements.append('### ' + name + ' — issue ' + issue)
82
+ if stem == 'mangkhut':
83
+ replacements.append('The corrected 20-second player appears near the top of this card; its moving-core repair and audit are described there.')
84
+ else:
85
+ replacements.append(player(stem, name + ' original fixed-patch film'))
86
+ replacements.append('[Play / download ' + name.split(' (')[0] + ' MP4](' + media_url(stem) + ') · [Provenance](evaluation/release_data/' + stem + '_video.json)')
87
+ card = card[:start] + '\n\n'.join(replacements) + card[end:]
88
+
89
+ # Relative GitHub links need HF-specific URLs. Keep evolving documentation
90
+ # on main; pin unchanged scientific image bytes to the verified asset commit.
91
+ def link(match):
92
+ image, label, url = match.groups()
93
+ if url.startswith(('https:', 'http:', '#', 'mailto:')):
94
+ return match.group(0)
95
+ path, _, fragment = url.partition('#')
96
+ resolved = (ROOT / path).resolve()
97
+ if not resolved.is_relative_to(ROOT) or not resolved.exists():
98
+ raise ValueError('Invalid or missing relative public link: ' + path)
99
+ kind = 'resolve/' + MEDIA_REVISION if image else ('tree/main' if resolved.is_dir() else 'blob/main')
100
+ target = HF + '/' + kind + '/' + path + ('#' + fragment if fragment else '')
101
+ return image + '[' + label + '](' + target + ')'
102
+ card = re.sub(r'(!?)\[([^\[\]\n]*)\]\(([^)\s]+)\)', link, card)
103
+ card = metadata.group(0) + '\n' + card
104
+ if card.count('<video ') != 4:
105
+ raise ValueError('Expected exactly one native player for each of four films')
106
+ for stem in FILMS:
107
+ if card.count('src="' + media_url(stem) + '"') != 1:
108
+ raise ValueError('Missing, repeated or stale player: ' + stem)
109
+ if '/resolve/main/docs/trackformer_1_2_' in card:
110
+ raise ValueError('Unversioned movie source')
111
+ return card
112
+
113
+
114
+ def frozen_identity(api, revision):
115
+ manifest = json.loads((ROOT / 'models/trackformer_1_2_field/manifest.json').read_text())
116
+ names = ['models/trackformer_1_2_field/weights.pt'] + [
117
+ 'models/trackformer_1_2_field/' + name for name in manifest['source_module_sha256']]
118
+ entries = {i.path: i for i in api.get_paths_info(REPO, names, revision=revision)}
119
+ weight = entries.get(names[0])
120
+ actual = getattr(getattr(weight, 'lfs', None), 'sha256', None)
121
+ if actual != manifest['inference_weights_sha256']:
122
+ raise ValueError('Released weights are absent or differ from frozen identity')
123
+ for name, expected in manifest['source_module_sha256'].items():
124
+ path = ROOT / 'models/trackformer_1_2_field' / name
125
+ data = path.read_bytes()
126
+ entry = entries.get('models/trackformer_1_2_field/' + name)
127
+ if sha(path) != expected or entry is None or entry.blob_id != hashlib.sha1(b'blob ' + str(len(data)).encode() + b'\0' + data).hexdigest():
128
+ raise ValueError('Frozen neural source differs: ' + name)
129
+ return actual
130
+
131
+
132
+ def run(output, publish=False):
133
+ # Lazy imports keep portable CI document tests independent of HF packages.
134
+ from huggingface_hub import CommitOperationAdd, HfApi, hf_hub_download
135
+ output = output.resolve()
136
+ if not output.is_relative_to(Path('/Volumes/D')):
137
+ raise ValueError('All generated documentation/cache artifacts must stay on D')
138
+ output.mkdir(parents=True, exist_ok=True)
139
+ api = HfApi()
140
+ parent = api.model_info(REPO).sha
141
+ weights_before = frozen_identity(api, parent)
142
+ original = Path(hf_hub_download(REPO, 'README.md', revision=parent, cache_dir=CACHE))
143
+ github = (ROOT / 'README.md').read_text()
144
+ card_path = output / 'README.md'
145
+ card_path.write_text(render_card(original.read_text(), github))
146
+ files = {'README.md': card_path, **{name: ROOT / name for name in SYNC_FILES}}
147
+ source_commit = subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=ROOT, text=True).strip()
148
+ receipt = {
149
+ 'status': 'prepared', 'repo_id': REPO, 'parent_commit': parent,
150
+ 'github_source_commit': source_commit,
151
+ 'github_readme_sha256': sha(ROOT / 'README.md'),
152
+ 'original_hf_readme_sha256': sha(original),
153
+ 'files': {name: sha(path) for name, path in files.items()},
154
+ 'media_revision': MEDIA_REVISION, 'native_video_players': 4,
155
+ 'inference_weights_sha256': weights_before,
156
+ 'weights_changed': False, 'neural_modules_changed': False,
157
+ 'forecast_arrays_changed': False, 'videos_changed': False,
158
+ }
159
+ if publish:
160
+ dirty = subprocess.check_output(['git', 'status', '--porcelain', '--', 'README.md', *SYNC_FILES], cwd=ROOT, text=True)
161
+ if dirty:
162
+ raise ValueError('Commit canonical README and matching source files before publishing')
163
+ committed = api.create_commit(repo_id=REPO, repo_type='model', parent_commit=parent,
164
+ commit_message='Refresh complete model card, pin corrected pressure movie, and sync inference documentation',
165
+ operations=[CommitOperationAdd(path_in_repo=name, path_or_fileobj=str(path)) for name, path in files.items()])
166
+ for name, expected in receipt['files'].items():
167
+ saved = hf_hub_download(REPO, name, revision=committed.oid, cache_dir=CACHE)
168
+ if sha(saved) != expected:
169
+ raise ValueError('Published documentation/source differs: ' + name)
170
+ if frozen_identity(api, committed.oid) != weights_before:
171
+ raise ValueError('Frozen identity changed during documentation publication')
172
+ receipt.update(status='published_verified', commit=committed.oid,
173
+ commit_url=str(committed.commit_url), all_remote_files_sha256_verified=True)
174
+ (output / 'publication.json').write_text(json.dumps(receipt, indent=2) + '\n')
175
+ print(json.dumps({key: value for key, value in receipt.items() if key != 'files'}, indent=2))
176
+
177
+
178
+ if __name__ == '__main__':
179
+ parser = argparse.ArgumentParser(description=__doc__)
180
+ parser.add_argument('--output', type=Path, required=True)
181
+ parser.add_argument('--publish', action='store_true')
182
+ args = parser.parse_args()
183
+ run(args.output, args.publish)
release_tools/test_public_model_cards.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Portable regression checks for complete shared README publication."""
2
+ import re
3
+ import unittest
4
+ from sync_public_model_cards import MEDIA_REVISION, ROOT, media_url, render_card
5
+
6
+
7
+ class PublicModelCardsTest(unittest.TestCase):
8
+ def setUp(self):
9
+ self.source = (ROOT / 'README.md').read_text()
10
+ self.header = '---\nlicense: mit\ntags:\n- research\ncustom_preserved: yes\n---\n'
11
+ self.original = self.header + '\nOld model card\n'
12
+
13
+ def test_full_card_and_remote_metadata_are_preserved(self):
14
+ rendered = render_card(self.original, self.source)
15
+ self.assertTrue(rendered.startswith(self.header))
16
+ for text in ('README revision: 1 October 2026',
17
+ 'Daily-issue benchmark: 1,473 forecasts',
18
+ 'API contract **1.1**', 'still partial', '25.03 kt'):
19
+ self.assertIn(text, rendered)
20
+ self.assertIn('N/A — no radius head', rendered)
21
+ self.assertNotIn('Old model card', rendered)
22
+
23
+ def test_exactly_four_pinned_players_and_corrected_primary(self):
24
+ rendered = render_card(self.original, self.source)
25
+ self.assertEqual(rendered.count('<video '), 4)
26
+ for stem in ('fung_wong', 'soudelor', 'mangkhut', 'meranti'):
27
+ self.assertEqual(rendered.count('src="' + media_url(stem) + '"'), 1)
28
+ self.assertLess(rendered.index('src="' + media_url('mangkhut') + '"'), rendered.index('## What changed'))
29
+ self.assertNotIn('/resolve/main/docs/trackformer_', rendered)
30
+ self.assertIn(MEDIA_REVISION, rendered)
31
+ self.assertIn('Original fixed-patch film', rendered)
32
+
33
+ def test_no_github_relative_links_survive(self):
34
+ rendered = render_card(self.original, self.source)
35
+ for url in re.findall(r'\]\(([^)\s]+)\)', rendered):
36
+ self.assertTrue(url.startswith(('https:', 'http:', '#', 'mailto:')), url)
37
+
38
+ def test_missing_anchor_fails_instead_of_partial_stale_update(self):
39
+ with self.assertRaisesRegex(ValueError, 'corrected-film'):
40
+ render_card(self.original, self.source.replace('## Corrected pressure forecast', '## Wrong heading'))
41
+
42
+ def test_missing_metadata_fails_closed(self):
43
+ with self.assertRaisesRegex(ValueError, 'YAML metadata'):
44
+ render_card('No metadata', self.source)
45
+
46
+ def test_relative_paths_cannot_escape_public_repository(self):
47
+ with self.assertRaisesRegex(ValueError, 'relative public link'):
48
+ render_card(self.original, self.source + '\n[No secrets](../../private-file.txt)\n')
49
+
50
+
51
+ if __name__ == '__main__':
52
+ unittest.main()