Entropy-stable split quadratic layer / split_bench.py

Failed on benchmark

Raw ⬇ ZIP
  1import json, math, sys
  2from pathlib import Path
  3import numpy as np
  4import torch
  5from torch import nn
  6
  7sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
  8from bench import train_model, evaluate, sweep_baseline, make_report
  9
 10META = {
 11    'name': 'burgers_periodic_split',
 12    'domain': 'pde',
 13    'description': 'Periodic underresolved Burgers one-step regression on a 32-point grid.'
 14}
 15
 16
 17def _D(v):
 18    n = v.shape[-1]
 19    # x=2*pi*j/n, so Fourier integer wavenumbers are fftfreq(n)*n.
 20    k = torch.fft.rfftfreq(n, d=1.0/n).to(v.device)
 21    return torch.fft.irfft(1j * k * torch.fft.rfft(v, dim=-1), n=n, dim=-1)
 22
 23
 24def split_quadratic(u, alpha):
 25    beta = 1.0 - float(alpha)
 26    return float(alpha) * _D(0.5 * u * u) + beta * u * _D(u)
 27
 28
 29class SplitQuadraticNet(nn.Module):
 30    def __init__(self, alpha):
 31        super().__init__()
 32        self.alpha = float(alpha)
 33        self.inp = nn.Conv1d(1, 16, 1)
 34        self.mix = nn.Conv1d(16, 16, 1)
 35        self.out = nn.Conv1d(16, 1, 1)
 36        self.gamma = nn.Parameter(torch.tensor(0.1))
 37
 38    def forward(self, x):
 39        z = torch.tanh(self.inp(x))
 40        h = torch.tanh(self.mix(z))
 41        # The only intervention is the split coefficient in this quadratic block.
 42        return self.out(h + self.gamma * split_quadratic(h, self.alpha))
 43
 44
 45def get_dataset(seed, n_train=400, n_test=160):
 46    rng = np.random.default_rng(int(seed)); n = 32
 47    x = np.arange(n) * 2 * np.pi / n
 48
 49    def make(m):
 50        xs, ys = [], []
 51        for _ in range(int(m)):
 52            u = np.zeros(n)
 53            # Deliberately weakly underresolved high modes expose product aliasing.
 54            for mode in rng.choice(np.arange(5, 16), size=4, replace=False):
 55                u += rng.normal(0, 0.16 / np.sqrt(mode)) * np.sin(mode*x + rng.uniform(0, 2*np.pi))
 56                u += rng.normal(0, 0.16 / np.sqrt(mode)) * np.cos(mode*x + rng.uniform(0, 2*np.pi))
 57            dt = 0.025
 58            uh = np.fft.rfft(u); k = np.fft.rfftfreq(n, d=1.0/n)
 59            du = np.fft.irfft(1j*k*uh, n=n)
 60            # Target is a stable, filtered Burgers Euler step generated by padded product.
 61            mgrid = 48; up = np.zeros(mgrid, complex); q=n//2
 62            up[:q] = uh[:q] * (mgrid/n); up[-q:] = uh[-q:] * (mgrid/n)
 63            vp = np.fft.irfft(up, n=mgrid)
 64            kp=np.fft.rfftfreq(mgrid,d=1.0/mgrid)
 65            flux=np.fft.irfft(1j*kp*np.fft.rfft(0.5*vp*vp),n=mgrid)
 66            fluxh=np.fft.rfft(flux); trunc=np.zeros(n,complex)
 67            trunc[:q]=fluxh[:q]*(n/mgrid); trunc[-q:]=fluxh[-q:]*(n/mgrid)
 68            target=u-dt*(np.fft.irfft(trunc,n=n))
 69            xs.append(u.astype('float32')); ys.append(target.astype('float32'))
 70        return np.asarray(xs)[:,None,:], np.asarray(ys)[:,None,:]
 71
 72    xtr,ytr=make(n_train); xte,yte=make(n_test)
 73    return {'xtr':xtr,'ytr':ytr,'xte':xte,'yte':yte,'task':'regression','metric':'mse',
 74            'input_shape':(1,n),'out_dim':n}
 75
 76
 77def run():
 78    torch.set_num_threads(4)
 79    # All idea learning rates are also evaluated for the baseline.
 80    lrs = [1e-3, 3e-3, 1e-2]
 81    grid = [{'lr': lr, 'alpha': 1.0} for lr in lrs]
 82    idea_grid = [{'lr': lr, 'alpha': 1/3} for lr in lrs]
 83
 84    def factory(cfg):
 85        def train(seed):
 86            torch.manual_seed(seed); np.random.seed(seed)
 87            d=get_dataset(seed,400,160)
 88            d={k:(torch.from_numpy(v) if isinstance(v,np.ndarray) else v) for k,v in d.items()}
 89            net,metric,_=train_model(SplitQuadraticNet(cfg['alpha']),d,epochs=18,lr=cfg['lr'],batch=128,log=lambda *_:None)
 90            return float(metric) if metric is not None else float('inf')
 91        return train
 92
 93    base=sweep_baseline(factory,grid)
 94    # Equal-sized idea sweep; its selected setting is evaluated on all eight paired seeds.
 95    idea_sweep=sweep_baseline(factory,idea_grid)
 96    idea=idea_sweep['full']
 97    rep=make_report('burgers_periodic_split','custom_split_quadratic_net',base,idea,{
 98        'predicted': {'aliasing_root_alpha_for_tau4_i1_j1': 1/3,
 99                      'prediction': 'alpha=1/3 reduces the dominant unresolved quadratic-product aliasing proxy'},
100        'observed': {'baseline_alpha': 1.0, 'idea_alpha': 1/3,
101                     'baseline_test_mse': base['full']['mean'],
102                     'idea_test_mse': idea['mean'],
103                     'idea_sweep': idea_sweep['sweep']},
104        'confirmed': False,
105        'measurement_note': 'The trained-model aliasing proxy was not retained by the generic harness metric path; confirmation is false rather than inferred from the formula.'
106    })
107    rep['custom_track']={'name':META['name'],'file':'split_bench.py','domain':'pde'}
108    Path('bench_report.json').write_text(json.dumps(rep,indent=2))
109    print(json.dumps(rep,indent=2))
110
111if __name__ == '__main__': run()