tests/test.sh exit code (0 → resolved); the classification below is post-hoc and cannot change it.cd /app && ls -la && echo "---- task_config ----" && cat task_config.json && echo "---- feature_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
---- task_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"]
}
}
---- feature_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, deep
…[truncated]cd /app && python3 -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 && python3 -c "
import torch
def stats(name, pts):
flat = pts.reshape(-1,4)
print('====', name, '====')
for i,ch in enumerate(['x','y','vel','aoa']):
c = flat[:,i]
print(f' {ch:4s} min={c.min():.4f} max={c.max():.4f} mean={c.mean():.4f} std={c.std():.4f}')
# per-case velocity and aoa (broadcast) -> take point 0
vel = pts[:,0,2]; aoa = pts[:,0,3]
print(' per-case vel: min=%.4f max=%.4f'%(vel.min(),vel.max()))
print(' per-case aoa: min=%.4f max=%.4f (rad), deg min=%.3f max=%.3f'%(aoa.min(),aoa.max(), aoa.min()*57.2958, aoa.max()*57.2958))
for split in ['train','val','public_test']:
p = torch.load(f'data/{split}/split.pt', weights_only=True)
stats(split, p['points'])
"
==== 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.2830 max=74.9240 per-case aoa: min=-0.0862 max=0.2565 (rad), deg min=-4.940 max=14.697 ==== 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.4680 max=74.1620 per-case aoa: min=-0.0836 max=0.2512 (rad), deg min=-4.790 max=14.393 ==== 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.0300 max=81.5930 per-case aoa: min=-0.0857 max=0.2582 (rad), deg min=-4.910 max=14.794
cd /app && python3 -c "
import torch
for split in ['train','val']:
p = torch.load(f'data/{split}/split.pt', weights_only=True)
t = p['targets']
print('====', split, '====')
for i,name in enumerate(['C_D','C_L']):
c=t[:,i]
print(f' {name}: min={c.min():.5f} max={c.max():.5f} mean={c.mean():.5f} std={c.std():.5f}')
# correlations with vel and aoa
vel = p['points'][:,0,2]; aoa = p['points'][:,0,3]
import numpy as np
for i,name in enumerate(['C_D','C_L']):
c=t[:,i]
cv=np.corrcoef(c.numpy(), vel.numpy())[0,1]
ca=np.corrcoef(c.numpy(), aoa.numpy())[0,1]
print(f' corr({name},vel)={cv:.3f} corr({name},aoa)={ca:.3f}')
"
==== train ==== C_D: min=0.00727 max=0.04595 mean=0.01276 std=0.00510 C_L: min=-0.53363 max=1.85012 mean=0.65962 std=0.57620 corr(C_D,vel)=-0.154 corr(C_D,aoa)=0.739 corr(C_L,vel)=-0.022 corr(C_L,aoa)=0.943 ==== val ==== C_D: min=0.00756 max=0.03459 mean=0.01268 std=0.00533 C_L: min=-0.48027 max=1.80035 mean=0.67952 std=0.59777 corr(C_D,vel)=-0.298 corr(C_D,aoa)=0.806 corr(C_L,vel)=-0.102 corr(C_L,aoa)=0.939
cd /app && python3 -c "
import torch, numpy as np
p = torch.load('data/train/split.pt', weights_only=True)
t = p['targets'].numpy()
pts = p['points'].numpy()
vel = pts[:,0,2]; aoa = pts[:,0,3]
N = len(t)
# Linear regression of each target on [1, aoa, vel, aoa^2]
def fit(X, y):
beta, res, *_ = np.linalg.lstsq(X, y, rcond=None)
pred = X@beta
ss_res = ((y-pred)**2).sum(); ss_tot=((y-y.mean())**2).sum()
return beta, 1-ss_res/ss_tot
for i,name in enumerate(['C_D','C_L']):
y=t[:,i]
X1=np.column_stack([np.ones(N),aoa])
X2=np.column_stack([np.ones(N),aoa,vel])
X3=np.column_stack([np.ones(N),aoa,aoa**2,vel,vel**2, aoa*vel])
print(name)
print(' R2 aoa only :', round(fit(X1,y)[1],3))
print(' R2 aoa+vel :', round(fit(X2,y)[1],3))
print(' R2 quad aoa,vel :', round(fit(X3,y)[1],3))
"
C_D R2 aoa only : 0.546 R2 aoa+vel : 0.566 R2 quad aoa,vel : 0.766 C_L R2 aoa only : 0.89 R2 aoa+vel : 0.89 R2 quad aoa,vel : 0.892
cd /app && python3 -c "
import torch, numpy as np
p = torch.load('data/train/split.pt', weights_only=True)
pts = p['points'].numpy(); t=p['targets'].numpy()
# shape signature = rounded x,y coords
shapes = pts[:,:,:2].reshape(len(pts),-1)
# hash by rounding
import hashlib
sig = [hashlib.md5(np.round(s,5).tobytes()).hexdigest() for s in shapes]
from collections import defaultdict
groups=defaultdict(list)
for i,s in enumerate(sig): groups[s].append(i)
mult=[g for g in groups.values() if len(g)>1]
print('unique shapes:', len(groups), ' shapes with >1 case:', len(mult))
# distribution of group sizes
from collections import Counter
print('group size counts:', Counter(len(g) for g in groups.values()))
"
unique shapes: 630 shapes with >1 case: 0
group size counts: Counter({1: 630})cd /app && python3 -c "
import torch, time
print('torch', torch.__version__, 'cuda', torch.cuda.is_available())
print('cpu threads', torch.get_num_threads())
import os; print('cpus', os.cpu_count())
# quick timing of a forward/backward
import sys; sys.path.insert(0,'.')
import json
cfg=json.load(open('task_config.json'))
from candidate_model import build_model
m=build_model(cfg)
x=torch.randn(32,256,4)
t=time.time()
for _ in range(20):
y=m(x); y.sum().backward()
print('20 fwd/bwd bs32 on cpu: %.3fs'%(time.time()-t))
n=sum(p.numel() for p in m.parameters()); print('params', n)
"
torch 2.5.1+cu124 cuda False cpu threads 16 cpus 16 20 fwd/bwd bs32 on cpu: 1.595s params 83842
cd /app && python3 -c "
import torch, numpy as np
p = torch.load('data/train/split.pt', weights_only=True)
vel = p['points'][:,0,2].numpy()
print('quantiles of train velocity:')
for q in [0,10,25,50,75,80,85,90,95,100]:
print(f' {q:3d}%: {np.percentile(vel,q):.2f}')
# how many above various thresholds
for T in [60,63,65,67,70]:
print(f' vel>={T}: {(vel>=T).sum()} cases')
"
quantiles of train velocity:
0%: 31.28
10%: 35.42
25%: 41.74
50%: 53.27
75%: 64.14
80%: 66.60
85%: 68.00
90%: 70.35
95%: 72.63
100%: 74.92
vel>=60: 217 cases
vel>=63: 173 cases
vel>=65: 148 cases
vel>=67: 117 cases
vel>=70: 70 casescommand: create
path: /app/exp.py
file_text: """Experiment harness for the airfoil force-coefficient task.
Core idea: the scored set lives ABOVE the training velocity range, but the
provided val split shares the training range. So we build a *velocity
extrapolation* holdout from the training data itself (train on low velocities,
evaluate on the highest) to measure the thing that actually matters.
"""
from __future__ import annotations
import json, os, sys, copy, math
from pathlib import Path
import numpy as np
import torch
import torch.nn.functional as F
APP_DIR = Path('/app')
sys.path.insert(0, str(APP_DIR))
from candidate_model import build_model
from dataset import load_split
CFG = json.load(open(APP_DIR / 'task_config.json'))
def load_all():
ptr, ttr = load_split(APP_DIR / 'data' / 'train')
pva, tva = load_split(APP_DIR / 'data' / 'val')
return ptr, ttr, pva, tva
def standardize_stats(points, targets):
flat = points.reshape(-1, points.shape[-1])
fmean = flat.mean(0); fstd = flat.std(0).clamp_min(1e-8)
tmean = targets.mean(0); tstd = targets.std(0).clamp_min(1e-8)
return fmean, fstd, tmean, tstd
def nrmse_metrics(pred, true):
"""pred,true: [N,2] physical units. Returns dict of per-target nrmse etc."""
pred = pred.numpy() if torch.is_tensor(pred) else pred
true = true.numpy() if torch.is_tensor(true) else true
out = {}
names = ['cd', 'cl']
nrmse_std = []; nrmse_rng = []
for i, n in enumerate(names):
err = pred[:, i] - true[:, i]
rmse = np.sqrt((err ** 2).mean())
std = true[:, i].std(); rng = true[:, i].max() - true[:, i].min()
out[f'{n}_rmse'] = rmse
out[f'{n}_nrmse_std'] = rmse / (std + 1e-12)
out[f'{n}_nrmse_rng'] = rmse / (rng + 1e-12)
out[f'{n}_mae'] = np.abs(err).mean()
out[f'{n}_nmae'] = np.abs(err).mean() / (np.abs(true[:, i]).mean() + 1e-12)
nrmse_std.append(rmse / (std + 1e-12)); nrmse_rng.append(rmse / (rng + 1e-12))
out['mean_nrmse_std'] = float(np.mean(nrmse_std))
out['worst_nrmse_std'] = float(np.max(nrmse_std))
out['mean_nrmse_rng'] = float(np.mean(nrmse_rng))
return out
def augment(xb_phys, cfg, fmean, fstd):
"""xb_phys: [B,256,4] physical units. Returns standardized, augmented batch."""
B = xb_phys.shape[0]
x = xb_phys.clone()
# velocity augmentation (per-case multiplicative)
vfac = cfg.get('vel_mult', 0.0)
if vfac > 0:
f = torch.empty(B, 1).uniform_(1.0 - vfac, 1.0 + cfg.get('vel_mult_hi', vfac))
x[:, :, 2] = x[:, :, 2] * f
vadd = cfg.get('vel_add', 0.0)
if vadd > 0:
x[:, :, 2] = x[:, :, 2] + torch.randn(B, 1) * vadd
# geometry jitter on x,y
gj = cfg.get('geom_jitter', 0.0)
if gj > 0:
x[:, :, :2] = x[:, :, :2] + torch.randn(B, 256, 2) * gj
# aoa jitter
aj = cfg.get('aoa_jitter', 0.0)
if aj > 0:
x[:, :, 3] = x[:, :, 3] + torch.randn(B, 1) * aj
xs = (x - fmean.view(1, 1, -1)) / fstd.view(1, 1, -1)
return xs
def train_model(xtr_phys, ytr, cfg, fmean, fstd, tmean, tstd, seed=0, verbose=False):
torch.manual_seed(seed); np.random.seed(seed)
device = 'cpu'
model = build_model(CFG).to(device)
ys = (ytr - tmean.view(1, -1)) / tstd.view(1, -1)
epochs = cfg.get('epochs', 80)
bs = cfg.get('bs', 32)
lr = cfg.get('lr', 1e-3)
wd = cfg.get('wd', 1e-4)
opt = torch.optim.Adam(model.parameters(), lr=lr, weight_decay=wd)
n = xtr_phys.shape[0]
sched_type = cfg.get('sched', 'cosine')
steps_per = max(1, math.ceil(n / bs))
total_steps = epochs * steps_per
def lr_at(step):
warm = cfg.get('warmup_steps', 0)
if step < warm:
return lr * (step + 1) / warm
if sched_type == 'cosine':
prog = (step - warm) / max(1, total_steps - warm)
return cfg.get('lr_min', 1e-5) + 0.5 * (lr - cfg.get('lr_min', 1e-5)) * (1 + math.cos(math.pi * prog))
return lr
tw = cfg.get(
…[truncated]New file created successfully at /app/exp.py
command: create
path: /app/run_exp.py
file_text: from __future__ import annotations
import sys, json, time
import numpy as np
import torch
from exp import (load_all, standardize_stats, train_model, recompute_bn,
predict, nrmse_metrics)
def pseudo_split(points, targets, thresh):
vel = points[:, 0, 2]
tr = vel < thresh
te = vel >= thresh
return points[tr], targets[tr], points[te], targets[te]
def run(cfg, thresh=63.0, seeds=(0,), which='final', recompute=True, verbose=True):
ptr_all, ttr_all, pva, tva = load_all()
xtr, ytr, xte, yte = pseudo_split(ptr_all, ttr_all, thresh)
fmean, fstd, tmean, tstd = standardize_stats(xtr, ytr)
preds = []
for seed in seeds:
res, _ = train_model(xtr, ytr, cfg, fmean, fstd, tmean, tstd, seed=seed)
state = res[which] if which in res else res['final']
if recompute and which in ('ema', 'swa'):
state = recompute_bn(state, xtr, fmean, fstd, cfg)
p = predict(state, xte, fmean, fstd, tmean, tstd)
preds.append(p)
pred = torch.stack(preds).mean(0)
m = nrmse_metrics(pred, yte)
if verbose:
print(f" [thresh={thresh} which={which} seeds={len(seeds)}] "
f"mean_nrmse_std={m['mean_nrmse_std']:.4f} "
f"cd_nrmse={m['cd_nrmse_std']:.4f} cl_nrmse={m['cl_nrmse_std']:.4f} "
f"worst={m['worst_nrmse_std']:.4f} cd_nmae={m['cd_nmae']:.4f}")
return m
if __name__ == '__main__':
name = sys.argv[1] if len(sys.argv) > 1 else 'baseline'
configs = {
'baseline': dict(epochs=40, lr=1e-3, wd=1e-4, sched='const', loss='mse'),
'baseline_cos': dict(epochs=80, lr=1e-3, wd=1e-4, sched='cosine', loss='mse'),
}
cfg = configs[name]
t = time.time()
run(cfg, thresh=63.0, seeds=(0, 1, 2))
print(' time %.1fs' % (time.time() - t))
New file created successfully at /app/run_exp.py
cd /app && echo "=== baseline (const lr, MSE, 40ep, no aug) ===" && python3 run_exp.py baseline && echo "=== baseline cosine 80ep ===" && python3 run_exp.py baseline_cos
=== baseline (const lr, MSE, 40ep, no aug) === [thresh=63.0 which=final seeds=3] mean_nrmse_std=0.1713 cd_nrmse=0.1939 cl_nrmse=0.1487 worst=0.1939 cd_nmae=0.0494 time 57.1s === baseline cosine 80ep === [thresh=63.0 which=final seeds=3] mean_nrmse_std=0.1227 cd_nrmse=0.1414 cl_nrmse=0.1040 worst=0.1414 cd_nmae=0.0342 time 102.5s
command: create
path: /app/sweep.py
file_text: from __future__ import annotations
import time, itertools
import numpy as np
from run_exp import run
BASE = dict(epochs=80, lr=1e-3, wd=1e-4, sched='cosine', loss='mse')
def cfg(**kw):
c = dict(BASE); c.update(kw); return c
EXPS = {
'cos80': cfg(),
'vel_m10_30': cfg(vel_mult=0.10, vel_mult_hi=0.30),
'vel_m15_40': cfg(vel_mult=0.15, vel_mult_hi=0.40),
'vel_m20_50': cfg(vel_mult=0.20, vel_mult_hi=0.50),
'geom002': cfg(geom_jitter=0.002),
'vel_m15_40_g002': cfg(vel_mult=0.15, vel_mult_hi=0.40, geom_jitter=0.002),
'huber': cfg(loss='huber', huber_beta=1.0),
'vel_m15_40_huber': cfg(vel_mult=0.15, vel_mult_hi=0.40, loss='huber', huber_beta=1.0),
}
if __name__ == '__main__':
import sys
keys = sys.argv[1:] if len(sys.argv) > 1 else list(EXPS)
for k in keys:
t = time.time()
print(f'### {k}')
run(EXPS[k], thresh=63.0, seeds=(0, 1, 2))
print(' (%.1fs)' % (time.time() - t))
New file created successfully at /app/sweep.py
cd /app && python3 sweep.py vel_m10_30 vel_m15_40 vel_m20_50 2>&1
### vel_m10_30 [thresh=63.0 which=final seeds=3] mean_nrmse_std=0.1599 cd_nrmse=0.2634 cl_nrmse=0.0563 worst=0.2634 cd_nmae=0.0503 (103.8s) ### vel_m15_40 [thresh=63.0 which=final seeds=3] mean_nrmse_std=0.1952 cd_nrmse=0.3377 cl_nrmse=0.0528 worst=0.3377 cd_nmae=0.0624 (87.3s) ### vel_m20_50 [thresh=63.0 which=final seeds=3] mean_nrmse_std=0.2199 cd_nrmse=0.3889 cl_nrmse=0.0509 worst=0.3889 cd_nmae=0.0730 (102.7s)
cd /app && python3 -c "
import torch, numpy as np
p = torch.load('data/train/split.pt', weights_only=True)
t = p['targets'].numpy(); pts=p['points'].numpy()
vel=pts[:,0,2]; aoa=pts[:,0,3]
cd=t[:,0]; cl=t[:,1]
N=len(cd)
def fit(X,y):
beta,*_=np.linalg.lstsq(X,y,rcond=None); pred=X@beta
r2=1-((y-pred)**2).sum()/((y-y.mean())**2).sum(); return beta,r2
# log(cd) ~ 1, aoa, aoa^2, aoa^3, log(vel)
X=np.column_stack([np.ones(N),aoa,aoa**2,aoa**3,np.log(vel)])
b,r2=fit(X,np.log(cd))
print('log(cd) model R2=%.3f, coef log(vel)=%.3f'%(r2,b[-1]))
# cl ~ aoa poly + log vel
X2=np.column_stack([np.ones(N),aoa,aoa**2,aoa**3,np.log(vel)])
b2,r2b=fit(X2,cl)
print('cl model R2=%.3f, coef log(vel)=%.4f'%(r2b,b2[-1]))
# also linear vel coefficient on cd controlling aoa
X3=np.column_stack([np.ones(N),aoa,aoa**2,aoa**3,vel])
b3,r3=fit(X3,cd); print('cd linear-vel coef=%.3e per m/s (cd mean %.4f)'%(b3[-1],cd.mean()))
"
log(cd) model R2=0.818, coef log(vel)=-0.209 cl model R2=0.892, coef log(vel)=-0.0155 cd linear-vel coef=-5.982e-05 per m/s (cd mean 0.0128)
command: str_replace
path: /app/exp.py
old_str: def augment(xb_phys, cfg, fmean, fstd):
"""xb_phys: [B,256,4] physical units. Returns standardized, augmented batch."""
B = xb_phys.shape[0]
x = xb_phys.clone()
# velocity augmentation (per-case multiplicative)
vfac = cfg.get('vel_mult', 0.0)
if vfac > 0:
f = torch.empty(B, 1).uniform_(1.0 - vfac, 1.0 + cfg.get('vel_mult_hi', vfac))
x[:, :, 2] = x[:, :, 2] * f
vadd = cfg.get('vel_add', 0.0)
if vadd > 0:
x[:, :, 2] = x[:, :, 2] + torch.randn(B, 1) * vadd
# geometry jitter on x,y
gj = cfg.get('geom_jitter', 0.0)
if gj > 0:
x[:, :, :2] = x[:, :, :2] + torch.randn(B, 256, 2) * gj
# aoa jitter
aj = cfg.get('aoa_jitter', 0.0)
if aj > 0:
x[:, :, 3] = x[:, :, 3] + torch.randn(B, 1) * aj
xs = (x - fmean.view(1, 1, -1)) / fstd.view(1, 1, -1)
return xs
new_str: def augment(xb_phys, yb_phys, cfg, fmean, fstd):
"""xb_phys:[B,256,4] physical, yb_phys:[B,2] physical (C_D,C_L).
Returns (standardized augmented inputs, physical augmented targets).
Velocity is scaled multiplicatively; the C_D target is corrected along a
Reynolds power law C_D *= f**cd_exp (cd_exp~-0.21), C_L left unchanged.
"""
B = xb_phys.shape[0]
x = xb_phys.clone()
y = yb_phys.clone()
# velocity augmentation (per-case multiplicative) with Reynolds target fix
vfac = cfg.get('vel_mult', 0.0)
if vfac > 0:
f = torch.empty(B).uniform_(1.0 - vfac, 1.0 + cfg.get('vel_mult_hi', vfac))
x[:, :, 2] = x[:, :, 2] * f.view(B, 1)
cd_exp = cfg.get('cd_exp', -0.21)
y[:, 0] = y[:, 0] * f.pow(cd_exp)
cl_exp = cfg.get('cl_exp', 0.0)
if cl_exp != 0.0:
y[:, 1] = y[:, 1] * f.pow(cl_exp)
# geometry jitter on x,y
gj = cfg.get('geom_jitter', 0.0)
if gj > 0:
x[:, :, :2] = x[:, :, :2] + torch.randn(B, 256, 2) * gj
# aoa jitter
aj = cfg.get('aoa_jitter', 0.0)
if aj > 0:
x[:, :, 3] = x[:, :, 3] + torch.randn(B, 1) * aj
xs = (x - fmean.view(1, 1, -1)) / fstd.view(1, 1, -1)
return xs, yThe file /app/exp.py has been edited successfully.
command: str_replace
path: /app/exp.py
old_str: ys = (ytr - tmean.view(1, -1)) / tstd.view(1, -1)
epochs = cfg.get('epochs', 80)
new_str: epochs = cfg.get('epochs', 80)The file /app/exp.py has been edited successfully.
command: str_replace
path: /app/exp.py
old_str: idx = perm[s:s + bs]
xb = augment(xtr_phys[idx], cfg, fmean, fstd)
yb = ys[idx]
new_str: idx = perm[s:s + bs]
xb, yb_phys = augment(xtr_phys[idx], ytr[idx], cfg, fmean, fstd)
yb = (yb_phys - tmean.view(1, -1)) / tstd.view(1, -1)The file /app/exp.py has been edited successfully.
cd /app && python3 -c "
from run_exp import run
BASE=dict(epochs=80,lr=1e-3,wd=1e-4,sched='cosine',loss='mse')
def cfg(**kw):
c=dict(BASE); c.update(kw); return c
exps={
'no_aug': cfg(),
'reyn_m10_30': cfg(vel_mult=0.10, vel_mult_hi=0.30, cd_exp=-0.21),
'reyn_m15_40': cfg(vel_mult=0.15, vel_mult_hi=0.40, cd_exp=-0.21),
'reyn_m20_60': cfg(vel_mult=0.20, vel_mult_hi=0.60, cd_exp=-0.21),
}
for k,c in exps.items():
print('###',k)
run(c, thresh=63.0, seeds=(0,1,2))
"[stderr] Shell command timed out. Tool interaction exceeded 300 seconds.
restart: true
<system>tool has been restarted.</system>
cd /app && cat > runbg.py << 'EOF'
import sys, time, json
from run_exp import run
BASE=dict(epochs=80,lr=1e-3,wd=1e-4,sched='cosine',loss='mse')
def cfg(**kw):
c=dict(BASE); c.update(kw); return c
ALL={
'no_aug': cfg(),
'reyn_m10_30': cfg(vel_mult=0.10, vel_mult_hi=0.30, cd_exp=-0.21),
'reyn_m15_40': cfg(vel_mult=0.15, vel_mult_hi=0.40, cd_exp=-0.21),
'reyn_m20_60': cfg(vel_mult=0.20, vel_mult_hi=0.60, cd_exp=-0.21),
'reyn_m10_30_e0':cfg(vel_mult=0.10, vel_mult_hi=0.30, cd_exp=0.0),
}
keys=sys.argv[1:]
out=open('results.txt','a')
for k in keys:
t=time.time()
m=run(ALL[k], thresh=63.0, seeds=(0,1,2), verbose=False)
line=f"{k:18s} mean={m['mean_nrmse_std']:.4f} cd={m['cd_nrmse_std']:.4f} cl={m['cl_nrmse_std']:.4f} worst={m['worst_nrmse_std']:.4f} cd_nmae={m['cd_nmae']:.4f} ({time.time()-t:.0f}s)"
print(line); out.write(line+'\n'); out.flush()
out.close()
EOF
echo "written"written
cd /app && rm -f results.txt && export OMP_NUM_THREADS=16 && nohup python3 runbg.py no_aug reyn_m10_30 reyn_m15_40 reyn_m20_60 reyn_m10_30_e0 > sweep1.log 2>&1 & echo "started pid $!"
started pid 16276
cd /app && sleep 240 && echo "--- results so far ---" && cat results.txt 2>/dev/null
--- results so far --- no_aug mean=0.1227 cd=0.1414 cl=0.1040 worst=0.1414 cd_nmae=0.0342 (98s) reyn_m10_30 mean=0.1281 cd=0.2002 cl=0.0559 worst=0.2002 cd_nmae=0.0306 (112s)
cd /app && sleep 200 && cat results.txt 2>/dev/null
no_aug mean=0.1227 cd=0.1414 cl=0.1040 worst=0.1414 cd_nmae=0.0342 (98s) reyn_m10_30 mean=0.1281 cd=0.2002 cl=0.0559 worst=0.2002 cd_nmae=0.0306 (112s) reyn_m15_40 mean=0.1572 cd=0.2631 cl=0.0514 worst=0.2631 cd_nmae=0.0345 (111s) reyn_m20_60 mean=0.1808 cd=0.3131 cl=0.0485 worst=0.3131 cd_nmae=0.0366 (112s)
cd /app && sleep 30 && cat results.txt 2>/dev/null | tail -1
# investigate C_D error structure: no_aug vs reyn, where do errors concentrate?
python3 -c "
import torch, numpy as np
from run_exp import pseudo_split
from exp import load_all, standardize_stats, train_model, predict
ptr,ttr,_,_=load_all()
xtr,ytr,xte,yte=pseudo_split(ptr,ttr,63.0)
fmean,fstd,tmean,tstd=standardize_stats(xtr,ytr)
velte=xte[:,0,2].numpy(); aoate=xte[:,0,3].numpy()
def run1(cfg,seed=0):
res,_=train_model(xtr,ytr,cfg,fmean,fstd,tmean,tstd,seed=seed)
return predict(res['final'],xte,fmean,fstd,tmean,tstd).numpy()
BASE=dict(epochs=80,lr=1e-3,wd=1e-4,sched='cosine',loss='mse')
na=run1(BASE)
ra=run1(dict(BASE,vel_mult=0.10,vel_mult_hi=0.30,cd_exp=-0.21))
cd_true=yte[:,0].numpy()
for nm,pr in [('no_aug',na),('reyn',ra)]:
err=np.abs(pr[:,0]-cd_true)
order=np.argsort(-err)[:5]
print(nm,'top5 CD abs err:', np.round(err[order],4), 'at cd_true',np.round(cd_true[order],4),'vel',np.round(velte[order],1),'aoa',np.round(aoate[order],3))
" 2>&1 | tail -8reyn_m20_60 mean=0.1808 cd=0.3131 cl=0.0485 worst=0.3131 cd_nmae=0.0366 (112s) no_aug top5 CD abs err: [0.0031 0.0027 0.0023 0.0022 0.002 ] at cd_true [0.0269 0.0166 0.0199 0.0142 0.0232] vel [70.5 74.9 67. 74.6 65.9] aoa [0.233 0.208 0.232 0.188 0.242] reyn top5 CD abs err: [0.0079 0.0034 0.0033 0.0027 0.0025] at cd_true [0.0084 0.0232 0.0199 0.0235 0.0237] vel [65.6 65.9 67. 72.6 74.3] aoa [-0.079 0.242 0.232 0.244 0.251]
cd /app && cat > runbg2.py << 'EOF'
import sys, time
from run_exp import run
BASE=dict(epochs=80,lr=1e-3,wd=1e-4,sched='cosine',loss='mse')
def cfg(**kw):
c=dict(BASE); c.update(kw); return c
ALL={
'no_aug': cfg(),
'reyn_m05_25': cfg(vel_mult=0.05, vel_mult_hi=0.25, cd_exp=-0.21),
'reyn_m05_20': cfg(vel_mult=0.05, vel_mult_hi=0.20, cd_exp=-0.21),
'reyn_m10_30': cfg(vel_mult=0.10, vel_mult_hi=0.30, cd_exp=-0.21),
'reyn_m08_25_hub':cfg(vel_mult=0.08, vel_mult_hi=0.25, cd_exp=-0.21, loss='huber', huber_beta=0.5),
}
thresh=float(sys.argv[1]); keys=sys.argv[2:]
out=open('results.txt','a')
for k in keys:
t=time.time()
m=run(ALL[k], thresh=thresh, seeds=(0,1,2), verbose=False)
line=f"T{thresh:.0f} {k:16s} mean={m['mean_nrmse_std']:.4f} cd={m['cd_nrmse_std']:.4f} cl={m['cl_nrmse_std']:.4f} worst={m['worst_nrmse_std']:.4f} cd_nmae={m['cd_nmae']:.4f} meanRNG={m['mean_nrmse_rng']:.4f} ({time.time()-t:.0f}s)"
print(line); out.write(line+'\n'); out.flush()
out.close()
EOF
rm -f results.txt
export OMP_NUM_THREADS=16
nohup python3 runbg2.py 70 no_aug reyn_m05_25 reyn_m10_30 > sweepT70.log 2>&1 &
echo "started T70 pid $!"started T70 pid 27254
cd /app && sleep 290 && cat results.txt 2>/dev/null
T70 no_aug mean=0.1064 cd=0.1326 cl=0.0802 worst=0.1326 cd_nmae=0.0235 meanRNG=0.0257 (137s) T70 reyn_m05_25 mean=0.0827 cd=0.1192 cl=0.0462 worst=0.1192 cd_nmae=0.0248 meanRNG=0.0194 (137s)
cd /app && sleep 130 && cat results.txt 2>/dev/null | tail -2 # launch refinement around gentle aug at T70 and also T67 export OMP_NUM_THREADS=16 nohup python3 runbg2.py 70 reyn_m05_20 reyn_m08_25_hub > sweepT70b.log 2>&1 & echo "started T70b pid $!"
T70 reyn_m05_25 mean=0.0827 cd=0.1192 cl=0.0462 worst=0.1192 cd_nmae=0.0248 meanRNG=0.0194 (137s) T70 reyn_m10_30 mean=0.0861 cd=0.1284 cl=0.0438 worst=0.1284 cd_nmae=0.0268 meanRNG=0.0201 (132s) started T70b pid 27302
cd /app && echo "--- sweep1.log (T63, includes cd_exp=0) ---" && cat sweep1.log 2>/dev/null | grep -E "no_aug|reyn"
--- sweep1.log (T63, includes cd_exp=0) --- no_aug mean=0.1227 cd=0.1414 cl=0.1040 worst=0.1414 cd_nmae=0.0342 (98s) reyn_m10_30 mean=0.1281 cd=0.2002 cl=0.0559 worst=0.2002 cd_nmae=0.0306 (112s) reyn_m15_40 mean=0.1572 cd=0.2631 cl=0.0514 worst=0.2631 cd_nmae=0.0345 (111s) reyn_m20_60 mean=0.1808 cd=0.3131 cl=0.0485 worst=0.3131 cd_nmae=0.0366 (112s) reyn_m10_30_e0 mean=0.1599 cd=0.2634 cl=0.0563 worst=0.2634 cd_nmae=0.0503 (154s)
cd /app && sleep 280 && grep "T70" results.txt 2>/dev/null
T70 no_aug mean=0.1064 cd=0.1326 cl=0.0802 worst=0.1326 cd_nmae=0.0235 meanRNG=0.0257 (137s) T70 reyn_m05_25 mean=0.0827 cd=0.1192 cl=0.0462 worst=0.1192 cd_nmae=0.0248 meanRNG=0.0194 (137s) T70 reyn_m10_30 mean=0.0861 cd=0.1284 cl=0.0438 worst=0.1284 cd_nmae=0.0268 meanRNG=0.0201 (132s) T70 reyn_m05_20 mean=0.0825 cd=0.1156 cl=0.0494 worst=0.1156 cd_nmae=0.0253 meanRNG=0.0195 (132s) T70 reyn_m08_25_hub mean=0.0709 cd=0.1038 cl=0.0380 worst=0.1038 cd_nmae=0.0186 meanRNG=0.0166 (137s)
cd /app && cat > runbg3.py << 'EOF'
import sys, time
from run_exp import run
BASE=dict(epochs=80,lr=1e-3,wd=1e-4,sched='cosine')
def cfg(**kw):
c=dict(BASE); c.update(kw); return c
H=dict(vel_mult=0.08, vel_mult_hi=0.25, cd_exp=-0.21, loss='huber', huber_beta=0.5)
ALL={
'hub50': cfg(**H),
'hub30': cfg(**{**H,'huber_beta':0.3}),
'hub100': cfg(**{**H,'huber_beta':1.0}),
'hub50_ema': cfg(**{**H,'ema':0.995}),
'hub50_swa': cfg(**{**H,'swa_start_frac':0.6}),
'hub50_g002':cfg(**{**H,'geom_jitter':0.002}),
'hub50_e120':cfg(**{**H,'epochs':120}),
}
thresh=float(sys.argv[1]); which=sys.argv[2]; keys=sys.argv[3:]
out=open('results.txt','a')
for k in keys:
t=time.time()
m=run(ALL[k], thresh=thresh, seeds=(0,1,2), which=which)
line=f"T{thresh:.0f} {which:5s} {k:12s} mean={m['mean_nrmse_std']:.4f} cd={m['cd_nrmse_std']:.4f} cl={m['cl_nrmse_std']:.4f} cd_nmae={m['cd_nmae']:.4f} ({time.time()-t:.0f}s)"
out.write(line+'\n'); out.flush()
out.close()
EOF
export OMP_NUM_THREADS=16
nohup python3 runbg3.py 70 final hub30 hub100 > s3a.log 2>&1 &
echo "started pid $!"started pid 27356
cd /app && sleep 290 && grep -E "hub30|hub100" results.txt 2>/dev/null
T70 final hub30 mean=0.0690 cd=0.1000 cl=0.0380 cd_nmae=0.0183 (153s) T70 final hub100 mean=0.0767 cd=0.1117 cl=0.0418 cd_nmae=0.0197 (128s)
cd /app && export OMP_NUM_THREADS=16 nohup python3 runbg3.py 70 ema hub50_ema > s3b.log 2>&1 & nohup python3 runbg3.py 70 swa hub50_swa > s3c.log 2>&1 & echo "started ema+swa"
started ema+swa
cd /app && sleep 295 && grep -E "hub50_ema|hub50_swa" results.txt 2>/dev/null; echo "(still running if empty)"; ps aux | grep -c "[r]unbg3"
T70 swa hub50_swa mean=0.0870 cd=0.1225 cl=0.0515 cd_nmae=0.0281 (265s) T70 ema hub50_ema mean=0.0835 cd=0.1168 cl=0.0501 cd_nmae=0.0278 (270s) (still running if empty) 0
command: str_replace
path: /app/exp.py
old_str: def train_model(xtr_phys, ytr, cfg, fmean, fstd, tmean, tstd, seed=0, verbose=False):
torch.manual_seed(seed); np.random.seed(seed)
device = 'cpu'
model = build_model(CFG).to(device)
new_str: def train_model(xtr_phys, ytr, cfg, fmean, fstd, tmean, tstd, seed=0, verbose=False,
init_seed=None):
device = 'cpu'
# Optionally fix the initialization across runs (for weight-averaging soups)
torch.manual_seed(init_seed if init_seed is not None else seed)
model = build_model(CFG).to(device)
torch.manual_seed(seed); np.random.seed(seed)The file /app/exp.py has been edited successfully.
cd /app && cat > soup_test.py << 'EOF'
import time, torch
import numpy as np
from run_exp import pseudo_split
from exp import load_all, standardize_stats, train_model, recompute_bn, predict, nrmse_metrics
ptr,ttr,pva,tva=load_all()
xtr,ytr,xte,yte=pseudo_split(ptr,ttr,70.0)
fmean,fstd,tmean,tstd=standardize_stats(xtr,ytr)
BASE=dict(epochs=80,lr=1e-3,wd=1e-4,sched='cosine',vel_mult=0.08,vel_mult_hi=0.25,cd_exp=-0.21,loss='huber',huber_beta=0.3)
def avg_states(states):
out={}
for k in states[0]:
if states[0][k].dtype.is_floating_point:
out[k]=sum(s[k].float() for s in states)/len(states)
else:
out[k]=states[0][k].clone()
return out
# Per-seed (independent init) performance + prediction-ensemble
states_indep=[]; preds_indep=[]
for sd in [0,1,2]:
res,_=train_model(xtr,ytr,BASE,fmean,fstd,tmean,tstd,seed=sd)
states_indep.append(res['final'])
preds_indep.append(predict(res['final'],xte,fmean,fstd,tmean,tstd))
for i,sd in enumerate([0,1,2]):
m=nrmse_metrics(preds_indep[i],yte)
print(f"seed{sd} single: mean={m['mean_nrmse_std']:.4f} cd={m['cd_nrmse_std']:.4f} cl={m['cl_nrmse_std']:.4f}")
m=nrmse_metrics(torch.stack(preds_indep).mean(0),yte)
print(f"PRED-ENSEMBLE(3 indep): mean={m['mean_nrmse_std']:.4f} cd={m['cd_nrmse_std']:.4f} cl={m['cl_nrmse_std']:.4f}")
# Soup: same init, different noise seeds
states_soup=[]
for sd in [10,11,12,13]:
res,_=train_model(xtr,ytr,BASE,fmean,fstd,tmean,tstd,seed=sd,init_seed=777)
states_soup.append(res['final'])
soup=avg_states(states_soup)
soup=recompute_bn(soup,xtr,fmean,fstd,BASE)
m=nrmse_metrics(predict(soup,xte,fmean,fstd,tmean,tstd),yte)
print(f"SOUP(4 same-init): mean={m['mean_nrmse_std']:.4f} cd={m['cd_nrmse_std']:.4f} cl={m['cl_nrmse_std']:.4f}")
EOF
export OMP_NUM_THREADS=16
nohup python3 soup_test.py > soup.log 2>&1 &
echo "started soup pid $!"started soup pid 27492
cd /app && sleep 295 && cat soup.log 2>/dev/null; echo "(empty=still running)"
(empty=still running)
cd /app && sleep 120 && cat soup.log 2>/dev/null; echo "(empty=still running)"
seed0 single: mean=0.0673 cd=0.0893 cl=0.0452 seed1 single: mean=0.0623 cd=0.0865 cl=0.0382 seed2 single: mean=0.0753 cd=0.1041 cl=0.0464 PRED-ENSEMBLE(3 indep): mean=0.0647 cd=0.0890 cl=0.0404 SOUP(4 same-init): mean=0.0984 cd=0.1319 cl=0.0648 (empty=still running)
cd /app && cat > runbg4.py << 'EOF'
import sys, time
from run_exp import run
BASE=dict(epochs=80,lr=1e-3,wd=1e-4,sched='cosine',vel_mult=0.08,vel_mult_hi=0.25,cd_exp=-0.21,loss='huber',huber_beta=0.3)
def cfg(**kw):
c=dict(BASE); c.update(kw); return c
ALL={
'b30': cfg(),
'b20': cfg(huber_beta=0.2),
'b30_e120': cfg(epochs=120),
'b30_up30': cfg(vel_mult_hi=0.30),
'b30_e120_up30': cfg(epochs=120, vel_mult_hi=0.30),
}
thresh=float(sys.argv[1]); keys=sys.argv[2:]
out=open('results.txt','a')
for k in keys:
t=time.time()
m=run(ALL[k], thresh=thresh, seeds=(0,1,2))
line=f"T{thresh:.0f} FIN {k:14s} mean={m['mean_nrmse_std']:.4f} cd={m['cd_nrmse_std']:.4f} cl={m['cl_nrmse_std']:.4f} cd_nmae={m['cd_nmae']:.4f} ({time.time()-t:.0f}s)"
out.write(line+'\n'); out.flush()
out.close()
EOF
export OMP_NUM_THREADS=16
nohup python3 runbg4.py 70 b20 b30_e120 b30_up30 > s4a.log 2>&1 &
echo "started pid $!"started pid 27546
cd /app && sleep 295 && grep -E "FIN (b20|b30_e120|b30_up30)" results.txt 2>/dev/null
T70 FIN b20 mean=0.0656 cd=0.0901 cl=0.0411 cd_nmae=0.0167 (111s) T70 FIN b30_e120 mean=0.0583 cd=0.0821 cl=0.0346 cd_nmae=0.0160 (154s)
cd /app && sleep 90 && grep -E "FIN b30_up30" results.txt 2>/dev/null export OMP_NUM_THREADS=16 cat >> runbg4.py.ignore << 'EOF' EOF nohup python3 runbg4.py 70 b30_e120_up30 > s4b.log 2>&1 & echo "launched e120_up30"
T70 FIN b30_up30 mean=0.0641 cd=0.0895 cl=0.0387 cd_nmae=0.0167 (105s) launched e120_up30
cd /app && cat > runbg5.py << 'EOF'
import sys, time
from run_exp import run
BASE=dict(lr=1e-3,wd=1e-4,sched='cosine',vel_mult=0.08,vel_mult_hi=0.25,cd_exp=-0.21,loss='huber',huber_beta=0.3)
def cfg(**kw):
c=dict(BASE); c.update(kw); return c
ALL={
'b30_e160': cfg(epochs=160),
'b30_e200': cfg(epochs=200),
'b20_e120': cfg(epochs=120, huber_beta=0.2),
'b30_e120_wd3':cfg(epochs=120, wd=3e-4),
}
thresh=float(sys.argv[1]); keys=sys.argv[2:]
out=open('results.txt','a')
for k in keys:
t=time.time()
m=run(ALL[k], thresh=thresh, seeds=(0,1,2))
line=f"T{thresh:.0f} FIN {k:14s} mean={m['mean_nrmse_std']:.4f} cd={m['cd_nrmse_std']:.4f} cl={m['cl_nrmse_std']:.4f} cd_nmae={m['cd_nmae']:.4f} ({time.time()-t:.0f}s)"
out.write(line+'\n'); out.flush()
out.close()
EOF
export OMP_NUM_THREADS=16
nohup python3 runbg5.py 70 b30_e160 b20_e120 > s5a.log 2>&1 &
echo "started pid $!"started pid 27632
cd /app && sleep 298 && grep -E "FIN (b30_e160|b20_e120|b30_e120_up30)" results.txt 2>/dev/null
<system>Tool ran without output or errors</system>
cd /app && sleep 90 && grep -E "FIN (b30_e160|b20_e120|b30_e120_up30)" results.txt 2>/dev/null; echo "---tail---"; tail -3 results.txt
---tail--- T70 FIN b20 mean=0.0656 cd=0.0901 cl=0.0411 cd_nmae=0.0167 (111s) T70 FIN b30_e120 mean=0.0583 cd=0.0821 cl=0.0346 cd_nmae=0.0160 (154s) T70 FIN b30_up30 mean=0.0641 cd=0.0895 cl=0.0387 cd_nmae=0.0167 (105s)
cd /app && ps aux | grep "[r]unbg" | awk '{print $2, $11, $12, $13, $14}'; echo "---"; sleep 150 && grep -E "FIN (b30_e160|b20_e120|b30_e120_up30)" results.txt 2>/dev/null27594 python3 runbg4.py 70 b30_e120_up30 27632 python3 runbg5.py 70 b30_e160 --- T70 FIN b30_e120_up30 mean=0.0577 cd=0.0822 cl=0.0331 cd_nmae=0.0160 (426s) T70 FIN b30_e160 mean=0.0582 cd=0.0842 cl=0.0322 cd_nmae=0.0160 (490s)
cd /app && sleep 60 && grep -E "FIN b20_e120" results.txt 2>/dev/null; echo "launching robustness checks at T67 and T63"
cat > runbg6.py << 'EOF'
import sys, time
from run_exp import run
CHOSEN=dict(epochs=120,lr=1e-3,wd=1e-4,sched='cosine',vel_mult=0.08,vel_mult_hi=0.30,cd_exp=-0.21,loss='huber',huber_beta=0.3)
NOAUG=dict(epochs=120,lr=1e-3,wd=1e-4,sched='cosine',loss='mse')
ALL={'chosen':CHOSEN,'noaug120':NOAUG}
thresh=float(sys.argv[1]); keys=sys.argv[2:]
out=open('results.txt','a')
for k in keys:
t=time.time()
m=run(ALL[k], thresh=thresh, seeds=(0,1,2))
line=f"T{thresh:.0f} CHK {k:10s} mean={m['mean_nrmse_std']:.4f} cd={m['cd_nrmse_std']:.4f} cl={m['cl_nrmse_std']:.4f} cd_nmae={m['cd_nmae']:.4f} ({time.time()-t:.0f}s)"
out.write(line+'\n'); out.flush()
out.close()
EOF
export OMP_NUM_THREADS=16
nohup python3 runbg6.py 63 chosen noaug120 > s6a.log 2>&1 &
echo "started T63 check pid $!"launching robustness checks at T67 and T63 started T63 check pid 27700
cd /app && sleep 280 && grep -E "FIN b20_e120|T63 CHK" results.txt 2>/dev/null
T70 FIN b20_e120 mean=0.0594 cd=0.0828 cl=0.0359 cd_nmae=0.0155 (276s) T63 CHK chosen mean=0.0852 cd=0.1312 cl=0.0391 cd_nmae=0.0218 (221s)
cd /app && sleep 200 && grep -E "T63 CHK" results.txt 2>/dev/null
T63 CHK chosen mean=0.0852 cd=0.1312 cl=0.0391 cd_nmae=0.0218 (221s) T63 CHK noaug120 mean=0.1123 cd=0.1133 cl=0.1114 cd_nmae=0.0268 (113s)
command: create
path: /app/final_train.py
file_text: """Final trainer for the airfoil force-coefficient surrogate.
Strategy (validated on a velocity-extrapolation holdout built from the training
data, since the provided val split shares the training velocity range while the
scored set lies above it):
* Train on train+val combined for maximum data.
* Physics-grounded velocity augmentation: multiply each case's inlet velocity
by f~U(0.92,1.30) and correct the C_D target by f**(-0.21) (the measured
turbulent Reynolds exponent), leaving C_L unchanged (it is ~velocity
invariant). This both teaches the correct weak velocity dependence and
exposes the model to the higher-velocity regime it is scored on.
* Huber loss (beta=0.3) in standardized target space -> robust to C_D outliers.
* Cosine LR schedule, 120 epochs; plain final weights (EMA/SWA were worse).
* Train several seeds and ship the most representative one (closest to the
seed-ensemble mean on the public_test inputs) to hedge seed variance.
"""
from __future__ import annotations
import json, sys, time
from pathlib import Path
import numpy as np
import torch
APP = Path('/app'); sys.path.insert(0, str(APP))
from exp import (standardize_stats, train_model, predict, nrmse_metrics, CFG)
from dataset import load_split
CONFIG = dict(epochs=120, lr=1e-3, wd=1e-4, sched='cosine', lr_min=1e-5,
vel_mult=0.08, vel_mult_hi=0.30, cd_exp=-0.21,
loss='huber', huber_beta=0.3)
SEEDS = [0, 1, 2, 3, 4]
def main():
t0 = time.time()
ptr, ttr = load_split(APP / 'data' / 'train')
pva, tva = load_split(APP / 'data' / 'val')
points = torch.cat([ptr, pva], 0)
targets = torch.cat([ttr, tva], 0)
print(f'[final] combined train+val: {points.shape[0]} cases')
fmean, fstd, tmean, tstd = standardize_stats(points, targets)
print('[final] feat_mean', fmean.tolist())
print('[final] feat_std ', fstd.tolist())
print('[final] targ_mean', tmean.tolist(), 'targ_std', tstd.tolist())
ptest, _ = load_split(APP / 'data' / 'public_test')
# In-distribution val (for a sanity read only; NOT used for selection).
states, val_preds, test_preds = [], [], []
for sd in SEEDS:
res, _ = train_model(points, targets, CONFIG, fmean, fstd, tmean, tstd, seed=sd)
st = res['final']
states.append(st)
vp = predict(st, pva, fmean, fstd, tmean, tstd)
tp = predict(st, ptest, fmean, fstd, tmean, tstd)
val_preds.append(vp); test_preds.append(tp)
m = nrmse_metrics(vp, tva)
print(f'[final] seed={sd} in-dist val mean_nrmse={m["mean_nrmse_std"]:.4f} '
f'cd={m["cd_nrmse_std"]:.4f} cl={m["cl_nrmse_std"]:.4f} '
f'(t={time.time()-t0:.0f}s)', flush=True)
# Pick the most representative seed: closest to the ensemble-mean prediction
# on the public_test inputs (uses inputs only -> legitimate, label-free).
test_stack = torch.stack(test_preds) # [S, N, 2]
centroid = test_stack.mean(0)
# normalize each target channel by its spread so both count equally
scale = centroid.std(0, keepdim=True).clamp_min(1e-8)
dists = (((test_stack - centroid[None]) / scale[None]) ** 2).mean(dim=(1, 2))
best = int(torch.argmin(dists).item())
print('[final] seed centroid distances:', [round(float(d), 5) for d in dists])
print(f'[final] selected representative seed = {SEEDS[best]}')
# Report that seed's in-dist val and ensemble val for context.
m_sel = nrmse_metrics(val_preds[best], tva)
m_ens = nrmse_metrics(torch.stack(val_preds).mean(0), tva)
print(f'[final] selected seed in-dist val mean_nrmse={m_sel["mean_nrmse_std"]:.4f}')
print(f'[final] ensemble in-dist val mean_nrmse={m_ens["mean_nrmse_std"]:.4f}')
state = states[best]
out_dir = APP / 'outputs'; out_dir.mkdir(parents=True, exist_ok=True)
torch.save({
'state_dict': {k: v.cpu() for k, v in state.items()},
'feat_me
…[truncated]New file created successfully at /app/final_train.py
cd /app && python3 -c " import final_train as ft ft.CONFIG['epochs']=2 ft.SEEDS=[0,1] ft.main() " 2>&1 | tail -20
[final] combined train+val: 710 cases [final] feat_mean [0.38364875316619873, 0.007897475734353065, 53.19941711425781, 0.07678442448377609] [final] feat_std [0.39559754729270935, 0.03333360701799393, 12.652849197387695, 0.09548104554414749] [final] targ_mean [0.012753964401781559, 0.6618618965148926] targ_std [0.005126286298036575, 0.5782685279846191] [final] seed=0 in-dist val mean_nrmse=0.2980 cd=0.3831 cl=0.2129 (t=5s) [final] seed=1 in-dist val mean_nrmse=0.2875 cd=0.3797 cl=0.1953 (t=6s) [final] seed centroid distances: [0.00397, 0.00397] [final] selected representative seed = 0 [final] selected seed in-dist val mean_nrmse=0.2980 [final] ensemble in-dist val mean_nrmse=0.2859 [final] wrote /app/outputs/model.pt (total 6s)
command: create
path: /app/verify.py
file_text: """Grader-style verification: load the checkpoint strictly into the pinned
architecture, run a forward pass on public_test, de-standardize, sanity-check."""
from __future__ import annotations
import json, 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
cfg = json.load(open(APP / 'task_config.json'))
ckpt = torch.load(APP / 'outputs' / 'model.pt', weights_only=True)
assert set(['state_dict', 'feat_mean', 'feat_std', 'targ_mean', 'targ_std']).issubset(ckpt), 'missing keys'
for k, n in [('feat_mean', 4), ('feat_std', 4), ('targ_mean', 2), ('targ_std', 2)]:
t = ckpt[k]; assert tuple(t.shape) == (n,), f'{k} shape {t.shape}'
assert torch.isfinite(t).all(), f'{k} not finite'
assert (ckpt['feat_std'] > 0).all() and (ckpt['targ_std'] > 0).all(), 'non-positive std'
model = build_model(cfg)
missing, unexpected = model.load_state_dict(ckpt['state_dict'], strict=True)
print('strict load OK; missing=', missing, 'unexpected=', unexpected)
model.eval()
fmean = ckpt['feat_mean'].view(1, 1, -1); fstd = ckpt['feat_std'].view(1, 1, -1)
tmean = ckpt['targ_mean'].view(1, -1); tstd = ckpt['targ_std'].view(1, -1)
for split in ['val', 'public_test']:
pts, tgt = load_split(APP / 'data' / split)
with torch.no_grad():
out = model((pts - fmean) / fstd) * tstd + tmean
print(f'== {split} (n={pts.shape[0]}) ==')
print(' pred C_D: min=%.4f max=%.4f mean=%.4f' % (out[:, 0].min(), out[:, 0].max(), out[:, 0].mean()))
print(' pred C_L: min=%.4f max=%.4f mean=%.4f' % (out[:, 1].min(), out[:, 1].max(), out[:, 1].mean()))
assert torch.isfinite(out).all()
if tgt is not None:
import numpy as np
for i, nm in enumerate(['C_D', 'C_L']):
err = (out[:, i] - tgt[:, i]).numpy()
rmse = float((err ** 2).mean() ** 0.5)
nrmse = rmse / float(tgt[:, i].std())
print(f' {nm}: rmse={rmse:.5f} nrmse_std={nrmse:.4f}')
print('VERIFY OK')
New file created successfully at /app/verify.py
cd /app && python3 verify.py 2>&1 | tail -25
strict load OK; missing= [] unexpected= [] == val (n=80) == pred C_D: min=0.0084 max=0.0219 mean=0.0125 pred C_L: min=-0.1036 max=1.5723 mean=0.7091 C_D: rmse=0.00203 nrmse_std=0.3807 C_L: rmse=0.12647 nrmse_std=0.2116 == public_test (n=100) == pred C_D: min=0.0080 max=0.0212 mean=0.0114 pred C_L: min=-0.2978 max=1.5303 mean=0.6754 VERIFY OK
cd /app && export OMP_NUM_THREADS=16 && nohup python3 final_train.py > final.log 2>&1 & echo "started final training pid $!"; sleep 5; cat final.log
started final training pid 33607
cd /app && sleep 240 && cat final.log
[final] combined train+val: 710 cases [final] feat_mean [0.38364875316619873, 0.007897475734353065, 53.19941711425781, 0.07678442448377609] [final] feat_std [0.39559754729270935, 0.03333360701799393, 12.652849197387695, 0.09548104554414749] [final] targ_mean [0.012753964401781559, 0.6618618965148926] targ_std [0.005126286298036575, 0.5782685279846191] [final] seed=0 in-dist val mean_nrmse=0.0559 cd=0.0765 cl=0.0353 (t=91s) [final] seed=1 in-dist val mean_nrmse=0.0574 cd=0.0819 cl=0.0329 (t=151s) [final] seed=2 in-dist val mean_nrmse=0.0568 cd=0.0808 cl=0.0329 (t=207s)
cd /app && sleep 175 && cat final.log
[final] combined train+val: 710 cases [final] feat_mean [0.38364875316619873, 0.007897475734353065, 53.19941711425781, 0.07678442448377609] [final] feat_std [0.39559754729270935, 0.03333360701799393, 12.652849197387695, 0.09548104554414749] [final] targ_mean [0.012753964401781559, 0.6618618965148926] targ_std [0.005126286298036575, 0.5782685279846191] [final] seed=0 in-dist val mean_nrmse=0.0559 cd=0.0765 cl=0.0353 (t=91s) [final] seed=1 in-dist val mean_nrmse=0.0574 cd=0.0819 cl=0.0329 (t=151s) [final] seed=2 in-dist val mean_nrmse=0.0568 cd=0.0808 cl=0.0329 (t=207s) [final] seed=3 in-dist val mean_nrmse=0.0560 cd=0.0784 cl=0.0335 (t=260s) [final] seed=4 in-dist val mean_nrmse=0.0712 cd=0.1080 cl=0.0344 (t=314s) [final] seed centroid distances: [0.00146, 0.00112, 0.00048, 0.00069, 0.00112] [final] selected representative seed = 2 [final] selected seed in-dist val mean_nrmse=0.0568 [final] ensemble in-dist val mean_nrmse=0.0505 [final] wrote /app/outputs/model.pt (total 314s)
cd /app && python3 verify.py 2>&1 | tail -25
strict load OK; missing= [] unexpected= [] == val (n=80) == pred C_D: min=0.0076 max=0.0338 mean=0.0128 pred C_L: min=-0.4182 max=1.8086 mean=0.6825 C_D: rmse=0.00043 nrmse_std=0.0803 C_L: rmse=0.01952 nrmse_std=0.0327 == public_test (n=100) == pred C_D: min=0.0072 max=0.0278 mean=0.0119 pred C_L: min=-0.5005 max=1.7121 mean=0.6760 VERIFY OK
cd /app && python3 -c "
import json, torch, numpy as np, sys
sys.path.insert(0,'.')
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=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)
pts,_=load_split('data/public_test')
def pred(p):
with torch.no_grad(): return (m((p-fm)/fs)*ts+tm)
# take 8 cases, sweep velocity 70..95, report mean predicted CD, CL trend
base=pts[:8].clone()
print('vel meanC_D meanC_L (sweep on 8 public_test cases)')
cds=[]
for v in [65,70,75,80,85,90,95]:
p=base.clone(); p[:,:,2]=v
o=pred(p); cds.append(o[:,0].mean().item())
print(f'{v:3d} {o[:,0].mean():.5f} {o[:,1].mean():.4f}')
# compare CD ratio 75->90 to reynolds f^-0.21
r=(cds[5]/cds[2]); print('CD(90)/CD(75)=%.3f ideal (90/75)^-0.21=%.3f'%(r,(90/75)**-0.21))
print('check predictions finite & positive CD across sweep:', all(c>0 for c in cds))
"vel meanC_D meanC_L (sweep on 8 public_test cases) 65 0.01185 0.6598 70 0.01169 0.6595 75 0.01154 0.6590 80 0.01139 0.6593 85 0.01125 0.6605 90 0.01112 0.6622 95 0.01101 0.6630 CD(90)/CD(75)=0.964 ideal (90/75)^-0.21=0.962 check predictions finite & positive CD across sweep: True
cd /app && ls -la outputs/ && python3 -c "
import torch
ck=torch.load('outputs/model.pt', weights_only=True)
print('keys:', list(ck.keys()))
for k in ['feat_mean','feat_std','targ_mean','targ_std']:
print(f' {k}: shape={tuple(ck[k].shape)} dtype={ck[k].dtype} vals={[round(float(x),4) for x in ck[k]]}')
print('state_dict tensors:', len(ck['state_dict']))
print('total params:', sum(v.numel() for v in ck['state_dict'].values() if v.dtype.is_floating_point))
"total 347 drwxr-sr-x 2 h2tagent h2tagent 4096 Jun 19 22:51 . drwxrws--- 5 h2tagent h2tagent 4096 Jun 19 22:51 .. -rw-r--r-- 1 h2tagent h2tagent 346611 Jun 19 22:57 model.pt keys: ['state_dict', 'feat_mean', 'feat_std', 'targ_mean', 'targ_std'] feat_mean: shape=(4,) dtype=torch.float32 vals=[0.3836, 0.0079, 53.1994, 0.0768] feat_std: shape=(4,) dtype=torch.float32 vals=[0.3956, 0.0333, 12.6528, 0.0955] targ_mean: shape=(2,) dtype=torch.float32 vals=[0.0128, 0.6619] targ_std: shape=(2,) dtype=torch.float32 vals=[0.0051, 0.5783] state_dict tensors: 27 total params: 84738
No file changes, the agent left the workspace untouched.
# SyncVals normalized verifier summary # task: airfrans-high-reynolds-drag-extrapolation # attempt: 5 status: completed raw_score: 0.46 # reward rule: continuous: score 0.46 < 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). ---------------------------------------------------------------------- total 347 drwxr-sr-x 2 h2tagent h2tagent 4096 Jun 19 22:51 . drwxrws--- 5 h2tagent h2tagent 4096 Jun 19 22:51 .. -rw-r--r-- 1 h2tagent h2tagent 346611 Jun 19 22:57 model.pt keys: ['state_dict', 'feat_mean', 'feat_std', 'targ_mean', 'targ_std'] feat_mean: shape=(4,) dtype=torch.float32 vals=[0.3836, 0.0079, 53.1994, 0.0768] feat_std: shape=(4,) dtype=torch.float32 vals=[0.3956, 0.0333, 12.6528, 0.0955] targ_mean: shape=(2,) dtype=torch.float32 vals=[0.0128, 0.6619] targ_std: shape=(2,) dtype=torch.float32 vals=[0.0051, 0.5783] state_dict tensors: 27 total params: 84738
Reproduce this trial: git checkout 2f94510 && PYTHONPATH=src python3 scripts/build_site.py , then open trial/trial_951a5d1420f24896. Re-running the agent live requires EVAL_PLATFORM_ENABLE_OAUTH_SMOKE=1 and is non-deterministic.
Trial trial_951a5d1420f24896 · verifier authoritative; classifier explanatory.