Positive-envelope stability for complex state updates / bench_experiment.py

Mechanism confirmed, baseline not beaten

Raw ⬇ ZIP
  1import sys, json, time, random
  2from pathlib import Path
  3import numpy as np
  4import torch
  5import torch.nn as nn
  6
  7sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
  8from bench import get_dataset, make_report, evaluate, sweep_baseline
  9
 10HERE = Path(__file__).resolve().parent
 11DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'
 12
 13class FactorRNN(nn.Module):
 14    def __init__(self, hidden=32):
 15        super().__init__()
 16        self.inp = nn.Linear(3, hidden)
 17        self.f1 = nn.Parameter(0.42 * torch.eye(hidden) + 0.035 * torch.randn(hidden, hidden))
 18        self.f2 = nn.Parameter(0.92 * torch.eye(hidden) + 0.035 * torch.randn(hidden, hidden))
 19        self.head = nn.Linear(hidden, 1)
 20
 21    def transition(self):
 22        return self.f2 @ self.f1
 23
 24    def envelope(self):
 25        return self.f2.abs() @ self.f1.abs()
 26
 27    def forward(self, x):
 28        seq = x.view(x.shape[0], -1, 3)
 29        h = torch.zeros(x.shape[0], self.f1.shape[0], device=x.device, dtype=x.dtype)
 30        A = self.transition()
 31        for t in range(seq.shape[1]):
 32            h = torch.tanh(h @ A.T + self.inp(seq[:, t]))
 33        return self.head(h)
 34
 35
 36def seed_all(seed):
 37    random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
 38    if torch.cuda.is_available():
 39        torch.cuda.manual_seed_all(seed)
 40
 41
 42def fit(seed, lr, epochs, idea=False, beta=0.0, target=0.92):
 43    seed_all(seed)
 44    ds = get_dataset('dynamics', seed, n_train=400, n_test=200)
 45    model = FactorRNN().to(DEVICE)
 46    opt = torch.optim.Adam(model.parameters(), lr=lr)
 47    xtr, ytr = ds['xtr'].to(DEVICE), ds['ytr'].to(DEVICE)
 48    xte, yte = ds['xte'].to(DEVICE), ds['yte'].to(DEVICE)
 49    # Equal full-batch budget for both systems; CPU fallback handles CUDA errors.
 50    def run(dev):
 51        nonlocal model, opt, xtr, ytr, xte, yte
 52        model = model.to(dev); xtr, ytr, xte, yte = xtr.to(dev), ytr.to(dev), xte.to(dev), yte.to(dev)
 53        opt = torch.optim.Adam(model.parameters(), lr=lr)
 54        model.train()
 55        for _ in range(epochs):
 56            opt.zero_grad(set_to_none=True)
 57            pred = model(xtr)
 58            loss = (pred-ytr).pow(2).mean()
 59            if idea:
 60                P = model.envelope()
 61                # Cheap conservative infinity-norm envelope constraint plus cancellation reward.
 62                rows = P.sum(dim=1)
 63                env_pen = torch.relu(rows.max() - target).pow(2)
 64                A = model.transition()
 65                cancel = (P.sum() - A.abs().sum()) / (P.sum() + 1e-6)
 66                loss = loss + 0.08 * env_pen + beta * cancel
 67            if not torch.isfinite(loss):
 68                raise RuntimeError('nonfinite loss')
 69            loss.backward(); torch.nn.utils.clip_grad_norm_(model.parameters(), 10.0); opt.step()
 70        model.eval()
 71        with torch.no_grad():
 72            metric = float((model(xte)-yte).pow(2).mean().cpu())
 73        return metric
 74    try:
 75        metric = run(DEVICE)
 76    except Exception:
 77        DEVICE_CPU = 'cpu'
 78        metric = run(DEVICE_CPU)
 79    return metric, model, ds
 80
 81
 82def train_fn(cfg, idea=False):
 83    def f(seed):
 84        return fit(seed, cfg['lr'], cfg['epochs'], idea, cfg.get('beta', 0.0), cfg.get('target', 0.92))[0]
 85    return f
 86
 87
 88def signature(seed, cfg):
 89    metric, model, ds = fit(seed, cfg['lr'], cfg['epochs'], True, cfg.get('beta', 0.0), cfg.get('target', 0.92))
 90    with torch.no_grad():
 91        A = model.transition(); P = model.envelope()
 92        # Re-test the NN-scale prediction |Ax| <= P|x| on observed hidden probes.
 93        rng = torch.Generator(device=next(model.parameters()).device).manual_seed(seed+991)
 94        x = torch.randn(256, A.shape[0], generator=rng, device=A.device)
 95        lhs = (x @ A.T).abs(); rhs = x.abs() @ P.T
 96        violation = float((lhs-rhs).max().cpu())
 97        ratio = float((lhs/(rhs+1e-8)).max().cpu())
 98        row_env = float(P.sum(1).max().cpu())
 99        observed_A = float(A.abs().sum().cpu()); observed_P = float(P.sum().cpu())
100    return {'metric': metric, 'max_component_violation': violation,
101            'max_observed_ratio': ratio, 'predicted_bound': 'nonpositive violation',
102            'observed_envelope_row_sum': row_env, 'observed_sum_abs_A': observed_A,
103            'observed_sum_P': observed_P,
104            'confirmed': bool(violation <= 2e-5 and ratio <= 1.00002)}
105
106
107def main():
108    # Shared architecture and equal epochs. The union of learning rates is evaluated
109    # on both systems; baseline tuning uses the harness four-seed sweep, followed by
110    # full eight-seed evaluations for every shared lr.
111    base_grid = [{'lr': lr, 'epochs': 18, 'beta': 0.0}
112                 for lr in (0.0015, 0.003, 0.006)]
113    idea_grid = [{'lr': lr, 'epochs': 18, 'beta': beta, 'target': 0.92}
114                 for lr, beta in ((0.0015, 0.005), (0.003, 0.01), (0.006, 0.02))]
115    seeds = tuple(range(8))
116    t0 = time.perf_counter()
117    base = sweep_baseline(train_fn, base_grid)
118    # Full paired evaluations at every shared lr establish search-space parity.
119    base_full_by_lr = []
120    for cfg in base_grid:
121        r = evaluate(train_fn(cfg, False), seeds=seeds)
122        base_full_by_lr.append({'cfg': cfg, **r})
123    best_base_entry = min(base_full_by_lr, key=lambda z: z['mean'])
124    base['full_all_union'] = base_full_by_lr
125    base['best_cfg'] = best_base_entry['cfg']
126    base['full'] = {k: best_base_entry[k] for k in ('mean', 'std', 'per_seed', 'n')}
127
128    idea_all = []
129    for cfg in idea_grid:
130        r = evaluate(train_fn(cfg, True), seeds=seeds)
131        idea_all.append({'cfg': cfg, **r})
132    best_idea = min(idea_all, key=lambda z: z['mean'])
133    idea = {k: best_idea[k] for k in ('mean', 'std', 'per_seed', 'n')}
134    idea_cfg = best_idea['cfg']
135    sig = signature(0, idea_cfg)
136    report = make_report('dynamics', 'factor_rnn', base, idea,
137                         {'track_structure': 'controlled pendulum multi-step dynamics', **sig,
138                          'config': idea_cfg, 'wall_seconds': time.perf_counter()-t0})
139    report['idea_grid'] = idea_all
140    report['device'] = DEVICE
141    report['note'] = ('Baseline and idea are end-to-end trained systems with identical '
142                      'FactorRNN architecture; only envelope loss differs. All shared '
143                      'learning rates were evaluated on the same eight paired seeds.')
144    (HERE/'bench_report.json').write_text(json.dumps(report, indent=2))
145    print(json.dumps(report, indent=2))
146
147if __name__ == '__main__':
148    main()