import json, math, random, time from pathlib import Path import numpy as np SEED=7 np.random.seed(SEED); random.seed(SEED) # ---------- Stage 1: transfer-function sanity check ---------- def lockin_demo(): # Stable diagonal A, driven by sinusoidal input. Exact H is known. lambdas=np.array([0.7, 2.0, 5.0]) B=np.array([1.0, .4, .2]); C=np.array([.8, -.3, .5]) w=1.3; eps=.03; dt=.01; periods=25 T=2*np.pi/w*periods; n=int(T/dt); t=np.arange(n)*dt x=np.zeros(3); ys=[] for ti in t: u=eps*np.sin(w*ti) x += dt*(-lambdas*x+B*u) ys.append(C@x) # discard first periods as transient and correlate on integer-period tail cut=n//5; tt=t[cut:]; yy=np.asarray(ys[cut:]) # For y=Re(H eps exp(i wt)), 2 mean(y exp(-iwt))/eps estimates H. Hhat=1j*(2*np.mean(yy*np.exp(-1j*w*tt))/eps) Hexact=np.sum(C*B/(lambdas+1j*w)) rel=abs(Hhat-Hexact)/abs(Hexact) # discrete-gradient stability transition is independently checked eta_grid=np.linspace(.02, .55, 107) stable=[abs(1-eta*max(lambdas))<1 for eta in eta_grid] empirical=max(eta for eta,s in zip(eta_grid,stable) if s) boundary=2/max(lambdas) # Direct quadratic mode test: z_{t+1}=(1-eta*lambda)z_t, with lambda_max=5. # Below 2/lambda_max it decays; above it grows (alternating when eta*lambda>1). quad=[] for eta in [0.30, 0.39, 0.41, 0.50]: z=np.array([1.0, 0.3, -0.2]); norms=[] for _ in range(80): z=(1-eta*lambdas)*z; norms.append(float(np.linalg.norm(z))) quad.append({'eta':eta,'final_norm':norms[-1], 'decays':bool(norms[-1]