Wittrick–Williams Mode Enumerator / run_bench.py

Unverified

Raw ⬇ ZIP
  1import sys, json, random
  2import numpy as np
  3import torch
  4from scipy.linalg import eigh
  5sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
  6from bench import make_model, sweep_baseline, evaluate, make_report
  7from custom_beam_track import get_dataset, matrices
  8
  9SEEDS = tuple(range(8))
 10GRID = [
 11    {'lr': 1e-3, 'epochs': 30},
 12    {'lr': 3e-3, 'epochs': 30},
 13    {'lr': 1e-3, 'epochs': 50},
 14]
 15TARGET = 3  # one-based mode index
 16
 17
 18def seed_all(s):
 19    random.seed(s); np.random.seed(s); torch.manual_seed(s)
 20
 21
 22def ww_bracket(scale, target=TARGET, tol=2e-4):
 23    K, M = matrices(float(scale))
 24    def inertia(w):
 25        A = K - (w*w)*M
 26        ev = np.linalg.eigvalsh(A)
 27        return int(np.sum(ev < -1e-10 * max(1., np.max(np.abs(A)))))
 28    exact = np.sqrt(np.maximum(eigh(K, M, eigvals_only=True), 0.0))
 29    upper = float(exact[-1] * 1.05); lo = 0.0
 30    grid = np.linspace(0.0, upper, 180)
 31    for a, b in zip(grid[:-1], grid[1:]):
 32        if inertia(a) == target-1 and inertia(b) >= target:
 33            lo, upper = float(a), float(b); break
 34    while upper - lo > tol:
 35        mid = (lo + upper) / 2
 36        if inertia(mid) < target: lo = mid
 37        else: upper = mid
 38    return lo, upper, exact
 39
 40
 41def as_tensors(d):
 42    return (torch.tensor(d['xtr'], dtype=torch.float32),
 43            torch.tensor(d['ytr'], dtype=torch.float32),
 44            torch.tensor(d['xte'], dtype=torch.float32),
 45            torch.tensor(d['yte'], dtype=torch.float32))
 46
 47
 48def train_one(seed, cfg, idea=False, inspect=False):
 49    seed_all(seed)
 50    d = get_dataset(seed, 400, 200)
 51    xtr, ytr, xte, yte = as_tensors(d)
 52    # Preprocessing is deterministic and independent of the trained model.
 53    brackets = np.asarray([ww_bracket(x[0])[0:2] for x in xtr.numpy()], np.float32)
 54    test_brackets = np.asarray([ww_bracket(x[0])[0:2] for x in xte.numpy()], np.float32)
 55    device = 'cuda' if torch.cuda.is_available() else 'cpu'
 56    try:
 57        net = make_model('mlp_tiny', (1,), 1).to(device)
 58        opt = torch.optim.Adam(net.parameters(), lr=cfg['lr'])
 59        xx, yy = xtr.to(device), ytr.to(device)
 60        bb = torch.tensor(brackets, device=device)
 61        for epoch in range(cfg['epochs']):
 62            opt.zero_grad(set_to_none=True)
 63            pred = net(xx)
 64            loss = torch.mean((pred - yy)**2)
 65            if idea:
 66                # Same MLP and supervised task; only WW certified box loss differs.
 67                low, high = bb[:, :1], bb[:, 1:]
 68                box = torch.relu(low-pred)**2 + torch.relu(pred-high)**2
 69                loss = loss + cfg.get('box_weight', 100.0) * box.mean()
 70            loss.backward(); opt.step()
 71        with torch.no_grad():
 72            pred = net(xte.to(device)).cpu().numpy().reshape(-1)
 73        metric = float(np.mean((pred - yte.numpy().reshape(-1))**2))
 74        if inspect:
 75            inside = ((pred >= test_brackets[:,0]) & (pred <= test_brackets[:,1]))
 76            return metric, {'pred': pred, 'inside': inside, 'brackets': test_brackets}
 77        return metric
 78    except Exception:
 79        if device == 'cuda':
 80            torch.cuda.empty_cache()
 81            torch.set_default_device('cpu')
 82            return train_one(seed, cfg, idea, inspect)
 83        raise
 84
 85
 86def main():
 87    # Baseline sweep uses exactly the same union of learning rates/epochs as idea.
 88    base = sweep_baseline(lambda cfg: (lambda s: train_one(s, cfg, False)), GRID)
 89    best = base['best_cfg']
 90    idea_grid = [dict(c, box_weight=100.0) for c in GRID]
 91    # The idea sweep is intentionally three nearby settings; baseline evaluated all shared lr/epoch settings.
 92    idea_runs = []
 93    for cfg in idea_grid:
 94        r = evaluate(lambda s, c=cfg: train_one(s, c, True))
 95        idea_runs.append({'cfg': cfg, 'result': r})
 96    idea_block = min(idea_runs, key=lambda z: z['result']['mean'])
 97    idea = idea_block['result']
 98
 99    # Signature is measured on predictions of the selected trained idea models, not analytically.
100    sigs=[]
101    for s in SEEDS:
102        _, info = train_one(s, idea_block['cfg'], True, True)
103        sigs.append(float(np.mean(info['inside'])))
104    # Stage-1 prediction: certified box should prevent frequency leaving its mode interval.
105    signature = {
106        'prediction': 'WW box constraint keeps predicted frequency inside the one-mode certified bracket',
107        'predicted_inside_rate': 1.0,
108        'observed_inside_rate_mean': float(np.mean(sigs)),
109        'observed_inside_rate_per_seed': sigs,
110        'confirmed': bool(np.mean(sigs) >= 0.95)
111    }
112    report = make_report('custom_beam_eigenfrequency', 'mlp_tiny', base, idea,
113        {'custom_track': {'name':'beam_eigenfrequency','file':'custom_beam_track.py','domain':'structural_eigenproblem'},
114         'idea_sweep': idea_runs, 'mechanism_signature': signature})
115    with open('bench_report.json','w') as f: json.dump(report, f, indent=2)
116    print(json.dumps(report, indent=2))
117
118if __name__ == '__main__': main()