euler314 commited on
Commit
3465ef2
·
verified ·
1 Parent(s): 832d082

Upload trackformer_v23.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. trackformer_v23.py +229 -0
trackformer_v23.py ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()