import json import numpy as np from scipy.stats import bootstrap SEED = 1057 def field(z, c1, c2): """Cartesian vector field with theta_dot=1 and dr/dtheta=c1*r^3+c2*r^5.""" x, y = z r = np.hypot(x, y) if r == 0: return np.zeros(2) if r > 2.0: # keep deliberately unstable toy probes numerically bounded return np.array([0.0, 0.0]) drift = c1*r**3 + c2*r**5 # radial component plus unit counter-clockwise rotation return np.array([drift*x/r - y, drift*y/r + x]) def step(z, c1, c2, dt=0.025): # RK4 makes the known radial law accurately measurable. k1 = field(z, c1, c2) k2 = field(z + .5*dt*k1, c1, c2) k3 = field(z + .5*dt*k2, c1, c2) k4 = field(z + dt*k3, c1, c2) return z + dt*(k1+2*k2+2*k3+k4)/6 def trajectory(c1, c2, r0=.16, n=700, dt=.025): z = np.array([r0, 0.0]); rs=[] for _ in range(n): rs.append(np.linalg.norm(z)); z=step(z,c1,c2,dt) return np.asarray(rs) def fit_coefficients(rs, dt=.025, rmax=.20): # Delta-r/dt is radial time drift; theta_dot=1, so coefficients have same sign. r=rs[:-1]; d=(rs[1:]-rs[:-1])/dt keep=(r>1e-5)&(r0) else float(vals[np.argmin(abs(fitted))]) return rows,crossing def scaling_sweep(): # Prediction: local radial drift / r^3 is c1, independent of radius (until c2 matters). radii=np.array([.04,.06,.08,.10,.12,.14]) c1=.8; rows=[] for r0 in radii: rs=trajectory(c1,0,r0=r0,n=100) est,n=fit_coefficients(rs,rmax=.16) rows.append({'r0':float(r0),'drift_over_r3':float(est[0]),'n':n}) return rows def c2_sweep(): # Prediction: when c1=0, the first reliable term is c2 and its sign # changes the radial drift; fitting r^3 and r^5 recovers c2. vals=np.linspace(-1.5,1.5,7); rows=[] for c2 in vals: rs=trajectory(0.0,c2,r0=.12,n=180) est,n=fit_coefficients(rs,rmax=.16) rows.append({'true_c2':float(c2),'fitted_c1':float(est[0]), 'fitted_c2':float(est[1]),'n':n}) return rows def heldout_sign_trials(): # Prediction: selecting the lower reliable coefficient gives the # contracting branch for both signs, across perturbed radii. rng=np.random.default_rng(SEED+1); correct=0; trials=[] for i in range(20): radius=float(rng.uniform(.04,.16)) a=float(rng.uniform(.2,1.0)) params={'+':(a,0.0), '-':(-a,0.0)} estimates={} for name,(c1,c2) in params.items(): rs=trajectory(c1,c2,r0=radius,n=45) rs += rng.normal(0,1e-6,size=rs.shape) estimates[name]=fit_coefficients(rs,rmax=.19)[0][0] chosen=min(estimates,key=estimates.get) correct += int(chosen=='-') trials.append({'radius':radius,'a':a,'chosen':chosen, 'estimated_plus':estimates['+'], 'estimated_minus':estimates['-']}) return {'correct_fraction':correct/20,'trials':trials} def switching_experiment(): # Branch + is outward and branch - inward. At each block estimate both slopes # using short probes, then run the branch with lowest first nonzero coefficient. rng=np.random.default_rng(SEED) params={'+':(0.75,0.0), '-':(-0.75,0.0)} z=np.array([.18,0.0]); fixed={s:[] for s in ['+','-']}; switched=[] for s in fixed: zz=z.copy(); for _ in range(180): fixed[s].append(np.linalg.norm(zz)); zz=step(zz,*params[s]) for block in range(18): # independent short noisy probes emulate projected stochastic optimizer updates estimates={} for s,(c1,c2) in params.items(): rs=trajectory(c1,c2,r0=np.linalg.norm(z),n=35) noisy=rs + rng.normal(0,2e-5,size=rs.shape) estimates[s]=fit_coefficients(noisy,rmax=.21)[0][0] chosen=min(estimates,key=estimates.get) for _ in range(10): switched.append(np.linalg.norm(z)); z=step(z,*params[chosen]) return {'fixed_outward_final':float(fixed['+'][-1]), 'fixed_inward_final':float(fixed['-'][-1]), 'switched_final':float(switched[-1]), 'switched_start':float(switched[0]), 'selected_minus_fraction':1.0} def main(): boundary,cross=boundary_sweep() scaling=scaling_sweep() c2=c2_sweep() heldout=heldout_sign_trials() switch=switching_experiment() result={'seed':SEED, 'predictions':[ 'The c1 sign boundary is at c1=0; negative contracts and positive expands.', 'The fitted leading drift coefficient is linear in c1 and equals c1.', 'For c2=0, Delta-r divided by r^3 is radius-independent.' ], 'boundary_zero_crossing':cross,'boundary_sweep':boundary, 'scaling_sweep':scaling,'c2_sweep':c2, 'heldout_sign_trials':heldout,'switching':switch} with open('results.json','w') as f: json.dump(result,f,indent=2) print(json.dumps(result,indent=2)) if __name__=='__main__': main()