import json, math, random from pathlib import Path import numpy as np from scipy.special import lambertw import torch from torch import nn SEED = 1082 random.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED) # x'(t)=a x(t)+b x(t-tau), with characteristic roots # lambda = a + W_k(b*tau*exp(-a*tau))/tau. def roots_discrete(a, b, tau, branches=range(-40, 41)): if tau == 0: return np.array([a+b], dtype=complex) z = b*tau*np.exp(-a*tau) return np.array([a + lambertw(z, k)/tau for k in branches], dtype=complex) def rightmost(a, b, tau): rr = roots_discrete(a,b,tau) return rr[np.argmax(rr.real)] def hopf_prediction(a,b,k=0): w = math.sqrt(b*b-a*a) # Direct transcription of the supplied atan2 formula, normalized to tau>0. phase = math.atan2(-w/b, -a/b) + 2*math.pi*k while phase <= 0: phase += 2*math.pi return phase/w, w def math_check(): # Parameter sweep: each case has |b|>|a| and therefore a Hopf candidate. cases=[(-1.0,-2.0),(-0.5,-1.5),(-1.5,-2.5),(-0.8,-1.8)] rows=[] for a,b in cases: tau_pred,w_pred=hopf_prediction(a,b) taus=np.linspace(0.02, tau_pred*2.0, 700) rs=np.array([rightmost(a,b,t).real for t in taus]) ix=np.where(np.sign(rs[:-1]) != np.sign(rs[1:]))[0][0] tc=taus[ix] - rs[ix]*(taus[ix+1]-taus[ix])/(rs[ix+1]-rs[ix]) rc=rightmost(a,b,tc) eps=max(.001,tau_pred*0.005) slope=(rightmost(a,b,tc+eps).real-rightmost(a,b,tc-eps).real)/(2*eps) freq=float(abs(rc.imag)) below=rightmost(a,b,tau_pred*.8).real above=rightmost(a,b,tau_pred*1.2).real rows.append({"a":a,"b":b,"predicted_tau_c":tau_pred, "observed_tau_c":float(tc),"tau_error_pct":float(abs(tc-tau_pred)/tau_pred*100), "predicted_omega":w_pred,"observed_omega":freq, "omega_error_pct":float(abs(freq-w_pred)/w_pred*100), "dr_dtau_observed":float(slope),"stable_side_r":float(below), "unstable_side_r":float(above),"confirmed":bool(abs(tc-tau_pred)/tau_pred<.02 and abs(freq-w_pred)/w_pred<.02 and slope>0 and below<0