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, '==')
print('keys:', list(p.keys()))
pts = p['points']
print('points shape/dtype:', pts.shape, pts.dtype)
if 'targets' in p:
t = p['targets']
print('targets shape/dtype:', t.shape, t.dtype)
"== train == keys: ['points', 'targets'] points shape/dtype: torch.Size([630, 256, 4]) torch.float32 targets shape/dtype: torch.Size([630, 2]) torch.float32 == val == keys: ['points', 'targets'] points shape/dtype: torch.Size([80, 256, 4]) torch.float32 targets shape/dtype: torch.Size([80, 2]) torch.float32 == public_test == keys: ['points'] points shape/dtype: torch.Size([100, 256, 4]) torch.float32
cd /app && python -c "
import torch
def stats(name, t, names):
print(f'--- {name} ---')
for i,n in enumerate(names):
c = t[...,i].reshape(-1)
print(f' {n:18s} min={c.min():.5f} max={c.max():.5f} mean={c.mean():.5f} std={c.std():.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)
chan = ['x','y','inlet_velocity','angle_of_attack']
stats('train points', tr['points'], chan)
stats('val points', va['points'], chan)
stats('public_test points', pt['points'], chan)
print()
stats('train targets', tr['targets'], ['C_D','C_L'])
stats('val targets', va['targets'], ['C_D','C_L'])
"--- train points --- x min=-0.00702 max=1.00000 mean=0.38353 std=0.39556 y min=-0.09970 max=0.16270 mean=0.00791 std=0.03346 inlet_velocity min=31.28300 max=74.92400 mean=52.97366 std=12.78588 angle_of_attack min=-0.08622 max=0.25651 mean=0.07639 std=0.09557 --- val points --- x min=-0.00328 max=1.00000 mean=0.38458 std=0.39590 y min=-0.09394 max=0.13663 mean=0.00777 std=0.03233 inlet_velocity min=31.46800 max=74.16200 mean=54.97725 std=11.39685 angle_of_attack min=-0.08360 max=0.25121 mean=0.07986 std=0.09472 --- public_test points --- x min=-0.00553 max=1.00000 mean=0.37608 std=0.39461 y min=-0.09606 max=0.15281 mean=0.00879 std=0.03464 inlet_velocity min=75.03000 max=81.59300 mean=78.20860 std=1.96583 angle_of_attack min=-0.08570 max=0.25820 mean=0.07571 std=0.09605 --- train targets --- C_D min=0.00727 max=0.04595 mean=0.01276 std=0.00510 C_L min=-0.53363 max=1.85012 mean=0.65962 std=0.57620 --- val targets --- C_D min=0.00756 max=0.03459 mean=0.01268 std=0.00533 C_L min=-0.48027 max=1.80035 mean=0.67952 std=0.59777
cd /app && python -c "
import torch, numpy as np
tr = torch.load('data/train/split.pt', weights_only=True)
pts, tg = tr['points'], tr['targets']
# per-case scalar features: velocity and aoa are constant per point
vel = pts[:,0,2].numpy()
aoa = pts[:,0,3].numpy()
cd = tg[:,0].numpy(); cl = tg[:,1].numpy()
# check velocity/aoa truly constant across points
print('vel per-point std (max over cases):', pts[:,:,2].std(dim=1).max().item())
print('aoa per-point std (max over cases):', pts[:,:,3].std(dim=1).max().item())
def corr(a,b): return np.corrcoef(a,b)[0,1]
print()
print('corr(vel, C_D) =', corr(vel,cd))
print('corr(aoa, C_D) =', corr(aoa,cd))
print('corr(vel, C_L) =', corr(vel,cl))
print('corr(aoa, C_L) =', corr(aoa,cl))
print('corr(aoa^2, C_D)=', corr(aoa**2,cd))
print()
# Does C_L depend on velocity at fixed aoa? Bin by aoa
print('C_L vs vel within aoa bins:')
order = np.argsort(aoa)
for q in range(5):
lo=int(q*len(aoa)/5); hi=int((q+1)*len(aoa)/5)
idx = order[lo:hi]
print(f' aoa[{aoa[idx].min():.3f},{aoa[idx].max():.3f}] corr(vel,CL)={corr(vel[idx],cl[idx]):+.3f} corr(vel,CD)={corr(vel[idx],cd[idx]):+.3f}')
"vel per-point std (max over cases): 0.0 aoa per-point std (max over cases): 0.0 corr(vel, C_D) = -0.15417731232687426 corr(aoa, C_D) = 0.738708826123821 corr(vel, C_L) = -0.022042552660221815 corr(aoa, C_L) = 0.9433278860776938 corr(aoa^2, C_D)= 0.8596726372791974 C_L vs vel within aoa bins: aoa[-0.086,-0.019] corr(vel,CL)=+0.035 corr(vel,CD)=-0.167 aoa[-0.019,0.039] corr(vel,CL)=+0.028 corr(vel,CD)=-0.408 aoa[0.039,0.107] corr(vel,CL)=-0.085 corr(vel,CD)=-0.271 aoa[0.109,0.173] corr(vel,CL)=+0.048 corr(vel,CD)=-0.236 aoa[0.174,0.257] corr(vel,CL)=-0.139 corr(vel,CD)=-0.402
cd /app && python -c "
import torch, numpy as np
tr = torch.load('data/train/split.pt', weights_only=True)
pts, tg = tr['points'], tr['targets']
vel = pts[:,0,2].numpy(); aoa = pts[:,0,3].numpy()
cd = tg[:,0].numpy(); cl = tg[:,1].numpy()
# identify unique shapes by hashing the x,y geometry
geo = pts[:,:,:2].reshape(pts.shape[0],-1).numpy()
geo_r = np.round(geo,5)
import hashlib
hashes = [hashlib.md5(g.tobytes()).hexdigest() for g in geo_r]
uniq = set(hashes)
print('n cases=',len(hashes),' unique geometries=',len(uniq))
# distribution of how many cases per shape
from collections import Counter
c = Counter(hashes)
counts = np.array(sorted(c.values()))
print('cases-per-shape: min',counts.min(),'max',counts.max(),'mean',counts.mean())
# For shapes with multiple cases, check vel spread
multi = [h for h,n in c.items() if n>=3]
print('shapes with >=3 cases:', len(multi))
"n cases= 630 unique geometries= 630 cases-per-shape: min 1 max 1 mean 1.0 shapes with >=3 cases: 0
cd /app && python -c "
import torch, numpy as np
tr = torch.load('data/train/split.pt', weights_only=True)
pts, tg = tr['points'], tr['targets']
vel = pts[:,0,2].numpy(); aoa = pts[:,0,3].numpy()
cd = tg[:,0].numpy(); cl = tg[:,1].numpy()
lcd = np.log(cd); lv = np.log(vel)
# Regress log(cd) on [1, aoa, aoa^2, log(vel)]
X = np.stack([np.ones_like(aoa), aoa, aoa**2, lv], axis=1)
beta, res, *_ = np.linalg.lstsq(X, lcd, rcond=None)
pred = X@beta
ss = 1 - ((lcd-pred)**2).sum()/((lcd-lcd.mean())**2).sum()
print('log-CD model coefs [1,aoa,aoa^2,log(vel)]:', np.round(beta,4))
print(' -> velocity exponent p =', round(beta[-1],4), ' R2=', round(ss,4))
# add log(vel)*aoa interaction
X2 = np.stack([np.ones_like(aoa), aoa, aoa**2, lv, lv*aoa, lv*aoa**2], axis=1)
beta2,*_ = np.linalg.lstsq(X2, lcd, rcond=None); pred2=X2@beta2
ss2 = 1-((lcd-pred2)**2).sum()/((lcd-lcd.mean())**2).sum()
print('with interactions R2=', round(ss2,4), 'coefs', np.round(beta2,4))
# Now C_L model: linear in aoa
Xl = np.stack([np.ones_like(aoa), aoa, lv], axis=1)
bl,*_ = np.linalg.lstsq(Xl, cl, rcond=None); pl=Xl@bl
ssl = 1-((cl-pl)**2).sum()/((cl-cl.mean())**2).sum()
print()
print('C_L ~ [1,aoa,log(vel)] coefs:', np.round(bl,4), 'R2=',round(ssl,4))
print(' velocity coef for C_L:', round(bl[-1],5),'(near 0 => vel-independent)')
"log-CD model coefs [1,aoa,aoa^2,log(vel)]: [-3.8419 0.3852 14.6366 -0.2106] -> velocity exponent p = -0.2106 R2= 0.8178 with interactions R2= 0.8196 coefs [-4.0026 -1.7473 35.9617 -0.17 0.543 -5.4129] C_L ~ [1,aoa,log(vel)] coefs: [ 0.295 5.6824 -0.0176] R2= 0.8899 velocity coef for C_L: -0.01764 (near 0 => vel-independent)
cd /app && python -c "
import torch, numpy as np
tr = torch.load('data/train/split.pt', weights_only=True)
pts, tg = tr['points'], tr['targets']
vel = pts[:,0,2].numpy(); aoa = pts[:,0,3].numpy()
cd = tg[:,0].numpy(); lcd=np.log(cd); lv=np.log(vel)
# Fit exponent using ONLY low-velocity cases, then see if it predicts high-vel CD
for thr in [60, 65, 68]:
lo = vel<thr; hi = vel>=thr
X = np.stack([np.ones(lo.sum()), aoa[lo], aoa[lo]**2, lv[lo]],1)
b,*_ = np.linalg.lstsq(X, lcd[lo], rcond=None)
Xh = np.stack([np.ones(hi.sum()), aoa[hi], aoa[hi]**2, lv[hi]],1)
ph = Xh@b
r2h = 1-((lcd[hi]-ph)**2).sum()/((lcd[hi]-lcd[hi].mean())**2).sum()
# also compare to using the low-vel exponent extrapolated
print(f'thr={thr}: train on V<{thr} (n={lo.sum()}), exponent={b[-1]:.3f}, R2 on V>={thr} (n={hi.sum()})={r2h:.3f}')
"thr=60: train on V<60 (n=413), exponent=-0.184, R2 on V>=60 (n=217)=0.858 thr=65: train on V<65 (n=482), exponent=-0.222, R2 on V>=65 (n=148)=0.847 thr=68: train on V<68 (n=535), exponent=-0.228, R2 on V>=68 (n=95)=0.815
cd /app && python -c "
import torch, time
print('torch', torch.__version__, 'cuda', torch.cuda.is_available())
print('cpu threads', torch.get_num_threads())
" && nproc && free -h | head -2torch 2.5.1+cu124 cuda False
cpu threads 16
16
total used free shared buff/cache available
Mem: 69Gi 70Mi 68Gi 0B 29Mi 68Gicd /app && python -c "
import torch, numpy as np
# Check how CL varies with velocity more carefully via partial residuals, and CD residual after aoa
tr = torch.load('data/train/split.pt', weights_only=True)
pts, tg = tr['points'], tr['targets']
vel = pts[:,0,2].numpy(); aoa = pts[:,0,3].numpy()
cd = tg[:,0].numpy(); cl = tg[:,1].numpy()
# Fit CL ~ cubic in aoa, check residual vs velocity
Xa = np.stack([aoa**k for k in range(4)],1)
bl,*_=np.linalg.lstsq(Xa,cl,rcond=None); rl=cl-Xa@bl
print('CL: corr(resid_after_aoa_cubic, vel)=', np.corrcoef(rl,vel)[0,1], 'resid std', rl.std(), 'vs CL std', cl.std())
# So velocity explains essentially nothing extra for CL
# CD: log model residual vs shape proxies (thickness)
yloc = pts[:,:,1].numpy()
thick = yloc.max(1)-yloc.min(1) # max thickness proxy
lcd=np.log(cd); lv=np.log(vel)
X=np.stack([np.ones_like(aoa),aoa,aoa**2,lv],1)
b,*_=np.linalg.lstsq(X,lcd,rcond=None); r=lcd-X@b
print('CD: corr(log-resid, thickness)=', np.corrcoef(r,thick)[0,1])
print('thickness range', thick.min(), thick.max())
"CL: corr(resid_after_aoa_cubic, vel)= -0.020116020990871914 resid std 0.18913952 vs CL std 0.5757378 CD: corr(log-resid, thickness)= 0.4911331672413428 thickness range 0.0509462 0.2018896
command: create
path: /app/lib.py
file_text: """Shared training/eval utilities for the airfoil force-coefficient task."""
from __future__ import annotations
import json
import os
from pathlib import Path
import numpy as np
import torch
import torch.nn.functional as F
APP_DIR = Path(os.environ.get("APP_DIR", "/app"))
import sys
sys.path.insert(0, str(APP_DIR))
from candidate_model import build_model # noqa: E402
# indices into the 4 input channels
X_I, Y_I, V_I, A_I = 0, 1, 2, 3
CD_I, CL_I = 0, 1
CD_VEL_EXPONENT = -0.20 # C_D ~ V^p (turbulent skin-friction scaling, empirically -0.21)
def load_cfg():
with (APP_DIR / "task_config.json").open() as fh:
return json.load(fh)
def load_split(name):
p = torch.load(APP_DIR / "data" / name / "split.pt", map_location="cpu", weights_only=True)
pts = p["points"].float()
tg = p["targets"].float() if "targets" in p else None
return pts, tg
def velocity_augment(pts, tg, vmin, vmax, p=CD_VEL_EXPONENT, gen=None):
"""Rescale each case's inlet velocity to a random target in [vmin,vmax];
adjust C_D by the power law V^p and keep C_L unchanged."""
gen = gen or torch.Generator().manual_seed(0)
n = pts.shape[0]
v_old = pts[:, 0, V_I].clone()
v_new = vmin + (vmax - vmin) * torch.rand(n, generator=gen)
ratio = v_new / v_old
pts2 = pts.clone()
pts2[:, :, V_I] = v_new.view(-1, 1)
tg2 = tg.clone()
tg2[:, CD_I] = tg[:, CD_I] * ratio.pow(p)
return pts2, tg2
def standardize_fit(pts, tg):
flat = pts.reshape(-1, pts.shape[-1])
fmean = flat.mean(0)
fstd = flat.std(0).clamp_min(1e-8)
tmean = tg.mean(0)
tstd = tg.std(0).clamp_min(1e-8)
return fmean, fstd, tmean, tstd
def nrmse_report(pred, true):
"""Per-target metrics. NRMSE normalized by std of true targets (on this set)."""
err = pred - true
rmse = err.pow(2).mean(0).sqrt()
mae = err.abs().mean(0)
std = true.std(0).clamp_min(1e-12)
mean_abs = true.abs().mean(0).clamp_min(1e-12)
rng = (true.max(0).values - true.min(0).values).clamp_min(1e-12)
nrmse_std = rmse / std
nrmse_rng = rmse / rng
nmae_mean = mae / mean_abs
return {
"rmse": rmse.tolist(),
"mae": mae.tolist(),
"nrmse_std": nrmse_std.tolist(),
"mean_nrmse_std": nrmse_std.mean().item(),
"worst_nrmse_std": nrmse_std.max().item(),
"nrmse_rng": nrmse_rng.tolist(),
"mean_nrmse_rng": nrmse_rng.mean().item(),
"cd_nmae_mean": nmae_mean[CD_I].item(),
}
def predict(model, pts, fmean, fstd, tmean, tstd, device="cpu", bs=256):
model.eval()
outs = []
with torch.no_grad():
for i in range(0, pts.shape[0], bs):
xb = ((pts[i:i+bs] - fmean.view(1, 1, -1)) / fstd.view(1, 1, -1)).to(device)
o = model(xb).cpu()
outs.append(o * tstd.view(1, -1) + tmean.view(1, -1))
return torch.cat(outs, 0)
def train_model(
train_pts, train_tg, cfg,
epochs=120, bs=32, lr=1e-3, wd=1e-4,
aug_vmax=None, aug_vmin=None, aug_p=CD_VEL_EXPONENT,
jitter=0.0, w_cd=1.0, w_cl=1.0, seed=0,
fmean=None, fstd=None, tmean=None, tstd=None,
val_pts=None, val_tg=None, swa_start=None, verbose=False,
loss_type="mse", huber_delta=1.0,
):
torch.manual_seed(seed)
np.random.seed(seed)
gen = torch.Generator().manual_seed(seed)
device = "cpu"
# Build the velocity-augmented pool once to fit normalization over the wide range.
if fmean is None:
if aug_vmax is not None:
ap, at = velocity_augment(train_pts, train_tg, aug_vmin, aug_vmax, aug_p, gen)
pool_pts = torch.cat([train_pts, ap], 0)
pool_tg = torch.cat([train_tg, at], 0)
else:
pool_pts, pool_tg = train_pts, train_tg
fmean, fstd, tmean, tstd = standardize_fit(pool_pts, pool_tg)
model = build_model(cfg).to(device)
opt = torch.optim.Adam(model.parameters(), lr=lr, weight_decay=wd)
sched = torch.optim.lr_sc
…[truncated]New file created successfully at /app/lib.py
cd /app && cat > /tmp/exp1.py << 'EOF'
import torch, numpy as np, time
import lib
cfg = lib.load_cfg()
pts, tg = lib.load_split("train")
vel = pts[:,0,2]
thr = 65.0
tr_idx = (vel < thr).nonzero().squeeze(1)
te_idx = (vel >= thr).nonzero().squeeze(1)
tp, tt = pts[tr_idx], tg[tr_idx]
ep, et = pts[te_idx], tg[te_idx]
print(f"pseudo-split: train n={len(tr_idx)} (V<{thr}), test n={len(te_idx)} (V>={thr})")
print(f"test V range {ep[:,0,2].min():.1f}-{ep[:,0,2].max():.1f}")
def run(tag, **kw):
t0=time.time()
model, norms = lib.train_model(tp, tt, cfg, seed=0, **kw)
pr = lib.predict(model, ep, *norms)
r = lib.nrmse_report(pr, et)
print(f"[{tag}] mean_nrmse_std={r['mean_nrmse_std']:.4f} cd={r['nrmse_std'][0]:.4f} cl={r['nrmse_std'][1]:.4f} "
f"cd_rmse={r['rmse'][0]:.5f} ({time.time()-t0:.0f}s)")
return r
run("baseline no-aug", epochs=80, aug_vmax=None)
run("vel-aug up to 85", epochs=80, aug_vmin=45, aug_vmax=85)
EOF
python /tmp/exp1.py[stderr]
Traceback (most recent call last):
File "/tmp/exp1.py", line 2, in <module>
import lib
ModuleNotFoundError: No module named 'lib'cd /app && sed -i '1i import sys; sys.path.insert(0, "/app")' /tmp/exp1.py && python /tmp/exp1.py
pseudo-split: train n=482 (V<65.0), test n=148 (V>=65.0) test V range 65.0-74.9 [baseline no-aug] mean_nrmse_std=0.1996 cd=0.2605 cl=0.1387 cd_rmse=0.00109 (46s) [vel-aug up to 85] mean_nrmse_std=0.2078 cd=0.3779 cl=0.0376 cd_rmse=0.00158 (64s)
cd /app && cat > /tmp/exp2.py << 'EOF'
import sys; sys.path.insert(0,"/app")
import torch, numpy as np, time
import lib
cfg = lib.load_cfg()
pts, tg = lib.load_split("train")
vel = pts[:,0,2]
thr=65.0
tp,tt = pts[vel<thr], tg[vel<thr]
ep,et = pts[vel>=thr], tg[vel>=thr]
def run(tag, **kw):
t0=time.time()
model, norms = lib.train_model(tp, tt, cfg, seed=0, **kw)
pr = lib.predict(model, ep, *norms)
r = lib.nrmse_report(pr, et)
print(f"[{tag:28s}] mean={r['mean_nrmse_std']:.4f} cd={r['nrmse_std'][0]:.4f} cl={r['nrmse_std'][1]:.4f} cd_rmse={r['rmse'][0]:.5f} ({time.time()-t0:.0f}s)")
return r
# Fit normalization from original only, but still augment during training
fm,fs,tm,ts = lib.standardize_fit(tp, tt)
run("aug[45,85] norm=orig", epochs=80, aug_vmin=45, aug_vmax=85, fmean=fm,fstd=fs,tmean=tm,tstd=ts)
run("aug[55,80] exp-0.2", epochs=80, aug_vmin=55, aug_vmax=80)
run("aug[45,85] exp-0.25", epochs=80, aug_vmin=45, aug_vmax=85, aug_p=-0.25)
run("aug cl-only (exp0 cd)", epochs=80, aug_vmin=45, aug_vmax=85, aug_p=0.0)
EOF
python /tmp/exp2.py[stderr] Shell command timed out. Tool interaction exceeded 300 seconds.
restart: true
<system>tool has been restarted.</system>
cd /app && cat > /tmp/exp2.py << 'EOF'
import sys; sys.path.insert(0,"/app")
import torch, time
import lib
cfg = lib.load_cfg()
pts, tg = lib.load_split("train")
vel = pts[:,0,2]
thr=65.0
tp,tt = pts[vel<thr], tg[vel<thr]
ep,et = pts[vel>=thr], tg[vel>=thr]
def run(tag, **kw):
t0=time.time()
model, norms = lib.train_model(tp, tt, cfg, seed=0, **kw)
r = lib.nrmse_report(lib.predict(model, ep, *norms), et)
print(f"[{tag:26s}] mean={r['mean_nrmse_std']:.4f} cd={r['nrmse_std'][0]:.4f} cl={r['nrmse_std'][1]:.4f} cd_rmse={r['rmse'][0]:.5f} ({time.time()-t0:.0f}s)",flush=True)
fm,fs,tm,ts = lib.standardize_fit(tp, tt)
run("aug[45,85]norm=orig", epochs=70, aug_vmin=45, aug_vmax=85, fmean=fm,fstd=fs,tmean=tm,tstd=ts)
run("aug[55,80]exp-0.2", epochs=70, aug_vmin=55, aug_vmax=80)
run("aug-clonly(cd exp0)", epochs=70, aug_vmin=45, aug_vmax=85, aug_p=0.0)
run("baseline", epochs=70)
print("DONE",flush=True)
EOF
nohup python /tmp/exp2.py > /tmp/exp2.out 2>&1 &
echo "started PID $!"started PID 455
sleep 240 && cat /tmp/exp2.out
[aug[45,85]norm=orig ] mean=0.2082 cd=0.3376 cl=0.0787 cd_rmse=0.00141 (75s) [aug[55,80]exp-0.2 ] mean=0.2517 cd=0.4440 cl=0.0594 cd_rmse=0.00185 (55s) [aug-clonly(cd exp0) ] mean=0.3183 cd=0.5814 cl=0.0553 cd_rmse=0.00243 (52s) [baseline ] mean=0.2302 cd=0.3206 cl=0.1399 cd_rmse=0.00134 (24s) DONE
cd /app && cat > /tmp/probe.py << 'EOF'
import sys; sys.path.insert(0,"/app")
import torch, numpy as np
import lib
cfg = lib.load_cfg()
pts, tg = lib.load_split("train")
vel = pts[:,0,2]
thr=65.0
tp,tt = pts[vel<thr], tg[vel<thr]
# take 30 airfoils, sweep velocity 40..90, see predicted CD and CL
sel = pts[vel<thr][:40].clone()
def sweep(model, norms, label):
print(f"-- {label}: predicted response to velocity (median over 40 airfoils) --")
cds=[]; cls=[]
vs=[40,50,60,65,70,75,80,85,90]
for v in vs:
s = sel.clone(); s[:,:,2]=v
pr = lib.predict(model, s, *norms)
cds.append(pr[:,0].median().item()); cls.append(pr[:,1].median().item())
print(" V :", " ".join(f"{v:7d}" for v in vs))
print(" CD :", " ".join(f"{c:7.5f}" for c in cds))
print(" CL :", " ".join(f"{c:7.4f}" for c in cls))
# ratio CD(v)/CD(60) to compare to power law (v/60)^-0.2
base=cds[vs.index(60)]
print(" CDr:", " ".join(f"{c/base:7.4f}" for c in cds), "(network)")
print(" pwr:", " ".join(f"{(v/60)**-0.2:7.4f}" for v in vs), "(V^-0.2)")
m0,n0 = lib.train_model(tp, tt, cfg, seed=0, epochs=70)
sweep(m0,n0,"baseline")
m1,n1 = lib.train_model(tp, tt, cfg, seed=0, epochs=70, aug_vmin=45, aug_vmax=90)
sweep(m1,n1,"vel-aug[45,90]")
print("DONE",flush=True)
EOF
nohup python /tmp/probe.py > /tmp/probe.out 2>&1 &
echo "started $!"started 501
sleep 120 && cat /tmp/probe.out
-- baseline: predicted response to velocity (median over 40 airfoils) -- V : 40 50 60 65 70 75 80 85 90 CD : 0.01123 0.01093 0.01051 0.01048 0.01054 0.01057 0.01065 0.01091 0.01144 CL : 0.4945 0.5379 0.4792 0.4618 0.4719 0.4938 0.5218 0.5563 0.6011 CDr: 1.0678 1.0398 1.0000 0.9967 1.0022 1.0057 1.0127 1.0376 1.0877 (network) pwr: 1.0845 1.0371 1.0000 0.9841 0.9696 0.9564 0.9441 0.9327 0.9221 (V^-0.2) -- vel-aug[45,90]: predicted response to velocity (median over 40 airfoils) -- V : 40 50 60 65 70 75 80 85 90 CD : 0.01128 0.01079 0.01034 0.01017 0.01001 0.00985 0.00972 0.00964 0.00961 CL : 0.4223 0.4341 0.4348 0.4361 0.4404 0.4433 0.4394 0.4302 0.4217 CDr: 1.0900 1.0428 1.0000 0.9831 0.9679 0.9523 0.9393 0.9319 0.9292 (network) pwr: 1.0845 1.0371 1.0000 0.9841 0.9696 0.9564 0.9441 0.9327 0.9221 (V^-0.2) DONE
cd /app && cat > /tmp/exp3.py << 'EOF'
import sys; sys.path.insert(0,"/app")
import torch, time
import lib
cfg = lib.load_cfg()
pts, tg = lib.load_split("train")
vel = pts[:,0,2]
thr=69.0
tp,tt = pts[vel<thr], tg[vel<thr]
ep,et = pts[vel>=thr], tg[vel>=thr]
print(f"train n={len(tp)} (V<{thr}), test n={len(ep)} (V in {ep[:,0,2].min():.1f}-{ep[:,0,2].max():.1f})",flush=True)
def run(tag, reps=2, **kw):
accs=[]
for s in range(reps):
model, norms = lib.train_model(tp, tt, cfg, seed=s, **kw)
r = lib.nrmse_report(lib.predict(model, ep, *norms), et)
accs.append((r['mean_nrmse_std'],r['nrmse_std'][0],r['nrmse_std'][1],r['rmse'][0]))
import numpy as np; a=np.array(accs).mean(0)
print(f"[{tag:22s}] mean={a[0]:.4f} cd={a[1]:.4f} cl={a[2]:.4f} cd_rmse={a[3]:.5f}",flush=True)
run("baseline", epochs=70)
run("aug[35,90]exp-0.2", epochs=70, aug_vmin=35, aug_vmax=90)
run("aug[35,90]exp-0.18", epochs=70, aug_vmin=35, aug_vmax=90, aug_p=-0.18)
print("DONE",flush=True)
EOF
nohup python /tmp/exp3.py > /tmp/exp3.out 2>&1 &
echo "started $!"started 547
sleep 280 && cat /tmp/exp3.out
train n=550 (V<69.0), test n=80 (V in 69.0-74.9) [baseline ] mean=0.1220 cd=0.1435 cl=0.1005 cd_rmse=0.00060 [aug[35,90]exp-0.2 ] mean=0.0913 cd=0.1416 cl=0.0411 cd_rmse=0.00059
sleep 30; cat /tmp/exp3.out | tail -2
[baseline ] mean=0.1220 cd=0.1435 cl=0.1005 cd_rmse=0.00060 [aug[35,90]exp-0.2 ] mean=0.0913 cd=0.1416 cl=0.0411 cd_rmse=0.00059
cd /app && cat > /tmp/sweep.py << 'EOF'
import sys; sys.path.insert(0,"/app")
import torch, numpy as np
import lib
cfg = lib.load_cfg()
pts, tg = lib.load_split("train")
vpts, vtg = lib.load_split("val")
vel = pts[:,0,2]; thr=69.0
tp,tt = pts[vel<thr], tg[vel<thr]
ep,et = pts[vel>=thr], tg[vel>=thr]
def run(tag, reps=2, **kw):
E=[]; V=[]
for s in range(reps):
model, norms = lib.train_model(tp, tt, cfg, seed=s, **kw)
E.append(list(lib.nrmse_report(lib.predict(model, ep, *norms), et).values())[3:5]) # placeholder
r = lib.nrmse_report(lib.predict(model, ep, *norms), et)
rv = lib.nrmse_report(lib.predict(model, vpts, *norms), vtg)
E.append((r['mean_nrmse_std'],r['nrmse_std'][0],r['nrmse_std'][1]))
V.append(rv['mean_nrmse_std'])
E=np.array([e for e in E if isinstance(e,tuple)]).mean(0); V=np.mean(V)
print(f"[{tag:24s}] EXTRAP mean={E[0]:.4f} cd={E[1]:.4f} cl={E[2]:.4f} | inrange_val={V:.4f}",flush=True)
for tag,kw in [
("aug[35,90]exp-0.15",dict(aug_vmin=35,aug_vmax=90,aug_p=-0.15)),
("aug[35,90]exp-0.25",dict(aug_vmin=35,aug_vmax=90,aug_p=-0.25)),
("aug[35,95]exp-0.2", dict(aug_vmin=35,aug_vmax=95,aug_p=-0.20)),
("aug[45,85]exp-0.2", dict(aug_vmin=45,aug_vmax=85,aug_p=-0.20)),
]:
run(tag, epochs=70, **kw)
print("DONE",flush=True)
EOF
nohup python /tmp/sweep.py > /tmp/sweep.out 2>&1 &
echo "started $!"started 1650
sleep 290 && cat /tmp/sweep.out
[aug[35,90]exp-0.15 ] EXTRAP mean=0.1050 cd=0.1682 cl=0.0418 | inrange_val=0.1328
sleep 200 && cat /tmp/sweep.out
[aug[35,90]exp-0.15 ] EXTRAP mean=0.1050 cd=0.1682 cl=0.0418 | inrange_val=0.1328 [aug[35,90]exp-0.25 ] EXTRAP mean=0.0903 cd=0.1405 cl=0.0401 | inrange_val=0.1230 [aug[35,95]exp-0.2 ] EXTRAP mean=0.1020 cd=0.1638 cl=0.0402 | inrange_val=0.1308
sleep 60 && cat /tmp/sweep.out
[aug[35,90]exp-0.15 ] EXTRAP mean=0.1050 cd=0.1682 cl=0.0418 | inrange_val=0.1328 [aug[35,90]exp-0.25 ] EXTRAP mean=0.0903 cd=0.1405 cl=0.0401 | inrange_val=0.1230 [aug[35,95]exp-0.2 ] EXTRAP mean=0.1020 cd=0.1638 cl=0.0402 | inrange_val=0.1308
sleep 45 && cat /tmp/sweep.out | tail -3
[aug[35,95]exp-0.2 ] EXTRAP mean=0.1020 cd=0.1638 cl=0.0402 | inrange_val=0.1308 [aug[45,85]exp-0.2 ] EXTRAP mean=0.0911 cd=0.1403 cl=0.0418 | inrange_val=0.1298 DONE
command: str_replace
path: /app/lib.py
old_str: def velocity_augment(pts, tg, vmin, vmax, p=CD_VEL_EXPONENT, gen=None):
"""Rescale each case's inlet velocity to a random target in [vmin,vmax];
adjust C_D by the power law V^p and keep C_L unchanged."""
gen = gen or torch.Generator().manual_seed(0)
n = pts.shape[0]
v_old = pts[:, 0, V_I].clone()
v_new = vmin + (vmax - vmin) * torch.rand(n, generator=gen)
ratio = v_new / v_old
pts2 = pts.clone()
pts2[:, :, V_I] = v_new.view(-1, 1)
tg2 = tg.clone()
tg2[:, CD_I] = tg[:, CD_I] * ratio.pow(p)
return pts2, tg2
new_str: def velocity_augment(pts, tg, vmin, vmax, p=CD_VEL_EXPONENT, gen=None, n_aug=1):
"""Rescale each case's inlet velocity to a random target in [vmin,vmax];
adjust C_D by the power law V^p and keep C_L unchanged. Returns n_aug copies."""
gen = gen or torch.Generator().manual_seed(0)
outs_p, outs_t = [], []
n = pts.shape[0]
v_old = pts[:, 0, V_I]
for _ in range(n_aug):
v_new = vmin + (vmax - vmin) * torch.rand(n, generator=gen)
ratio = v_new / v_old
pts2 = pts.clone()
pts2[:, :, V_I] = v_new.view(-1, 1)
tg2 = tg.clone()
tg2[:, CD_I] = tg[:, CD_I] * ratio.pow(p)
outs_p.append(pts2)
outs_t.append(tg2)
return torch.cat(outs_p, 0), torch.cat(outs_t, 0)The file /app/lib.py has been edited successfully.
cd /app && sed -i 's/aug_vmax=None, aug_vmin=None, aug_p=CD_VEL_EXPONENT,/aug_vmax=None, aug_vmin=None, aug_p=CD_VEL_EXPONENT, n_aug=1,/' lib.py
# update the two velocity_augment calls inside train_model/_update_bn to pass n_aug, and pass n_aug through _update_bn
python - << 'EOF'
import re
s=open('/app/lib.py').read()
s=s.replace("ap, at = velocity_augment(train_pts, train_tg, aug_vmin, aug_vmax, aug_p, gen)\n pool_pts",
"ap, at = velocity_augment(train_pts, train_tg, aug_vmin, aug_vmax, aug_p, gen, n_aug)\n pool_pts")
s=s.replace("ap, at = velocity_augment(train_pts, train_tg, aug_vmin, aug_vmax, aug_p, gen)\n ep_pts",
"ap, at = velocity_augment(train_pts, train_tg, aug_vmin, aug_vmax, aug_p, gen, n_aug)\n ep_pts")
s=s.replace("_update_bn(model, train_pts, train_tg, fmean, fstd, tmean, tstd, aug_vmin, aug_vmax, aug_p, gen, bs)",
"_update_bn(model, train_pts, train_tg, fmean, fstd, tmean, tstd, aug_vmin, aug_vmax, aug_p, gen, bs, n_aug)")
s=s.replace("def _update_bn(model, train_pts, train_tg, fmean, fstd, tmean, tstd, aug_vmin, aug_vmax, aug_p, gen, bs):",
"def _update_bn(model, train_pts, train_tg, fmean, fstd, tmean, tstd, aug_vmin, aug_vmax, aug_p, gen, bs, n_aug=1):")
s=s.replace("ap, at = velocity_augment(train_pts, train_tg, aug_vmin, aug_vmax, aug_p, gen)\n pts = torch.cat",
"ap, at = velocity_augment(train_pts, train_tg, aug_vmin, aug_vmax, aug_p, gen, n_aug)\n pts = torch.cat")
open('/app/lib.py','w').write(s)
print("patched")
EOF
python -c "import sys; sys.path.insert(0,'/app'); import lib; print('import OK')"
grep -n "n_aug" lib.pypatched import OK 35:def velocity_augment(pts, tg, vmin, vmax, p=CD_VEL_EXPONENT, gen=None, n_aug=1): 37: adjust C_D by the power law V^p and keep C_L unchanged. Returns n_aug copies.""" 42: for _ in range(n_aug): 100: aug_vmax=None, aug_vmin=None, aug_p=CD_VEL_EXPONENT, n_aug=1, 114: ap, at = velocity_augment(train_pts, train_tg, aug_vmin, aug_vmax, aug_p, gen, n_aug) 133: ap, at = velocity_augment(train_pts, train_tg, aug_vmin, aug_vmax, aug_p, gen, n_aug) 175: _update_bn(model, train_pts, train_tg, fmean, fstd, tmean, tstd, aug_vmin, aug_vmax, aug_p, gen, bs, n_aug) 179:def _update_bn(model, train_pts, train_tg, fmean, fstd, tmean, tstd, aug_vmin, aug_vmax, aug_p, gen, bs, n_aug): 187: ap, at = velocity_augment(train_pts, train_tg, aug_vmin, aug_vmax, aug_p, gen, n_aug)
cd /app && cat > /tmp/sweep2.py << 'EOF'
import sys; sys.path.insert(0,"/app")
import numpy as np
import lib
cfg = lib.load_cfg()
pts, tg = lib.load_split("train")
vpts, vtg = lib.load_split("val")
vel = pts[:,0,2]; thr=69.0
tp,tt = pts[vel<thr], tg[vel<thr]
ep,et = pts[vel>=thr], tg[vel>=thr]
def run(tag, reps=3, **kw):
E=[];V=[]
for s in range(reps):
model, norms = lib.train_model(tp, tt, cfg, seed=s, **kw)
r = lib.nrmse_report(lib.predict(model, ep, *norms), et)
rv = lib.nrmse_report(lib.predict(model, vpts, *norms), vtg)
E.append((r['mean_nrmse_std'],r['nrmse_std'][0],r['nrmse_std'][1]))
V.append(rv['mean_nrmse_std'])
E=np.array(E).mean(0); V=np.mean(V)
print(f"[{tag:22s}] EXTRAP mean={E[0]:.4f} cd={E[1]:.4f} cl={E[2]:.4f} | inrange={V:.4f}",flush=True)
base=dict(aug_vmin=35,aug_vmax=90,aug_p=-0.22)
run("ref exp-0.22 70ep", epochs=70, **base)
run("SWA 110ep start60", epochs=110, swa_start=60, **base)
run("jitter0.02 70ep", epochs=70, jitter=0.02, **base)
run("w_cd2 70ep", epochs=70, w_cd=2.0, **base)
run("n_aug2 70ep", epochs=70, n_aug=2, **base)
print("DONE",flush=True)
EOF
nohup python /tmp/sweep2.py > /tmp/sweep2.out 2>&1 &
echo "started $!"started 4070
sleep 290 && cat /tmp/sweep2.out
[ref exp-0.22 70ep ] EXTRAP mean=0.1022 cd=0.1649 cl=0.0395 | inrange=0.1278
sleep 280 && cat /tmp/sweep2.out
[ref exp-0.22 70ep ] EXTRAP mean=0.1022 cd=0.1649 cl=0.0395 | inrange=0.1278 [SWA 110ep start60 ] EXTRAP mean=0.1005 cd=0.1669 cl=0.0342 | inrange=0.1074
sleep 200 && cat /tmp/sweep2.out
[ref exp-0.22 70ep ] EXTRAP mean=0.1022 cd=0.1649 cl=0.0395 | inrange=0.1278 [SWA 110ep start60 ] EXTRAP mean=0.1005 cd=0.1669 cl=0.0342 | inrange=0.1074 [jitter0.02 70ep ] EXTRAP mean=0.1362 cd=0.2309 cl=0.0414 | inrange=0.1519
sleep 120 && cat /tmp/sweep2.out | tail -3
[SWA 110ep start60 ] EXTRAP mean=0.1005 cd=0.1669 cl=0.0342 | inrange=0.1074 [jitter0.02 70ep ] EXTRAP mean=0.1362 cd=0.2309 cl=0.0414 | inrange=0.1519 [w_cd2 70ep ] EXTRAP mean=0.1028 cd=0.1623 cl=0.0433 | inrange=0.1238
sleep 60 && cat /tmp/sweep2.out | tail -2
[jitter0.02 70ep ] EXTRAP mean=0.1362 cd=0.2309 cl=0.0414 | inrange=0.1519 [w_cd2 70ep ] EXTRAP mean=0.1028 cd=0.1623 cl=0.0433 | inrange=0.1238
sleep 45; cat /tmp/sweep2.out | tail -1; echo "---physics floor---"; cd /app && python -c "
import sys; sys.path.insert(0,'/app'); import torch, numpy as np, lib
pts,tg=lib.load_split('train'); vel=pts[:,0,2].numpy(); aoa=pts[:,0,3].numpy(); cd=tg[:,0].numpy()
thr=69.0; tr=vel<thr; te=vel>=thr
lv=np.log(vel); lcd=np.log(cd)
# physics: log cd = poly(aoa deg2) + p*log v, fit on train
X=lambda m: np.stack([np.ones(m.sum()),aoa[m],aoa[m]**2,lv[m]],1)
b,*_=np.linalg.lstsq(X(tr),lcd[tr],rcond=None)
pred=np.exp(X(te)@b)
rmse=np.sqrt(((pred-cd[te])**2).mean()); nrmse=rmse/cd[te].std()
print(f'physics aoa2+V^{b[-1]:.3f}: cd extrap nrmse={nrmse:.4f} rmse={rmse:.5f}')
# add cubic aoa
X3=lambda m: np.stack([np.ones(m.sum()),aoa[m],aoa[m]**2,aoa[m]**3,lv[m]],1)
b3,*_=np.linalg.lstsq(X3(tr),lcd[tr],rcond=None); pred3=np.exp(X3(te)@b3)
print(f'physics aoa3+V: cd extrap nrmse={np.sqrt(((pred3-cd[te])**2).mean())/cd[te].std():.4f}')
print(f'test cd std={cd[te].std():.5f} mean={cd[te].mean():.5f}')
"[w_cd2 70ep ] EXTRAP mean=0.1028 cd=0.1623 cl=0.0433 | inrange=0.1238 ---physics floor--- physics aoa2+V^-0.227: cd extrap nrmse=0.4049 rmse=0.00168 physics aoa3+V: cd extrap nrmse=0.4069 test cd std=0.00415 mean=0.01180
sleep 30; tail -2 /tmp/sweep2.out
[jitter0.02 70ep ] EXTRAP mean=0.1362 cd=0.2309 cl=0.0414 | inrange=0.1519 [w_cd2 70ep ] EXTRAP mean=0.1028 cd=0.1623 cl=0.0433 | inrange=0.1238
sleep 40; tail -1 /tmp/sweep2.out; ps aux | grep -c "[s]weep2"
[w_cd2 70ep ] EXTRAP mean=0.1028 cd=0.1623 cl=0.0433 | inrange=0.1238 1
sleep 45; tail -2 /tmp/sweep2.out
[jitter0.02 70ep ] EXTRAP mean=0.1362 cd=0.2309 cl=0.0414 | inrange=0.1519 [w_cd2 70ep ] EXTRAP mean=0.1028 cd=0.1623 cl=0.0433 | inrange=0.1238
sleep 30; tail -1 /tmp/sweep2.out
DONE
grep n_aug2 /tmp/sweep2.out
[n_aug2 70ep ] EXTRAP mean=0.1453 cd=0.2502 cl=0.0405 | inrange=0.1361
command: str_replace path: /app/lib.py old_str: aug_vmax=None, aug_vmin=None, aug_p=CD_VEL_EXPONENT, n_aug=1, new_str: aug_vmax=None, aug_vmin=None, aug_p=CD_VEL_EXPONENT, n_aug=1, aug_cd_weight=1.0,
The file /app/lib.py has been edited successfully.
command: str_replace
path: /app/lib.py
old_str: perm = torch.randperm(m, generator=gen)
for s in range(0, m, bs):
idx = perm[s:s+bs]
xb, yb = xs[idx], ys[idx]
opt.zero_grad(set_to_none=True)
out = model(xb)
if loss_type == "huber":
per = F.huber_loss(out, yb, reduction="none", delta=huber_delta)
else:
per = (out - yb) ** 2
loss = (per * wvec.view(1, -1)).mean()
new_str: # per-row C_D weight: 1.0 for originals, aug_cd_weight for augmented rows
cd_rw = torch.ones(m)
if aug_vmax is not None and aug_cd_weight != 1.0:
cd_rw[n:] = aug_cd_weight
perm = torch.randperm(m, generator=gen)
for s in range(0, m, bs):
idx = perm[s:s+bs]
xb, yb = xs[idx], ys[idx]
opt.zero_grad(set_to_none=True)
out = model(xb)
if loss_type == "huber":
per = F.huber_loss(out, yb, reduction="none", delta=huber_delta)
else:
per = (out - yb) ** 2
rw = torch.ones_like(per)
rw[:, CD_I] = cd_rw[idx]
loss = (per * wvec.view(1, -1) * rw).mean()The file /app/lib.py has been edited successfully.
cd /app && python -c "import sys; sys.path.insert(0,'/app'); import lib; print('OK')" && cat > /tmp/sweep3.py << 'EOF'
import sys; sys.path.insert(0,"/app")
import numpy as np, lib
cfg = lib.load_cfg()
pts, tg = lib.load_split("train"); vpts, vtg = lib.load_split("val")
vel = pts[:,0,2]; thr=69.0
tp,tt = pts[vel<thr], tg[vel<thr]; ep,et = pts[vel>=thr], tg[vel>=thr]
def run(tag, reps=3, **kw):
E=[];V=[]
for s in range(reps):
model, norms = lib.train_model(tp, tt, cfg, seed=s, **kw)
r = lib.nrmse_report(lib.predict(model, ep, *norms), et)
rv = lib.nrmse_report(lib.predict(model, vpts, *norms), vtg)
E.append((r['mean_nrmse_std'],r['nrmse_std'][0],r['nrmse_std'][1])); V.append(rv['mean_nrmse_std'])
E=np.array(E).mean(0); V=np.mean(V)
print(f"[{tag:20s}] EXTRAP mean={E[0]:.4f} cd={E[1]:.4f} cl={E[2]:.4f} | inrange={V:.4f}",flush=True)
base=dict(aug_vmin=35,aug_vmax=90,aug_p=-0.21,epochs=110,swa_start=65)
run("cd_aug_w1.0", **base)
run("cd_aug_w0.5", aug_cd_weight=0.5, **base)
run("cd_aug_w0.3", aug_cd_weight=0.3, **base)
print("DONE",flush=True)
EOF
nohup python /tmp/sweep3.py > /tmp/sweep3.out 2>&1 &
echo "started $!"OK started 8096
sleep 295 && cat /tmp/sweep3.out
<system>Tool ran without output or errors</system>
sleep 250 && cat /tmp/sweep3.out
[cd_aug_w1.0 ] EXTRAP mean=0.0990 cd=0.1643 cl=0.0337 | inrange=0.1090
sleep 290 && cat /tmp/sweep3.out
[cd_aug_w1.0 ] EXTRAP mean=0.0990 cd=0.1643 cl=0.0337 | inrange=0.1090 [cd_aug_w0.5 ] EXTRAP mean=0.0923 cd=0.1503 cl=0.0344 | inrange=0.1026
sleep 200 && cat /tmp/sweep3.out
[cd_aug_w1.0 ] EXTRAP mean=0.0990 cd=0.1643 cl=0.0337 | inrange=0.1090 [cd_aug_w0.5 ] EXTRAP mean=0.0923 cd=0.1503 cl=0.0344 | inrange=0.1026 [cd_aug_w0.3 ] EXTRAP mean=0.0863 cd=0.1382 cl=0.0345 | inrange=0.1012 DONE
cd /app && cat > /tmp/probe2.py << 'EOF'
import sys; sys.path.insert(0,"/app")
import torch, numpy as np, lib
cfg = lib.load_cfg()
pts, tg = lib.load_split("train"); vel=pts[:,0,2]; thr=69.0
tp,tt = pts[vel<thr], tg[vel<thr]
sel = tp[:40].clone()
vs=[60,69,75,80,85,90]
def sweep(w):
m,n = lib.train_model(tp,tt,cfg,seed=0,aug_vmin=35,aug_vmax=90,aug_p=-0.21,epochs=110,swa_start=65,aug_cd_weight=w)
cds=[]
for v in vs:
s=sel.clone(); s[:,:,2]=v
cds.append(lib.predict(m,s,*n)[:,0].median().item())
base=cds[0]
print(f"w={w}: CDr", " ".join(f"{c/base:.4f}" for c in cds))
print("V :", " ".join(f"{v:6d}" for v in vs))
print("power V^-.21:", " ".join(f"{(v/60)**-0.21:.4f}" for v in vs))
for w in [1.0,0.5,0.3,0.15]:
sweep(w)
print("DONE",flush=True)
EOF
nohup python /tmp/probe2.py > /tmp/probe2.out 2>&1 &
echo "started $!"started 12126
sleep 295 && cat /tmp/probe2.out
<system>Tool ran without output or errors</system>
sleep 140 && cat /tmp/probe2.out
<system>Tool ran without output or errors</system>
sleep 60 && cat /tmp/probe2.out
<system>Tool ran without output or errors</system>
ps aux | grep "[p]robe2" >/dev/null && echo "still running"; sleep 60; cat /tmp/probe2.out
still running V : 60 69 75 80 85 90 power V^-.21: 1.0000 0.9711 0.9542 0.9414 0.9295 0.9184 w=1.0: CDr 1.0000 0.9695 0.9528 0.9387 0.9268 0.9181 w=0.5: CDr 1.0000 0.9652 0.9446 0.9295 0.9174 0.9093 w=0.3: CDr 1.0000 0.9721 0.9522 0.9378 0.9254 0.9179 w=0.15: CDr 1.0000 0.9688 0.9484 0.9337 0.9241 0.9188 DONE
cd /app && cat > /tmp/sweep4.py << 'EOF'
import sys; sys.path.insert(0,"/app")
import numpy as np, lib
cfg = lib.load_cfg()
pts, tg = lib.load_split("train"); vpts,vtg=lib.load_split("val")
vel = pts[:,0,2]
def evalsplit(thr_tr, thr_te, tag, wlist):
tp,tt = pts[vel<thr_tr], tg[vel<thr_tr]; ep,et = pts[vel>=thr_te], tg[vel>=thr_te]
print(f"== split train V<{thr_tr} (n={len(tp)}), test V>={thr_te} (n={len(ep)}, {ep[:,0,2].min():.0f}-{ep[:,0,2].max():.0f}) ==",flush=True)
for w in wlist:
E=[]
for s in range(3):
m,n=lib.train_model(tp,tt,cfg,seed=s,aug_vmin=35,aug_vmax=92,aug_p=-0.21,epochs=110,swa_start=65,aug_cd_weight=w)
r=lib.nrmse_report(lib.predict(m,ep,*n),et); E.append((r['mean_nrmse_std'],r['nrmse_std'][0],r['nrmse_std'][1]))
E=np.array(E).mean(0)
print(f" w={w}: mean={E[0]:.4f} cd={E[1]:.4f} cl={E[2]:.4f}",flush=True)
evalsplit(62,70,"extreme",[0.3,0.5])
print("DONE",flush=True)
EOF
nohup python /tmp/sweep4.py > /tmp/sweep4.out 2>&1 &
echo "started $!"started 13958
sleep 295 && cat /tmp/sweep4.out
== split train V<62 (n=440), test V>=70 (n=70, 70-75) == w=0.3: mean=0.0852 cd=0.1278 cl=0.0427
sleep 160 && cat /tmp/sweep4.out
== split train V<62 (n=440), test V>=70 (n=70, 70-75) == w=0.3: mean=0.0852 cd=0.1278 cl=0.0427
sleep 120 && cat /tmp/sweep4.out
== split train V<62 (n=440), test V>=70 (n=70, 70-75) == w=0.3: mean=0.0852 cd=0.1278 cl=0.0427 w=0.5: mean=0.0833 cd=0.1254 cl=0.0412 DONE
cd /app && cat > /tmp/final_val.py << 'EOF'
import sys; sys.path.insert(0,"/app")
import numpy as np, lib
cfg = lib.load_cfg()
pts, tg = lib.load_split("train"); vpts,vtg=lib.load_split("val")
vel = pts[:,0,2]; thr=69.0
tp,tt = pts[vel<thr], tg[vel<thr]; ep,et = pts[vel>=thr], tg[vel>=thr]
CFG=dict(aug_vmin=35,aug_vmax=92,aug_p=-0.21,epochs=125,swa_start=72,aug_cd_weight=0.4,n_aug=1)
E=[];V=[]
for s in range(4):
m,n=lib.train_model(tp,tt,cfg,seed=s,**CFG)
r=lib.nrmse_report(lib.predict(m,ep,*n),et); rv=lib.nrmse_report(lib.predict(m,vpts,*n),vtg)
E.append((r['mean_nrmse_std'],r['nrmse_std'][0],r['nrmse_std'][1],r['nrmse_rng'][0],r['nrmse_rng'][1],r['cd_nmae_mean']))
V.append(rv['mean_nrmse_std'])
E=np.array(E); m=E.mean(0); sd=E.std(0)
print(f"FINAL CONFIG, held-out V>=69 (4 seeds):")
print(f" mean_nrmse_std={m[0]:.4f}+-{sd[0]:.4f} cd={m[1]:.4f}+-{sd[1]:.4f} cl={m[2]:.4f}+-{sd[2]:.4f}")
print(f" nrmse_rng cd={m[3]:.4f} cl={m[4]:.4f} | cd_nmae={m[5]:.4f} | in-range val mean={np.mean(V):.4f}")
print("DONE",flush=True)
EOF
nohup python /tmp/final_val.py > /tmp/final_val.out 2>&1 &
echo "started $!"started 14020
command: create
path: /app/train_final.py
file_text: """Final trainer for the airfoil force-coefficient PointNet.
Strategy (see analysis): the scored cases sit at higher inlet velocity than any
training case. The data obeys a clean physics law -- C_D ~ V^(-0.21) (turbulent
skin-friction scaling) while C_L is velocity-independent. We exploit this with a
physics-informed velocity augmentation that rescales each case's inlet velocity
across a wide range [35, 92] m/s, scaling C_D by the power law and holding C_L
fixed. This turns the velocity extrapolation into interpolation: it teaches the
net the correct decreasing-C_D trend and the velocity-invariance of C_L, instead
of the spurious upward drift a plain PointNet extrapolates into.
C_D loss on augmented rows is down-weighted (0.4) so the real cases anchor the
absolute C_D level while the augmented rows still supply the (consistent) trend.
SWA over the training tail stabilizes the single checkpoint we must ship. The
architecture is untouched; only the four normalization tensors + state_dict are
saved.
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
import numpy as np
import torch
APP_DIR = Path(os.environ.get("APP_DIR", "/app"))
sys.path.insert(0, str(APP_DIR))
import lib # noqa: E402
CONFIG = dict(
aug_vmin=35.0, aug_vmax=92.0, aug_p=-0.21,
n_aug=1, aug_cd_weight=0.4,
epochs=130, swa_start=74,
bs=32, lr=1e-3, wd=1e-4, w_cd=1.0, w_cl=1.0,
jitter=0.0,
)
def main() -> None:
cfg = lib.load_cfg()
torch.manual_seed(int(cfg["seed"]))
tr_pts, tr_tg = lib.load_split("train")
va_pts, va_tg = lib.load_split("val")
# Train on train+val: both are in-range; the extra airfoil shapes improve
# geometry generalization, which helps C_D/C_L at every velocity.
pts = torch.cat([tr_pts, va_pts], 0)
tg = torch.cat([tr_tg, va_tg], 0)
print(f"[final] training cases: {pts.shape[0]} (train {tr_pts.shape[0]} + val {va_pts.shape[0]})", flush=True)
print(f"[final] config: {CONFIG}", flush=True)
model, norms = lib.train_model(pts, tg, cfg, seed=int(cfg["seed"]), **CONFIG)
fmean, fstd, tmean, tstd = norms
print(f"[final] feat_mean={fmean.tolist()}")
print(f"[final] feat_std ={fstd.tolist()}")
print(f"[final] targ_mean={tmean.tolist()} targ_std={tstd.tolist()}")
# Report fit on the available (in-range) labeled data for sanity.
for name, p, t in [("train", tr_pts, tr_tg), ("val", va_pts, va_tg)]:
r = lib.nrmse_report(lib.predict(model, p, *norms), t)
print(f"[final] {name}: mean_nrmse_std={r['mean_nrmse_std']:.4f} "
f"cd={r['nrmse_std'][0]:.4f} cl={r['nrmse_std'][1]:.4f}", flush=True)
out = APP_DIR / "outputs" / "model.pt"
lib.save_checkpoint(model, norms, out)
print(f"[final] wrote {out}", flush=True)
if __name__ == "__main__":
main()
New file created successfully at /app/train_final.py
sleep 180 && cat /tmp/final_val.out
<system>Tool ran without output or errors</system>
sleep 200 && cat /tmp/final_val.out
<system>Tool ran without output or errors</system>
ps aux | grep "[f]inal_val" >/dev/null && echo running; sleep 60; cat /tmp/final_val.out
running
sleep 90 && cat /tmp/final_val.out
FINAL CONFIG, held-out V>=69 (4 seeds): mean_nrmse_std=0.1043+-0.0305 cd=0.1755+-0.0635 cl=0.0331+-0.0032 nrmse_rng cd=0.0374 cl=0.0093 | cd_nmae=0.0258 | in-range val mean=0.0959 DONE
cd /app && cat > /tmp/swa_test.py << 'EOF'
import sys; sys.path.insert(0,"/app")
import numpy as np, lib
cfg = lib.load_cfg()
pts, tg = lib.load_split("train")
vel = pts[:,0,2]; thr=69.0
tp,tt = pts[vel<thr], tg[vel<thr]; ep,et = pts[vel>=thr], tg[vel>=thr]
def test(tag, **kw):
C=[]
for s in range(4):
m,n=lib.train_model(tp,tt,cfg,seed=s,aug_vmin=35,aug_vmax=92,aug_p=-0.21,aug_cd_weight=0.4,**kw)
r=lib.nrmse_report(lib.predict(m,ep,*n),et); C.append(r['nrmse_std'][0])
C=np.array(C); print(f"[{tag}] cd mean={C.mean():.4f} std={C.std():.4f} min={C.min():.4f} max={C.max():.4f}",flush=True)
test("ep125 swa72", epochs=125, swa_start=72)
test("ep170 swa85", epochs=170, swa_start=85)
print("DONE",flush=True)
EOF
nohup python /tmp/swa_test.py > /tmp/swa_test.out 2>&1 &
echo "started $!"started 16101
sleep 290 && cat /tmp/swa_test.out
<system>Tool ran without output or errors</system>
sleep 200 && cat /tmp/swa_test.out
<system>Tool ran without output or errors</system>
sleep 180 && cat /tmp/swa_test.out
[ep125 swa72] cd mean=0.1755 std=0.0635 min=0.1207 max=0.2779
sleep 160 && cat /tmp/swa_test.out
[ep125 swa72] cd mean=0.1755 std=0.0635 min=0.1207 max=0.2779
sleep 120 && cat /tmp/swa_test.out | tail -2
[ep125 swa72] cd mean=0.1755 std=0.0635 min=0.1207 max=0.2779
sleep 120 && cat /tmp/swa_test.out | tail -2
[ep125 swa72] cd mean=0.1755 std=0.0635 min=0.1207 max=0.2779
ps aux | grep "[s]wa_test" >/dev/null && echo "running"; sleep 90; cat /tmp/swa_test.out | tail -2
running [ep125 swa72] cd mean=0.1755 std=0.0635 min=0.1207 max=0.2779
sleep 120 && cat /tmp/swa_test.out | tail -2
[ep170 swa85] cd mean=0.1526 std=0.0397 min=0.1233 max=0.2201 DONE
command: str_replace path: /app/train_final.py old_str: epochs=130, swa_start=74, new_str: epochs=185, swa_start=90,
The file /app/train_final.py has been edited successfully.
cd /app && nohup python train_final.py > /tmp/train_final.out 2>&1 & echo "started $!"
started 20940
command: create
path: /app/verify_checkpoint.py
file_text: """Verify the saved checkpoint loads into the pinned architecture and runs,
mimicking the grader's inference contract exactly."""
from __future__ import annotations
import json
import sys
from pathlib import Path
import torch
APP = Path("/app")
sys.path.insert(0, str(APP))
from candidate_model import build_model
from dataset import load_split
def main() -> None:
cfg = json.load((APP / "task_config.json").open())
ckpt = torch.load(APP / "outputs" / "model.pt", map_location="cpu", weights_only=True)
# contract: all four norm tensors present with correct shapes
for k in ["state_dict", "feat_mean", "feat_std", "targ_mean", "targ_std"]:
assert k in ckpt, f"missing key {k}"
assert ckpt["feat_mean"].shape == (4,) and ckpt["feat_std"].shape == (4,)
assert ckpt["targ_mean"].shape == (2,) and ckpt["targ_std"].shape == (2,)
assert torch.isfinite(ckpt["feat_std"]).all() and (ckpt["feat_std"] > 0).all()
assert torch.isfinite(ckpt["targ_std"]).all() and (ckpt["targ_std"] > 0).all()
# load into the exact pinned architecture (strict)
model = build_model(cfg)
missing = model.load_state_dict(ckpt["state_dict"], strict=True)
print("[verify] state_dict loaded strict=True:", missing)
model.eval()
fmean, fstd = ckpt["feat_mean"], ckpt["feat_std"]
tmean, tstd = ckpt["targ_mean"], ckpt["targ_std"]
def infer(pts):
x = (pts - fmean.view(1, 1, -1)) / fstd.view(1, 1, -1)
with torch.no_grad():
out = model(x)
return out * tstd.view(1, -1) + tmean.view(1, -1)
# run on public_test (no labels) -- must be finite and physical
pts, _ = load_split(APP / "data" / "public_test")
pred = infer(pts)
assert torch.isfinite(pred).all(), "non-finite predictions!"
cd, cl = pred[:, 0], pred[:, 1]
print(f"[verify] public_test n={len(pts)} V range {pts[:,0,2].min():.1f}-{pts[:,0,2].max():.1f}")
print(f"[verify] pred C_D: min={cd.min():.5f} max={cd.max():.5f} mean={cd.mean():.5f}")
print(f"[verify] pred C_L: min={cl.min():.4f} max={cl.max():.4f} mean={cl.mean():.4f}")
# physical sanity: C_D positive and in a plausible band, C_L in training span
assert (cd > 0).all(), "negative drag!"
assert cd.max() < 0.1 and cd.min() > 0.002, "C_D out of plausible band"
assert cl.min() > -1.5 and cl.max() < 3.0, "C_L out of plausible band"
# report metrics on the labeled val split through the exact same path
vpts, vtg = load_split(APP / "data" / "val")
vp = infer(vpts)
err = vp - vtg
rmse = err.pow(2).mean(0).sqrt()
nrmse = rmse / vtg.std(0)
print(f"[verify] val nrmse_std: cd={nrmse[0]:.4f} cl={nrmse[1]:.4f} mean={nrmse.mean():.4f}")
print("[verify] OK - checkpoint satisfies the inference contract.")
if __name__ == "__main__":
main()
New file created successfully at /app/verify_checkpoint.py
sleep 120 && cat /tmp/train_final.out
[final] training cases: 710 (train 630 + val 80)
[final] config: {'aug_vmin': 35.0, 'aug_vmax': 92.0, 'aug_p': -0.21, 'n_aug': 1, 'aug_cd_weight': 0.4, 'epochs': 185, 'swa_start': 90, 'bs': 32, 'lr': 0.001, 'wd': 0.0001, 'w_cd': 1.0, 'w_cl': 1.0, 'jitter': 0.0}sleep 280 && cat /tmp/train_final.out
[final] training cases: 710 (train 630 + val 80)
[final] config: {'aug_vmin': 35.0, 'aug_vmax': 92.0, 'aug_p': -0.21, 'n_aug': 1, 'aug_cd_weight': 0.4, 'epochs': 185, 'swa_start': 90, 'bs': 32, 'lr': 0.001, 'wd': 0.0001, 'w_cd': 1.0, 'w_cl': 1.0, 'jitter': 0.0}
[final] feat_mean=[0.38364872336387634, 0.007897475734353065, 57.995487213134766, 0.07678443193435669]
[final] feat_std =[0.395596981048584, 0.033333562314510345, 15.351306915283203, 0.09548091143369675]
[final] targ_mean=[0.012543597258627415, 0.6618618965148926] targ_std=[0.005036944057792425, 0.5780647397041321]
[final] train: mean_nrmse_std=0.0846 cd=0.1426 cl=0.0265
[final] val: mean_nrmse_std=0.0592 cd=0.0927 cl=0.0256
[final] wrote /app/outputs/model.ptcd /app && python verify_checkpoint.py
[verify] state_dict loaded strict=True: <All keys matched successfully> [verify] public_test n=100 V range 75.0-81.6 [verify] pred C_D: min=0.00718 max=0.02951 mean=0.01194 [verify] pred C_L: min=-0.5056 max=1.7097 mean=0.6751 [verify] val nrmse_std: cd=0.0927 cl=0.0256 mean=0.0592 [verify] OK - checkpoint satisfies the inference contract.
cd /app && python -c "
import sys; sys.path.insert(0,'/app'); import torch, numpy as np, json
from candidate_model import build_model
from dataset import load_split
cfg=json.load(open('task_config.json'))
ck=torch.load('outputs/model.pt',weights_only=True)
m=build_model(cfg); m.load_state_dict(ck['state_dict']); m.eval()
fm,fs,tm,ts=ck['feat_mean'],ck['feat_std'],ck['targ_mean'],ck['targ_std']
def infer(p):
x=(p-fm.view(1,1,-1))/fs.view(1,1,-1)
with torch.no_grad(): o=m(x)
return o*ts.view(1,-1)+tm.view(1,-1)
# velocity response of FINAL model on 40 public_test airfoils
pt,_=load_split('data/public_test')
sel=pt[:40].clone()
print('Final model C_D velocity response (median over 40 test airfoils):')
prev=None; vs=[60,70,75,80,85,90,95]
row=[]
for v in vs:
s=sel.clone(); s[:,:,2]=v; row.append(infer(s)[:,0].median().item())
print(' V :',' '.join(f'{v:7d}' for v in vs))
print(' C_D:',' '.join(f'{c:7.5f}' for c in row))
print(' mono-decreasing:', all(row[i]>row[i+1] for i in range(len(row)-1)))
# Compare public_test preds to physics model fitted on train
tr_p,tr_t=load_split('data/train')
aoa=tr_p[:,0,3].numpy(); lv=np.log(tr_p[:,0,2].numpy()); lcd=np.log(tr_t[:,0].numpy())
X=np.stack([np.ones_like(aoa),aoa,aoa**2,lv],1); b,*_=np.linalg.lstsq(X,lcd,rcond=None)
pa=pt[:,0,3].numpy(); plv=np.log(pt[:,0,2].numpy())
Xp=np.stack([np.ones_like(pa),pa,pa**2,plv],1); phys=np.exp(Xp@b)
net=infer(pt)[:,0].numpy()
print()
print('public_test C_D: net mean=%.5f physics mean=%.5f corr=%.3f'%(net.mean(),phys.mean(),np.corrcoef(net,phys)[0,1]))
print(' median |net-phys|/phys = %.1f%%'%(100*np.median(np.abs(net-phys)/phys)))
"Final model C_D velocity response (median over 40 test airfoils): V : 60 70 75 80 85 90 95 C_D: 0.01030 0.00998 0.00985 0.00972 0.00961 0.00943 0.00925 mono-decreasing: True public_test C_D: net mean=0.01194 physics mean=0.01155 corr=0.936 median |net-phys|/phys = 7.9%
command: create
path: /app/select_final.py
file_text: """Train K candidates on train+val and ship the one closest to the cross-seed
consensus on the (unlabeled) public_test cases. Averaging predictions across
independent inits is not allowed (single checkpoint), but the candidate nearest
the consensus is the least likely to be an outlier draw -- a robust way to pick
one model given the C_D seed variance."""
from __future__ import annotations
import json
import sys
from pathlib import Path
import numpy as np
import torch
APP = Path("/app")
sys.path.insert(0, str(APP))
import lib
from candidate_model import build_model
from dataset import load_split
CONFIG = dict(
aug_vmin=35.0, aug_vmax=92.0, aug_p=-0.21,
n_aug=1, aug_cd_weight=0.4,
epochs=185, swa_start=90,
bs=32, lr=1e-3, wd=1e-4, w_cd=1.0, w_cl=1.0, jitter=0.0,
)
SEEDS = [20260518, 1, 2, 7, 13]
def main() -> None:
cfg = lib.load_cfg()
tr_pts, tr_tg = lib.load_split("train")
va_pts, va_tg = lib.load_split("val")
pts = torch.cat([tr_pts, va_pts], 0)
tg = torch.cat([tr_tg, va_tg], 0)
test_pts, _ = load_split(APP / "data" / "public_test")
preds, ckpts, mono = [], [], []
vs = [60, 70, 75, 80, 85, 90]
sel = test_pts[:40].clone()
for sd in SEEDS:
model, norms = lib.train_model(pts, tg, cfg, seed=sd, **CONFIG)
p = lib.predict(model, test_pts, *norms)
preds.append(p.numpy())
# record state for saving
fmean, fstd, tmean, tstd = norms
ckpts.append({
"state_dict": {k: v.cpu() for k, v in model.state_dict().items()},
"feat_mean": fmean, "feat_std": fstd, "targ_mean": tmean, "targ_std": tstd,
})
# monotonic C_D velocity response check
row = []
for v in vs:
s = sel.clone(); s[:, :, 2] = v
row.append(lib.predict(model, s, *norms)[:, 0].median().item())
is_mono = all(row[i] > row[i + 1] for i in range(len(row) - 1))
mono.append(is_mono)
rv = lib.nrmse_report(lib.predict(model, va_pts, *norms), va_tg)
print(f"[seed {sd}] mono_cd={is_mono} val_cd={rv['nrmse_std'][0]:.4f} "
f"test_cd_mean={p[:,0].mean():.5f} test_cl_mean={p[:,1].mean():.4f}", flush=True)
preds = np.stack(preds, 0) # [K, N, 2]
consensus = np.median(preds, 0) # [N, 2]
# per-target scale for normalization
scale = np.array([consensus[:, 0].std(), consensus[:, 1].std()])
scale = np.clip(scale, 1e-8, None)
dev = np.sqrt((((preds - consensus[None]) / scale[None, None]) ** 2).mean(axis=(1, 2)))
print("[select] per-seed deviation from consensus:",
{SEEDS[i]: round(float(dev[i]), 4) for i in range(len(SEEDS))}, flush=True)
# restrict to monotonic models, then pick the one closest to consensus
order = [i for i in np.argsort(dev) if mono[i]] or list(np.argsort(dev))
best = order[0]
print(f"[select] chosen seed={SEEDS[best]} (dev={dev[best]:.4f}, mono={mono[best]})", flush=True)
out = APP / "outputs" / "model.pt"
torch.save(ckpts[best], out)
print(f"[select] wrote {out}", flush=True)
if __name__ == "__main__":
main()
New file created successfully at /app/select_final.py
cd /app && cp outputs/model.pt outputs/model_backup_seed20260518.pt && ls -la outputs/ && nohup python select_final.py > /tmp/select.out 2>&1 & echo "started $!"
started 21826
sleep 290 && cat /tmp/select.out
[seed 20260518] mono_cd=True val_cd=0.0927 test_cd_mean=0.01194 test_cl_mean=0.6751
sleep 290 && cat /tmp/select.out
[seed 20260518] mono_cd=True val_cd=0.0927 test_cd_mean=0.01194 test_cl_mean=0.6751 [seed 1] mono_cd=True val_cd=0.0605 test_cd_mean=0.01187 test_cl_mean=0.6699 [seed 2] mono_cd=True val_cd=0.0743 test_cd_mean=0.01182 test_cl_mean=0.6697
sleep 290 && cat /tmp/select.out
[seed 20260518] mono_cd=True val_cd=0.0927 test_cd_mean=0.01194 test_cl_mean=0.6751
[seed 1] mono_cd=True val_cd=0.0605 test_cd_mean=0.01187 test_cl_mean=0.6699
[seed 2] mono_cd=True val_cd=0.0743 test_cd_mean=0.01182 test_cl_mean=0.6697
[seed 7] mono_cd=True val_cd=0.0683 test_cd_mean=0.01185 test_cl_mean=0.6734
[seed 13] mono_cd=True val_cd=0.0612 test_cd_mean=0.01187 test_cl_mean=0.6713
[select] per-seed deviation from consensus: {20260518: 0.0313, 1: 0.0417, 2: 0.0286, 7: 0.0197, 13: 0.0171}
[select] chosen seed=13 (dev=0.0171, mono=True)
[select] wrote /app/outputs/model.ptcd /app && python verify_checkpoint.py && echo "=== velocity response ===" && python -c "
import sys; sys.path.insert(0,'/app'); import torch, numpy as np, json
from candidate_model import build_model
from dataset import load_split
cfg=json.load(open('task_config.json')); ck=torch.load('outputs/model.pt',weights_only=True)
m=build_model(cfg); m.load_state_dict(ck['state_dict']); m.eval()
fm,fs,tm,ts=ck['feat_mean'],ck['feat_std'],ck['targ_mean'],ck['targ_std']
def infer(p):
x=(p-fm.view(1,1,-1))/fs.view(1,1,-1)
with torch.no_grad(): o=m(x)
return o*ts.view(1,-1)+tm.view(1,-1)
pt,_=load_split('data/public_test'); sel=pt[:40].clone()
vs=[50,60,70,75,80,85,90,95]; cd=[];cl=[]
for v in vs:
s=sel.clone(); s[:,:,2]=v; pr=infer(s); cd.append(pr[:,0].median().item()); cl.append(pr[:,1].median().item())
print('V :',' '.join(f'{v:7d}' for v in vs))
print('C_D:',' '.join(f'{c:7.5f}' for c in cd),'mono-dec:',all(cd[i]>cd[i+1] for i in range(len(cd)-1)))
print('C_L:',' '.join(f'{c:7.4f}' for c in cl),'(flat across V => vel-invariant)')
"[verify] state_dict loaded strict=True: <All keys matched successfully> [verify] public_test n=100 V range 75.0-81.6 [verify] pred C_D: min=0.00714 max=0.02876 mean=0.01187 [verify] pred C_L: min=-0.4963 max=1.6966 mean=0.6713 [verify] val nrmse_std: cd=0.0612 cl=0.0220 mean=0.0416 [verify] OK - checkpoint satisfies the inference contract. === velocity response === V : 50 60 70 75 80 85 90 95 C_D: 0.01060 0.01023 0.00991 0.00973 0.00962 0.00953 0.00941 0.00936 mono-dec: True C_L: 0.7696 0.7573 0.7554 0.7579 0.7637 0.7741 0.7888 0.8062 (flat across V => vel-invariant)
cd /app && python -c "
import torch
ck=torch.load('outputs/model.pt',weights_only=True)
print('keys:', list(ck.keys()))
nan=0; tot=0
for k,v in ck['state_dict'].items():
tot+=v.numel(); nan+=torch.isnan(v).sum().item()+torch.isinf(v).sum().item()
print(f'state_dict tensors={len(ck[\"state_dict\"])} params={tot} nan/inf={nan}')
for k in ['feat_mean','feat_std','targ_mean','targ_std']:
v=ck[k]; print(f' {k}: shape={tuple(v.shape)} dtype={v.dtype} vals={[round(x,5) for x in v.tolist()]}')
" && echo "--- cleanup ---" && rm -f outputs/model_backup_seed20260518.pt && ls -la outputs/keys: ['state_dict', 'feat_mean', 'feat_std', 'targ_mean', 'targ_std'] state_dict tensors=27 params=84741 nan/inf=0 feat_mean: shape=(4,) dtype=torch.float32 vals=[0.38365, 0.0079, 58.78354, 0.07678] feat_std: shape=(4,) dtype=torch.float32 vals=[0.3956, 0.03333, 15.60666, 0.09548] targ_mean: shape=(2,) dtype=torch.float32 vals=[0.0125, 0.66186] targ_std: shape=(2,) dtype=torch.float32 vals=[0.005, 0.57806] --- cleanup --- total 347 drwxr-sr-x 2 h2tagent h2tagent 4096 Jun 19 23:54 . drwxrws--- 5 h2tagent h2tagent 4096 Jun 19 23:38 .. -rw-r--r-- 1 h2tagent h2tagent 346611 Jun 19 23:51 model.pt
cd /app && python verify_checkpoint.py
[verify] state_dict loaded strict=True: <All keys matched successfully> [verify] public_test n=100 V range 75.0-81.6 [verify] pred C_D: min=0.00714 max=0.02876 mean=0.01187 [verify] pred C_L: min=-0.4963 max=1.6966 mean=0.6713 [verify] val nrmse_std: cd=0.0612 cl=0.0220 mean=0.0416 [verify] OK - checkpoint satisfies the inference contract.
No file changes, the agent left the workspace untouched.
# SyncVals normalized verifier summary # task: airfrans-high-reynolds-drag-extrapolation # attempt: 8 status: completed raw_score: 0.5 # reward rule: continuous: score 0.5 >= 0.5 # => reward = 1.0 (PASS) # # Note: these tasks score via an external hidden grader; no pytest/verifier # block is embedded in the agent transcript. The block below is the last # sizable tool-output captured in the run (supporting evidence). ---------------------------------------------------------------------- [verify] state_dict loaded strict=True: <All keys matched successfully> [verify] public_test n=100 V range 75.0-81.6 [verify] pred C_D: min=0.00714 max=0.02876 mean=0.01187 [verify] pred C_L: min=-0.4963 max=1.6966 mean=0.6713 [verify] val nrmse_std: cd=0.0612 cl=0.0220 mean=0.0416 [verify] OK - checkpoint satisfies the inference contract.
Reproduce this trial: git checkout 2f94510 && PYTHONPATH=src python3 scripts/build_site.py , then open trial/trial_0f94d593db944c63. Re-running the agent live requires EVAL_PLATFORM_ENABLE_OAUTH_SMOKE=1 and is non-deterministic.
Trial trial_0f94d593db944c63 · verifier authoritative; classifier explanatory.