euler314 commited on
Commit
dfe8a5e
·
verified ·
1 Parent(s): 02a5ede

Add trained checkpoint and inference artifacts

Browse files
Files changed (5) hide show
  1. best.pt +3 -0
  2. forecast.json +74 -0
  3. forecast_world_map.html +61 -0
  4. requirements.txt +5 -0
  5. run_inference.py +94 -0
best.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ea6e045bea4e354c36b7fc0ccb72de99efa5634ad813de96e545aed4d7c2840e
3
+ size 1436940
forecast.json ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
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
run_inference.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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))