tests/test.sh exit code (0 → resolved); the classification below is post-hoc and cannot change it.cd /app && ls -la && echo "---CONFIG---" && cat task_config.json && echo "---SCHEMA---" && cat feature_schema.json
total 21
drwxrws--- 3 h2tagent h2tagent 4096 Jun 19 21:15 .
drwxr-xr-x 26 root root 4096 Jun 19 21:26 ..
-rw-rw---- 1 h2tagent h2tagent 2093 Jun 19 21:13 candidate_model.py
drwxrws--- 5 h2tagent h2tagent 4096 Jun 19 21:13 data
-rw-rw---- 1 h2tagent h2tagent 1411 Jun 19 21:13 dataset.py
-rw-rw---- 1 h2tagent h2tagent 723 Jun 19 21:13 feature_schema.json
-rw-rw---- 1 h2tagent h2tagent 547 Jun 19 21:13 task_config.json
-rw-rw---- 1 h2tagent h2tagent 2900 Jun 19 21:13 train.py
---CONFIG---
{
"task_id": "airfrans-high-reynolds-drag-extrapolation",
"dataset_name": "airfrans-original-subset",
"n_points": 256,
"in_dim": 4,
"n_targets": 2,
"chord_axis": 1,
"aoa_index": 3,
"lift_index": 1,
"velocity_index": 2,
"seed": 20260518,
"model": {
"point_mlp": [64, 128, 256],
"head": [128, 64]
},
"training": {
"epochs": 40,
"batch_size": 32,
"lr": 0.001,
"weight_decay": 0.0001
},
"evaluation": {
"primary_metric": "mean_nrmse",
"secondary_metrics": ["worst_nrmse", "cd_nmae"]
}
}
---SCHEMA---
{
"version": "airfrans-original-subset-v1",
"point_cloud": {
"n_points": 256,
"in_dim": 4,
"channels": [
{"name": "x", "description": "Airfoil-surface node x coordinate (chord-normalized)."},
{"name": "y", "description": "Airfoil-surface node y coordinate (chord-normalized)."},
{"name": "inlet_velocity", "description": "Free-stream inlet velocity magnitude in m/s, broadcast to every point."},
{"name": "angle_of_attack", "description": "Free-stream angle of attack in radians, broadcast to every point."}
]
},
"targets": [
{"name": "C_D", "description": "Drag coefficient of the airfoil."},
{"name": "C_L", "description": "Lift coefficient of the airfoil."}
]
}cd /app && echo "===MODEL===" && cat candidate_model.py && echo "===DATASET===" && cat dataset.py && echo "===TRAIN===" && cat train.py
===MODEL===
"""PointNet regressor for the airfoil force-coefficient task.
Your trained weights are loaded back into the exact module returned by
build_model and a forward pass is run on held-out point clouds, so the
architecture is part of the task contract. The model maps a surface point cloud
[B, P, in_dim] carrying per-point geometry and broadcast flow conditions to
n_targets coefficients via a shared per-point MLP, global max-pool, and a head.
Output is in standardized target space and is mapped back to physical units
afterward.
"""
from __future__ import annotations
from typing import Mapping
import torch
import torch.nn as nn
class SharedMLP(nn.Module):
def __init__(self, channels: list[int]) -> None:
super().__init__()
layers: list[nn.Module] = []
for i in range(len(channels) - 1):
layers += [nn.Conv1d(channels[i], channels[i + 1], 1), nn.BatchNorm1d(channels[i + 1]), nn.GELU()]
self.net = nn.Sequential(*layers)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.net(x)
class PointNetRegressor(nn.Module):
def __init__(self, in_dim: int, point_mlp: list[int], head: list[int], n_targets: int) -> None:
super().__init__()
self.n_targets = n_targets
self.encoder = SharedMLP([in_dim, *point_mlp])
head_layers: list[nn.Module] = []
dims = [point_mlp[-1], *head]
for i in range(len(dims) - 1):
head_layers += [nn.Linear(dims[i], dims[i + 1]), nn.GELU()]
head_layers += [nn.Linear(dims[-1], n_targets)]
self.head = nn.Sequential(*head_layers)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = x.transpose(1, 2)
feat = self.encoder(x)
pooled = feat.max(dim=2).values
return self.head(pooled)
def build_model(config: Mapping) -> PointNetRegressor:
m = config["model"]
return PointNetRegressor(
in_dim=int(config["in_dim"]),
point_mlp=[int(c) for c in m["point_mlp"]],
head=[int(c) for c in m["head"]],
n_targets=int(config["n_targets"]),
)
===DATASET===
"""Point-cloud dataset utilities for the airfoil force-coefficient task.
On-disk format per split: a single split.pt containing a dict with key points
(tensor [N, P, in_dim], float16 on disk) and, for labeled splits, targets
(tensor [N, n_targets], float32). The public_test split ships points only; the
hidden split ships points and labels separately and only points are exposed to
the model at inference.
"""
from __future__ import annotations
from pathlib import Path
import torch
def load_split(split_dir: str | Path) -> tuple[torch.Tensor, torch.Tensor | None]:
payload = torch.load(Path(split_dir) / "split.pt", map_location="cpu", weights_only=True)
targets = payload["targets"].float() if "targets" in payload else None
return payload["points"].float(), targets
def coord_standardizer(points: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
flat = points.reshape(-1, points.shape[-1])
mean = flat.mean(dim=0)
std = flat.std(dim=0).clamp_min(1e-8)
return mean, std
def target_standardizer(targets: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
mean = targets.mean(dim=0)
std = targets.std(dim=0).clamp_min(1e-8)
return mean, std
def iter_indices(n: int, batch_size: int, shuffle: bool = True):
idx = torch.randperm(n) if shuffle else torch.arange(n)
for start in range(0, n, batch_size):
yield idx[start : start + batch_size]
===TRAIN===
"""Weak baseline trainer: single global-pool PointNet, plain MSE, short budget.
Standardizes point coordinates and targets, trains the pinned regressor to
predict standardized targets under MSE, and saves the four normalization
tensors alongside the state_dict. No augmentation, no per-target weighting, no
early stopping. Candidates are expected to do better on the held-out regime --
input jitter/rotation augmentation, deeper pooling, per-target loss bal
…[truncated]cd /app && python -c "
import torch
for split in ['train','val','public_test']:
p = torch.load(f'data/{split}/split.pt', weights_only=True)
print('===', split, '===')
print('keys:', list(p.keys()))
print('points shape/dtype:', p['points'].shape, p['points'].dtype)
if 'targets' in p:
t = p['targets']
print('targets shape/dtype:', t.shape, t.dtype)
"
=== train === keys: ['points', 'targets'] points shape/dtype: torch.Size([630, 256, 4]) torch.float32 targets shape/dtype: torch.Size([630, 2]) torch.float32 === val === keys: ['points', 'targets'] points shape/dtype: torch.Size([80, 256, 4]) torch.float32 targets shape/dtype: torch.Size([80, 2]) torch.float32 === public_test === keys: ['points'] points shape/dtype: torch.Size([100, 256, 4]) torch.float32
cd /app && python -c "
import torch
def stats(name, pts):
flat = pts.reshape(-1,4)
print(f'--- {name} per-channel (x,y,vel,aoa) ---')
for i,ch in enumerate(['x','y','vel','aoa']):
c = flat[:,i]
print(f' {ch}: min={c.min():.4f} max={c.max():.4f} mean={c.mean():.4f} std={c.std():.4f}')
# per-case velocity and aoa (they are broadcast, so take point 0)
v = pts[:,0,2]; a = pts[:,0,3]
print(f' per-case vel: min={v.min():.4f} max={v.max():.4f}')
print(f' per-case aoa: min={a.min():.4f} max={a.max():.4f}')
for split in ['train','val','public_test']:
p = torch.load(f'data/{split}/split.pt', weights_only=True)
stats(split, p['points'])
"
--- train per-channel (x,y,vel,aoa) --- x: min=-0.0070 max=1.0000 mean=0.3835 std=0.3956 y: min=-0.0997 max=0.1627 mean=0.0079 std=0.0335 vel: min=31.2830 max=74.9240 mean=52.9737 std=12.7859 aoa: min=-0.0862 max=0.2565 mean=0.0764 std=0.0956 per-case vel: min=31.2830 max=74.9240 per-case aoa: min=-0.0862 max=0.2565 --- val per-channel (x,y,vel,aoa) --- x: min=-0.0033 max=1.0000 mean=0.3846 std=0.3959 y: min=-0.0939 max=0.1366 mean=0.0078 std=0.0323 vel: min=31.4680 max=74.1620 mean=54.9773 std=11.3969 aoa: min=-0.0836 max=0.2512 mean=0.0799 std=0.0947 per-case vel: min=31.4680 max=74.1620 per-case aoa: min=-0.0836 max=0.2512 --- public_test per-channel (x,y,vel,aoa) --- x: min=-0.0055 max=1.0000 mean=0.3761 std=0.3946 y: min=-0.0961 max=0.1528 mean=0.0088 std=0.0346 vel: min=75.0300 max=81.5930 mean=78.2086 std=1.9658 aoa: min=-0.0857 max=0.2582 mean=0.0757 std=0.0961 per-case vel: min=75.0300 max=81.5930 per-case aoa: min=-0.0857 max=0.2582
cd /app && python -c "
import torch
for split in ['train','val']:
p = torch.load(f'data/{split}/split.pt', weights_only=True)
t = p['targets']
print(f'--- {split} targets ---')
for i,ch in enumerate(['C_D','C_L']):
c = t[:,i]
print(f' {ch}: min={c.min():.5f} max={c.max():.5f} mean={c.mean():.5f} std={c.std():.5f}')
# correlation with velocity and aoa
v = p['points'][:,0,2]; a = p['points'][:,0,3]
import numpy as np
V=v.numpy(); A=a.numpy(); CD=t[:,0].numpy(); CL=t[:,1].numpy()
print(' corr(vel,CD)=%.3f corr(aoa,CD)=%.3f'%(np.corrcoef(V,CD)[0,1], np.corrcoef(A,CD)[0,1]))
print(' corr(vel,CL)=%.3f corr(aoa,CL)=%.3f'%(np.corrcoef(V,CL)[0,1], np.corrcoef(A,CL)[0,1]))
"
--- train targets --- C_D: min=0.00727 max=0.04595 mean=0.01276 std=0.00510 C_L: min=-0.53363 max=1.85012 mean=0.65962 std=0.57620 corr(vel,CD)=-0.154 corr(aoa,CD)=0.739 corr(vel,CL)=-0.022 corr(aoa,CL)=0.943 --- val targets --- C_D: min=0.00756 max=0.03459 mean=0.01268 std=0.00533 C_L: min=-0.48027 max=1.80035 mean=0.67952 std=0.59777 corr(vel,CD)=-0.298 corr(aoa,CD)=0.806 corr(vel,CL)=-0.102 corr(aoa,CL)=0.939
cd /app && python -c "
import torch, numpy as np
p = torch.load('data/train/split.pt', weights_only=True)
pts = p['points']; t = p['targets']
# hash geometry (x,y) of each case
geom = pts[:,:,:2].reshape(pts.shape[0], -1).numpy()
import hashlib
hashes = [hashlib.md5(np.round(g,5).tobytes()).hexdigest() for g in geom]
uniq = set(hashes)
print('n cases:', len(hashes), 'unique shapes:', len(uniq))
# count cases per shape
from collections import Counter
c = Counter(hashes)
print('cases per shape distribution:', Counter(c.values()))
"
n cases: 630 unique shapes: 630
cases per shape distribution: Counter({1: 630})cd /app && python -c "
import torch, numpy as np
print('torch', torch.__version__, 'cuda', torch.cuda.is_available())
try:
import sklearn; print('sklearn', sklearn.__version__)
except Exception as e: print('no sklearn', e)
try:
import scipy; print('scipy', scipy.__version__)
except Exception as e: print('no scipy')
" && nproc && free -g 2>/dev/null | head -2torch 2.5.1+cu124 cuda False
no sklearn No module named 'sklearn'
no scipy
16
total used free shared buff/cache available
Mem: 69 0 68 0 0 68cd /app && python -c "
import torch, numpy as np
p = torch.load('data/train/split.pt', weights_only=True)
pts = p['points'].numpy(); t = p['targets'].numpy()
N = pts.shape[0]
aoa = pts[:,0,3]; vel = pts[:,0,2]
CD = t[:,0]; CL = t[:,1]
# geometry descriptors per case
x = pts[:,:,0]; y = pts[:,:,1]
thick = y.max(1) - y.min(1) # max thickness proxy
camber = 0.5*(y.max(1)+y.min(1)) # camber proxy
ymean = y.mean(1)
# standardize helper
def z(a): return (a-a.mean())/a.std()
# Build design matrix for CL: intercept, aoa, aoa^2, vel, thick, camber
def fit(y, cols, names):
X = np.column_stack([np.ones(N)]+cols)
beta, *_ = np.linalg.lstsq(X, y, rcond=None)
pred = X@beta
ss_res = ((y-pred)**2).sum(); ss_tot=((y-y.mean())**2).sum()
print(' R2=%.4f'%(1-ss_res/ss_tot))
for n,b in zip(['int']+names, beta):
print(f' {n}: {b:.5f}')
print('=== C_L fit ===')
fit(CL, [aoa, aoa**2, vel, thick, camber], ['aoa','aoa^2','vel','thick','camber'])
print('=== C_D fit ===')
fit(CD, [aoa, aoa**2, vel, thick, camber], ['aoa','aoa^2','vel','thick','camber'])
print()
print('=== C_D fit with 1/vel and log(vel) ===')
fit(CD, [aoa, aoa**2, vel, 1.0/vel, thick, camber, aoa*vel], ['aoa','aoa^2','vel','1/vel','thick','camber','aoa*vel'])
# residual correlation of CD with vel after removing aoa,aoa^2,thick,camber
X = np.column_stack([np.ones(N), aoa, aoa**2, thick, camber])
beta,*_ = np.linalg.lstsq(X, CD, rcond=None); res = CD - X@beta
print('partial corr(vel, CD|aoa,geom) =', np.corrcoef(vel, res)[0,1])
X2 = np.column_stack([np.ones(N), aoa, aoa**2, thick, camber])
beta2,*_ = np.linalg.lstsq(X2, CL, rcond=None); resL = CL - X2@beta2
print('partial corr(vel, CL|aoa,geom) =', np.corrcoef(vel, resL)[0,1])
"
=== C_L fit ===
R2=0.9805
int: 0.05759
aoa: 6.15948
aoa^2: -2.61576
vel: 0.00026
thick: -0.54856
camber: 10.96557
=== C_D fit ===
R2=0.8109
int: 0.00921
aoa: -0.00532
aoa^2: 0.27235
vel: -0.00006
thick: 0.01281
camber: 0.06019
=== C_D fit with 1/vel and log(vel) ===
R2=0.8139
int: 0.00635
aoa: 0.00618
aoa^2: 0.27375
vel: -0.00002
1/vel: 0.04871
thick: 0.01299
camber: 0.05874
aoa*vel: -0.00022
partial corr(vel, CD|aoa,geom) = -0.3090243383529545
partial corr(vel, CL|aoa,geom) = 0.04087290393507286cd /app && python -c "
import torch, numpy as np
p = torch.load('data/train/split.pt', weights_only=True)
pts = p['points'].numpy(); t = p['targets'].numpy()
N = pts.shape[0]
aoa = pts[:,0,3]; vel = pts[:,0,2]
CD = t[:,0]; CL = t[:,1]
x = pts[:,:,0]; y = pts[:,:,1]
thick = y.max(1)-y.min(1); camber = 0.5*(y.max(1)+y.min(1))
# richer geometry features: y stats at sorted x bins
def geomfeats(pts):
x=pts[:,:,0]; y=pts[:,:,1]
feats=[y.max(1)-y.min(1), 0.5*(y.max(1)+y.min(1)), y.mean(1), y.std(1),
np.abs(y).max(1), x.min(1)]
return np.column_stack(feats)
G = geomfeats(pts)
lv = np.log(vel); lcd = np.log(CD)
# fit log CD with log vel controlling for aoa, geom
X = np.column_stack([np.ones(N), aoa, aoa**2, G, lv])
beta,*_=np.linalg.lstsq(X,lcd,rcond=None)
pred=X@beta; r2=1-((lcd-pred)**2).sum()/((lcd-lcd.mean())**2).sum()
print('log-CD model R2=%.4f, exponent p(log vel)=%.4f'%(r2, beta[-1]))
# linear CD slope controlling for aoa,geom
X2=np.column_stack([np.ones(N),aoa,aoa**2,G,vel])
b2,*_=np.linalg.lstsq(X2,CD,rcond=None)
pred2=X2@b2; r2b=1-((CD-pred2)**2).sum()/((CD-CD.mean())**2).sum()
print('linear-CD model R2=%.4f, dCD/dvel=%.6e'%(r2b, b2[-1]))
# bin CD residual by velocity to see monotonic trend
Xg=np.column_stack([np.ones(N),aoa,aoa**2,G])
bg,*_=np.linalg.lstsq(Xg,CD,rcond=None); res=CD-Xg@bg
order=np.argsort(vel)
for q in range(5):
idx=order[q*N//5:(q+1)*N//5]
print(f' vel bin {q}: vel~{vel[idx].mean():.1f} meanCDresid={res[idx].mean():+.5f}')
"
log-CD model R2=0.8983, exponent p(log vel)=-0.2111 linear-CD model R2=0.8139, dCD/dvel=-5.657849e-05 vel bin 0: vel~35.3 meanCDresid=+0.00107 vel bin 1: vel~43.9 meanCDresid=+0.00031 vel bin 2: vel~53.2 meanCDresid=+0.00014 vel bin 3: vel~62.1 meanCDresid=-0.00059 vel bin 4: vel~70.5 meanCDresid=-0.00093
cd /app && python -c "
import torch, numpy as np
def load(s):
p=torch.load(f'data/{s}/split.pt',weights_only=True)
return p['points'].numpy(), (p['targets'].numpy() if 'targets' in p else None)
def geomfeats(pts):
x=pts[:,:,0]; y=pts[:,:,1]
return np.column_stack([y.max(1)-y.min(1),0.5*(y.max(1)+y.min(1)),y.mean(1),y.std(1),np.abs(y).max(1),x.min(1)])
def expo(pts,t):
N=len(pts); aoa=pts[:,0,3]; vel=pts[:,0,2]; CD=t[:,0]
G=geomfeats(pts); lv=np.log(vel); lcd=np.log(CD)
X=np.column_stack([np.ones(N),aoa,aoa**2,G,lv])
b,*_=np.linalg.lstsq(X,lcd,rcond=None); return b[-1]
ptr,ttr=load('train'); pv,tv=load('val')
print('train exponent:', round(expo(ptr,ttr),4))
print('val exponent:', round(expo(pv,tv),4))
# split train by aoa sign and by velocity half
aoa=ptr[:,0,3]; vel=ptr[:,0,2]
for m,lbl in [(aoa<np.median(aoa),'low-aoa'),(aoa>=np.median(aoa),'high-aoa'),
(vel<np.median(vel),'low-vel'),(vel>=np.median(vel),'high-vel')]:
print(f' {lbl}: exponent={expo(ptr[m],ttr[m]):.4f}')
# combined train+val exponent for final use
pall=np.concatenate([ptr,pv]); tall=np.concatenate([ttr,tv])
print('train+val exponent:', round(expo(pall,tall),4))
"
train exponent: -0.2111 val exponent: -0.1991 low-aoa: exponent=-0.1959 high-aoa: exponent=-0.2069 low-vel: exponent=-0.1432 high-vel: exponent=-0.1716 train+val exponent: -0.212
cd /app && python -c "
import torch, numpy as np
# Build a high-velocity holdout to simulate extrapolation and measure baseline behavior
p=torch.load('data/train/split.pt',weights_only=True)
pts=p['points']; t=p['targets']
vel=pts[:,0,2].numpy()
thr=np.quantile(vel,0.8)
print('holdout threshold vel=%.2f; n_hold=%d n_fit=%d'%(thr,(vel>=thr).sum(),(vel<thr).sum()))
print('holdout vel range: %.1f-%.1f'%(vel[vel>=thr].min(),vel[vel>=thr].max()))
print('This mimics extrapolation but gap is smaller than real test (75-82).')
"
holdout threshold vel=66.60; n_hold=126 n_fit=504 holdout vel range: 66.6-74.9 This mimics extrapolation but gap is smaller than real test (75-82).
cat > /app/exp.py << 'PYEOF'
"""Scratch experiment harness for the airfoil task (not a deliverable)."""
from __future__ import annotations
import json, os, sys, time, math
from pathlib import Path
import numpy as np
import torch
import torch.nn.functional as F
APP=Path('/app'); sys.path.insert(0,str(APP))
from candidate_model import build_model
with (APP/'task_config.json').open() as fh: CFG=json.load(fh)
def load(split):
p=torch.load(APP/'data'/split/'split.pt',map_location='cpu',weights_only=True)
return p['points'].float(), (p['targets'].float() if 'targets' in p else None)
def make_holdout(pts,t,q=0.8,seed=0):
"""Top-q velocity cases as extrapolation holdout."""
vel=pts[:,0,2].numpy(); thr=np.quantile(vel,q)
hold=vel>=thr; fit=~hold
return pts[fit],t[fit],pts[hold],t[hold]
def standardizers(pts,t):
flat=pts.reshape(-1,4)
fm=flat.mean(0); fs=flat.std(0).clamp_min(1e-8)
tm=t.mean(0); ts=t.std(0).clamp_min(1e-8)
return fm,fs,tm,ts
def augment(pts,t,cfg,gen):
"""pts:[B,256,4] raw, t:[B,2] raw (CD,CL). Returns augmented raw copies."""
B=pts.shape[0]
pts=pts.clone(); t=t.clone()
p=cfg.get('p_exp',-0.20)
if cfg.get('vel_aug',False):
prob=cfg.get('vel_aug_prob',0.5)
mask=torch.rand(B,generator=gen)<prob
lo,hi=cfg.get('vel_range',(35.0,90.0))
newv=torch.rand(B,generator=gen)*(hi-lo)+lo
oldv=pts[:,0,2].clone()
sel=mask
ratio=(newv/oldv).clamp_min(1e-6)
cd_scale=ratio**p
# apply only where mask
v_applied=torch.where(sel,newv,oldv)
pts[:,:,2]=v_applied.unsqueeze(1)
t[:,0]=torch.where(sel,t[:,0]*cd_scale,t[:,0])
js=cfg.get('coord_jitter',0.0)
if js>0:
pts[:,:,:2]+=torch.randn(pts[:,:,:2].shape,generator=gen)*js
return pts,t
def evaluate(model,pts,t,fm,fs,tm,ts,ref_std):
model.eval()
with torch.no_grad():
x=(pts-fm.view(1,1,-1))/fs.view(1,1,-1)
out=model(x)
pred=out*ts.view(1,-1)+tm.view(1,-1)
err=pred-t
rmse=torch.sqrt((err**2).mean(0))
mae=err.abs().mean(0)
nrmse=rmse/ref_std
return dict(rmse=rmse.tolist(),mae=mae.tolist(),nrmse=nrmse.tolist(),
mean_nrmse=float(nrmse.mean()),pred=pred)
def train_run(cfg,ptr,ttr,val_sets,ref_std,seed=0,verbose=False):
torch.manual_seed(seed); np.random.seed(seed)
gen=torch.Generator().manual_seed(seed+12345)
fm,fs,tm,ts=cfg['norm']
model=build_model(CFG)
epochs=cfg.get('epochs',200); bs=cfg.get('bs',32)
lr=cfg.get('lr',1e-3); wd=cfg.get('wd',1e-4)
opt=torch.optim.AdamW(model.parameters(),lr=lr,weight_decay=wd)
sched=torch.optim.lr_scheduler.CosineAnnealingLR(opt,T_max=epochs)
N=ptr.shape[0]
loss_type=cfg.get('loss','mse'); w=torch.tensor(cfg.get('tw',[1.0,1.0]))
# SWA
swa=cfg.get('swa',False); swa_start=cfg.get('swa_start',int(epochs*0.75))
swa_model=None; swa_n=0
for ep in range(epochs):
model.train()
idx=torch.randperm(N,generator=gen)
for s in range(0,N,bs):
bi=idx[s:s+bs]
pb,tb=augment(ptr[bi],ttr[bi],cfg,gen)
x=(pb-fm.view(1,1,-1))/fs.view(1,1,-1)
y=(tb-tm.view(1,-1))/ts.view(1,-1)
opt.zero_grad(set_to_none=True)
out=model(x)
if loss_type=='mse':
per=((out-y)**2).mean(0)
elif loss_type=='huber':
per=F.smooth_l1_loss(out,y,reduction='none',beta=1.0).mean(0)
loss=(per*w).sum()/w.sum()
loss.backward(); opt.step()
sched.step()
if swa and ep>=swa_start:
with torch.no_grad():
if swa_model is None:
swa_model={k:v.clone().float() for k,v in model.state_dict().items()}
swa_n=1
else:
swa_n+=1
for k,v in model.state_dict().items():
swa_model[k]+=(v.float()-swa_model[k])/swa
…[truncated]written exp.py
cd /app && cat > run_cmp.py << 'PYEOF'
import torch, numpy as np, time
from exp import load, make_holdout, standardizers, train_run
ptr_all,ttr_all=load('train'); pv,tv=load('val')
ref_std=ttr_all.std(0).clamp_min(1e-8)
# extrapolation holdout from train
pf,tf,ph,th=make_holdout(ptr_all,ttr_all,q=0.8)
fm,fs,tm,ts=standardizers(pf,tf)
norm=(fm,fs,tm,ts)
val_sets={'extrap_hold':(ph,th),'val_interp':(pv,tv)}
base=dict(norm=norm,epochs=150,bs=32,lr=1e-3,wd=1e-4,loss='mse',tw=[1.0,1.0])
def show(tag,res):
for k,r in res.items():
print(f' [{tag}/{k}] CD: rmse={r["rmse"][0]:.5f} nrmse={r["nrmse"][0]:.3f} mae={r["mae"][0]:.5f} | CL: rmse={r["rmse"][1]:.4f} nrmse={r["nrmse"][1]:.3f} | mean_nrmse={r["mean_nrmse"]:.3f}')
t0=time.time()
cfg0=dict(base); cfg0.update(vel_aug=False)
_,r0=train_run(cfg0,pf,tf,val_sets,ref_std,seed=0)
print('NO-AUG baseline (%.1fs)'%(time.time()-t0)); show('noaug',r0)
t0=time.time()
cfg1=dict(base); cfg1.update(vel_aug=True,vel_aug_prob=0.5,vel_range=(35.0,90.0),p_exp=-0.20)
_,r1=train_run(cfg1,pf,tf,val_sets,ref_std,seed=0)
print('VEL-AUG (%.1fs)'%(time.time()-t0)); show('velaug',r1)
PYEOF
python run_cmp.pyNO-AUG baseline (80.0s) [noaug/extrap_hold] CD: rmse=0.00045 nrmse=0.089 mae=0.00031 | CL: rmse=0.0417 nrmse=0.072 | mean_nrmse=0.080 [noaug/val_interp] CD: rmse=0.00073 nrmse=0.144 mae=0.00034 | CL: rmse=0.0274 nrmse=0.048 | mean_nrmse=0.096 VEL-AUG (42.9s) [velaug/extrap_hold] CD: rmse=0.00048 nrmse=0.094 mae=0.00027 | CL: rmse=0.0256 nrmse=0.044 | mean_nrmse=0.069 [velaug/val_interp] CD: rmse=0.00093 nrmse=0.183 mae=0.00032 | CL: rmse=0.0223 nrmse=0.039 | mean_nrmse=0.111
cd /app && cat > run_cmp2.py << 'PYEOF'
import torch, numpy as np, time
from exp import load, standardizers, train_run
ptr_all,ttr_all=load('train'); pv,tv=load('val')
ref_std=ttr_all.std(0).clamp_min(1e-8)
vel=ptr_all[:,0,2].numpy()
# HARD holdout: fit on vel< q45, hold out vel> q80 (big gap, mimics extrapolation)
q_fit=np.quantile(vel,0.55); q_hold=np.quantile(vel,0.80)
pf,tf=ptr_all[vel<q_fit],ttr_all[vel<q_fit]
ph,th=ptr_all[vel>=q_hold],ttr_all[vel>=q_hold]
print('fit vel<%.1f (n=%d); hold vel>=%.1f (n=%d, range %.1f-%.1f)'%(
q_fit,len(pf),q_hold,len(ph),vel[vel>=q_hold].min(),vel[vel>=q_hold].max()))
fm,fs,tm,ts=standardizers(pf,tf); norm=(fm,fs,tm,ts)
vs={'hard_hold':(ph,th)}
base=dict(norm=norm,epochs=150,bs=32,lr=1e-3,wd=1e-4,loss='mse',tw=[1.0,1.0])
def show(tag,res):
r=res['hard_hold']
print(f' [{tag}] CD: rmse={r["rmse"][0]:.5f} nrmse={r["nrmse"][0]:.3f} mae={r["mae"][0]:.5f} bias={float((r["pred"][:,0]-th[:,0]).mean()):+.5f} | CL: nrmse={r["nrmse"][1]:.3f} | mean_nrmse={r["mean_nrmse"]:.3f}')
for name,cfg_up in [
('noaug',dict(vel_aug=False)),
('aug_p0.5',dict(vel_aug=True,vel_aug_prob=0.5,vel_range=(30.0,90.0),p_exp=-0.20)),
('aug_p0.7',dict(vel_aug=True,vel_aug_prob=0.7,vel_range=(30.0,90.0),p_exp=-0.20)),
('aug_p0.5_noCDscale',dict(vel_aug=True,vel_aug_prob=0.5,vel_range=(30.0,90.0),p_exp=0.0)),
]:
cfg=dict(base); cfg.update(cfg_up)
t0=time.time(); _,r=train_run(cfg,pf,tf,vs,ref_std,seed=0)
print('%s (%.1fs)'%(name,time.time()-t0)); show(name,r)
PYEOF
python run_cmp2.pyfit vel<55.7 (n=346); hold vel>=66.6 (n=126, range 66.6-74.9) noaug (55.3s) [noaug] CD: rmse=0.00131 nrmse=0.256 mae=0.00105 bias=+0.00040 | CL: nrmse=0.357 | mean_nrmse=0.307 aug_p0.5 (29.1s) [aug_p0.5] CD: rmse=0.00067 nrmse=0.131 mae=0.00038 bias=-0.00004 | CL: nrmse=0.071 | mean_nrmse=0.101 aug_p0.7 (29.3s) [aug_p0.7] CD: rmse=0.00075 nrmse=0.147 mae=0.00037 bias=+0.00005 | CL: nrmse=0.058 | mean_nrmse=0.102 aug_p0.5_noCDscale (32.3s) [aug_p0.5_noCDscale] CD: rmse=0.00158 nrmse=0.310 mae=0.00124 bias=+0.00124 | CL: nrmse=0.062 | mean_nrmse=0.186
cd /app && cat > run_tune.py << 'PYEOF'
import torch, numpy as np, time, itertools
from exp import load, standardizers, train_run
ptr_all,ttr_all=load('train'); pv,tv=load('val')
ref_std=ttr_all.std(0).clamp_min(1e-8)
vel=ptr_all[:,0,2].numpy()
q_fit=np.quantile(vel,0.55); q_hold=np.quantile(vel,0.80)
pf,tf=ptr_all[vel<q_fit],ttr_all[vel<q_fit]
ph,th=ptr_all[vel>=q_hold],ttr_all[vel>=q_hold]
fm,fs,tm,ts=standardizers(pf,tf); norm=(fm,fs,tm,ts)
vs={'h':(ph,th)}
base=dict(norm=norm,epochs=150,bs=32,lr=1e-3,wd=1e-4,loss='mse',tw=[1.0,1.0],
vel_aug=True,vel_aug_prob=0.6,vel_range=(30.0,90.0),p_exp=-0.20)
def run(cfg_up,seeds=(0,1)):
accs=[]
for s in seeds:
cfg=dict(base); cfg.update(cfg_up)
_,r=train_run(cfg,pf,tf,vs,ref_std,seed=s)
rr=r['h']; accs.append([rr['nrmse'][0],rr['nrmse'][1],rr['mean_nrmse']])
a=np.array(accs).mean(0)
return a
print('sweep p_exp:')
for p in [-0.15,-0.20,-0.25]:
a=run(dict(p_exp=p)); print(' p=%+.2f CD_nrmse=%.3f CL_nrmse=%.3f mean=%.3f'%(p,a[0],a[1],a[2]))
print('sweep loss/tw:')
for up,lbl in [(dict(),'mse tw11'),(dict(loss='huber'),'huber tw11'),
(dict(tw=[2.0,1.0]),'mse tw21'),(dict(tw=[3.0,1.0]),'mse tw31')]:
a=run(up); print(' %-12s CD_nrmse=%.3f CL_nrmse=%.3f mean=%.3f'%(lbl,a[0],a[1],a[2]))
print('sweep jitter/prob:')
for up,lbl in [(dict(coord_jitter=0.002),'jit.002'),(dict(coord_jitter=0.005),'jit.005'),
(dict(vel_aug_prob=0.5),'prob.5'),(dict(vel_aug_prob=0.75),'prob.75')]:
a=run(up); print(' %-10s CD_nrmse=%.3f CL_nrmse=%.3f mean=%.3f'%(lbl,a[0],a[1],a[2]))
PYEOF
python run_tune.py[stderr] Shell command timed out. Tool interaction exceeded 300 seconds.
restart: true
<system>tool has been restarted.</system>
cd /app && export OMP_NUM_THREADS=8 && cat > run_tune.py << 'PYEOF'
import torch, numpy as np, sys
torch.set_num_threads(8)
from exp import load, standardizers, train_run
ptr_all,ttr_all=load('train')
ref_std=ttr_all.std(0).clamp_min(1e-8)
vel=ptr_all[:,0,2].numpy()
q_fit=np.quantile(vel,0.55); q_hold=np.quantile(vel,0.80)
pf,tf=ptr_all[vel<q_fit],ttr_all[vel<q_fit]
ph,th=ptr_all[vel>=q_hold],ttr_all[vel>=q_hold]
fm,fs,tm,ts=standardizers(pf,tf); norm=(fm,fs,tm,ts)
vs={'h':(ph,th)}
base=dict(norm=norm,epochs=120,bs=32,lr=1e-3,wd=1e-4,loss='mse',tw=[1.0,1.0],
vel_aug=True,vel_aug_prob=0.6,vel_range=(30.0,90.0),p_exp=-0.20)
def run(cfg_up,seeds=(0,1)):
accs=[]
for s in seeds:
cfg=dict(base); cfg.update(cfg_up)
_,r=train_run(cfg,pf,tf,vs,ref_std,seed=s)
rr=r['h']; accs.append([rr['nrmse'][0],rr['nrmse'][1],rr['mean_nrmse']])
return np.array(accs).mean(0)
import json
results={}
print('sweep p_exp:',flush=True)
for p in [-0.15,-0.20,-0.25]:
a=run(dict(p_exp=p)); print(' p=%+.2f CD=%.3f CL=%.3f mean=%.3f'%(p,a[0],a[1],a[2]),flush=True)
print('DONE',flush=True)
PYEOF
nohup python run_tune.py > tune1.log 2>&1 &
echo "started PID $!"started PID 472
cd /app && sleep 170 && cat tune1.log
sweep p_exp: p=-0.15 CD=0.158 CL=0.063 mean=0.111 p=-0.20 CD=0.133 CL=0.063 mean=0.098
cd /app && sleep 45 && cat tune1.log
sweep p_exp: p=-0.15 CD=0.158 CL=0.063 mean=0.111 p=-0.20 CD=0.133 CL=0.063 mean=0.098 p=-0.25 CD=0.140 CL=0.065 mean=0.103 DONE
cd /app && export OMP_NUM_THREADS=8 && cat > run_tune2.py << 'PYEOF'
import torch, numpy as np
torch.set_num_threads(8)
from exp import load, standardizers, train_run
ptr_all,ttr_all=load('train')
ref_std=ttr_all.std(0).clamp_min(1e-8)
vel=ptr_all[:,0,2].numpy()
q_fit=np.quantile(vel,0.55); q_hold=np.quantile(vel,0.80)
pf,tf=ptr_all[vel<q_fit],ttr_all[vel<q_fit]
ph,th=ptr_all[vel>=q_hold],ttr_all[vel>=q_hold]
fm,fs,tm,ts=standardizers(pf,tf); norm=(fm,fs,tm,ts)
vs={'h':(ph,th)}
base=dict(norm=norm,epochs=120,bs=32,lr=1e-3,wd=1e-4,loss='mse',tw=[1.0,1.0],
vel_aug=True,vel_aug_prob=0.6,vel_range=(30.0,90.0),p_exp=-0.20)
def run(cfg_up,seeds=(0,1,2)):
accs=[]
for s in seeds:
cfg=dict(base); cfg.update(cfg_up)
_,r=train_run(cfg,pf,tf,vs,ref_std,seed=s)
rr=r['h']; accs.append([rr['nrmse'][0],rr['nrmse'][1],rr['mean_nrmse']])
return np.array(accs).mean(0)
for up,lbl in [
(dict(),'base mse tw11 p.6'),
(dict(loss='huber'),'huber'),
(dict(tw=[2.0,1.0]),'tw21'),
(dict(coord_jitter=0.002),'jit.002'),
(dict(coord_jitter=0.005),'jit.005'),
(dict(vel_aug_prob=0.5),'prob.5'),
(dict(vel_aug_prob=0.75),'prob.75'),
(dict(coord_jitter=0.002,tw=[2.0,1.0]),'jit.002+tw21'),
]:
a=run(up); print(' %-18s CD=%.3f CL=%.3f mean=%.3f'%(lbl,a[0],a[1],a[2]),flush=True)
print('DONE',flush=True)
PYEOF
nohup python run_tune2.py > tune2.log 2>&1 &
echo "started PID $!"started PID 532
cd /app && sleep 210 && cat tune2.log
base mse tw11 p.6 CD=0.123 CL=0.062 mean=0.092 huber CD=0.099 CL=0.054 mean=0.076
cd /app && sleep 200 && cat tune2.log
base mse tw11 p.6 CD=0.123 CL=0.062 mean=0.092 huber CD=0.099 CL=0.054 mean=0.076 tw21 CD=0.135 CL=0.069 mean=0.102 jit.002 CD=0.293 CL=0.070 mean=0.182
cd /app && sleep 170 && cat tune2.log
base mse tw11 p.6 CD=0.123 CL=0.062 mean=0.092 huber CD=0.099 CL=0.054 mean=0.076 tw21 CD=0.135 CL=0.069 mean=0.102 jit.002 CD=0.293 CL=0.070 mean=0.182 jit.005 CD=0.423 CL=0.086 mean=0.255 prob.5 CD=0.127 CL=0.065 mean=0.096
cd /app && sleep 80 && cat tune2.log | tail -4
jit.002 CD=0.293 CL=0.070 mean=0.182 jit.005 CD=0.423 CL=0.086 mean=0.255 prob.5 CD=0.127 CL=0.065 mean=0.096 prob.75 CD=0.123 CL=0.058 mean=0.091
cd /app && export OMP_NUM_THREADS=8 && cat > run_tune3.py << 'PYEOF'
import torch, numpy as np
torch.set_num_threads(8)
from exp import load, standardizers, train_run
ptr_all,ttr_all=load('train')
ref_std=ttr_all.std(0).clamp_min(1e-8)
vel=ptr_all[:,0,2].numpy()
q_fit=np.quantile(vel,0.55); q_hold=np.quantile(vel,0.80)
pf,tf=ptr_all[vel<q_fit],ttr_all[vel<q_fit]
ph,th=ptr_all[vel>=q_hold],ttr_all[vel>=q_hold]
fm,fs,tm,ts=standardizers(pf,tf); norm=(fm,fs,tm,ts)
vs={'h':(ph,th)}
base=dict(norm=norm,epochs=120,bs=32,lr=1e-3,wd=1e-4,loss='huber',tw=[1.0,1.0],
vel_aug=True,vel_aug_prob=0.7,vel_range=(30.0,90.0),p_exp=-0.20)
def run(cfg_up,seeds=(0,1,2)):
accs=[]
for s in seeds:
cfg=dict(base); cfg.update(cfg_up)
_,r=train_run(cfg,pf,tf,vs,ref_std,seed=s)
rr=r['h']; accs.append([rr['nrmse'][0],rr['nrmse'][1],rr['mean_nrmse']])
return np.array(accs).mean(0)
for up,lbl in [
(dict(),'huber120'),
(dict(swa=True,swa_start=90),'huber120+swa'),
(dict(epochs=200,swa=True,swa_start=140),'huber200+swa'),
(dict(wd=3e-4),'wd3e-4'),
(dict(vel_range=(35.0,95.0)),'range35-95'),
(dict(vel_range=(30.0,85.0)),'range30-85'),
]:
a=run(up); print(' %-16s CD=%.3f CL=%.3f mean=%.3f'%(lbl,a[0],a[1],a[2]),flush=True)
print('DONE',flush=True)
PYEOF
nohup python run_tune3.py > tune3.log 2>&1 &
echo "started PID $!"started PID 609
cd /app && sleep 60 && export OMP_NUM_THREADS=8 && cat > verify_contract.py << 'PYEOF'
import torch, json
from pathlib import Path
from candidate_model import build_model
APP=Path('/app')
cfg=json.load(open(APP/'task_config.json'))
# simulate the grader's load+forward+destandardize using a dummy checkpoint
m=build_model(cfg)
ckpt={'state_dict':{k:v.clone() for k,v in m.state_dict().items()},
'feat_mean':torch.zeros(4),'feat_std':torch.ones(4),
'targ_mean':torch.zeros(2),'targ_std':torch.ones(2)}
# reload path
m2=build_model(cfg); m2.load_state_dict(ckpt['state_dict']); m2.eval()
pts,_=torch.load(APP/'data/public_test/split.pt',weights_only=True).values(),None
pts=torch.load(APP/'data/public_test/split.pt',weights_only=True)['points'].float()
fm,fs=ckpt['feat_mean'],ckpt['feat_std']; tm,ts=ckpt['targ_mean'],ckpt['targ_std']
with torch.no_grad():
x=(pts-fm.view(1,1,-1))/fs.view(1,1,-1)
pred=m2(x)*ts.view(1,-1)+tm.view(1,-1)
print('forward OK, pred shape',pred.shape)
print('state_dict keys match:', set(ckpt['state_dict'])==set(build_model(cfg).state_dict()))
PYEOF
python verify_contract.pyforward OK, pred shape torch.Size([100, 2]) state_dict keys match: True
cd /app && sleep 180 && cat tune3.log
huber120 CD=0.095 CL=0.054 mean=0.074 huber120+swa CD=0.102 CL=0.051 mean=0.076
cd /app && sleep 220 && cat tune3.log
huber120 CD=0.095 CL=0.054 mean=0.074 huber120+swa CD=0.102 CL=0.051 mean=0.076 huber200+swa CD=0.092 CL=0.049 mean=0.070 wd3e-4 CD=0.095 CL=0.054 mean=0.074
cd /app && sleep 140 && cat tune3.log | tail -3
huber200+swa CD=0.092 CL=0.049 mean=0.070 wd3e-4 CD=0.095 CL=0.054 mean=0.074 range35-95 CD=0.094 CL=0.054 mean=0.074
cd /app && sleep 45 && cat tune3.log | tail -2
range30-85 CD=0.097 CL=0.054 mean=0.075 DONE
cd /app && export OMP_NUM_THREADS=8 && cat > run_tune4.py << 'PYEOF'
import torch, numpy as np
torch.set_num_threads(8)
from exp import load, standardizers, train_run
ptr_all,ttr_all=load('train')
ref_std=ttr_all.std(0).clamp_min(1e-8)
vel=ptr_all[:,0,2].numpy()
# Holdout H1: fit<q55, hold>=q80 ; H2 (harder gap): fit<q60, hold>=q88
def mk(qf,qh):
pf,tf=ptr_all[vel<np.quantile(vel,qf)],ttr_all[vel<np.quantile(vel,qf)]
ph,th=ptr_all[vel>=np.quantile(vel,qh)],ttr_all[vel>=np.quantile(vel,qh)]
fm,fs,tm,ts=standardizers(pf,tf)
return pf,tf,{'h':(ph,th)},(fm,fs,tm,ts)
def run(norm,pf,tf,vs,cfg_up,seeds=(0,1,2)):
base=dict(norm=norm,epochs=200,bs=32,lr=1e-3,wd=1e-4,loss='huber',tw=[1.0,1.0],
vel_aug=True,vel_aug_prob=0.7,vel_range=(30.0,90.0),p_exp=-0.20,swa=True,swa_start=140)
accs=[]
for s in seeds:
cfg=dict(base); cfg.update(cfg_up)
_,r=train_run(cfg,pf,tf,vs,ref_std,seed=s)
rr=r['h']; accs.append([rr['nrmse'][0],rr['nrmse'][1],rr['mean_nrmse']])
return np.array(accs).mean(0)
print('== Holdout H1 (fit<q55, hold>=q80, range 66.6-74.9) ==',flush=True)
pf,tf,vs,norm=mk(0.55,0.80)
for up,lbl in [(dict(),'200/swa140'),(dict(epochs=300,swa_start=210),'300/swa210'),
(dict(vel_aug_prob=0.8),'prob.8')]:
a=run(norm,pf,tf,vs,up); print(' %-12s CD=%.3f CL=%.3f mean=%.3f'%(lbl,a[0],a[1],a[2]),flush=True)
print('== Holdout H2 (fit<q60, hold>=q88, bigger gap) ==',flush=True)
pf,tf,vs,norm=mk(0.60,0.88)
print(' hold range %.1f-%.1f n=%d'%(vel[vel>=np.quantile(vel,0.88)].min(),vel.max(),(vel>=np.quantile(vel,0.88)).sum()),flush=True)
for up,lbl in [(dict(),'200/swa140'),(dict(epochs=300,swa_start=210),'300/swa210')]:
a=run(norm,pf,tf,vs,up); print(' %-12s CD=%.3f CL=%.3f mean=%.3f'%(lbl,a[0],a[1],a[2]),flush=True)
print('DONE',flush=True)
PYEOF
nohup python run_tune4.py > tune4.log 2>&1 &
echo "started PID $!"started PID 710
cd /app && cat > train_final.py << 'PYEOF'
"""Final trainer: physics-informed velocity augmentation for high-Re extrapolation.
Key ideas (validated on a held-out high-velocity split that mimics the scored
regime):
* C_L is velocity-independent -> augment velocity while keeping C_L fixed so
the network learns to ignore the spurious velocity signal.
* C_D follows the turbulent Reynolds law C_D ~ velocity^p (p ~= -0.20,
measured from the data) -> when we resample velocity we rescale C_D by
(v_new/v_old)^p, teaching the network the true high-velocity trend.
* Huber loss + cosine schedule + SWA weight averaging for a robust single
checkpoint. No coordinate jitter (it destroys the C_D-relevant geometry).
The architecture in candidate_model.py is untouched; we only control the four
normalization tensors, the loss/augmentation/schedule, and the weights.
"""
from __future__ import annotations
import json, os, sys
from pathlib import Path
import numpy as np
import torch
import torch.nn.functional as F
APP=Path(os.environ.get('APP_DIR','/app')); sys.path.insert(0,str(APP))
from candidate_model import build_model
from dataset import load_split
# ---- hyperparameters (chosen via high-velocity holdout experiments) ----
P_EXP = -0.20 # C_D ~ velocity^P_EXP
VEL_AUG_P = 0.7 # fraction of samples with resampled velocity
VEL_RANGE = (30.0, 90.0) # augmented velocity sampling range (test is 75-82)
EPOCHS = int(os.environ.get('EPOCHS','260'))
SWA_START = int(os.environ.get('SWA_START','180'))
BS = 32
LR = 1e-3
WD = 1e-4
HUBER_BETA = 1.0
SEED = int(os.environ.get('SEED','0'))
def augment(pts,t,gen):
B=pts.shape[0]; pts=pts.clone(); t=t.clone()
mask=torch.rand(B,generator=gen)<VEL_AUG_P
lo,hi=VEL_RANGE
newv=torch.rand(B,generator=gen)*(hi-lo)+lo
oldv=pts[:,0,2].clone()
cd_scale=(newv/oldv).clamp_min(1e-6)**P_EXP
v_applied=torch.where(mask,newv,oldv)
pts[:,:,2]=v_applied.unsqueeze(1)
t[:,0]=torch.where(mask,t[:,0]*cd_scale,t[:,0])
return pts,t
def main():
cfg=json.load(open(APP/'task_config.json'))
torch.manual_seed(SEED); np.random.seed(SEED)
gen=torch.Generator().manual_seed(SEED+12345)
# Use ALL labeled data (train + val) for the final fit.
ptr,ttr=load_split(APP/'data'/'train')
pv,tv=load_split(APP/'data'/'val')
pts=torch.cat([ptr,pv],0); tg=torch.cat([ttr,tv],0)
N=pts.shape[0]
print(f'[final] training on {N} labeled cases (train+val)',flush=True)
# Normalization from raw features/targets (the exact transform the grader uses).
flat=pts.reshape(-1,4)
feat_mean=flat.mean(0); feat_std=flat.std(0).clamp_min(1e-8)
targ_mean=tg.mean(0); targ_std=tg.std(0).clamp_min(1e-8)
fm=feat_mean.view(1,1,-1); fs=feat_std.view(1,1,-1)
tm=targ_mean.view(1,-1); ts=targ_std.view(1,-1)
model=build_model(cfg)
opt=torch.optim.AdamW(model.parameters(),lr=LR,weight_decay=WD)
sched=torch.optim.lr_scheduler.CosineAnnealingLR(opt,T_max=EPOCHS)
swa_w=None; swa_n=0
for ep in range(EPOCHS):
model.train()
idx=torch.randperm(N,generator=gen)
for s in range(0,N,BS):
bi=idx[s:s+BS]
pb,tb=augment(pts[bi],tg[bi],gen)
x=(pb-fm)/fs
y=(tb-tm)/ts
opt.zero_grad(set_to_none=True)
out=model(x)
loss=F.smooth_l1_loss(out,y,beta=HUBER_BETA)
loss.backward(); opt.step()
sched.step()
if ep>=SWA_START:
with torch.no_grad():
sd=model.state_dict()
if swa_w is None:
swa_w={k:v.clone().float() for k,v in sd.items()}; swa_n=1
else:
swa_n+=1
for k,v in sd.items(): swa_w[k]+=(v.float()-swa_w[k])/swa_n
if (ep+1)%40==0 or ep==EPOCHS-1:
print(f'[final] epoch {ep+1}/{EPOCHS} loss={loss.item():.4f}',flush=True)
# Load S
…[truncated]wrote train_final.py
cd /app && sleep 30 && cat tune4.log
== Holdout H1 (fit<q55, hold>=q80, range 66.6-74.9) ==
cd /app && sleep 230 && cat tune4.log
== Holdout H1 (fit<q55, hold>=q80, range 66.6-74.9) == 200/swa140 CD=0.092 CL=0.049 mean=0.070
cd /app && sleep 250 && cat tune4.log
== Holdout H1 (fit<q55, hold>=q80, range 66.6-74.9) == 200/swa140 CD=0.092 CL=0.049 mean=0.070 300/swa210 CD=0.137 CL=0.049 mean=0.093
cd /app && sleep 200 && cat tune4.log
== Holdout H1 (fit<q55, hold>=q80, range 66.6-74.9) == 200/swa140 CD=0.092 CL=0.049 mean=0.070 300/swa210 CD=0.137 CL=0.049 mean=0.093 prob.8 CD=0.096 CL=0.047 mean=0.072 == Holdout H2 (fit<q60, hold>=q88, bigger gap) == hold range 69.1-74.9 n=76 200/swa140 CD=0.112 CL=0.052 mean=0.082
cd /app && pkill -f run_tune4 2>/dev/null; sleep 2; export OMP_NUM_THREADS=8 && cat > run_epoch.py << 'PYEOF'
import torch, numpy as np
torch.set_num_threads(8)
from exp import load, standardizers, train_run
ptr_all,ttr_all=load('train')
ref_std=ttr_all.std(0).clamp_min(1e-8)
vel=ptr_all[:,0,2].numpy()
qh=np.quantile(vel,0.80)
pf,tf=ptr_all[vel<qh],ttr_all[vel<qh] # 504 samples, close to final 710
ph,th=ptr_all[vel>=qh],ttr_all[vel>=qh]
print('fit n=%d, hold n=%d range %.1f-%.1f'%(len(pf),len(ph),vel[vel>=qh].min(),vel.max()),flush=True)
fm,fs,tm,ts=standardizers(pf,tf); norm=(fm,fs,tm,ts)
vs={'h':(ph,th)}
def run(ep,sw,seeds=(0,1,2)):
base=dict(norm=norm,epochs=ep,bs=32,lr=1e-3,wd=1e-4,loss='huber',tw=[1.0,1.0],
vel_aug=True,vel_aug_prob=0.7,vel_range=(30.0,90.0),p_exp=-0.20,swa=True,swa_start=sw)
accs=[]
for s in seeds:
_,r=train_run(base,pf,tf,vs,ref_std,seed=s)
rr=r['h']; accs.append([rr['nrmse'][0],rr['nrmse'][1],rr['mean_nrmse']])
return np.array(accs).mean(0)
for ep,sw in [(100,70),(140,100),(180,130),(240,170)]:
a=run(ep,sw); print(' ep=%d swa=%d CD=%.3f CL=%.3f mean=%.3f'%(ep,sw,a[0],a[1],a[2]),flush=True)
print('DONE',flush=True)
PYEOF
nohup python run_epoch.py > epoch.log 2>&1 &
echo "started PID $!"started PID 795 [stderr] [4]+ Terminated nohup python run_tune4.py > tune4.log 2>&1
cd /app && sleep 260 && cat epoch.log
fit n=504, hold n=126 range 66.6-74.9 ep=100 swa=70 CD=0.092 CL=0.038 mean=0.065
cd /app && sleep 250 && cat epoch.log
fit n=504, hold n=126 range 66.6-74.9 ep=100 swa=70 CD=0.092 CL=0.038 mean=0.065 ep=140 swa=100 CD=0.080 CL=0.040 mean=0.060 ep=180 swa=130 CD=0.082 CL=0.040 mean=0.061
cd /app && sleep 120 && cat epoch.log | tail -2
ep=140 swa=100 CD=0.080 CL=0.040 mean=0.060 ep=180 swa=130 CD=0.082 CL=0.040 mean=0.061
cd /app && sleep 90 && cat epoch.log | tail -2
ep=140 swa=100 CD=0.080 CL=0.040 mean=0.060 ep=180 swa=130 CD=0.082 CL=0.040 mean=0.061
cd /app && sleep 80 && cat epoch.log | tail -2
ep=240 swa=170 CD=0.079 CL=0.041 mean=0.060 DONE
cd /app && sed -i "s/EPOCHS = int(os.environ.get('EPOCHS','260'))/EPOCHS = int(os.environ.get('EPOCHS','180'))/" train_final.py
sed -i "s/SWA_START = int(os.environ.get('SWA_START','180'))/SWA_START = int(os.environ.get('SWA_START','120'))/" train_final.py
grep -E "EPOCHS|SWA_START" train_final.py | head -2
export OMP_NUM_THREADS=8 && nohup python train_final.py > final.log 2>&1 &
echo "started final PID $!"EPOCHS = int(os.environ.get('EPOCHS','180'))
SWA_START = int(os.environ.get('SWA_START','120'))
started final PID 885cd /app && sleep 200 && cat final.log
[final] training on 710 labeled cases (train+val) [final] epoch 40/180 loss=0.0309 [final] epoch 80/180 loss=0.3294 [final] epoch 120/180 loss=0.1914 [final] epoch 160/180 loss=0.0568 [final] epoch 180/180 loss=0.0104 [final] applied SWA over 60 epochs; recomputing BN stats [final] VAL rmse CD=0.00037 CL=0.0164 | nrmse CD=0.072 CL=0.028 [final] PUBLIC_TEST pred CD mean=0.01189 [0.00720,0.02995] [final] PUBLIC_TEST pred CL mean=0.6703 [-0.4953,1.7071] [final] wrote /app/outputs/model.pt
cd /app && export OMP_NUM_THREADS=8 && cat > check_ckpt.py << 'PYEOF'
import torch, json
from pathlib import Path
from candidate_model import build_model
APP=Path('/app'); cfg=json.load(open(APP/'task_config.json'))
ck=torch.load(APP/'outputs/model.pt',map_location='cpu',weights_only=True)
print('keys:',sorted(ck.keys()))
for k in ['feat_mean','feat_std','targ_mean','targ_std']:
print(f' {k}: shape={tuple(ck[k].shape)} vals={ck[k].tolist()}')
# strict load into the fixed architecture
m=build_model(cfg)
missing,unexpected=m.load_state_dict(ck['state_dict'],strict=True) if False else ([],[])
m.load_state_dict(ck['state_dict']); m.eval()
print('state_dict loaded strict OK; n params tensors=',len(ck['state_dict']))
# grader-style inference
pts=torch.load(APP/'data/public_test/split.pt',weights_only=True)['points'].float()
fm=ck['feat_mean'].view(1,1,-1); fs=ck['feat_std'].view(1,1,-1)
tm=ck['targ_mean'].view(1,-1); ts=ck['targ_std'].view(1,-1)
with torch.no_grad():
pred=m((pts-fm)/fs)*ts+tm
print('pred CD: mean=%.5f min=%.5f max=%.5f all positive=%s'%(pred[:,0].mean(),pred[:,0].min(),pred[:,0].max(),bool((pred[:,0]>0).all())))
print('pred CL: mean=%.4f min=%.4f max=%.4f'%(pred[:,1].mean(),pred[:,1].min(),pred[:,1].max()))
# check std/mean finite
import math
assert torch.isfinite(torch.cat([ck['feat_mean'],ck['feat_std'],ck['targ_mean'],ck['targ_std']])).all()
assert (ck['feat_std']>0).all() and (ck['targ_std']>0).all()
print('ALL CHECKS PASSED')
PYEOF
python check_ckpt.pykeys: ['feat_mean', 'feat_std', 'state_dict', 'targ_mean', 'targ_std'] feat_mean: shape=(4,) vals=[0.38364875316619873, 0.007897475734353065, 53.19941711425781, 0.07678442448377609] feat_std: shape=(4,) vals=[0.39559754729270935, 0.03333360701799393, 12.652849197387695, 0.09548104554414749] targ_mean: shape=(2,) vals=[0.012753964401781559, 0.6618618965148926] targ_std: shape=(2,) vals=[0.005126286298036575, 0.5782685279846191] state_dict loaded strict OK; n params tensors= 27 pred CD: mean=0.01189 min=0.00720 max=0.02995 all positive=True pred CL: mean=0.6703 min=-0.4953 max=1.7071 ALL CHECKS PASSED
cd /app && sed -i "s#out_dir=APP/'outputs'; out_dir.mkdir(parents=True,exist_ok=True)#out_dir=Path(os.environ.get('OUT_DIR',str(APP/'outputs'))); out_dir.mkdir(parents=True,exist_ok=True)#" train_final.py
grep -n "OUT_DIR" train_final.py
export OMP_NUM_THREADS=8
( SEED=1 OUT_DIR=/app/seed1 python train_final.py > s1.log 2>&1 ; echo S1DONE >> s1.log ) &
( SEED=2 OUT_DIR=/app/seed2 python train_final.py > s2.log 2>&1 ; echo S2DONE >> s2.log ) &
echo "launched seeds 1,2"127: out_dir=Path(os.environ.get('OUT_DIR',str(APP/'outputs'))); out_dir.mkdir(parents=True,exist_ok=True)
launched seeds 1,2cd /app && sleep 210 && tail -3 s1.log && echo "---" && tail -3 s2.log
[final] PUBLIC_TEST pred CL mean=0.6748 [-0.5159,1.7081] [final] wrote /app/seed1/model.pt S1DONE --- [final] PUBLIC_TEST pred CL mean=0.6710 [-0.5124,1.7105] [final] wrote /app/seed2/model.pt S2DONE
cd /app && export OMP_NUM_THREADS=8 && cat > compare_seeds.py << 'PYEOF'
import torch, json, numpy as np
from pathlib import Path
from candidate_model import build_model
APP=Path('/app'); cfg=json.load(open(APP/'task_config.json'))
pts=torch.load(APP/'data/public_test/split.pt',weights_only=True)['points'].float()
vel=pts[:,0,2].numpy(); aoa=pts[:,0,3].numpy()
def predict(path):
ck=torch.load(path,map_location='cpu',weights_only=True)
m=build_model(cfg); m.load_state_dict(ck['state_dict']); m.eval()
fm=ck['feat_mean'].view(1,1,-1); fs=ck['feat_std'].view(1,1,-1)
tm=ck['targ_mean'].view(1,-1); ts=ck['targ_std'].view(1,-1)
with torch.no_grad(): return (m((pts-fm)/fs)*ts+tm).numpy()
P=[predict(p) for p in ['outputs/model.pt','seed1/model.pt','seed2/model.pt']]
P=np.stack(P) # [3,100,2]
print('Per-seed public_test means: CD',[f'{P[i,:,0].mean():.5f}' for i in range(3)],
'CL',[f'{P[i,:,1].mean():.4f}' for i in range(3)])
# cross-seed disagreement vs signal
cd_std=P[:,:,0].std(0).mean(); cl_std=P[:,:,1].std(0).mean()
print('mean cross-seed std: CD=%.5f (vs CD spread %.5f) | CL=%.4f (vs CL spread %.4f)'%(
cd_std, P.mean(0)[:,0].std(), cl_std, P.mean(0)[:,1].std()))
# agreement correlation between seed0 and seed1
print('corr seed0-seed1: CD=%.4f CL=%.4f'%(np.corrcoef(P[0,:,0],P[1,:,0])[0,1], np.corrcoef(P[0,:,1],P[1,:,1])[0,1]))
# physical trends on ensemble-mean predictions
mp=P.mean(0)
print('corr(vel, predCD)=%.3f (expect negative)'%np.corrcoef(vel,mp[:,0])[0,1])
print('corr(aoa, predCL)=%.3f (expect ~+1)'%np.corrcoef(aoa,mp[:,1])[0,1])
# compare predicted CD-vs-vel slope in log space to physics (-0.2)
b=np.polyfit(np.log(vel), np.log(mp[:,0]), 1)[0]
print('pred log-log CD~vel exponent on test=%.3f (physics ~ -0.2, but test vel range is narrow)'%b)
PYEOF
python compare_seeds.pyPer-seed public_test means: CD ['0.01189', '0.01191', '0.01192'] CL ['0.6703', '0.6748', '0.6710'] mean cross-seed std: CD=0.00008 (vs CD spread 0.00467) | CL=0.0064 (vs CL spread 0.5525) corr seed0-seed1: CD=0.9993 CL=0.9997 corr(vel, predCD)=-0.085 (expect negative) corr(aoa, predCL)=0.945 (expect ~+1) pred log-log CD~vel exponent on test=-1.405 (physics ~ -0.2, but test vel range is narrow)
cd /app && export OMP_NUM_THREADS=8 && cat > confirm_aug.py << 'PYEOF'
import torch, numpy as np
torch.set_num_threads(8)
from exp import load, standardizers, train_run
ptr,ttr=load('train'); pv,tv=load('val')
pts=torch.cat([ptr,pv]); tg=torch.cat([ttr,tv])
ref_std=tg.std(0).clamp_min(1e-8)
fm,fs,tm,ts=standardizers(pts,tg); norm=(fm,fs,tm,ts)
pte=load('public_test')[0]
dummy=torch.zeros(pte.shape[0],2)
vs={'test':(pte,dummy)}
def get(cfg_up):
base=dict(norm=norm,epochs=150,bs=32,lr=1e-3,wd=1e-4,loss='huber',tw=[1.0,1.0],
vel_aug=True,vel_aug_prob=0.7,vel_range=(30.0,90.0),p_exp=-0.20,swa=True,swa_start=100)
base.update(cfg_up)
_,r=train_run(base,pts,tg,vs,ref_std,seed=0)
return r['test']['pred'][:,0].numpy()
cd_aug=get({})
cd_noaug=get({'vel_aug':False})
print('train C_D mean (vel~53): %.5f'%tg[:,0].mean())
print('public_test (vel 75-82) predicted C_D mean:')
print(' NO-AUG : %.5f'%cd_noaug.mean())
print(' AUG : %.5f'%cd_aug.mean())
print(' => augmentation lowers high-vel C_D by %.1f%% (physics expects ~%.1f%%)'%(
100*(1-cd_aug.mean()/cd_noaug.mean()), 100*(1-(78/53)**-0.2)))
PYEOF
nohup python confirm_aug.py > confirm.log 2>&1 &
echo "started PID $!"started PID 1009
cd /app && sed -n '1,60p' train_final.py
"""Final trainer: physics-informed velocity augmentation for high-Re extrapolation.
Key ideas (validated on a held-out high-velocity split that mimics the scored
regime):
* C_L is velocity-independent -> augment velocity while keeping C_L fixed so
the network learns to ignore the spurious velocity signal.
* C_D follows the turbulent Reynolds law C_D ~ velocity^p (p ~= -0.20,
measured from the data) -> when we resample velocity we rescale C_D by
(v_new/v_old)^p, teaching the network the true high-velocity trend.
* Huber loss + cosine schedule + SWA weight averaging for a robust single
checkpoint. No coordinate jitter (it destroys the C_D-relevant geometry).
The architecture in candidate_model.py is untouched; we only control the four
normalization tensors, the loss/augmentation/schedule, and the weights.
"""
from __future__ import annotations
import json, os, sys
from pathlib import Path
import numpy as np
import torch
import torch.nn.functional as F
APP=Path(os.environ.get('APP_DIR','/app')); sys.path.insert(0,str(APP))
from candidate_model import build_model
from dataset import load_split
# ---- hyperparameters (chosen via high-velocity holdout experiments) ----
P_EXP = -0.20 # C_D ~ velocity^P_EXP
VEL_AUG_P = 0.7 # fraction of samples with resampled velocity
VEL_RANGE = (30.0, 90.0) # augmented velocity sampling range (test is 75-82)
EPOCHS = int(os.environ.get('EPOCHS','180'))
SWA_START = int(os.environ.get('SWA_START','120'))
BS = 32
LR = 1e-3
WD = 1e-4
HUBER_BETA = 1.0
SEED = int(os.environ.get('SEED','0'))
def augment(pts,t,gen):
B=pts.shape[0]; pts=pts.clone(); t=t.clone()
mask=torch.rand(B,generator=gen)<VEL_AUG_P
lo,hi=VEL_RANGE
newv=torch.rand(B,generator=gen)*(hi-lo)+lo
oldv=pts[:,0,2].clone()
cd_scale=(newv/oldv).clamp_min(1e-6)**P_EXP
v_applied=torch.where(mask,newv,oldv)
pts[:,:,2]=v_applied.unsqueeze(1)
t[:,0]=torch.where(mask,t[:,0]*cd_scale,t[:,0])
return pts,t
def main():
cfg=json.load(open(APP/'task_config.json'))
torch.manual_seed(SEED); np.random.seed(SEED)
gen=torch.Generator().manual_seed(SEED+12345)
# Use ALL labeled data (train + val) for the final fit.
ptr,ttr=load_split(APP/'data'/'train')
pv,tv=load_split(APP/'data'/'val')
pts=torch.cat([ptr,pv],0); tg=torch.cat([ttr,tv],0)
N=pts.shape[0]cd /app && sleep 230 && cat confirm.log
train C_D mean (vel~53): 0.01275 public_test (vel 75-82) predicted C_D mean: NO-AUG : 0.01177 AUG : 0.01180 => augmentation lowers high-vel C_D by -0.3% (physics expects ~7.4%)
cd /app && export OMP_NUM_THREADS=8 && cat > run_realistic.py << 'PYEOF'
import torch, numpy as np
torch.set_num_threads(8)
from exp import load, standardizers, train_run
ptr,ttr=load('train'); pv,tv=load('val')
Pall=torch.cat([ptr,pv]); Tall=torch.cat([ttr,tv])
ref_std=Tall.std(0).clamp_min(1e-8)
vel=Pall[:,0,2].numpy()
def run_holdout(qcut, tag):
thr=np.quantile(vel,qcut)
pf,tf=Pall[vel<thr],Tall[vel<thr]
ph,th=Pall[vel>=thr],Tall[vel>=thr]
fm,fs,tm,ts=standardizers(pf,tf); norm=(fm,fs,tm,ts); vs={'h':(ph,th)}
print('== %s: train vel<%.1f (n=%d), test vel %.1f-%.1f (n=%d) =='%(
tag,thr,len(pf),vel[vel>=thr].min(),vel.max(),len(ph)),flush=True)
def go(aug):
base=dict(norm=norm,epochs=150,bs=32,lr=1e-3,wd=1e-4,loss='huber',tw=[1.0,1.0],
vel_aug=aug,vel_aug_prob=0.7,vel_range=(30.0,90.0),p_exp=-0.20,swa=True,swa_start=100)
accs=[]
for s in (0,1,2):
_,r=train_run(base,pf,tf,vs,ref_std,seed=s)
rr=r['h']; accs.append([rr['nrmse'][0],rr['nrmse'][1],rr['mean_nrmse']])
return np.array(accs).mean(0)
for aug,lbl in [(False,'NO-AUG'),(True,'AUG ')]:
a=go(aug); print(' %s CD=%.3f CL=%.3f mean=%.3f'%(lbl,a[0],a[1],a[2]),flush=True)
run_holdout(0.85,'small gap (test just above train)')
run_holdout(0.90,'smaller holdout, top 10%')
print('DONE',flush=True)
PYEOF
nohup python run_realistic.py > realistic.log 2>&1 &
echo "started PID $!"started PID 1068
cd /app && sleep 270 && cat realistic.log
== small gap (test just above train): train vel<67.9 (n=603), test vel 67.9-74.9 (n=107) == NO-AUG CD=0.080 CL=0.062 mean=0.071
cd /app && sleep 200 && cat realistic.log
== small gap (test just above train): train vel<67.9 (n=603), test vel 67.9-74.9 (n=107) == NO-AUG CD=0.080 CL=0.062 mean=0.071 AUG CD=0.073 CL=0.038 mean=0.055 == smaller holdout, top 10%: train vel<70.3 (n=639), test vel 70.3-74.9 (n=71) ==
cd /app && sleep 180 && cat realistic.log
== small gap (test just above train): train vel<67.9 (n=603), test vel 67.9-74.9 (n=107) == NO-AUG CD=0.080 CL=0.062 mean=0.071 AUG CD=0.073 CL=0.038 mean=0.055 == smaller holdout, top 10%: train vel<70.3 (n=639), test vel 70.3-74.9 (n=71) == NO-AUG CD=0.096 CL=0.057 mean=0.076
cd /app && sleep 120 && cat realistic.log | tail -2
== smaller holdout, top 10%: train vel<70.3 (n=639), test vel 70.3-74.9 (n=71) == NO-AUG CD=0.096 CL=0.057 mean=0.076
cd /app && sleep 90 && cat realistic.log | tail -2
AUG CD=0.073 CL=0.040 mean=0.057 DONE
cd /app && export OMP_NUM_THREADS=8 && python check_ckpt.py && echo "=== file ===" && ls -la outputs/model.pt
keys: ['feat_mean', 'feat_std', 'state_dict', 'targ_mean', 'targ_std'] feat_mean: shape=(4,) vals=[0.38364875316619873, 0.007897475734353065, 53.19941711425781, 0.07678442448377609] feat_std: shape=(4,) vals=[0.39559754729270935, 0.03333360701799393, 12.652849197387695, 0.09548104554414749] targ_mean: shape=(2,) vals=[0.012753964401781559, 0.6618618965148926] targ_std: shape=(2,) vals=[0.005126286298036575, 0.5782685279846191] state_dict loaded strict OK; n params tensors= 27 pred CD: mean=0.01189 min=0.00720 max=0.02995 all positive=True pred CL: mean=0.6703 min=-0.4953 max=1.7071 ALL CHECKS PASSED === file === -rw-r--r-- 1 h2tagent h2tagent 346611 Jun 19 22:38 outputs/model.pt
cd /app && ls -la outputs/ && echo "--- deliverable verified; scratch seed dirs (not used for grading): ---" && ls -d seed1 seed2 2>/dev/null
total 347 drwxr-sr-x 2 h2tagent h2tagent 4096 Jun 19 22:38 . drwxrws--- 7 h2tagent h2tagent 4096 Jun 19 22:48 .. -rw-r--r-- 1 h2tagent h2tagent 346611 Jun 19 22:38 model.pt --- deliverable verified; scratch seed dirs (not used for grading): --- seed1 seed2
No file changes, the agent left the workspace untouched.
# SyncVals normalized verifier summary # task: airfrans-high-reynolds-drag-extrapolation # attempt: 3 status: completed raw_score: 0.51 # reward rule: continuous: score 0.51 >= 0.5 # => reward = 1.0 (PASS) # # Note: these tasks score via an external hidden grader; no pytest/verifier # block is embedded in the agent transcript. The block below is the last # sizable tool-output captured in the run (supporting evidence). ---------------------------------------------------------------------- total 347 drwxr-sr-x 2 h2tagent h2tagent 4096 Jun 19 22:38 . drwxrws--- 7 h2tagent h2tagent 4096 Jun 19 22:48 .. -rw-r--r-- 1 h2tagent h2tagent 346611 Jun 19 22:38 model.pt --- deliverable verified; scratch seed dirs (not used for grading): --- seed1 seed2
Reproduce this trial: git checkout 2f94510 && PYTHONPATH=src python3 scripts/build_site.py , then open trial/trial_2f1bf3413a6f4c74. Re-running the agent live requires EVAL_PLATFORM_ENABLE_OAUTH_SMOKE=1 and is non-deterministic.
Trial trial_2f1bf3413a6f4c74 · verifier authoritative; classifier explanatory.