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 "===candidate_model.py===" && cat candidate_model.py && echo "===dataset.py===" && cat dataset.py && echo "===train.py===" && cat train.py
===candidate_model.py===
"""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.py===
"""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.py===
"""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,
…[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, '===')
for k,v in p.items():
print(' ', k, tuple(v.shape), v.dtype)
"=== train === points (630, 256, 4) torch.float32 targets (630, 2) torch.float32 === val === points (80, 256, 4) torch.float32 targets (80, 2) torch.float32 === public_test === points (100, 256, 4) torch.float32
cd /app && python -c "
import torch
def desc(name, t):
print(f'{name:20s} min={t.min().item():.5f} max={t.max().item():.5f} mean={t.mean().item():.5f} std={t.std().item():.5f}')
tr = torch.load('data/train/split.pt', weights_only=True)
va = torch.load('data/val/split.pt', weights_only=True)
pt = torch.load('data/public_test/split.pt', weights_only=True)
names=['x','y','inlet_velocity','aoa']
for ci,cn in enumerate(names):
print('--- channel', cn, '---')
for split,p in [('train',tr),('val',va),('test',pt)]:
desc(split, p['points'][:,:,ci])
print()
print('=== TARGETS ===')
for ti,tn in enumerate(['C_D','C_L']):
print('--- target', tn, '---')
for split,p in [('train',tr),('val',va)]:
desc(split, p['targets'][:,ti])
"--- channel x --- train min=-0.00702 max=1.00000 mean=0.38353 std=0.39556 val min=-0.00328 max=1.00000 mean=0.38458 std=0.39590 test min=-0.00553 max=1.00000 mean=0.37608 std=0.39461 --- channel y --- train min=-0.09970 max=0.16270 mean=0.00791 std=0.03346 val min=-0.09394 max=0.13663 mean=0.00777 std=0.03233 test min=-0.09606 max=0.15281 mean=0.00879 std=0.03464 --- channel inlet_velocity --- train min=31.28300 max=74.92400 mean=52.97366 std=12.78588 val min=31.46800 max=74.16200 mean=54.97725 std=11.39685 test min=75.03000 max=81.59300 mean=78.20860 std=1.96583 --- channel aoa --- train min=-0.08622 max=0.25651 mean=0.07639 std=0.09557 val min=-0.08360 max=0.25121 mean=0.07986 std=0.09472 test min=-0.08570 max=0.25820 mean=0.07571 std=0.09605 === TARGETS === --- target C_D --- train min=0.00727 max=0.04595 mean=0.01276 std=0.00510 val min=0.00756 max=0.03459 mean=0.01268 std=0.00533 --- target C_L --- train min=-0.53363 max=1.85012 mean=0.65962 std=0.57620 val min=-0.48027 max=1.80035 mean=0.67952 std=0.59777
cd /app && python -c "
import torch
tr = torch.load('data/train/split.pt', weights_only=True)
pts = tr['points']; tg = tr['targets']
# per-case scalars: velocity and aoa are constant across points
v = pts[:,0,2]; aoa = pts[:,0,3]
cd = tg[:,0]; cl = tg[:,1]
# verify v, aoa constant across points
print('v spread within case (max-min) max:', (pts[:,:,2].max(1).values-pts[:,:,2].min(1).values).abs().max().item())
print('aoa spread within case max:', (pts[:,:,3].max(1).values-pts[:,:,3].min(1).values).abs().max().item())
import numpy as np
def corr(a,b):
a=a.numpy(); b=b.numpy(); return np.corrcoef(a,b)[0,1]
print('corr(v, cd)=', corr(v,cd))
print('corr(v, cl)=', corr(v,cl))
print('corr(aoa, cd)=', corr(aoa,cd))
print('corr(aoa, cl)=', corr(aoa,cl))
print('corr(aoa^2, cd)=', corr(aoa**2,cd))
# multiple regression cd ~ v, aoa, aoa^2, 1/v...
"v spread within case (max-min) max: 0.0 aoa spread within case max: 0.0 corr(v, cd)= -0.15417731232687426 corr(v, cl)= -0.022042552660221815 corr(aoa, cd)= 0.738708826123821 corr(aoa, cl)= 0.9433278860776938 corr(aoa^2, cd)= 0.8596726372791974
cd /app && python -c "
import torch, numpy as np
tr = torch.load('data/train/split.pt', weights_only=True)
v = tr['points'][:,0,2].numpy()
print('train velocity distribution percentiles:')
for q in [0,10,25,50,75,90,95,100]:
print(f' {q:3d}%: {np.percentile(v,q):.2f}')
print('counts: v>65:', (v>65).sum(), ' v>60:', (v>60).sum(), ' v>55:', (v>55).sum())
print('total train:', len(v))
"train velocity distribution percentiles:
0%: 31.28
10%: 35.42
25%: 41.74
50%: 53.27
75%: 64.14
90%: 70.35
95%: 72.63
100%: 74.92
counts: v>65: 148 v>60: 217 v>55: 293
total train: 630cd /app && python -c "
import torch, numpy as np
from numpy.linalg import lstsq
tr = torch.load('data/train/split.pt', weights_only=True)
pts = tr['points'].numpy(); tg = tr['targets'].numpy()
v = pts[:,0,2]; aoa = pts[:,0,3]
cd = tg[:,0]; cl = tg[:,1]
# geometry features from x,y cloud (ignore v,aoa channels)
x = pts[:,:,0]; y = pts[:,:,1]
thick = y.max(1)-y.min(1) # proxy max thickness
ymean = y.mean(1) # camber proxy
ymax = y.max(1); ymin=y.min(1)
area = np.trapz(np.sort(y,1),axis=1) # rough
feats_geo = np.stack([thick, ymean, ymax, ymin],1)
# velocity holdout: train v<=65, test v>65
mask_tr = v<=65; mask_te = v>65
def nrmse(pred,true):
return np.sqrt(np.mean((pred-true)**2))/true.std()
# Build design matrices for C_D and C_L
def design(aoa,v,geo):
return np.column_stack([np.ones_like(aoa), aoa, aoa**2, v, geo])
for name,tgt in [('C_D',cd),('C_L',cl)]:
for desc,useV in [('no-vel',False),('with-vel-linear',True)]:
if useV:
X = np.column_stack([np.ones_like(aoa), aoa, aoa**2, v, thick, ymean, ymax, ymin])
else:
X = np.column_stack([np.ones_like(aoa), aoa, aoa**2, thick, ymean, ymax, ymin])
beta,_,_,_ = lstsq(X[mask_tr], tgt[mask_tr], rcond=None)
pred = X@beta
print(f'{name:4s} {desc:16s} holdout-nrmse={nrmse(pred[mask_te],tgt[mask_te]):.4f} in-nrmse={nrmse(pred[mask_tr],tgt[mask_tr]):.4f}')
print()
"C_D no-vel holdout-nrmse=0.4452 in-nrmse=0.4699 C_D with-vel-linear holdout-nrmse=0.3350 in-nrmse=0.4566 C_L no-vel holdout-nrmse=0.1301 in-nrmse=0.1392 C_L with-vel-linear holdout-nrmse=0.1299 in-nrmse=0.1395 [stderr] <string>:14: DeprecationWarning: `trapz` is deprecated. Use `trapezoid` instead, or one of the numerical integration functions in `scipy.integrate`.
cd /app && python -c "
import torch, numpy as np
from numpy.linalg import lstsq
tr = torch.load('data/train/split.pt', weights_only=True)
pts = tr['points'].numpy(); tg = tr['targets'].numpy()
v = pts[:,0,2]; aoa = pts[:,0,3]; cd=tg[:,0]
x = pts[:,:,0]; y = pts[:,:,1]
thick=y.max(1)-y.min(1); ymean=y.mean(1); ymax=y.max(1); ymin=y.min(1)
mask_tr=v<=65; mask_te=v>65
def nrmse(p,t): return np.sqrt(np.mean((p-t)**2))/t.std()
geo=[thick,ymean,ymax,ymin]
configs={
'linear-v':[aoa,aoa**2,v],
'v^2':[aoa,aoa**2,v,v**2],
'1/v (Re-like)':[aoa,aoa**2,1.0/v],
'v^-0.2':[aoa,aoa**2,v**-0.2],
'log v':[aoa,aoa**2,np.log(v)],
'v & aoa*v':[aoa,aoa**2,v,aoa*v],
}
for name,cols in configs.items():
X=np.column_stack([np.ones_like(aoa)]+cols+geo)
beta,_,_,_=lstsq(X[mask_tr],cd[mask_tr],rcond=None)
pred=X@beta
print(f'C_D {name:14s} holdout={nrmse(pred[mask_te],cd[mask_te]):.4f} in={nrmse(pred[mask_tr],cd[mask_tr]):.4f}')
"C_D linear-v holdout=0.3350 in=0.4566 C_D v^2 holdout=0.3315 in=0.4563 C_D 1/v (Re-like) holdout=0.3406 in=0.4571 C_D v^-0.2 holdout=0.3299 in=0.4576 C_D log v holdout=0.3353 in=0.4553 C_D v & aoa*v holdout=0.3123 in=0.4546
cd /app && python -c "
import torch, time
print('cuda:', torch.cuda.is_available())
print('threads:', torch.get_num_threads())
import platform; print(platform.processor())
" && nproc && free -h 2>/dev/null | head -2cuda: False
threads: 16
x86_64
16
total used free shared buff/cache available
Mem: 69Gi 66Mi 68Gi 0B 29Mi 68Gicd /app && mkdir -p exp && cat > exp/harness.py << 'EOF'
import json, os, sys, time, math
from pathlib import Path
import torch, torch.nn as nn, torch.nn.functional as F
import numpy as np
sys.path.insert(0, '/app')
from candidate_model import build_model
with open('/app/task_config.json') as fh: CFG = json.load(fh)
def load(split):
p = torch.load(f'/app/data/{split}/split.pt', weights_only=True)
return p['points'].float(), (p['targets'].float() if 'targets' in p else None)
def nrmse(pred, true):
# per-target nrmse normalized by std of true on this eval set
out=[]
for i in range(true.shape[1]):
t=true[:,i]; p=pred[:,i]
rmse=torch.sqrt(torch.mean((p-t)**2))
out.append((rmse/ t.std()).item())
return out
def make_feats(points, feat_mean, feat_std):
return (points - feat_mean.view(1,1,-1)) / feat_std.view(1,1,-1)
def train_model(xtr, ytr, targ_mean, targ_std, cfg, seed=0, verbose=False):
torch.manual_seed(seed); np.random.seed(seed)
model = build_model(CFG)
epochs=cfg.get('epochs',120); bs=cfg.get('bs',32)
lr=cfg.get('lr',1e-3); wd=cfg.get('wd',1e-4)
tw=torch.tensor(cfg.get('target_weights',[1.0,1.0]))
opt=torch.optim.Adam(model.parameters(), lr=lr, weight_decay=wd)
sched=torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=epochs)
n=xtr.shape[0]
geo_jit=cfg.get('geo_jit',0.0); vel_aug=cfg.get('vel_aug',0.0)
feat_std=cfg['_feat_std']
for ep in range(epochs):
model.train()
idx=torch.randperm(n)
for s in range(0,n,bs):
b=idx[s:s+bs]
xb=xtr[b].clone(); yb=ytr[b].clone()
if geo_jit>0:
xb[:,:,0:2]+=torch.randn_like(xb[:,:,0:2])*geo_jit
opt.zero_grad(set_to_none=True)
pred=model(xb)
loss=(tw.view(1,-1)*(pred-yb)**2).mean()
loss.backward(); opt.step()
sched.step()
return model
def evaluate(model, x, y, targ_mean, targ_std):
model.eval()
with torch.no_grad():
pred_std=model(x)
pred=pred_std*targ_std.view(1,-1)+targ_mean.view(1,-1)
return nrmse(pred, y), pred
if __name__=='__main__':
pts,tg=load('train')
v=pts[:,0,2]
mtr=v<=65; mte=v>65
print('train',mtr.sum().item(),'holdout',mte.sum().item())
EOF
python exp/harness.pytrain 482 holdout 148
cd /app && cat > exp/run1.py << 'EOF'
import sys, time, torch, numpy as np
sys.path.insert(0,'/app/exp'); sys.path.insert(0,'/app')
from harness import load, train_model, evaluate, make_feats, nrmse
pts,tg=load('train')
v=pts[:,0,2]; mtr=v<=65; mte=v>65
ptr,ttr=pts[mtr],tg[mtr]; pte,tte=pts[mte],tg[mte]
# standardize by training subset
flat=ptr.reshape(-1,4)
feat_mean=flat.mean(0); feat_std=flat.std(0).clamp_min(1e-8)
targ_mean=ttr.mean(0); targ_std=ttr.std(0).clamp_min(1e-8)
xtr=make_feats(ptr,feat_mean,feat_std); ytr=(ttr-targ_mean)/targ_std
xte=make_feats(pte,feat_mean,feat_std)
cfg=dict(epochs=120,bs=32,lr=1e-3,wd=1e-4,target_weights=[1.0,1.0],_feat_std=feat_std)
t0=time.time()
m=train_model(xtr,ytr,targ_mean,targ_std,cfg,seed=0)
print('train time %.1fs'%(time.time()-t0))
nr,pred=evaluate(m,xte,tte,targ_mean,targ_std)
print('HOLDOUT nrmse C_D=%.4f C_L=%.4f mean=%.4f'%(nr[0],nr[1],np.mean(nr)))
# also in-sample
nri,_=evaluate(m,xtr,ttr,targ_mean,targ_std)
print('INSAMP nrmse C_D=%.4f C_L=%.4f'%(nri[0],nri[1]))
# inspect predictions vs true on holdout
print('pred C_D mean/std',pred[:,0].mean().item(),pred[:,0].std().item(),'true',tte[:,0].mean().item(),tte[:,0].std().item())
EOF
python exp/run1.pytrain time 79.4s HOLDOUT nrmse C_D=0.1804 C_L=0.1132 mean=0.1468 INSAMP nrmse C_D=0.2009 C_L=0.0980 pred C_D mean/std 0.012261729687452316 0.0039148791693151 true 0.01182961743324995 0.004172573797404766
cd /app && cat > exp/probe.py << 'EOF'
import sys, torch, numpy as np
sys.path.insert(0,'/app/exp'); sys.path.insert(0,'/app')
from harness import load, train_model, evaluate, make_feats
pts,tg=load('train')
v=pts[:,0,2]; mtr=v<=65
ptr,ttr=pts[mtr],tg[mtr]
flat=ptr.reshape(-1,4)
feat_mean=flat.mean(0); feat_std=flat.std(0).clamp_min(1e-8)
targ_mean=ttr.mean(0); targ_std=ttr.std(0).clamp_min(1e-8)
xtr=make_feats(ptr,feat_mean,feat_std); ytr=(ttr-targ_mean)/targ_std
cfg=dict(epochs=120,bs=32,lr=1e-3,wd=1e-4,_feat_std=feat_std)
m=train_model(xtr,ytr,targ_mean,targ_std,cfg,seed=0); m.eval()
# take 30 holdout geometries, sweep velocity
sel=torch.where(~mtr)[0][:40]
base=pts[sel].clone()
vels=[31,45,55,65,75,82,90]
print('vel : C_D_mean C_L_mean (avg over 40 shapes)')
for vv in vels:
b=base.clone(); b[:,:,2]=vv
xb=make_feats(b,feat_mean,feat_std)
with torch.no_grad():
pr=m(xb)*targ_std+targ_mean
tag=' <-- train range' if vv<=65 else ' (extrapolation)'
print('%3d : %10.5f %10.4f%s'%(vv,pr[:,0].mean(),pr[:,1].mean(),tag))
EOF
python exp/probe.pyvel : C_D_mean C_L_mean (avg over 40 shapes) 31 : 0.01386 0.5363 <-- train range 45 : 0.01381 0.5573 <-- train range 55 : 0.01257 0.5623 <-- train range 65 : 0.01222 0.5635 <-- train range 75 : 0.01292 0.5935 (extrapolation) 82 : 0.01395 0.6270 (extrapolation) 90 : 0.01540 0.6778 (extrapolation)
cd /app && python -c "
import torch, numpy as np
from numpy.linalg import lstsq
tr=torch.load('/app/data/train/split.pt',weights_only=True)
pts=tr['points'].numpy(); tg=tr['targets'].numpy()
v=pts[:,0,2]; aoa=pts[:,0,3]; cd=tg[:,0]; cl=tg[:,1]
y=pts[:,:,1]
thick=y.max(1)-y.min(1); ymean=y.mean(1); ymax=y.max(1); ymin=y.min(1)
geo=np.column_stack([thick,ymean,ymax,ymin,aoa,aoa**2])
# log-log fit for C_D velocity exponent p, controlling geometry/aoa
X=np.column_stack([np.ones_like(v), geo, np.log(v)])
beta,_,_,_=lstsq(X, np.log(cd), rcond=None)
print('C_D ~ v^p exponent p =', beta[-1])
# linear slope model
X2=np.column_stack([np.ones_like(v), geo, v])
beta2,_,_,_=lstsq(X2, cd, rcond=None)
print('C_D linear dC_D/dv =', beta2[-1], ' (mean C_D=%.4f)'%cd.mean())
# For C_L exponent
Xl=np.column_stack([np.ones_like(v), geo, np.log(v)])
betal,_,_,_=lstsq(Xl, np.log(np.clip(cl,1e-3,None)), rcond=None)
print('C_L ~ v^q exponent q =', betal[-1], '(near 0 => v-independent)')
# residual correlation check: does v help beyond geo for C_L?
Xl2=np.column_stack([np.ones_like(v), geo]); bl2,_,_,_=lstsq(Xl2,cl,rcond=None)
res=cl-Xl2@bl2; print('corr(resid C_L, v)=', np.corrcoef(res,v)[0,1])
"C_D ~ v^p exponent p = -0.21050662 C_D linear dC_D/dv = -5.6362576e-05 (mean C_D=0.0128) C_L ~ v^q exponent q = 0.27397084 (near 0 => v-independent) corr(resid C_L, v)= 0.04640957662693094
cd /app && python -c "
import torch, numpy as np
from numpy.linalg import lstsq
tr=torch.load('/app/data/train/split.pt',weights_only=True)
pts=tr['points'].numpy(); tg=tr['targets'].numpy()
v=pts[:,0,2]; aoa=pts[:,0,3]; cd=tg[:,0]
y=pts[:,:,1]
thick=y.max(1)-y.min(1); ymean=y.mean(1); ymax=y.max(1); ymin=y.min(1)
geo=np.column_stack([thick,ymean,ymax,ymin,aoa,aoa**2])
for lim in [55,60,65,75]:
m=v<=lim
X=np.column_stack([np.ones(m.sum()), geo[m], np.log(v[m])])
beta,_,_,_=lstsq(X, np.log(cd[m]), rcond=None)
print(f'v<={lim}: n={m.sum():3d} C_D exponent p={beta[-1]:.3f}')
"v<=55: n=337 C_D exponent p=-0.157 v<=60: n=413 C_D exponent p=-0.189 v<=65: n=482 C_D exponent p=-0.209 v<=75: n=630 C_D exponent p=-0.211
cd /app && cat > exp/harness2.py << 'EOF'
import json, sys, time
import torch, numpy as np
from numpy.linalg import lstsq
sys.path.insert(0,'/app')
from candidate_model import build_model
with open('/app/task_config.json') as fh: CFG=json.load(fh)
def load(split):
p=torch.load(f'/app/data/{split}/split.pt',weights_only=True)
return p['points'].float(), (p['targets'].float() if 'targets' in p else None)
def nrmse(pred,true):
out=[]
for i in range(true.shape[1]):
t=true[:,i]
out.append((torch.sqrt(torch.mean((pred[:,i]-t)**2))/t.std()).item())
return out
def est_cd_exponent(pts,tg):
v=pts[:,0,2].numpy(); aoa=pts[:,0,3].numpy(); cd=tg[:,0].numpy()
y=pts[:,:,1].numpy()
geo=np.column_stack([y.max(1)-y.min(1),y.mean(1),y.max(1),y.min(1),aoa,aoa**2])
X=np.column_stack([np.ones_like(v),geo,np.log(v)])
beta,_,_,_=lstsq(X,np.log(cd),rcond=None)
return float(beta[-1])
def train_model(pts_phys, tg_phys, feat_mean, feat_std, targ_mean, targ_std, cfg, seed=0):
torch.manual_seed(seed); np.random.seed(seed)
model=build_model(CFG)
epochs=cfg.get('epochs',120); bs=cfg.get('bs',32)
opt=torch.optim.Adam(model.parameters(),lr=cfg.get('lr',1e-3),weight_decay=cfg.get('wd',1e-4))
sched=torch.optim.lr_scheduler.CosineAnnealingLR(opt,T_max=epochs)
tw=torch.tensor(cfg.get('target_weights',[1.0,1.0])).float()
n=pts_phys.shape[0]
p_aug=cfg.get('p_aug',0.0); vlo=cfg.get('vlo',30.0); vhi=cfg.get('vhi',90.0)
expo=cfg.get('cd_expo',-0.21); geo_jit=cfg.get('geo_jit',0.0)
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)
for ep in range(epochs):
model.train(); idx=torch.randperm(n)
for s in range(0,n,bs):
b=idx[s:s+bs]
P=pts_phys[b].clone(); T=tg_phys[b].clone()
if p_aug>0:
m=torch.rand(P.shape[0])<p_aug
if m.any():
vnew=torch.empty(m.sum()).uniform_(vlo,vhi)
vorig=P[m,0,2]
ratio=(vnew/vorig)
P[m,:,2]=vnew.view(-1,1)
T[m,0]=T[m,0]*ratio.pow(expo) # C_D scales
# C_L unchanged
if geo_jit>0:
P[:,:,0:2]+=torch.randn_like(P[:,:,0:2])*geo_jit
xb=(P-fm)/fs; yb=(T-tm)/ts
opt.zero_grad(set_to_none=True)
pred=model(xb)
loss=(tw.view(1,-1)*(pred-yb)**2).mean()
loss.backward(); opt.step()
sched.step()
return model
def evaluate(model, pts_phys, tg_phys, feat_mean, feat_std, targ_mean, targ_std):
model.eval()
xb=(pts_phys-feat_mean.view(1,1,-1))/feat_std.view(1,1,-1)
with torch.no_grad():
pred=model(xb)*targ_std.view(1,-1)+targ_mean.view(1,-1)
return nrmse(pred,tg_phys), pred
EOF
echo "harness2 written"harness2 written
cd /app && cat > exp/run2.py << 'EOF'
import sys, time, torch, numpy as np
sys.path.insert(0,'/app/exp'); sys.path.insert(0,'/app')
from harness2 import load, train_model, evaluate, est_cd_exponent
pts,tg=load('train')
v=pts[:,0,2]; mtr=v<=65; mte=v>65
ptr,ttr=pts[mtr],tg[mtr]; pte,tte=pts[mte],tg[mte]
flat=ptr.reshape(-1,4)
feat_mean=flat.mean(0); feat_std=flat.std(0).clamp_min(1e-8)
targ_mean=ttr.mean(0); targ_std=ttr.std(0).clamp_min(1e-8)
expo=est_cd_exponent(ptr,ttr); print('estimated exponent on train subset:',round(expo,3))
def run(cfg,seeds=(0,1),label=''):
accs=[]
for s in seeds:
m=train_model(ptr,ttr,feat_mean,feat_std,targ_mean,targ_std,cfg,seed=s)
nr,_=evaluate(m,pte,tte,feat_mean,feat_std,targ_mean,targ_std)
accs.append(nr)
a=np.array(accs)
print(f'{label:28s} C_D={a[:,0].mean():.4f} C_L={a[:,1].mean():.4f} mean={a.mean(1).mean():.4f}')
return a.mean(1).mean()
base=dict(epochs=120,bs=32,lr=1e-3,wd=1e-4)
t0=time.time()
run(base,label='baseline (no aug)')
aug=dict(base,p_aug=0.5,vlo=30,vhi=90,cd_expo=expo)
run(aug,label='vel-aug p=0.5 [30,90]')
print('elapsed %.0fs'%(time.time()-t0))
EOF
python exp/run2.pyestimated exponent on train subset: -0.209 baseline (no aug) C_D=0.1611 C_L=0.1526 mean=0.1569 vel-aug p=0.5 [30,90] C_D=0.2524 C_L=0.0559 mean=0.1541 elapsed 254s
cd /app && python -c "
import torch, numpy as np
tr=torch.load('/app/data/train/split.pt',weights_only=True)
pts=tr['points']; tg=tr['targets']
v=pts[:,0,2].numpy(); aoa=pts[:,0,3].numpy()
y=pts[:,:,1].numpy(); thick=y.max(1)-y.min(1)
cd=tg[:,0].numpy(); cl=tg[:,1].numpy()
print('corr(v,aoa)=%.3f corr(v,thick)=%.3f'%(np.corrcoef(v,aoa)[0,1],np.corrcoef(v,thick)[0,1]))
print('corr(aoa,thick)=%.3f'%np.corrcoef(aoa,thick)[0,1])
"
echo "--- probe aug model C_D curve ---"
cat > exp/probe2.py << 'EOF'
import sys, torch, numpy as np
sys.path.insert(0,'/app/exp'); sys.path.insert(0,'/app')
from harness2 import load, train_model, evaluate, est_cd_exponent
pts,tg=load('train'); v=pts[:,0,2]; mtr=v<=65
ptr,ttr=pts[mtr],tg[mtr]
flat=ptr.reshape(-1,4); fm=flat.mean(0); fs=flat.std(0).clamp_min(1e-8)
tm=ttr.mean(0); ts=ttr.std(0).clamp_min(1e-8)
expo=est_cd_exponent(ptr,ttr)
for lbl,cfg in [('noaug',dict(epochs=120)),('aug',dict(epochs=120,p_aug=0.5,vlo=30,vhi=90,cd_expo=expo))]:
m=train_model(ptr,ttr,fm,fs,tm,ts,cfg,seed=0); m.eval()
sel=torch.where(~mtr)[0][:40]; base=pts[sel].clone()
print(lbl,'C_D by vel:')
for vv in [31,55,65,75,82,90]:
b=base.clone(); b[:,:,2]=vv; xb=(b-fm.view(1,1,-1))/fs.view(1,1,-1)
with torch.no_grad(): pr=m(xb)*ts+tm
print(' v=%3d C_D=%.5f C_L=%.4f'%(vv,pr[:,0].mean(),pr[:,1].mean()))
EOF
python exp/probe2.pycorr(v,aoa)=-0.015 corr(v,thick)=0.040 corr(aoa,thick)=-0.023 --- probe aug model C_D curve --- noaug C_D by vel: v= 31 C_D=0.01386 C_L=0.5363 v= 55 C_D=0.01257 C_L=0.5623 v= 65 C_D=0.01222 C_L=0.5635 v= 75 C_D=0.01292 C_L=0.5935 v= 82 C_D=0.01395 C_L=0.6270 v= 90 C_D=0.01540 C_L=0.6778 aug C_D by vel: v= 31 C_D=0.01440 C_L=0.6032 v= 55 C_D=0.01342 C_L=0.6233 v= 65 C_D=0.01274 C_L=0.6246 v= 75 C_D=0.01230 C_L=0.6170 v= 82 C_D=0.01206 C_L=0.6102 v= 90 C_D=0.01193 C_L=0.6062
cd /app && cat > exp/run3.py << 'EOF'
import sys, time, torch, numpy as np
sys.path.insert(0,'/app/exp'); sys.path.insert(0,'/app')
from harness2 import load, train_model, evaluate, est_cd_exponent
pts,tg=load('train'); v=pts[:,0,2]
mtr=v<=60; mte=v>=70
ptr,ttr=pts[mtr],tg[mtr]; pte,tte=pts[mte],tg[mte]
print('HARD holdout: train(v<=60)=%d test(v>=70)=%d test mean v=%.1f'%(mtr.sum(),mte.sum(),v[mte].mean()))
flat=ptr.reshape(-1,4); fm=flat.mean(0); fs=flat.std(0).clamp_min(1e-8)
tm=ttr.mean(0); ts=ttr.std(0).clamp_min(1e-8)
expo=est_cd_exponent(ptr,ttr); print('exponent:',round(expo,3))
def run(cfg,seeds=(0,1,2),label=''):
accs=[]
for s in seeds:
m=train_model(ptr,ttr,fm,fs,tm,ts,cfg,seed=s)
nr,_=evaluate(m,pte,tte,fm,fs,tm,ts); accs.append(nr)
a=np.array(accs)
print(f'{label:30s} C_D={a[:,0].mean():.4f} C_L={a[:,1].mean():.4f} mean={a.mean(1).mean():.4f} worst={a.max(1).mean():.4f}')
t0=time.time()
run(dict(epochs=100),label='baseline no-aug')
for p in [0.2,0.35,0.5]:
run(dict(epochs=100,p_aug=p,vlo=30,vhi=90,cd_expo=expo),label=f'aug p={p} [30,90]')
print('elapsed %.0fs'%(time.time()-t0))
EOF
python exp/run3.py[stderr] Shell command timed out. Tool interaction exceeded 300 seconds.
restart: true
<system>tool has been restarted.</system>
cd /app && nohup python exp/run3.py > exp/run3.log 2>&1 & echo "started pid $!"
started pid 585
cd /app && sleep 240 && cat exp/run3.log
<system>Tool ran without output or errors</system>
cd /app && ls -la exp/run3.log && echo "---content---" && cat exp/run3.log && echo "---proc---" && ps aux | grep run3 | grep -v grep | head
-rw-r--r-- 1 h2tagent h2tagent 0 Jun 19 21:46 exp/run3.log ---content--- ---proc--- h2tagent 587 1437 5.0 7032844 3635268 ? Rl 21:46 58:27 python exp/run3.py
cd /app && kill 587 2>/dev/null; sleep 2; pkill -f run3.py 2>/dev/null; sleep 1
sed -i 's/seeds=(0,1,2)/seeds=(0,1)/; s/epochs=100/epochs=80/g' exp/run3.py
# add flush to prints
python - << 'EOF'
import re
s=open('exp/run3.py').read()
s=s.replace("print(f'{label:30s}","import sys as _s; print(f'{label:30s}").replace("mean={a.mean(1).mean():.4f} worst={a.max(1).mean():.4f}')","mean={a.mean(1).mean():.4f} worst={a.max(1).mean():.4f}',flush=True)")
open('exp/run3.py','w').write(s)
print('patched')
EOF
nohup python -u exp/run3.py > exp/run3.log 2>&1 &
echo "started pid $!"patched started pid 648
cd /app && sleep 200 && cat exp/run3.log
HARD holdout: train(v<=60)=413 test(v>=70)=70 test mean v=72.5 exponent: -0.189 baseline no-aug C_D=0.4775 C_L=0.2638 mean=0.3706 worst=0.4775 aug p=0.2 [30,90] C_D=0.2012 C_L=0.0700 mean=0.1356 worst=0.2012 aug p=0.35 [30,90] C_D=0.1796 C_L=0.0606 mean=0.1201 worst=0.1796
cd /app && sleep 60 && cat exp/run3.log | tail -3
aug p=0.35 [30,90] C_D=0.1796 C_L=0.0606 mean=0.1201 worst=0.1796 aug p=0.5 [30,90] C_D=0.1749 C_L=0.0474 mean=0.1111 worst=0.1749 elapsed 250s
cd /app && cat > exp/run4.py << 'EOF'
import sys, time, torch, numpy as np
sys.path.insert(0,'/app/exp'); sys.path.insert(0,'/app')
from harness2 import load, train_model, evaluate, est_cd_exponent
pts,tg=load('train'); v=pts[:,0,2]
mtr=v<=60; mte=v>=70
ptr,ttr=pts[mtr],tg[mtr]; pte,tte=pts[mte],tg[mte]
flat=ptr.reshape(-1,4); fm=flat.mean(0); fs=flat.std(0).clamp_min(1e-8)
tm=ttr.mean(0); ts=ttr.std(0).clamp_min(1e-8)
expo=est_cd_exponent(ptr,ttr)
def run(cfg,seeds=(0,1),label=''):
accs=[]
for s in seeds:
m=train_model(ptr,ttr,fm,fs,tm,ts,cfg,seed=s)
nr,_=evaluate(m,pte,tte,fm,fs,tm,ts); accs.append(nr)
a=np.array(accs)
print(f'{label:32s} C_D={a[:,0].mean():.4f} C_L={a[:,1].mean():.4f} mean={a.mean(1).mean():.4f} worst={a.max(1).mean():.4f}',flush=True)
t0=time.time()
run(dict(epochs=70,p_aug=0.5,vlo=30,vhi=95,cd_expo=expo),label='p=0.5 vhi95')
run(dict(epochs=70,p_aug=0.65,vlo=30,vhi=95,cd_expo=expo),label='p=0.65 vhi95')
run(dict(epochs=70,p_aug=0.8,vlo=30,vhi=95,cd_expo=expo),label='p=0.8 vhi95')
run(dict(epochs=70,p_aug=0.65,vlo=30,vhi=95,cd_expo=-0.21),label='p=0.65 expo-0.21')
print('elapsed %.0fs'%(time.time()-t0),flush=True)
EOF
nohup python -u exp/run4.py > exp/run4.log 2>&1 &
echo "started $!"started 703
cd /app && sleep 240 && cat exp/run4.log
p=0.5 vhi95 C_D=0.1691 C_L=0.0548 mean=0.1120 worst=0.1691 p=0.65 vhi95 C_D=0.1840 C_L=0.0591 mean=0.1215 worst=0.1840 p=0.8 vhi95 C_D=0.1687 C_L=0.0588 mean=0.1138 worst=0.1687 p=0.65 expo-0.21 C_D=0.1770 C_L=0.0587 mean=0.1178 worst=0.1770 elapsed 169s
cd /app && cat > exp/run5.py << 'EOF'
import sys, time, torch, numpy as np
sys.path.insert(0,'/app/exp'); sys.path.insert(0,'/app')
from harness2 import load, train_model, evaluate, est_cd_exponent
pts,tg=load('train'); v=pts[:,0,2]
mtr=v<=60; mte=v>=70
ptr,ttr=pts[mtr],tg[mtr]; pte,tte=pts[mte],tg[mte]
flat=ptr.reshape(-1,4); fm=flat.mean(0); fs=flat.std(0).clamp_min(1e-8)
tm=ttr.mean(0); ts=ttr.std(0).clamp_min(1e-8)
expo=est_cd_exponent(ptr,ttr)
def run(cfg,seeds=(0,1),label=''):
accs=[]
for s in seeds:
m=train_model(ptr,ttr,fm,fs,tm,ts,cfg,seed=s)
nr,_=evaluate(m,pte,tte,fm,fs,tm,ts); accs.append(nr)
a=np.array(accs)
print(f'{label:30s} C_D={a[:,0].mean():.4f} C_L={a[:,1].mean():.4f} mean={a.mean(1).mean():.4f} worst={a.max(1).mean():.4f}',flush=True)
base=dict(epochs=80,p_aug=0.5,vlo=30,vhi=92,cd_expo=expo)
t0=time.time()
run(dict(base,geo_jit=0.002),label='geo_jit0.002')
run(dict(base,target_weights=[2.0,1.0]),label='tw[2,1]')
run(dict(base,target_weights=[3.0,1.0]),label='tw[3,1]')
run(dict(base,epochs=140),label='epochs140')
print('elapsed %.0fs'%(time.time()-t0),flush=True)
EOF
nohup python -u exp/run5.py > exp/run5.log 2>&1 &
echo "started $!"started 749
cd /app && sleep 260 && cat exp/run5.log
geo_jit0.002 C_D=0.1874 C_L=0.0621 mean=0.1248 worst=0.1874 tw[2,1] C_D=0.1826 C_L=0.0557 mean=0.1192 worst=0.1826 tw[3,1] C_D=0.1844 C_L=0.0601 mean=0.1222 worst=0.1844
cd /app && sleep 45 && tail -2 exp/run5.log
epochs140 C_D=0.1444 C_L=0.0516 mean=0.0980 worst=0.1444 elapsed 286s
cd /app && cat > exp/harness3.py << 'EOF'
import json, sys, copy
import torch, numpy as np
from numpy.linalg import lstsq
sys.path.insert(0,'/app')
from candidate_model import build_model
with open('/app/task_config.json') as fh: CFG=json.load(fh)
def load(split):
p=torch.load(f'/app/data/{split}/split.pt',weights_only=True)
return p['points'].float(), (p['targets'].float() if 'targets' in p else None)
def nrmse(pred,true):
return [ (torch.sqrt(torch.mean((pred[:,i]-true[:,i])**2))/true[:,i].std()).item() for i in range(true.shape[1]) ]
def est_cd_exponent(pts,tg):
v=pts[:,0,2].numpy(); aoa=pts[:,0,3].numpy(); cd=tg[:,0].numpy(); y=pts[:,:,1].numpy()
geo=np.column_stack([y.max(1)-y.min(1),y.mean(1),y.max(1),y.min(1),aoa,aoa**2])
X=np.column_stack([np.ones_like(v),geo,np.log(v)])
beta,_,_,_=lstsq(X,np.log(cd),rcond=None); return float(beta[-1])
def _avg_state(states):
avg=copy.deepcopy(states[0])
for k in avg:
if avg[k].dtype.is_floating_point:
avg[k]=torch.stack([s[k].float() for s in states],0).mean(0)
else:
avg[k]=states[-1][k]
return avg
def train_model(pts_phys,tg_phys,fm,fs,tm,ts,cfg,seed=0):
torch.manual_seed(seed); np.random.seed(seed)
model=build_model(CFG)
epochs=cfg.get('epochs',140); bs=cfg.get('bs',32)
opt=torch.optim.Adam(model.parameters(),lr=cfg.get('lr',1e-3),weight_decay=cfg.get('wd',1e-4))
sched=torch.optim.lr_scheduler.CosineAnnealingLR(opt,T_max=epochs)
tw=torch.tensor(cfg.get('target_weights',[1.0,1.0])).float().view(1,-1)
n=pts_phys.shape[0]
p_aug=cfg.get('p_aug',0.0); vlo=cfg.get('vlo',30.0); vhi=cfg.get('vhi',92.0)
expo=cfg.get('cd_expo',-0.21); geo_jit=cfg.get('geo_jit',0.0)
swa=cfg.get('swa',False); swa_start=cfg.get('swa_start',0.7)
FM=fm.view(1,1,-1); FS=fs.view(1,1,-1); TM=tm.view(1,-1); TS=ts.view(1,-1)
swa_states=[]
def aug_batch(P,T):
if p_aug>0:
m=torch.rand(P.shape[0])<p_aug
if m.any():
vnew=torch.empty(int(m.sum())).uniform_(vlo,vhi); vorig=P[m,0,2]
P[m,:,2]=vnew.view(-1,1); T[m,0]=T[m,0]*(vnew/vorig).pow(expo)
if geo_jit>0: P[:,:,0:2]+=torch.randn_like(P[:,:,0:2])*geo_jit
return P,T
for ep in range(epochs):
model.train(); idx=torch.randperm(n)
for s in range(0,n,bs):
b=idx[s:s+bs]; P=pts_phys[b].clone(); T=tg_phys[b].clone()
P,T=aug_batch(P,T)
xb=(P-FM)/FS; yb=(T-TM)/TS
opt.zero_grad(set_to_none=True)
loss=(tw*(model(xb)-yb)**2).mean(); loss.backward(); opt.step()
sched.step()
if swa and ep>=int(swa_start*epochs):
swa_states.append({k:v.detach().clone() for k,v in model.state_dict().items()})
if swa and swa_states:
model.load_state_dict(_avg_state(swa_states))
# BN recalibration with augmentation ON (covers high-v regime)
for mod in model.modules():
if isinstance(mod,torch.nn.BatchNorm1d):
mod.reset_running_stats(); mod.momentum=None
model.train()
with torch.no_grad():
for _ in range(cfg.get('bn_passes',3)):
idx=torch.randperm(n)
for s in range(0,n,bs):
b=idx[s:s+bs]; P=pts_phys[b].clone(); T=tg_phys[b].clone()
P,T=aug_batch(P,T); model((P-FM)/FS)
return model
def evaluate(model,pts_phys,tg_phys,fm,fs,tm,ts):
model.eval()
with torch.no_grad():
pred=model((pts_phys-fm.view(1,1,-1))/fs.view(1,1,-1))*ts.view(1,-1)+tm.view(1,-1)
return nrmse(pred,tg_phys), pred
EOF
echo okok
cd /app && cat > exp/run6.py << 'EOF'
import sys, time, torch, numpy as np
sys.path.insert(0,'/app/exp'); sys.path.insert(0,'/app')
from harness3 import load, train_model, evaluate, est_cd_exponent
pts,tg=load('train'); v=pts[:,0,2]
mtr=v<=60; mte=v>=70
ptr,ttr=pts[mtr],tg[mtr]; pte,tte=pts[mte],tg[mte]
flat=ptr.reshape(-1,4); fm=flat.mean(0); fs=flat.std(0).clamp_min(1e-8)
tm=ttr.mean(0); ts=ttr.std(0).clamp_min(1e-8)
expo=est_cd_exponent(ptr,ttr)
def run(cfg,seeds=(0,1,2),label=''):
accs=[]
for s in seeds:
m=train_model(ptr,ttr,fm,fs,tm,ts,cfg,seed=s)
nr,_=evaluate(m,pte,tte,fm,fs,tm,ts); accs.append(nr)
a=np.array(accs)
print(f'{label:26s} C_D={a[:,0].mean():.4f} C_L={a[:,1].mean():.4f} mean={a.mean(1).mean():.4f} worst={a.max(1).mean():.4f} (n={len(seeds)})',flush=True)
base=dict(p_aug=0.6,vlo=30,vhi=92,cd_expo=expo)
t0=time.time()
run(dict(base,epochs=200),label='ep200')
run(dict(base,epochs=200,swa=True,swa_start=0.6),label='ep200+swa')
run(dict(base,epochs=300),label='ep300')
run(dict(base,epochs=300,swa=True,swa_start=0.6),label='ep300+swa')
print('elapsed %.0fs'%(time.time()-t0),flush=True)
EOF
nohup python -u exp/run6.py > exp/run6.log 2>&1 &
echo "started $!"started 810
cd /app && sleep 290 && cat exp/run6.log
ep200 C_D=0.1321 C_L=0.0491 mean=0.0906 worst=0.1321 (n=3)
cd /app && sleep 290 && cat exp/run6.log
ep200 C_D=0.1321 C_L=0.0491 mean=0.0906 worst=0.1321 (n=3) ep200+swa C_D=0.1419 C_L=0.0446 mean=0.0933 worst=0.1419 (n=3)
cd /app && sleep 290 && cat exp/run6.log
ep200 C_D=0.1321 C_L=0.0491 mean=0.0906 worst=0.1321 (n=3) ep200+swa C_D=0.1419 C_L=0.0446 mean=0.0933 worst=0.1419 (n=3) ep300 C_D=0.1256 C_L=0.0397 mean=0.0827 worst=0.1256 (n=3)
cd /app && sleep 200 && cat exp/run6.log | tail -2
ep200+swa C_D=0.1419 C_L=0.0446 mean=0.0933 worst=0.1419 (n=3) ep300 C_D=0.1256 C_L=0.0397 mean=0.0827 worst=0.1256 (n=3)
cd /app && sleep 120 && cat exp/run6.log | tail -2
# launch longer-epoch test in background
cat > exp/run7.py << 'EOF'
import sys, time, torch, numpy as np
sys.path.insert(0,'/app/exp'); sys.path.insert(0,'/app')
from harness3 import load, train_model, evaluate, est_cd_exponent
pts,tg=load('train'); v=pts[:,0,2]
mtr=v<=60; mte=v>=70
ptr,ttr=pts[mtr],tg[mtr]; pte,tte=pts[mte],tg[mte]
flat=ptr.reshape(-1,4); fm=flat.mean(0); fs=flat.std(0).clamp_min(1e-8)
tm=ttr.mean(0); ts=ttr.std(0).clamp_min(1e-8)
expo=est_cd_exponent(ptr,ttr)
def run(cfg,seeds=(0,1,2),label=''):
accs=[]
for s in seeds:
m=train_model(ptr,ttr,fm,fs,tm,ts,cfg,seed=s)
nr,_=evaluate(m,pte,tte,fm,fs,tm,ts); accs.append(nr)
a=np.array(accs)
print(f'{label:20s} C_D={a[:,0].mean():.4f} C_L={a[:,1].mean():.4f} mean={a.mean(1).mean():.4f} worst={a.max(1).mean():.4f} std_mean={a.mean(1).std():.4f}',flush=True)
base=dict(p_aug=0.6,vlo=30,vhi=92,cd_expo=expo)
t0=time.time()
run(dict(base,epochs=450),label='ep450')
run(dict(base,epochs=600),label='ep600')
print('elapsed %.0fs'%(time.time()-t0),flush=True)
EOF
nohup python -u exp/run7.py > exp/run7.log 2>&1 &
echo "started run7 $!"ep300+swa C_D=0.1339 C_L=0.0408 mean=0.0873 worst=0.1339 (n=3) elapsed 1205s started run7 884
cd /app && sleep 280 && cat exp/run7.log
<system>Tool ran without output or errors</system>
cd /app && sleep 200 && cat exp/run7.log
<system>Tool ran without output or errors</system>
cd /app && ps aux | grep run7 | grep -v grep | head -1 | awk '{print "running, cpu-time",$10}'; sleep 120; cat exp/run7.logrunning, cpu-time 123:07
cd /app && sleep 180 && cat exp/run7.log
ep450 C_D=0.1161 C_L=0.0388 mean=0.0774 worst=0.1161 std_mean=0.0020
cd /app && sleep 260 && cat exp/run7.log
ep450 C_D=0.1161 C_L=0.0388 mean=0.0774 worst=0.1161 std_mean=0.0020
cd /app && sleep 150 && cat exp/run7.log | tail -2
ep450 C_D=0.1161 C_L=0.0388 mean=0.0774 worst=0.1161 std_mean=0.0020
cd /app && sleep 120 && cat exp/run7.log | tail -2
ep450 C_D=0.1161 C_L=0.0388 mean=0.0774 worst=0.1161 std_mean=0.0020
cd /app && ps aux | grep run7 | grep -v grep | head -1 | awk '{print "cpu-time",$10}'; sleep 90; cat exp/run7.log | tail -2cpu-time 331:12 ep600 C_D=0.1035 C_L=0.0438 mean=0.0737 worst=0.1035 std_mean=0.0034 elapsed 1385s
cd /app && cat > exp/run8.py << 'EOF'
import sys, time, torch, numpy as np
sys.path.insert(0,'/app/exp'); sys.path.insert(0,'/app')
from harness3 import load, train_model, evaluate, est_cd_exponent
pts,tg=load('train'); v=pts[:,0,2]
mtr=v<=60; mte=v>=70
ptr,ttr=pts[mtr],tg[mtr]; pte,tte=pts[mte],tg[mte]
flat=ptr.reshape(-1,4); fm=flat.mean(0); fs=flat.std(0).clamp_min(1e-8)
tm=ttr.mean(0); ts=ttr.std(0).clamp_min(1e-8)
expo=est_cd_exponent(ptr,ttr)
def run(cfg,seeds=(0,1),label=''):
accs=[]
for s in seeds:
m=train_model(ptr,ttr,fm,fs,tm,ts,cfg,seed=s)
nr,_=evaluate(m,pte,tte,fm,fs,tm,ts); accs.append(nr)
a=np.array(accs)
print(f'{label:24s} C_D={a[:,0].mean():.4f} C_L={a[:,1].mean():.4f} mean={a.mean(1).mean():.4f} worst={a.max(1).mean():.4f}',flush=True)
base=dict(epochs=450,p_aug=0.6,vlo=30,vhi=92,cd_expo=expo)
t0=time.time()
run(dict(epochs=450,p_aug=0.0),label='CONTROL no-aug ep450')
run(dict(base,target_weights=[1.5,1.0]),label='aug tw[1.5,1]')
run(dict(base,target_weights=[2.0,1.0]),label='aug tw[2,1]')
print('elapsed %.0fs'%(time.time()-t0),flush=True)
EOF
nohup python -u exp/run8.py > exp/run8.log 2>&1 &
echo "started $!"started 999
cd /app && cat > exp/run9.py << 'EOF'
import sys, time, torch, numpy as np
sys.path.insert(0,'/app/exp'); sys.path.insert(0,'/app')
from harness3 import load, train_model, evaluate, est_cd_exponent
pts,tg=load('train'); v=pts[:,0,2]
mtr=v<=55; mte=v>=68
ptr,ttr=pts[mtr],tg[mtr]; pte,tte=pts[mte],tg[mte]
print('EXTREME holdout: train(v<=55)=%d test(v>=68)=%d test-mean-v=%.1f gap=13'%(mtr.sum(),mte.sum(),v[mte].mean()),flush=True)
flat=ptr.reshape(-1,4); fm=flat.mean(0); fs=flat.std(0).clamp_min(1e-8)
tm=ttr.mean(0); ts=ttr.std(0).clamp_min(1e-8)
expo=est_cd_exponent(ptr,ttr); print('exponent on v<=55 subset:',round(expo,3),flush=True)
def run(cfg,seeds=(0,1),label=''):
accs=[]
for s in seeds:
m=train_model(ptr,ttr,fm,fs,tm,ts,cfg,seed=s)
nr,_=evaluate(m,pte,tte,fm,fs,tm,ts); accs.append(nr)
a=np.array(accs)
print(f'{label:24s} C_D={a[:,0].mean():.4f} C_L={a[:,1].mean():.4f} mean={a.mean(1).mean():.4f}',flush=True)
t0=time.time()
run(dict(epochs=450,p_aug=0.0),label='no-aug ep450')
run(dict(epochs=450,p_aug=0.6,vlo=30,vhi=92,cd_expo=expo),label='aug ep450')
print('elapsed %.0fs'%(time.time()-t0),flush=True)
EOF
nohup python -u exp/run9.py > exp/run9.log 2>&1 &
echo "started run9 $!"; sleep 250; echo '--run8--'; cat exp/run8.log; echo '--run9--'; cat exp/run9.logstarted run9 1037 --run8-- --run9-- EXTREME holdout: train(v<=55)=337 test(v>=68)=95 test-mean-v=71.5 gap=13 exponent on v<=55 subset: -0.157
cd /app && sleep 290; echo '--run8--'; cat exp/run8.log; echo '--run9--'; cat exp/run9.log
--run8-- --run9-- EXTREME holdout: train(v<=55)=337 test(v>=68)=95 test-mean-v=71.5 gap=13 exponent on v<=55 subset: -0.157 no-aug ep450 C_D=0.4420 C_L=0.3118 mean=0.3769
cd /app && sleep 200; echo '--run8--'; cat exp/run8.log; echo '--run9--'; cat exp/run9.log
--run8-- --run9-- EXTREME holdout: train(v<=55)=337 test(v>=68)=95 test-mean-v=71.5 gap=13 exponent on v<=55 subset: -0.157 no-aug ep450 C_D=0.4420 C_L=0.3118 mean=0.3769
cd /app && ps aux | grep -E 'run8|run9' | grep -v grep | awk '{print $NF,$10}'; sleep 180; echo '--run8--'; cat exp/run8.log; echo '--run9--'; cat exp/run9.logexp/run8.py 76:55 exp/run9.py 76:13 --run8-- CONTROL no-aug ep450 C_D=0.5479 C_L=0.1857 mean=0.3668 worst=0.5479 --run9-- EXTREME holdout: train(v<=55)=337 test(v>=68)=95 test-mean-v=71.5 gap=13 exponent on v<=55 subset: -0.157 no-aug ep450 C_D=0.4420 C_L=0.3118 mean=0.3769 aug ep450 C_D=0.1524 C_L=0.0498 mean=0.1011 elapsed 897s
cd /app && sleep 180; cat exp/run8.log
CONTROL no-aug ep450 C_D=0.5479 C_L=0.1857 mean=0.3668 worst=0.5479
cd /app && sleep 200; cat exp/run8.log
CONTROL no-aug ep450 C_D=0.5479 C_L=0.1857 mean=0.3668 worst=0.5479 aug tw[1.5,1] C_D=0.1101 C_L=0.0419 mean=0.0760 worst=0.1101
cd /app && sleep 160; cat exp/run8.log | tail -2
CONTROL no-aug ep450 C_D=0.5479 C_L=0.1857 mean=0.3668 worst=0.5479 aug tw[1.5,1] C_D=0.1101 C_L=0.0419 mean=0.0760 worst=0.1101
cd /app && sleep 120; cat exp/run8.log | tail -1
elapsed 1538s
cd /app && cat exp/run8.log
CONTROL no-aug ep450 C_D=0.5479 C_L=0.1857 mean=0.3668 worst=0.5479 aug tw[1.5,1] C_D=0.1101 C_L=0.0419 mean=0.0760 worst=0.1101 aug tw[2,1] C_D=0.1112 C_L=0.0438 mean=0.0775 worst=0.1112 elapsed 1538s
cd /app && cat > final_train.py << 'EOF'
"""Final trainer for the airfoil force-coefficient task.
Key idea: the scored cases live at higher inlet velocity (Reynolds) than any
training case, so the model must EXTRAPOLATE in velocity. A plain fit latches
onto spurious nonlinear velocity features and reverses the physical trend beyond
the training range (verified empirically). We fix this with physics-based
velocity augmentation grounded in the data itself:
* C_D follows a turbulent skin-friction-like law C_D(v) ~ v^p, with the
exponent p fit from the training data (~ -0.21, matching C_f ~ Re^-0.2).
* C_L is velocity-independent (residual corr with v ~ 0.05).
During training we resample the (constant) velocity channel of a fraction of
cases across a wide range that brackets the test regime, rescaling C_D by
(v_new/v_orig)^p and leaving C_L unchanged. This teaches the fixed PointNet the
correct monotonic high-velocity behavior and, as a bonus, calibrates the
BatchNorm running statistics on high-velocity activations. The architecture is
untouched; only the training procedure and the saved normalization change.
"""
from __future__ import annotations
import json, os, sys
from pathlib import Path
import numpy as np
import torch
from numpy.linalg import lstsq
APP_DIR = Path(os.environ.get("APP_DIR", "/app"))
sys.path.insert(0, str(APP_DIR))
from candidate_model import build_model # noqa: E402
from dataset import load_split # noqa: E402
# ----- hyperparameters chosen via a velocity-gap extrapolation holdout -----
EPOCHS = 500
BATCH_SIZE = 32
LR = 1e-3
WEIGHT_DECAY= 1e-4
P_AUG = 0.6 # fraction of each batch given a resampled velocity
V_LO, V_HI = 30.0, 95.0 # brackets train (31-75) and test (75-82+) regimes
TARGET_W = [1.5, 1.0] # mild emphasis on the harder C_D target
SEED = 0
def estimate_cd_velocity_exponent(points: torch.Tensor, targets: torch.Tensor) -> float:
"""Fit C_D ~ v^p controlling for geometry and angle of attack (log-log)."""
v = points[:, 0, 2].numpy(); aoa = points[:, 0, 3].numpy()
cd = targets[:, 0].numpy(); y = points[:, :, 1].numpy()
geo = np.column_stack([y.max(1) - y.min(1), y.mean(1), y.max(1), y.min(1), aoa, aoa**2])
X = np.column_stack([np.ones_like(v), geo, np.log(v)])
beta, *_ = lstsq(X, np.log(cd), rcond=None)
return float(beta[-1])
def main() -> None:
with (APP_DIR / "task_config.json").open() as fh:
cfg = json.load(fh)
torch.manual_seed(SEED); np.random.seed(SEED)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
points, targets = load_split(APP_DIR / "data" / "train")
# standardization (raw training stats; augmentation feeds physical values
# through this same affine map so the net sees the standardized test range)
flat = points.reshape(-1, points.shape[-1])
feat_mean = flat.mean(0); feat_std = flat.std(0).clamp_min(1e-8)
targ_mean = targets.mean(0); targ_std = targets.std(0).clamp_min(1e-8)
cd_expo = estimate_cd_velocity_exponent(points, targets)
print(f"[final] n={points.shape[0]} cd_velocity_exponent={cd_expo:.3f} "
f"epochs={EPOCHS} p_aug={P_AUG} v=[{V_LO},{V_HI}] tw={TARGET_W}", flush=True)
model = build_model(cfg).to(device)
opt = torch.optim.Adam(model.parameters(), lr=LR, weight_decay=WEIGHT_DECAY)
sched = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=EPOCHS)
tw = torch.tensor(TARGET_W, device=device).float().view(1, -1)
FM = feat_mean.view(1, 1, -1).to(device); FS = feat_std.view(1, 1, -1).to(device)
TM = targ_mean.view(1, -1).to(device); TS = targ_std.view(1, -1).to(device)
pts_dev = points.to(device); tg_dev = targets.to(device)
n = pts_dev.shape[0]
for ep in range(EPOCHS):
model.train(); idx = torch.randperm(n, device=device)
for s in range(0, n, BATCH_SIZE):
b = idx[s:s + BATCH_SIZE]
P = pts_dev[b].clone(); T = tg_dev[b].clone()
…[truncated]started final 1150 written
cd /app && sleep 200 && cat final_train.log
[final] n=630 cd_velocity_exponent=-0.211 epochs=500 p_aug=0.6 v=[30.0,95.0] tw=[1.5, 1.0] [final] epoch 100/500 loss=0.0852 [final] epoch 200/500 loss=0.0131 [final] epoch 300/500 loss=0.0947
cd /app && sleep 170 && cat final_train.log && echo "---" && ls -la outputs/ 2>/dev/null
[final] n=630 cd_velocity_exponent=-0.211 epochs=500 p_aug=0.6 v=[30.0,95.0] tw=[1.5, 1.0] [final] epoch 100/500 loss=0.0852 [final] epoch 200/500 loss=0.0131 [final] epoch 300/500 loss=0.0947 [final] epoch 400/500 loss=0.0040 [final] epoch 500/500 loss=0.0496 [final] wrote /app/outputs/model.pt --- total 347 drwxr-sr-x 2 h2tagent h2tagent 4096 Jun 19 23:23 . drwxrws--- 6 h2tagent h2tagent 4096 Jun 19 23:23 .. -rw-r--r-- 1 h2tagent h2tagent 346611 Jun 19 23:23 model.pt
cd /app && cat > exp/validate_final.py << 'EOF'
import sys, json, torch, numpy as np
sys.path.insert(0,'/app')
from candidate_model import build_model
from dataset import load_split
cfg=json.load(open('/app/task_config.json'))
ckpt=torch.load('/app/outputs/model.pt', weights_only=True)
# 1. required keys and shapes
print('keys:', sorted(ckpt.keys()))
for k,exp in [('feat_mean',4),('feat_std',4),('targ_mean',2),('targ_std',2)]:
t=ckpt[k]; assert tuple(t.shape)==(exp,), (k,t.shape); print(f' {k}={t.numpy().round(4)}')
# 2. load into EXACT arch (strict), replicate inference path
model=build_model(cfg)
missing,unexpected=model.load_state_dict(ckpt['state_dict'], strict=True)
print('strict load OK; missing',missing,'unexpected',unexpected)
model.eval()
fm=ckpt['feat_mean'].view(1,1,-1); fs=ckpt['feat_std'].view(1,1,-1)
tm=ckpt['targ_mean'].view(1,-1); ts=ckpt['targ_std'].view(1,-1)
def predict(points):
with torch.no_grad():
return model((points-fm)/fs)*ts+tm
def nrmse(p,t): return [(torch.sqrt(((p[:,i]-t[:,i])**2).mean())/t[:,i].std()).item() for i in range(2)]
def nmae(p,t,i): return (torch.abs(p[:,i]-t[:,i]).mean()/torch.abs(t[:,i]).mean()).item()
for split in ['train','val']:
pts,tg=load_split(f'/app/data/{split}')
pr=predict(pts); nr=nrmse(pr,tg)
print(f'{split}: NRMSE C_D={nr[0]:.4f} C_L={nr[1]:.4f} mean={np.mean(nr):.4f} | cd_nmae={nmae(pr,tg,0):.4f}')
# 3. public_test: pipeline runs, predictions sane, range coverage
pts,_=load_split('/app/data/public_test')
pr=predict(pts)
print('public_test v-range: [%.1f,%.1f]'%(pts[:,0,2].min(),pts[:,0,2].max()))
print('pred C_D min/mean/max: %.4f/%.4f/%.4f'%(pr[:,0].min(),pr[:,0].mean(),pr[:,0].max()))
print('pred C_L min/mean/max: %.4f/%.4f/%.4f'%(pr[:,1].min(),pr[:,1].mean(),pr[:,1].max()))
assert torch.isfinite(pr).all(); assert (pr[:,0]>0).all(), 'C_D must be positive'
print('public_test predictions finite and C_D>0: OK')
# 4. velocity-extrapolation probe on real test geometries
base=pts[:40].clone()
print('velocity sweep (avg over 40 test shapes):')
prev=None
for vv in [31,50,65,75,80,85,92]:
b=base.clone(); b[:,:,2]=vv
p=predict(b); cd=p[:,0].mean().item()
mono='' if prev is None else (' dec' if cd<prev else ' INC!')
print(' v=%3d C_D=%.5f C_L=%.4f%s'%(vv,cd,p[:,1].mean(),mono)); prev=cd
EOF
python exp/validate_final.pykeys: ['feat_mean', 'feat_std', 'state_dict', 'targ_mean', 'targ_std'] feat_mean=[3.83500e-01 7.90000e-03 5.29737e+01 7.64000e-02] feat_std=[ 0.3956 0.0335 12.7859 0.0956] targ_mean=[0.0128 0.6596] targ_std=[0.0051 0.5762] strict load OK; missing [] unexpected [] train: NRMSE C_D=0.1000 C_L=0.0350 mean=0.0675 | cd_nmae=0.0139 val: NRMSE C_D=0.0759 C_L=0.0374 mean=0.0566 | cd_nmae=0.0167 public_test v-range: [75.0,81.6] pred C_D min/mean/max: 0.0072/0.0118/0.0291 pred C_L min/mean/max: -0.5342/0.6601/1.6965 public_test predictions finite and C_D>0: OK velocity sweep (avg over 40 test shapes): v= 31 C_D=0.01483 C_L=0.7060 v= 50 C_D=0.01351 C_L=0.7077 dec v= 65 C_D=0.01279 C_L=0.7117 dec v= 75 C_D=0.01241 C_L=0.7109 dec v= 80 C_D=0.01224 C_L=0.7098 dec v= 85 C_D=0.01209 C_L=0.7082 dec v= 92 C_D=0.01189 C_L=0.7060 dec
cd /app && python -c "
import torch, sys, json
sys.path.insert(0,'/app'); from candidate_model import build_model
from dataset import load_split
cfg=json.load(open('/app/task_config.json'))
ck=torch.load('/app/outputs/model.pt', weights_only=True)
# dtypes + finiteness of norm tensors
for k in ['feat_mean','feat_std','targ_mean','targ_std']:
t=ck[k]; assert t.dtype==torch.float32, (k,t.dtype); assert torch.isfinite(t).all()
assert (ck['feat_std']>0).all() and (ck['targ_std']>0).all(), 'std must be positive'
# state_dict finite + BN running stats present & populated
sd=ck['state_dict']; nbn=0
for k,v in sd.items():
assert torch.isfinite(v).all(), k
if 'running_mean' in k or 'running_var' in k: nbn+=1
print('state_dict tensors:',len(sd),'| BN running-stat tensors:',nbn,'| all finite: OK')
# determinism: two eval passes identical
m=build_model(cfg); m.load_state_dict(sd); m.eval()
pts,_=load_split('/app/data/public_test')
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():
a=m((pts-fm)/fs)*ts+tm; b=m((pts-fm)/fs)*ts+tm
assert torch.equal(a,b), 'non-deterministic!'
# batch-size invariance (BN in eval uses running stats -> per-sample independent)
with torch.no_grad():
single=torch.stack([ (m((pts[i:i+1]-fm)/fs)*ts+tm)[0] for i in range(5)])
assert torch.allclose(single, a[:5], atol=1e-5), 'batch dependence!'
print('determinism + batch-size invariance: OK')
print('file size bytes:', __import__('os').path.getsize('/app/outputs/model.pt'))
"state_dict tensors: 27 | BN running-stat tensors: 6 | all finite: OK determinism + batch-size invariance: OK file size bytes: 346611
cd /app && pkill -f 'exp/run' 2>/dev/null; pkill -f final_train 2>/dev/null; sleep 1
echo "=== Deliverable ===" && ls -la /app/outputs/model.pt
python -c "
import torch
ck=torch.load('/app/outputs/model.pt', weights_only=True)
print('keys:', sorted(ck))
print('feat_mean', ck['feat_mean'].tolist())
print('feat_std ', ck['feat_std'].tolist())
print('targ_mean', ck['targ_mean'].tolist())
print('targ_std ', ck['targ_std'].tolist())
print('state_dict entries:', len(ck['state_dict']))
"
echo "=== no stray training procs ===" && (ps aux | grep -E 'run[0-9]|final_train' | grep -v grep | wc -l)=== Deliverable === -rw-r--r-- 1 h2tagent h2tagent 346611 Jun 19 23:23 /app/outputs/model.pt keys: ['feat_mean', 'feat_std', 'state_dict', 'targ_mean', 'targ_std'] feat_mean [0.3835304379463196, 0.007914070971310139, 52.973655700683594, 0.0763944685459137] feat_std [0.3955599069595337, 0.03345881402492523, 12.785883903503418, 0.09557002782821655] targ_mean [0.012763436883687973, 0.6596193313598633] targ_std [0.0051038553938269615, 0.5761952996253967] state_dict entries: 27 === no stray training procs === 0
No file changes, the agent left the workspace untouched.
# SyncVals normalized verifier summary # task: airfrans-high-reynolds-drag-extrapolation # attempt: 10 status: completed raw_score: 0.49 # reward rule: continuous: score 0.49 < 0.5 # => reward = 0.0 (FAIL) # # 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). ---------------------------------------------------------------------- === Deliverable === -rw-r--r-- 1 h2tagent h2tagent 346611 Jun 19 23:23 /app/outputs/model.pt keys: ['feat_mean', 'feat_std', 'state_dict', 'targ_mean', 'targ_std'] feat_mean [0.3835304379463196, 0.007914070971310139, 52.973655700683594, 0.0763944685459137] feat_std [0.3955599069595337, 0.03345881402492523, 12.785883903503418, 0.09557002782821655] targ_mean [0.012763436883687973, 0.6596193313598633] targ_std [0.0051038553938269615, 0.5761952996253967] state_dict entries: 27 === no stray training procs === 0
Reproduce this trial: git checkout 2f94510 && PYTHONPATH=src python3 scripts/build_site.py , then open trial/trial_a2944c69b75046ab. Re-running the agent live requires EVAL_PLATFORM_ENABLE_OAUTH_SMOKE=1 and is non-deterministic.
Trial trial_a2944c69b75046ab · verifier authoritative; classifier explanatory.