import json import sys from pathlib import Path import numpy as np import torch import torch.nn as nn sys.path.insert(0, "/home/maxwelhelp/all/math2nn") from bench import get_dataset, make_model, train_model, sweep_baseline, make_report TABLE = torch.tensor([[[0, 0], [1, 0]], [[0, 1], [2, 0]], [[1, 1], [2, 1]]], dtype=torch.long) WEIGHTS = torch.arange(3, dtype=torch.long) def math_check(): pairs = [(q, s) for q in range(3) for s in range(2)] images = [tuple(TABLE[q, s].tolist()) for q, s in pairs] errs = [] for q, s in pairs: nq, y = TABLE[q, s].tolist() errs.append(int(WEIGHTS[q] + s - WEIGHTS[nq] - y)) return {"local_bijective": len(set(images)) == 6, "local_conservation_errors": errs, "max_abs_conservation_error": max(abs(e) for e in errs)} class ReversibleMealyRNN(nn.Module): """Same 3-feature input and 64-wide head as rnn_small, with finite carrier scan.""" def __init__(self, hidden=64, out_dim=1, scale=1.0): super().__init__() self.inp = nn.Linear(3, hidden) self.symbol = nn.Linear(3, 1) self.emb = nn.Parameter(torch.randn(3, 2, hidden) * 0.02) self.head = nn.Linear(hidden, out_dim) self.scale = float(scale) self.register_buffer("table", TABLE) def forward(self, x): seq = x.view(x.shape[0], -1, 3) z = self.inp(seq) bits = (self.symbol(seq).squeeze(-1) > 0).long() q = torch.zeros(x.shape[0], dtype=torch.long, device=x.device) states = [] for t in range(seq.shape[1]): s = bits[:, t] pair = self.table[q, s] nq, y = pair[:, 0], pair[:, 1] states.append(q) zt = z[:, t] + self.scale * self.emb[q, s] if t == 0: h = zt else: h = h + zt q = nq return self.head(h / seq.shape[1]) def make_idea(cfg): def train(seed): torch.manual_seed(seed) np.random.seed(seed) d = get_dataset("dynamics", seed, n_train=400, n_test=200) net = ReversibleMealyRNN(hidden=64, out_dim=1, scale=cfg["scale"]) _, metric, _ = train_model(net, d, epochs=cfg["epochs"], lr=cfg["lr"], batch=128, weight_decay=0.0, log=lambda *_: None) return metric return train def make_base(cfg): def train(seed): torch.manual_seed(seed) np.random.seed(seed) d = get_dataset("dynamics", seed, n_train=400, n_test=200) net = make_model("rnn_small", d["input_shape"], d["out_dim"]) _, metric, _ = train_model(net, d, epochs=cfg["epochs"], lr=cfg["lr"], batch=128, weight_decay=0.0, log=lambda *_: None) return metric return train def signature(seed=0, cfg=None): if cfg is None: cfg = {"lr": 3e-3, "epochs": 12, "scale": 1.0} torch.manual_seed(seed) d = get_dataset("dynamics", seed, n_train=400, n_test=200) net = ReversibleMealyRNN(scale=cfg["scale"]) net, _, _ = train_model(net, d, epochs=cfg["epochs"], lr=cfg["lr"], batch=128, log=lambda *_: None) net.eval() with torch.no_grad(): dev = next(net.parameters()).device x = d["xte"].to(dev) seq = x.view(x.shape[0], -1, 3) bits = (net.symbol(seq).squeeze(-1) > 0).long() q = torch.zeros(x.shape[0], dtype=torch.long, device=dev) total_in = bits.sum().item() total_out = 0 carrier_delta = 0 for t in range(seq.shape[1]): s = bits[:, t] pair = net.table[q, s] nq, y = pair[:, 0], pair[:, 1] total_out += y.sum().item() carrier_delta += (nq - q).sum().item() q = nq observed_error = float(total_in - total_out - carrier_delta) predicted_error = 0.0 return {"quantity": "symbol_weight + carrier_weight conservation over trained model tokens", "predicted": predicted_error, "observed": observed_error, "absolute_error": abs(observed_error - predicted_error), "confirmed": abs(observed_error - predicted_error) < 1e-6} def main(): check = math_check() assert check["local_bijective"] and check["max_abs_conservation_error"] == 0 # Union parity: every idea lr is present in the baseline grid. grid = [{"lr": lr, "epochs": 12} for lr in (1e-3, 3e-3, 1e-2)] base = sweep_baseline(make_base, grid) idea_grid = [{"lr": c["lr"], "epochs": c["epochs"], "scale": scale} for c in grid for scale in (0.5, 1.0, 2.0)] # Comparable 3-setting idea sweep: baseline-best lr plus two nearby scales. best_lr = base["best_cfg"]["lr"] configs = [{"lr": best_lr, "epochs": 12, "scale": s} for s in (0.5, 1.0, 2.0)] vals = [] for cfg in configs: r = {"cfg": cfg, "result": __import__("bench").evaluate(make_idea(cfg))} vals.append(r) best = min(vals, key=lambda z: z["result"]["mean"]) report = make_report("dynamics", "rnn_small", base, best["result"], { "mechanism_signature": signature(0, best["cfg"]), "math_check": check, "idea_sweep": vals, "structural_match": "controlled damped pendulum rollout tests stability/control of sequential state evolution" }) report["idea"]["selected_cfg"] = best["cfg"] Path("bench_report.json").write_text(json.dumps(report, indent=2)) print(json.dumps(report, indent=2)) if __name__ == "__main__": main()