import json, math, random from pathlib import Path import numpy as np import torch import torch.nn as nn import torch.nn.functional as F import sys sys.path.insert(0, '/home/maxwelhelp/all/math2nn') from bench import get_dataset, train_model, evaluate, sweep_baseline, make_report SEED = 2852 OUT = Path('bench_report.json') class KineticMix(nn.Module): """Discrete phase-space operator on an 8-token dynamics window. Token index is x; feature channels are [theta, omega, u]. Diffusion is depthwise over x and transport shifts each channel along x by h*x_coord. """ def __init__(self, h=0.08, gate_bias=1.5): super().__init__() self.h = float(h) self.gate = nn.Conv1d(3, 3, 1) nn.init.zeros_(self.gate.weight) nn.init.constant_(self.gate.bias, gate_bias) self.mix = nn.Conv1d(3, 3, 1) def forward(self, z): # [B,T,3] b, t, c = z.shape q = z.transpose(1, 2) # normalized x coordinate, matching a characteristic velocity field xcoord = torch.linspace(-1., 1., t, device=z.device, dtype=z.dtype) sigma = max(math.sqrt(2.0 * self.h), 1e-3) radius = max(1, int(math.ceil(3.0 * sigma * t / 2.0))) offs = torch.arange(-radius, radius + 1, device=z.device) w = torch.exp(-0.5 * (offs / (sigma * t / 2.0)) ** 2) w = w / w.sum() diff = torch.zeros_like(q) for oi, wi in zip(offs.tolist(), w): diff = diff + wi * torch.roll(q, int(oi), dims=2) # periodic linear interpolation at token coordinate x + h*xcoord pos = torch.arange(t, device=z.device, dtype=z.dtype)[None, :] + self.h * xcoord[None, :] * t / 2.0 pos = pos.remainder(t) j0 = pos.floor().long().squeeze(0); a = (pos - j0).squeeze(0) j1 = (j0 + 1).remainder(t) shifted = diff[:, :, j0] * (1-a)[None, None, :] + diff[:, :, j1] * a[None, None, :] g = torch.sigmoid(self.gate(q)) return self.mix(q + g * (shifted - q)).transpose(1, 2) class DynamicsNet(nn.Module): def __init__(self, idea=False, h=.08): super().__init__() self.idea = idea self.kin = KineticMix(h) if idea else nn.Identity() self.rnn = nn.GRU(3, 32, batch_first=True) self.head = nn.Linear(32, 1) def forward(self, x): z = x.view(x.shape[0], 8, 3) z = self.kin(z) _, h = self.rnn(z) return self.head(h[-1]) def seed_all(seed): random.seed(seed); np.random.seed(seed); torch.manual_seed(seed) if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed) def run_one(seed, idea, lr, h=.08, epochs=14): seed_all(seed) ds = get_dataset('dynamics', seed, n_train=400, n_test=400) # canonical train_model supplies CUDA -> CPU fallback model = DynamicsNet(idea=idea, h=h) _, metric, _ = train_model(model, ds, epochs=epochs, lr=lr, batch=64, log=lambda *_: None) return float(metric) def main(): # Union parity: every lr tested for idea is in baseline sweep. lrs = [0.0015, 0.003, 0.006] baseline_grid = [{'lr': x, 'h': None} for x in lrs] base = sweep_baseline( lambda cfg: (lambda seed: run_one(seed, False, cfg['lr'], epochs=14)), baseline_grid) best_lr = float(base['best_cfg']['lr']) idea_grid = [{'lr': x, 'h': .08} for x in lrs] idea_sweep = [] for cfg in idea_grid: r = evaluate(lambda seed, cfg=cfg: run_one(seed, True, cfg['lr'], cfg['h'], epochs=14), seeds=range(4)) idea_sweep.append({'cfg': cfg, 'mean': r['mean']}) best_idea_cfg = min(idea_sweep, key=lambda x: x['mean'])['cfg'] idea = evaluate(lambda seed: run_one(seed, True, best_idea_cfg['lr'], best_idea_cfg['h'], epochs=14), seeds=range(8)) # Re-test the trained-model mechanism at NN scale: observed Fourier # attenuation and characteristic centroid displacement of KineticMix. seed_all(17) layer = KineticMix(.08).eval() t = 32; xx = torch.linspace(-1, 1, t)[None, :, None] k = 3.0; inp = torch.cos(k * xx).expand(1, t, 3).clone() with torch.no_grad(): out = layer(inp) # isolate the fixed operator's diffusive response using gate/mix neutralized raw = layer.gate; mix = layer.mix with torch.no_grad(): layer.gate.weight.zero_(); layer.gate.bias.fill_(30); layer.mix.weight.copy_(torch.eye(3).unsqueeze(-1)); layer.mix.bias.zero_() op = layer(inp) observed_att = float(op[:, :, 0].abs().mean() / inp[:, :, 0].abs().mean()) predicted_att = float(math.exp(-.08 * k*k)) # transport prediction: use a localized impulse and centroid shift impulse = torch.zeros(1,t,3); impulse[0,t//2,0] = 1 with torch.no_grad(): shifted = layer(impulse) mass = shifted[0,:,0].abs(); centroid = float((mass*torch.arange(t)).sum()/(mass.sum()+1e-8)) displacement = centroid - t//2 signature = { 'prediction': 'x Fourier attenuation approximately exp(-h*k^2) and transport displacement proportional to h*x', 'predicted_fourier_attenuation': predicted_att, 'observed_fourier_attenuation': observed_att, 'attenuation_relative_error': abs(observed_att-predicted_att)/(abs(predicted_att)+1e-8), 'predicted_transport_displacement': .08 * (t/2), 'observed_transport_displacement': displacement, 'confirmed': bool(abs(observed_att-predicted_att)/(abs(predicted_att)+1e-8) < .25 and np.isfinite(displacement)) } rep = make_report('dynamics', 'rnn_small_kinetic_vs_raw', base, idea, {'signature': signature, 'idea_sweep': idea_sweep, 'best_idea_cfg': best_idea_cfg, 'parameter_parity': { 'baseline': sum(p.numel() for p in DynamicsNet(False).parameters()), 'idea': sum(p.numel() for p in DynamicsNet(True).parameters())}}) rep['baseline']['union_lr_grid'] = lrs rep['custom_track'] = None OUT.write_text(json.dumps(rep, indent=2)) print(json.dumps(rep, indent=2)) if __name__ == '__main__': try: main() except RuntimeError as e: print('runtime failure:', repr(e)); raise