Add trained checkpoint and inference artifacts
Browse files- best.pt +3 -0
- forecast.json +74 -0
- forecast_world_map.html +61 -0
- requirements.txt +5 -0
- 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: '© 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))
|