euler314 commited on
Commit
2385fd7
·
verified ·
1 Parent(s): 2fe2fc1

Replace legacy files with Trackformer1.1

Browse files

Remove the previous model files and publish only the Trackformer1.1 causal inference bundle.

Files changed (34) hide show
  1. # Typhoon PredictTyphoon Predict is an ERA5-conditioned tropical-cyclone track research model. It predicts future cyclone center positions from recent track history and a local atmospheric-state patch. The project is intended for research, experimentation, and reproducible model development.## Model ArchitectureThe model combines:- A convolutional encoder for storm-centered ERA5 atmospheric patches.- A bidirectional GRU for recent cyclone track history.- A fusion multilayer perceptron that combines the atmospheric and track representations.- Probabilistic output heads that predict the mean and log-scale of future latitude and longitude offsets.- A latent projection used to create correlated ensemble members for uncertainty-aware forecasts.The forecast is generated recursively for multiple lead times. The ensemble represents sampled plausible tracks; the mean track is a summary and should not be treated as a guaranteed path.## Training ConfigurationThe released experiment uses:- ERA5 reanalysis data beginning in 1979.- Western Pacific basin samples.- Four historical track steps as input.- Forecast lead times of 6, 12, 24, 48, 72, 96, and 120 hours.- Storm-centered patches with an 8 degree half-width at 0.5 degree resolution.- Chronological split: training through 2015 and validation through 2019.- Batch size 64, learning rate 2e-4, weight decay 1e-4.- Up to 80 epochs with early stopping patience of 12 epochs.- 50 ensemble members for uncertainty sampling.The training pipeline normalizes atmospheric variables, extracts storm-centered windows, aligns ERA5 with track timestamps, and uses chronological validation to reduce temporal leakage.## Data PipelineTraining examples require:1. Quality-controlled tropical-cyclone track fixes with timestamp, latitude, and longitude.2. ERA5 atmospheric fields covering the same time range.3. Storm-centered spatial extraction around each observed cyclone position.4. Historical track sequences paired with future positions at the configured lead times.5. Chronological train and validation splits.Cached patches can be reused to reduce repeated preprocessing and storage overhead during experiments.## InferenceThe local inference script loads a PyTorch checkpoint, prepares the configured input sequence, runs ensemble sampling, and writes forecast points plus an optional HTML map. See run_inference.py and the GitHub repository for the current command-line workflow.When live ERA5 data is unavailable, the example inference path uses normalized atmospheric inputs as a software smoke test. Operational or scientific use requires real, correctly aligned atmospheric analysis data.## Model FormatsThe native release format is PyTorch. GGUF is designed primarily for transformer-style language models and is not a suitable interchange format for this custom CNN-GRU probabilistic model. ONNX or TorchScript conversion may be possible, but any converted model must be validated against the native checkpoint for numerical and forecast consistency.## LimitationsThis is a research model, not an operational warning system. Forecast quality depends on the training data, basin coverage, input alignment, preprocessing, and model calibration. It should not replace official meteorological agencies or emergency-management guidance.## License and AttributionReview the repository license and source-data terms before redistribution or commercial use. ERA5 data is provided by the Copernicus Climate Change Service and follows its applicable terms. +0 -0
  2. .gitattributes +2 -35
  3. README.md +92 -154
  4. forecast.json +0 -74
  5. forecast_world_map.html +0 -61
  6. models/trackformer_1_1/manifest.json +33 -0
  7. models/trackformer_1_1/trackformer_1_1_calibration.json +2180 -0
  8. v23_terrain_wp.npz → models/trackformer_1_1/trackformer_1_1_intensity_seed0.pt +2 -2
  9. best.pt → models/trackformer_1_1/trackformer_1_1_intensity_seed1.pt +2 -2
  10. v23_seed1.pt → models/trackformer_1_1/trackformer_1_1_intensity_seed2.pt +2 -2
  11. v23_norm_stats.npz → models/trackformer_1_1/trackformer_1_1_norm_stats.npz +0 -0
  12. v23_seed2.pt → models/trackformer_1_1/trackformer_1_1_structure_seed0.pt +2 -2
  13. models/trackformer_1_1/trackformer_1_1_structure_seed1.pt +3 -0
  14. models/trackformer_1_1/trackformer_1_1_structure_seed2.pt +3 -0
  15. models/trackformer_1_1/trackformer_1_1_temporal_seed0.pt +3 -0
  16. models/trackformer_1_1/trackformer_1_1_temporal_seed1.pt +3 -0
  17. models/trackformer_1_1/trackformer_1_1_temporal_seed2.pt +3 -0
  18. requirements.txt +3 -5
  19. run_inference.py +0 -94
  20. run_v23.py +0 -222
  21. trackformer_1_1.py +35 -0
  22. trackformer_1_1_base_route.py +419 -0
  23. trackformer_1_1_intensity.py +748 -0
  24. trackformer_1_1_route.py +209 -0
  25. trackformer_1_1_temporal.py +92 -0
  26. trackformer_v23.py +0 -229
  27. v23_seed0.pt +0 -3
  28. v23_seed3.pt +0 -3
  29. v23_seed4.pt +0 -3
  30. v23_seed5.pt +0 -3
  31. v23_seed6.pt +0 -3
  32. v23_seed7.pt +0 -3
  33. v23_seed8.pt +0 -3
  34. v23_seed9.pt +0 -3
# Typhoon PredictTyphoon Predict is an ERA5-conditioned tropical-cyclone track research model. It predicts future cyclone center positions from recent track history and a local atmospheric-state patch. The project is intended for research, experimentation, and reproducible model development.## Model ArchitectureThe model combines:- A convolutional encoder for storm-centered ERA5 atmospheric patches.- A bidirectional GRU for recent cyclone track history.- A fusion multilayer perceptron that combines the atmospheric and track representations.- Probabilistic output heads that predict the mean and log-scale of future latitude and longitude offsets.- A latent projection used to create correlated ensemble members for uncertainty-aware forecasts.The forecast is generated recursively for multiple lead times. The ensemble represents sampled plausible tracks; the mean track is a summary and should not be treated as a guaranteed path.## Training ConfigurationThe released experiment uses:- ERA5 reanalysis data beginning in 1979.- Western Pacific basin samples.- Four historical track steps as input.- Forecast lead times of 6, 12, 24, 48, 72, 96, and 120 hours.- Storm-centered patches with an 8 degree half-width at 0.5 degree resolution.- Chronological split: training through 2015 and validation through 2019.- Batch size 64, learning rate 2e-4, weight decay 1e-4.- Up to 80 epochs with early stopping patience of 12 epochs.- 50 ensemble members for uncertainty sampling.The training pipeline normalizes atmospheric variables, extracts storm-centered windows, aligns ERA5 with track timestamps, and uses chronological validation to reduce temporal leakage.## Data PipelineTraining examples require:1. Quality-controlled tropical-cyclone track fixes with timestamp, latitude, and longitude.2. ERA5 atmospheric fields covering the same time range.3. Storm-centered spatial extraction around each observed cyclone position.4. Historical track sequences paired with future positions at the configured lead times.5. Chronological train and validation splits.Cached patches can be reused to reduce repeated preprocessing and storage overhead during experiments.## InferenceThe local inference script loads a PyTorch checkpoint, prepares the configured input sequence, runs ensemble sampling, and writes forecast points plus an optional HTML map. See run_inference.py and the GitHub repository for the current command-line workflow.When live ERA5 data is unavailable, the example inference path uses normalized atmospheric inputs as a software smoke test. Operational or scientific use requires real, correctly aligned atmospheric analysis data.## Model FormatsThe native release format is PyTorch. GGUF is designed primarily for transformer-style language models and is not a suitable interchange format for this custom CNN-GRU probabilistic model. ONNX or TorchScript conversion may be possible, but any converted model must be validated against the native checkpoint for numerical and forecast consistency.## LimitationsThis is a research model, not an operational warning system. Forecast quality depends on the training data, basin coverage, input alignment, preprocessing, and model calibration. It should not replace official meteorological agencies or emergency-management guidance.## License and AttributionReview the repository license and source-data terms before redistribution or commercial use. ERA5 data is provided by the Copernicus Climate Change Service and follows its applicable terms. DELETED
File without changes
.gitattributes CHANGED
@@ -1,35 +1,2 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ models/**/*.pt filter=lfs diff=lfs merge=lfs -text
2
+ models/trackformer_1_1/trackformer_1_1_norm_stats.npz filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -3,165 +3,103 @@ license: mit
3
  library_name: pytorch
4
  pipeline_tag: time-series-forecasting
5
  tags:
6
- - tropical-cyclone
7
- - weather-forecasting
8
- - pytorch
9
- - era5
10
- - ibtracs
11
  ---
12
 
13
- # Typhoon Predict — tropical-cyclone forecasting
14
-
15
- **Research models — not an operational warning system. Do not use for evacuation, aviation,
16
- maritime, or emergency decisions.**
17
-
18
- ## Current best model: TrackFormer v23
19
-
20
- TrackFormer v23 predicts the atmospheric steering flow that carries a storm as an explicit
21
- chain-of-thought (CoT) intermediate step, conditions that estimate on how the flow has been
22
- evolving over the previous day (t-24h, t-12h, now), and derives track from it. Result:
23
- **434.96 km RMS track error** (10-seed ensemble), WP+EP 2020+, full 20-lead-horizon test set
24
- (3,763 windows).
25
-
26
- This is the best-performing model in the whole project, reached through a longer architecture
27
- progression:
28
-
29
- 1. **v10–v20** — a small CNN encoder reads a deep-layer-mean steering-wind patch around the storm.
30
- 2. **v21, v22** — chain-of-thought: predict the steering flow itself, then derive track from it
31
- (v22 adds a latent CoT with weight-tied feedback rounds).
32
- 3. **v23** — add a temporal history of the CoT steering representation. Best result: **434.96 km**.
33
- 4. **v24–v29** — four further environmental additions on top of v23 (an environmental token, an
34
- ocean-heat CNN patch, a drift adapter, raw ERA5 steering wind) all came back **null**: once a
35
- CoT representation already extracts the steering signal that matters, handing the model the raw
36
- field again is redundant.
37
- 5. **v31–v34** — land/terrain-interaction correction, motivated by real-world reports of typhoons
38
- stalling at mountainous coastlines (Typhoon Gaemi, 2024, at Taiwan) and terrain-deflection
39
- literature (AOT-TCNet, arXiv 2603.29200):
40
-
41
- | model | aggregate track (km) | Typhoon Tip 1979 (km) | Typhoon Noul 2026, ocean/landfall (km) |
42
- |---|---|---|---|
43
- | v23 (baseline) | **434.96** | 939 | 267 / 356 |
44
- | v31 — LandDrag, uniform training | 443.07 (+8.11) | — | — |
45
- | v32 — LandDrag, window-oversampled | 460.48 (+25.52, backfired) | — | — |
46
- | v33 — LandDrag, storm-normalized | 442.33 (+7.37) | **876** | **243 / 319** |
47
- | v34 — LandGate, **frozen v23 backbone** | 460.52 (−0.33 vs. own backbone) | **795** | 287 / 382 |
48
-
49
- v34 is the methodologically important result: v31–v33 each retrained the *entire* architecture
50
- from scratch, so their deltas vs. v23 include ~19 km/seed of ordinary retrain noise on top of
51
- whatever the land correction did. v34 instead freezes a real, already-trained v23 checkpoint and
52
- trains only a new ~437-parameter gated correction — a true same-backbone-plus-one-addition
53
- comparison. Result: essentially null everywhere, including the mountainous-near-land regime every
54
- earlier attempt targeted. On the two real out-of-training storms available, v33 and v34 each split
55
- 1–1 against v23 — a small-n disagreement with the aggregate test set, not a reliable effect.
56
-
57
- **Methodological lessons:** retrain-to-retrain seed noise (~19 km/seed) is large enough to
58
- manufacture or hide most small version-to-version deltas; freezing a real backbone and training
59
- only a small addition isolates a causal effect that comparing two from-scratch runs cannot; and a
60
- large in-distribution aggregate test set does not always agree with genuinely out-of-training
61
- real-storm validation.
62
-
63
- v23's 10-seed ensemble is released in this repo (`v23_seed0.pt`–`v23_seed9.pt`, fp32, 52.6 MB
64
- each) with a standalone architecture module and CLI — see "Usage — v23" below. Full write-up,
65
- architecture equations, and every intermediate result: `paper/trackformer.pdf`, "Chain-of-thought
66
- steering and land-interaction testing," in the GitHub repo
67
- (**https://github.com/yu314-coder/typhoon-predict**).
68
-
69
- ## Usage — v23
70
-
71
- Two modes, controlled by whether you pass `--steering`:
72
-
73
- **IBTrACS-only** (default) — give it nothing but the storm's own recent track (position, max wind,
74
- central pressure). This is what any best-track record gives you for a storm, nothing more. The
75
- steering field and its 12h/24h history are zero-filled with an explicit availability flag — the
76
- same "unavailable == exact zeros, not fabricated" convention used throughout this project.
77
 
78
- ```bash
79
- python run_v23.py --track my_storm.json --out forecast.json
80
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
 
82
- **Full data** — additionally supply a real deep-layer-mean steering-wind patch (850/500/200 hPa
83
- u/v, 2.5° resolution, ±20° box centered on the storm) for the current fix and, ideally, the two
84
- fixes 12h/24h before it. This is what the headline **434.96 km** result requires.
85
 
86
  ```bash
87
- python run_v23.py --track my_storm.json --steering my_steering.npz --out forecast.json
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  ```
89
 
90
- `my_storm.json`: a list of fixes, oldest→newest, spaced 6h apart, ending at the fix to forecast
91
- from — see `run_v23.py`'s docstring for the exact schema. `my_steering.npz`: keyed by the same ISO
92
- timestamps; the GitHub repo's `_fetch_dolphin_steering.py` is a complete working example of
93
- building one from NOAA/NOMADS GFS analysis fields for a live storm (ERA5 works the same way for a
94
- past one).
95
-
96
- **How much does the steering field matter?** Tested on Typhoon Dolphin (2026, active as of this
97
- writing): with real fetched GFS steering, v23's 120h forecast was 14.7°N,150.7°E / 99 kt / 951 hPa;
98
- with the steering field zeroed out (IBTrACS-only) it was 17.5°N,156.3°E / 90 kt / 957 hPa — the
99
- track moved by several hundred km while intensity only softened modestly. So on this storm the
100
- steering field mainly earns its keep on **track**, not intensity.
101
-
102
- ## Released checkpoints: StormFusion-MT & TrackFormer v1–v9
103
-
104
- The earlier, fully released and locally-runnable line. Each predicts, at 20 six-hourly lead times
105
- (6–120 h), a 17-dim state per lead: east/north storm motion (km), max wind (kt), central pressure
106
- (hPa), radius of max wind (km), and 34/50/64-kt wind radii in four quadrants.
107
-
108
- | model | params | inputs | training data |
109
- |---|---|---|---|
110
- | **TrackFormer v9** | 17M (fp16, 33MB) | **track history + IBTrACS environment, protected triple-stream** | all basins, 1980+, 193k partial-lead windows |
111
- | TrackFormer v8 | 15M (fp16, 30MB) | track history only, protected dual-stream | all basins, 1980+, 193k partial-lead windows |
112
- | StormFusion-MT v2 | 3.3M (fp16, 6.7MB) | ERA5 patches + track history | WP, 2000+, 1,337 storm-centered windows |
113
- | TrackFormer (v1) | 21M (fp16, 43MB) | track history only (single-stream) | all basins, 1980+, 84,150 windows |
114
-
115
- Weights and full reproducible code (dataset builders, training, eval) are in the GitHub repo
116
- (`models/`).
117
-
118
- ### Results — WP 2020+ held-out test (lower is better)
119
-
120
- | model | track km | vmax kt | pres hPa | rmw km | radius km |
121
- |---|---|---|---|---|---|
122
- | StormFusion-MT v2 (3.3M, ERA5) | 729 | 24.2 | 21.6 | 16.2 | 31.8 |
123
- | TrackFormer v1 (21M, single-stream) | 720 | 22.1 | 21.2 | 11.8 | 31.5 |
124
- | TrackFormer v3 (15M, dual-stream) | 659 | 21.6 | 18.1 | 11.8 | 28.8 |
125
- | TrackFormer v8 (15M, +partial-lead data) | 649 | 20.7 | 15.9 | **11.3** | 27.8 |
126
- | **TrackFormer v9 (17M, +IBTrACS environment)** | **618** | **18.6** | 15.8 | 11.5 | **27.2** |
127
-
128
- **Key findings.** (1) A track-only model that never sees ERA5 **matches or beats** the full ERA5
129
- model, so **data diversity > engineered features > parameters** (a 17.7M ERA5 model overfit and did
130
- *worse* than the 3.3M one). (2) Naively adding motion-dynamics features to a single-stream model
131
- improves intensity but hurts track through **negative transfer**; **TrackFormer v3** fixes this with a
132
- protected dual-stream architecture (separate kinematic/thermodynamic encoders, gradient routing, a
133
- zero-init gated thermo→track adapter, and a persistence-residual track head), cutting WP-2020+ track
134
- error to 659 km (−61, storm-bootstrap 95% CI [−103, −16] km, p≈0.995) while keeping the intensity
135
- gains. Full architecture and derivation (incl. a random-matrix block-covariance uncertainty head) in
136
- `paper/trackformer.pdf`, Appendix A, in the GitHub repo.
137
-
138
- ### Architectures
139
-
140
- - **StormFusion-MT v2** — separate inner/outer ERA5 conv encoders keeping a 3×3 grid of spatial
141
- tokens, track/environment token encoders, a temporal Transformer context, learned + sinusoidal
142
- lead-time queries, cross-attention decoding, and multi-task state / log-scale heads.
143
- - **TrackFormer** — the same decoder design, track-only: a 40-dim track-history projection →
144
- Transformer context (d_model 384, 8 heads, 4+6 layers) → lead queries → dual heads. No
145
- atmospheric inputs.
146
-
147
- ### Usage
148
-
149
- See the GitHub repo for `model_v2.py` / `train_track.py`, the checkpoints, and normalization
150
- stats. Inputs are per-feature standardized (stats saved with each checkpoint / dataset);
151
- multiply predictions by `TARGET_SCALE = [100,100,35,20,50] + [50]*12` for physical units.
152
-
153
- ## Data
154
-
155
- IBTrACS v04r01 best tracks (NOAA NCEI) and ERA5 reanalysis (Copernicus/ECMWF). Obtain the source
156
- data under its own access and licensing terms.
157
-
158
- ## Limitations
159
-
160
- - Research models throughout — not operational quality in either line. TrackFormer v23 (434.96 km
161
- RMS track error, 6–120 h) and the released StormFusion-MT/TrackFormer v1–v9 checkpoints
162
- (~618–730 km, different test split) are both far from operational.
163
- - v24–v34 (the further additions and land-interaction line built on top of v23) remain research
164
- artifacts, not packaged for this card's load/run format — only v23 itself is released.
165
- - The real ceiling is storm **diversity** (~13k storms have ever existed); larger models overfit.
166
- - Wind-radius labels are sparse; no calibration or comparison against official agency forecasts.
167
- - Pre-satellite track/intensity labels are lower quality.
 
3
  library_name: pytorch
4
  pipeline_tag: time-series-forecasting
5
  tags:
6
+ - tropical-cyclone
7
+ - weather-forecasting
8
+ - pytorch
9
+ - ibtracs
10
+ - causal-inference
11
  ---
12
 
13
+ # Trackformer1.1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
+ Trackformer1.1 is a research model for causal tropical-cyclone track,
16
+ pressure, intensity, and wind-structure inference over the western Pacific.
17
+ It is not an operational warning system and must not be used for evacuation,
18
+ aviation, maritime, emergency-management, or other safety-critical decisions.
19
+
20
+ ## What it uses
21
+
22
+ Inference accepts the observed storm history and weather analyses available at
23
+ the issue time. It uses the current, 12-hour, and 24-hour analysis states to
24
+ extrapolate a bounded western-Pacific pressure and flow state. The route sees
25
+ a domain covering China, Japan, Taiwan, the Philippines, and the open western
26
+ Pacific, so nearby lows, subtropical ridges, troughs, jets, and other storms
27
+ can affect the steering field.
28
+
29
+ No positive-lead weather field, official agency forecast, or post-issue
30
+ observation is passed to the model. Later observations can be used only for
31
+ verification after a forecast has been generated.
32
+
33
+ ## Model structure
34
+
35
+ 1. A causal regional state module extrapolates pressure, 500 hPa height, and
36
+ 850/500/200 hPa wind from three analysis snapshots.
37
+ 2. A weighted steering ensemble samples inner, deep-layer, ridge, trough, and
38
+ jet views across the full western-Pacific domain and integrates the route.
39
+ 3. Three primary neural experts predict maximum wind, central pressure, RMW,
40
+ and directional 34/50/64 kt radii from a nine-step track window and the
41
+ current four-channel analysis patch.
42
+ 4. Three structure experts provide a validation-selected secondary blend.
43
+ 5. Three temporal experts read only same-storm 12-hour and 24-hour analysis
44
+ patches. Their residual branch is enabled only for the validated wind
45
+ adjustment; pressure remains on the calibrated primary path.
46
+ 6. The final intensity output is coupled to the causal pressure-map minimum,
47
+ pressure deficit, 850 hPa wind, and quadrant anomaly extent. Radius outputs
48
+ remain ordered as R34 >= R50 >= R64.
49
+
50
+ The public bundle contains frozen inference checkpoints and calibration state;
51
+ it does not include raw weather archives or a training service.
52
+
53
+ ## Files
54
+
55
+ - `trackformer_1_1.py` - public loader and route API.
56
+ - `trackformer_1_1_route.py` - whole-domain causal pressure-state route.
57
+ - `trackformer_1_1_intensity.py` - intensity and wind-structure inference.
58
+ - `trackformer_1_1_temporal.py` - inference-only temporal expert architecture.
59
+ - `models/trackformer_1_1/` - three expert groups, calibration, and manifest.
60
 
61
+ ## Quick start
 
 
62
 
63
  ```bash
64
+ python -m pip install -r requirements.txt
65
+ python - <<'PY'
66
+ import numpy as np
67
+ from trackformer_1_1 import load_intensity, forecast_pacific_state
68
+
69
+ model = load_intensity(device="cpu")
70
+ track = np.zeros((9, 54), dtype="float32")
71
+ field = np.zeros((4, 17, 17), dtype="float32")
72
+ current_structure = np.full(13, np.nan, dtype="float32")
73
+ rows, metadata = model.predict(
74
+ track,
75
+ field,
76
+ current_wind=65.0,
77
+ current_pressure=980.0,
78
+ previous_wind=60.0,
79
+ previous_pressure=985.0,
80
+ current_structure=current_structure,
81
+ )
82
+ print(rows[0])
83
+ print(metadata["model"])
84
+ PY
85
  ```
86
 
87
+ The intensity contract is `track=(9,54)`, `field=(4,17,17)`, and optional
88
+ `history_field=(8,17,17)` for the 12-hour and 24-hour analysis patches. The
89
+ route contract is `fields=(3,7,latitude,longitude)` and
90
+ `pressure=(3,latitude,longitude)`, ordered current, 12 hours before, and 24
91
+ hours before. See `models/trackformer_1_1/manifest.json` for the complete
92
+ contract.
93
+
94
+ ## Requirements and limits
95
+
96
+ CPU inference works with Python 3.10+, NumPy, PyTorch, and SciPy. A modern
97
+ Mac with 8 GB RAM is a practical minimum; 16 GB RAM is preferable when
98
+ decoding large analysis grids. The checkpoints are native PyTorch files.
99
+ GGUF is not an appropriate format for this CNN/Transformer weather model;
100
+ keep the native PyTorch weights or convert to another format only after
101
+ numerical parity testing.
102
+
103
+ The model is experimental. Track and intensity errors vary by storm, basin,
104
+ analysis source, and data quality. Use official meteorological agencies for
105
+ real-world forecasts and warnings.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
forecast.json DELETED
@@ -1,74 +0,0 @@
1
- {
2
- "storm": "Bavi",
3
- "source": "JTWC/TCGP fixes through 2026-07-10 0600 UTC",
4
- "initial_time": "2026-07-10T06:00:00Z",
5
- "initial_lat": 21.9,
6
- "initial_lon": 126.9,
7
- "points": [
8
- {
9
- "lead_hours": 6,
10
- "lat": 22.466064453125,
11
- "lon": 126.55152130126953,
12
- "p10_lat": 22.288002014160156,
13
- "p90_lat": 22.62434959411621,
14
- "p10_lon": 126.4314956665039,
15
- "p90_lon": 126.70477294921875
16
- },
17
- {
18
- "lead_hours": 12,
19
- "lat": 22.821269989013672,
20
- "lon": 126.27210998535156,
21
- "p10_lat": 22.475460052490234,
22
- "p90_lat": 23.098590850830078,
23
- "p10_lon": 126.02592468261719,
24
- "p90_lon": 126.5489273071289
25
- },
26
- {
27
- "lead_hours": 24,
28
- "lat": 23.898107528686523,
29
- "lon": 125.54524230957031,
30
- "p10_lat": 23.513370513916016,
31
- "p90_lat": 24.373027801513672,
32
- "p10_lon": 124.97864532470703,
33
- "p90_lon": 126.13536834716797
34
- },
35
- {
36
- "lead_hours": 48,
37
- "lat": 25.304624557495117,
38
- "lon": 124.66926574707031,
39
- "p10_lat": 24.34754180908203,
40
- "p90_lat": 26.398544311523438,
41
- "p10_lon": 123.34161376953125,
42
- "p90_lon": 125.87503814697266
43
- },
44
- {
45
- "lead_hours": 72,
46
- "lat": 26.52523422241211,
47
- "lon": 123.29611206054688,
48
- "p10_lat": 24.960338592529297,
49
- "p90_lat": 28.163558959960938,
50
- "p10_lon": 120.4675064086914,
51
- "p90_lon": 125.69815826416016
52
- },
53
- {
54
- "lead_hours": 96,
55
- "lat": 26.998743057250977,
56
- "lon": 122.69322204589844,
57
- "p10_lat": 24.666839599609375,
58
- "p90_lat": 29.167266845703125,
59
- "p10_lon": 119.0267562866211,
60
- "p90_lon": 125.30994415283203
61
- },
62
- {
63
- "lead_hours": 120,
64
- "lat": 28.687833786010742,
65
- "lon": 121.19312286376953,
66
- "p10_lat": 26.278013229370117,
67
- "p90_lat": 31.288469314575195,
68
- "p10_lon": 115.32806396484375,
69
- "p90_lon": 125.7118911743164
70
- }
71
- ],
72
- "device": "mps",
73
- "note": "Mac checkpoint inference using current track fixes and mean-normalized atmospheric input because 2026 ERA5 fields are unavailable locally. Not an operational forecast."
74
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
forecast_world_map.html DELETED
@@ -1,61 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>Bavi Mac Model Forecast</title>
7
- <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
8
- <style>
9
- html, body, #map { height: 100%; margin: 0; }
10
- body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
11
- .panel { position: absolute; z-index: 1000; top: 16px; left: 16px; width: min(360px, calc(100vw - 32px)); padding: 14px 16px; background: rgba(255,255,255,.96); border: 1px solid #d5d9df; box-shadow: 0 3px 14px rgba(0,0,0,.18); }
12
- h1 { margin: 0 0 7px; font-size: 20px; }
13
- p { margin: 5px 0; font-size: 13px; line-height: 1.4; color: #374151; }
14
- .legend { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 9px; font-size: 12px; color: #374151; }
15
- .key { display: inline-flex; align-items: center; gap: 5px; }
16
- .swatch { width: 20px; height: 3px; display: inline-block; }
17
- .mean { background: #d7263d; }
18
- .actual { background: #111827; }
19
- .corridor { width: 20px; height: 10px; background: rgba(245,158,11,.32); border: 1px solid #d97706; }
20
- </style>
21
- </head>
22
- <body>
23
- <div id="map"></div>
24
- <section class="panel">
25
- <h1>Bavi: Mac checkpoint prediction</h1>
26
- <p><b>Initial fix:</b> 10 Jul 2026 06:00 UTC, 21.9°N 126.9°E</p>
27
- <p><b>Run:</b> local Mac Apple MPS, 50 ensemble members, 120-hour horizon</p>
28
- <p><b>Important:</b> this is a research proxy using a mean-normalized atmospheric field because the matching 2026 ERA5 field is not available locally. It is not an operational warning forecast.</p>
29
- <div class="legend">
30
- <span class="key"><i class="swatch actual"></i>official initial fix</span>
31
- <span class="key"><i class="swatch mean"></i>model mean</span>
32
- <span class="key"><i class="swatch corridor"></i>10–90% corridor</span>
33
- </div>
34
- </section>
35
- <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
36
- <script>
37
- const initial = [21.9, 126.9];
38
- const forecast = [
39
- {h: 6, lat: 22.4336, lon: 126.5818, la: 22.3279, lb: 22.5795, oa: 126.4390, ob: 126.7272},
40
- {h: 12, lat: 22.7873, lon: 126.3022, la: 22.5105, lb: 23.0514, oa: 126.0263, ob: 126.6464},
41
- {h: 24, lat: 23.8919, lon: 125.4258, la: 23.5613, lb: 24.3624, oa: 124.9874, ob: 125.9527},
42
- {h: 48, lat: 25.6544, lon: 124.6313, la: 24.6905, lb: 26.3908, oa: 123.1265, ob: 126.0750},
43
- {h: 72, lat: 26.4361, lon: 123.5473, la: 24.9631, lb: 28.2892, oa: 120.7869, ob: 125.8778},
44
- {h: 96, lat: 26.6257, lon: 121.9343, la: 24.2808, lb: 28.4809, oa: 118.0243, ob: 126.7538},
45
- {h: 120, lat: 28.6126, lon: 121.8454, la: 26.2118, lb: 30.8639, oa: 117.2112, ob: 126.6462}
46
- ];
47
- const map = L.map('map', {worldCopyJump: true}).setView([25, 126], 4);
48
- L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {maxZoom: 18, attribution: '&copy; OpenStreetMap contributors'}).addTo(map);
49
- L.marker(initial).addTo(map).bindPopup('<b>Official initial fix</b><br>21.9°N, 126.9°E<br>10 Jul 2026 06:00 UTC').openPopup();
50
- const meanLine = [initial];
51
- forecast.forEach(p => {
52
- meanLine.push([p.lat, p.lon]);
53
- L.rectangle([[p.la, p.oa], [p.lb, p.ob]], {color:'#d97706', weight:1, fillColor:'#f59e0b', fillOpacity:.18}).addTo(map).bindTooltip('+' + p.h + 'h model 10–90% box');
54
- L.circleMarker([p.lat, p.lon], {radius: 5, color:'#d7263d', fillColor:'#d7263d', fillOpacity:1}).addTo(map).bindTooltip('+' + p.h + 'h: ' + p.lat.toFixed(2) + '°N, ' + p.lon.toFixed(2) + '°E');
55
- });
56
- L.polyline(meanLine, {color:'#d7263d', weight:4, opacity:.9}).addTo(map);
57
- L.polyline([initial, [21.9,126.9]], {color:'#111827', weight:5}).addTo(map);
58
- map.fitBounds(L.latLngBounds(meanLine), {padding: [45, 45]});
59
- </script>
60
- </body>
61
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
models/trackformer_1_1/manifest.json ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "Trackformer1.1",
3
+ "task": "causal western-Pacific tropical-cyclone track, pressure, intensity, and wind-structure research inference",
4
+ "model_type": "regional analysis-state route plus spatial/temporal neural structure ensemble",
5
+ "checkpoint_count": {
6
+ "primary_intensity": 3,
7
+ "structure_experts": 3,
8
+ "temporal_experts": 3
9
+ },
10
+ "training_data": {
11
+ "track": "IBTrACS best-track history",
12
+ "weather": "analysis/reanalysis fields only",
13
+ "split": "storm-held-out chronological train/validation/test split",
14
+ "future_weather_as_input": false,
15
+ "official_forecast_as_input": false
16
+ },
17
+ "track_input_shape": [9, 54],
18
+ "intensity_field_shape": [4, 17, 17],
19
+ "history_field_shape": [8, 17, 17],
20
+ "route_field_layout": "[snapshots=3, channels=7, latitude, longitude] with hgt500,u/v850,u/v500,u/v200",
21
+ "lead_hours": [6, 12, 18, 24, 30, 36, 42, 48, 54, 60, 66, 72, 78, 84, 90, 96, 102, 108, 114, 120],
22
+ "outputs": [
23
+ "track latitude/longitude",
24
+ "maximum sustained wind",
25
+ "central pressure",
26
+ "radius of maximum wind",
27
+ "34/50/64 kt wind radii by quadrant"
28
+ ],
29
+ "native_distance_unit": "nautical miles",
30
+ "public_distance_unit": "kilometres",
31
+ "license": "MIT",
32
+ "warning": "Research output only; not an operational warning or safety-critical forecast."
33
+ }
models/trackformer_1_1/trackformer_1_1_calibration.json ADDED
@@ -0,0 +1,2180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "version": "Trackformer1.1-calibration",
3
+ "created_utc": "2026-08-04T03:35:06.917313+00:00",
4
+ "fit_split": "val (storm-held-out 2016-2019)",
5
+ "evaluation_split": "test (storm-held-out 2020+)",
6
+ "features": [
7
+ "bias",
8
+ "current_pressure_hpa",
9
+ "current_wind_kt",
10
+ "primary_predicted_wind_kt",
11
+ "predicted_wind_minus_current_wind_kt",
12
+ "current_pressure_minus_previous_pressure_hpa",
13
+ "current_wind_minus_previous_wind_kt"
14
+ ],
15
+ "wind_target_convention": "same as Trackformer1.1 model output (1-minute estimate); blend uses current wind as an observed forecast-time anchor",
16
+ "checkpoint_count": 3,
17
+ "metrics": {
18
+ "raw_val_pressure_mae_all_leads_hpa": 12.745838165283203,
19
+ "calibrated_val_pressure_mae_all_leads_hpa": 13.02608874699582,
20
+ "raw_test_pressure_mae_all_leads_hpa": 12.803126335144043,
21
+ "calibrated_test_pressure_mae_all_leads_hpa": 12.420955012285965,
22
+ "joint_calibrated_val_pressure_mae_all_leads_hpa": 12.730707934229281,
23
+ "joint_calibrated_test_pressure_mae_all_leads_hpa": 12.230934403282232,
24
+ "raw_val_pressure_mae_120h_hpa": 16.14628028869629,
25
+ "calibrated_val_pressure_mae_120h_hpa": 15.681344145679113,
26
+ "raw_test_pressure_mae_120h_hpa": 16.42153549194336,
27
+ "calibrated_test_pressure_mae_120h_hpa": 14.524238379900986,
28
+ "joint_calibrated_val_pressure_mae_120h_hpa": 15.545918282225045,
29
+ "joint_calibrated_test_pressure_mae_120h_hpa": 14.458328501318364,
30
+ "pressure_anchored_val_pressure_mae_all_leads_hpa": 12.587559582395707,
31
+ "pressure_anchored_test_pressure_mae_all_leads_hpa": 11.99345142652379,
32
+ "pressure_anchored_val_pressure_mae_120h_hpa": 15.544537539571403,
33
+ "pressure_anchored_test_pressure_mae_120h_hpa": 14.435201089126327,
34
+ "raw_val_wind_mae_all_leads_kt": 17.10424041748047,
35
+ "calibrated_val_wind_mae_all_leads_kt": 16.91584014892578,
36
+ "raw_test_wind_mae_all_leads_kt": 16.560529708862305,
37
+ "calibrated_test_wind_mae_all_leads_kt": 16.396728515625,
38
+ "raw_val_wind_mae_120h_kt": 21.62657356262207,
39
+ "calibrated_val_wind_mae_120h_kt": 21.440736770629883,
40
+ "raw_test_wind_mae_120h_kt": 20.42694664001465,
41
+ "calibrated_test_wind_mae_120h_kt": 20.36166763305664,
42
+ "raw_structure_mae_120h_native": [
43
+ 13.61,
44
+ 34.27,
45
+ 35.875,
46
+ 31.447,
47
+ 32.959,
48
+ 18.523,
49
+ 19.544,
50
+ 17.72,
51
+ 17.436,
52
+ 11.272,
53
+ 11.788,
54
+ 10.746,
55
+ 10.529
56
+ ],
57
+ "blended_structure_mae_120h_native": [
58
+ 13.615,
59
+ 34.109,
60
+ 35.432,
61
+ 31.447,
62
+ 32.737,
63
+ 18.626,
64
+ 19.763,
65
+ 17.891,
66
+ 17.661,
67
+ 11.29,
68
+ 11.747,
69
+ 10.746,
70
+ 10.475
71
+ ]
72
+ },
73
+ "calibrations": [
74
+ {
75
+ "beta": [
76
+ 979.3647389439432,
77
+ 1.9158192596454662,
78
+ -10.625316061573692,
79
+ -10.76484441891422,
80
+ 1.4216365382963003,
81
+ 0.355287389506127,
82
+ -2.653478803931415
83
+ ],
84
+ "mean": [
85
+ 0.0,
86
+ 978.1810383079426,
87
+ 64.5815707735542,
88
+ 65.59845112223823,
89
+ 1.0168803486840257,
90
+ 1.905043632598728,
91
+ 1.4701967164620617
92
+ ],
93
+ "scale": [
94
+ 1.0,
95
+ 50.53637228468569,
96
+ 33.10783692877566,
97
+ 32.14420199228587,
98
+ 4.047375438494807,
99
+ 54.79089485626104,
100
+ 6.246907173086729
101
+ ]
102
+ },
103
+ {
104
+ "beta": [
105
+ 978.6045857988166,
106
+ 1.159678080634184,
107
+ -10.476473797695414,
108
+ -11.027839130141558,
109
+ 0.25385328395687057,
110
+ 0.45833242498682186,
111
+ -3.181226204489663
112
+ ],
113
+ "mean": [
114
+ 0.0,
115
+ 976.7190828402366,
116
+ 64.52144970414201,
117
+ 65.9957684040775,
118
+ 1.4743186999354843,
119
+ 1.9063609467455622,
120
+ 1.4738165680473372
121
+ ],
122
+ "scale": [
123
+ 1.0,
124
+ 62.987212868626365,
125
+ 33.18977815879015,
126
+ 31.37842015427967,
127
+ 6.600915311239932,
128
+ 54.79480783300083,
129
+ 6.245251042331157
130
+ ]
131
+ },
132
+ {
133
+ "beta": [
134
+ 978.0161338069863,
135
+ 0.6738374269289117,
136
+ -10.062859593303838,
137
+ -11.437666297247567,
138
+ -1.0826424344554468,
139
+ 0.3649143931938072,
140
+ -2.4904427275749708
141
+ ],
142
+ "mean": [
143
+ 0.0,
144
+ 975.2491119005329,
145
+ 64.46758436944938,
146
+ 65.93912073678699,
147
+ 1.4715363673376147,
148
+ 1.7575488454706927,
149
+ 1.4773534635879217
150
+ ],
151
+ "scale": [
152
+ 1.0,
153
+ 73.34317349947774,
154
+ 33.277770226738575,
155
+ 30.210884717895517,
156
+ 9.857814699499565,
157
+ 56.14964419892791,
158
+ 6.253661273135333
159
+ ]
160
+ },
161
+ {
162
+ "beta": [
163
+ 977.6126259632483,
164
+ 0.3647266027866861,
165
+ -9.488025865326827,
166
+ -11.892068495452548,
167
+ -2.183654318457364,
168
+ 0.26197009521576825,
169
+ -1.7017446652271828
170
+ ],
171
+ "mean": [
172
+ 0.0,
173
+ 973.9270895080024,
174
+ 64.41123295791346,
175
+ 65.77106518447222,
176
+ 1.359832226558769,
177
+ 1.5952874925903973,
178
+ 1.4903675163011263
179
+ ],
180
+ "scale": [
181
+ 1.0,
182
+ 81.55905643779808,
183
+ 33.37057257362826,
184
+ 28.96361227936494,
185
+ 12.738465555338884,
186
+ 57.48200889919564,
187
+ 6.2549372779430845
188
+ ]
189
+ },
190
+ {
191
+ "beta": [
192
+ 977.3788958147819,
193
+ 0.22978396861298192,
194
+ -8.819607237745391,
195
+ -12.14781196679265,
196
+ -2.743935857195942,
197
+ 0.04992926797301592,
198
+ -1.3349625849092375
199
+ ],
200
+ "mean": [
201
+ 0.0,
202
+ 972.7582368655387,
203
+ 64.35366577619472,
204
+ 65.45164637299878,
205
+ 1.0979805968040597,
206
+ 1.5890471950133571,
207
+ 1.5031166518254675
208
+ ],
209
+ "scale": [
210
+ 1.0,
211
+ 88.23866281788312,
212
+ 33.456506355578135,
213
+ 27.75689837682731,
214
+ 15.347421693937026,
215
+ 57.524265495181936,
216
+ 6.247664265837107
217
+ ]
218
+ },
219
+ {
220
+ "beta": [
221
+ 977.2594465932758,
222
+ 0.2760853766109035,
223
+ -8.128811856734984,
224
+ -12.238453665761691,
225
+ -2.9696078556524057,
226
+ -0.03436852261542168,
227
+ -1.1949635193531134
228
+ ],
229
+ "mean": [
230
+ 0.0,
231
+ 972.4617673311515,
232
+ 64.34275513240107,
233
+ 64.98030532864153,
234
+ 0.6375501962404668,
235
+ 1.584498661112764,
236
+ 1.5165129425766142
237
+ ],
238
+ "scale": [
239
+ 1.0,
240
+ 89.9186470511227,
241
+ 33.49768138488822,
242
+ 26.56546307724779,
243
+ 17.78815483866843,
244
+ 57.59243440899489,
245
+ 6.244839191073782
246
+ ]
247
+ },
248
+ {
249
+ "beta": [
250
+ 977.275208830549,
251
+ 0.3302926221894572,
252
+ -7.4827672649673,
253
+ -12.181590337721174,
254
+ -2.958535712519607,
255
+ -0.05888547667459402,
256
+ -1.1528998888740287
257
+ ],
258
+ "mean": [
259
+ 0.0,
260
+ 972.166915274463,
261
+ 64.32801312649164,
262
+ 64.60189941317412,
263
+ 0.2738862863979647,
264
+ 1.5853221957040573,
265
+ 1.527744630071599
266
+ ],
267
+ "scale": [
268
+ 1.0,
269
+ 91.5873950283228,
270
+ 33.53705182833371,
271
+ 25.505423746027805,
272
+ 20.194468969727616,
273
+ 57.6690743414348,
274
+ 6.241065684632235
275
+ ]
276
+ },
277
+ {
278
+ "beta": [
279
+ 977.4265828468792,
280
+ 0.3456389495464159,
281
+ -6.887855834667866,
282
+ -11.982728429603078,
283
+ -2.8483573166969975,
284
+ -0.01676464039392852,
285
+ -1.155605952556869
286
+ ],
287
+ "mean": [
288
+ 0.0,
289
+ 971.8718754677443,
290
+ 64.30399640772339,
291
+ 63.97318780188467,
292
+ -0.3308086069806788,
293
+ 1.5837449483610238,
294
+ 1.545577009429726
295
+ ],
296
+ "scale": [
297
+ 1.0,
298
+ 93.26701702857976,
299
+ 33.579947436293786,
300
+ 24.620441527249962,
301
+ 22.370585532622172,
302
+ 57.76717976524845,
303
+ 6.2318182994660445
304
+ ]
305
+ },
306
+ {
307
+ "beta": [
308
+ 977.7077824519231,
309
+ 0.3742494390176481,
310
+ -6.340741475621145,
311
+ -11.754265657938584,
312
+ -2.6828266265386937,
313
+ 0.016897803479940448,
314
+ -1.1134641714774627
315
+ ],
316
+ "mean": [
317
+ 0.0,
318
+ 971.5954026442307,
319
+ 64.2549579326923,
320
+ 62.96718305378006,
321
+ -1.2877748783391256,
322
+ 1.5760216346153846,
323
+ 1.5689603365384615
324
+ ],
325
+ "scale": [
326
+ 1.0,
327
+ 94.93760398672073,
328
+ 33.613050887448374,
329
+ 23.706436575938007,
330
+ 24.41700063160801,
331
+ 57.87417128115503,
332
+ 6.217322594269493
333
+ ]
334
+ },
335
+ {
336
+ "beta": [
337
+ 978.1375037775763,
338
+ 0.4649928695065894,
339
+ -5.8395505850139715,
340
+ -11.477235869564007,
341
+ -2.4819997208339086,
342
+ 0.10752048000060792,
343
+ -1.1103025131158928
344
+ ],
345
+ "mean": [
346
+ 0.0,
347
+ 971.3303112722876,
348
+ 64.17527954064673,
349
+ 62.03331732324835,
350
+ -2.1419622182629916,
351
+ 1.5754004230885463,
352
+ 1.5929283771532186
353
+ ],
354
+ "scale": [
355
+ 1.0,
356
+ 96.6811703822946,
357
+ 33.65355257692086,
358
+ 22.79463316794241,
359
+ 26.2208909033306,
360
+ 58.03843631538493,
361
+ 6.204621897205797
362
+ ]
363
+ },
364
+ {
365
+ "beta": [
366
+ 978.6816245816854,
367
+ 0.6120426122179327,
368
+ -5.393762657505621,
369
+ -11.16348574493176,
370
+ -2.2680355887737136,
371
+ 0.21808458899551153,
372
+ -1.107812340721413
373
+ ],
374
+ "mean": [
375
+ 0.0,
376
+ 971.0885305749924,
377
+ 64.04745968968665,
378
+ 60.8586191099612,
379
+ -3.1888405779846396,
380
+ 1.5728627928202008,
381
+ 1.6186492242166108
382
+ ],
383
+ "scale": [
384
+ 1.0,
385
+ 98.45100928052985,
386
+ 33.671915141098914,
387
+ 21.924313547692126,
388
+ 27.825420835081715,
389
+ 58.230564926174964,
390
+ 6.192075094590328
391
+ ]
392
+ },
393
+ {
394
+ "beta": [
395
+ 979.3450283438027,
396
+ 0.7319180819424854,
397
+ -5.008376019672533,
398
+ -10.768778319983676,
399
+ -2.0429282545741065,
400
+ 0.3674105985052596,
401
+ -1.2015351802290106
402
+ ],
403
+ "mean": [
404
+ 0.0,
405
+ 970.8610387620653,
406
+ 63.89229354986977,
407
+ 59.479008114427415,
408
+ -4.413285438072377,
409
+ 1.5874061590317143,
410
+ 1.6341351309943313
411
+ ],
412
+ "scale": [
413
+ 1.0,
414
+ 100.23062800348737,
415
+ 33.674191443192385,
416
+ 21.208226375492284,
417
+ 29.24792990211032,
418
+ 58.4371110933118,
419
+ 6.184937924572314
420
+ ]
421
+ },
422
+ {
423
+ "beta": [
424
+ 980.105604446503,
425
+ 0.7666194688253678,
426
+ -4.689530902345973,
427
+ -10.289081391756739,
428
+ -1.8255623125939082,
429
+ 0.4955171144086594,
430
+ -1.3491949486587504
431
+ ],
432
+ "mean": [
433
+ 0.0,
434
+ 970.6553960166743,
435
+ 63.68905357418558,
436
+ 58.03452988909188,
437
+ -5.654523683032338,
438
+ 1.6220472440944882,
439
+ 1.6265246255982708
440
+ ],
441
+ "scale": [
442
+ 1.0,
443
+ 102.0201408123609,
444
+ 33.62765022480105,
445
+ 20.715588686897885,
446
+ 30.3788753865059,
447
+ 58.65838429251217,
448
+ 6.17016637339649
449
+ ]
450
+ },
451
+ {
452
+ "beta": [
453
+ 980.9283154121864,
454
+ 0.7315739553554528,
455
+ -4.397436869389718,
456
+ -9.8222155905967,
457
+ -1.6061360661083666,
458
+ 0.6129502278031091,
459
+ -1.459642620721356
460
+ ],
461
+ "mean": [
462
+ 0.0,
463
+ 970.4040829047842,
464
+ 63.52236247467664,
465
+ 56.605309408885475,
466
+ -6.917053074410943,
467
+ 1.6501480442574412,
468
+ 1.619448340345956
469
+ ],
470
+ "scale": [
471
+ 1.0,
472
+ 103.88514324892287,
473
+ 33.61229933848826,
474
+ 20.186672231267604,
475
+ 31.42790190641402,
476
+ 58.92879090161479,
477
+ 6.170992037104559
478
+ ]
479
+ },
480
+ {
481
+ "beta": [
482
+ 981.8252993068684,
483
+ 0.5928998786610551,
484
+ -4.101469153326283,
485
+ -9.42271938876919,
486
+ -1.4771057284324305,
487
+ 0.663139976586497,
488
+ -1.582271831745498
489
+ ],
490
+ "mean": [
491
+ 0.0,
492
+ 970.11468178954,
493
+ 63.39004410838059,
494
+ 55.095669031969464,
495
+ -8.29437507190416,
496
+ 1.6852551984877127,
497
+ 1.613421550094518
498
+ ],
499
+ "scale": [
500
+ 1.0,
501
+ 105.82467616006231,
502
+ 33.61784905160175,
503
+ 19.689630541879367,
504
+ 32.26821406536604,
505
+ 59.243797369533446,
506
+ 6.163178774221445
507
+ ]
508
+ },
509
+ {
510
+ "beta": [
511
+ 982.781539444267,
512
+ 0.6025154669117084,
513
+ -3.823209689209051,
514
+ -9.064186918081038,
515
+ -1.3626446088366062,
516
+ 0.659595056954032,
517
+ -1.7078633128626304
518
+ ],
519
+ "mean": [
520
+ 0.0,
521
+ 970.1533056531459,
522
+ 63.22868093260939,
523
+ 53.804770275488785,
524
+ -9.423910663973901,
525
+ 1.7376237623762376,
526
+ 1.6045991695943789
527
+ ],
528
+ "scale": [
529
+ 1.0,
530
+ 106.49924076137351,
531
+ 33.55507285388263,
532
+ 19.118239462204585,
533
+ 32.921422156523604,
534
+ 59.64370137739178,
535
+ 6.154107714919649
536
+ ]
537
+ },
538
+ {
539
+ "beta": [
540
+ 983.7722258273848,
541
+ 0.6687148174280548,
542
+ -3.5729812231535414,
543
+ -8.792480353894183,
544
+ -1.3003606126715186,
545
+ 0.499498994331008,
546
+ -1.6753764571780796
547
+ ],
548
+ "mean": [
549
+ 0.0,
550
+ 970.1787800129786,
551
+ 63.05759247242051,
552
+ 52.68523681194731,
553
+ -10.372355683216563,
554
+ 1.6383841661258922,
555
+ 1.5837118754055808
556
+ ],
557
+ "scale": [
558
+ 1.0,
559
+ 107.29605632482229,
560
+ 33.53430196992395,
561
+ 18.571850136403008,
562
+ 33.45834533772734,
563
+ 58.75240472843225,
564
+ 6.141393220321764
565
+ ]
566
+ },
567
+ {
568
+ "beta": [
569
+ 984.8051476654017,
570
+ 0.7936591779096613,
571
+ -3.3305818529271134,
572
+ -8.53375512175729,
573
+ -1.2606585077726875,
574
+ 0.3600260216844111,
575
+ -1.762847372231583
576
+ ],
577
+ "mean": [
578
+ 0.0,
579
+ 970.1988120772149,
580
+ 62.87163834350767,
581
+ 51.340679551052034,
582
+ -11.5309587998509,
583
+ 1.3928394654347467,
584
+ 1.5367101138426003
585
+ ],
586
+ "scale": [
587
+ 1.0,
588
+ 108.15524982595403,
589
+ 33.52528278502775,
590
+ 18.101358971225256,
591
+ 33.91105548737279,
592
+ 56.37757439013659,
593
+ 6.0910850383445165
594
+ ]
595
+ },
596
+ {
597
+ "beta": [
598
+ 985.8285618053217,
599
+ 0.9087397364417128,
600
+ -3.022972086698143,
601
+ -8.322423688142754,
602
+ -1.3446136356566873,
603
+ 0.3946433377092663,
604
+ -1.7388411472291765
605
+ ],
606
+ "mean": [
607
+ 0.0,
608
+ 970.2800606264735,
609
+ 62.76507241495453,
610
+ 49.89703195558408,
611
+ -12.868040449894732,
612
+ 1.1162007409902324,
613
+ 1.5151566183900302
614
+ ],
615
+ "scale": [
616
+ 1.0,
617
+ 108.48954799478317,
618
+ 33.561878314366474,
619
+ 17.68762257887688,
620
+ 34.08294834352858,
621
+ 53.89123657113572,
622
+ 6.0795658479880625
623
+ ]
624
+ },
625
+ {
626
+ "beta": [
627
+ 986.8479875626188,
628
+ 0.9988686965957757,
629
+ -2.6834677443169466,
630
+ -8.099529947859729,
631
+ -1.4667797091389447,
632
+ 0.5844770131345003,
633
+ -1.5645908925443726
634
+ ],
635
+ "mean": [
636
+ 0.0,
637
+ 970.663672482294,
638
+ 62.6716185869753,
639
+ 48.54065532316958,
640
+ -14.130963267924193,
641
+ 0.833822767317326,
642
+ 1.4964588011746416
643
+ ],
644
+ "scale": [
645
+ 1.0,
646
+ 107.56308024859545,
647
+ 33.588233290907006,
648
+ 17.283634363151315,
649
+ 33.904315820004165,
650
+ 51.321735772531284,
651
+ 6.081771020585556
652
+ ]
653
+ }
654
+ ],
655
+ "pressure_joint_features": [
656
+ "bias",
657
+ "current_pressure_hpa",
658
+ "current_wind_kt",
659
+ "calibrated_predicted_wind_kt",
660
+ "raw_predicted_pressure_hpa",
661
+ "raw_predicted_pressure_minus_current_hpa",
662
+ "calibrated_predicted_wind_minus_current_kt",
663
+ "current_pressure_minus_previous_pressure_hpa",
664
+ "current_wind_minus_previous_wind_kt"
665
+ ],
666
+ "pressure_joint_calibrations": [
667
+ {
668
+ "beta": [
669
+ 979.3647389439432,
670
+ 2.1244914734848255,
671
+ -6.002552197162719,
672
+ -6.110930904804655,
673
+ 10.470565126637606,
674
+ 3.817114397046572,
675
+ 0.5822412387685045,
676
+ 0.16010090364060767,
677
+ -1.5439923636219977
678
+ ],
679
+ "mean": [
680
+ 0.0,
681
+ 978.1810383079426,
682
+ 64.5815707735542,
683
+ 65.6266441483449,
684
+ 979.0951558127063,
685
+ 0.9141175047636491,
686
+ 1.0450733747906875,
687
+ 1.905043632598728,
688
+ 1.4701967164620617
689
+ ],
690
+ "scale": [
691
+ 1.0,
692
+ 50.53637228468569,
693
+ 33.10783692877566,
694
+ 32.124343617572436,
695
+ 24.762051237279874,
696
+ 39.796705658565024,
697
+ 4.159573246659055,
698
+ 54.79089485626104,
699
+ 6.246907173086729
700
+ ]
701
+ },
702
+ {
703
+ "beta": [
704
+ 978.6045857988166,
705
+ 0.8713799654438338,
706
+ -7.7527258211177985,
707
+ -8.237667857241275,
708
+ 6.714629045232456,
709
+ 2.1450799243690115,
710
+ 0.018155804213034234,
711
+ 0.2910972149675519,
712
+ -2.3186426773038207
713
+ ],
714
+ "mean": [
715
+ 0.0,
716
+ 976.7190828402366,
717
+ 64.52144970414201,
718
+ 66.21541114460082,
719
+ 978.656016247089,
720
+ 1.9369334068524062,
721
+ 1.6939614404588057,
722
+ 1.9063609467455622,
723
+ 1.4738165680473372
724
+ ],
725
+ "scale": [
726
+ 1.0,
727
+ 62.987212868626365,
728
+ 33.18977815879015,
729
+ 31.219218256081252,
730
+ 25.2167769986195,
731
+ 53.347899343611246,
732
+ 7.58431677406538,
733
+ 54.79480783300083,
734
+ 6.245251042331157
735
+ ]
736
+ },
737
+ {
738
+ "beta": [
739
+ 978.0161338069863,
740
+ 0.42796089044169344,
741
+ -8.495149928651433,
742
+ -9.72355373046241,
743
+ 4.306850308592872,
744
+ 1.2549277151505402,
745
+ -0.9592509347076426,
746
+ -0.05607486236487535,
747
+ -2.032501028079223
748
+ ],
749
+ "mean": [
750
+ 0.0,
751
+ 975.2491119005329,
752
+ 64.46758436944938,
753
+ 66.03674610847548,
754
+ 978.6352417010256,
755
+ 3.3861298004927334,
756
+ 1.5691617390261097,
757
+ 1.7575488454706927,
758
+ 1.4773534635879217
759
+ ],
760
+ "scale": [
761
+ 1.0,
762
+ 73.34317349947774,
763
+ 33.277770226738575,
764
+ 30.11070808190084,
765
+ 26.15742021970667,
766
+ 64.75917499444205,
767
+ 10.511785983418822,
768
+ 56.14964419892791,
769
+ 6.253661273135333
770
+ ]
771
+ },
772
+ {
773
+ "beta": [
774
+ 977.6126259632483,
775
+ 0.25733740761036766,
776
+ -8.223026850989438,
777
+ -10.354225400510941,
778
+ 3.7337223226615857,
779
+ 1.0377977674639305,
780
+ -1.9363265512130752,
781
+ -0.3109780621723116,
782
+ -1.4713391665546236
783
+ ],
784
+ "mean": [
785
+ 0.0,
786
+ 973.9270895080024,
787
+ 64.41123295791346,
788
+ 65.7963597440974,
789
+ 978.8774953411398,
790
+ 4.950405833137481,
791
+ 1.3851267861839434,
792
+ 1.5952874925903973,
793
+ 1.4903675163011263
794
+ ],
795
+ "scale": [
796
+ 1.0,
797
+ 81.55905643779808,
798
+ 33.37057257362826,
799
+ 28.928456009421513,
800
+ 26.279899465884235,
801
+ 74.324356368596,
802
+ 12.97541456694104,
803
+ 57.48200889919564,
804
+ 6.2549372779430845
805
+ ]
806
+ },
807
+ {
808
+ "beta": [
809
+ 977.3788958147819,
810
+ 0.19565831212711526,
811
+ -7.627340048103773,
812
+ -10.525660728364645,
813
+ 3.65180442270761,
814
+ 0.9114316402072032,
815
+ -2.4560523652352155,
816
+ -0.5048895919362318,
817
+ -1.2221630437590159
818
+ ],
819
+ "mean": [
820
+ 0.0,
821
+ 972.7582368655387,
822
+ 64.35366577619472,
823
+ 65.43843451049428,
824
+ 979.1345288578711,
825
+ 6.376291992332433,
826
+ 1.0847687342995598,
827
+ 1.5890471950133571,
828
+ 1.5031166518254675
829
+ ],
830
+ "scale": [
831
+ 1.0,
832
+ 88.23866281788312,
833
+ 33.456506355578135,
834
+ 27.78207056922819,
835
+ 25.33951308468857,
836
+ 82.58471046960283,
837
+ 15.162746063357607,
838
+ 57.524265495181936,
839
+ 6.247664265837107
840
+ ]
841
+ },
842
+ {
843
+ "beta": [
844
+ 977.2594465932758,
845
+ 0.21130054047204685,
846
+ -7.018341900403828,
847
+ -10.58329505292094,
848
+ 3.5920580788574266,
849
+ 0.8153638167483641,
850
+ -2.727568831621463,
851
+ -0.5801340900358954,
852
+ -1.1526400476893242
853
+ ],
854
+ "mean": [
855
+ 0.0,
856
+ 972.4617673311515,
857
+ 64.34275513240107,
858
+ 64.95803123366723,
859
+ 979.3716344541115,
860
+ 6.909867122959999,
861
+ 0.6152761012661567,
862
+ 1.584498661112764,
863
+ 1.5165129425766142
864
+ ],
865
+ "scale": [
866
+ 1.0,
867
+ 89.9186470511227,
868
+ 33.49768138488822,
869
+ 26.6383543934097,
870
+ 24.546593638967988,
871
+ 84.83689103290898,
872
+ 17.166710109155744,
873
+ 57.59243440899489,
874
+ 6.244839191073782
875
+ ]
876
+ },
877
+ {
878
+ "beta": [
879
+ 977.275208830549,
880
+ 0.2227503073241216,
881
+ -6.426683692995159,
882
+ -10.520635051916873,
883
+ 3.5429980271404626,
884
+ 0.74185897909257,
885
+ -2.8219107567360324,
886
+ -0.6004254146464897,
887
+ -1.1561584357339976
888
+ ],
889
+ "mean": [
890
+ 0.0,
891
+ 972.166915274463,
892
+ 64.32801312649164,
893
+ 64.58668019188688,
894
+ 979.5777262002586,
895
+ 7.410810925795525,
896
+ 0.25866706539523343,
897
+ 1.5853221957040573,
898
+ 1.527744630071599
899
+ ],
900
+ "scale": [
901
+ 1.0,
902
+ 91.5873950283228,
903
+ 33.53705182833371,
904
+ 25.602260620153988,
905
+ 23.943168400019932,
906
+ 86.8486866400537,
907
+ 19.072189386922403,
908
+ 57.6690743414348,
909
+ 6.241065684632235
910
+ ]
911
+ },
912
+ {
913
+ "beta": [
914
+ 977.4265828468792,
915
+ 0.23446887366411298,
916
+ -5.841713308306412,
917
+ -10.298255141336625,
918
+ 3.538099855573987,
919
+ 0.6848819961434182,
920
+ -2.804375619473821,
921
+ -0.5602620390530704,
922
+ -1.1905193358282722
923
+ ],
924
+ "mean": [
925
+ 0.0,
926
+ 971.8718754677443,
927
+ 64.30399640772339,
928
+ 63.996917009317954,
929
+ 979.8604918721014,
930
+ 7.9886164043570975,
931
+ -0.30707939897642006,
932
+ 1.5837449483610238,
933
+ 1.545577009429726
934
+ ],
935
+ "scale": [
936
+ 1.0,
937
+ 93.26701702857976,
938
+ 33.579947436293786,
939
+ 24.703194308095586,
940
+ 23.414953388343157,
941
+ 89.03173262580074,
942
+ 20.765893804880417,
943
+ 57.76717976524845,
944
+ 6.2318182994660445
945
+ ]
946
+ },
947
+ {
948
+ "beta": [
949
+ 977.7077824519231,
950
+ 0.2731983522105385,
951
+ -5.277213350632824,
952
+ -10.088854720334119,
953
+ 3.4939775555802064,
954
+ 0.590462787082304,
955
+ -2.808858166118079,
956
+ -0.5226757418073821,
957
+ -1.1750023522827215
958
+ ],
959
+ "mean": [
960
+ 0.0,
961
+ 971.5954026442307,
962
+ 64.2549579326923,
963
+ 63.085182326989106,
964
+ 980.3641269023602,
965
+ 8.768724258129414,
966
+ -1.1697756054166417,
967
+ 1.5760216346153846,
968
+ 1.5689603365384615
969
+ ],
970
+ "scale": [
971
+ 1.0,
972
+ 94.93760398672073,
973
+ 33.613050887448374,
974
+ 23.75717496005899,
975
+ 22.878541050644525,
976
+ 91.45421724246802,
977
+ 22.17963402594675,
978
+ 57.87417128115503,
979
+ 6.217322594269493
980
+ ]
981
+ },
982
+ {
983
+ "beta": [
984
+ 978.1375037775763,
985
+ 0.3554194868427499,
986
+ -4.767789317327665,
987
+ -9.900624192069188,
988
+ 3.3333618901542037,
989
+ 0.42821914484805135,
990
+ -2.7793970447450915,
991
+ -0.40881233039958326,
992
+ -1.1973209086991168
993
+ ],
994
+ "mean": [
995
+ 0.0,
996
+ 971.3303112722876,
997
+ 64.17527954064673,
998
+ 62.25949261227415,
999
+ 981.2040692465151,
1000
+ 9.873757974227486,
1001
+ -1.9157869289489844,
1002
+ 1.5754004230885463,
1003
+ 1.5929283771532186
1004
+ ],
1005
+ "scale": [
1006
+ 1.0,
1007
+ 96.6811703822946,
1008
+ 33.65355257692086,
1009
+ 22.790069308723673,
1010
+ 22.401349860189654,
1011
+ 94.13272251001614,
1012
+ 23.452160129348044,
1013
+ 58.03843631538493,
1014
+ 6.204621897205797
1015
+ ]
1016
+ },
1017
+ {
1018
+ "beta": [
1019
+ 978.6816245816854,
1020
+ 0.45700856193464073,
1021
+ -4.282115832870207,
1022
+ -9.676417705655844,
1023
+ 3.202586323985946,
1024
+ 0.263424547765887,
1025
+ -2.744635628620983,
1026
+ -0.2812257628990227,
1027
+ -1.2145691549473818
1028
+ ],
1029
+ "mean": [
1030
+ 0.0,
1031
+ 971.0885305749924,
1032
+ 64.04745968968665,
1033
+ 61.24103552841074,
1034
+ 982.0910472678383,
1035
+ 11.002516692845989,
1036
+ -2.806424161275905,
1037
+ 1.5728627928202008,
1038
+ 1.6186492242166108
1039
+ ],
1040
+ "scale": [
1041
+ 1.0,
1042
+ 98.45100928052985,
1043
+ 33.671915141098914,
1044
+ 21.846830745260633,
1045
+ 22.00199125306356,
1046
+ 96.68925057678072,
1047
+ 24.488503386648656,
1048
+ 58.230564926174964,
1049
+ 6.192075094590328
1050
+ ]
1051
+ },
1052
+ {
1053
+ "beta": [
1054
+ 979.3450283438027,
1055
+ 0.5026116383483132,
1056
+ -3.815461840782289,
1057
+ -9.321996789582482,
1058
+ 3.1706830476456243,
1059
+ 0.1869869500870577,
1060
+ -2.67047300578785,
1061
+ -0.13156395947336183,
1062
+ -1.321484648871915
1063
+ ],
1064
+ "mean": [
1065
+ 0.0,
1066
+ 970.8610387620653,
1067
+ 63.89229354986977,
1068
+ 60.07496924171664,
1069
+ 983.040380796722,
1070
+ 12.179342034656822,
1071
+ -3.8173243087375806,
1072
+ 1.5874061590317143,
1073
+ 1.6341351309943313
1074
+ ],
1075
+ "scale": [
1076
+ 1.0,
1077
+ 100.23062800348737,
1078
+ 33.674191443192385,
1079
+ 21.02995207203543,
1080
+ 21.69896940964998,
1081
+ 98.52812879500274,
1082
+ 25.29834728692795,
1083
+ 58.4371110933118,
1084
+ 6.184937924572314
1085
+ ]
1086
+ },
1087
+ {
1088
+ "beta": [
1089
+ 980.105604446503,
1090
+ 0.4819710508401576,
1091
+ -3.454897296475616,
1092
+ -8.997754477229522,
1093
+ 2.9217652089719137,
1094
+ 0.13715468467271474,
1095
+ -2.6075446851545543,
1096
+ 0.031810072532767186,
1097
+ -1.467156119894784
1098
+ ],
1099
+ "mean": [
1100
+ 0.0,
1101
+ 970.6553960166743,
1102
+ 63.68905357418558,
1103
+ 58.87242237660589,
1104
+ 983.8862911363949,
1105
+ 13.230895119720502,
1106
+ -4.816631199052089,
1107
+ 1.6220472440944882,
1108
+ 1.6265246255982708
1109
+ ],
1110
+ "scale": [
1111
+ 1.0,
1112
+ 102.0201408123609,
1113
+ 33.62765022480105,
1114
+ 20.411349603025727,
1115
+ 21.523634442810142,
1116
+ 100.00571132508524,
1117
+ 25.877311290276214,
1118
+ 58.65838429251217,
1119
+ 6.17016637339649
1120
+ ]
1121
+ },
1122
+ {
1123
+ "beta": [
1124
+ 980.9283154121864,
1125
+ 0.42406183474284354,
1126
+ -3.101795843309463,
1127
+ -8.59816263901043,
1128
+ 2.823608314829573,
1129
+ 0.16132514777708762,
1130
+ -2.4809137481393724,
1131
+ 0.1604517720706006,
1132
+ -1.5813368102627405
1133
+ ],
1134
+ "mean": [
1135
+ 0.0,
1136
+ 970.4040829047842,
1137
+ 63.52236247467664,
1138
+ 57.70297459142684,
1139
+ 984.7348281791393,
1140
+ 14.330745274355168,
1141
+ -5.819387881169164,
1142
+ 1.6501480442574412,
1143
+ 1.619448340345956
1144
+ ],
1145
+ "scale": [
1146
+ 1.0,
1147
+ 103.88514324892287,
1148
+ 33.61229933848826,
1149
+ 19.754848726816206,
1150
+ 21.389626694015444,
1151
+ 101.29978840472074,
1152
+ 26.440624986798476,
1153
+ 58.92879090161479,
1154
+ 6.170992037104559
1155
+ ]
1156
+ },
1157
+ {
1158
+ "beta": [
1159
+ 981.8252993068684,
1160
+ 0.3282559402948829,
1161
+ -2.7917770927328625,
1162
+ -8.299277606331454,
1163
+ 2.6249222689964187,
1164
+ 0.20733431960316168,
1165
+ -2.409438660750097,
1166
+ 0.24058273814096195,
1167
+ -1.7023733781274468
1168
+ ],
1169
+ "mean": [
1170
+ 0.0,
1171
+ 970.11468178954,
1172
+ 63.39004410838059,
1173
+ 56.4489657036824,
1174
+ 985.5594694910368,
1175
+ 15.44478770149678,
1176
+ -6.941078406200517,
1177
+ 1.6852551984877127,
1178
+ 1.613421550094518
1179
+ ],
1180
+ "scale": [
1181
+ 1.0,
1182
+ 105.82467616006231,
1183
+ 33.61784905160175,
1184
+ 19.14823400203281,
1185
+ 21.34911374648086,
1186
+ 102.74317126116405,
1187
+ 27.003378378170513,
1188
+ 59.243797369533446,
1189
+ 6.163178774221445
1190
+ ]
1191
+ },
1192
+ {
1193
+ "beta": [
1194
+ 982.781539444267,
1195
+ 0.31618464576562727,
1196
+ -2.511209001172497,
1197
+ -7.999402402069606,
1198
+ 2.497911494808863,
1199
+ 0.18749180315001615,
1200
+ -2.3085711754224185,
1201
+ 0.2585546959223717,
1202
+ -1.8317178283595457
1203
+ ],
1204
+ "mean": [
1205
+ 0.0,
1206
+ 970.1533056531459,
1207
+ 63.22868093260939,
1208
+ 55.3443473903031,
1209
+ 986.1220949673112,
1210
+ 15.96878931416524,
1211
+ -7.884333544438428,
1212
+ 1.7376237623762376,
1213
+ 1.6045991695943789
1214
+ ],
1215
+ "scale": [
1216
+ 1.0,
1217
+ 106.49924076137351,
1218
+ 33.55507285388263,
1219
+ 18.482496364875466,
1220
+ 21.217461982998845,
1221
+ 103.07606579674263,
1222
+ 27.54307460962646,
1223
+ 59.64370137739178,
1224
+ 6.154107714919649
1225
+ ]
1226
+ },
1227
+ {
1228
+ "beta": [
1229
+ 983.7722258273848,
1230
+ 0.33981880304597933,
1231
+ -2.2724190975909124,
1232
+ -7.716540651393952,
1233
+ 2.487078414467849,
1234
+ 0.15245280072872294,
1235
+ -2.188940669353884,
1236
+ 0.08654409924485539,
1237
+ -1.8049092360904309
1238
+ ],
1239
+ "mean": [
1240
+ 0.0,
1241
+ 970.1787800129786,
1242
+ 63.05759247242051,
1243
+ 54.32578140431453,
1244
+ 987.0499083947857,
1245
+ 16.871128381807104,
1246
+ -8.731811068724843,
1247
+ 1.6383841661258922,
1248
+ 1.5837118754055808
1249
+ ],
1250
+ "scale": [
1251
+ 1.0,
1252
+ 107.29605632482229,
1253
+ 33.53430196992395,
1254
+ 17.865324126234672,
1255
+ 21.019255280469395,
1256
+ 103.73911523783231,
1257
+ 28.166402893346593,
1258
+ 58.75240472843225,
1259
+ 6.141393220321764
1260
+ ]
1261
+ },
1262
+ {
1263
+ "beta": [
1264
+ 984.8051476654017,
1265
+ 0.3951285735895073,
1266
+ -2.045812347972766,
1267
+ -7.401342794020973,
1268
+ 2.5680992481457343,
1269
+ 0.10213632471215839,
1270
+ -2.0796369943547277,
1271
+ -0.0974652163959373,
1272
+ -1.9027420660166647
1273
+ ],
1274
+ "mean": [
1275
+ 0.0,
1276
+ 970.1988120772149,
1277
+ 62.87163834350767,
1278
+ 53.108948937069755,
1279
+ 988.0273557485792,
1280
+ 17.82854367136424,
1281
+ -9.76268940423507,
1282
+ 1.3928394654347467,
1283
+ 1.5367101138426003
1284
+ ],
1285
+ "scale": [
1286
+ 1.0,
1287
+ 108.15524982595403,
1288
+ 33.52528278502775,
1289
+ 17.333953998949035,
1290
+ 20.799190418897737,
1291
+ 104.55786154820235,
1292
+ 28.71080692804529,
1293
+ 56.37757439013659,
1294
+ 6.0910850383445165
1295
+ ]
1296
+ },
1297
+ {
1298
+ "beta": [
1299
+ 985.8285618053217,
1300
+ 0.44350304929556517,
1301
+ -1.8276319568600226,
1302
+ -7.161537529233555,
1303
+ 2.612368345826643,
1304
+ 0.055391102093499026,
1305
+ -2.0487106124521177,
1306
+ -0.10864333910550422,
1307
+ -1.8863535960463707
1308
+ ],
1309
+ "mean": [
1310
+ 0.0,
1311
+ 970.2800606264735,
1312
+ 62.76507241495453,
1313
+ 51.71612292526145,
1314
+ 989.0245838364434,
1315
+ 18.744523209969817,
1316
+ -11.048949497080926,
1317
+ 1.1162007409902324,
1318
+ 1.5151566183900302
1319
+ ],
1320
+ "scale": [
1321
+ 1.0,
1322
+ 108.48954799478317,
1323
+ 33.561878314366474,
1324
+ 16.93686204423229,
1325
+ 20.63978997941638,
1326
+ 104.76933022718211,
1327
+ 29.264807512918956,
1328
+ 53.89123657113572,
1329
+ 6.0795658479880625
1330
+ ]
1331
+ },
1332
+ {
1333
+ "beta": [
1334
+ 986.8479875626188,
1335
+ 0.470340939618217,
1336
+ -1.6504285707864457,
1337
+ -6.963977969610529,
1338
+ 2.5909179161370255,
1339
+ 0.02709494425615321,
1340
+ -2.0348327401780897,
1341
+ 0.045200539893647855,
1342
+ -1.722190325073895
1343
+ ],
1344
+ "mean": [
1345
+ 0.0,
1346
+ 970.663672482294,
1347
+ 62.6716185869753,
1348
+ 50.29897747716873,
1349
+ 989.9238731027123,
1350
+ 19.260200620418303,
1351
+ -12.372641116066658,
1352
+ 0.833822767317326,
1353
+ 1.4964588011746416
1354
+ ],
1355
+ "scale": [
1356
+ 1.0,
1357
+ 107.56308024859545,
1358
+ 33.588233290907006,
1359
+ 16.634206748030838,
1360
+ 20.60855568110453,
1361
+ 103.4789331077719,
1362
+ 29.685587447486235,
1363
+ 51.321735772531284,
1364
+ 6.081771020585556
1365
+ ]
1366
+ }
1367
+ ],
1368
+ "pressure_anchor_alpha": [
1369
+ 0.5522160094389178,
1370
+ 0.6962881299556641,
1371
+ 0.7642130854290704,
1372
+ 0.8250283768554111,
1373
+ 0.8682641709344661,
1374
+ 0.8950311405808169,
1375
+ 0.912311094958641,
1376
+ 0.9251489323454353,
1377
+ 0.9362969190603518,
1378
+ 0.9450910275730012,
1379
+ 0.953263937622913,
1380
+ 0.9605904396465027,
1381
+ 0.9650098139429872,
1382
+ 0.9694222567928482,
1383
+ 0.973242185094525,
1384
+ 0.9765976015127111,
1385
+ 0.9792250259796094,
1386
+ 0.9817388698882592,
1387
+ 0.9835805375991149,
1388
+ 0.9838229441690179
1389
+ ],
1390
+ "pressure_anchor_policy": "validation-fitted blend with valid observed current pressure; no blend when current pressure is missing",
1391
+ "wind_blend_alpha": [
1392
+ 1.0277212275681893,
1393
+ 1.1489795568075691,
1394
+ 1.0663403998709395,
1395
+ 1.0186010559120267,
1396
+ 0.987967013306198,
1397
+ 0.9650641191631195,
1398
+ 0.9444263916806881,
1399
+ 0.9282677834453357,
1400
+ 0.9083684904148223,
1401
+ 0.8944074477909971,
1402
+ 0.8800766496867902,
1403
+ 0.8649619816342672,
1404
+ 0.8518192654447947,
1405
+ 0.8413105316408789,
1406
+ 0.8368414301404576,
1407
+ 0.8366307550923628,
1408
+ 0.8418348967136898,
1409
+ 0.8466503456441108,
1410
+ 0.8586348526797178,
1411
+ 0.8755695728454723
1412
+ ],
1413
+ "structure_blend_features": [
1414
+ "current_rmw_or_radius_native_nm",
1415
+ "primary_structure_native_nm",
1416
+ "optional_secondary_structure_expert_native_nm"
1417
+ ],
1418
+ "structure_blend_alpha": [
1419
+ [
1420
+ 1.0,
1421
+ 1.0,
1422
+ 0.5549493412791997,
1423
+ 1.0,
1424
+ 0.9753869181876241,
1425
+ 0.6093764778483237,
1426
+ 0.6047313500745581,
1427
+ 0.6098850823506468,
1428
+ 0.592881985511388,
1429
+ 0.42590516633443765,
1430
+ 0.6195051390738097,
1431
+ 1.0,
1432
+ 0.5608806719907258
1433
+ ],
1434
+ [
1435
+ 1.0,
1436
+ 1.0,
1437
+ 0.7786456821183296,
1438
+ 1.0,
1439
+ 1.0,
1440
+ 0.8160746904360723,
1441
+ 0.7729585422358642,
1442
+ 0.8425760483695786,
1443
+ 0.8142225430932971,
1444
+ 0.7046296166746046,
1445
+ 0.7969846939122845,
1446
+ 1.0,
1447
+ 0.6884011708218611
1448
+ ],
1449
+ [
1450
+ 1.0,
1451
+ 1.0,
1452
+ 0.8090536105281151,
1453
+ 1.0,
1454
+ 1.0,
1455
+ 0.8471455035922965,
1456
+ 0.7857574635319617,
1457
+ 0.8874206525034055,
1458
+ 0.860082995947976,
1459
+ 0.8239691354642307,
1460
+ 0.8772408335676136,
1461
+ 1.0,
1462
+ 0.7127141967586793
1463
+ ],
1464
+ [
1465
+ 1.0,
1466
+ 1.0,
1467
+ 0.7979206267482302,
1468
+ 1.0,
1469
+ 1.0,
1470
+ 0.8513245701071473,
1471
+ 0.7557281044138736,
1472
+ 0.8953281132435532,
1473
+ 0.8823452204080448,
1474
+ 0.8994108661181437,
1475
+ 0.935888284206547,
1476
+ 1.0,
1477
+ 0.7614745781233312
1478
+ ],
1479
+ [
1480
+ 1.0,
1481
+ 1.0,
1482
+ 0.7889656833371654,
1483
+ 1.0,
1484
+ 0.9995282276968595,
1485
+ 0.813974979553649,
1486
+ 0.7261373602450153,
1487
+ 0.8520991012979712,
1488
+ 0.8571141216277187,
1489
+ 0.9547290251465658,
1490
+ 0.959194489030165,
1491
+ 1.0,
1492
+ 0.8093554855049242
1493
+ ],
1494
+ [
1495
+ 1.0,
1496
+ 1.0,
1497
+ 0.790841522213487,
1498
+ 1.0,
1499
+ 0.989425399763569,
1500
+ 0.7677379120463775,
1501
+ 0.6804313851656493,
1502
+ 0.8081026737119595,
1503
+ 0.8160784965854719,
1504
+ 0.9553568730634433,
1505
+ 0.94906212902518,
1506
+ 1.0,
1507
+ 0.8506601404079891
1508
+ ],
1509
+ [
1510
+ 1.0,
1511
+ 1.0,
1512
+ 0.7946505021912497,
1513
+ 1.0,
1514
+ 0.9842208422424614,
1515
+ 0.7309946140644263,
1516
+ 0.6519656259054535,
1517
+ 0.7832582102291513,
1518
+ 0.7751977883552036,
1519
+ 0.9498443768015948,
1520
+ 0.9364057805619501,
1521
+ 1.0,
1522
+ 0.8686047360783458
1523
+ ],
1524
+ [
1525
+ 1.0,
1526
+ 1.0,
1527
+ 0.8048051919510004,
1528
+ 1.0,
1529
+ 0.977409615843526,
1530
+ 0.7224643906694298,
1531
+ 0.6365795828673083,
1532
+ 0.766592821423072,
1533
+ 0.7398517247851228,
1534
+ 0.9404076028219883,
1535
+ 0.9255773996092755,
1536
+ 1.0,
1537
+ 0.86686536561545
1538
+ ],
1539
+ [
1540
+ 0.9989969287476876,
1541
+ 1.0,
1542
+ 0.8060150457554067,
1543
+ 1.0,
1544
+ 0.9635497591152855,
1545
+ 0.7298933141800126,
1546
+ 0.6276519134653644,
1547
+ 0.7587540062356607,
1548
+ 0.7370947132207954,
1549
+ 0.9282816956429373,
1550
+ 0.8960508524231412,
1551
+ 1.0,
1552
+ 0.8517048378522423
1553
+ ],
1554
+ [
1555
+ 0.9962949755088357,
1556
+ 1.0,
1557
+ 0.8024267962891638,
1558
+ 1.0,
1559
+ 0.9532191018037218,
1560
+ 0.7414856091287201,
1561
+ 0.629047832296785,
1562
+ 0.7588638521544967,
1563
+ 0.7411730238585288,
1564
+ 0.9244463977489501,
1565
+ 0.896517586802664,
1566
+ 1.0,
1567
+ 0.8363283369905071
1568
+ ],
1569
+ [
1570
+ 0.9906150062159008,
1571
+ 0.9928592943545933,
1572
+ 0.8017538447631487,
1573
+ 1.0,
1574
+ 0.9481221332985175,
1575
+ 0.7504970569022518,
1576
+ 0.6437585926652084,
1577
+ 0.7682739191164304,
1578
+ 0.7458488449448917,
1579
+ 0.9016502567528687,
1580
+ 0.9196549454100816,
1581
+ 1.0,
1582
+ 0.8222928380829917
1583
+ ],
1584
+ [
1585
+ 0.9872786368824288,
1586
+ 0.9870256484759862,
1587
+ 0.8036197038036084,
1588
+ 1.0,
1589
+ 0.9456840187031937,
1590
+ 0.7536197843475184,
1591
+ 0.6720399020859967,
1592
+ 0.7861770994146164,
1593
+ 0.7545241776213014,
1594
+ 0.8622271776091911,
1595
+ 0.9544728012066145,
1596
+ 1.0,
1597
+ 0.8091403753140322
1598
+ ],
1599
+ [
1600
+ 0.9858912730817866,
1601
+ 0.9782260755748675,
1602
+ 0.8224480738020116,
1603
+ 1.0,
1604
+ 0.9408562399433626,
1605
+ 0.751923185451568,
1606
+ 0.7079975704373077,
1607
+ 0.8081543972650469,
1608
+ 0.7607623883181744,
1609
+ 0.838646332479633,
1610
+ 1.0,
1611
+ 1.0,
1612
+ 0.8165953708628378
1613
+ ],
1614
+ [
1615
+ 0.9871317357369944,
1616
+ 0.9675020524431993,
1617
+ 0.8435905187514622,
1618
+ 1.0,
1619
+ 0.9354981762431331,
1620
+ 0.7469631531510479,
1621
+ 0.746366785020888,
1622
+ 0.8291036037207948,
1623
+ 0.7566102823880994,
1624
+ 0.8383450731194783,
1625
+ 1.0,
1626
+ 1.0,
1627
+ 0.826557900171084
1628
+ ],
1629
+ [
1630
+ 0.9854462797686748,
1631
+ 0.9575337922720787,
1632
+ 0.8632940254404728,
1633
+ 1.0,
1634
+ 0.9361793399856915,
1635
+ 0.7280315743113683,
1636
+ 0.7782362972629818,
1637
+ 0.8461591629929479,
1638
+ 0.7364949444253786,
1639
+ 0.8495574180679718,
1640
+ 1.0,
1641
+ 1.0,
1642
+ 0.8319355974514233
1643
+ ],
1644
+ [
1645
+ 0.9799350216481773,
1646
+ 0.948053493097228,
1647
+ 0.8777561442871291,
1648
+ 1.0,
1649
+ 0.9389259406046586,
1650
+ 0.7268423625933376,
1651
+ 0.8056289626997586,
1652
+ 0.858630296390731,
1653
+ 0.7303639110314664,
1654
+ 0.8680875406118934,
1655
+ 1.0,
1656
+ 1.0,
1657
+ 0.8546179948062796
1658
+ ],
1659
+ [
1660
+ 0.9740716852608886,
1661
+ 0.9403381171414777,
1662
+ 0.8814982444352361,
1663
+ 1.0,
1664
+ 0.9376654486264268,
1665
+ 0.720231177359567,
1666
+ 0.8229001102851932,
1667
+ 0.883352887181907,
1668
+ 0.7374999300634676,
1669
+ 0.862734447860439,
1670
+ 1.0,
1671
+ 1.0,
1672
+ 0.8757411053631797
1673
+ ],
1674
+ [
1675
+ 0.9686221325152385,
1676
+ 0.9305462545814487,
1677
+ 0.883874105956774,
1678
+ 1.0,
1679
+ 0.9383603227817988,
1680
+ 0.714941943020425,
1681
+ 0.8307300156928373,
1682
+ 0.9061038362396052,
1683
+ 0.7477493305948096,
1684
+ 0.8656451575094964,
1685
+ 1.0,
1686
+ 1.0,
1687
+ 0.9054612373084847
1688
+ ],
1689
+ [
1690
+ 0.9662039837991756,
1691
+ 0.9275664889767167,
1692
+ 0.8912243532082841,
1693
+ 1.0,
1694
+ 0.9403396402123274,
1695
+ 0.7157872514985167,
1696
+ 0.8458014076196586,
1697
+ 0.9215386465479368,
1698
+ 0.7592801106445597,
1699
+ 0.8765838611817265,
1700
+ 1.0,
1701
+ 1.0,
1702
+ 0.9177342044253676
1703
+ ],
1704
+ [
1705
+ 0.9568044542361462,
1706
+ 0.9385390035449604,
1707
+ 0.9215888189795768,
1708
+ 1.0,
1709
+ 0.9503495133295314,
1710
+ 0.7244552996295812,
1711
+ 0.8600858326127176,
1712
+ 0.9420104384137908,
1713
+ 0.7495840140906448,
1714
+ 0.9216970099860546,
1715
+ 1.0,
1716
+ 1.0,
1717
+ 0.9582402976615332
1718
+ ]
1719
+ ],
1720
+ "checkpoint_root": "models/trackformer_1_1",
1721
+ "structure_checkpoint_root": "models/trackformer_1_1",
1722
+ "structure_expert_alpha": [
1723
+ [
1724
+ 0.0,
1725
+ 0.9361481070518494,
1726
+ 0.934322714805603,
1727
+ 0.8858486413955688,
1728
+ 0.9938719272613525,
1729
+ 0.9489486813545227,
1730
+ 0.9187760949134827,
1731
+ 0.9228118062019348,
1732
+ 0.9387768507003784,
1733
+ 0.7600101828575134,
1734
+ 0.7809425592422485,
1735
+ 0.8700641393661499,
1736
+ 0.8380165100097656
1737
+ ],
1738
+ [
1739
+ 0.0,
1740
+ 0.7883517742156982,
1741
+ 0.8298157453536987,
1742
+ 0.7546190619468689,
1743
+ 0.891186535358429,
1744
+ 0.8175759315490723,
1745
+ 0.7462324500083923,
1746
+ 0.7695027589797974,
1747
+ 0.7692294716835022,
1748
+ 0.6570655107498169,
1749
+ 0.5868498086929321,
1750
+ 0.7810206413269043,
1751
+ 0.6184545159339905
1752
+ ],
1753
+ [
1754
+ 0.0,
1755
+ 0.6988456845283508,
1756
+ 0.7698739171028137,
1757
+ 0.6341904401779175,
1758
+ 0.781299352645874,
1759
+ 0.7261586785316467,
1760
+ 0.7200528383255005,
1761
+ 0.6777958273887634,
1762
+ 0.6547061204910278,
1763
+ 0.6781790852546692,
1764
+ 0.38850289583206177,
1765
+ 0.739601194858551,
1766
+ 0.4023652672767639
1767
+ ],
1768
+ [
1769
+ 0.0,
1770
+ 0.6077090501785278,
1771
+ 0.75852370262146,
1772
+ 0.5542184710502625,
1773
+ 0.668627917766571,
1774
+ 0.6401088833808899,
1775
+ 0.7013946771621704,
1776
+ 0.6870279908180237,
1777
+ 0.6323707103729248,
1778
+ 0.7411512136459351,
1779
+ 0.19386884570121765,
1780
+ 0.6939995288848877,
1781
+ 0.3757428824901581
1782
+ ],
1783
+ [
1784
+ 0.0,
1785
+ 0.5452795028686523,
1786
+ 0.8159465193748474,
1787
+ 0.5034195780754089,
1788
+ 0.5881041884422302,
1789
+ 0.6064771413803101,
1790
+ 0.7117744088172913,
1791
+ 0.6996253728866577,
1792
+ 0.6671974062919617,
1793
+ 0.7673929929733276,
1794
+ 0.1434878557920456,
1795
+ 0.6283478736877441,
1796
+ 0.3827730417251587
1797
+ ],
1798
+ [
1799
+ 0.0,
1800
+ 0.4475332498550415,
1801
+ 0.8445053696632385,
1802
+ 0.47751277685165405,
1803
+ 0.49595507979393005,
1804
+ 0.5493122339248657,
1805
+ 0.6586169004440308,
1806
+ 0.6564105749130249,
1807
+ 0.6699652671813965,
1808
+ 0.7021469473838806,
1809
+ 0.0653224065899849,
1810
+ 0.5543050765991211,
1811
+ 0.38605213165283203
1812
+ ],
1813
+ [
1814
+ 0.006327976007014513,
1815
+ 0.34530171751976013,
1816
+ 0.8709386587142944,
1817
+ 0.4365202486515045,
1818
+ 0.4342052638530731,
1819
+ 0.5322895646095276,
1820
+ 0.6248392462730408,
1821
+ 0.5894513726234436,
1822
+ 0.7012799382209778,
1823
+ 0.626562237739563,
1824
+ 0.015733113512396812,
1825
+ 0.44189080595970154,
1826
+ 0.42251747846603394
1827
+ ],
1828
+ [
1829
+ 0.005135390907526016,
1830
+ 0.2684456408023834,
1831
+ 0.8810635805130005,
1832
+ 0.4226278066635132,
1833
+ 0.39568835496902466,
1834
+ 0.5028242468833923,
1835
+ 0.6358619332313538,
1836
+ 0.5655720829963684,
1837
+ 0.7411352396011353,
1838
+ 0.5535630583763123,
1839
+ 0.0,
1840
+ 0.3738625943660736,
1841
+ 0.4239470660686493
1842
+ ],
1843
+ [
1844
+ 0.023205677047371864,
1845
+ 0.21901144087314606,
1846
+ 0.8607876896858215,
1847
+ 0.43362921476364136,
1848
+ 0.3754803538322449,
1849
+ 0.5236393809318542,
1850
+ 0.6005999445915222,
1851
+ 0.5589695572853088,
1852
+ 0.7332651615142822,
1853
+ 0.47174206376075745,
1854
+ 0.009717775508761406,
1855
+ 0.35761409997940063,
1856
+ 0.4059191644191742
1857
+ ],
1858
+ [
1859
+ 0.038556747138500214,
1860
+ 0.1978488266468048,
1861
+ 0.834216296672821,
1862
+ 0.4369942843914032,
1863
+ 0.3645400106906891,
1864
+ 0.5292827486991882,
1865
+ 0.556195855140686,
1866
+ 0.5879309773445129,
1867
+ 0.7033751010894775,
1868
+ 0.4590916037559509,
1869
+ 0.06794524937868118,
1870
+ 0.3964718282222748,
1871
+ 0.42892006039619446
1872
+ ],
1873
+ [
1874
+ 0.0822339728474617,
1875
+ 0.15992115437984467,
1876
+ 0.7834641337394714,
1877
+ 0.42339903116226196,
1878
+ 0.34574249386787415,
1879
+ 0.48923566937446594,
1880
+ 0.5099923610687256,
1881
+ 0.627753496170044,
1882
+ 0.6312965750694275,
1883
+ 0.4491918981075287,
1884
+ 0.08122247457504272,
1885
+ 0.33692267537117004,
1886
+ 0.465788871049881
1887
+ ],
1888
+ [
1889
+ 0.11642027646303177,
1890
+ 0.118315190076828,
1891
+ 0.7409539222717285,
1892
+ 0.4470598101615906,
1893
+ 0.3227274417877197,
1894
+ 0.4294480085372925,
1895
+ 0.49222809076309204,
1896
+ 0.7322760820388794,
1897
+ 0.5469415783882141,
1898
+ 0.36480313539505005,
1899
+ 0.024686744436621666,
1900
+ 0.2646491229534149,
1901
+ 0.4103580713272095
1902
+ ],
1903
+ [
1904
+ 0.1868918538093567,
1905
+ 0.10242144018411636,
1906
+ 0.6756333708763123,
1907
+ 0.4364897906780243,
1908
+ 0.290968120098114,
1909
+ 0.4043419063091278,
1910
+ 0.45341530442237854,
1911
+ 0.7665934562683105,
1912
+ 0.5088798999786377,
1913
+ 0.24618837237358093,
1914
+ 0.0,
1915
+ 0.16969110071659088,
1916
+ 0.35006994009017944
1917
+ ],
1918
+ [
1919
+ 0.2503180205821991,
1920
+ 0.1268322765827179,
1921
+ 0.5735778212547302,
1922
+ 0.4366355836391449,
1923
+ 0.30388057231903076,
1924
+ 0.442385733127594,
1925
+ 0.398971289396286,
1926
+ 0.7520959377288818,
1927
+ 0.5388989448547363,
1928
+ 0.26548394560813904,
1929
+ 0.008171271532773972,
1930
+ 0.1257970780134201,
1931
+ 0.3485977053642273
1932
+ ],
1933
+ [
1934
+ 0.2900908887386322,
1935
+ 0.16893012821674347,
1936
+ 0.4577920436859131,
1937
+ 0.4277336597442627,
1938
+ 0.3232477009296417,
1939
+ 0.531777560710907,
1940
+ 0.39727750420570374,
1941
+ 0.6795014142990112,
1942
+ 0.48868805170059204,
1943
+ 0.2898959517478943,
1944
+ 0.1418028324842453,
1945
+ 0.12177688628435135,
1946
+ 0.2917960286140442
1947
+ ],
1948
+ [
1949
+ 0.2792268693447113,
1950
+ 0.2319025993347168,
1951
+ 0.4227851331233978,
1952
+ 0.44706737995147705,
1953
+ 0.3128238320350647,
1954
+ 0.6999660134315491,
1955
+ 0.43848201632499695,
1956
+ 0.5300739407539368,
1957
+ 0.39969006180763245,
1958
+ 0.39730560779571533,
1959
+ 0.26391106843948364,
1960
+ 0.18718558549880981,
1961
+ 0.33163970708847046
1962
+ ],
1963
+ [
1964
+ 0.28902363777160645,
1965
+ 0.28392016887664795,
1966
+ 0.4272365868091583,
1967
+ 0.5053210854530334,
1968
+ 0.2757094204425812,
1969
+ 0.8456398248672485,
1970
+ 0.4518815577030182,
1971
+ 0.45749273896217346,
1972
+ 0.3181617558002472,
1973
+ 0.4448428452014923,
1974
+ 0.3280032277107239,
1975
+ 0.21995072066783905,
1976
+ 0.40605419874191284
1977
+ ],
1978
+ [
1979
+ 0.30310046672821045,
1980
+ 0.3220006227493286,
1981
+ 0.4188046455383301,
1982
+ 0.5786864757537842,
1983
+ 0.2313844859600067,
1984
+ 0.9909186959266663,
1985
+ 0.3816840648651123,
1986
+ 0.41152098774909973,
1987
+ 0.2809295356273651,
1988
+ 0.5186776518821716,
1989
+ 0.4197472035884857,
1990
+ 0.19759920239448547,
1991
+ 0.5271488428115845
1992
+ ],
1993
+ [
1994
+ 0.3526214361190796,
1995
+ 0.35234275460243225,
1996
+ 0.43079662322998047,
1997
+ 0.5791170597076416,
1998
+ 0.1899081915616989,
1999
+ 1.0,
2000
+ 0.36282944679260254,
2001
+ 0.3539854884147644,
2002
+ 0.28582316637039185,
2003
+ 0.496574729681015,
2004
+ 0.43650829792022705,
2005
+ 0.10935331135988235,
2006
+ 0.5543745160102844
2007
+ ],
2008
+ [
2009
+ 0.41773712635040283,
2010
+ 0.3903321921825409,
2011
+ 0.47357064485549927,
2012
+ 0.5240141749382019,
2013
+ 0.20888248085975647,
2014
+ 1.0,
2015
+ 0.37714311480522156,
2016
+ 0.2732824683189392,
2017
+ 0.40228506922721863,
2018
+ 0.46711480617523193,
2019
+ 0.3247523307800293,
2020
+ 0.06386715173721313,
2021
+ 0.638704240322113
2022
+ ]
2023
+ ],
2024
+ "structure_native_unit": "nautical_miles",
2025
+ "track_input_clip": {
2026
+ "method": "per-feature 0.1%-99.9% bounds from train split only",
2027
+ "quantiles": [
2028
+ 0.001,
2029
+ 0.999
2030
+ ],
2031
+ "lower": [
2032
+ -3.8563146591186523,
2033
+ -3.859726667404175,
2034
+ -2.669590950012207,
2035
+ -2.828439712524414,
2036
+ -1.514838695526123,
2037
+ -0.877439558506012,
2038
+ -0.20029544830322266,
2039
+ -0.5029191970825195,
2040
+ -0.4581102430820465,
2041
+ -0.4543793201446533,
2042
+ -0.44044506549835205,
2043
+ -0.4524264335632324,
2044
+ -0.32242000102996826,
2045
+ -0.3159736096858978,
2046
+ -0.31081247329711914,
2047
+ -0.31689807772636414,
2048
+ -0.2571345567703247,
2049
+ -0.25485461950302124,
2050
+ -0.25090861320495605,
2051
+ -0.253328412771225,
2052
+ 0.0,
2053
+ -1.109837293624878,
2054
+ -1.4902359247207642,
2055
+ -1.5492080450057983,
2056
+ -3.043226718902588,
2057
+ -0.8778678178787231,
2058
+ -0.22049888968467712,
2059
+ -0.6087565422058105,
2060
+ -0.5303210616111755,
2061
+ -0.5303064584732056,
2062
+ -0.530174195766449,
2063
+ -0.5302548408508301,
2064
+ -0.3700577914714813,
2065
+ -0.3699590861797333,
2066
+ -0.36966797709465027,
2067
+ -0.36991986632347107,
2068
+ -0.28939348459243774,
2069
+ -0.2892126142978668,
2070
+ -0.2889581322669983,
2071
+ -0.2892405688762665,
2072
+ -1.000949740409851,
2073
+ -1.9555184841156006,
2074
+ -1.3818809986114502,
2075
+ -3.743236541748047,
2076
+ -4.178081035614014,
2077
+ -7.5739426612854,
2078
+ -1.9869922399520874,
2079
+ -0.791445791721344,
2080
+ -1.895654320716858,
2081
+ -1.900766134262085,
2082
+ -1.8269368410110474,
2083
+ -1.1999244689941406,
2084
+ -1.2392349243164062,
2085
+ -1.5779284238815308
2086
+ ],
2087
+ "upper": [
2088
+ 3.46697735786438,
2089
+ 2.962402105331421,
2090
+ 3.792782783508301,
2091
+ 3.5486209392547607,
2092
+ 3.303658962249756,
2093
+ 1.1986064910888672,
2094
+ 9.119026184082031,
2095
+ 6.6407904624938965,
2096
+ 5.609043598175049,
2097
+ 5.1235761642456055,
2098
+ 5.236553192138672,
2099
+ 5.678617000579834,
2100
+ 6.238978385925293,
2101
+ 6.22589635848999,
2102
+ 6.73668098449707,
2103
+ 6.552648067474365,
2104
+ 7.652637958526611,
2105
+ 7.037979602813721,
2106
+ 6.988698482513428,
2107
+ 7.6510162353515625,
2108
+ 0.0,
2109
+ 1.7232352495193481,
2110
+ 1.4755607843399048,
2111
+ 1.5490014553070068,
2112
+ 0.3285984992980957,
2113
+ 1.1391236782073975,
2114
+ 4.535170078277588,
2115
+ 1.6426928043365479,
2116
+ 1.8856501579284668,
2117
+ 1.8857020139694214,
2118
+ 1.8861727714538574,
2119
+ 1.88588547706604,
2120
+ 2.7022807598114014,
2121
+ 2.7030017375946045,
2122
+ 2.705130100250244,
2123
+ 2.7032880783081055,
2124
+ 3.45550274848938,
2125
+ 3.4576637744903564,
2126
+ 3.460709571838379,
2127
+ 3.457329511642456,
2128
+ 2.0224556922912598,
2129
+ 1.5678421258926392,
2130
+ 4.212479591369629,
2131
+ 3.814079999923706,
2132
+ 4.954610824584961,
2133
+ 5.965235233306885,
2134
+ 0.5032732486724854,
2135
+ 1.2635104656219482,
2136
+ 2.1631147861480713,
2137
+ 3.6985673904418945,
2138
+ 1.0047227144241333,
2139
+ 2.321204662322998,
2140
+ 3.150118350982666,
2141
+ 1.4707427024841309
2142
+ ]
2143
+ },
2144
+ "temporal_checkpoint_root": "models/trackformer_1_1",
2145
+ "temporal_wind_blend_alpha": [
2146
+ 1.006157773537495,
2147
+ 1.082670297723516,
2148
+ 1.0076812926999208,
2149
+ 0.9684541176843595,
2150
+ 0.937035559922028,
2151
+ 0.9061196390585828,
2152
+ 0.8828505612779566,
2153
+ 0.8649551163976384,
2154
+ 0.850160066458564,
2155
+ 0.8436538164907559,
2156
+ 0.8360733120277236,
2157
+ 0.8246513293372426,
2158
+ 0.8130469984314852,
2159
+ 0.8040146581229802,
2160
+ 0.8026821984394348,
2161
+ 0.806057957319184,
2162
+ 0.8108057351642745,
2163
+ 0.8134246099298341,
2164
+ 0.8212623454663425,
2165
+ 0.8365160626176127
2166
+ ],
2167
+ "temporal_wind_min_current_kt": 80.0,
2168
+ "temporal_wind_metrics": {
2169
+ "val": {
2170
+ "all_leads_kt": 16.9397,
2171
+ "lead_120h_kt": 21.4157
2172
+ },
2173
+ "test": {
2174
+ "all_leads_kt": 16.3692,
2175
+ "lead_120h_kt": 20.328
2176
+ }
2177
+ },
2178
+ "temporal_input_policy": "same-storm t-12 h and t-24 h dlm4 analysis patches; no future atmospheric fields",
2179
+ "pressure_policy": "retain the calibrated pressure path; temporal branch is wind-only"
2180
+ }
v23_terrain_wp.npz → models/trackformer_1_1/trackformer_1_1_intensity_seed0.pt RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:6b0f49401b54cb40b566fd96c6db86a2e9ef1ccf31fabc685e73b13ef6e7ba14
3
- size 164910
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f26a55bb3db6e88a69656e2633e8316f71698866d4be4c336a1b2524a5f58401
3
+ size 8896661
best.pt → models/trackformer_1_1/trackformer_1_1_intensity_seed1.pt RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:ea6e045bea4e354c36b7fc0ccb72de99efa5634ad813de96e545aed4d7c2840e
3
- size 1436940
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:74c379bda87dd52a3d7558d58110d104e427d7972ad59e2dca02b4e61b641e1c
3
+ size 8896725
v23_seed1.pt → models/trackformer_1_1/trackformer_1_1_intensity_seed2.pt RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:0aeda7ef5f821983661590daf65add15530ee9ed2a84ac8d862296b3edafaabd
3
- size 52625227
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7023a827ebc100d19a70183e5ffe9f23023da58c178eb9fcd668d681a2c90b53
3
+ size 8896661
v23_norm_stats.npz → models/trackformer_1_1/trackformer_1_1_norm_stats.npz RENAMED
File without changes
v23_seed2.pt → models/trackformer_1_1/trackformer_1_1_structure_seed0.pt RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:efd58477767017dbb88c0db74c01f39ee36ae30444db55333865aa1e8d97b2fa
3
- size 52625227
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:46a2d38cae5ce1cb727f589d5dbd969a05480aaf32e3b33d4b7ab3e285beb42a
3
+ size 8896961
models/trackformer_1_1/trackformer_1_1_structure_seed1.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:13d1840bc8f4d7bb144dad2d1c07dd9f8ea8cf7bdd81675788431672e9755b8e
3
+ size 8897025
models/trackformer_1_1/trackformer_1_1_structure_seed2.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f46a8f03b4723baa4fd9068877b3a4006edf898f084e2dd7be6be90ea44dd562
3
+ size 8896961
models/trackformer_1_1/trackformer_1_1_temporal_seed0.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:251ae688062543dfa748f46840054d4e2bb54d0701b2b73f8f51c976f9ac3c78
3
+ size 9531936
models/trackformer_1_1/trackformer_1_1_temporal_seed1.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9c705fea9bc73b0971be8949d777c22d6bede38b40134f34f00f1082eb081b49
3
+ size 9531872
models/trackformer_1_1/trackformer_1_1_temporal_seed2.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c90ce4dc0a409585bf5a81f0747248be086c2698be9a790296a1dc56a9ca76ae
3
+ size 9531936
requirements.txt CHANGED
@@ -1,5 +1,3 @@
1
- numpy==2.5.1
2
- torch==2.13.0
3
- scikit-learn==1.9.0
4
- matplotlib==3.11.0
5
- folium==0.20.0
 
1
+ numpy>=1.26
2
+ torch>=2.1
3
+ scipy>=1.11
 
 
run_inference.py DELETED
@@ -1,94 +0,0 @@
1
- import json
2
- import argparse
3
- from pathlib import Path
4
- import numpy as np
5
- import torch
6
- from torch import nn
7
-
8
- ROOT = Path(__file__).resolve().parent
9
- parser = argparse.ArgumentParser(description="Run the Bavi typhoon checkpoint locally.")
10
- parser.add_argument("--checkpoint", type=Path, default=ROOT / "best.pt")
11
- parser.add_argument("--output", type=Path, default=ROOT / "forecast.json")
12
- parser.add_argument("--device", choices=["auto", "mps", "cpu"], default="auto")
13
- args = parser.parse_args()
14
- CKPT = args.checkpoint.expanduser().resolve()
15
- OUT = args.output.expanduser().resolve()
16
-
17
- class FieldEncoder(nn.Module):
18
- def __init__(self, channels, hidden=128):
19
- super().__init__()
20
- self.net = nn.Sequential(
21
- nn.Conv2d(channels,64,3,padding=1), nn.GELU(), nn.BatchNorm2d(64),
22
- nn.Conv2d(64,96,3,stride=2,padding=1), nn.GELU(), nn.BatchNorm2d(96),
23
- nn.Conv2d(96,hidden,3,stride=2,padding=1), nn.GELU(),
24
- nn.AdaptiveAvgPool2d(1))
25
- def forward(self, x): return self.net(x).flatten(1)
26
-
27
- class ERA5CycloneEnsemble(nn.Module):
28
- def __init__(self, track_dim, channels, steps, output_dim, hidden=192, latent=32):
29
- super().__init__()
30
- self.steps, self.output_dim, self.latent = steps, output_dim, latent
31
- self.field = FieldEncoder(channels, 128)
32
- self.track_gru = nn.GRU(track_dim,96,batch_first=True,bidirectional=True)
33
- self.fuse = nn.Sequential(nn.Linear(128*steps+192,hidden),nn.GELU(),nn.Dropout(.15),nn.Linear(hidden,hidden),nn.GELU())
34
- self.mean = nn.Linear(hidden,output_dim)
35
- self.log_scale = nn.Linear(hidden,output_dim)
36
- self.latent_proj = nn.Sequential(nn.Linear(latent,hidden),nn.GELU(),nn.Linear(hidden,output_dim))
37
- def encode(self, track_x, field_x):
38
- b,t,c,h,w=field_x.shape
39
- field_z=self.field(field_x.reshape(b*t,c,h,w)).reshape(b,t,-1).flatten(1)
40
- _,state=self.track_gru(track_x)
41
- return self.fuse(torch.cat([field_z,state.transpose(0,1).flatten(1)],dim=1))
42
- def sample(self, track_x, field_x, n, temperature=1.0):
43
- z=self.encode(track_x,field_x)
44
- mean=self.mean(z); log_scale=self.log_scale(z).clamp(-5,2)
45
- eps=torch.randn(n,z.shape[0],self.latent,device=z.device)
46
- low=self.latent_proj(eps.reshape(-1,self.latent)).reshape(n,z.shape[0],-1)
47
- independent=torch.randn_like(low)*torch.exp(log_scale).unsqueeze(0)
48
- return mean.unsqueeze(0)+temperature*(low+independent)
49
-
50
- raw=torch.load(CKPT,map_location="cpu",weights_only=False)
51
- if args.device == "mps":
52
- device = torch.device("mps")
53
- elif args.device == "cpu":
54
- device = torch.device("cpu")
55
- else:
56
- device=torch.device("mps" if torch.backends.mps.is_available() else "cpu")
57
- config=raw["config"]
58
- track_scaler=raw["track_scaler"]; y_scaler=raw["y_scaler"]
59
- field_mean=np.asarray(raw["field_mean"],dtype="float32")
60
- field_std=np.asarray(raw["field_std"],dtype="float32")
61
- model=ERA5CycloneEnsemble(9,10,1,28).to(device)
62
- model.load_state_dict(raw["model_state"]); model.eval()
63
-
64
- fixes=[
65
- ("2026-07-09T12:00:00",19.2,128.8,100,952),
66
- ("2026-07-09T18:00:00",20.1,128.2,90,953),
67
- ("2026-07-10T00:00:00",20.8,127.3,75,964),
68
- ("2026-07-10T06:00:00",21.9,126.9,75,962),
69
- ]
70
- track=[]
71
- for i,(stamp,lat,lon,wind,pres) in enumerate(fixes):
72
- dlat=0 if i==0 else lat-fixes[i-1][1]
73
- dlon=0 if i==0 else ((lon-fixes[i-1][2]+180)%360)-180
74
- track.append([lat,lon,wind,pres,dlat,dlon,float(np.hypot(dlat,dlon)),np.sin(2*np.pi*191/366),np.cos(2*np.pi*191/366)])
75
- track=np.asarray(track,dtype="float32")
76
- track[:,:1]-=track[-1:,0:1]
77
- track[:,1:2]=((track[:,1:2]-track[-1:,1:2]+180)%360)-180
78
- track[:,2]/=100.0
79
- track[:,3]=(track[:,3]-950.0)/50.0
80
- xtrack=track_scaler.transform(track).astype("float32")[None]
81
- field=np.zeros((1,1,10,33,33),dtype="float32")
82
- with torch.no_grad():
83
- ens=model.sample(torch.from_numpy(xtrack).to(device),torch.from_numpy(field).to(device),50,float(config.get("sample_temperature",1.0))).cpu().numpy()
84
- ens=ens.mean(0,keepdims=True)+(ens-ens.mean(0,keepdims=True))*1.1297996044158936
85
- pred=y_scaler.inverse_transform(ens[:,0,:])
86
- base_lat,base_lon=fixes[-1][1],fixes[-1][2]
87
- points=[]
88
- for k,lead in enumerate(config["lead_hours"]):
89
- j=4*k
90
- lat=base_lat+pred[:,j]; lon=(base_lon+pred[:,j+1])%360
91
- points.append({"lead_hours":int(lead),"lat":float(lat.mean()),"lon":float(lon.mean()),"p10_lat":float(np.quantile(lat,.1)),"p90_lat":float(np.quantile(lat,.9)),"p10_lon":float(np.quantile(lon,.1)),"p90_lon":float(np.quantile(lon,.9))})
92
- result={"storm":"Bavi","source":"JTWC/TCGP fixes through 2026-07-10 0600 UTC","initial_time":"2026-07-10T06:00:00Z","initial_lat":base_lat,"initial_lon":base_lon,"points":points,"device":str(device),"note":"Mac checkpoint inference using current track fixes and mean-normalized atmospheric input because 2026 ERA5 fields are unavailable locally. Not an operational forecast."}
93
- OUT.write_text(json.dumps(result,indent=2))
94
- print(json.dumps(result,indent=2))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
run_v23.py DELETED
@@ -1,222 +0,0 @@
1
- """Run TrackFormer v23 on a storm's track history, in either of two modes:
2
-
3
- IBTrACS-only (default, no --steering given): the model sees only the storm's own recent
4
- positions/wind/pressure -- exactly what IBTrACS (or any best-track record) gives you for a past
5
- storm, nothing else. The steering field and its 12h/24h history are zero-filled with an explicit
6
- availability flag, the same "unavailable == exact zeros, not fabricated" convention used
7
- throughout this project whenever a field genuinely isn't there.
8
-
9
- Full data (--steering given): the model additionally sees a real deep-layer-mean steering-wind
10
- patch (850/500/200 hPa u/v, weighted 0.269/0.500/0.231) around the storm -- for the current fix
11
- and, if present, t-12h/t-24h. This is what the project's headline 434.96 km number requires; on
12
- Typhoon Dolphin (2026) zeroing this field out shifted the 120h forecast position by ~600 km while
13
- softening the intensity forecast only modestly (99 -> 90 kt) -- see the project's README for the
14
- full ablation.
15
-
16
- Usage:
17
- python run_v23.py --track my_storm.json --out forecast.json
18
- python run_v23.py --track my_storm.json --steering my_steering.npz --out forecast.json
19
-
20
- --track JSON format: a list of fixes, OLDEST to NEWEST, spaced 6 hours apart, ending at the fix to
21
- forecast from ("now"). Up to 9 fixes are used (fewer is fine -- the model pads with the same
22
- pre-genesis zero-fill it saw for young storms in training); extra leading fixes beyond 9 are
23
- ignored.
24
- [{"time": "2026-07-29T00:00", "lat": 14.1, "lon": 169.1, "vmax_kt": 121.6, "pres_hpa": 941},
25
- {"time": "2026-07-29T06:00", "lat": 14.5, "lon": 168.4, "vmax_kt": 121.7, "pres_hpa": 941}]
26
- `pres_hpa` may be null/omitted per-fix if unknown -- it is then treated as unavailable for that fix
27
- (zero-filled, flagged), not fabricated.
28
-
29
- --steering NPZ format (optional): float32 arrays of shape [2,17,17] (u,v in m/s, 2.5 deg
30
- resolution, +-20 deg box centered on the storm), keyed by ISO time strings matching entries in
31
- --track (e.g. "2026-07-29T06:00"). Only the LAST fix's key is required; keys for the fixes 12h and
32
- 24h before it are used for v23's temporal-history stack if present, and zero-filled (flagged
33
- unavailable) if not -- so a steering file with only the current fix still runs, just without the
34
- temporal-history benefit. See _fetch_dolphin_steering.py in the repo root for a working example of
35
- building this from NOAA/NOMADS GFS analysis fields for a live storm, or from ERA5 for a past one.
36
- """
37
- import argparse
38
- import json
39
- import math
40
- import os
41
- import sys
42
-
43
- import numpy as np
44
- import torch
45
-
46
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
47
- from trackformer_v23 import build_v23, TMEAN, TSTD, DSC, TARGET_SCALE # noqa: E402
48
-
49
- R = 111.2 # km per degree latitude
50
- HIST = 9 # kinematic-history window length the model was trained with
51
-
52
- _here = os.path.dirname(os.path.abspath(__file__))
53
- _terrain = np.load(os.path.join(_here, "v23_terrain_wp.npz"))
54
- _T_LAT, _T_LON, _LSM = _terrain["lat"], _terrain["lon"], _terrain["lsm"]
55
- _LAND = _LSM > 0.5
56
- _LAND_LAT = _T_LAT[np.where(_LAND)[0]]
57
- _LAND_LON = _T_LON[np.where(_LAND)[1]]
58
-
59
-
60
- def dist2land(lat_i, lon_i):
61
- if len(_LAND_LAT) == 0:
62
- return 3000.0
63
- dlat = _LAND_LAT - lat_i
64
- dlon = (_LAND_LON - lon_i) * math.cos(math.radians(lat_i))
65
- return float(np.hypot(dlon * R, dlat * R).min())
66
-
67
-
68
- def load_track(path):
69
- fixes = json.load(open(path))
70
- fixes = fixes[-HIST:] if len(fixes) > HIST else fixes
71
- times = [f["time"] for f in fixes]
72
- lat = np.array([f["lat"] for f in fixes], dtype="float64")
73
- lon = np.array([f["lon"] for f in fixes], dtype="float64")
74
- vmax = np.array([f["vmax_kt"] for f in fixes], dtype="float64")
75
- pres = np.array([f.get("pres_hpa", None) if f.get("pres_hpa", None) is not None else np.nan
76
- for f in fixes], dtype="float64")
77
- tns = np.array([np.datetime64(t).astype("datetime64[ns]").astype("int64") for t in times])
78
- return times, tns, lat, lon, vmax, pres
79
-
80
-
81
- def build_window(times, tns, lat, lon, vmax, pres):
82
- """Kinematic/thermodynamic feature window -- same construction as this project's other
83
- real-storm inference scripts (e.g. _dolphin_v23_v35.py / _noul_v33.py)."""
84
- n = len(times)
85
- base = n - 1
86
- hidx = [max(0, base - HIST + 1 + k) for k in range(HIST)]
87
- n_padded = max(0, HIST - 1 - base)
88
- t0 = int(tns[base])
89
- doy = (np.datetime64(times[base]) - np.datetime64(times[base][:4] + "-01-01")).astype(int) + 1
90
- phase = 2 * math.pi * doy / 365.25
91
- seq = np.zeros((HIST, 54), dtype="float32")
92
- prev, pdir = -1, None
93
-
94
- def mkm(a, b, c, d):
95
- dlat = c - a; dlon = ((d - b + 180) % 360) - 180
96
- return dlon * R * math.cos(math.radians((a + c) / 2)), dlat * R
97
-
98
- for i, idx in enumerate(hidx):
99
- e, n_ = mkm(lat[base], lon[base], lat[idx], lon[idx])
100
- se, sn = (0., 0.) if prev < 0 else mkm(lat[prev], lon[prev], lat[idx], lon[idx])
101
- f = seq[i]; f[0:4] = [e, n_, se, sn]
102
- vv = [vmax[idx], pres[idx], np.nan, np.nan]
103
- for j in range(4):
104
- f[4 + j] = vv[j] if np.isfinite(vv[j]) else 0.
105
- f[24:28] = [float(np.isfinite(x)) for x in vv]
106
- f[21:23] = [math.sin(phase), math.cos(phase)]; f[23] = (t0 - int(tns[idx])) / 3.6e12
107
- sp = math.hypot(se, sn); hs, hc = (se / sp, sn / sp) if (sp > 1e-3 and prev >= 0) else (0., 0.)
108
- f[40], f[41], f[42] = hs, hc, sp
109
- f[43] = (pdir[0] * hc - pdir[1] * hs) if (pdir and (hs or hc) and (pdir[0] or pdir[1])) else 0.
110
- if prev >= 0:
111
- dv = np.isfinite(vmax[prev]) and np.isfinite(vmax[idx])
112
- dp = np.isfinite(pres[prev]) and np.isfinite(pres[idx])
113
- f[44] = vmax[idx] - vmax[prev] if dv else 0.
114
- f[45] = pres[idx] - pres[prev] if dp else 0.
115
- f[46], f[47] = float(dv), float(dp)
116
- lat_i, lon_i = lat[idx], lon[idx]
117
- m = np.datetime64(times[idx]).astype("datetime64[M]").astype(int) % 12 + 1
118
- d2l = dist2land(lat_i, lon_i % 360)
119
- thermal = 0.5 * 23.44 * math.sin(2 * math.pi * (m - 3) / 12.0)
120
- f[48] = lat_i; f[49] = abs(lat_i); f[50] = math.sin(math.radians(lon_i)); f[51] = math.cos(math.radians(lon_i))
121
- f[52] = d2l; f[53] = max(0., min(31., 30. - 0.30 * abs(lat_i - thermal) ** 1.4))
122
- if hs or hc:
123
- pdir = (hs, hc)
124
- prev = idx
125
-
126
- seq_n = (seq - TMEAN) / TSTD
127
- vpair = np.concatenate([seq[-1, 2:4], seq[-2, 2:4]]).astype("float32")
128
- return seq_n, vpair, n_padded
129
-
130
-
131
- def load_steering(path, times):
132
- """Returns (slp[1,4,17,17], hist[1,8,17,17], have[1,2]) for the LAST fix in `times`. `path` may
133
- be None -- then everything is zero-filled (the IBTrACS-only ablation)."""
134
- if path is None:
135
- return (np.zeros((1, 4, 17, 17), "float32"),
136
- np.zeros((1, 8, 17, 17), "float32"),
137
- np.zeros((1, 2), "float32"))
138
- dlm = np.load(path)
139
- now_t = np.datetime64(times[-1])
140
-
141
- def key_at(back_h):
142
- return str(now_t - np.timedelta64(back_h, "h"))
143
-
144
- slp = np.zeros((1, 4, 17, 17), "float32")
145
- now_key = times[-1]
146
- if now_key in dlm:
147
- uv = dlm[now_key]
148
- slp[0, 2:4] = np.clip(uv / DSC[:, None, None], -4.0, 4.0)
149
- elif key_at(0) in dlm:
150
- uv = dlm[key_at(0)]
151
- slp[0, 2:4] = np.clip(uv / DSC[:, None, None], -4.0, 4.0)
152
-
153
- hist = np.zeros((1, 8, 17, 17), "float32")
154
- have = np.zeros((1, 2), "float32")
155
- cur = slp[0]
156
- for c, back in enumerate((12, 24)):
157
- k = key_at(back)
158
- if k in dlm:
159
- uv = dlm[k]
160
- hist[0, c * 4 + 2:c * 4 + 4] = np.clip(uv / DSC[:, None, None], -4.0, 4.0)
161
- have[0, c] = 1.0
162
- else:
163
- hist[0, c * 4:(c + 1) * 4] = cur
164
- return slp, hist, have
165
-
166
-
167
- @torch.no_grad()
168
- def forecast(models, times, tns, lat, lon, vmax, pres, steering_path):
169
- seq_n, vpair, n_padded = build_window(times, tns, lat, lon, vmax, pres)
170
- tr = torch.from_numpy(seq_n[None]); vp = torch.from_numpy(vpair[None])
171
- slp, hist, have = load_steering(steering_path, times)
172
- args = [tr, vp, torch.from_numpy(slp), torch.from_numpy(hist), torch.from_numpy(have)]
173
- motion = torch.stack([m(*args)[0] for m in models]).mean(0)[0] * TARGET_SCALE
174
- motion = motion.float().numpy() # [20, 17]
175
- la, lo = float(lat[-1]), float(lon[-1])
176
- lats, lons, vmaxs, presses = [], [], [], []
177
- for L in range(20):
178
- e, n_ = motion[L, 0], motion[L, 1]
179
- la = la + n_ / R; lo = lo + e / (R * math.cos(math.radians(la)))
180
- lats.append(la); lons.append(lo)
181
- vmaxs.append(float(motion[L, 2])); presses.append(float(motion[L, 3]))
182
- return lats, lons, vmaxs, presses, n_padded
183
-
184
-
185
- def main():
186
- ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
187
- ap.add_argument("--track", required=True, help="track-history JSON, see module docstring")
188
- ap.add_argument("--steering", default=None, help="steering NPZ (omit for IBTrACS-only mode)")
189
- ap.add_argument("--seeds", default=os.path.join(_here, "v23_seed*.pt"), help="checkpoint glob")
190
- ap.add_argument("--out", default=None, help="write forecast JSON here (default: stdout)")
191
- args = ap.parse_args()
192
-
193
- import glob
194
- ckpts = sorted(glob.glob(args.seeds))
195
- if not ckpts:
196
- sys.exit(f"no checkpoints matched {args.seeds!r}")
197
- models = []
198
- for c in ckpts:
199
- m = build_v23().eval()
200
- m.load_state_dict(torch.load(c, map_location="cpu", weights_only=False)["model"])
201
- models.append(m)
202
- mode = "IBTrACS-only (steering zeroed)" if args.steering is None else f"full data ({args.steering})"
203
- print(f"loaded {len(models)} v23 seeds, mode: {mode}", file=sys.stderr)
204
-
205
- times, tns, lat, lon, vmax, pres = load_track(args.track)
206
- lats, lons, vmaxs, presses, n_padded = forecast(models, times, tns, lat, lon, vmax, pres, args.steering)
207
-
208
- out = {"issue_time": times[-1], "mode": mode, "base_lat": float(lat[-1]), "base_lon": float(lon[-1]),
209
- "lead_hours": list(range(6, 121, 6)),
210
- "lats": [round(float(x), 3) for x in lats], "lons": [round(float(x), 3) for x in lons],
211
- "vmax_kt": [round(float(x), 1) for x in vmaxs], "pres_hpa": [round(float(x), 1) for x in presses],
212
- "n_padded_history": n_padded}
213
- text = json.dumps(out, indent=2)
214
- if args.out:
215
- open(args.out, "w").write(text)
216
- print(f"wrote {args.out}", file=sys.stderr)
217
- else:
218
- print(text)
219
-
220
-
221
- if __name__ == "__main__":
222
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
trackformer_1_1.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Public Trackformer1.1 loading and inference API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from trackformer_1_1_intensity import Trackformer11IntensityEnsemble
8
+ from trackformer_1_1_route import (
9
+ LEAD_HOURS,
10
+ build_pacific_route,
11
+ detect_pressure_systems,
12
+ forecast_pacific_state,
13
+ )
14
+
15
+
16
+ PACKAGE_ROOT = Path(__file__).resolve().parent
17
+ MODEL_ROOT = PACKAGE_ROOT / "models" / "trackformer_1_1"
18
+ CALIBRATION_PATH = MODEL_ROOT / "trackformer_1_1_calibration.json"
19
+
20
+
21
+ def load_intensity(device: str | None = None) -> Trackformer11IntensityEnsemble:
22
+ """Load the frozen Trackformer1.1 intensity and structure experts."""
23
+
24
+ return Trackformer11IntensityEnsemble(MODEL_ROOT, CALIBRATION_PATH, device=device)
25
+
26
+
27
+ __all__ = [
28
+ "CALIBRATION_PATH",
29
+ "LEAD_HOURS",
30
+ "MODEL_ROOT",
31
+ "build_pacific_route",
32
+ "detect_pressure_systems",
33
+ "forecast_pacific_state",
34
+ "load_intensity",
35
+ ]
trackformer_1_1_base_route.py ADDED
@@ -0,0 +1,419 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Causal large-system steering ensemble for out-of-archive cases.
3
+
4
+ Trackformer1.1 is a physical route candidate, not a consensus of official forecasts.
5
+ Each member reads only analysis snapshots at or before the issue time. It
6
+ samples multiple pressure levels over inner and synoptic-scale rings around
7
+ the evolving center, and a small SLP-gradient component can represent the
8
+ large-scale pressure field. The final route is the weighted mean of the
9
+ integrated members so curvature and disagreement remain visible.
10
+
11
+ Input field layout:
12
+ fields: (snapshots, 7, latitude, longitude)
13
+ channels: hgt500, u850, v850, u500, v500, u200, v200
14
+ snapshots: current, t-12, t-24 (earlier snapshots may be zeroed)
15
+ pressure: (snapshots, latitude, longitude) SLP in hPa, optional
16
+
17
+ No forecast lead field, official forecast track, or future observation is
18
+ accepted by this module.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import math
24
+ from typing import Sequence
25
+
26
+ import numpy as np
27
+
28
+
29
+ VERSION = "Trackformer1.1-causal-dynamic-big-system-steering-ensemble"
30
+ LEADS = 20
31
+ LEVEL_WEIGHTS = np.asarray([0.269, 0.500, 0.231], dtype="float32")
32
+ MOTION_SLOPES = np.asarray([0.76, 0.78], dtype="float32")
33
+ MOTION_INTERCEPTS = np.asarray([-2.03, 0.40], dtype="float32")
34
+ TENDENCY_SCALES = (0.0, 0.65, 1.25)
35
+ TENDENCY_SCALE_WEIGHTS = (0.15, 0.45, 0.40)
36
+
37
+
38
+ ROUTE_VARIANTS = (
39
+ {
40
+ "name": "inner_850",
41
+ "level_weights": (1.0, 0.0, 0.0),
42
+ "ring_degrees": (3.0, 5.0),
43
+ "pressure_fraction": 0.00,
44
+ "weight": 0.30,
45
+ },
46
+ {
47
+ "name": "deep_layer_inner",
48
+ "level_weights": (0.269, 0.500, 0.231),
49
+ "ring_degrees": (3.0, 8.0),
50
+ "pressure_fraction": 0.04,
51
+ "weight": 0.22,
52
+ },
53
+ {
54
+ "name": "broad_850_ridge",
55
+ "level_weights": (1.0, 0.0, 0.0),
56
+ "ring_degrees": (5.0, 11.0),
57
+ "pressure_fraction": 0.05,
58
+ "weight": 0.16,
59
+ },
60
+ {
61
+ "name": "broad_500_trough",
62
+ "level_weights": (0.0, 1.0, 0.0),
63
+ "ring_degrees": (5.0, 12.0),
64
+ "pressure_fraction": 0.04,
65
+ "weight": 0.12,
66
+ },
67
+ {
68
+ "name": "synoptic_deep",
69
+ "level_weights": (0.269, 0.500, 0.231),
70
+ "ring_degrees": (7.0, 16.0),
71
+ "pressure_fraction": 0.10,
72
+ "weight": 0.12,
73
+ },
74
+ {
75
+ "name": "outer_200_jet",
76
+ "level_weights": (0.0, 0.0, 1.0),
77
+ "ring_degrees": (8.0, 18.0),
78
+ "pressure_fraction": 0.02,
79
+ "weight": 0.08,
80
+ },
81
+ )
82
+ CURVATURE_VARIANTS = (0.0, 0.20, 0.40)
83
+ SNAPSHOT_WEIGHTS = (0.50, 0.30, 0.20)
84
+
85
+
86
+ def _sorted_axes(latitude: np.ndarray, longitude: np.ndarray, field: np.ndarray) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
87
+ latitude = np.asarray(latitude, dtype="float32").reshape(-1)
88
+ longitude = np.asarray(longitude, dtype="float32").reshape(-1)
89
+ field = np.asarray(field, dtype="float32")
90
+ lat_order = np.argsort(latitude)
91
+ lon_order = np.argsort(longitude)
92
+ return latitude[lat_order], longitude[lon_order], field[..., lat_order, :][..., :, lon_order]
93
+
94
+
95
+ def _longitude_queries(values: np.ndarray, longitude: np.ndarray) -> np.ndarray:
96
+ values = np.asarray(values, dtype="float64")
97
+ low, high = float(longitude[0]), float(longitude[-1])
98
+ if low >= 0.0 and high > 180.0:
99
+ return np.mod(values, 360.0)
100
+ if low < 0.0 and high <= 180.0:
101
+ return ((values + 180.0) % 360.0) - 180.0
102
+ return np.clip(values, low, high)
103
+
104
+
105
+ def _bilinear(field: np.ndarray, latitude: np.ndarray, longitude: np.ndarray, query_lat: np.ndarray, query_lon: np.ndarray) -> np.ndarray:
106
+ """Bilinear sample a 2-D field or a 2-channel field on a regular grid."""
107
+
108
+ lat, lon, values = _sorted_axes(latitude, longitude, field)
109
+ query_lat = np.clip(np.asarray(query_lat, dtype="float64"), float(lat[0]), float(lat[-1]))
110
+ query_lon = _longitude_queries(query_lon, lon)
111
+ row = np.interp(query_lat, lat, np.arange(len(lat), dtype="float64"))
112
+ column = np.interp(query_lon, lon, np.arange(len(lon), dtype="float64"))
113
+ row0 = np.floor(row).astype("int64")
114
+ col0 = np.floor(column).astype("int64")
115
+ row1 = np.minimum(row0 + 1, len(lat) - 1)
116
+ col1 = np.minimum(col0 + 1, len(lon) - 1)
117
+ rf = row - row0
118
+ cf = column - col0
119
+ if values.ndim == 2:
120
+ return (
121
+ values[row0, col0] * (1.0 - rf) * (1.0 - cf)
122
+ + values[row1, col0] * rf * (1.0 - cf)
123
+ + values[row0, col1] * (1.0 - rf) * cf
124
+ + values[row1, col1] * rf * cf
125
+ ).astype("float32")
126
+ return np.stack([
127
+ _bilinear(values[channel], lat, lon, query_lat, query_lon)
128
+ for channel in range(values.shape[0])
129
+ ], axis=1).astype("float32")
130
+
131
+
132
+ def _geostrophic_field(pressure: np.ndarray, latitude: np.ndarray, longitude: np.ndarray) -> np.ndarray:
133
+ """Approximate geostrophic wind from causal SLP gradients."""
134
+
135
+ pressure = np.asarray(pressure, dtype="float32")
136
+ lat, lon, sorted_pressure = _sorted_axes(latitude, longitude, pressure)
137
+ earth_radius = 6_371_000.0
138
+ omega = 7.2921159e-5
139
+ dy = np.gradient(np.deg2rad(lat) * earth_radius)
140
+ dx = np.deg2rad(float(np.median(np.diff(lon)))) * earth_radius * np.cos(np.deg2rad(lat))
141
+ dp_dy = np.gradient(sorted_pressure.astype("float64"), axis=0) * 100.0 / dy[:, None]
142
+ dp_dx = np.gradient(sorted_pressure.astype("float64"), axis=1) * 100.0 / dx[:, None]
143
+ coriolis = 2.0 * omega * np.sin(np.deg2rad(lat))
144
+ coriolis = np.where(np.abs(coriolis) < 2.0e-5, np.sign(coriolis) * 2.0e-5, coriolis)
145
+ coriolis = np.where(coriolis == 0.0, 2.0e-5, coriolis)
146
+ rho = 1.15
147
+ u = -dp_dy / (rho * coriolis[:, None])
148
+ v = dp_dx / (rho * coriolis[:, None])
149
+ return np.stack([u, v]).astype("float32")
150
+
151
+
152
+ def _ring(ring_degrees: tuple[float, float]) -> tuple[np.ndarray, np.ndarray]:
153
+ radii = np.arange(float(ring_degrees[0]), float(ring_degrees[1]) + 0.01, 1.0, dtype="float32")
154
+ angles = np.linspace(0.0, 2.0 * math.pi, 32, endpoint=False, dtype="float32")
155
+ north = (radii[:, None] * np.sin(angles)[None, :]).reshape(-1)
156
+ east = (radii[:, None] * np.cos(angles)[None, :]).reshape(-1)
157
+ return north, east
158
+
159
+
160
+ def _step(flow: tuple[float, float]) -> np.ndarray:
161
+ u, v = flow
162
+ return np.asarray([
163
+ (float(MOTION_SLOPES[0]) * u + float(MOTION_INTERCEPTS[0])) * 21.6,
164
+ (float(MOTION_SLOPES[1]) * v + float(MOTION_INTERCEPTS[1])) * 21.6,
165
+ ], dtype="float32")
166
+
167
+
168
+ def _clip_tendency(values: np.ndarray) -> np.ndarray:
169
+ """Limit noisy analysis differences without using any future frame."""
170
+
171
+ values = np.nan_to_num(np.asarray(values, dtype="float32"), copy=True)
172
+ if values.ndim < 3:
173
+ return values
174
+ for channel in range(values.shape[0]):
175
+ scale = float(np.nanpercentile(np.abs(values[channel]), 98.0))
176
+ if scale > 0.0 and math.isfinite(scale):
177
+ values[channel] = np.clip(values[channel], -2.0 * scale, 2.0 * scale)
178
+ return values
179
+
180
+
181
+ def build_route(
182
+ fields: np.ndarray,
183
+ latitude: np.ndarray,
184
+ longitude: np.ndarray,
185
+ base_latitude: float,
186
+ base_longitude: float,
187
+ pressure: np.ndarray | None = None,
188
+ available: Sequence[float] = (1.0, 1.0),
189
+ route_variants: Sequence[dict] = ROUTE_VARIANTS,
190
+ curvature_variants: Sequence[float] = CURVATURE_VARIANTS,
191
+ snapshot_weights: Sequence[float] = SNAPSHOT_WEIGHTS,
192
+ tendency_scales: Sequence[float] = TENDENCY_SCALES,
193
+ history_motion_km_per_6h: tuple[float, float] | None = None,
194
+ ) -> tuple[np.ndarray, np.ndarray, dict]:
195
+ """Return dynamic member displacements, weights, and causal diagnostics.
196
+
197
+ The current analysis is advanced with a clipped tendency estimated only
198
+ from the two preceding analysis frames. The fields are never replaced by
199
+ a positive-lead forecast. Every member still starts at the same issue
200
+ position, but its broad steering environment can evolve with lead.
201
+ """
202
+
203
+ fields = np.asarray(fields, dtype="float32")
204
+ if fields.ndim != 4 or fields.shape[0] != 3 or fields.shape[1] != 7:
205
+ raise ValueError(f"expected fields (3,7,H,W), got {fields.shape}")
206
+ latitude = np.asarray(latitude, dtype="float32")
207
+ longitude = np.asarray(longitude, dtype="float32")
208
+ if pressure is None:
209
+ pressure = np.zeros((3, len(latitude), len(longitude)), dtype="float32")
210
+ pressure_available = np.zeros(3, dtype=bool)
211
+ else:
212
+ pressure = np.asarray(pressure, dtype="float32")
213
+ if pressure.shape != (3, len(latitude), len(longitude)):
214
+ raise ValueError(f"expected pressure (3,H,W), got {pressure.shape}")
215
+ pressure_available = np.isfinite(pressure).all(axis=(1, 2))
216
+ valid_snapshot = np.ones(3, dtype=bool)
217
+ avail = np.asarray(available, dtype="float64").reshape(-1)
218
+ if avail.size >= 1:
219
+ valid_snapshot[1] = bool(avail[0] > 0.5)
220
+ if avail.size >= 2:
221
+ valid_snapshot[2] = bool(avail[1] > 0.5)
222
+ valid_snapshot &= np.isfinite(fields).all(axis=(1, 2, 3))
223
+ if not valid_snapshot[0]:
224
+ raise RuntimeError("current causal analysis snapshot is unavailable")
225
+ snap_weights = np.asarray(snapshot_weights, dtype="float64") * valid_snapshot
226
+ snap_weights /= snap_weights.sum()
227
+ curve_values = np.asarray(tuple(curvature_variants), dtype="float64")
228
+ curve_values = np.clip(curve_values[np.isfinite(curve_values)], 0.0, 0.60)
229
+ if not len(curve_values):
230
+ raise ValueError("curvature_variants is empty")
231
+ tendency_values = np.asarray(tuple(tendency_scales), dtype="float64")
232
+ tendency_weights = np.asarray(tuple(TENDENCY_SCALE_WEIGHTS), dtype="float64")
233
+ if tendency_values.shape != tendency_weights.shape or np.any(~np.isfinite(tendency_values)):
234
+ raise ValueError("tendency_scales must match the fixed tendency weight table")
235
+ tendency_values = np.maximum(tendency_values, 0.0)
236
+ tendency_weights = np.maximum(tendency_weights, 0.0)
237
+ tendency_weights /= tendency_weights.sum()
238
+ variant_weights = np.asarray([float(item["weight"]) for item in route_variants], dtype="float64")
239
+ variant_weights = np.maximum(variant_weights, 0.0)
240
+ variant_weights /= variant_weights.sum()
241
+ geo_fields = np.zeros((3, 2, len(latitude), len(longitude)), dtype="float32")
242
+ for index in range(3):
243
+ if pressure_available[index]:
244
+ geo_fields[index] = _geostrophic_field(pressure[index], latitude, longitude)
245
+
246
+ current_fields = fields[0]
247
+ recent_delta = _clip_tendency(fields[0] - fields[1]) if valid_snapshot[1] else np.zeros_like(current_fields)
248
+ older_delta = _clip_tendency(fields[1] - fields[2]) if valid_snapshot[2] else recent_delta.copy()
249
+ tendency_views = (
250
+ recent_delta,
251
+ _clip_tendency(0.5 * (recent_delta + older_delta)),
252
+ older_delta,
253
+ )
254
+ current_geo = geo_fields[0]
255
+ recent_geo_delta = _clip_tendency(geo_fields[0] - geo_fields[1]) if pressure_available[1] else np.zeros_like(current_geo)
256
+ older_geo_delta = _clip_tendency(geo_fields[1] - geo_fields[2]) if pressure_available[2] else recent_geo_delta.copy()
257
+ geo_tendency_views = (
258
+ recent_geo_delta,
259
+ _clip_tendency(0.5 * (recent_geo_delta + older_geo_delta)),
260
+ older_geo_delta,
261
+ )
262
+ history_step = None
263
+ if history_motion_km_per_6h is not None:
264
+ candidate = np.asarray(history_motion_km_per_6h, dtype="float32").reshape(-1)
265
+ if candidate.size == 2 and np.isfinite(candidate).all():
266
+ history_step = candidate
267
+
268
+ members: list[np.ndarray] = []
269
+ weights: list[float] = []
270
+ member_rows: list[dict] = []
271
+ current_wind_levels = np.stack(
272
+ [current_fields[1:3], current_fields[3:5], current_fields[5:7]],
273
+ axis=0,
274
+ )
275
+ for snapshot_index in range(3):
276
+ if snap_weights[snapshot_index] <= 0.0:
277
+ continue
278
+ for variant_index, variant in enumerate(route_variants):
279
+ level_weights = np.asarray(variant["level_weights"], dtype="float32")
280
+ level_weights /= level_weights.sum()
281
+ ring_north, ring_east = _ring(tuple(variant["ring_degrees"]))
282
+ wind_field = np.tensordot(level_weights, current_wind_levels, axes=(0, 0)).astype("float32")
283
+ wind_tendency = np.tensordot(
284
+ level_weights,
285
+ np.stack(
286
+ [
287
+ tendency_views[snapshot_index][1:3],
288
+ tendency_views[snapshot_index][3:5],
289
+ tendency_views[snapshot_index][5:7],
290
+ ],
291
+ axis=0,
292
+ ),
293
+ axes=(0, 0),
294
+ ).astype("float32")
295
+ pressure_field = current_geo
296
+ geo_tendency = geo_tendency_views[snapshot_index]
297
+ pressure_fraction = float(variant.get("pressure_fraction", 0.0)) if pressure_available[0] else 0.0
298
+
299
+ def flow_from(field: np.ndarray, lat: float, lon: float) -> tuple[float, float]:
300
+ sample_lat = float(lat) + ring_north
301
+ sample_lon = float(lon) + ring_east / max(math.cos(math.radians(float(lat))), 0.20)
302
+ samples = _bilinear(field, latitude, longitude, sample_lat, sample_lon)
303
+ return float(np.nanmean(samples[:, 0])), float(np.nanmean(samples[:, 1]))
304
+
305
+ for tendency_index, tendency_scale in enumerate(tendency_values):
306
+ for curvature in curve_values:
307
+ lat = float(base_latitude)
308
+ lon = float(base_longitude)
309
+ previous_step = _step(flow_from(wind_field, lat, lon))
310
+ steps = np.zeros((LEADS, 2), dtype="float32")
311
+ waypoints = []
312
+ for lead in range(LEADS):
313
+ # A bounded extrapolation of the observed analysis
314
+ # tendency lets the steering regime change with lead.
315
+ progress = min(2.0, 0.5 * ((lead + 1) * 6.0 / 12.0))
316
+ dynamic_wind = wind_field + float(tendency_scale) * progress * wind_tendency
317
+ dynamic_geo = pressure_field + float(tendency_scale) * progress * geo_tendency
318
+ local_u, local_v = flow_from(dynamic_wind, lat, lon)
319
+ local_geo_u, local_geo_v = flow_from(dynamic_geo, lat, lon) if pressure_fraction else (0.0, 0.0)
320
+ local_flow = (
321
+ (1.0 - pressure_fraction) * local_u + pressure_fraction * local_geo_u,
322
+ (1.0 - pressure_fraction) * local_v + pressure_fraction * local_geo_v,
323
+ )
324
+ weather_step = _step(local_flow)
325
+ if history_step is not None:
326
+ history_weight = 0.20 * math.exp(-((lead + 1) * 6.0) / 36.0)
327
+ weather_step = (1.0 - history_weight) * weather_step + history_weight * history_step
328
+ inertia = min(0.18, 0.04 + 0.08 * float(curvature)) if lead else 0.0
329
+ step = (1.0 - inertia) * weather_step + inertia * previous_step
330
+ previous_step = step
331
+ steps[lead] = step
332
+ lat += float(step[1]) / 111.2
333
+ lon += float(step[0]) / (111.2 * max(math.cos(math.radians(lat)), 0.20))
334
+ lon %= 360.0
335
+ if lead in (0, 3, 7, 11, 15, 19):
336
+ waypoints.append({
337
+ "lead_hours": (lead + 1) * 6,
338
+ "latitude": round(lat, 4),
339
+ "longitude": round(lon, 4),
340
+ "u_mean_mps": round(local_flow[0], 4),
341
+ "v_mean_mps": round(local_flow[1], 4),
342
+ "tendency_progress": round(progress, 4),
343
+ })
344
+ members.append(steps)
345
+ weights.append(
346
+ float(snap_weights[snapshot_index])
347
+ * float(variant_weights[variant_index])
348
+ * float(tendency_weights[tendency_index])
349
+ / float(len(curve_values))
350
+ )
351
+ member_rows.append({
352
+ "member_index": len(members) - 1,
353
+ "snapshot_index": snapshot_index,
354
+ "variant": str(variant["name"]),
355
+ "ring_degrees": list(variant["ring_degrees"]),
356
+ "level_weights": level_weights.tolist(),
357
+ "pressure_fraction": pressure_fraction,
358
+ "curvature_fraction": round(float(curvature), 4),
359
+ "tendency_scale": round(float(tendency_scale), 4),
360
+ "sampled_waypoints": waypoints,
361
+ })
362
+ member_weights = np.asarray(weights, dtype="float64")
363
+ member_weights /= member_weights.sum()
364
+ return np.stack(members).astype("float32"), member_weights, {
365
+ "version": VERSION,
366
+ "policy": "causal dynamic multi-level large-system analysis ensemble",
367
+ "input_policy": "current, t-12, and t-24 analysis fields only; the lead evolution is a bounded extrapolation of their observed tendency; no future analysis, forecast product, official track, or future observed row",
368
+ "channels": ["hgt500", "u850", "v850", "u500", "v500", "u200", "v200"],
369
+ "snapshot_weights": snap_weights.tolist(),
370
+ "member_weights": member_weights.tolist(),
371
+ "member_count": len(member_rows),
372
+ "route_variants": [dict(item) for item in route_variants],
373
+ "curvature_variants": [float(value) for value in curve_values],
374
+ "tendency_scales": [float(value) for value in tendency_values],
375
+ "tendency_weights": tendency_weights.tolist(),
376
+ "tendency_method": "bounded current-minus-past analysis tendency, capped at two 12-hour differences",
377
+ "history_motion_km_per_6h": None if history_step is None else history_step.tolist(),
378
+ "member_rows": member_rows,
379
+ "large_systems": [
380
+ "Pacific subtropical ridge represented by broad 850/500-hPa flow and 500-hPa height field",
381
+ "midlatitude trough and jet influence represented by broad 500/200-hPa rings",
382
+ "pressure-gradient steering represented by causal SLP geostrophic component when supplied",
383
+ ],
384
+ }
385
+
386
+
387
+ def integrate_from_issue(member_displacements: np.ndarray, base_latitude: float, base_longitude: float) -> list[list[dict]]:
388
+ """Convert member km steps to serializable geographic paths."""
389
+
390
+ paths = []
391
+ for member in np.asarray(member_displacements, dtype="float32"):
392
+ lat, lon = float(base_latitude), float(base_longitude)
393
+ points = []
394
+ for lead, step in enumerate(member, start=1):
395
+ lon += float(step[0]) / (111.2 * max(math.cos(math.radians(lat)), 0.20))
396
+ lat += float(step[1]) / 111.2
397
+ lon %= 360.0
398
+ points.append({
399
+ "lead_hours": lead * 6,
400
+ "latitude": round(lat, 4),
401
+ "longitude": round(lon, 4),
402
+ })
403
+ paths.append(points)
404
+ return paths
405
+
406
+
407
+ def weighted_route(member_displacements: np.ndarray, member_weights: np.ndarray) -> np.ndarray:
408
+ return np.tensordot(np.asarray(member_weights, dtype="float32"), np.asarray(member_displacements, dtype="float32"), axes=(0, 0)).astype("float32")
409
+
410
+
411
+ __all__ = [
412
+ "VERSION",
413
+ "ROUTE_VARIANTS",
414
+ "CURVATURE_VARIANTS",
415
+ "SNAPSHOT_WEIGHTS",
416
+ "build_route",
417
+ "integrate_from_issue",
418
+ "weighted_route",
419
+ ]
trackformer_1_1_intensity.py ADDED
@@ -0,0 +1,748 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Causal Trackformer1.1 intensity and wind-structure inference.
3
+
4
+ The Trackformer1.1 route is responsible for position and the western-Pacific pressure
5
+ state. This module supplies the previously missing storm-structure outputs
6
+ without changing that route: maximum sustained wind, central pressure, radius
7
+ of maximum wind, and the four-quadrant R34/R50/R64 radii.
8
+
9
+ The default weights are a validated residual-anchor spatial ensemble with a
10
+ secondary structure expert and causal temporal branch. They are loaded only
11
+ for inference and are conditioned on the same nine-step observed track window
12
+ plus the current four-channel analysis patch.
13
+ No positive-lead atmospheric field or official agency forecast is consumed.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import os
20
+ from pathlib import Path
21
+
22
+ import numpy as np
23
+ import torch
24
+ import torch.nn as nn
25
+
26
+
27
+ LEADS = 20
28
+ NM_TO_KM = 1.852
29
+ TARGET_SCALE = np.asarray([100.0, 100.0, 35.0, 20.0, 50.0] + [50.0] * 12, dtype="float32")
30
+ STRUCTURE_SCALE = TARGET_SCALE[2:]
31
+ THERMO_ENV_COLS = (
32
+ [4, 5, 6, 7]
33
+ + list(range(8, 20))
34
+ + list(range(24, 40))
35
+ + [44, 45, 46, 47, 48, 49, 50, 51, 52, 53]
36
+ )
37
+ # The 90th percentile of positive six-hour central-pressure changes in the
38
+ # training split is 6 hPa. Use that train-only statistic to stop a coarse
39
+ # forecast-map minimum from disappearing in one step and being replaced by a
40
+ # different synoptic cell.
41
+ MAP_PRESSURE_RECOVERY_LIMIT_HPA = 6.0
42
+
43
+
44
+ def sinusoidal(length: int, width: int) -> torch.Tensor:
45
+ position = torch.arange(length).unsqueeze(1).float()
46
+ divisor = torch.exp(torch.arange(0, width, 2).float() * (-np.log(10000.0) / width))
47
+ result = torch.zeros(length, width)
48
+ result[:, 0::2] = torch.sin(position * divisor)
49
+ result[:, 1::2] = torch.cos(position * divisor)
50
+ return result
51
+
52
+
53
+ class StructureSpatialExpert(nn.Module):
54
+ """Inference copy of the architecture used by the frozen experts."""
55
+
56
+ def __init__(self, width: int, layers: int, heads: int, structure_residual: bool = False):
57
+ super().__init__()
58
+ self.structure_residual = bool(structure_residual)
59
+ self.track_proj = nn.Linear(len(THERMO_ENV_COLS), width)
60
+ self.register_buffer("track_time", sinusoidal(9, width).unsqueeze(0))
61
+ track_layer = nn.TransformerEncoderLayer(
62
+ width,
63
+ heads,
64
+ width * 4,
65
+ 0.12,
66
+ batch_first=True,
67
+ norm_first=True,
68
+ activation="gelu",
69
+ )
70
+ self.track_encoder = nn.TransformerEncoder(track_layer, layers)
71
+ self.field_encoder = nn.Sequential(
72
+ nn.Conv2d(4, 64, 3, padding=1),
73
+ nn.GroupNorm(8, 64),
74
+ nn.SiLU(),
75
+ nn.Conv2d(64, width, 3, stride=2, padding=1),
76
+ nn.GroupNorm(8, width),
77
+ nn.SiLU(),
78
+ )
79
+ self.field_pool = nn.AvgPool2d(kernel_size=2, stride=2)
80
+ self.field_norm = nn.LayerNorm(width)
81
+ self.field_pos = nn.Parameter(torch.randn(1, 16, width) * 0.02)
82
+ decoder_layer = nn.TransformerDecoderLayer(
83
+ width,
84
+ heads,
85
+ width * 4,
86
+ 0.12,
87
+ batch_first=True,
88
+ norm_first=True,
89
+ activation="gelu",
90
+ )
91
+ self.query = nn.Parameter(torch.randn(1, LEADS, width) * 0.02)
92
+ self.register_buffer("lead_time", sinusoidal(LEADS, width).unsqueeze(0))
93
+ self.decoder = nn.TransformerDecoder(decoder_layer, layers)
94
+ self.state = nn.Linear(width, 15)
95
+ self.log_scale = nn.Linear(width, 15)
96
+
97
+ def forward(
98
+ self,
99
+ track: torch.Tensor,
100
+ field: torch.Tensor,
101
+ current: torch.Tensor,
102
+ available: torch.Tensor,
103
+ current_structure: torch.Tensor | None = None,
104
+ structure_available: torch.Tensor | None = None,
105
+ ) -> tuple[torch.Tensor, torch.Tensor]:
106
+ track_tokens = self.track_encoder(self.track_proj(track[:, :, THERMO_ENV_COLS]) + self.track_time)
107
+ field_tokens = self.field_pool(self.field_encoder(field)).flatten(2).transpose(1, 2)
108
+ field_tokens = self.field_norm(field_tokens + self.field_pos)
109
+ memory = torch.cat([track_tokens, field_tokens], dim=1)
110
+ query = (self.query + self.lead_time).expand(track.shape[0], -1, -1)
111
+ hidden = self.decoder(query, memory)
112
+ state = self.state(hidden)
113
+ state = state.clone()
114
+ state[:, :, :2] = state[:, :, :2] + (current * available)[:, None, :]
115
+ if self.structure_residual:
116
+ if current_structure is None or structure_available is None:
117
+ raise ValueError("structure residual mode requires current structure and availability tensors")
118
+ state[:, :, 2:] = state[:, :, 2:] + current_structure[:, None, :] * structure_available[:, None, :]
119
+ return state, self.log_scale(hidden)
120
+
121
+
122
+ def _device(requested: str | None) -> torch.device:
123
+ value = requested or os.environ.get("TRACKFORMER_1_1_DEVICE")
124
+ if value:
125
+ return torch.device(value)
126
+ if torch.cuda.is_available():
127
+ return torch.device("cuda")
128
+ if torch.backends.mps.is_available():
129
+ return torch.device("mps")
130
+ return torch.device("cpu")
131
+
132
+
133
+ def _calibrated_wind(states: np.ndarray, current_wind: float, calibration: dict) -> np.ndarray:
134
+ alphas = calibration.get("wind_blend_alpha")
135
+ if not alphas:
136
+ return states[:, :, 0]
137
+ predicted = states[:, :, 0]
138
+ result = np.empty_like(predicted)
139
+ for lead, alpha in enumerate(alphas[:LEADS]):
140
+ result[:, lead] = float(alpha) * predicted[:, lead] + (1.0 - float(alpha)) * current_wind
141
+ return np.clip(result, 0.0, 190.0)
142
+
143
+
144
+ def _calibrated_pressure(
145
+ states: np.ndarray,
146
+ current_wind: float,
147
+ current_pressure: float,
148
+ previous_wind: float,
149
+ previous_pressure: float,
150
+ calibrated_wind: np.ndarray,
151
+ calibration: dict,
152
+ ) -> np.ndarray:
153
+ joint = calibration.get("pressure_joint_calibrations")
154
+ if not joint:
155
+ return np.clip(states[:, :, 1], 850.0, 1025.0)
156
+ predicted_pressure = states[:, :, 1]
157
+ result = np.empty_like(predicted_pressure)
158
+ for lead, item in enumerate(joint[:LEADS]):
159
+ features = np.column_stack([
160
+ np.full(len(predicted_pressure), 1.0, dtype="float32"),
161
+ np.full(len(predicted_pressure), current_pressure, dtype="float32"),
162
+ np.full(len(predicted_pressure), current_wind, dtype="float32"),
163
+ calibrated_wind[:, lead],
164
+ predicted_pressure[:, lead],
165
+ predicted_pressure[:, lead] - current_pressure,
166
+ calibrated_wind[:, lead] - current_wind,
167
+ np.full(len(predicted_pressure), current_pressure - previous_pressure, dtype="float32"),
168
+ np.full(len(predicted_pressure), current_wind - previous_wind, dtype="float32"),
169
+ ])
170
+ mean = np.asarray(item["mean"], dtype="float32")
171
+ scale = np.maximum(np.asarray(item["scale"], dtype="float32"), 1e-6)
172
+ normalized = (features - mean) / scale
173
+ normalized[:, 0] = 1.0
174
+ result[:, lead] = normalized @ np.asarray(item["beta"], dtype="float32")
175
+ anchor_alpha = calibration.get("pressure_anchor_alpha")
176
+ if anchor_alpha and np.isfinite(current_pressure) and current_pressure > 0.0:
177
+ for lead, alpha in enumerate(anchor_alpha[:LEADS]):
178
+ alpha = float(np.clip(alpha, 0.0, 2.0))
179
+ result[:, lead] = alpha * result[:, lead] + (1.0 - alpha) * float(current_pressure)
180
+ return np.clip(result, 850.0, 1025.0)
181
+
182
+
183
+ def _sanitize(states: np.ndarray) -> np.ndarray:
184
+ """Apply output bounds and preserve the R34 >= R50 >= R64 ordering."""
185
+
186
+ result = np.asarray(states, dtype="float32").copy()
187
+ result[:, :, 0] = np.clip(result[:, :, 0], 0.0, 190.0)
188
+ result[:, :, 1] = np.clip(result[:, :, 1], 850.0, 1025.0)
189
+ result[:, :, 2] = np.clip(result[:, :, 2], 0.0, 300.0)
190
+ radii = np.clip(result[:, :, 3:15], 0.0, 1000.0)
191
+ for offset in range(4):
192
+ radii[:, :, 4 + offset] = np.minimum(radii[:, :, 4 + offset], radii[:, :, offset])
193
+ radii[:, :, 8 + offset] = np.minimum(radii[:, :, 8 + offset], radii[:, :, 4 + offset])
194
+ result[:, :, 3:15] = radii
195
+ return result
196
+
197
+
198
+ def _row(mean: np.ndarray, spread: np.ndarray, lead: int) -> dict:
199
+ # The historical IBTrACS structure labels are nautical miles. Keep the
200
+ # neural/calibration state in that native unit, but expose all distance
201
+ # fields in kilometres because the map and route use kilometres.
202
+ radii = mean[lead, 3:15] * NM_TO_KM
203
+ radius_spread = spread[lead, 3:15] * NM_TO_KM
204
+ return {
205
+ "vmax_kt": round(float(mean[lead, 0]), 2),
206
+ "vmax_spread_kt": round(float(spread[lead, 0]), 2),
207
+ "central_pressure_hpa": round(float(mean[lead, 1]), 2),
208
+ "pressure_spread_hpa": round(float(spread[lead, 1]), 2),
209
+ "rmw_km": round(float(mean[lead, 2] * NM_TO_KM), 2),
210
+ "rmw_spread_km": round(float(spread[lead, 2] * NM_TO_KM), 2),
211
+ "wind_radii_km": [round(float(value), 2) for value in radii],
212
+ "wind_radii_spread_km": [round(float(value), 2) for value in radius_spread],
213
+ # Keep the map renderer's generic pressure field alias as well.
214
+ "pressure_hpa": round(float(mean[lead, 1]), 2),
215
+ }
216
+
217
+
218
+ def _map_grid_feature(
219
+ pressure: np.ndarray,
220
+ fields: np.ndarray,
221
+ latitude: np.ndarray,
222
+ longitude: np.ndarray,
223
+ query_latitude: float,
224
+ query_longitude: float,
225
+ ) -> dict:
226
+ """Extract a storm-relative signal from one forecast pressure map.
227
+
228
+ The map can be much coarser than the route grid, especially for the Tip
229
+ reanalysis. We therefore use a nearby pressure minimum, a broad annulus
230
+ environment, and quadrant-wise anomaly extents rather than pretending that
231
+ a single grid cell is an exact storm center.
232
+ """
233
+
234
+ pressure = np.asarray(pressure, dtype="float32")
235
+ fields = np.asarray(fields, dtype="float32")
236
+ latitude = np.asarray(latitude, dtype="float32").reshape(-1)
237
+ longitude = np.asarray(longitude, dtype="float32").reshape(-1)
238
+ lat_order = np.argsort(latitude)
239
+ lon_order = np.argsort(longitude)
240
+ latitude = latitude[lat_order]
241
+ longitude = longitude[lon_order]
242
+ pressure = pressure[np.ix_(lat_order, lon_order)]
243
+ fields = fields[:, lat_order, :][:, :, lon_order]
244
+ lat_grid, lon_grid = np.meshgrid(latitude, longitude, indexing="ij")
245
+ delta_lon = ((lon_grid - float(query_longitude) + 180.0) % 360.0) - 180.0
246
+ delta_lat = lat_grid - float(query_latitude)
247
+ distance_deg = np.hypot(delta_lat, delta_lon * np.cos(np.deg2rad(float(query_latitude))))
248
+ query_row, query_column = np.unravel_index(int(np.nanargmin(distance_deg)), distance_deg.shape)
249
+ query_pressure = float(pressure[query_row, query_column])
250
+ query_wind850 = float(np.hypot(fields[1, query_row, query_column], fields[2, query_row, query_column]) * 1.94384) if fields.shape[0] >= 3 else float("nan")
251
+ # A minimum farther than this is likely a separate synoptic system rather
252
+ # than the cyclone represented by the route point. Keep the local query
253
+ # as a fallback instead of allowing a distant low to control intensity.
254
+ nearby = distance_deg <= 6.0
255
+ finite = np.isfinite(pressure)
256
+ candidate = np.where(nearby & finite, pressure, np.inf)
257
+ if not np.isfinite(candidate).any():
258
+ row, column = query_row, query_column
259
+ else:
260
+ row, column = np.unravel_index(int(np.argmin(candidate)), candidate.shape)
261
+ center_latitude = float(latitude[row])
262
+ center_longitude = float(longitude[column])
263
+ center_pressure = float(pressure[row, column])
264
+ center_delta_lon = ((lon_grid - center_longitude + 180.0) % 360.0) - 180.0
265
+ center_delta_lat = lat_grid - center_latitude
266
+ center_distance_deg = np.hypot(
267
+ center_delta_lat,
268
+ center_delta_lon * np.cos(np.deg2rad(center_latitude)),
269
+ )
270
+ annulus = (
271
+ (center_distance_deg >= 2.5)
272
+ & (center_distance_deg <= 6.0)
273
+ & np.isfinite(pressure)
274
+ )
275
+ environment = float(np.nanmedian(pressure[annulus])) if annulus.any() else center_pressure + 15.0
276
+ anomaly = np.maximum(environment - pressure, 0.0)
277
+ # Four degrees is a conservative upper bound for a pressure-derived
278
+ # tropical wind footprint on this coarse map. Without it, a weak broad
279
+ # gradient would be misreported as an 800-km R34.
280
+ core = (center_distance_deg <= 4.0) & np.isfinite(anomaly)
281
+ peak_anomaly = float(np.nanmax(anomaly[core])) if core.any() else 0.0
282
+ radii: list[float] = []
283
+ # These are pressure-anomaly fractions used as a stable proxy for the
284
+ # R34/R50/R64 shape. The learned radius output remains the absolute
285
+ # baseline; only its map-observed expansion/contraction is applied.
286
+ for fraction in (0.25, 0.50, 0.70):
287
+ for quadrant in ((1.0, 1.0), (1.0, -1.0), (-1.0, -1.0), (-1.0, 1.0)):
288
+ north_sign, east_sign = quadrant
289
+ quadrant_mask = core & (center_delta_lat * north_sign >= 0.0) & (center_delta_lon * east_sign >= 0.0)
290
+ threshold = peak_anomaly * fraction
291
+ valid = quadrant_mask & (anomaly >= threshold) if peak_anomaly > 0.0 else np.zeros_like(core)
292
+ if valid.any():
293
+ radii.append(float(np.nanmax(center_distance_deg[valid]) * 111.2))
294
+ else:
295
+ radii.append(float("nan"))
296
+ wind850 = float(np.hypot(fields[1, row, column], fields[2, row, column]) * 1.94384) if fields.shape[0] >= 3 else float("nan")
297
+ center_offset_km = float(distance_deg[row, column] * 111.2)
298
+ center_trusted = bool(center_offset_km <= 333.6 and (environment - center_pressure) >= 4.0)
299
+ offset_degrees = center_offset_km / 111.2
300
+ center_confidence = 1.0 if center_trusted else float(np.clip(1.0 - 0.75 * (offset_degrees - 3.0) / 3.0, 0.25, 0.75))
301
+ return {
302
+ "map_min_pressure_hpa": round(center_pressure, 2),
303
+ "map_query_pressure_hpa": round(query_pressure, 2) if np.isfinite(query_pressure) else None,
304
+ "map_environment_pressure_hpa": round(environment, 2),
305
+ "map_pressure_deficit_hpa": round(max(0.0, environment - center_pressure), 2),
306
+ "map_center_latitude": round(center_latitude, 3),
307
+ "map_center_longitude": round(center_longitude % 360.0, 3),
308
+ "map_center_offset_km": round(center_offset_km, 2),
309
+ "map_center_trusted": center_trusted,
310
+ "map_center_confidence": round(center_confidence, 3),
311
+ "map_wind850_kt": round(wind850, 2) if np.isfinite(wind850) else None,
312
+ "map_query_wind850_kt": round(query_wind850, 2) if np.isfinite(query_wind850) else None,
313
+ "map_pressure_radii_km": [round(value, 2) if np.isfinite(value) else None for value in radii],
314
+ }
315
+
316
+
317
+ def couple_forecast_to_pressure_map(
318
+ structure_rows: list[dict],
319
+ pressure_states: np.ndarray,
320
+ field_states: np.ndarray,
321
+ latitude: np.ndarray,
322
+ longitude: np.ndarray,
323
+ base_latitude: float,
324
+ base_longitude: float,
325
+ forecast_points: list[dict],
326
+ current_wind: float,
327
+ current_pressure: float,
328
+ ) -> tuple[list[dict], dict]:
329
+ """Use the causal pressure-map trajectory to correct structure forecasts.
330
+
331
+ The correction is anchored to the observed current wind and pressure, so
332
+ coarse reanalysis cannot replace a known storm intensity. Future changes
333
+ in the map minimum drive bounded pressure/wind changes, and changes in the
334
+ map's quadrant anomaly extent rescale the learned radii. This is the same
335
+ forecast-state family used by the track route, not a future official field.
336
+ """
337
+
338
+ pressure_states = np.asarray(pressure_states, dtype="float32")
339
+ field_states = np.asarray(field_states, dtype="float32")
340
+ if pressure_states.ndim != 3 or field_states.ndim != 4 or field_states.shape[1] < 3:
341
+ raise ValueError(f"invalid map state shapes: {pressure_states.shape}, {field_states.shape}")
342
+ if len(structure_rows) != len(forecast_points) or len(pressure_states) < len(structure_rows) + 1:
343
+ raise ValueError("map states, route points, and structure rows have incompatible lengths")
344
+ if not np.isfinite(current_wind) or not np.isfinite(current_pressure):
345
+ return structure_rows, {
346
+ "enabled": False,
347
+ "reason": "current observed wind and pressure are unavailable",
348
+ "official_forecasts_used": False,
349
+ }
350
+
351
+ features = [_map_grid_feature(
352
+ pressure_states[index],
353
+ field_states[index],
354
+ latitude,
355
+ longitude,
356
+ base_latitude if index == 0 else float(forecast_points[index - 1]["lat"]),
357
+ base_longitude if index == 0 else float(forecast_points[index - 1]["lon"]),
358
+ ) for index in range(len(structure_rows) + 1)]
359
+ # Use a trusted nearby minimum when available. Otherwise sample the map
360
+ # at the route point; this keeps another low/typhoon from hijacking the
361
+ # structure forecast merely because it is the strongest minimum nearby.
362
+ effective_minimum = np.asarray([
363
+ float(item["map_query_pressure_hpa"])
364
+ + float(item["map_center_confidence"]) * (
365
+ float(item["map_min_pressure_hpa"]) - float(item["map_query_pressure_hpa"])
366
+ )
367
+ for item in features
368
+ ], dtype="float32")
369
+ effective_deficit = np.asarray([
370
+ float(item["map_pressure_deficit_hpa"]) * float(item["map_center_confidence"])
371
+ for item in features
372
+ ], dtype="float32")
373
+ effective_wind850 = np.asarray([
374
+ float(item["map_query_wind850_kt"])
375
+ + float(item["map_center_confidence"]) * (
376
+ float(item["map_wind850_kt"]) - float(item["map_query_wind850_kt"])
377
+ )
378
+ for item in features
379
+ ], dtype="float32")
380
+ # A short causal smoother prevents a one-cell minimum handoff from making
381
+ # a six-hour intensity jump while retaining the map's lead-time trend.
382
+ smooth_minimum = effective_minimum.copy()
383
+ smooth_deficit = effective_deficit.copy()
384
+ smooth_wind850 = effective_wind850.copy()
385
+ for index in range(1, len(smooth_minimum)):
386
+ smooth_minimum[index] = 0.65 * smooth_minimum[index - 1] + 0.35 * effective_minimum[index]
387
+ smooth_deficit[index] = 0.65 * smooth_deficit[index - 1] + 0.35 * effective_deficit[index]
388
+ if np.isfinite(effective_wind850[index]) and np.isfinite(smooth_wind850[index - 1]):
389
+ smooth_wind850[index] = 0.65 * smooth_wind850[index - 1] + 0.35 * effective_wind850[index]
390
+ if np.isfinite(smooth_minimum[index - 1]) and np.isfinite(smooth_minimum[index]):
391
+ smooth_minimum[index] = min(
392
+ smooth_minimum[index],
393
+ smooth_minimum[index - 1] + MAP_PRESSURE_RECOVERY_LIMIT_HPA,
394
+ )
395
+ reference_radii = np.asarray([
396
+ float(value) if value is not None else np.nan
397
+ for value in features[0]["map_pressure_radii_km"]
398
+ ], dtype="float32")
399
+ corrected: list[dict] = []
400
+ map_weight = 0.35
401
+ for index, base in enumerate(structure_rows, start=1):
402
+ row = dict(base)
403
+ # A distant minimum is an environmental candidate, not a reliable
404
+ # storm-center intensity observation. The previous floor kept more
405
+ # than one-fifth of the map correction active even when confidence
406
+ # had fallen to 0.25, which made another synoptic cell rewrite the
407
+ # central pressure and wind. Let confidence directly control the
408
+ # intensity correction; the map remains available for diagnostics and
409
+ # trusted radius adjustments.
410
+ row_weight = map_weight * float(np.clip(features[index]["map_center_confidence"], 0.0, 1.0))
411
+ map_pressure = float(current_pressure + smooth_minimum[index] - smooth_minimum[0])
412
+ pressure_signal = float(np.clip(
413
+ 0.75 * (smooth_minimum[0] - smooth_minimum[index])
414
+ + 0.25 * (smooth_deficit[index] - smooth_deficit[0]),
415
+ -30.0,
416
+ 30.0,
417
+ ))
418
+ wind_signal = pressure_signal
419
+ if np.isfinite(smooth_wind850[index]) and np.isfinite(smooth_wind850[0]):
420
+ wind_signal += float(np.clip(0.18 * (smooth_wind850[index] - smooth_wind850[0]), -8.0, 8.0))
421
+ map_wind = float(current_wind + 0.70 * wind_signal)
422
+ row["central_pressure_hpa"] = round(float(np.clip(
423
+ (1.0 - row_weight) * float(base["central_pressure_hpa"]) + row_weight * map_pressure,
424
+ 850.0,
425
+ 1025.0,
426
+ )), 2)
427
+ row["pressure_hpa"] = row["central_pressure_hpa"]
428
+ row["vmax_kt"] = round(float(np.clip(
429
+ (1.0 - row_weight) * float(base["vmax_kt"]) + row_weight * map_wind,
430
+ 0.0,
431
+ 190.0,
432
+ )), 2)
433
+ row["pressure_spread_hpa"] = round(float(np.hypot(
434
+ float(base.get("pressure_spread_hpa", 0.0)),
435
+ row_weight * abs(map_pressure - float(base["central_pressure_hpa"])),
436
+ )), 2)
437
+ row["vmax_spread_kt"] = round(float(np.hypot(
438
+ float(base.get("vmax_spread_kt", 0.0)),
439
+ row_weight * abs(map_wind - float(base["vmax_kt"])),
440
+ )), 2)
441
+ map_radii = np.asarray([
442
+ float(value) if value is not None else np.nan
443
+ for value in features[index]["map_pressure_radii_km"]
444
+ ], dtype="float32")
445
+ base_radii = np.asarray(base["wind_radii_km"], dtype="float32")
446
+ if not features[0]["map_center_trusted"] or not features[index]["map_center_trusted"]:
447
+ map_radii[:] = np.nan
448
+ if np.isfinite(reference_radii).any() and np.isfinite(map_radii).any():
449
+ valid = np.isfinite(reference_radii) & np.isfinite(map_radii) & (reference_radii >= 20.0)
450
+ ratios = np.ones(12, dtype="float32")
451
+ ratios[valid] = np.clip(map_radii[valid] / reference_radii[valid], 0.60, 1.50)
452
+ if valid.any():
453
+ ratios[~valid] = float(np.clip(np.nanmedian(ratios[valid]), 0.60, 1.50))
454
+ radius_factor = 1.0 + 0.25 * (ratios - 1.0)
455
+ adjusted_radii = base_radii * radius_factor
456
+ else:
457
+ adjusted_radii = base_radii
458
+ adjusted_radii = np.clip(adjusted_radii, 0.0, 1000.0)
459
+ for quadrant in range(4):
460
+ adjusted_radii[4 + quadrant] = min(adjusted_radii[4 + quadrant], adjusted_radii[quadrant])
461
+ adjusted_radii[8 + quadrant] = min(adjusted_radii[8 + quadrant], adjusted_radii[4 + quadrant])
462
+ row["wind_radii_km"] = [round(float(value), 2) for value in adjusted_radii]
463
+ map_rmw_ratio = 1.0
464
+ if features[0]["map_center_trusted"] and features[index]["map_center_trusted"]:
465
+ ref_r64 = float(np.nanmedian(reference_radii[8:])) if np.isfinite(reference_radii[8:]).any() else np.nan
466
+ future_r64 = float(np.nanmedian(map_radii[8:])) if np.isfinite(map_radii[8:]).any() else np.nan
467
+ if np.isfinite(ref_r64) and np.isfinite(future_r64) and ref_r64 >= 20.0:
468
+ map_rmw_ratio = float(np.clip(future_r64 / ref_r64, 0.75, 1.25))
469
+ row["rmw_km"] = round(float(np.clip(
470
+ float(base["rmw_km"]) * (1.0 + 0.15 * (map_rmw_ratio - 1.0)),
471
+ 0.0,
472
+ 300.0,
473
+ )), 2)
474
+ row["pressure_map_features"] = features[index]
475
+ corrected.append(row)
476
+ metadata = {
477
+ "enabled": True,
478
+ "method": "causal forecast pressure-map query plus confidence-weighted local-minimum/anomaly-radius coupling",
479
+ "map_weight": map_weight,
480
+ "minimum_confidence": "local minimum contribution decays directly with route-to-minimum confidence; untrusted minima have near-zero central-intensity weight",
481
+ "pressure_recovery_limit_hpa_per_6h": MAP_PRESSURE_RECOVERY_LIMIT_HPA,
482
+ "pressure_recovery_limit_source": "training-split 90th percentile of positive six-hour pressure changes",
483
+ "pressure_signal_weights": {
484
+ "tracked_map_minimum": 0.75,
485
+ "map_anomaly_extent": 0.25,
486
+ },
487
+ "wind_pressure_anchor": "observed current wind/pressure; map drives only future changes",
488
+ "radius_method": "learned radius baseline rescaled by trusted local quadrant pressure-anomaly extent; max four-degree footprint",
489
+ "map_features": features,
490
+ "official_forecasts_used": False,
491
+ "positive_lead_weather_product_used": False,
492
+ }
493
+ return corrected, metadata
494
+
495
+
496
+ class Trackformer11IntensityEnsemble:
497
+ """Load frozen Trackformer1.1 experts and emit calibrated structure rows."""
498
+
499
+ def __init__(
500
+ self,
501
+ checkpoint_root: Path,
502
+ calibration_path: Path | None = None,
503
+ device: str | None = None,
504
+ ):
505
+ self.checkpoint_root = Path(checkpoint_root)
506
+ self.calibration_path = Path(calibration_path) if calibration_path else None
507
+ self.device = _device(device)
508
+ paths = sorted(self.checkpoint_root.glob("trackformer_1_1_intensity_seed*.pt"))
509
+ if len(paths) < 3:
510
+ raise FileNotFoundError(
511
+ f"expected three Trackformer1.1 intensity checkpoints in {self.checkpoint_root}; found {len(paths)}"
512
+ )
513
+ self.models: list[StructureSpatialExpert] = []
514
+ for path in paths[:3]:
515
+ payload = torch.load(path, map_location="cpu", weights_only=False)
516
+ config = payload["config"]
517
+ model = StructureSpatialExpert(
518
+ config["width"],
519
+ config["layers"],
520
+ config["heads"],
521
+ structure_residual=bool(config.get("structure_residual", False)),
522
+ )
523
+ model.load_state_dict(payload["model"])
524
+ self.models.append(model.to(self.device).eval())
525
+ self.calibration = {}
526
+ if self.calibration_path and self.calibration_path.exists():
527
+ self.calibration = json.loads(self.calibration_path.read_text(encoding="utf-8"))
528
+ self.structure_models: list[StructureSpatialExpert] = []
529
+ structure_root = self.calibration.get("structure_checkpoint_root")
530
+ if structure_root:
531
+ structure_root_path = self._resolve_root(structure_root)
532
+ structure_paths = sorted(structure_root_path.glob("trackformer_1_1_structure_seed*.pt"))
533
+ for path in structure_paths[:3]:
534
+ payload = torch.load(path, map_location="cpu", weights_only=False)
535
+ config = payload["config"]
536
+ model = StructureSpatialExpert(
537
+ config["width"], config["layers"], config["heads"],
538
+ structure_residual=bool(config.get("structure_residual", False)),
539
+ )
540
+ model.load_state_dict(payload["model"])
541
+ self.structure_models.append(model.to(self.device).eval())
542
+ self.temporal_models = []
543
+ temporal_root = self.calibration.get("temporal_checkpoint_root")
544
+ self.temporal_calibration = self.calibration
545
+ if temporal_root:
546
+ from trackformer_1_1_temporal import TemporalStructureSpatial
547
+
548
+ temporal_root_path = self._resolve_root(temporal_root)
549
+ temporal_paths = sorted(temporal_root_path.glob("trackformer_1_1_temporal_seed*.pt"))
550
+ for path in temporal_paths[:3]:
551
+ payload = torch.load(path, map_location="cpu", weights_only=False)
552
+ config = payload["config"]
553
+ model = TemporalStructureSpatial(
554
+ config["width"], config["layers"], config["heads"]
555
+ )
556
+ model.load_state_dict(payload["model"])
557
+ self.temporal_models.append(model.to(self.device).eval())
558
+
559
+ @staticmethod
560
+ def _resolve_root(value: str | Path) -> Path:
561
+ path = Path(value)
562
+ return path if path.is_absolute() else Path(__file__).resolve().parent / path
563
+
564
+ @torch.no_grad()
565
+ def predict(
566
+ self,
567
+ track: np.ndarray,
568
+ field: np.ndarray,
569
+ current_wind: float,
570
+ current_pressure: float,
571
+ previous_wind: float,
572
+ previous_pressure: float,
573
+ current_structure: np.ndarray | None = None,
574
+ history_field: np.ndarray | None = None,
575
+ history_available: np.ndarray | None = None,
576
+ ) -> tuple[list[dict], dict]:
577
+ track = np.asarray(track, dtype="float32")
578
+ field = np.asarray(field, dtype="float32")
579
+ if track.shape != (9, 54):
580
+ raise ValueError(f"Trackformer1.1 track must have shape (9, 54), got {track.shape}")
581
+ if field.shape != (4, 17, 17):
582
+ raise ValueError(f"Trackformer1.1 field must have shape (4, 17, 17), got {field.shape}")
583
+ if history_field is not None:
584
+ history_field = np.asarray(history_field, dtype="float32")
585
+ if history_field.shape != (8, 17, 17):
586
+ raise ValueError(
587
+ f"Trackformer1.1 history field must have shape (8, 17, 17), got {history_field.shape}"
588
+ )
589
+ if history_available is None:
590
+ history_available = np.ones(2, dtype="float32")
591
+ history_available = np.asarray(history_available, dtype="float32").reshape(-1)
592
+ if history_available.shape != (2,):
593
+ raise ValueError("Trackformer1.1 history availability must have shape (2,)")
594
+ if not np.isfinite(track).all() or not np.isfinite(field).all():
595
+ raise ValueError("Trackformer1.1 inputs contain non-finite values")
596
+ track_clip = self.calibration.get("track_input_clip", {})
597
+ lower = np.asarray(track_clip.get("lower", []), dtype="float32")
598
+ upper = np.asarray(track_clip.get("upper", []), dtype="float32")
599
+ if lower.shape == (track.shape[1],) and upper.shape == (track.shape[1],):
600
+ track = np.clip(track, lower[None, :], upper[None, :])
601
+ if current_structure is not None:
602
+ current_structure = np.asarray(current_structure, dtype="float32").reshape(-1)
603
+ if current_structure.shape != (13,):
604
+ raise ValueError(
605
+ "Trackformer1.1 current structure must contain RMW plus twelve radii in native nautical miles"
606
+ )
607
+ structure_available = (
608
+ np.isfinite(current_structure).astype("float32")
609
+ if current_structure is not None
610
+ else None
611
+ )
612
+ current_available = float(np.isfinite(current_wind) and np.isfinite(current_pressure))
613
+ current_values = np.nan_to_num(
614
+ np.asarray([current_wind, current_pressure], dtype="float32") / TARGET_SCALE[2:4],
615
+ nan=0.0,
616
+ )
617
+ available = np.asarray([current_available, current_available], dtype="float32")
618
+ track_tensor = torch.from_numpy(track[None]).to(self.device)
619
+ field_tensor = torch.from_numpy(field[None]).to(self.device)
620
+ current_tensor = torch.from_numpy(current_values[None]).to(self.device)
621
+ available_tensor = torch.from_numpy(available[None]).to(self.device)
622
+ structure_tensor = None
623
+ structure_available_tensor = None
624
+ if current_structure is not None:
625
+ structure_tensor = torch.from_numpy(
626
+ np.nan_to_num(
627
+ current_structure / TARGET_SCALE[4:],
628
+ nan=0.0,
629
+ posinf=0.0,
630
+ neginf=0.0,
631
+ )[None]
632
+ ).to(self.device)
633
+ structure_available_tensor = torch.from_numpy(structure_available[None]).to(self.device)
634
+ states = np.stack([
635
+ model(
636
+ track_tensor,
637
+ field_tensor,
638
+ current_tensor,
639
+ available_tensor,
640
+ structure_tensor,
641
+ structure_available_tensor,
642
+ )[0][0].detach().cpu().numpy()
643
+ for model in self.models
644
+ ]).astype("float32")
645
+ states *= STRUCTURE_SCALE[None, None, :]
646
+ if self.structure_models:
647
+ structure_states = np.stack([
648
+ model(track_tensor, field_tensor, current_tensor, available_tensor)[0][0].detach().cpu().numpy()
649
+ for model in self.structure_models
650
+ ]).astype("float32") * STRUCTURE_SCALE[None, None, :]
651
+ expert_alpha = np.asarray(
652
+ self.calibration.get("structure_expert_alpha", []), dtype="float32"
653
+ )
654
+ if expert_alpha.shape != (LEADS, 13):
655
+ expert_alpha = np.zeros((LEADS, 13), dtype="float32")
656
+ for lead in range(LEADS):
657
+ states[:, lead, 2:] = (
658
+ expert_alpha[lead][None, :] * states[:, lead, 2:]
659
+ + (1.0 - expert_alpha[lead][None, :]) * structure_states[:, lead, 2:]
660
+ )
661
+ temporal_states = None
662
+ if self.temporal_models and history_field is not None and current_structure is not None:
663
+ history_tensor = torch.from_numpy(history_field[None]).to(self.device)
664
+ history_available_tensor = torch.from_numpy(history_available[None]).to(self.device)
665
+ temporal_states = np.stack([
666
+ model(
667
+ track_tensor,
668
+ field_tensor,
669
+ current_tensor,
670
+ available_tensor,
671
+ structure_tensor,
672
+ structure_available_tensor,
673
+ history_tensor,
674
+ history_available_tensor,
675
+ )[0][0].detach().cpu().numpy()
676
+ for model in self.temporal_models
677
+ ]).astype("float32") * STRUCTURE_SCALE[None, None, :]
678
+ if current_available:
679
+ calibrated_wind = _calibrated_wind(states, float(current_wind), self.calibration)
680
+ states[:, :, 1] = _calibrated_pressure(
681
+ states,
682
+ float(current_wind),
683
+ float(current_pressure),
684
+ float(previous_wind) if np.isfinite(previous_wind) else float(current_wind),
685
+ float(previous_pressure) if np.isfinite(previous_pressure) else float(current_pressure),
686
+ calibrated_wind,
687
+ self.calibration,
688
+ )
689
+ states[:, :, 0] = calibrated_wind
690
+ if temporal_states is not None:
691
+ temporal_alpha = self.calibration.get("temporal_wind_blend_alpha")
692
+ temporal_min_wind = float(self.calibration.get("temporal_wind_min_current_kt", 0.0))
693
+ if temporal_alpha and float(current_wind) >= temporal_min_wind:
694
+ temporal_calibration = {"wind_blend_alpha": temporal_alpha}
695
+ states[:, :, 0] = _calibrated_wind(
696
+ temporal_states,
697
+ float(current_wind),
698
+ temporal_calibration,
699
+ )
700
+ structure_alpha = np.asarray(
701
+ self.calibration.get("structure_blend_alpha", []), dtype="float32"
702
+ )
703
+ if current_structure is not None and structure_alpha.shape == (LEADS, 13):
704
+ available_structure = np.isfinite(current_structure)
705
+ anchor = np.nan_to_num(current_structure, nan=0.0, posinf=0.0, neginf=0.0)
706
+ for lead in range(LEADS):
707
+ alpha = np.clip(structure_alpha[lead], 0.0, 1.0)
708
+ for offset in np.flatnonzero(available_structure):
709
+ channel = 2 + int(offset)
710
+ states[:, lead, channel] = (
711
+ alpha[int(offset)] * states[:, lead, channel]
712
+ + (1.0 - alpha[int(offset)]) * anchor[int(offset)]
713
+ )
714
+ states = _sanitize(states)
715
+ mean = states.mean(axis=0)
716
+ spread = states.std(axis=0)
717
+ model_label = (
718
+ "Trackformer1.1 residual-anchor spatial ensemble"
719
+ if self.models and self.models[0].structure_residual
720
+ else "frozen spatial structure ensemble"
721
+ )
722
+ rows = [_row(mean, spread, lead) for lead in range(LEADS)]
723
+ metadata = {
724
+ "model": f"{model_label} used as the Trackformer1.1 intensity head",
725
+ "checkpoint_count": len(self.models),
726
+ "calibration": str(self.calibration_path) if self.calibration_path and self.calibration_path.exists() else None,
727
+ "field_contract": "4x17x17 analysis patch, q/31.75 then clipped to [-4,4]",
728
+ "outputs": ["vmax_kt", "central_pressure_hpa", "rmw_km", "wind_radii_km"],
729
+ "native_structure_unit": "nautical_miles",
730
+ "output_distance_unit": "kilometres",
731
+ "ensemble_spread": "standard deviation across the three frozen spatial experts",
732
+ "structure_calibration": "lead- and component-wise validation blend with observed current RMW/radii when available",
733
+ "structure_expert_blend": bool(self.structure_models),
734
+ "temporal_wind_branch": bool(
735
+ self.temporal_models
736
+ and history_field is not None
737
+ and current_available
738
+ and float(current_wind) >= float(self.calibration.get("temporal_wind_min_current_kt", 0.0))
739
+ ),
740
+ "temporal_wind_branch_policy": "same-storm t-12/t-24 analysis patches; wind only; enabled above the validation-selected current-wind gate; base pressure calibration retained",
741
+ "pressure_anchor_alpha": self.calibration.get("pressure_anchor_alpha"),
742
+ "pressure_anchor_policy": self.calibration.get("pressure_anchor_policy"),
743
+ "track_input_policy": self.calibration.get("track_input_clip", {}).get("method"),
744
+ "official_forecasts_used": False,
745
+ "positive_lead_weather_used": False,
746
+ "device": str(self.device),
747
+ }
748
+ return rows, metadata
trackformer_1_1_route.py ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Causal western-Pacific state and steering route.
3
+
4
+ This module first extrapolates a bounded regional atmospheric state from the
5
+ current, t-12, and t-24 analysis fields. It then integrates the track using
6
+ steering views that reach across the western Pacific rather than only a small
7
+ storm-centered ring. The pressure map and low-center diagnostics are derived
8
+ from the same analysis-only state.
9
+
10
+ No positive-lead weather field, official forecast track, or future observation
11
+ is accepted as an input.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import math
17
+ from typing import Sequence
18
+
19
+ import numpy as np
20
+
21
+ from trackformer_1_1_base_route import (
22
+ CURVATURE_VARIANTS,
23
+ SNAPSHOT_WEIGHTS,
24
+ TENDENCY_SCALES,
25
+ build_route,
26
+ )
27
+
28
+
29
+ VERSION = "Trackformer1.1-causal-western-Pacific-state-route"
30
+ CAUSAL_ONLY = True
31
+ PACIFIC_LON_RANGE = (100.0, 190.0)
32
+ PACIFIC_LAT_RANGE = (0.0, 60.0)
33
+ LEAD_HOURS = tuple(range(0, 121, 6))
34
+
35
+ # The outer views make Japan, the East China Sea, Taiwan, the Philippines,
36
+ # the subtropical ridge, and the western Pacific trough visible together.
37
+ PACIFIC_ROUTE_VARIANTS = (
38
+ {"name": "inner_850", "level_weights": (1.0, 0.0, 0.0), "ring_degrees": (3.0, 6.0), "pressure_fraction": 0.05, "weight": 0.10},
39
+ {"name": "deep_inner", "level_weights": (0.269, 0.500, 0.231), "ring_degrees": (4.0, 10.0), "pressure_fraction": 0.10, "weight": 0.14},
40
+ {"name": "broad_850_ridge", "level_weights": (1.0, 0.0, 0.0), "ring_degrees": (8.0, 20.0), "pressure_fraction": 0.15, "weight": 0.18},
41
+ {"name": "pacific_850_environment", "level_weights": (1.0, 0.0, 0.0), "ring_degrees": (12.0, 32.0), "pressure_fraction": 0.18, "weight": 0.18},
42
+ {"name": "pacific_deep_environment", "level_weights": (0.269, 0.500, 0.231), "ring_degrees": (10.0, 30.0), "pressure_fraction": 0.22, "weight": 0.20},
43
+ {"name": "broad_500_trough", "level_weights": (0.0, 1.0, 0.0), "ring_degrees": (12.0, 34.0), "pressure_fraction": 0.16, "weight": 0.11},
44
+ {"name": "outer_200_jet", "level_weights": (0.0, 0.0, 1.0), "ring_degrees": (15.0, 35.0), "pressure_fraction": 0.08, "weight": 0.09},
45
+ )
46
+
47
+
48
+ def _clip_delta(values: np.ndarray) -> np.ndarray:
49
+ values = np.nan_to_num(np.asarray(values, dtype="float32"), copy=True)
50
+ for channel in range(values.shape[0]):
51
+ scale = float(np.nanpercentile(np.abs(values[channel]), 98.0))
52
+ if scale > 0.0 and math.isfinite(scale):
53
+ values[channel] = np.clip(values[channel], -2.0 * scale, 2.0 * scale)
54
+ return values
55
+
56
+
57
+ def build_pacific_route(
58
+ fields: np.ndarray,
59
+ pressure: np.ndarray,
60
+ latitude: np.ndarray,
61
+ longitude: np.ndarray,
62
+ base_latitude: float,
63
+ base_longitude: float,
64
+ history_motion_km_per_6h: tuple[float, float] | None = None,
65
+ ) -> tuple[np.ndarray, np.ndarray, dict]:
66
+ """Build a broad-domain causal route from analysis-only inputs."""
67
+
68
+ members, weights, metadata = build_route(
69
+ fields,
70
+ latitude,
71
+ longitude,
72
+ base_latitude,
73
+ base_longitude,
74
+ pressure,
75
+ available=(1.0, 1.0),
76
+ route_variants=PACIFIC_ROUTE_VARIANTS,
77
+ curvature_variants=CURVATURE_VARIANTS,
78
+ snapshot_weights=SNAPSHOT_WEIGHTS,
79
+ tendency_scales=TENDENCY_SCALES,
80
+ history_motion_km_per_6h=history_motion_km_per_6h,
81
+ )
82
+ metadata = {
83
+ **metadata,
84
+ "version": VERSION,
85
+ "domain": {
86
+ "longitude_east": list(PACIFIC_LON_RANGE),
87
+ "latitude_north": list(PACIFIC_LAT_RANGE),
88
+ "steering_ring_max_degrees": 35.0,
89
+ },
90
+ "large_system_policy": "The route samples the complete analysis grid through broad 850/500/200-hPa and SLP-gradient views; nearby lows are represented by the same causal pressure field.",
91
+ }
92
+ return members, weights, metadata
93
+
94
+
95
+ def forecast_pacific_state(
96
+ fields: np.ndarray,
97
+ pressure: np.ndarray,
98
+ lead_hours: Sequence[int] = LEAD_HOURS,
99
+ ) -> tuple[np.ndarray, np.ndarray, dict]:
100
+ """Return causal whole-domain pressure and multilevel states.
101
+
102
+ The forecast state is a bounded extrapolation of analysis tendency. It is
103
+ deliberately not a claimed NWP forecast: no future weather product is
104
+ read, and no official forecast field is substituted.
105
+ """
106
+
107
+ fields = np.asarray(fields, dtype="float32")
108
+ pressure = np.asarray(pressure, dtype="float32")
109
+ if fields.shape[0] != 3 or pressure.shape[0] != 3:
110
+ raise ValueError(f"expected three causal snapshots, got {fields.shape} and {pressure.shape}")
111
+ recent_fields = fields[0] - fields[1]
112
+ older_fields = fields[1] - fields[2]
113
+ field_tendency = _clip_delta(0.6 * recent_fields + 0.4 * older_fields)
114
+ recent_pressure = pressure[0] - pressure[1]
115
+ older_pressure = pressure[1] - pressure[2]
116
+ pressure_tendency = _clip_delta(0.6 * recent_pressure[None, ...] + 0.4 * older_pressure[None, ...])[0]
117
+ scale = float(np.dot(np.asarray(TENDENCY_SCALES), np.asarray((0.15, 0.45, 0.40))))
118
+ state_fields = []
119
+ state_pressure = []
120
+ for hours in lead_hours:
121
+ progress = min(2.0, 0.5 * max(float(hours), 0.0) / 12.0)
122
+ state_fields.append(fields[0] + scale * progress * field_tendency)
123
+ state_pressure.append(pressure[0] + scale * progress * pressure_tendency)
124
+ return np.stack(state_fields).astype("float32"), np.stack(state_pressure).astype("float32"), {
125
+ "version": VERSION,
126
+ "causal_only": CAUSAL_ONLY,
127
+ "lead_hours": [int(value) for value in lead_hours],
128
+ "analysis_tendency_scale": scale,
129
+ "tendency_method": "0.6 * (current - t-12) + 0.4 * (t-12 - t-24), clipped per channel at the 98th percentile",
130
+ "input_policy": "current, t-12, and t-24 analysis fields only; no positive-lead or official forecast field",
131
+ "domain": {
132
+ "longitude_east": list(PACIFIC_LON_RANGE),
133
+ "latitude_north": list(PACIFIC_LAT_RANGE),
134
+ },
135
+ }
136
+
137
+
138
+ def _distance_degrees(lat_a: float, lon_a: float, lat_b: float, lon_b: float) -> float:
139
+ delta_lon = ((lon_a - lon_b + 180.0) % 360.0) - 180.0
140
+ return math.hypot(lat_a - lat_b, delta_lon * math.cos(math.radians(0.5 * (lat_a + lat_b))))
141
+
142
+
143
+ def detect_pressure_systems(
144
+ pressure: np.ndarray,
145
+ latitude: np.ndarray,
146
+ longitude: np.ndarray,
147
+ storm_latitude: float,
148
+ storm_longitude: float,
149
+ maximum: int = 8,
150
+ ) -> list[dict]:
151
+ """Find candidate closed lows from an analysis-only SLP field.
152
+
153
+ These are weather-field vortices, not labels imported from a typhoon
154
+ warning center. They are used for diagnostics and route context.
155
+ """
156
+
157
+ try:
158
+ from scipy.ndimage import maximum_filter, minimum_filter
159
+ except ImportError:
160
+ return []
161
+ latitude = np.asarray(latitude, dtype="float32")
162
+ longitude = np.asarray(longitude, dtype="float32")
163
+ pressure = np.asarray(pressure, dtype="float32")
164
+ lat_mask = (latitude >= PACIFIC_LAT_RANGE[0]) & (latitude <= PACIFIC_LAT_RANGE[1])
165
+ lon_mask = (longitude >= PACIFIC_LON_RANGE[0]) & (longitude <= PACIFIC_LON_RANGE[1])
166
+ if not lat_mask.any() or not lon_mask.any():
167
+ return []
168
+ local = pressure[np.ix_(lat_mask, lon_mask)]
169
+ minimum = minimum_filter(local, size=13, mode="nearest")
170
+ surrounding_maximum = maximum_filter(local, size=41, mode="nearest")
171
+ threshold = float(np.nanpercentile(local, 18.0))
172
+ candidates = np.argwhere((local <= minimum + 0.05) & (local <= threshold) & ((surrounding_maximum - local) >= 2.0))
173
+ rows = []
174
+ lat_values = latitude[lat_mask]
175
+ lon_values = longitude[lon_mask]
176
+ for row, column in candidates:
177
+ lat = float(lat_values[row])
178
+ lon = float(lon_values[column])
179
+ if _distance_degrees(lat, lon, storm_latitude, storm_longitude) < 8.0:
180
+ continue
181
+ rows.append({
182
+ "latitude": round(lat, 3),
183
+ "longitude": round(lon, 3),
184
+ "pressure_hpa": round(float(local[row, column]), 2),
185
+ "local_low_prominence_hpa": round(float(surrounding_maximum[row, column] - local[row, column]), 2),
186
+ "kind": "analysis low/vortex candidate",
187
+ })
188
+ rows.sort(key=lambda item: (-item["local_low_prominence_hpa"], item["pressure_hpa"]))
189
+ selected = []
190
+ for row in rows:
191
+ if any(_distance_degrees(row["latitude"], row["longitude"], item["latitude"], item["longitude"]) < 4.0 for item in selected):
192
+ continue
193
+ selected.append(row)
194
+ if len(selected) >= maximum:
195
+ break
196
+ return selected
197
+
198
+
199
+ __all__ = [
200
+ "VERSION",
201
+ "CAUSAL_ONLY",
202
+ "PACIFIC_LON_RANGE",
203
+ "PACIFIC_LAT_RANGE",
204
+ "LEAD_HOURS",
205
+ "PACIFIC_ROUTE_VARIANTS",
206
+ "build_pacific_route",
207
+ "forecast_pacific_state",
208
+ "detect_pressure_systems",
209
+ ]
trackformer_1_1_temporal.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Inference-only temporal branch used by Trackformer1.1.
2
+
3
+ The training implementation is intentionally not part of the public model
4
+ package. This module contains only the architecture needed to load the
5
+ frozen temporal expert checkpoints.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import torch
11
+ import torch.nn as nn
12
+
13
+ from trackformer_1_1_intensity import StructureSpatialExpert
14
+
15
+
16
+ class TemporalStructureSpatial(StructureSpatialExpert):
17
+ """Spatial expert augmented with same-storm t-12/t-24 analysis fields."""
18
+
19
+ def __init__(self, width: int, layers: int, heads: int):
20
+ super().__init__(width, layers, heads, structure_residual=True)
21
+ self.history_encoder = nn.Sequential(
22
+ nn.Conv2d(10, 64, 3, padding=1),
23
+ nn.GroupNorm(8, 64),
24
+ nn.SiLU(),
25
+ nn.Conv2d(64, width, 3, stride=2, padding=1),
26
+ nn.GroupNorm(8, width),
27
+ nn.SiLU(),
28
+ )
29
+ self.history_pool = nn.AvgPool2d(kernel_size=2, stride=2)
30
+ self.history_norm = nn.LayerNorm(width)
31
+ self.history_pos = nn.Parameter(torch.randn(1, 16, width) * 0.02)
32
+ self.history_out = nn.Conv2d(width, width, 1)
33
+ nn.init.zeros_(self.history_out.weight)
34
+ nn.init.zeros_(self.history_out.bias)
35
+
36
+ def forward(
37
+ self,
38
+ track: torch.Tensor,
39
+ field: torch.Tensor,
40
+ current: torch.Tensor,
41
+ available: torch.Tensor,
42
+ current_structure: torch.Tensor | None = None,
43
+ structure_available: torch.Tensor | None = None,
44
+ history: torch.Tensor | None = None,
45
+ history_available: torch.Tensor | None = None,
46
+ ) -> tuple[torch.Tensor, torch.Tensor]:
47
+ track_tokens = self.track_encoder(
48
+ self.track_proj(track[:, :, self._thermo_cols]) + self.track_time
49
+ )
50
+ field_tokens = self.field_pool(self.field_encoder(field)).flatten(2).transpose(1, 2)
51
+ field_tokens = self.field_norm(field_tokens + self.field_pos)
52
+ if history is not None:
53
+ if history_available is None:
54
+ history_available = history.new_ones((history.shape[0], 2))
55
+ flags = history_available.view(-1, 2, 1, 1).expand(-1, 2, 17, 17)
56
+ history_tokens = self.history_pool(
57
+ self.history_encoder(torch.cat([history, flags], dim=1))
58
+ )
59
+ history_tokens = history_tokens + self.history_pos.permute(0, 2, 1).reshape(
60
+ 1, history_tokens.shape[1], 4, 4
61
+ )
62
+ history_tokens = self.history_norm(history_tokens.flatten(2).transpose(1, 2))
63
+ history_tokens = history_tokens + self.history_out(
64
+ history_tokens.transpose(1, 2).reshape(
65
+ history_tokens.shape[0], history_tokens.shape[2], 4, 4
66
+ )
67
+ ).flatten(2).transpose(1, 2)
68
+ field_tokens = field_tokens + history_tokens
69
+ memory = torch.cat([track_tokens, field_tokens], dim=1)
70
+ query = (self.query + self.lead_time).expand(track.shape[0], -1, -1)
71
+ hidden = self.decoder(query, memory)
72
+ state = self.state(hidden).clone()
73
+ state[:, :, :2] = state[:, :, :2] + (current * available)[:, None, :]
74
+ if current_structure is None or structure_available is None:
75
+ raise ValueError("Trackformer1.1 temporal expert requires current structure tensors")
76
+ state[:, :, 2:] = (
77
+ state[:, :, 2:]
78
+ + current_structure[:, None, :] * structure_available[:, None, :]
79
+ )
80
+ return state, self.log_scale(hidden)
81
+
82
+ @property
83
+ def _thermo_cols(self):
84
+ return (
85
+ [4, 5, 6, 7]
86
+ + list(range(8, 20))
87
+ + list(range(24, 40))
88
+ + [44, 45, 46, 47, 48, 49, 50, 51, 52, 53]
89
+ )
90
+
91
+
92
+ __all__ = ["TemporalStructureSpatial"]
trackformer_v23.py DELETED
@@ -1,229 +0,0 @@
1
- """Standalone TrackFormer v23 architecture — the best model in this project (434.96 km RMS track
2
- error, 10-seed ensemble, WP+EP 2020+ full-20-lead test set). Chain-of-thought (CoT) steering-flow
3
- prediction (v21) plus a temporal history of that steering representation (v23's addition).
4
-
5
- This file has zero notebook/exec tricks: every class below is copied verbatim from the training
6
- scripts that produced the released checkpoints (colab_train_v17.ipynb for TrackFormerV17, the "Base"
7
- that v21/v23 build on; colab_v26_train.py for TrackFormerCoT, v21's chain-of-thought forward pass;
8
- colab_v28_train.py for HistStem/TrackFormerHist, v23's temporal-history addition) -- so this module
9
- IS the architecture the checkpoints were trained with, not a reimplementation from memory. See
10
- run_v23.py for how to load a checkpoint and get a forecast, in either IBTrACS-only or full-steering
11
- mode.
12
- """
13
- import math
14
- import torch
15
- import torch.nn as nn
16
-
17
- # ---- input column layout (54-dim per-6h track/thermo/env feature row) -----------------------
18
- KIN_COLS = [0, 1, 2, 3, 21, 22, 23, 40, 41, 42, 43]
19
- THERMO_COLS = [4, 5, 6, 7] + list(range(8, 20)) + list(range(24, 40)) + [44, 45, 46, 47]
20
- ENV_COLS = [48, 49, 50, 51, 52, 53]
21
- KIN_DIM, THERMO_DIM, ENV_DIM = len(KIN_COLS), len(THERMO_COLS), len(ENV_COLS)
22
-
23
- TARGET_SCALE = torch.tensor([100., 100., 35., 20., 50.] + [50.] * 12)
24
-
25
- # eval-only: this dict form is a leftover of the training scripts' exec-in-a-dict pattern
26
- # (TrackFormerCoT/TrackFormerHist index into it as G["..."], never as a bare global) -- kept as-is
27
- # rather than rewritten, since these classes are pasted in verbatim from the scripts that actually
28
- # produced the checkpoints. STEER_DROP only affects self.training branches, irrelevant at eval.
29
- G = {"KIN_COLS": KIN_COLS, "THERMO_COLS": THERMO_COLS, "ENV_COLS": ENV_COLS, "STEER_DROP": 0.0}
30
- STEER_DROP = 0.0 # bare-name fallback referenced by TrackFormerV17.forward (never actually
31
- # called for v21/v23 -- TrackFormerCoT overrides forward entirely -- kept
32
- # only so the class body is valid to define)
33
- USE_FLOW = 1
34
- USE_HIST = 1
35
- KM6H = 6 * 3600 / 1000.0
36
-
37
- _i, _j = torch.meshgrid(torch.arange(17) - 8, torch.arange(17) - 8, indexing="ij")
38
- _r = torch.hypot(_i.float(), _j.float()) * 2.5
39
- ANN = ((_r >= 3.0) & (_r <= 8.0)).float() # 3-8 deg annulus mask matching the training target
40
-
41
-
42
- def sinusoidal(n, d):
43
- p = torch.arange(n).unsqueeze(1).float()
44
- dv = torch.exp(torch.arange(0, d, 2).float() * (-math.log(10000.0) / d))
45
- e = torch.zeros(n, d); e[:, 0::2] = torch.sin(p * dv); e[:, 1::2] = torch.cos(p * dv)
46
- return e
47
-
48
-
49
- def enc(d, h, ffn, dr, depth):
50
- return nn.TransformerEncoder(nn.TransformerEncoderLayer(d, h, ffn, dr, batch_first=True,
51
- norm_first=True, activation="gelu"), depth)
52
-
53
-
54
- def dec(d, h, ffn, dr, depth):
55
- return nn.TransformerDecoder(nn.TransformerDecoderLayer(d, h, ffn, dr, batch_first=True,
56
- norm_first=True, activation="gelu"), depth)
57
-
58
-
59
- class TrackFormerV17(nn.Module):
60
- """Base architecture: track/thermo/env history encoders + steering-CNN + cross-attention
61
- decoders. v21/v23 build on this but override forward() -- it is never called directly for v23,
62
- kept here only because TrackFormerCoT inherits __init__ from it."""
63
-
64
- def __init__(self, d=256, h=8, ffn=1024, dr=0.15, hist=9, leads=20):
65
- super().__init__()
66
- self.leads = leads
67
- self.kin_proj = nn.Linear(KIN_DIM, d); self.thermo_proj = nn.Linear(THERMO_DIM, d)
68
- self.env_proj = nn.Linear(ENV_DIM, d)
69
- self.register_buffer("kin_time", sinusoidal(hist, d).unsqueeze(0))
70
- self.register_buffer("thermo_time", sinusoidal(hist, d).unsqueeze(0))
71
- self.register_buffer("env_time", sinusoidal(hist, d).unsqueeze(0))
72
- self.kin_enc = enc(d, h, ffn, dr, 3); self.thermo_enc = enc(d, h, ffn, dr, 3)
73
- self.env_enc = enc(d, h, ffn, dr, 2)
74
- self.track_dec = dec(d, h, ffn, dr, 3); self.int_dec = dec(d, h, ffn, dr, 3)
75
- self.track_q = nn.Parameter(torch.randn(1, leads, d) * 0.02)
76
- self.int_q = nn.Parameter(torch.randn(1, leads, d) * 0.02)
77
- self.register_buffer("qpos", sinusoidal(leads, d))
78
- self.adapter = nn.Sequential(nn.Linear(d, d), nn.GELU(), nn.Linear(d, d))
79
- nn.init.zeros_(self.adapter[-1].weight); nn.init.zeros_(self.adapter[-1].bias)
80
- self.alpha = nn.Parameter(torch.zeros(leads)); self.rho = nn.Parameter(torch.ones(leads))
81
- self.gturn = nn.Parameter(torch.zeros(leads))
82
- self.steer_cnn = nn.Sequential(
83
- nn.Conv2d(4, 24, 3, padding=1), nn.GELU(), nn.Dropout2d(0.10),
84
- nn.Conv2d(24, 48, 3, stride=2, padding=1), nn.GELU(), nn.Dropout2d(0.10),
85
- nn.Conv2d(48, d, 3, stride=2, padding=1), nn.GELU())
86
- self.steer_pos = nn.Parameter(torch.zeros(1, 25, d))
87
- self.track_res = nn.Linear(d, 2)
88
- nn.init.zeros_(self.track_res.weight); nn.init.zeros_(self.track_res.bias)
89
- self.int_state = nn.Linear(d, 15); self.int_logscale = nn.Linear(d, 15)
90
-
91
- def forward(self, track, vpair, slp):
92
- b = track.shape[0]
93
- kin = self.kin_enc(self.kin_proj(track[:, :, KIN_COLS]) + self.kin_time)
94
- thermo = self.thermo_enc(self.thermo_proj(track[:, :, THERMO_COLS]) + self.thermo_time)
95
- env = self.env_enc(self.env_proj(track[:, :, ENV_COLS]) + self.env_time)
96
- if self.training and STEER_DROP > 0:
97
- keep = (torch.rand(b, 1, 1, 1, device=slp.device) >= STEER_DROP).float()
98
- slp = slp * keep
99
- st = self.steer_cnn(slp).flatten(2).transpose(1, 2) + self.steer_pos
100
- tq = (self.track_q + self.qpos.unsqueeze(0)).expand(b, -1, -1)
101
- h_track = self.track_dec(tq, torch.cat([kin, env, st], dim=1))
102
- h_track = h_track + self.alpha.view(1, self.leads, 1) * self.adapter(thermo.mean(1).detach()).unsqueeze(1)
103
- v0, vp = vpair[:, :2], vpair[:, 2:]
104
- s0 = v0.norm(dim=1, keepdim=True).clamp(min=1e-3)
105
- phi0 = torch.atan2(v0[:, 1], v0[:, 0])
106
- dphi = phi0 - torch.atan2(vp[:, 1], vp[:, 0])
107
- omega = torch.atan2(torch.sin(dphi), torch.cos(dphi))
108
- phil = phi0.unsqueeze(1) + self.gturn.view(1, self.leads) * omega.unsqueeze(1)
109
- speed = self.rho.view(1, self.leads) * s0
110
- base = torch.stack([speed * torch.cos(phil), speed * torch.sin(phil)], dim=-1) / 100.0
111
- motion = base + self.track_res(h_track)
112
- iq = (self.int_q + self.qpos.unsqueeze(0)).expand(b, -1, -1)
113
- h_int = self.int_dec(iq, torch.cat([thermo, env, kin.detach(), st.detach()], dim=1))
114
- istate = self.int_state(h_int); ilog = self.int_logscale(h_int)
115
- return torch.cat([motion, istate], -1), torch.cat([torch.zeros_like(motion), ilog], -1)
116
-
117
-
118
- class TrackFormerCoT(TrackFormerV17):
119
- """v20's network, with the track derived from a predicted steering flow (v21)."""
120
-
121
- def __init__(self, **kw):
122
- super().__init__(**kw)
123
- d = self.track_q.shape[-1]
124
- self.flow_delta = nn.Linear(d, 2)
125
- nn.init.zeros_(self.flow_delta.weight); nn.init.zeros_(self.flow_delta.bias)
126
- self.A = nn.Parameter(torch.tensor([0.76, 0.91]))
127
-
128
- def forward(self, track, vpair, slp):
129
- b = track.shape[0]
130
- KIN_COLS, THERMO_COLS, ENV_COLS = G["KIN_COLS"], G["THERMO_COLS"], G["ENV_COLS"]
131
- STEER_DROP = G["STEER_DROP"]
132
- kin = self.kin_enc(self.kin_proj(track[:, :, KIN_COLS]) + self.kin_time)
133
- thermo = self.thermo_enc(self.thermo_proj(track[:, :, THERMO_COLS]) + self.thermo_time)
134
- env = self.env_enc(self.env_proj(track[:, :, ENV_COLS]) + self.env_time)
135
- if self.training and STEER_DROP > 0:
136
- keep = (torch.rand(b, 1, 1, 1, device=slp.device) >= STEER_DROP).float()
137
- slp = slp * keep
138
- st = self.steer_cnn(slp).flatten(2).transpose(1, 2) + self.steer_pos
139
- tq = (self.track_q + self.qpos.unsqueeze(0)).expand(b, -1, -1)
140
- h_track = self.track_dec(tq, torch.cat([kin, env, st], dim=1))
141
- h_track = h_track + self.alpha.view(1, self.leads, 1) * self.adapter(thermo.mean(1).detach()).unsqueeze(1)
142
-
143
- w = ANN / ANN.sum()
144
- sc = torch.as_tensor(DSC, device=slp.device, dtype=slp.dtype)
145
- flow_now = (slp[:, 2:4] * w).sum((-2, -1)) * sc
146
- fd = self.flow_delta(h_track)
147
- flow_pred = flow_now.unsqueeze(1) + fd
148
-
149
- v0, vp = vpair[:, :2], vpair[:, 2:]
150
- s0 = v0.norm(dim=1, keepdim=True).clamp(min=1e-3)
151
- phi0 = torch.atan2(v0[:, 1], v0[:, 0])
152
- dphi = phi0 - torch.atan2(vp[:, 1], vp[:, 0])
153
- omega = torch.atan2(torch.sin(dphi), torch.cos(dphi))
154
- phil = phi0.unsqueeze(1) + self.gturn.view(1, self.leads) * omega.unsqueeze(1)
155
- speed = self.rho.view(1, self.leads) * s0
156
- base = torch.stack([speed * torch.cos(phil), speed * torch.sin(phil)], dim=-1) / 100.0
157
- motion = base + self.track_res(h_track)
158
- if USE_FLOW:
159
- motion = motion + (self.A.view(1, 1, 2) * fd) * KM6H / 100.0
160
- iq = (self.int_q + self.qpos.unsqueeze(0)).expand(b, -1, -1)
161
- h_int = self.int_dec(iq, torch.cat([thermo, env, kin.detach(), st.detach()], dim=1))
162
- istate = self.int_state(h_int); ilog = self.int_logscale(h_int)
163
- return (torch.cat([motion, istate], -1),
164
- torch.cat([torch.zeros_like(motion), ilog], -1), flow_pred)
165
-
166
-
167
- class HistStem(nn.Module):
168
- """v17's steering stem, plus a zero-initialised residual carrying t-12h and t-24h (v23)."""
169
-
170
- def __init__(self, base, ch):
171
- super().__init__()
172
- self.base = base
173
- self.stem = nn.Sequential(
174
- nn.Conv2d(10, 24, 3, padding=1), nn.GELU(), nn.Dropout2d(0.10),
175
- nn.Conv2d(24, 48, 3, stride=2, padding=1), nn.GELU(), nn.Dropout2d(0.10),
176
- nn.Conv2d(48, ch, 3, stride=2, padding=1), nn.GELU())
177
- self.out = nn.Conv2d(ch, ch, 1)
178
- nn.init.zeros_(self.out.weight); nn.init.zeros_(self.out.bias)
179
- self.ctx = None
180
-
181
- def forward(self, slp):
182
- st = self.base(slp)
183
- if USE_HIST and self.ctx is not None:
184
- hist, have = self.ctx
185
- hv = have.view(-1, 2, 1, 1).expand(-1, 2, hist.shape[-2], hist.shape[-1])
186
- st = st + self.out(self.stem(torch.cat([hist, hv], 1)))
187
- return st
188
-
189
-
190
- class TrackFormerHist(TrackFormerCoT):
191
- """v23: v21 + a temporal history of the steering representation (t-12h, t-24h). This is the
192
- class the released v23 checkpoints instantiate."""
193
-
194
- def __init__(self, **kw):
195
- super().__init__(**kw)
196
- self.steer_cnn = HistStem(self.steer_cnn, self.steer_pos.shape[-1])
197
-
198
- def forward(self, tr, vp, slp, hist=None, have=None):
199
- sd = G["STEER_DROP"]
200
- drop = self.training and sd > 0 and hist is not None
201
- if drop:
202
- keep = (torch.rand(tr.shape[0], 1, 1, 1, device=slp.device) >= sd).float()
203
- slp = slp * keep
204
- hist = hist * keep
205
- have = have * keep.view(-1, 1)
206
- G["STEER_DROP"] = 0.0
207
- self.steer_cnn.ctx = (hist, have) if hist is not None else None
208
- try:
209
- return super().forward(tr, vp, slp)
210
- finally:
211
- self.steer_cnn.ctx = None
212
- G["STEER_DROP"] = sd
213
-
214
-
215
- # ---- loaded at import time from the small companion norm-stats file --------------------------
216
- import os as _os
217
- import numpy as _np
218
-
219
- _stats = _np.load(_os.path.join(_os.path.dirname(__file__), "v23_norm_stats.npz"))
220
- TMEAN = _stats["tmean"] # (54,) float32 -- per-column track/thermo/env feature mean
221
- TSTD = _stats["tstd"] # (54,) float32 -- per-column std
222
- DSC = _stats["dsc"] # (2,) float32 -- deep-layer-mean steering u/v de-normalization scale
223
- TARGET_SCALE = torch.from_numpy(_stats["target_scale"]) # (17,) -- motion(2)+intensity(15) scale
224
-
225
-
226
- def build_v23():
227
- """Returns an uninitialized TrackFormerHist -- load_state_dict a v23_seed*.pt checkpoint,
228
- call .eval()."""
229
- return TrackFormerHist()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
v23_seed0.pt DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:7d849efd9c16f14f1a13f4eea9ba2395d5f3d880f30a77a3e6a12b512a35f4d4
3
- size 52625227
 
 
 
 
v23_seed3.pt DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:bf109a94ca734e18b442ce34ea4858921607d768e63f0b7eaa6508163c38fdd5
3
- size 52625227
 
 
 
 
v23_seed4.pt DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:e170d8368e8f7e712015efc728b42f2202bd5d902aa3397acd9bc0f66c8dcb5f
3
- size 52625227
 
 
 
 
v23_seed5.pt DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:3f834873a59350285b19ca197a2cbcea02db1e3f851fe614b9d0fcd0f5bb9018
3
- size 52625227
 
 
 
 
v23_seed6.pt DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:3beecc4f8ecd08e3973c5ac2ca5019982b7fc3a99f877acb4c5be6a8d93fc7b8
3
- size 52625227
 
 
 
 
v23_seed7.pt DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:cb4fca855e66ec026d3281d1c445d4570348eb18d52a51535383226294fecfc0
3
- size 52625227
 
 
 
 
v23_seed8.pt DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:d7b25707064415dde3286aee34a87d3930374009a2da076b2d9ad914d4c0e08e
3
- size 52625227
 
 
 
 
v23_seed9.pt DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:90ed26ece67621226779dfc606b9ddfb602200637d292142900f8ab1fa0ee140
3
- size 52625227