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 && python3 -c "
import torch
for split in ['train','val','public_test']:
p = f'data/{split}/split.pt'
d = torch.load(p, weights_only=True)
print('===', split, '===')
print('keys:', list(d.keys()))
pts = d['points']
print('points shape/dtype:', pts.shape, pts.dtype)
if 'targets' in d:
t = d['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 && python3 -c "
import torch
def stats(name, pts, t=None):
print('===', name, '===')
# per-channel stats over all points
flat = pts.reshape(-1,4)
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}')
# velocity and aoa are per-case constant; get per-case values
vel = pts[:,0,2]
aoa = pts[:,0,3]
print(f' per-case vel: min={vel.min():.3f} max={vel.max():.3f} mean={vel.mean():.3f}')
print(f' per-case aoa: min={aoa.min():.4f} max={aoa.max():.4f} mean={aoa.mean():.4f}')
if t is not None:
for i,nm in enumerate(['C_D','C_L']):
c=t[:,i]
print(f' {nm}: min={c.min():.5f} max={c.max():.5f} mean={c.mean():.5f} std={c.std():.5f}')
for split in ['train','val','public_test']:
d = torch.load(f'data/{split}/split.pt', weights_only=True)
stats(split, d['points'], d.get('targets'))
"=== train === 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.283 max=74.924 mean=52.974 per-case aoa: min=-0.0862 max=0.2565 mean=0.0764 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 === val === 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.468 max=74.162 mean=54.977 per-case aoa: min=-0.0836 max=0.2512 mean=0.0799 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 === public_test === 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.030 max=81.593 mean=78.209 per-case aoa: min=-0.0857 max=0.2582 mean=0.0757
cd /app && python3 -c "
import torch, numpy as np
d = torch.load('data/train/split.pt', weights_only=True)
pts, t = d['points'], d['targets']
vel = pts[:,0,2].numpy(); aoa = pts[:,0,3].numpy()
cd = t[:,0].numpy(); cl = t[:,1].numpy()
import numpy as np
print('corr(vel, C_D):', np.corrcoef(vel, cd)[0,1])
print('corr(vel, C_L):', np.corrcoef(vel, cl)[0,1])
print('corr(aoa, C_D):', np.corrcoef(aoa, cd)[0,1])
print('corr(aoa, C_L):', np.corrcoef(aoa, cl)[0,1])
# Multi-linear regression C ~ aoa, aoa^2, vel
X = np.stack([np.ones_like(aoa), aoa, aoa**2, vel], axis=1)
for nm, y in [('C_D', cd), ('C_L', cl)]:
coef, res, *_ = np.linalg.lstsq(X, y, rcond=None)
pred = X@coef
ss = 1 - ((y-pred)**2).sum()/((y-y.mean())**2).sum()
print(f'{nm}: coefs(1,aoa,aoa^2,vel)=', np.round(coef,6), 'R2=', round(ss,4))
"corr(vel, C_D): -0.15417731232687426 corr(vel, C_L): -0.022042552660221815 corr(aoa, C_D): 0.738708826123821 corr(aoa, C_L): 0.9433278860776938 C_D: coefs(1,aoa,aoa^2,vel)= [ 1.22060e-02 -4.53900e-03 2.65212e-01 -5.80000e-05] R2= 0.7616 C_L: coefs(1,aoa,aoa^2,vel)= [ 2.508290e-01 6.184932e+00 -3.041384e+00 -3.430000e-04] R2= 0.8919
cd /app && python3 -c "
import torch
print('cuda:', torch.cuda.is_available())
print('torch:', torch.__version__)
import os; print('cpus:', os.cpu_count())
" && echo "---" && free -h 2>/dev/null | head -2; nvidia-smi -L 2>/dev/null || echo "no gpu"cuda: False
torch: 2.5.1+cu124
cpus: 16
---
total used free shared buff/cache available
Mem: 69Gi 68Mi 68Gi 0B 29Mi 68Gi
no gpucd /app && python3 -c "
import torch, numpy as np
d = torch.load('data/train/split.pt', weights_only=True)
vel = d['points'][:,0,2].numpy()
print('vel percentiles:')
for p in [0,10,25,50,75,90,95,100]:
print(f' {p}%: {np.percentile(vel,p):.2f}')
print('counts by band:')
for lo,hi in [(31,50),(50,60),(60,65),(65,70),(70,75)]:
print(f' [{lo},{hi}): {((vel>=lo)&(vel<hi)).sum()}')
print('n with vel>=65:', (vel>=65).sum())
print('n with vel>=68:', (vel>=68).sum())
print('n with vel>=70:', (vel>=70).sum())
"vel 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 by band: [31,50): 272 [50,60): 141 [60,65): 69 [65,70): 78 [70,75): 70 n with vel>=65: 148 n with vel>=68: 95 n with vel>=70: 70
cd /app && python3 -c "
import torch, numpy as np
d = torch.load('data/train/split.pt', weights_only=True)
pts, t = d['points'], d['targets']
geo = pts[:,:,:2].reshape(pts.shape[0],-1).numpy() # x,y only
# round to reduce float noise
key = np.round(geo,4)
import hashlib
hashes = [hashlib.md5(k.tobytes()).hexdigest() for k in key]
from collections import defaultdict
groups = defaultdict(list)
for i,h in enumerate(hashes): groups[h].append(i)
sizes = [len(v) for v in groups.values()]
print('n unique geometries:', len(groups), 'of', len(hashes))
print('group size distribution:', {s:sizes.count(s) for s in sorted(set(sizes))})
# Show a few multi-velocity groups
multi = [v for v in groups.values() if len(v)>=3][:5]
vel = pts[:,0,2].numpy(); aoa = pts[:,0,3].numpy(); cd=t[:,0].numpy(); cl=t[:,1].numpy()
for g in multi:
g=sorted(g, key=lambda i: vel[i])
print('--- group ---')
for i in g:
print(f' vel={vel[i]:.2f} aoa={aoa[i]:.4f} C_D={cd[i]:.5f} C_L={cl[i]:.4f}')
"n unique geometries: 630 of 630
group size distribution: {1: 630}cd /app && cat > harness.py << 'PYEOF'
"""Experiment harness for the airfoil force-coefficient extrapolation task."""
from __future__ import annotations
import json, os, time, math
from pathlib import Path
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
APP = Path('/app')
import sys; sys.path.insert(0, str(APP))
from candidate_model import build_model
torch.set_num_threads(16)
with (APP/'task_config.json').open() as fh:
CFG = json.load(fh)
def load(split):
d = torch.load(APP/'data'/split/'split.pt', weights_only=True)
return d['points'].float(), (d['targets'].float() if 'targets' in d else None)
TRAIN_PTS, TRAIN_T = load('train')
VAL_PTS, VAL_T = load('val')
def vel_split(pts, thr_lo, thr_hi):
"""indices for subtrain (vel<thr_lo) and holdout (vel>=thr_hi)."""
v = pts[:,0,2]
return torch.where(v < thr_lo)[0], torch.where(v >= thr_hi)[0]
def nrmse(pred, true):
# per-target RMSE normalized by std of true targets on this eval set
rmse = torch.sqrt(((pred-true)**2).mean(0))
std = true.std(0).clamp_min(1e-8)
return (rmse/std)
def evaluate(model, pts, true, feat_mean, feat_std, targ_mean, targ_std):
model.eval()
with torch.no_grad():
x = (pts - feat_mean.view(1,1,-1))/feat_std.view(1,1,-1)
out = model(x)
pred = out*targ_std.view(1,-1) + targ_mean.view(1,-1)
n = nrmse(pred, true)
mae = (pred-true).abs().mean(0)
return {'nrmse_cd': n[0].item(), 'nrmse_cl': n[1].item(),
'mean_nrmse': n.mean().item(), 'worst_nrmse': n.max().item(),
'cd_mae': mae[0].item(), 'cl_mae': mae[1].item()}, pred
def train_model(train_pts, train_t, opts, seed=0):
torch.manual_seed(seed); np.random.seed(seed)
dev = 'cpu'
# ---- feature normalization (velocity can be overridden for extended range)
flat = train_pts.reshape(-1,4)
fm = flat.mean(0).clone(); fs = flat.std(0).clamp_min(1e-8).clone()
if opts.get('vel_meanstd'):
fm[2], fs[2] = opts['vel_meanstd']
if opts.get('vel_suppress'):
fs[2] = fs[2]*opts['vel_suppress']
tm = train_t.mean(0).clone(); ts = train_t.std(0).clamp_min(1e-8).clone()
if opts.get('targ_log_cd'):
pass
model = build_model(CFG).to(dev)
epochs = opts.get('epochs', 120)
bs = opts.get('bs', 32)
lr = opts.get('lr', 1e-3)
wd = opts.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 = train_pts.shape[0]
va = opts.get('vel_aug'); geo_j = opts.get('geo_jitter',0.0)
loss_type = opts.get('loss','mse'); huber_d = opts.get('huber_delta',1.0)
w = torch.tensor(opts.get('tw',[1.0,1.0]))
for ep in range(epochs):
model.train()
perm = torch.randperm(n)
for s in range(0,n,bs):
idx = perm[s:s+bs]
xb = train_pts[idx].clone(); yb = train_t[idx].clone()
# velocity augmentation (invariance / extend)
if va:
b = xb.shape[0]
if va['mode']=='extend':
p = va.get('p',1.0)
m = torch.rand(b) < p
newv = torch.empty(b).uniform_(va['lo'], va['hi'])
xb[m,:,2] = newv[m].view(-1,1)
elif va['mode']=='jitter':
p = va.get('p',1.0)
m = (torch.rand(b) < p).float().view(-1,1)
fac = torch.empty(b,1).uniform_(1-va['rel'],1+va['rel'])
xb[:,:,2] = xb[:,:,2]*(1+m*(fac-1))
if geo_j>0:
xb[:,:,:2] = xb[:,:,:2] + torch.randn_like(xb[:,:,:2])*geo_j
x = (xb - fm.view(1,1,-1))/fs.view(1,1,-1)
y = (yb - tm.view(1,-1))/ts.view(1,-1)
opt.zero_grad(set_to_none=True)
out = model(x)
if loss_type=='mse':
loss = (w*( (out-y)**2 )).mean()
else:
…[truncated]harness written
cd /app && python3 -c "
import torch, numpy as np
from harness import TRAIN_PTS, TRAIN_T, vel_split, nrmse
sub,hold = vel_split(TRAIN_PTS,65,65)
tt=TRAIN_T[sub]; et=TRAIN_T[hold]
# trivial: predict train mean
pred = tt.mean(0,keepdim=True).repeat(et.shape[0],1)
n=nrmse(pred,et); print('MEAN-predictor : mean_nrmse=%.4f cd=%.4f cl=%.4f'%(n.mean(),n[0],n[1]))
# physics linear on aoa (ignore velocity -> extrapolates trivially)
aoa_s=TRAIN_PTS[sub,0,3].numpy(); aoa_h=TRAIN_PTS[hold,0,3].numpy()
Xs=np.stack([np.ones_like(aoa_s),aoa_s,aoa_s**2],1); Xh=np.stack([np.ones_like(aoa_h),aoa_h,aoa_h**2],1)
pr=[]
for i in range(2):
c,*_=np.linalg.lstsq(Xs,tt[:,i].numpy(),rcond=None); pr.append(Xh@c)
pr=torch.tensor(np.stack(pr,1),dtype=torch.float32)
n=nrmse(pr,et); print('PHYS-linear(aoa) : mean_nrmse=%.4f cd=%.4f cl=%.4f'%(n.mean(),n[0],n[1]))
"MEAN-predictor : mean_nrmse=1.0186 cd=1.0387 cl=0.9985 PHYS-linear(aoa) : mean_nrmse=0.4096 cd=0.5138 cl=0.3054
cd /app && python3 -c "
from harness import run
import time
base = dict(epochs=120, bs=32, lr=1e-3, wd=1e-4, loss='mse')
print('A. starter-style (40ep, plain):')
run(dict(base, epochs=40))
print('B. longer 120ep cosine:')
run(base)
print('C. vel suppressed x8 (nearly ignore velocity):')
run(dict(base, vel_suppress=8.0))
print('D. vel invariance extend [30,95] p=0.7:')
run(dict(base, vel_aug={'mode':'extend','lo':30,'hi':95,'p':0.7}, vel_meanstd=(62.0,18.0)))
print('E. vel jitter rel=0.25 p=0.8:')
run(dict(base, vel_aug={'mode':'jitter','rel':0.25,'p':0.8}))
" 2>&1 | grep -v WarningA. starter-style (40ep, plain): train_n=482 hold_n=148 | mean_nrmse=0.2079 cd=0.2364 cl=0.1793 worst=0.2364 cd_mae=0.00071 B. longer 120ep cosine: train_n=482 hold_n=148 | mean_nrmse=0.1473 cd=0.1841 cl=0.1105 worst=0.1841 cd_mae=0.00060 C. vel suppressed x8 (nearly ignore velocity): train_n=482 hold_n=148 | mean_nrmse=0.4611 cd=0.8351 cl=0.0871 worst=0.8351 cd_mae=0.00076 D. vel invariance extend [30,95] p=0.7: train_n=482 hold_n=148 | mean_nrmse=0.3482 cd=0.5927 cl=0.1037 worst=0.5927 cd_mae=0.00121 E. vel jitter rel=0.25 p=0.8: train_n=482 hold_n=148 | mean_nrmse=0.2390 cd=0.3595 cl=0.1184 worst=0.3595 cd_mae=0.00068
cd /app && python3 -c "
from harness import run
print('Harder proxy: train vel<55, holdout vel>=68 (gap, further extrapolation)')
base = dict(epochs=120, bs=32, lr=1e-3, wd=1e-4, loss='mse')
print('B plain:')
run(base, thr_lo=55, thr_hi=68)
print('E jitter .25:')
run(dict(base, vel_aug={'mode':'jitter','rel':0.25,'p':0.8}), thr_lo=55, thr_hi=68)
print('C suppress x8:')
run(dict(base, vel_suppress=8.0), thr_lo=55, thr_hi=68)
print('D extend invariance:')
run(dict(base, vel_aug={'mode':'extend','lo':30,'hi':95,'p':0.7}, vel_meanstd=(62.0,18.0)), thr_lo=55, thr_hi=68)
" 2>&1 | grep -v WarningHarder proxy: train vel<55, holdout vel>=68 (gap, further extrapolation) B plain: train_n=337 hold_n=95 | mean_nrmse=0.4505 cd=0.6330 cl=0.2680 worst=0.6330 cd_mae=0.00215 E jitter .25: train_n=337 hold_n=95 | mean_nrmse=0.2503 cd=0.3365 cl=0.1640 worst=0.3365 cd_mae=0.00106 C suppress x8: train_n=337 hold_n=95 | mean_nrmse=0.3064 cd=0.5346 cl=0.0783 worst=0.5346 cd_mae=0.00101 D extend invariance: train_n=337 hold_n=95 | mean_nrmse=0.2511 cd=0.4471 cl=0.0552 worst=0.4471 cd_mae=0.00141
cd /app && python3 -c "
import torch, numpy as np
from harness import TRAIN_PTS, TRAIN_T, vel_split, train_model
base = dict(epochs=120, bs=32, lr=1e-3, wd=1e-4, loss='mse')
sub,hold = vel_split(TRAIN_PTS,55,68)
tp,tt=TRAIN_PTS[sub],TRAIN_T[sub]
# pick 12 holdout cases, sweep velocity, see predicted C_D/C_L response
probe = TRAIN_PTS[hold][:12].clone()
def sweep(model,fm,fs,tm,ts,label):
model.eval()
vs=[30,45,55,65,75,85,95]
print(label)
with torch.no_grad():
for v in vs:
pp=probe.clone(); pp[:,:,2]=v
x=(pp-fm.view(1,1,-1))/fs.view(1,1,-1)
out=model(x)*ts.view(1,-1)+tm.view(1,-1)
print(f' v={v:2d}: meanC_D={out[:,0].mean():.5f} meanC_L={out[:,1].mean():.4f}')
m,fm,fs,tm,ts=train_model(tp,tt,base,seed=0)
sweep(m,fm,fs,tm,ts,'PLAIN (trained on vel<55):')
m,fm,fs,tm,ts=train_model(tp,tt,dict(base,vel_aug={'mode':'jitter','rel':0.25,'p':0.8}),seed=0)
sweep(m,fm,fs,tm,ts,'JITTER .25:')
" 2>&1 | grep -v WarningPLAIN (trained on vel<55): v=30: meanC_D=0.01488 meanC_L=0.7259 v=45: meanC_D=0.01502 meanC_L=0.7416 v=55: meanC_D=0.01401 meanC_L=0.7518 v=65: meanC_D=0.01395 meanC_L=0.8025 v=75: meanC_D=0.01534 meanC_L=0.9077 v=85: meanC_D=0.01748 meanC_L=1.0612 v=95: meanC_D=0.01986 meanC_L=1.2388 JITTER .25: v=30: meanC_D=0.01507 meanC_L=0.7154 v=45: meanC_D=0.01467 meanC_L=0.7281 v=55: meanC_D=0.01435 meanC_L=0.7349 v=65: meanC_D=0.01422 meanC_L=0.7188 v=75: meanC_D=0.01456 meanC_L=0.7110 v=85: meanC_D=0.01521 meanC_L=0.7229 v=95: meanC_D=0.01604 meanC_L=0.7556
cd /app && python3 -c "
import torch
from harness import TRAIN_PTS, vel_split
sub,hold=vel_split(TRAIN_PTS,69,69)
print('train<69:',len(sub),' holdout>=69:',len(hold))
v=TRAIN_PTS[hold,0,2]
print('holdout vel range:',v.min().item(),v.max().item())
print('pct beyond subtrain max: min=%.1f%% max=%.1f%%'%(((v.min()-69)/69*100),((v.max()-69)/69*100)))
"train<69: 550 holdout>=69: 80 holdout vel range: 69.01799774169922 74.92400360107422 pct beyond subtrain max: min=0.0% max=8.6%
cd /app && python3 -c "
from harness import run
base = dict(epochs=120, bs=32, lr=1e-3, wd=1e-4, loss='mse')
S=(0,1)
print('Matched proxy (train<69, test>=69), 2 seeds:')
print('B plain:'); run(base, thr_lo=69, thr_hi=69, seeds=S)
print('E jitter .25 p.8:'); run(dict(base, vel_aug={'mode':'jitter','rel':0.25,'p':0.8}), thr_lo=69, thr_hi=69, seeds=S)
print('E2 jitter .15 p.8:'); run(dict(base, vel_aug={'mode':'jitter','rel':0.15,'p':0.8}), thr_lo=69, thr_hi=69, seeds=S)
print('E3 jitter .40 p.9:'); run(dict(base, vel_aug={'mode':'jitter','rel':0.40,'p':0.9}), thr_lo=69, thr_hi=69, seeds=S)
" 2>&1 | grep -v Warning[stderr] Shell command timed out. Tool interaction exceeded 300 seconds.
restart: true
<system>tool has been restarted.</system>
cd /app && timeout 120 python3 -c "
import time
from harness import run
base = dict(epochs=120, bs=32, lr=1e-3, wd=1e-4, loss='mse')
t=time.time()
run(base, thr_lo=69, thr_hi=69, seeds=(0,))
print('one 120ep run took %.1fs'%(time.time()-t))
" 2>&1 | grep -v Warningtrain_n=550 hold_n=80 | mean_nrmse=0.0954 cd=0.1120 cl=0.0789 worst=0.1120 cd_mae=0.00032 one 120ep run took 70.1s
cd /app && cat > exp1.py << 'PYEOF'
from harness import run
base = dict(epochs=120, bs=32, lr=1e-3, wd=1e-4, loss='mse')
S=(0,1)
exps = [
('B plain', dict(base)),
('E jitter.25 p.8', dict(base, vel_aug={'mode':'jitter','rel':0.25,'p':0.8})),
('E2 jitter.15 p.8', dict(base, vel_aug={'mode':'jitter','rel':0.15,'p':0.8})),
('E3 jitter.40 p.9', dict(base, vel_aug={'mode':'jitter','rel':0.40,'p':0.9})),
('F extendUp 75-92 p.5',dict(base, vel_aug={'mode':'extend','lo':75,'hi':92,'p':0.5}, vel_meanstd=(60.0,16.0))),
]
for name,opt in exps:
print(name, flush=True)
run(opt, thr_lo=69, thr_hi=69, seeds=S)
PYEOF
nohup python3 exp1.py > exp1.log 2>&1 &
echo "launched exp1 pid $!"launched exp1 pid 636
cd /app && sleep 180 && cat exp1.log 2>&1 | grep -v Warning
B plain train_n=550 hold_n=80 | mean_nrmse=0.1048 cd=0.1278 cl=0.0818 worst=0.1278 cd_mae=0.00032 E jitter.25 p.8
cd /app && sleep 240 && cat exp1.log 2>&1 | grep -v Warning
B plain train_n=550 hold_n=80 | mean_nrmse=0.1048 cd=0.1278 cl=0.0818 worst=0.1278 cd_mae=0.00032 E jitter.25 p.8 train_n=550 hold_n=80 | mean_nrmse=0.1118 cd=0.1670 cl=0.0566 worst=0.1670 cd_mae=0.00047 E2 jitter.15 p.8 train_n=550 hold_n=80 | mean_nrmse=0.1074 cd=0.1540 cl=0.0608 worst=0.1540 cd_mae=0.00045 E3 jitter.40 p.9
cd /app && sleep 170 && cat exp1.log 2>&1 | grep -v Warning && echo "=== done? ===" && (ps aux | grep -q "[e]xp1.py" && echo RUNNING || echo FINISHED)
B plain train_n=550 hold_n=80 | mean_nrmse=0.1048 cd=0.1278 cl=0.0818 worst=0.1278 cd_mae=0.00032 E jitter.25 p.8 train_n=550 hold_n=80 | mean_nrmse=0.1118 cd=0.1670 cl=0.0566 worst=0.1670 cd_mae=0.00047 E2 jitter.15 p.8 train_n=550 hold_n=80 | mean_nrmse=0.1074 cd=0.1540 cl=0.0608 worst=0.1540 cd_mae=0.00045 E3 jitter.40 p.9 train_n=550 hold_n=80 | mean_nrmse=0.1308 cd=0.2123 cl=0.0493 worst=0.2123 cd_mae=0.00058 F extendUp 75-92 p.5 === done? === RUNNING
cd /app && sleep 150 && cat exp1.log 2>&1 | grep -v Warning && (ps aux | grep -q "[e]xp1.py" && echo RUNNING || echo FINISHED)
B plain train_n=550 hold_n=80 | mean_nrmse=0.1048 cd=0.1278 cl=0.0818 worst=0.1278 cd_mae=0.00032 E jitter.25 p.8 train_n=550 hold_n=80 | mean_nrmse=0.1118 cd=0.1670 cl=0.0566 worst=0.1670 cd_mae=0.00047 E2 jitter.15 p.8 train_n=550 hold_n=80 | mean_nrmse=0.1074 cd=0.1540 cl=0.0608 worst=0.1540 cd_mae=0.00045 E3 jitter.40 p.9 train_n=550 hold_n=80 | mean_nrmse=0.1308 cd=0.2123 cl=0.0493 worst=0.2123 cd_mae=0.00058 F extendUp 75-92 p.5 train_n=550 hold_n=80 | mean_nrmse=0.1864 cd=0.3261 cl=0.0467 worst=0.3261 cd_mae=0.00087 FINISHED
cd /app && cat > probe_real.py << 'PYEOF'
import torch, numpy as np
from harness import TRAIN_PTS, TRAIN_T, VAL_PTS, VAL_T, train_model, evaluate, load
base = dict(epochs=120, bs=32, lr=1e-3, wd=1e-4, loss='mse')
PT,_ = load('public_test')
def sweep_and_eval(opt, label):
m,fm,fs,tm,ts = train_model(TRAIN_PTS, TRAIN_T, opt, seed=0)
val_metrics,_ = evaluate(m, VAL_PTS, VAL_T, fm,fs,tm,ts)
print(f'{label}: VAL(in-range) mean_nrmse={val_metrics[\"mean_nrmse\"]:.4f} cd={val_metrics[\"nrmse_cd\"]:.4f} cl={val_metrics[\"nrmse_cl\"]:.4f}')
probe = PT[:40].clone()
m.eval()
print(' vel-sweep on 40 public_test geoms (mean pred):')
with torch.no_grad():
for v in [70,75,78,82,86,90]:
pp=probe.clone(); pp[:,:,2]=v
x=(pp-fm.view(1,1,-1))/fs.view(1,1,-1)
out=m(x)*ts.view(1,-1)+tm.view(1,-1)
print(f' v={v}: C_D={out[:,0].mean():.5f} C_L={out[:,1].mean():.4f}')
# actual public_test predictions at their true velocities
with torch.no_grad():
x=(PT-fm.view(1,1,-1))/fs.view(1,1,-1)
out=m(x)*ts.view(1,-1)+tm.view(1,-1)
print(f' public_test ACTUAL preds: C_D mean={out[:,0].mean():.5f} [{out[:,0].min():.5f},{out[:,0].max():.5f}] C_L mean={out[:,1].mean():.4f} [{out[:,1].min():.4f},{out[:,1].max():.4f}]')
sweep_and_eval(dict(base), 'PLAIN')
sweep_and_eval(dict(base, vel_aug={'mode':'jitter','rel':0.15,'p':0.8}), 'JITTER.15')
PYEOF
nohup python3 probe_real.py > probe_real.log 2>&1 &
echo "launched pid $!"launched pid 731
cd /app && sleep 170 && cat probe_real.log 2>&1 | grep -v Warning && (ps aux | grep -q "[p]robe_real.py" && echo RUNNING || echo FINISHED)
File "/app/probe_real.py", line 8
print(f'{label}: VAL(in-range) mean_nrmse={val_metrics[\"mean_nrmse\"]:.4f} cd={val_metrics[\"nrmse_cd\"]:.4f} cl={val_metrics[\"nrmse_cl\"]:.4f}')
^
SyntaxError: f-string expression part cannot include a backslash
FINISHEDcd /app && cat > probe_real.py << 'PYEOF'
import torch, numpy as np
from harness import TRAIN_PTS, TRAIN_T, VAL_PTS, VAL_T, train_model, evaluate, load
base = dict(epochs=120, bs=32, lr=1e-3, wd=1e-4, loss='mse')
PT,_ = load('public_test')
def sweep_and_eval(opt, label):
m,fm,fs,tm,ts = train_model(TRAIN_PTS, TRAIN_T, opt, seed=0)
vm,_ = evaluate(m, VAL_PTS, VAL_T, fm,fs,tm,ts)
print('%s: VAL(in-range) mean=%.4f cd=%.4f cl=%.4f'%(label, vm['mean_nrmse'], vm['nrmse_cd'], vm['nrmse_cl']))
probe = PT[:40].clone(); m.eval()
print(' vel-sweep on 40 public_test geoms (mean pred):')
with torch.no_grad():
for v in [70,75,78,82,86,90]:
pp=probe.clone(); pp[:,:,2]=v
x=(pp-fm.view(1,1,-1))/fs.view(1,1,-1)
out=m(x)*ts.view(1,-1)+tm.view(1,-1)
print(' v=%d: C_D=%.5f C_L=%.4f'%(v, out[:,0].mean(), out[:,1].mean()))
with torch.no_grad():
x=(PT-fm.view(1,1,-1))/fs.view(1,1,-1)
out=m(x)*ts.view(1,-1)+tm.view(1,-1)
print(' public_test ACTUAL: C_D mean=%.5f [%.5f,%.5f] C_L mean=%.4f [%.4f,%.4f]'%(
out[:,0].mean(),out[:,0].min(),out[:,0].max(),out[:,1].mean(),out[:,1].min(),out[:,1].max()))
sweep_and_eval(dict(base), 'PLAIN')
sweep_and_eval(dict(base, vel_aug={'mode':'jitter','rel':0.15,'p':0.8}), 'JITTER.15')
PYEOF
nohup python3 probe_real.py > probe_real.log 2>&1 &
echo "launched pid $!"launched pid 751
cd /app && python3 -c "
import torch, numpy as np
d=torch.load('data/train/split.pt', weights_only=True)
pts,t=d['points'],d['targets']
vel=pts[:,0,2].numpy(); aoa=pts[:,0,3].numpy()
cd=t[:,0].numpy(); cl=t[:,1].numpy()
# partial regression: include aoa, aoa^2, vel; report vel slope
X=np.stack([np.ones_like(aoa),aoa,aoa**2,vel],1)
for nm,y in [('C_D',cd),('C_L',cl)]:
c,*_=np.linalg.lstsq(X,y,rcond=None)
print(nm,'vel slope (ctrl aoa)=%.3e'%c[3], ' per +7m/s delta=%.3e'%(c[3]*7))
print('C_D std=%.3e C_L std=%.3e'%(cd.std(),cl.std()))
" 2>&1 | grep -v WarningC_D vel slope (ctrl aoa)=-5.789e-05 per +7m/s delta=-4.052e-04 C_L vel slope (ctrl aoa)=-3.431e-04 per +7m/s delta=-2.402e-03 C_D std=5.100e-03 C_L std=5.757e-01
command: str_replace
path: /app/harness.py
old_str: if va:
b = xb.shape[0]
if va['mode']=='extend':
new_str: if va:
b = xb.shape[0]
if va['mode']=='linaug':
# sample new velocity over wide range; adjust targets by per-target slope
p = va.get('p',1.0)
m = torch.rand(b) < p
newv = torch.empty(b).uniform_(va['lo'], va['hi'])
oldv = xb[:,0,2].clone()
dv = (newv - oldv)
sl = torch.tensor(va['slopes']) # [slope_cd, slope_cl]
yb[m] = yb[m] + dv[m].view(-1,1)*sl.view(1,-1)
xb[m,:,2] = newv[m].view(-1,1)
elif va['mode']=='extend':The file /app/harness.py has been edited successfully.
cd /app && sleep 60 && cat probe_real.log 2>&1 | grep -v Warning && (ps aux | grep -q "[p]robe_real.py" && echo RUNNING || echo FINISHED)
<system>Tool ran without output or errors</system>
cd /app && cat probe_real.log && echo "===" && (ps aux | grep -q "[p]robe_real.py" && echo RUNNING || echo FINISHED)
=== RUNNING
cd /app && sleep 150 && cat probe_real.log 2>&1 | grep -v Warning && (ps aux | grep -q "[p]robe_real.py" && echo RUNNING || echo FINISHED)
PLAIN: VAL(in-range) mean=0.0825 cd=0.1212 cl=0.0439
vel-sweep on 40 public_test geoms (mean pred):
v=70: C_D=0.01245 C_L=0.7216
v=75: C_D=0.01228 C_L=0.7272
v=78: C_D=0.01225 C_L=0.7356
v=82: C_D=0.01231 C_L=0.7546
v=86: C_D=0.01248 C_L=0.7816
v=90: C_D=0.01275 C_L=0.8125
public_test ACTUAL: C_D mean=0.01181 [0.00748,0.02443] C_L mean=0.6857 [-0.4952,1.7035]
JITTER.15: VAL(in-range) mean=0.0837 cd=0.1269 cl=0.0404
vel-sweep on 40 public_test geoms (mean pred):
v=70: C_D=0.01270 C_L=0.7201
v=75: C_D=0.01253 C_L=0.7193
v=78: C_D=0.01246 C_L=0.7194
v=82: C_D=0.01242 C_L=0.7232
v=86: C_D=0.01245 C_L=0.7318
v=90: C_D=0.01255 C_L=0.7427
public_test ACTUAL: C_D mean=0.01196 [0.00735,0.02595] C_L mean=0.6717 [-0.4913,1.7113]
FINISHEDcd /app && cat > exp2.py << 'PYEOF'
from harness import run
base = dict(epochs=120, bs=32, lr=1e-3, wd=1e-4, loss='mse')
CD_SL=-5.8e-5
configs = {
'plain': dict(base),
'jit.15': dict(base, vel_aug={'mode':'jitter','rel':0.15,'p':0.8}),
'linaug_cd': dict(base, vel_aug={'mode':'linaug','lo':30,'hi':92,'p':0.8,'slopes':[CD_SL,0.0]}, vel_meanstd=(60.0,16.0)),
'linaug_both':dict(base, vel_aug={'mode':'linaug','lo':30,'hi':92,'p':0.8,'slopes':[CD_SL,-3.4e-4]}, vel_meanstd=(60.0,16.0)),
}
print('=== MATCHED proxy (train<69, test>=69), 2 seeds ===', flush=True)
for nm,opt in configs.items():
print(nm, flush=True); run(opt, thr_lo=69, thr_hi=69, seeds=(0,1))
print('=== HARDER proxy (train<55, test>=68), 1 seed ===', flush=True)
for nm,opt in configs.items():
print(nm, flush=True); run(opt, thr_lo=55, thr_hi=68, seeds=(0,))
PYEOF
nohup python3 -u exp2.py > exp2.log 2>&1 &
echo "launched pid $!"launched pid 870
cd /app && sleep 280 && cat exp2.log 2>&1 | grep -v Warning
=== MATCHED proxy (train<69, test>=69), 2 seeds === plain train_n=550 hold_n=80 | mean_nrmse=0.1048 cd=0.1278 cl=0.0818 worst=0.1278 cd_mae=0.00032 jit.15 train_n=550 hold_n=80 | mean_nrmse=0.1074 cd=0.1540 cl=0.0608 worst=0.1540 cd_mae=0.00045 linaug_cd
cd /app && sleep 220 && cat exp2.log 2>&1 | grep -v Warning
=== MATCHED proxy (train<69, test>=69), 2 seeds === plain train_n=550 hold_n=80 | mean_nrmse=0.1048 cd=0.1278 cl=0.0818 worst=0.1278 cd_mae=0.00032 jit.15 train_n=550 hold_n=80 | mean_nrmse=0.1074 cd=0.1540 cl=0.0608 worst=0.1540 cd_mae=0.00045 linaug_cd train_n=550 hold_n=80 | mean_nrmse=0.1588 cd=0.2692 cl=0.0483 worst=0.2692 cd_mae=0.00072 linaug_both
cd /app && sleep 260 && cat exp2.log 2>&1 | grep -v Warning
=== MATCHED proxy (train<69, test>=69), 2 seeds === plain train_n=550 hold_n=80 | mean_nrmse=0.1048 cd=0.1278 cl=0.0818 worst=0.1278 cd_mae=0.00032 jit.15 train_n=550 hold_n=80 | mean_nrmse=0.1074 cd=0.1540 cl=0.0608 worst=0.1540 cd_mae=0.00045 linaug_cd train_n=550 hold_n=80 | mean_nrmse=0.1588 cd=0.2692 cl=0.0483 worst=0.2692 cd_mae=0.00072 linaug_both train_n=550 hold_n=80 | mean_nrmse=0.1611 cd=0.2734 cl=0.0488 worst=0.2734 cd_mae=0.00072 === HARDER proxy (train<55, test>=68), 1 seed === plain train_n=337 hold_n=95 | mean_nrmse=0.4505 cd=0.6330 cl=0.2680 worst=0.6330 cd_mae=0.00215 jit.15 train_n=337 hold_n=95 | mean_nrmse=0.3061 cd=0.4315 cl=0.1808 worst=0.4315 cd_mae=0.00143 linaug_cd train_n=337 hold_n=95 | mean_nrmse=0.1747 cd=0.2971 cl=0.0524 worst=0.2971 cd_mae=0.00088 linaug_both train_n=337 hold_n=95 | mean_nrmse=0.1742 cd=0.2874 cl=0.0610 worst=0.2874 cd_mae=0.00088
command: str_replace
path: /app/harness.py
old_str: elif va['mode']=='extend':
new_str: elif va['mode']=='shiftup':
# push a fraction of samples UP by a positive delta into the test band;
# keep the rest at real velocity. Adjust targets by per-target slopes.
p = va.get('p',0.5)
m = torch.rand(b) < p
delta = torch.empty(b).uniform_(0.0, va['dmax'])
oldv = xb[:,0,2].clone()
newv = (oldv + delta).clamp(max=va.get('cap',95.0))
realdelta = newv - oldv
sl = torch.tensor(va['slopes'])
yb[m] = yb[m] + realdelta[m].view(-1,1)*sl.view(1,-1)
xb[m,:,2] = newv[m].view(-1,1)
# optional mild symmetric jitter on all for local flatness
if va.get('rel',0)>0:
fac = torch.empty(b,1).uniform_(1-va['rel'],1+va['rel'])
xb[:,:,2] = xb[:,:,2]*fac
elif va['mode']=='extend':The file /app/harness.py has been edited successfully.
cd /app && cat > exp3.py << 'PYEOF'
from harness import run
base = dict(epochs=120, bs=32, lr=1e-3, wd=1e-4, loss='mse')
CD=-5.8e-5
cfgs = {
'jit.10': dict(base, vel_aug={'mode':'jitter','rel':0.10,'p':0.8}),
'jit.20': dict(base, vel_aug={'mode':'jitter','rel':0.20,'p':0.8}),
'shiftup_d18_cd': dict(base, vel_aug={'mode':'shiftup','dmax':18,'p':0.5,'slopes':[CD,0.0]}),
'shiftup_d25_cd_j08': dict(base, vel_aug={'mode':'shiftup','dmax':25,'p':0.5,'slopes':[CD,0.0],'rel':0.08}),
}
print('=== MATCHED (train<69,test>=69) 2 seeds ===', flush=True)
for n,o in cfgs.items():
print(n, flush=True); run(o, thr_lo=69, thr_hi=69, seeds=(0,1))
print('=== HARDER (train<55,test>=68) 1 seed ===', flush=True)
for n,o in cfgs.items():
print(n, flush=True); run(o, thr_lo=55, thr_hi=68, seeds=(0,))
PYEOF
nohup python3 -u exp3.py > exp3.log 2>&1 &
echo "launched pid $!"launched pid 962
command: str_replace
path: /app/harness.py
old_str: n = train_pts.shape[0]
va = opts.get('vel_aug'); geo_j = opts.get('geo_jitter',0.0)
loss_type = opts.get('loss','mse'); huber_d = opts.get('huber_delta',1.0)
w = torch.tensor(opts.get('tw',[1.0,1.0]))
new_str: n = train_pts.shape[0]
va = opts.get('vel_aug'); geo_j = opts.get('geo_jitter',0.0)
loss_type = opts.get('loss','mse'); huber_d = opts.get('huber_delta',1.0)
w = torch.tensor(opts.get('tw',[1.0,1.0]))
swa = opts.get('swa'); swa_state=None; swa_cnt=0
swa_start = int(epochs*swa.get('start_frac',0.6)) if swa else epochs+1The file /app/harness.py has been edited successfully.
command: str_replace
path: /app/harness.py
old_str: loss.backward(); opt.step()
sched.step()
return model, fm, fs, tm, ts
new_str: loss.backward(); opt.step()
sched.step()
if swa and ep>=swa_start:
sd = {k: v.detach().clone().float() for k,v in model.state_dict().items()}
if swa_state is None:
swa_state = sd; swa_cnt=1
else:
swa_cnt+=1
for k in swa_state:
if torch.is_floating_point(swa_state[k]):
swa_state[k] += (sd[k]-swa_state[k])/swa_cnt
if swa and swa_state is not None:
model.load_state_dict(swa_state)
# recompute BatchNorm running stats with averaged weights
for m_ in model.modules():
if isinstance(m_, nn.BatchNorm1d):
m_.reset_running_stats(); m_.momentum=None
model.train()
with torch.no_grad():
for s in range(0,n,bs):
xb = train_pts[s:s+bs]
x = (xb - fm.view(1,1,-1))/fs.view(1,1,-1)
model(x)
model.eval()
return model, fm, fs, tm, tsThe file /app/harness.py has been edited successfully.
cd /app && sleep 285 && cat exp3.log 2>&1 | grep -v Warning
=== MATCHED (train<69,test>=69) 2 seeds === jit.10 train_n=550 hold_n=80 | mean_nrmse=0.1066 cd=0.1443 cl=0.0688 worst=0.1443 cd_mae=0.00043 jit.20 train_n=550 hold_n=80 | mean_nrmse=0.1098 cd=0.1626 cl=0.0570 worst=0.1626 cd_mae=0.00046 shiftup_d18_cd
cd /app && sleep 250 && cat exp3.log 2>&1 | grep -v Warning
=== MATCHED (train<69,test>=69) 2 seeds === jit.10 train_n=550 hold_n=80 | mean_nrmse=0.1066 cd=0.1443 cl=0.0688 worst=0.1443 cd_mae=0.00043 jit.20 train_n=550 hold_n=80 | mean_nrmse=0.1098 cd=0.1626 cl=0.0570 worst=0.1626 cd_mae=0.00046 shiftup_d18_cd train_n=550 hold_n=80 | mean_nrmse=0.1058 cd=0.1633 cl=0.0483 worst=0.1633 cd_mae=0.00046 shiftup_d25_cd_j08 train_n=550 hold_n=80 | mean_nrmse=0.1526 cd=0.2402 cl=0.0651 worst=0.2402 cd_mae=0.00057 === HARDER (train<55,test>=68) 1 seed === jit.10
cd /app && sleep 255 && cat exp3.log 2>&1 | grep -v Warning
=== MATCHED (train<69,test>=69) 2 seeds === jit.10 train_n=550 hold_n=80 | mean_nrmse=0.1066 cd=0.1443 cl=0.0688 worst=0.1443 cd_mae=0.00043 jit.20 train_n=550 hold_n=80 | mean_nrmse=0.1098 cd=0.1626 cl=0.0570 worst=0.1626 cd_mae=0.00046 shiftup_d18_cd train_n=550 hold_n=80 | mean_nrmse=0.1058 cd=0.1633 cl=0.0483 worst=0.1633 cd_mae=0.00046 shiftup_d25_cd_j08 train_n=550 hold_n=80 | mean_nrmse=0.1526 cd=0.2402 cl=0.0651 worst=0.2402 cd_mae=0.00057 === HARDER (train<55,test>=68) 1 seed === jit.10 train_n=337 hold_n=95 | mean_nrmse=0.3758 cd=0.4970 cl=0.2546 worst=0.4970 cd_mae=0.00172 jit.20 train_n=337 hold_n=95 | mean_nrmse=0.2761 cd=0.3835 cl=0.1686 worst=0.3835 cd_mae=0.00128 shiftup_d18_cd train_n=337 hold_n=95 | mean_nrmse=0.2110 cd=0.3182 cl=0.1037 worst=0.3182 cd_mae=0.00073 shiftup_d25_cd_j08 train_n=337 hold_n=95 | mean_nrmse=0.1508 cd=0.2280 cl=0.0736 worst=0.2280 cd_mae=0.00071
cd /app && cat > exp4.py << 'PYEOF'
from harness import run
base = dict(epochs=160, bs=32, lr=1e-3, wd=1e-4, loss='mse')
CD=-5.8e-5
su = {'mode':'shiftup','dmax':18,'p':0.5,'slopes':[CD,0.0]}
swa = {'start_frac':0.55}
print('shiftup+SWA MATCHED 2 seeds', flush=True)
run(dict(base, vel_aug=su, swa=swa), thr_lo=69, thr_hi=69, seeds=(0,1))
print('shiftup+SWA HARDER 1 seed', flush=True)
run(dict(base, vel_aug=su, swa=swa), thr_lo=55, thr_hi=68, seeds=(0,))
print('shiftup+SWA VAL(in-range) 1 seed', flush=True)
run(dict(base, vel_aug=su, swa=swa), proxy=False, seeds=(0,))
print('shiftup NOSWA VAL(in-range) 1 seed', flush=True)
run(dict(base, vel_aug=su), proxy=False, seeds=(0,))
print('plain NOSWA VAL(in-range) 1 seed', flush=True)
run(dict(base), proxy=False, seeds=(0,))
PYEOF
nohup python3 -u exp4.py > exp4.log 2>&1 &
echo "launched pid $!"launched pid 1066
cd /app && sleep 200 && cat exp4.log 2>&1 | grep -v Warning
shiftup+SWA MATCHED 2 seeds train_n=550 hold_n=80 | mean_nrmse=0.1102 cd=0.1708 cl=0.0495 worst=0.1708 cd_mae=0.00061 shiftup+SWA HARDER 1 seed
cd /app && sleep 220 && cat exp4.log 2>&1 | grep -v Warning
shiftup+SWA MATCHED 2 seeds train_n=550 hold_n=80 | mean_nrmse=0.1102 cd=0.1708 cl=0.0495 worst=0.1708 cd_mae=0.00061 shiftup+SWA HARDER 1 seed train_n=337 hold_n=95 | mean_nrmse=0.1691 cd=0.2293 cl=0.1090 worst=0.2293 cd_mae=0.00066 shiftup+SWA VAL(in-range) 1 seed train_n=630 hold_n=80 | mean_nrmse=0.0875 cd=0.1340 cl=0.0410 worst=0.1340 cd_mae=0.00039 shiftup NOSWA VAL(in-range) 1 seed train_n=630 hold_n=80 | mean_nrmse=0.0962 cd=0.1532 cl=0.0392 worst=0.1532 cd_mae=0.00036 plain NOSWA VAL(in-range) 1 seed train_n=630 hold_n=80 | mean_nrmse=0.0892 cd=0.1303 cl=0.0480 worst=0.1303 cd_mae=0.00033
command: create
path: /app/final_train.py
file_text: """Final trainer for the airfoil force-coefficient extrapolation task.
Strategy (justified by extrapolation-proxy experiments, see notes):
* The scored cases sit ABOVE the training inlet-velocity range (train<=75 m/s,
test 75-82+). Force coefficients are dimensionless and nearly
velocity-invariant: C_L is flat in velocity; C_D has only a weak negative
(Reynolds) trend. A plain model overfits the velocity channel and invents
spurious C_L growth when extrapolated.
* Fix: "shift-up" velocity augmentation. Half of each batch is pushed UP in
velocity into (and beyond) the test band, with targets corrected by the
physically-estimated per-target slope (mild negative for C_D, flat for C_L).
The other half keeps its real velocity so the in-range structure that drives
C_D is preserved. This makes the learned velocity response physically
correct and robust far past the training range, at no in-range cost.
* SWA (tail weight averaging on a cosine schedule, with BatchNorm stats
recomputed afterwards) gives a single robust checkpoint.
Writes /app/outputs/model.pt with state_dict + the four normalization tensors.
"""
from __future__ import annotations
import json, os, sys
from pathlib import Path
import numpy as np
import torch
import torch.nn as nn
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
torch.set_num_threads(int(os.environ.get("NTHREADS", "16")))
# ---- chosen hyperparameters (selected on the extrapolation proxies) ----
EPOCHS = 160
BS = 32
LR = 1e-3
WD = 1e-4
DMAX = 18.0 # max upward velocity shift (m/s)
SHIFT_P = 0.5 # fraction of samples shifted up each step
CAP = 95.0 # hard cap on augmented velocity
CD_SLOPE = -5.8e-5 # dC_D/dV (per m/s), from AoA-controlled regression
CL_SLOPE = 0.0 # C_L treated as velocity-invariant
SWA_START_F = 0.55
SEED = 0
def main() -> None:
with (APP / "task_config.json").open() as fh:
cfg = json.load(fh)
torch.manual_seed(SEED); np.random.seed(SEED)
points, targets = load_split(APP / "data" / "train")
# Normalization from the original (un-augmented) training statistics.
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)
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)
n = points.shape[0]
slopes = torch.tensor([CD_SLOPE, CL_SLOPE])
swa_state = None
swa_cnt = 0
swa_start = int(EPOCHS * SWA_START_F)
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()
perm = torch.randperm(n)
for s in range(0, n, BS):
idx = perm[s:s + BS]
xb = points[idx].clone()
yb = targets[idx].clone()
b = xb.shape[0]
# shift-up velocity augmentation with per-target slope correction
m = torch.rand(b) < SHIFT_P
delta = torch.empty(b).uniform_(0.0, DMAX)
oldv = xb[:, 0, 2].clone()
newv = (oldv + delta).clamp(max=CAP)
realdelta = newv - oldv
yb[m] = yb[m] + realdelta[m].view(-1, 1) * slopes.view(1, -1)
xb[m, :, 2] = newv[m].view(-1, 1)
x = (xb - fm) / fs
y = (yb - tm) / ts
opt.zero_grad(set_to_none=True)
loss = ((model(x) - y) ** 2).mean()
loss.backward()
opt.step()
sched.step()
if ep >= swa_start:
sd = {k: v.detach().clone().float() for k, v in model.state_d
…[truncated]New file created successfully at /app/final_train.py
cd /app && nohup python3 -u final_train.py > final_train.log 2>&1 & echo "launched pid $!"
launched pid 1145
cd /app && sleep 120 && cat final_train.log 2>&1 | grep -v Warning && ls -la outputs/ 2>/dev/null
[final] wrote /app/outputs/model.pt total 347 drwxr-sr-x 2 h2tagent h2tagent 4096 Jun 19 22:44 . drwxrws--- 5 h2tagent h2tagent 4096 Jun 19 22:44 .. -rw-r--r-- 1 h2tagent h2tagent 346611 Jun 19 22:44 model.pt
cd /app && cat > validate_ckpt.py << 'PYEOF'
import json, torch
from pathlib import Path
from candidate_model import build_model
from dataset import load_split
APP = Path('/app')
cfg = json.load(open(APP/'task_config.json'))
ck = torch.load(APP/'outputs'/'model.pt', weights_only=True)
# 1) keys + shapes
print('ckpt keys:', sorted(ck.keys()))
for k in ['feat_mean','feat_std','targ_mean','targ_std']:
assert k in ck, f'MISSING {k}'
print(f' {k}: shape={tuple(ck[k].shape)} vals={ck[k].numpy().round(4)}')
assert ck['feat_mean'].numel()==4 and ck['feat_std'].numel()==4
assert ck['targ_mean'].numel()==2 and ck['targ_std'].numel()==2
# 2) strict load into the exact architecture
model = build_model(cfg)
missing = model.load_state_dict(ck['state_dict'], strict=True)
print('load_state_dict strict=True OK:', missing)
model.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)
def predict(pts):
with torch.no_grad():
return model((pts-fm)/fs)*ts+tm
# 3) VAL metrics (in-range sanity)
vp, vt = load_split(APP/'data'/'val')
pred = predict(vp)
rmse = torch.sqrt(((pred-vt)**2).mean(0)); std=vt.std(0)
nrmse = rmse/std
print('\nVAL in-range: mean_nrmse=%.4f cd=%.4f cl=%.4f worst=%.4f'%(
nrmse.mean(), nrmse[0], nrmse[1], nrmse.max()))
print('VAL cd_mae=%.5f cl_mae=%.4f'%((pred-vt).abs().mean(0)[0],(pred-vt).abs().mean(0)[1]))
# 4) public_test inference sanity
pt,_ = load_split(APP/'data'/'public_test')
pp = predict(pt)
print('\npublic_test preds: C_D mean=%.5f [%.5f,%.5f] C_L mean=%.4f [%.4f,%.4f]'%(
pp[:,0].mean(),pp[:,0].min(),pp[:,0].max(),pp[:,1].mean(),pp[:,1].min(),pp[:,1].max()))
print('C_D all positive:', bool((pp[:,0]>0).all()), ' any NaN:', bool(torch.isnan(pp).any()))
# 5) physical velocity sweep on 40 public_test geometries
probe = pt[:40].clone()
print('\nvelocity sweep (mean pred over 40 geoms):')
for v in [60,70,75,78,82,86,90]:
q=probe.clone(); q[:,:,2]=v
o=predict(q)
print(' v=%2d: C_D=%.5f C_L=%.4f'%(v,o[:,0].mean(),o[:,1].mean()))
PYEOF
python3 validate_ckpt.py 2>&1 | grep -v Warningckpt keys: ['feat_mean', 'feat_std', 'state_dict', 'targ_mean', 'targ_std'] feat_mean: shape=(4,) vals=[3.83500e-01 7.90000e-03 5.29737e+01 7.64000e-02] feat_std: shape=(4,) vals=[ 0.3956 0.0335 12.7859 0.0956] targ_mean: shape=(2,) vals=[0.0128 0.6596] targ_std: shape=(2,) vals=[0.0051 0.5762] load_state_dict strict=True OK: <All keys matched successfully> VAL in-range: mean_nrmse=0.0875 cd=0.1340 cl=0.0410 worst=0.1340 VAL cd_mae=0.00039 cl_mae=0.0183 public_test preds: C_D mean=0.01134 [0.00644,0.02622] C_L mean=0.6833 [-0.4750,1.7357] C_D all positive: True any NaN: False velocity sweep (mean pred over 40 geoms): v=60: C_D=0.01290 C_L=0.7281 v=70: C_D=0.01235 C_L=0.7304 v=75: C_D=0.01205 C_L=0.7306 v=78: C_D=0.01186 C_L=0.7309 v=82: C_D=0.01164 C_L=0.7333 v=86: C_D=0.01146 C_L=0.7386 v=90: C_D=0.01135 C_L=0.7470
cd /app && cat > seedcheck.py << 'PYEOF'
from harness import train_model, evaluate, TRAIN_PTS, TRAIN_T, VAL_PTS, VAL_T
CD=-5.8e-5
opt = dict(epochs=160, bs=32, lr=1e-3, wd=1e-4, loss='mse',
vel_aug={'mode':'shiftup','dmax':18,'p':0.5,'slopes':[CD,0.0]},
swa={'start_frac':0.55})
for sd in (0,1,2):
m,fm,fs,tm,ts = train_model(TRAIN_PTS, TRAIN_T, opt, seed=sd)
vm,_ = evaluate(m, VAL_PTS, VAL_T, fm,fs,tm,ts)
print('seed %d: VAL mean=%.4f cd=%.4f cl=%.4f'%(sd, vm['mean_nrmse'], vm['nrmse_cd'], vm['nrmse_cl']), flush=True)
PYEOF
nohup python3 -u seedcheck.py > seedcheck.log 2>&1 &
echo "launched pid $!"launched pid 1248
cd /app && sleep 290 && cat seedcheck.log 2>&1 | grep -v Warning
seed 0: VAL mean=0.0875 cd=0.1340 cl=0.0410 seed 1: VAL mean=0.0931 cd=0.1452 cl=0.0410 seed 2: VAL mean=0.0877 cd=0.1400 cl=0.0354
cd /app && cat > twcheck.py << 'PYEOF'
from harness import run
CD=-5.8e-5
base = dict(epochs=160, bs=32, lr=1e-3, wd=1e-4, loss='mse',
vel_aug={'mode':'shiftup','dmax':18,'p':0.5,'slopes':[CD,0.0]},
swa={'start_frac':0.55})
for tw in ([1.0,1.0],[1.5,1.0],[2.5,1.0]):
print('tw=%s VAL:'%tw, flush=True); run(dict(base, tw=tw), proxy=False, seeds=(0,))
print('tw=%s MATCHED:'%tw, flush=True); run(dict(base, tw=tw), thr_lo=69, thr_hi=69, seeds=(0,))
PYEOF
nohup python3 -u twcheck.py > twcheck.log 2>&1 &
echo "launched pid $!"launched pid 1310
cd /app && sleep 295 && cat twcheck.log 2>&1 | grep -v Warning
tw=[1.0, 1.0] VAL: train_n=630 hold_n=80 | mean_nrmse=0.0875 cd=0.1340 cl=0.0410 worst=0.1340 cd_mae=0.00039 tw=[1.0, 1.0] MATCHED: train_n=550 hold_n=80 | mean_nrmse=0.1182 cd=0.1807 cl=0.0557 worst=0.1807 cd_mae=0.00064 tw=[1.5, 1.0] VAL: train_n=630 hold_n=80 | mean_nrmse=0.0896 cd=0.1372 cl=0.0420 worst=0.1372 cd_mae=0.00039 tw=[1.5, 1.0] MATCHED:
cd /app && sleep 180 && cat twcheck.log 2>&1 | grep -v Warning
tw=[1.0, 1.0] VAL: train_n=630 hold_n=80 | mean_nrmse=0.0875 cd=0.1340 cl=0.0410 worst=0.1340 cd_mae=0.00039 tw=[1.0, 1.0] MATCHED: train_n=550 hold_n=80 | mean_nrmse=0.1182 cd=0.1807 cl=0.0557 worst=0.1807 cd_mae=0.00064 tw=[1.5, 1.0] VAL: train_n=630 hold_n=80 | mean_nrmse=0.0896 cd=0.1372 cl=0.0420 worst=0.1372 cd_mae=0.00039 tw=[1.5, 1.0] MATCHED: train_n=550 hold_n=80 | mean_nrmse=0.1144 cd=0.1684 cl=0.0605 worst=0.1684 cd_mae=0.00061 tw=[2.5, 1.0] VAL: train_n=630 hold_n=80 | mean_nrmse=0.0903 cd=0.1362 cl=0.0443 worst=0.1362 cd_mae=0.00038 tw=[2.5, 1.0] MATCHED:
cd /app && pkill -f twcheck.py 2>/dev/null; pkill -f seedcheck.py 2>/dev/null; sleep 2 echo "=== processes ===" && (ps aux | grep -E "[p]ython3" || echo "none running") echo "=== checkpoint on disk ===" && ls -la outputs/model.pt && md5sum outputs/model.pt
=== processes === root 28 0.0 0.0 10940 4216 ? S 21:26 0:00 /bin/sh -c /opt/h2t-venv/bin/python3 /mcp_server/server.py root 29 0.0 0.0 222484 70212 ? Sl 21:26 0:01 /opt/h2t-venv/bin/python3 /mcp_server/server.py === checkpoint on disk === -rw-r--r-- 1 h2tagent h2tagent 346611 Jun 19 22:44 outputs/model.pt d68e88a8ace9b2a5c3b020ef7a6381e3 outputs/model.pt [stderr] [9]+ Terminated nohup python3 -u twcheck.py > twcheck.log 2>&1
cd /app && python3 -c "
import json, torch
from pathlib import Path
from candidate_model import build_model
from dataset import load_split
APP=Path('/app'); cfg=json.load(open(APP/'task_config.json'))
# ---- grader-style load ----
ck=torch.load(APP/'outputs'/'model.pt', weights_only=True)
assert all(k in ck for k in ['state_dict','feat_mean','feat_std','targ_mean','targ_std']), 'missing key'
assert ck['feat_mean'].numel()==4 and ck['feat_std'].numel()==4 and ck['targ_mean'].numel()==2 and ck['targ_std'].numel()==2
m=build_model(cfg); m.load_state_dict(ck['state_dict'], strict=True); 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)
def pred(p):
with torch.no_grad(): return m((p-fm)/fs)*ts+tm
# determinism
pt,_=load_split(APP/'data'/'public_test')
a=pred(pt); b=pred(pt)
print('deterministic:', torch.allclose(a,b), '| no NaN:', not bool(torch.isnan(a).any()), '| C_D>0:', bool((a[:,0]>0).all()))
# metrics on train+val
for sp in ['train','val']:
p,t=load_split(APP/'data'/sp); pr=pred(p)
nr=(torch.sqrt(((pr-t)**2).mean(0))/t.std(0))
print(f'{sp:5s}: mean_nrmse={nr.mean():.4f} cd={nr[0]:.4f} cl={nr[1]:.4f} worst={nr.max():.4f}')
print('ALL CHECKS PASSED')
" 2>&1 | grep -v Warningdeterministic: True | no NaN: True | C_D>0: True train: mean_nrmse=0.0756 cd=0.1140 cl=0.0372 worst=0.1140 val : mean_nrmse=0.0875 cd=0.1340 cl=0.0410 worst=0.1340 ALL CHECKS PASSED
cd /app && rm -f exp1.py exp2.py exp3.py exp4.py twcheck.py seedcheck.py probe_real.py *.log echo "=== final /app contents ===" && ls -la && echo "=== deliverable ===" && ls -la outputs/
=== final /app contents === total 45 drwxrws--- 5 h2tagent h2tagent 4096 Jun 19 22:59 . drwxr-xr-x 26 root root 4096 Jun 19 21:26 .. drwxr-sr-x 2 h2tagent h2tagent 4096 Jun 19 22:42 __pycache__ -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-r--r-- 1 h2tagent h2tagent 5170 Jun 19 22:42 final_train.py -rw-r--r-- 1 h2tagent h2tagent 7524 Jun 19 22:20 harness.py drwxr-sr-x 2 h2tagent h2tagent 4096 Jun 19 22:44 outputs -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 -rw-r--r-- 1 h2tagent h2tagent 2049 Jun 19 22:44 validate_ckpt.py === deliverable === total 347 drwxr-sr-x 2 h2tagent h2tagent 4096 Jun 19 22:44 . drwxrws--- 5 h2tagent h2tagent 4096 Jun 19 22:59 .. -rw-r--r-- 1 h2tagent h2tagent 346611 Jun 19 22:44 model.pt
No file changes, the agent left the workspace untouched.
# SyncVals normalized verifier summary # task: airfrans-high-reynolds-drag-extrapolation # attempt: 9 status: completed raw_score: 0.08 # reward rule: continuous: score 0.08 < 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). ---------------------------------------------------------------------- === final /app contents === total 45 drwxrws--- 5 h2tagent h2tagent 4096 Jun 19 22:59 . drwxr-xr-x 26 root root 4096 Jun 19 21:26 .. drwxr-sr-x 2 h2tagent h2tagent 4096 Jun 19 22:42 __pycache__ -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-r--r-- 1 h2tagent h2tagent 5170 Jun 19 22:42 final_train.py -rw-r--r-- 1 h2tagent h2tagent 7524 Jun 19 22:20 harness.py drwxr-sr-x 2 h2tagent h2tagent 4096 Jun 19 22:44 outputs -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 -rw-r--r-- 1 h2tagent h2tagent 2049 Jun 19 22:44 validate_ckpt.py === deliverable === total 347 drwxr-sr-x 2 h2tagent h2tagent 4096 Jun 19 22:44 . drwxrws--- 5 h2tagent h2tagent 4096 Jun 19 22:59 .. -rw-r--r-- 1 h2tagent h2tagent 346611 Jun 19 22:44 model.pt
Reproduce this trial: git checkout 2f94510 && PYTHONPATH=src python3 scripts/build_site.py , then open trial/trial_9f12a436f08c4dfb. Re-running the agent live requires EVAL_PLATFORM_ENABLE_OAUTH_SMOKE=1 and is non-deterministic.
Trial trial_9f12a436f08c4dfb · verifier authoritative; classifier explanatory.