Add pre-2024 chart and verified Trackformer 1.2 training sources and cutoffs
Browse files- .gitattributes +1 -0
- docs/deepmind_daily_benchmark.md +4 -2
- docs/trackformer_1_2_evaluation.md +2 -0
- docs/trackformer_1_2_training_data.md +56 -0
- evaluation/released_daily/model_1_2_after_2024_benchmark.json +20 -0
- evaluation/released_daily/model_1_2_before_2024_benchmark.json +77 -0
- evaluation/released_daily/model_1_2_before_2024_benchmark.png +3 -0
- evaluation/released_daily/model_1_2_benchmark.json +20 -0
- evaluation/training_data/trackformer_1_2_provenance.json +153 -0
- models/trackformer_1_2_field/README.md +2 -0
- release_tools/audit_training_data.py +124 -0
- release_tools/plot_model_announcement.py +37 -5
- release_tools/sync_public_model_cards.py +8 -2
- release_tools/test_deepmind_period_comparison.py +19 -2
- release_tools/test_public_model_cards.py +40 -0
.gitattributes
CHANGED
|
@@ -198,3 +198,4 @@ evaluation/released_daily/pressure_comparison.png filter=lfs diff=lfs merge=lfs
|
|
| 198 |
evaluation/released_daily/model_1_2_benchmark.png filter=lfs diff=lfs merge=lfs -text
|
| 199 |
docs/trackformer_1_2_mangkhut.gif filter=lfs diff=lfs merge=lfs -text
|
| 200 |
evaluation/released_daily/model_1_2_after_2024_benchmark.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 198 |
evaluation/released_daily/model_1_2_benchmark.png filter=lfs diff=lfs merge=lfs -text
|
| 199 |
docs/trackformer_1_2_mangkhut.gif filter=lfs diff=lfs merge=lfs -text
|
| 200 |
evaluation/released_daily/model_1_2_after_2024_benchmark.png filter=lfs diff=lfs merge=lfs -text
|
| 201 |
+
evaluation/released_daily/model_1_2_before_2024_benchmark.png filter=lfs diff=lfs merge=lfs -text
|
docs/deepmind_daily_benchmark.md
CHANGED
|
@@ -43,7 +43,7 @@ Mini has better recent-only track scores even though 1.2 has lower position/head
|
|
| 43 |
|
| 44 |
### Strict forecast-date partition and separate charts
|
| 45 |
|
| 46 |
-
|
| 47 |
|
| 48 |
| UTC issue year | Starts / share | Storms / share |
|
| 49 |
| --- | --- | --- |
|
|
@@ -53,7 +53,9 @@ Both README charts use **`WeatherNextCyclones_Mini_<2024`**, the same official c
|
|
| 53 |
|
| 54 |
The original `recent_2024_onward` aggregate includes 2024 and is retained unchanged. The new **strict `after_2024`** derivative excludes 2024: track position MAE is **963.02 / 469.40 / 258.67 km**, direction error **57.88 / 33.81 / 28.73°**, and JMA pressure MAE **12.59 / 11.12 / 9.55 hPa** for 1.1 / 1.2 / Mini. Track uses 61 starts / 18 storms; pressure uses 59 starts / 18 storms / 1,174 exact common leads. The total chart still uses the original scores. Its historical 230 storms contribute 85.2% of equal-storm track weight, not 90.7%.
|
| 55 |
|
| 56 |
-
|
|
|
|
|
|
|
| 57 |
|
| 58 |
Reproduce these derivative summaries without inference or raw-weather downloads:
|
| 59 |
|
|
|
|
| 43 |
|
| 44 |
### Strict forecast-date partition and separate charts
|
| 45 |
|
| 46 |
+
All three README charts—total, strict `<2024` and strict `>2024`—use **`WeatherNextCyclones_Mini_<2024`**, the same official checkpoint trained through 2023, software v0.3.0, native 1° and one CUDA member. A post-2024 forecast is not a post-2024-trained model. The following proportions refer to this evaluation cohort, not the model's training dataset.
|
| 47 |
|
| 48 |
| UTC issue year | Starts / share | Storms / share |
|
| 49 |
| --- | --- | --- |
|
|
|
|
| 53 |
|
| 54 |
The original `recent_2024_onward` aggregate includes 2024 and is retained unchanged. The new **strict `after_2024`** derivative excludes 2024: track position MAE is **963.02 / 469.40 / 258.67 km**, direction error **57.88 / 33.81 / 28.73°**, and JMA pressure MAE **12.59 / 11.12 / 9.55 hPa** for 1.1 / 1.2 / Mini. Track uses 61 starts / 18 storms; pressure uses 59 starts / 18 storms / 1,174 exact common leads. The total chart still uses the original scores. Its historical 230 storms contribute 85.2% of equal-storm track weight, not 90.7%.
|
| 55 |
|
| 56 |
+
The strict **`before_2024`** chart uses the 1,336 historical starts / 230 storms (1980–1999 here): position MAE **780.50 / 470.17 / 534.86 km** and direction error **51.36 / 35.01 / 41.26°** for 1.1 / 1.2 / Mini. All three shared JMA pressure scores are null, with zero eligible common starts: frozen historical 1.1 intensity inputs are unavailable. That panel says **Not scored** and contains no bars, not zero-height bars. Track coverage must never be reused as pressure coverage.
|
| 57 |
+
|
| 58 |
+
[UTC partitions, exact case IDs, all metrics and source hashes](../evaluation/deepmind_daily/period_comparison.json) · [Total chart](../evaluation/released_daily/model_1_2_benchmark.png) · [Pre-2024 chart](../evaluation/released_daily/model_1_2_before_2024_benchmark.png) · [Post-2024 chart](../evaluation/released_daily/model_1_2_after_2024_benchmark.png)
|
| 59 |
|
| 60 |
Reproduce these derivative summaries without inference or raw-weather downloads:
|
| 61 |
|
docs/trackformer_1_2_evaluation.md
CHANGED
|
@@ -51,3 +51,5 @@ This plots the saved completed equal-storm results; it performs no new inference
|
|
| 51 |
## Validation boundary
|
| 52 |
|
| 53 |
Training used whole-storm year partitions: 2000–2021 training, 2022–2023 validation, 2024–2025 test in the original pipeline, with 13,949 / 1,041 / 1,195 windows respectively; native-pressure windows total 800 / 100 / 100. Repeatedly viewed benchmarks, TIP and calibration cases have become development evidence. Freeze a genuinely unused storm-level holdout with matched issue-time data before claiming generalization. Evaluate route, central pressure and pressure fields at every lead on common cases and masks; a lower map average alone is not a model win.
|
|
|
|
|
|
|
|
|
| 51 |
## Validation boundary
|
| 52 |
|
| 53 |
Training used whole-storm year partitions: 2000–2021 training, 2022–2023 validation, 2024–2025 test in the original pipeline, with 13,949 / 1,041 / 1,195 windows respectively; native-pressure windows total 800 / 100 / 100. Repeatedly viewed benchmarks, TIP and calibration cases have become development evidence. Freeze a genuinely unused storm-level holdout with matched issue-time data before claiming generalization. Evaluate route, central pressure and pressure fields at every lead on common cases and masks; a lower map average alone is not a model win.
|
| 54 |
+
|
| 55 |
+
The fitting sources are IBTrACS-derived storm records, NOAA PSL NCEP/NCAR basin analyses, ARCO-ERA5 regional MSLP and static ERA5 land fraction/elevation. Newer 2026 archive records are not part of the released model's gradient fitting. See [training-source documentation](trackformer_1_2_training_data.md) and [checksum-verified split receipt](../evaluation/training_data/trackformer_1_2_provenance.json).
|
docs/trackformer_1_2_training_data.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Trackformer 1.2 training data
|
| 2 |
+
|
| 3 |
+
The **released 1.2 model's fitting cutoff is 2021**, not 2025 or 2026. Its selected checkpoint is development version 1.2.73, epoch 4. The dataset and normalization identities are frozen in the [release manifest](../models/trackformer_1_2_field/manifest.json). This document describes that release, not later 1.3 training or live-input downloads.
|
| 4 |
+
|
| 5 |
+
## Sources actually consumed by the released training loader
|
| 6 |
+
|
| 7 |
+
| Component | Dataset | Variables and sampling |
|
| 8 |
+
| --- | --- | --- |
|
| 9 |
+
| Storm observations / supervised labels | [NOAA IBTrACS](https://www.ncei.noaa.gov/products/international-best-track-archive)-derived `track_windows_v13.npz` | Position, prior motion, available maximum wind and central pressure, exact storm/time identities and label masks. Only eligible Western Pacific-domain windows enter this release. |
|
| 10 |
+
| Basin weather | [NOAA PSL NCEP/NCAR Reanalysis 1](https://psl.noaa.gov/data/gridded/data.ncep.reanalysis.html), `basin_all_int8.npz` and the exact-time SLP atlas | MSLP, 500 hPa geopotential height, zonal/meridional winds at 850/500/200 hPa. Six-hour analyses on 100–180°E, 0–60°N; 2.5° grid, 25 × 33 cells. |
|
| 11 |
+
| Native regional pressure | [ARCO-ERA5](https://github.com/google-research/arco-era5), `pressure_hpa.npy` with its frozen `plan.json` | Mean sea-level pressure on 0.25° fixed issue-centred 121 × 121 patches. 1,000 original windows across all three partitions, not native detail for every coarse window. |
|
| 12 |
+
| Static geography | Same ARCO-ERA5 source, `geography.npz` | Land fraction and surface geopotential / 9.80665 to obtain elevation in metres; latitude/longitude encodings. Static context is not an additional temporal fitting period. |
|
| 13 |
+
|
| 14 |
+
The eight weather channels are **MSLP, HGT500, U850, V850, U500, V500, U200, V200**. No satellite cloud imagery, humidity/vapour, SST or ocean-heat-content channel is present in this released input contract. Such features in other experiments must not be attributed to 1.2.
|
| 15 |
+
|
| 16 |
+
The selected dataset manifest identifies the **derived IBTrACS archive by SHA-256**, not by an independently recorded raw IBTrACS release/download ID. A newer downloaded IBTrACS file elsewhere is not evidence of which raw snapshot built these frozen windows. Available wind labels and the learned auxiliary head do not establish a harmonized agency averaging period or a validated surface-wind/radius forecast.
|
| 17 |
+
|
| 18 |
+
## Temporal partitions
|
| 19 |
+
|
| 20 |
+
| Role | Whole-storm years | Eligible windows / storms | Native-pressure windows | First → last eligible UTC issue |
|
| 21 |
+
| --- | --- | --- | ---: | --- |
|
| 22 |
+
| Fitting and normalization | 2000–2021 | 13,949 / 611 | 800 | 2000-05-05 18:00 → 2021-12-20 12:00 |
|
| 23 |
+
| Validation / checkpoint selection | 2022–2023 | 1,041 / 46 | 100 | 2022-03-31 00:00 → 2023-12-17 18:00 |
|
| 24 |
+
| Original test / evaluation | 2024–2025 | 1,195 / 56 | 100 | 2024-05-22 00:00 → 2025-12-02 12:00 |
|
| 25 |
+
|
| 26 |
+
These are loader-eligible window counts, not independent storms or the number of gradient updates. Native-pressure windows are a **subset** of the corresponding eligible windows, not additional cases. Storm membership uses each storm's first year in the frozen archive. Windows whose −48 h history or +120 h targets cross a partition boundary are excluded. Weather normalization uses the 2000–2021 basin frames only.
|
| 27 |
+
|
| 28 |
+
Every forecast history comprises nine consecutive six-hour analyses at **−48, −42, …, 0 h**. Supervised fields and storm labels at **+6, +12, …, +120 h** are targets only; they cannot be inference inputs. Missing native detail and missing intensity labels retain their masks. Validation is not gradient fitting, but it does inform checkpoint selection. Previously inspected original tests and showcases are development evidence, not a new unused holdout.
|
| 29 |
+
|
| 30 |
+
## Newer data do not change the fitting cutoff
|
| 31 |
+
|
| 32 |
+
The checksum-pinned caches contain records beyond the fitting years:
|
| 33 |
+
|
| 34 |
+
- Derived storm-window archive: **1980-01-03 00 UTC to 2026-07-13 00 UTC**.
|
| 35 |
+
- Coarse NCEP weather archive: **1980-01-01 00 UTC to 2026-03-17 18 UTC**.
|
| 36 |
+
- Original native-pressure plan: fitting, validation and test windows in **2000–2025**, separated as above.
|
| 37 |
+
|
| 38 |
+
Those are **source-cache endpoints**, not years all used to fit 1.2. The 2026 live/backfill pipeline uses the unchanged released model with later causal inputs; GFS transfer is experimental, and generating a new forecast does not retrain the weights. Retrospective reanalysis availability is not proof that the same inputs were available operationally at each historical issue time.
|
| 39 |
+
|
| 40 |
+
## Verified identity and reproduction
|
| 41 |
+
|
| 42 |
+
The [training-data receipt](../evaluation/training_data/trackformer_1_2_provenance.json) rechecks all six derived dataset file hashes, the released dataset-manifest hash, exact split counts, storm separation and native-patch membership. It records the original cache endpoints and exact earliest histories/latest targets. This is not a new raw-source-download audit.
|
| 43 |
+
|
| 44 |
+
| Frozen identity | SHA-256 |
|
| 45 |
+
| --- | --- |
|
| 46 |
+
| Dataset manifest | `04fd0d30620137aad20173dcd59e22da2a6c20fae2f17de593e91a59517459c1` |
|
| 47 |
+
| Selected training checkpoint | `f194a23d3f91ea76ad776dfad942fabd669eeae8b3fd665815463095367e9ee0` |
|
| 48 |
+
| Inference-only weights | `db49f36e85a3766defc4c172746897a1f783705d1ce8e6f9dfb8e87ae1d902cb` |
|
| 49 |
+
|
| 50 |
+
With the original frozen dataset available, reproduce the small receipt without inference, downloads or refreshing the training cache:
|
| 51 |
+
|
| 52 |
+
```bash
|
| 53 |
+
python release_tools/audit_training_data.py --dataset-root /Volumes/D/typhoon_predict/data/v164_reuse --output /Volumes/D/typhoon_predict/remote_typhoon_predict_repo/evaluation/training_data/trackformer_1_2_provenance.json
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
The original full training arrays are not newly bundled by this documentation update. Model weights, neural source, forecasts and training processes are unchanged.
|
evaluation/released_daily/model_1_2_after_2024_benchmark.json
CHANGED
|
@@ -48,6 +48,26 @@
|
|
| 48 |
"deepmind"
|
| 49 |
],
|
| 50 |
"bars_per_panel": 3,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
"error_bars": false,
|
| 52 |
"zero_fill": false,
|
| 53 |
"model_forecasts_modified": false,
|
|
|
|
| 48 |
"deepmind"
|
| 49 |
],
|
| 50 |
"bars_per_panel": 3,
|
| 51 |
+
"available_bars_per_panel": {
|
| 52 |
+
"pressure_JMA_hpa": 3,
|
| 53 |
+
"mean_track_error_km": 3,
|
| 54 |
+
"direction_error_deg": 3
|
| 55 |
+
},
|
| 56 |
+
"coverage": {
|
| 57 |
+
"pressure_JMA_hpa": {
|
| 58 |
+
"daily_issues": 59,
|
| 59 |
+
"storms": 18
|
| 60 |
+
},
|
| 61 |
+
"mean_track_error_km": {
|
| 62 |
+
"daily_issues": 61,
|
| 63 |
+
"storms": 18
|
| 64 |
+
},
|
| 65 |
+
"direction_error_deg": {
|
| 66 |
+
"daily_issues": 61,
|
| 67 |
+
"storms": 18
|
| 68 |
+
}
|
| 69 |
+
},
|
| 70 |
+
"unavailable_metrics": [],
|
| 71 |
"error_bars": false,
|
| 72 |
"zero_fill": false,
|
| 73 |
"model_forecasts_modified": false,
|
evaluation/released_daily/model_1_2_before_2024_benchmark.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"source": "evaluation/deepmind_daily/period_comparison.json",
|
| 3 |
+
"source_sha256": "0aef2eb8b5f808e5100cbbe132af3dc3e5d2de6fefe7d92d916190049cdf25cb",
|
| 4 |
+
"chart": "evaluation/released_daily/model_1_2_before_2024_benchmark.png",
|
| 5 |
+
"chart_sha256": "d441f512bd6eeb6761bc2ac4bb3bb09d64c9efccc8241fdc93b489fd442f5104",
|
| 6 |
+
"period": "before_2024",
|
| 7 |
+
"period_definition": "UTC issue year < 2024",
|
| 8 |
+
"metrics": [
|
| 9 |
+
"pressure_JMA_hpa",
|
| 10 |
+
"mean_track_error_km",
|
| 11 |
+
"direction_error_deg"
|
| 12 |
+
],
|
| 13 |
+
"values": {
|
| 14 |
+
"pressure_JMA_hpa": {
|
| 15 |
+
"1.1": null,
|
| 16 |
+
"1.2": null,
|
| 17 |
+
"deepmind": null
|
| 18 |
+
},
|
| 19 |
+
"mean_track_error_km": {
|
| 20 |
+
"1.1": 780.4956251297209,
|
| 21 |
+
"1.2": 470.1683227329181,
|
| 22 |
+
"deepmind": 534.8619522162429
|
| 23 |
+
},
|
| 24 |
+
"direction_error_deg": {
|
| 25 |
+
"1.1": 51.35513468466848,
|
| 26 |
+
"1.2": 35.008663500237326,
|
| 27 |
+
"deepmind": 41.2608569444939
|
| 28 |
+
}
|
| 29 |
+
},
|
| 30 |
+
"deepmind_status": "complete_verified",
|
| 31 |
+
"deepmind_model": {
|
| 32 |
+
"key": "deepmind",
|
| 33 |
+
"label": "Google DeepMind WeatherNext Cyclones Mini (<2024)",
|
| 34 |
+
"model_checkpoint": "WeatherNextCyclones_Mini_<2024",
|
| 35 |
+
"software_version": "0.3.0",
|
| 36 |
+
"software_commit": "89c4b2a77a1c57b328b909c575550fd2e5aadc9c",
|
| 37 |
+
"trained_through": 2023,
|
| 38 |
+
"resolution_degrees": 1,
|
| 39 |
+
"member_count": 1,
|
| 40 |
+
"member_policy": "One official Mini member, seed 0",
|
| 41 |
+
"checkpoint_sha256": "a1bb151457077248d70a458b1a7b19deacd2926fbcb46eee4ff2691444e3596e",
|
| 42 |
+
"method": "Official predictor and initialized-storm tracker; causal ERA5; RTX 3070 CUDA",
|
| 43 |
+
"status": "complete_verified"
|
| 44 |
+
},
|
| 45 |
+
"models": [
|
| 46 |
+
"1.1",
|
| 47 |
+
"1.2",
|
| 48 |
+
"deepmind"
|
| 49 |
+
],
|
| 50 |
+
"bars_per_panel": null,
|
| 51 |
+
"available_bars_per_panel": {
|
| 52 |
+
"pressure_JMA_hpa": 0,
|
| 53 |
+
"mean_track_error_km": 3,
|
| 54 |
+
"direction_error_deg": 3
|
| 55 |
+
},
|
| 56 |
+
"coverage": {
|
| 57 |
+
"pressure_JMA_hpa": {
|
| 58 |
+
"daily_issues": 0,
|
| 59 |
+
"storms": 0
|
| 60 |
+
},
|
| 61 |
+
"mean_track_error_km": {
|
| 62 |
+
"daily_issues": 1336,
|
| 63 |
+
"storms": 230
|
| 64 |
+
},
|
| 65 |
+
"direction_error_deg": {
|
| 66 |
+
"daily_issues": 1336,
|
| 67 |
+
"storms": 230
|
| 68 |
+
}
|
| 69 |
+
},
|
| 70 |
+
"unavailable_metrics": [
|
| 71 |
+
"pressure_JMA_hpa"
|
| 72 |
+
],
|
| 73 |
+
"error_bars": false,
|
| 74 |
+
"zero_fill": false,
|
| 75 |
+
"model_forecasts_modified": false,
|
| 76 |
+
"publication_audit_sha256": "52c767b46452a29341c024f24a774328ed6fd4169f5f28e923a81e01c641b222"
|
| 77 |
+
}
|
evaluation/released_daily/model_1_2_before_2024_benchmark.png
ADDED
|
Git LFS Details
|
evaluation/released_daily/model_1_2_benchmark.json
CHANGED
|
@@ -48,6 +48,26 @@
|
|
| 48 |
"deepmind"
|
| 49 |
],
|
| 50 |
"bars_per_panel": 3,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
"error_bars": false,
|
| 52 |
"zero_fill": false,
|
| 53 |
"model_forecasts_modified": false,
|
|
|
|
| 48 |
"deepmind"
|
| 49 |
],
|
| 50 |
"bars_per_panel": 3,
|
| 51 |
+
"available_bars_per_panel": {
|
| 52 |
+
"pressure_JMA_hpa": 3,
|
| 53 |
+
"mean_track_error_km": 3,
|
| 54 |
+
"direction_error_deg": 3
|
| 55 |
+
},
|
| 56 |
+
"coverage": {
|
| 57 |
+
"pressure_JMA_hpa": {
|
| 58 |
+
"daily_issues": 134,
|
| 59 |
+
"storms": 40
|
| 60 |
+
},
|
| 61 |
+
"mean_track_error_km": {
|
| 62 |
+
"daily_issues": 1473,
|
| 63 |
+
"storms": 270
|
| 64 |
+
},
|
| 65 |
+
"direction_error_deg": {
|
| 66 |
+
"daily_issues": 1473,
|
| 67 |
+
"storms": 270
|
| 68 |
+
}
|
| 69 |
+
},
|
| 70 |
+
"unavailable_metrics": [],
|
| 71 |
"error_bars": false,
|
| 72 |
"zero_fill": false,
|
| 73 |
"model_forecasts_modified": false,
|
evaluation/training_data/trackformer_1_2_provenance.json
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"schema": "trackformer-1.2-training-data-audit-v1",
|
| 3 |
+
"status": "verified",
|
| 4 |
+
"public_version": "1.2",
|
| 5 |
+
"source_checkpoint_version": "1.2.73",
|
| 6 |
+
"source_checkpoint_sha256": "f194a23d3f91ea76ad776dfad942fabd669eeae8b3fd665815463095367e9ee0",
|
| 7 |
+
"inference_weights_sha256": "db49f36e85a3766defc4c172746897a1f783705d1ce8e6f9dfb8e87ae1d902cb",
|
| 8 |
+
"dataset_manifest_sha256": "04fd0d30620137aad20173dcd59e22da2a6c20fae2f17de593e91a59517459c1",
|
| 9 |
+
"verified_archive_files": {
|
| 10 |
+
"track": {
|
| 11 |
+
"file": "track_windows_v13.npz",
|
| 12 |
+
"sha256": "cb8d7291aa8140124ca522feb448a1d44124b018efc4ba1d54824b60a4f73451",
|
| 13 |
+
"bytes": 42755049
|
| 14 |
+
},
|
| 15 |
+
"basin": {
|
| 16 |
+
"file": "basin_all_int8.npz",
|
| 17 |
+
"sha256": "71e5f1bb46c44e5936c506a3f8af021c1fe94c238ed030c05c66e8f342374b6d",
|
| 18 |
+
"bytes": 274115577
|
| 19 |
+
},
|
| 20 |
+
"slp": {
|
| 21 |
+
"file": "basin_slp_atlas_float16.npy",
|
| 22 |
+
"sha256": "9f6a4ec7b2f6beadfa1075af9176d2a653e39a824d504d989e5e56e19c571521",
|
| 23 |
+
"bytes": 111394928
|
| 24 |
+
},
|
| 25 |
+
"pressure": {
|
| 26 |
+
"file": "pressure_hpa.npy",
|
| 27 |
+
"sha256": "16cc1b153aeee5eff4b67337b599892dcd86a5716554596b8031048035d247ba",
|
| 28 |
+
"bytes": 1698356128
|
| 29 |
+
},
|
| 30 |
+
"plan": {
|
| 31 |
+
"file": "plan.json",
|
| 32 |
+
"sha256": "117eb291e385a326191be2f73182ed6dcbf0e9f99745780ac9e49a29def3d8da",
|
| 33 |
+
"bytes": 295005
|
| 34 |
+
},
|
| 35 |
+
"geography": {
|
| 36 |
+
"file": "geography.npz",
|
| 37 |
+
"sha256": "f5471471da4a1e15f85fbfb2529065806281357c8e50cb272fcd2b02e8553b96",
|
| 38 |
+
"bytes": 2547017
|
| 39 |
+
}
|
| 40 |
+
},
|
| 41 |
+
"sources": {
|
| 42 |
+
"track": "NOAA IBTrACS-derived track_windows_v13",
|
| 43 |
+
"basin": "NOAA PSL NCEP/NCAR Reanalysis 1",
|
| 44 |
+
"native_pressure": "https://storage.googleapis.com/gcp-public-data-arco-era5/ar/full_37-1h-0p25deg-chunk-1.zarr-v3",
|
| 45 |
+
"geography": "https://storage.googleapis.com/gcp-public-data-arco-era5/ar/full_37-1h-0p25deg-chunk-1.zarr-v3",
|
| 46 |
+
"raw_ibtracs_release": "Not recorded in the selected dataset manifest; derived archive hash is authoritative"
|
| 47 |
+
},
|
| 48 |
+
"channels": [
|
| 49 |
+
"mslp",
|
| 50 |
+
"hgt500",
|
| 51 |
+
"uwnd850",
|
| 52 |
+
"vwnd850",
|
| 53 |
+
"uwnd500",
|
| 54 |
+
"vwnd500",
|
| 55 |
+
"uwnd200",
|
| 56 |
+
"vwnd200"
|
| 57 |
+
],
|
| 58 |
+
"normalization_fit_years": [
|
| 59 |
+
2000,
|
| 60 |
+
2021
|
| 61 |
+
],
|
| 62 |
+
"partitions": {
|
| 63 |
+
"train": {
|
| 64 |
+
"years": [
|
| 65 |
+
2000,
|
| 66 |
+
2021
|
| 67 |
+
],
|
| 68 |
+
"windows": 13949,
|
| 69 |
+
"storms": 611,
|
| 70 |
+
"native_pressure_windows": 800,
|
| 71 |
+
"first_issue_utc": "2000-05-05T18:00:00Z",
|
| 72 |
+
"last_issue_utc": "2021-12-20T12:00:00Z",
|
| 73 |
+
"earliest_history_utc": "2000-05-03T18:00:00Z",
|
| 74 |
+
"latest_target_utc": "2021-12-25T12:00:00Z"
|
| 75 |
+
},
|
| 76 |
+
"validation": {
|
| 77 |
+
"years": [
|
| 78 |
+
2022,
|
| 79 |
+
2023
|
| 80 |
+
],
|
| 81 |
+
"windows": 1041,
|
| 82 |
+
"storms": 46,
|
| 83 |
+
"native_pressure_windows": 100,
|
| 84 |
+
"first_issue_utc": "2022-03-31T00:00:00Z",
|
| 85 |
+
"last_issue_utc": "2023-12-17T18:00:00Z",
|
| 86 |
+
"earliest_history_utc": "2022-03-29T00:00:00Z",
|
| 87 |
+
"latest_target_utc": "2023-12-22T18:00:00Z"
|
| 88 |
+
},
|
| 89 |
+
"test": {
|
| 90 |
+
"years": [
|
| 91 |
+
2024,
|
| 92 |
+
2025
|
| 93 |
+
],
|
| 94 |
+
"windows": 1195,
|
| 95 |
+
"storms": 56,
|
| 96 |
+
"native_pressure_windows": 100,
|
| 97 |
+
"first_issue_utc": "2024-05-22T00:00:00Z",
|
| 98 |
+
"last_issue_utc": "2025-12-02T12:00:00Z",
|
| 99 |
+
"earliest_history_utc": "2024-05-20T00:00:00Z",
|
| 100 |
+
"latest_target_utc": "2025-12-07T12:00:00Z"
|
| 101 |
+
}
|
| 102 |
+
},
|
| 103 |
+
"whole_storm_splits": true,
|
| 104 |
+
"boundary_crossing_windows_excluded": true,
|
| 105 |
+
"history_leads_hours": [
|
| 106 |
+
-48,
|
| 107 |
+
-42,
|
| 108 |
+
-36,
|
| 109 |
+
-30,
|
| 110 |
+
-24,
|
| 111 |
+
-18,
|
| 112 |
+
-12,
|
| 113 |
+
-6,
|
| 114 |
+
0
|
| 115 |
+
],
|
| 116 |
+
"target_leads_hours": [
|
| 117 |
+
6,
|
| 118 |
+
12,
|
| 119 |
+
18,
|
| 120 |
+
24,
|
| 121 |
+
30,
|
| 122 |
+
36,
|
| 123 |
+
42,
|
| 124 |
+
48,
|
| 125 |
+
54,
|
| 126 |
+
60,
|
| 127 |
+
66,
|
| 128 |
+
72,
|
| 129 |
+
78,
|
| 130 |
+
84,
|
| 131 |
+
90,
|
| 132 |
+
96,
|
| 133 |
+
102,
|
| 134 |
+
108,
|
| 135 |
+
114,
|
| 136 |
+
120
|
| 137 |
+
],
|
| 138 |
+
"upstream_cache_coverage": {
|
| 139 |
+
"track": {
|
| 140 |
+
"first_issue_utc": "1980-01-03T00:00:00Z",
|
| 141 |
+
"last_issue_utc": "2026-07-13T00:00:00Z"
|
| 142 |
+
},
|
| 143 |
+
"basin": {
|
| 144 |
+
"first_analysis_utc": "1980-01-01T00:00:00Z",
|
| 145 |
+
"last_analysis_utc": "2026-03-17T18:00:00Z"
|
| 146 |
+
}
|
| 147 |
+
},
|
| 148 |
+
"cutoff_interpretation": "Fitting through 2021; validation through 2023; original test through 2025. Newer cache/live records are not gradient fitting.",
|
| 149 |
+
"inference_performed": false,
|
| 150 |
+
"model_weights_modified": false,
|
| 151 |
+
"training_data_modified": false,
|
| 152 |
+
"audit_scope": "All six derived archive file hashes, release manifest identity and loader-equivalent split/patch membership. Not a new raw-download or operational-availability audit."
|
| 153 |
+
}
|
models/trackformer_1_2_field/README.md
CHANGED
|
@@ -4,6 +4,8 @@ The released pressure-field model predicts Western Pacific storm tracks, central
|
|
| 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:
|
| 8 |
|
| 9 |
```bash
|
|
|
|
| 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 |
+
**Training-data cutoff:** fitting and weather normalization use **2000–2021**; validation/checkpoint selection uses **2022–2023**; the original test partition uses **2024–2025**. Inputs come from IBTrACS-derived storm records, NOAA PSL NCEP/NCAR Reanalysis basin fields, ARCO-ERA5 regional pressure and static land/elevation. Newer 2026 archive/live inputs are inference, not retraining. [Sources and exact split coverage](../../docs/trackformer_1_2_training_data.md) · [Verified training-data receipt](../../evaluation/training_data/trackformer_1_2_provenance.json).
|
| 8 |
+
|
| 9 |
Use Python with PyTorch and NumPy. The export environment used PyTorch 2.13.0; the source was evaluated on macOS MPS. Example:
|
| 10 |
|
| 11 |
```bash
|
release_tools/audit_training_data.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Read-only audit of the released 1.2 dataset; no inference or cache refresh.
|
| 2 |
+
|
| 3 |
+
Requires the original checksum-pinned local dataset, not new downloads. Writes
|
| 4 |
+
only the requested small provenance receipt on /Volumes/D. Unlike the training
|
| 5 |
+
loader, this does not rewrite its verified basin mmap or initialize PyTorch.
|
| 6 |
+
"""
|
| 7 |
+
import argparse
|
| 8 |
+
from collections import Counter
|
| 9 |
+
import hashlib
|
| 10 |
+
import json
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
import numpy as np
|
| 14 |
+
|
| 15 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 16 |
+
HOUR = 3_600_000_000_000
|
| 17 |
+
SPLITS = {'train': (2000, 2021, 13949, 800),
|
| 18 |
+
'validation': (2022, 2023, 1041, 100),
|
| 19 |
+
'test': (2024, 2025, 1195, 100)}
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def sha(path):
|
| 23 |
+
with path.open('rb') as stream:
|
| 24 |
+
return hashlib.file_digest(stream, 'sha256').hexdigest()
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def utc(ns):
|
| 28 |
+
return str(np.datetime64(int(ns), 'ns').astype('datetime64[s]')) + 'Z'
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def audit(dataset_root):
|
| 32 |
+
released = json.loads((ROOT / 'models/trackformer_1_2_field/manifest.json').read_text())
|
| 33 |
+
path = dataset_root / 'manifest.json'
|
| 34 |
+
if sha(path) != released['dataset_sha256']:
|
| 35 |
+
raise ValueError('Dataset manifest differs from the released checkpoint')
|
| 36 |
+
data = json.loads(path.read_text())
|
| 37 |
+
if data['schema'] != 'v164-reuse-pressure-v1' or data['pilot']:
|
| 38 |
+
raise ValueError('Exact full dataset required')
|
| 39 |
+
files = {}
|
| 40 |
+
for key, entry in data['files'].items():
|
| 41 |
+
local = dataset_root / entry['path']
|
| 42 |
+
actual = sha(local)
|
| 43 |
+
if actual != entry['sha256']:
|
| 44 |
+
raise ValueError('Dataset file checksum differs: ' + key)
|
| 45 |
+
files[key] = dict(file=entry['path'], sha256=actual, bytes=local.stat().st_size)
|
| 46 |
+
with np.load(dataset_root / files['track']['file'], allow_pickle=False) as z:
|
| 47 |
+
times, storms = z['base_time'], z['storm_id']
|
| 48 |
+
lat, lon = z['base_lat'], z['base_lon'] % 360
|
| 49 |
+
with np.load(dataset_root / files['basin']['file'], allow_pickle=False) as z:
|
| 50 |
+
atlas, channels = z['time'], z['channels'].tolist()
|
| 51 |
+
if channels != released['data_contract']['channels'][1:]:
|
| 52 |
+
raise ValueError('Basin channel order differs')
|
| 53 |
+
if not np.all(np.diff(atlas) == 6 * HOUR):
|
| 54 |
+
raise ValueError('Expected exact regular six-hour atlas')
|
| 55 |
+
years = times.astype('datetime64[ns]').astype('datetime64[Y]').astype(int) + 1970
|
| 56 |
+
first = {}
|
| 57 |
+
for sid, year in zip(storms, years):
|
| 58 |
+
first[str(sid)] = min(first.get(str(sid), 9999), int(year))
|
| 59 |
+
first_years = np.array([first[str(sid)] for sid in storms])
|
| 60 |
+
bounds = np.stack((times - 48 * HOUR, times + 120 * HOUR), axis=1)
|
| 61 |
+
bound_years = bounds.astype('datetime64[ns]').astype('datetime64[Y]').astype(int) + 1970
|
| 62 |
+
indices = np.searchsorted(atlas, times)
|
| 63 |
+
eligible = ((indices >= 8) & (indices + 20 < len(atlas)) &
|
| 64 |
+
(atlas[np.clip(indices, 0, len(atlas) - 1)] == times) &
|
| 65 |
+
np.isfinite(lat) & np.isfinite(lon) &
|
| 66 |
+
(lat > 0) & (lat < 60) & (lon > 100) & (lon < 180))
|
| 67 |
+
plan = json.loads((dataset_root / files['plan']['file']).read_text())
|
| 68 |
+
patches = Counter(row['split'] for row in plan['rows'])
|
| 69 |
+
if plan['shape'] != [1000, 29, 121, 121] or plan['lead_hours'] != list(range(-48, 121, 6)):
|
| 70 |
+
raise ValueError('Native pressure package shape/time mismatch')
|
| 71 |
+
split_report, split_ids = {}, {}
|
| 72 |
+
for name, (low, high, expected, native) in SPLITS.items():
|
| 73 |
+
mask = (eligible & (first_years >= low) & (first_years <= high) &
|
| 74 |
+
(bound_years[:, 0] >= low) & (bound_years[:, 1] <= high))
|
| 75 |
+
if int(mask.sum()) != expected or patches[name] != native:
|
| 76 |
+
raise ValueError('Split counts differ: ' + name)
|
| 77 |
+
split_ids[name] = set(storms[mask])
|
| 78 |
+
rows = set(np.flatnonzero(mask).tolist())
|
| 79 |
+
for patch in (r for r in plan['rows'] if r['split'] == name):
|
| 80 |
+
i = patch['track_archive_row']
|
| 81 |
+
if i not in rows or patch['issue_ns'] != int(times[i]) or patch['storm_id'] != str(storms[i]):
|
| 82 |
+
raise ValueError('Native patch is not in the matching original split')
|
| 83 |
+
split_report[name] = dict(years=[low, high], windows=expected,
|
| 84 |
+
storms=len(split_ids[name]), native_pressure_windows=native,
|
| 85 |
+
first_issue_utc=utc(times[mask].min()), last_issue_utc=utc(times[mask].max()),
|
| 86 |
+
earliest_history_utc=utc(bounds[mask, 0].min()),
|
| 87 |
+
latest_target_utc=utc(bounds[mask, 1].max()))
|
| 88 |
+
if any(split_ids[a] & split_ids[b] for a, b in (('train', 'validation'), ('train', 'test'), ('validation', 'test'))):
|
| 89 |
+
raise ValueError('A storm appears in multiple partitions')
|
| 90 |
+
norm = released['data_contract']['normalization']
|
| 91 |
+
if norm['fit_split'] != 'train' or norm['end_year'] != 2021:
|
| 92 |
+
raise ValueError('Released normalization cutoff differs')
|
| 93 |
+
return dict(schema='trackformer-1.2-training-data-audit-v1', status='verified',
|
| 94 |
+
public_version='1.2', source_checkpoint_version=released['source_checkpoint_version'],
|
| 95 |
+
source_checkpoint_sha256=released['source_checkpoint_sha256'],
|
| 96 |
+
inference_weights_sha256=released['inference_weights_sha256'],
|
| 97 |
+
dataset_manifest_sha256=released['dataset_sha256'], verified_archive_files=files,
|
| 98 |
+
sources=dict(track='NOAA IBTrACS-derived track_windows_v13',
|
| 99 |
+
basin='NOAA PSL NCEP/NCAR Reanalysis 1',
|
| 100 |
+
native_pressure=plan['source'], geography=plan['source'],
|
| 101 |
+
raw_ibtracs_release='Not recorded in the selected dataset manifest; derived archive hash is authoritative'),
|
| 102 |
+
channels=released['data_contract']['channels'], normalization_fit_years=[2000, 2021],
|
| 103 |
+
partitions=split_report, whole_storm_splits=True, boundary_crossing_windows_excluded=True,
|
| 104 |
+
history_leads_hours=list(range(-48, 1, 6)), target_leads_hours=list(range(6, 121, 6)),
|
| 105 |
+
upstream_cache_coverage=dict(
|
| 106 |
+
track=dict(first_issue_utc=utc(times.min()), last_issue_utc=utc(times.max())),
|
| 107 |
+
basin=dict(first_analysis_utc=utc(atlas.min()), last_analysis_utc=utc(atlas.max()))),
|
| 108 |
+
cutoff_interpretation='Fitting through 2021; validation through 2023; original test through 2025. Newer cache/live records are not gradient fitting.',
|
| 109 |
+
inference_performed=False, model_weights_modified=False, training_data_modified=False,
|
| 110 |
+
audit_scope='All six derived archive file hashes, release manifest identity and loader-equivalent split/patch membership. Not a new raw-download or operational-availability audit.')
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
if __name__ == '__main__':
|
| 114 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 115 |
+
parser.add_argument('--dataset-root', type=Path, required=True)
|
| 116 |
+
parser.add_argument('--output', type=Path, required=True)
|
| 117 |
+
args = parser.parse_args()
|
| 118 |
+
output = args.output.resolve()
|
| 119 |
+
if not output.is_relative_to(Path('/Volumes/D')):
|
| 120 |
+
raise ValueError('Diagnostic output must remain on /Volumes/D')
|
| 121 |
+
report = audit(args.dataset_root.resolve())
|
| 122 |
+
output.parent.mkdir(parents=True, exist_ok=True)
|
| 123 |
+
output.write_text(json.dumps(report, indent=2, allow_nan=False) + '\n')
|
| 124 |
+
print(json.dumps({'status': report['status'], 'partitions': report['partitions'], 'output': str(output)}))
|
release_tools/plot_model_announcement.py
CHANGED
|
@@ -12,6 +12,21 @@ SOURCE = ROOT/'evaluation/released_daily/released_daily_benchmark.json'
|
|
| 12 |
DEST = ROOT/'evaluation/released_daily/model_1_2_benchmark.png'
|
| 13 |
PERIOD_SOURCE = ROOT/'evaluation/deepmind_daily/period_comparison.json'
|
| 14 |
POST_DEST = ROOT/'evaluation/released_daily/model_1_2_after_2024_benchmark.png'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
|
| 17 |
def period_metrics(period):
|
|
@@ -42,6 +57,7 @@ def main():
|
|
| 42 |
if periods['model'] != reference or not periods['single_checkpoint_across_all_periods']:
|
| 43 |
raise ValueError('Period groups are not different checkpoint generations')
|
| 44 |
for key, dest, title in (('total', DEST, 'Total · mixed-year comparison'),
|
|
|
|
| 45 |
('after_2024', POST_DEST, 'After 2024 · 2025–2026 comparison')):
|
| 46 |
metrics = period_metrics(periods['periods'][key])
|
| 47 |
draw_chart(metrics, reference, periods, key, dest, title)
|
|
@@ -60,9 +76,20 @@ def draw_chart(metrics, reference, periods, period_key, dest, title):
|
|
| 60 |
for ax, (key, panel_title, label, maximum, digits) in zip(axes, specs):
|
| 61 |
row = metrics[key]
|
| 62 |
values = [row['values'][k] for k in models]
|
| 63 |
-
if any(v is None or not np.isfinite(v) or v < 0 for v in values):
|
| 64 |
-
raise ValueError('Missing or invalid metric cannot become a zero bar')
|
| 65 |
ax.set_facecolor('#f6f8fc')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
ax.set_axisbelow(True)
|
| 67 |
ax.grid(axis='y', color='#dfe5ee', linewidth=.8)
|
| 68 |
ax.bar([0, 1, 2], values, width=.55, color=['#9aa9bc', '#3c67d6', '#9073cf'], zorder=3)
|
|
@@ -72,8 +99,6 @@ def draw_chart(metrics, reference, periods, period_key, dest, title):
|
|
| 72 |
ax.set_ylim(0, maximum)
|
| 73 |
ax.set_xticks([0, 1, 2], ['1.1', '1.2\nmean of 50', 'DeepMind Mini\n<2024 · 1 member'])
|
| 74 |
ax.tick_params(axis='both', length=0, labelcolor='#53647c')
|
| 75 |
-
ax.set_title(panel_title, loc='left', fontsize=16, fontweight='bold', color='#21334c', pad=38)
|
| 76 |
-
ax.text(0, 1.03, label, transform=ax.transAxes, color='#53647c', fontsize=10)
|
| 77 |
coverage = row['coverage']
|
| 78 |
ax.text(.5, -.33, f"{coverage['daily_issues']:,} daily starts / {coverage['storms']} storms",
|
| 79 |
transform=ax.transAxes, ha='center', color='#53647c', fontsize=10)
|
|
@@ -82,6 +107,8 @@ def draw_chart(metrics, reference, periods, period_key, dest, title):
|
|
| 82 |
fig.text(.055, .84, 'Same saved forecasts and valid times · +6 to +120 h · equal storm weight · lower is better', fontsize=11, color='#53647c')
|
| 83 |
if period_key == 'total':
|
| 84 |
note = 'Storm proportions: <2024 85.2% · 2024 8.1% · >2024 6.7%. Historical dates overlap Mini fitting years.'
|
|
|
|
|
|
|
| 85 |
else:
|
| 86 |
note = 'Strict UTC issue year >2024: 61 starts / 18 storms. Calendar 2024 is excluded, not counted as >2024.'
|
| 87 |
fig.text(.055, .09, note, fontsize=10, color='#53647c')
|
|
@@ -94,7 +121,12 @@ def draw_chart(metrics, reference, periods, period_key, dest, title):
|
|
| 94 |
'period': period_key, 'period_definition': periods['period_definition'][period_key],
|
| 95 |
'metrics': [s[0] for s in specs], 'values': {k: metrics[k]['values'] for k, *_ in specs},
|
| 96 |
'deepmind_status': 'complete_verified', 'deepmind_model': reference,
|
| 97 |
-
'models': list(models),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
'zero_fill': False, 'model_forecasts_modified': False,
|
| 99 |
'publication_audit_sha256': hashlib.sha256((ROOT/'evaluation/deepmind_daily/publication_audit.json').read_bytes()).hexdigest()}
|
| 100 |
dest.with_suffix('.json').write_text(json.dumps(receipt, indent=2)+'\n')
|
|
|
|
| 12 |
DEST = ROOT/'evaluation/released_daily/model_1_2_benchmark.png'
|
| 13 |
PERIOD_SOURCE = ROOT/'evaluation/deepmind_daily/period_comparison.json'
|
| 14 |
POST_DEST = ROOT/'evaluation/released_daily/model_1_2_after_2024_benchmark.png'
|
| 15 |
+
PRE_DEST = ROOT/'evaluation/released_daily/model_1_2_before_2024_benchmark.png'
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def metric_available(row):
|
| 19 |
+
"""A zero-support metric is unavailable, never an error of zero."""
|
| 20 |
+
values = list(row['values'].values())
|
| 21 |
+
coverage = row['coverage']
|
| 22 |
+
if coverage['daily_issues'] == 0 and coverage['storms'] == 0:
|
| 23 |
+
if not all(v is None for v in values):
|
| 24 |
+
raise ValueError('Zero support requires null scores, not numeric bars')
|
| 25 |
+
return False
|
| 26 |
+
if coverage['daily_issues'] <= 0 or coverage['storms'] <= 0 or any(
|
| 27 |
+
v is None or not np.isfinite(v) or v < 0 for v in values):
|
| 28 |
+
raise ValueError('Missing or invalid metric cannot become a zero bar')
|
| 29 |
+
return True
|
| 30 |
|
| 31 |
|
| 32 |
def period_metrics(period):
|
|
|
|
| 57 |
if periods['model'] != reference or not periods['single_checkpoint_across_all_periods']:
|
| 58 |
raise ValueError('Period groups are not different checkpoint generations')
|
| 59 |
for key, dest, title in (('total', DEST, 'Total · mixed-year comparison'),
|
| 60 |
+
('before_2024', PRE_DEST, 'Before 2024 · historical comparison'),
|
| 61 |
('after_2024', POST_DEST, 'After 2024 · 2025–2026 comparison')):
|
| 62 |
metrics = period_metrics(periods['periods'][key])
|
| 63 |
draw_chart(metrics, reference, periods, key, dest, title)
|
|
|
|
| 76 |
for ax, (key, panel_title, label, maximum, digits) in zip(axes, specs):
|
| 77 |
row = metrics[key]
|
| 78 |
values = [row['values'][k] for k in models]
|
|
|
|
|
|
|
| 79 |
ax.set_facecolor('#f6f8fc')
|
| 80 |
+
ax.set_title(panel_title, loc='left', fontsize=16, fontweight='bold', color='#21334c', pad=38)
|
| 81 |
+
ax.text(0, 1.03, label, transform=ax.transAxes, color='#53647c', fontsize=10)
|
| 82 |
+
if not metric_available(row):
|
| 83 |
+
ax.set_xticks([])
|
| 84 |
+
ax.set_yticks([])
|
| 85 |
+
ax.text(.5, .65, 'Not scored', transform=ax.transAxes, ha='center',
|
| 86 |
+
fontsize=22, fontweight='bold', color='#53647c')
|
| 87 |
+
ax.text(.5, .35, 'No shared valid JMA pressure cases.\nFrozen 1.1 intensity inputs unavailable.\nMissing is not a zero error.',
|
| 88 |
+
transform=ax.transAxes, ha='center', va='center',
|
| 89 |
+
fontsize=11, linespacing=1.65, color='#53647c')
|
| 90 |
+
ax.text(.5, -.33, '0 shared pressure starts / 0 storms · no bars',
|
| 91 |
+
transform=ax.transAxes, ha='center', color='#53647c', fontsize=10)
|
| 92 |
+
continue
|
| 93 |
ax.set_axisbelow(True)
|
| 94 |
ax.grid(axis='y', color='#dfe5ee', linewidth=.8)
|
| 95 |
ax.bar([0, 1, 2], values, width=.55, color=['#9aa9bc', '#3c67d6', '#9073cf'], zorder=3)
|
|
|
|
| 99 |
ax.set_ylim(0, maximum)
|
| 100 |
ax.set_xticks([0, 1, 2], ['1.1', '1.2\nmean of 50', 'DeepMind Mini\n<2024 · 1 member'])
|
| 101 |
ax.tick_params(axis='both', length=0, labelcolor='#53647c')
|
|
|
|
|
|
|
| 102 |
coverage = row['coverage']
|
| 103 |
ax.text(.5, -.33, f"{coverage['daily_issues']:,} daily starts / {coverage['storms']} storms",
|
| 104 |
transform=ax.transAxes, ha='center', color='#53647c', fontsize=10)
|
|
|
|
| 107 |
fig.text(.055, .84, 'Same saved forecasts and valid times · +6 to +120 h · equal storm weight · lower is better', fontsize=11, color='#53647c')
|
| 108 |
if period_key == 'total':
|
| 109 |
note = 'Storm proportions: <2024 85.2% · 2024 8.1% · >2024 6.7%. Historical dates overlap Mini fitting years.'
|
| 110 |
+
elif period_key == 'before_2024':
|
| 111 |
+
note = 'Strict UTC issue year <2024: 1,336 starts / 230 storms, all 1980–1999 here. These dates overlap Mini fitting years.'
|
| 112 |
else:
|
| 113 |
note = 'Strict UTC issue year >2024: 61 starts / 18 storms. Calendar 2024 is excluded, not counted as >2024.'
|
| 114 |
fig.text(.055, .09, note, fontsize=10, color='#53647c')
|
|
|
|
| 121 |
'period': period_key, 'period_definition': periods['period_definition'][period_key],
|
| 122 |
'metrics': [s[0] for s in specs], 'values': {k: metrics[k]['values'] for k, *_ in specs},
|
| 123 |
'deepmind_status': 'complete_verified', 'deepmind_model': reference,
|
| 124 |
+
'models': list(models),
|
| 125 |
+
'bars_per_panel': 3 if all(metric_available(r) for r in metrics.values()) else None,
|
| 126 |
+
'available_bars_per_panel': {k: 3 if metric_available(r) else 0 for k, r in metrics.items()},
|
| 127 |
+
'coverage': {k: r['coverage'] for k, r in metrics.items()},
|
| 128 |
+
'unavailable_metrics': [k for k, r in metrics.items() if not metric_available(r)],
|
| 129 |
+
'error_bars': False,
|
| 130 |
'zero_fill': False, 'model_forecasts_modified': False,
|
| 131 |
'publication_audit_sha256': hashlib.sha256((ROOT/'evaluation/deepmind_daily/publication_audit.json').read_bytes()).hexdigest()}
|
| 132 |
dest.with_suffix('.json').write_text(json.dumps(receipt, indent=2)+'\n')
|
release_tools/sync_public_model_cards.py
CHANGED
|
@@ -24,6 +24,7 @@ FILMS = ('mangkhut',)
|
|
| 24 |
CURRENT_FIGURES = frozenset((
|
| 25 |
'evaluation/released_daily/model_1_2_benchmark.png',
|
| 26 |
'evaluation/released_daily/model_1_2_after_2024_benchmark.png',
|
|
|
|
| 27 |
'docs/trackformer_1_2_architecture.svg',
|
| 28 |
))
|
| 29 |
SYNC_FILES = (
|
|
@@ -51,6 +52,9 @@ SYNC_FILES = (
|
|
| 51 |
'docs/daily_storm_benchmark.md',
|
| 52 |
'docs/intensity_benchmark.md',
|
| 53 |
'docs/trackformer_1_2_evaluation.md',
|
|
|
|
|
|
|
|
|
|
| 54 |
'docs/deepmind_daily_benchmark.md',
|
| 55 |
'docs/showcase_archive.md',
|
| 56 |
'evaluation/README.md',
|
|
@@ -68,6 +72,8 @@ SYNC_FILES = (
|
|
| 68 |
'evaluation/released_daily/model_1_2_benchmark.json',
|
| 69 |
'evaluation/released_daily/model_1_2_after_2024_benchmark.png',
|
| 70 |
'evaluation/released_daily/model_1_2_after_2024_benchmark.json',
|
|
|
|
|
|
|
| 71 |
'evaluation/deepmind_daily/benchmark.json',
|
| 72 |
'evaluation/deepmind_daily/verification.json',
|
| 73 |
'evaluation/deepmind_daily/publication_audit.json',
|
|
@@ -187,13 +193,13 @@ def run(output, publish=False):
|
|
| 187 |
# immutable commit in the card. Never pin new bytes to the old movie
|
| 188 |
# revision or let a missing future image render as a stale figure.
|
| 189 |
assets = api.create_commit(repo_id=REPO, repo_type='model', parent_commit=parent,
|
| 190 |
-
commit_message='
|
| 191 |
operations=[CommitOperationAdd(path_in_repo=name, path_or_fileobj=str(ROOT/name)) for name in SYNC_FILES])
|
| 192 |
card_path.write_text(render_card(original.read_text(), github, assets.oid))
|
| 193 |
receipt['files']['README.md'] = sha(card_path)
|
| 194 |
receipt['figure_revision'] = assets.oid
|
| 195 |
committed = api.create_commit(repo_id=REPO, repo_type='model', parent_commit=assets.oid,
|
| 196 |
-
commit_message='Show
|
| 197 |
operations=[CommitOperationAdd(path_in_repo='README.md', path_or_fileobj=str(card_path))])
|
| 198 |
for name, expected in receipt['files'].items():
|
| 199 |
saved = hf_hub_download(REPO, name, revision=committed.oid, cache_dir=CACHE)
|
|
|
|
| 24 |
CURRENT_FIGURES = frozenset((
|
| 25 |
'evaluation/released_daily/model_1_2_benchmark.png',
|
| 26 |
'evaluation/released_daily/model_1_2_after_2024_benchmark.png',
|
| 27 |
+
'evaluation/released_daily/model_1_2_before_2024_benchmark.png',
|
| 28 |
'docs/trackformer_1_2_architecture.svg',
|
| 29 |
))
|
| 30 |
SYNC_FILES = (
|
|
|
|
| 52 |
'docs/daily_storm_benchmark.md',
|
| 53 |
'docs/intensity_benchmark.md',
|
| 54 |
'docs/trackformer_1_2_evaluation.md',
|
| 55 |
+
'docs/trackformer_1_2_training_data.md',
|
| 56 |
+
'evaluation/training_data/trackformer_1_2_provenance.json',
|
| 57 |
+
'release_tools/audit_training_data.py',
|
| 58 |
'docs/deepmind_daily_benchmark.md',
|
| 59 |
'docs/showcase_archive.md',
|
| 60 |
'evaluation/README.md',
|
|
|
|
| 72 |
'evaluation/released_daily/model_1_2_benchmark.json',
|
| 73 |
'evaluation/released_daily/model_1_2_after_2024_benchmark.png',
|
| 74 |
'evaluation/released_daily/model_1_2_after_2024_benchmark.json',
|
| 75 |
+
'evaluation/released_daily/model_1_2_before_2024_benchmark.png',
|
| 76 |
+
'evaluation/released_daily/model_1_2_before_2024_benchmark.json',
|
| 77 |
'evaluation/deepmind_daily/benchmark.json',
|
| 78 |
'evaluation/deepmind_daily/verification.json',
|
| 79 |
'evaluation/deepmind_daily/publication_audit.json',
|
|
|
|
| 193 |
# immutable commit in the card. Never pin new bytes to the old movie
|
| 194 |
# revision or let a missing future image render as a stale figure.
|
| 195 |
assets = api.create_commit(repo_id=REPO, repo_type='model', parent_commit=parent,
|
| 196 |
+
commit_message='Add pre-2024 chart and verified Trackformer 1.2 training sources and cutoffs',
|
| 197 |
operations=[CommitOperationAdd(path_in_repo=name, path_or_fileobj=str(ROOT/name)) for name in SYNC_FILES])
|
| 198 |
card_path.write_text(render_card(original.read_text(), github, assets.oid))
|
| 199 |
receipt['files']['README.md'] = sha(card_path)
|
| 200 |
receipt['figure_revision'] = assets.oid
|
| 201 |
committed = api.create_commit(repo_id=REPO, repo_type='model', parent_commit=assets.oid,
|
| 202 |
+
commit_message='Show total, pre-2024 and post-2024 results with explicit 1.2 training years',
|
| 203 |
operations=[CommitOperationAdd(path_in_repo='README.md', path_or_fileobj=str(card_path))])
|
| 204 |
for name, expected in receipt['files'].items():
|
| 205 |
saved = hf_hub_download(REPO, name, revision=committed.oid, cache_dir=CACHE)
|
release_tools/test_deepmind_period_comparison.py
CHANGED
|
@@ -5,7 +5,7 @@ import unittest
|
|
| 5 |
|
| 6 |
from build_deepmind_period_comparison import FOLDER, MODELS, ROOT, period_key, summarize
|
| 7 |
from import_deepmind_release_results import same
|
| 8 |
-
from plot_model_announcement import period_metrics
|
| 9 |
|
| 10 |
|
| 11 |
class DeepMindPeriodComparisonTest(unittest.TestCase):
|
|
@@ -85,7 +85,9 @@ class DeepMindPeriodComparisonTest(unittest.TestCase):
|
|
| 85 |
self.assertEqual(self.report['missing_results_scored_as_zero'], 0)
|
| 86 |
|
| 87 |
def test_chart_receipts_match_exact_data_and_bytes(self):
|
| 88 |
-
for name, key in (('model_1_2_benchmark', 'total'),
|
|
|
|
|
|
|
| 89 |
base = ROOT / 'evaluation/released_daily' / name
|
| 90 |
receipt = json.loads(base.with_suffix('.json').read_text())
|
| 91 |
self.assertEqual(receipt['period'], key)
|
|
@@ -95,6 +97,21 @@ class DeepMindPeriodComparisonTest(unittest.TestCase):
|
|
| 95 |
expected = period_metrics(self.report['periods'][key])
|
| 96 |
self.assertEqual(receipt['values'], {k: v['values'] for k, v in expected.items()})
|
| 97 |
self.assertFalse(receipt['zero_fill'])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
|
| 99 |
|
| 100 |
if __name__ == '__main__':
|
|
|
|
| 5 |
|
| 6 |
from build_deepmind_period_comparison import FOLDER, MODELS, ROOT, period_key, summarize
|
| 7 |
from import_deepmind_release_results import same
|
| 8 |
+
from plot_model_announcement import metric_available, period_metrics
|
| 9 |
|
| 10 |
|
| 11 |
class DeepMindPeriodComparisonTest(unittest.TestCase):
|
|
|
|
| 85 |
self.assertEqual(self.report['missing_results_scored_as_zero'], 0)
|
| 86 |
|
| 87 |
def test_chart_receipts_match_exact_data_and_bytes(self):
|
| 88 |
+
for name, key in (('model_1_2_benchmark', 'total'),
|
| 89 |
+
('model_1_2_before_2024_benchmark', 'before_2024'),
|
| 90 |
+
('model_1_2_after_2024_benchmark', 'after_2024')):
|
| 91 |
base = ROOT / 'evaluation/released_daily' / name
|
| 92 |
receipt = json.loads(base.with_suffix('.json').read_text())
|
| 93 |
self.assertEqual(receipt['period'], key)
|
|
|
|
| 97 |
expected = period_metrics(self.report['periods'][key])
|
| 98 |
self.assertEqual(receipt['values'], {k: v['values'] for k, v in expected.items()})
|
| 99 |
self.assertFalse(receipt['zero_fill'])
|
| 100 |
+
self.assertEqual(receipt['available_bars_per_panel'],
|
| 101 |
+
{k: 3 if metric_available(v) else 0 for k, v in expected.items()})
|
| 102 |
+
self.assertEqual(receipt['coverage'], {k: v['coverage'] for k, v in expected.items()})
|
| 103 |
+
if key == 'before_2024':
|
| 104 |
+
self.assertIsNone(receipt['bars_per_panel'])
|
| 105 |
+
self.assertEqual(receipt['unavailable_metrics'], ['pressure_JMA_hpa'])
|
| 106 |
+
|
| 107 |
+
def test_unavailable_panel_is_not_zero_fill_or_invalid_numeric_score(self):
|
| 108 |
+
empty = {'values': {m: None for m in MODELS}, 'coverage': {'daily_issues': 0, 'storms': 0}}
|
| 109 |
+
self.assertFalse(metric_available(empty))
|
| 110 |
+
for values, coverage in (({m: 0 for m in MODELS}, empty['coverage']),
|
| 111 |
+
(empty['values'], {'daily_issues': 1, 'storms': 1}),
|
| 112 |
+
({m: float('nan') for m in MODELS}, {'daily_issues': 1, 'storms': 1})):
|
| 113 |
+
with self.assertRaises(ValueError):
|
| 114 |
+
metric_available({'values': values, 'coverage': coverage})
|
| 115 |
|
| 116 |
|
| 117 |
if __name__ == '__main__':
|
release_tools/test_public_model_cards.py
CHANGED
|
@@ -156,6 +156,7 @@ class PublicModelCardsTest(unittest.TestCase):
|
|
| 156 |
for source in (self.source, render_card(self.original, self.source)):
|
| 157 |
self.assertLess(source.index('**DeepMind checkpoint:'), source.index('model_1_2_benchmark.png'))
|
| 158 |
self.assertEqual(source.count('model_1_2_benchmark.png'), 1)
|
|
|
|
| 159 |
self.assertEqual(source.count('model_1_2_after_2024_benchmark.png'), 1)
|
| 160 |
for text in ('90.7%', '85.2%', '5.2%', '8.1%', '4.1%', '6.7%',
|
| 161 |
'Calendar 2024', '59 days / 18 storms / 1,174',
|
|
@@ -163,6 +164,45 @@ class PublicModelCardsTest(unittest.TestCase):
|
|
| 163 |
'100% Mini `<2024`; 0% other checkpoints'):
|
| 164 |
self.assertIn(text, source)
|
| 165 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
def test_evaluation_index_and_worker_handoff_metadata_are_explained(self):
|
| 167 |
index = (ROOT / 'evaluation/README.md').read_text()
|
| 168 |
self.assertIn('completed three-model comparison', index)
|
|
|
|
| 156 |
for source in (self.source, render_card(self.original, self.source)):
|
| 157 |
self.assertLess(source.index('**DeepMind checkpoint:'), source.index('model_1_2_benchmark.png'))
|
| 158 |
self.assertEqual(source.count('model_1_2_benchmark.png'), 1)
|
| 159 |
+
self.assertEqual(source.count('model_1_2_before_2024_benchmark.png'), 1)
|
| 160 |
self.assertEqual(source.count('model_1_2_after_2024_benchmark.png'), 1)
|
| 161 |
for text in ('90.7%', '85.2%', '5.2%', '8.1%', '4.1%', '6.7%',
|
| 162 |
'Calendar 2024', '59 days / 18 storms / 1,174',
|
|
|
|
| 164 |
'100% Mini `<2024`; 0% other checkpoints'):
|
| 165 |
self.assertIn(text, source)
|
| 166 |
|
| 167 |
+
def test_historical_chart_and_missing_pressure_are_explicit(self):
|
| 168 |
+
report = json.loads((ROOT / 'evaluation/deepmind_daily/period_comparison.json').read_text())
|
| 169 |
+
before = report['periods']['before_2024']
|
| 170 |
+
for source in (self.source, render_card(self.original, self.source)):
|
| 171 |
+
section = source.split('### Before 2024', 1)[1].split('### After 2024', 1)[0]
|
| 172 |
+
for label, key, places in (('Historical track position MAE', 'mean_track_error_km', 1),
|
| 173 |
+
('Historical track-direction error', 'direction_error_deg', 2),
|
| 174 |
+
('Historical route-shape similarity', 'shape_similarity', 4)):
|
| 175 |
+
self.assert_metric_row(section, label,
|
| 176 |
+
{m: before['scores']['route'][m][key]['value'] for m in ('1.1', '1.2', 'deepmind')}, places)
|
| 177 |
+
for text in ('Pressure is not scored', '0 shared valid pressure starts', 'not zero error', '1980–1999'):
|
| 178 |
+
self.assertIn(text, section)
|
| 179 |
+
|
| 180 |
+
def test_training_years_sources_and_receipt_match_release(self):
|
| 181 |
+
manifest = json.loads((ROOT / 'models/trackformer_1_2_field/manifest.json').read_text())
|
| 182 |
+
audit = json.loads((ROOT / 'evaluation/training_data/trackformer_1_2_provenance.json').read_text())
|
| 183 |
+
self.assertEqual(audit['status'], 'verified')
|
| 184 |
+
for key in ('source_checkpoint_sha256', 'inference_weights_sha256'):
|
| 185 |
+
self.assertEqual(audit[key], manifest[key])
|
| 186 |
+
self.assertEqual(audit['dataset_manifest_sha256'], manifest['dataset_sha256'])
|
| 187 |
+
self.assertEqual(audit['normalization_fit_years'], [2000, 2021])
|
| 188 |
+
self.assertEqual(len(audit['verified_archive_files']), 6)
|
| 189 |
+
self.assertTrue(audit['whole_storm_splits'])
|
| 190 |
+
for name, years, count, native in (('train', [2000, 2021], 13949, 800),
|
| 191 |
+
('validation', [2022, 2023], 1041, 100),
|
| 192 |
+
('test', [2024, 2025], 1195, 100)):
|
| 193 |
+
row = audit['partitions'][name]
|
| 194 |
+
self.assertEqual((row['years'], row['windows'], row['native_pressure_windows']), (years, count, native))
|
| 195 |
+
for key in ('inference_performed', 'model_weights_modified', 'training_data_modified'):
|
| 196 |
+
self.assertFalse(audit[key])
|
| 197 |
+
for source in (self.source, render_card(self.original, self.source)):
|
| 198 |
+
for text in ('## Training data and year cutoffs', '2000–2021', '2022–2023', '2024–2025',
|
| 199 |
+
'not training through 2026', 'IBTrACS', 'NCEP/NCAR Reanalysis 1',
|
| 200 |
+
'ARCO-ERA5', 'land fraction', '13,949', '1,041', '1,195'):
|
| 201 |
+
self.assertIn(text, source)
|
| 202 |
+
for path in ('docs/trackformer_1_2_training_data.md',
|
| 203 |
+
'evaluation/training_data/trackformer_1_2_provenance.json', 'release_tools/audit_training_data.py'):
|
| 204 |
+
self.assertIn(path, SYNC_FILES)
|
| 205 |
+
|
| 206 |
def test_evaluation_index_and_worker_handoff_metadata_are_explained(self):
|
| 207 |
index = (ROOT / 'evaluation/README.md').read_text()
|
| 208 |
self.assertIn('completed three-model comparison', index)
|