euler314 commited on
Commit
b10ddba
·
verified ·
1 Parent(s): 87c51c8

Add pre-2024 chart and verified Trackformer 1.2 training sources and cutoffs

Browse files
.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
- Both README charts 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,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
- [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) · [Post-2024 chart](../evaluation/released_daily/model_1_2_after_2024_benchmark.png)
 
 
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

  • SHA256: d441f512bd6eeb6761bc2ac4bb3bb09d64c9efccc8241fdc93b489fd442f5104
  • Pointer size: 131 Bytes
  • Size of remote file: 220 kB
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), 'bars_per_panel': 3, 'error_bars': False,
 
 
 
 
 
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='Publish exact Mini <2024 identity, UTC proportions and total/post-2024 comparison charts',
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 completed Mini <2024 results with separate total and post-2024 benchmarks',
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'), ('model_1_2_after_2024_benchmark', 'after_2024')):
 
 
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)