Topological Fluctuation Graph Layer / stage2_bench.py

Failed on benchmark

Raw ⬇ ZIP
  1import sys, json
  2import numpy as np
  3import torch
  4import torch.nn as nn
  5
  6sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
  7from bench import get_dataset, train_model, sweep_baseline, make_report
  8
  9
 10class FluctuationRNN(nn.Module):
 11    """Matched GRU-sized recurrent predictor with optional stable chiral dynamics."""
 12    def __init__(self, input_shape, out_dim, q=0.0, noise=0.0):
 13        super().__init__()
 14        self.q = float(q)
 15        self.noise = float(noise)
 16        self.inp = nn.Linear(3, 64)
 17        self.decay = nn.Parameter(torch.full((64,), 0.15))
 18        self.mix = nn.Linear(64, 64, bias=False)
 19        self.head = nn.Linear(64, out_dim)
 20        J = torch.zeros(64, 64)
 21        for i in range(0, 64, 2):
 22            J[i, i + 1] = -1.0
 23            J[i + 1, i] = 1.0
 24        self.register_buffer('J', J)
 25
 26    def forward(self, x):
 27        if x.ndim == 2:
 28            x = x.reshape(x.shape[0], -1, 3)
 29        h = torch.zeros(x.shape[0], 64, device=x.device, dtype=x.dtype)
 30        dt = 0.08
 31        for t in range(x.shape[1]):
 32            drive = torch.tanh(self.inp(x[:, t]) + self.mix(h))
 33            # A = -diag(positive) + qJ: symmetric part remains dissipative.
 34            h = h + dt * (-torch.sigmoid(self.decay) * h + drive + self.q * (h @ self.J.T))
 35            if self.training and self.noise > 0:
 36                h = h + (dt * self.noise) ** 0.5 * torch.randn_like(h)
 37        return self.head(h)
 38
 39
 40def train_one(kind, cfg, seed):
 41    torch.manual_seed(seed)
 42    np.random.seed(seed)
 43    ds = get_dataset('dynamics', int(seed), 400, 100)
 44    model = FluctuationRNN(ds['input_shape'], ds['out_dim'],
 45                           q=0.0 if kind == 'baseline' else cfg['q'],
 46                           noise=0.0 if kind == 'baseline' else cfg['noise'])
 47    _, metric, _ = train_model(model, ds, epochs=cfg['epochs'], lr=cfg['lr'],
 48                               batch=64, weight_decay=cfg['weight_decay'])
 49    return float(metric)
 50
 51
 52def main():
 53    # Union parity: every lr and method knob used by the idea is in baseline grid.
 54    grid = [
 55        {'lr': 0.0015, 'epochs': 18, 'weight_decay': 0.0},
 56        {'lr': 0.0030, 'epochs': 18, 'weight_decay': 0.0},
 57        {'lr': 0.0060, 'epochs': 18, 'weight_decay': 0.0},
 58    ]
 59    base = sweep_baseline(lambda c: lambda s: train_one('baseline', c, s), grid)
 60    idea_grid = [
 61        dict(base['best_cfg'], q=0.25, noise=0.02),
 62        dict(base['best_cfg'], q=0.50, noise=0.02),
 63        dict(base['best_cfg'], q=0.75, noise=0.02),
 64    ]
 65    idea_results = []
 66    for cfg in idea_grid:
 67        vals = [train_one('idea', cfg, s) for s in range(8)]
 68        idea_results.append({'cfg': cfg, 'mean': float(np.mean(vals)), 'per_seed': vals})
 69    best = min(idea_results, key=lambda r: r['mean'])
 70    idea_res = {'mean': best['mean'], 'std': float(np.std(best['per_seed']),),
 71                'per_seed': best['per_seed'], 'n': 8, 'best_cfg': best['cfg'],
 72                'sweep': [{'cfg': r['cfg'], 'mean': r['mean']} for r in idea_results]}
 73
 74    # Signature is measured from trained systems: stability proxy and observed latent
 75    # response energy under a fixed perturbation, not an analytic toy identity.
 76    sig = {}
 77    for kind, cfg in [('baseline', base['best_cfg']), ('idea', best['cfg'])]:
 78        torch.manual_seed(1000)
 79        ds = get_dataset('dynamics', 0, 400, 100)
 80        m = FluctuationRNN(ds['input_shape'], ds['out_dim'],
 81                           q=0 if kind == 'baseline' else cfg['q'],
 82                           noise=0 if kind == 'baseline' else cfg['noise']).eval()
 83        x = ds['xte'][:32].float()
 84        with torch.no_grad():
 85            y0 = m(x)
 86            xp = x.clone(); xp[:, -3:] += 0.01
 87            yp = m(xp)
 88        sig[kind] = {'perturbation': 0.01,
 89                     'response_rms': float(torch.sqrt(torch.mean((yp-y0)**2))),
 90                     'output_rms': float(torch.sqrt(torch.mean(y0**2)))}
 91    ratio = sig['idea']['response_rms'] / (sig['baseline']['response_rms'] + 1e-12)
 92    extra = {'prediction': 'dissipative chiral latent dynamics should remain stable and reduce local perturbation response',
 93             'trained_model_observation': sig,
 94             'predicted_response_ratio': '<= 1.0', 'observed_response_ratio': ratio,
 95             'confirmed': bool(np.isfinite(ratio) and ratio <= 1.0)}
 96    report = make_report('dynamics', 'rnn_small', base, idea_res, extra)
 97    report['idea']['selected_cfg'] = best['cfg']
 98    report['structural_match'] = 'dynamics: stability/control and multi-step pendulum rollout'
 99    with open('bench_report.json', 'w') as f:
100        json.dump(report, f, indent=2)
101    print(json.dumps(report, indent=2))
102
103
104if __name__ == '__main__':
105    main()