Refresh complete model card, pin corrected pressure movie, and sync inference documentation
Browse files- README.md +72 -46
- models/trackformer_1_2_field/README.md +17 -0
- models/trackformer_1_2_field/WIND_ESTIMATION.md +46 -0
- models/trackformer_1_2_field/predict.py +11 -0
- models/trackformer_1_2_field/wind_estimation.py +151 -0
- release_tools/sync_public_model_cards.py +183 -0
- release_tools/test_public_model_cards.py +52 -0
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)** · **[
|
| 13 |
|
| 14 |
-
|
|
|
|
|
|
|
| 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 |
-
##
|
| 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 |
-

|
| 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 |
-

|
| 99 |
|
| 100 |
-

|
| 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 |
-

|
| 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 |
-

|
| 126 |
|
| 127 |
<details>
|
| 128 |
<summary>Fung-wong · issue 7 November 2025, 00 UTC</summary>
|
| 129 |
|
| 130 |
-

|
| 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 |
-

|
| 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 |
-

|
| 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 |
-

|
| 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
|
| 168 |
-
|
| 169 |
-
[](https://yu314-coder.github.io/typhoon-predict/trackformer_1_2_fung_wong.mp4)
|
| 170 |
|
| 171 |
-
**[Play or download the Fung-wong MP4](https://
|
| 172 |
|
| 173 |
-
The forecast starts **7 November 2025 at 00 UTC**.
|
| 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
|
| 188 |
|
| 189 |
-
[Play / download Soudelor MP4](https://huggingface.co/euler314/typhoon-predict/resolve/
|
| 190 |
|
| 191 |
### Mangkhut (2018) — issue 11 September 2018, 00 UTC
|
| 192 |
|
| 193 |
-
|
| 194 |
|
| 195 |
-
[Play / download Mangkhut MP4](https://huggingface.co/euler314/typhoon-predict/resolve/
|
| 196 |
|
| 197 |
### Meranti (2016) — issue 10 September 2016, 00 UTC
|
| 198 |
|
| 199 |
-
<video controls
|
| 200 |
|
| 201 |
-
[Play / download Meranti MP4](https://huggingface.co/euler314/typhoon-predict/resolve/
|
| 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 |
-

|
| 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 |
-
|
|
|
|
| 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 |
+

|
| 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 |
+

|
| 115 |
|
| 116 |
+

|
| 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 |
+

|
| 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 |
+

|
| 142 |
|
| 143 |
<details>
|
| 144 |
<summary>Fung-wong · issue 7 November 2025, 00 UTC</summary>
|
| 145 |
|
| 146 |
+

|
| 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 |
+

|
| 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 |
+

|
| 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 |
+

|
| 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 |
+

|
| 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 = '[ + ')](' + 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()
|