Characteristic-Invariant BT Monitor / bt_monitor_experiment.py

Failed on benchmark

Raw ⬇ ZIP
  1import json, random
  2from pathlib import Path
  3import numpy as np
  4import torch
  5
  6SEED = 2553
  7torch.set_default_dtype(torch.float64)
  8np.random.seed(SEED); random.seed(SEED); torch.manual_seed(SEED)
  9
 10# Canonical BT unfolding:
 11# x' = y
 12# y' = mu*x + nu*y + alpha*x^2 + beta*x*y
 13# At mu=nu=0 and z*=0, J=[[0,1],[0,0]], q0=(1,0).
 14def field(z, p):
 15    x, y = z
 16    mu, nu, alpha, beta = p
 17    return torch.stack((y, mu*x + nu*y + alpha*x*x + beta*x*y))
 18
 19def jacobian(z, p):
 20    # Explicit form keeps the z -> J -> invariant graph for higher derivatives.
 21    x, y = z
 22    mu, nu, alpha, beta = p
 23    return torch.stack((torch.stack((0*x, 1+0*x)),
 24                        torch.stack((mu + 2*alpha*x + beta*y, nu + beta*x))))
 25
 26def invariants(J):
 27    e1 = J[0, 0] + J[1, 1]
 28    e2 = J[0, 0]*J[1, 1] - J[0, 1]*J[1, 0]
 29    return e1, e2  # e1, e2; polynomial form supports higher derivatives
 30
 31def monitor(p, q_override=None, z_value=(0., 0.)):
 32    z = torch.tensor(z_value, dtype=p.dtype, requires_grad=True)
 33    J = jacobian(z, p)
 34    e1, e2 = invariants(J)
 35    if q_override is None:
 36        _, _, vh = torch.linalg.svd(J.detach())
 37        q = vh[-1].detach()
 38    else:
 39        q = torch.as_tensor(q_override, dtype=z.dtype)
 40    # For n=2: delta=e2/e0=e2 and tau=e1.
 41    gdelta = torch.autograd.grad(e2, z, retain_graph=True)[0]
 42    gtau = torch.autograd.grad(e1, z)[0]
 43    Ddelta = torch.dot(gdelta, q)
 44    Dtau = torch.dot(gtau, q)
 45    return {
 46        'J': J.detach().numpy().tolist(), 'q': q.numpy().tolist(),
 47        'delta': float(e2.detach()), 'tau': float(e1.detach()),
 48        'a': float((-0.5*Ddelta).detach()), 'b': float(Dtau.detach()),
 49        'e_transverse': 1.0,  # e_(n-2)=e0 for n=2
 50    }
 51
 52def finite_difference(alpha, beta, h=1e-5):
 53    p = torch.tensor([0., 0., alpha, beta])
 54    def vals(x):
 55        r = monitor(p, [1., 0.], (x, 0.))
 56        return r['delta'], r['tau']
 57    dm = (vals(h)[0] - vals(-h)[0])/(2*h)
 58    dt = (vals(h)[1] - vals(-h)[1])/(2*h)
 59    return -0.5*dm, dt
 60
 61def run_target(kind):
 62    torch.manual_seed(SEED)
 63    p = torch.nn.Parameter(torch.tensor([0.65, -0.55, 1.2, -0.8]))
 64    opt = torch.optim.Adam([p], lr=0.05)
 65    for _ in range(250):
 66        mu, nu = p[0], p[1]
 67        if kind == 'monitor':
 68            loss = mu*mu + nu*nu
 69        else:
 70            # Spectral-abscissa stability penalty: stable systems receive zero loss.
 71            disc = nu*nu + 4*mu
 72            rp = torch.where(disc >= 0, (nu + torch.sqrt(torch.clamp(disc, min=0)))/2, nu/2)
 73            loss = torch.relu(rp)**2
 74        opt.zero_grad(); loss.backward(); opt.step()
 75    r = monitor(p.detach(), [1., 0.])
 76    return {'final_p': p.detach().numpy().tolist(), 'delta': r['delta'], 'tau': r['tau'],
 77            'monitor_loss': r['delta']**2 + r['tau']**2}
 78
 79def verify():
 80    # Prediction 1: exact unfolding identities delta=-mu, tau=nu over a grid.
 81    identity = []
 82    for mu in np.linspace(-1, 1,  nine := 9):
 83        for nu in np.linspace(-1, 1, nine):
 84            r = monitor(torch.tensor([mu, nu, 1., 1.]), [1., 0.])
 85            identity.append((mu, nu, r['delta'], r['tau']))
 86    err_delta = max(abs(d + mu) for mu, nu, d, t in identity)
 87    err_tau = max(abs(t - nu) for mu, nu, d, t in identity)
 88
 89    # Prediction 2: a scales as alpha and b scales as beta at the BT point.
 90    coeff = []
 91    for alpha, beta in [(0.3,-0.7), (1.,2.), (-1.5,.25), (2.5,-3.)]:
 92        r = monitor(torch.tensor([0.,0.,alpha,beta]), [1.,0.])
 93        af, bf = finite_difference(alpha, beta)
 94        coeff.append({'alpha':alpha, 'beta':beta, 'a':r['a'], 'b':r['b'],
 95                      'a_fd':af, 'b_fd':bf, 'a_error':abs(r['a']-alpha),
 96                      'b_error':abs(r['b']-beta)})
 97
 98    # Prediction 3: the candidate is simultaneous delta=tau=0 only at (mu,nu)=(0,0).
 99    candidates = []
100    for mu in [-.2, 0., .2]:
101        for nu in [-.2, 0., .2]:
102            r = monitor(torch.tensor([mu,nu,1.,1.]), [1.,0.])
103            candidates.append({'mu':mu, 'nu':nu, 'delta':r['delta'], 'tau':r['tau'],
104                               'distance':abs(r['delta'])+abs(r['tau'])})
105    out = {
106        'prediction_1_unfolding': {'points':len(identity), 'max_abs_delta_minus_neg_mu':err_delta,
107                                   'max_abs_tau_minus_nu':err_tau},
108        'prediction_2_coefficients': coeff,
109        'prediction_3_candidate_grid': candidates,
110        'baseline': run_target('baseline'), 'idea_monitor': run_target('monitor')
111    }
112    return out
113
114def main():
115    result = verify()
116    Path('results.json').write_text(json.dumps(result, indent=2))
117    print(json.dumps(result, indent=2))
118
119if __name__ == '__main__':
120    main()