Limiter-Smoothing Bifurcation Guard / limiter_guard.py
Failed on benchmark
1import json, math
2from pathlib import Path
3import numpy as np
4
5
6def rot(theta):
7 return np.array([[math.cos(theta), -math.sin(theta)],
8 [math.sin(theta), math.cos(theta)]], dtype=float)
9
10
11def exact(v, R):
12 n = np.linalg.norm(v)
13 return v if n <= R else (R / n) * v
14
15
16def smooth(v, R):
17 # Differentiable radial surrogate from the idea.
18 return v / math.sqrt(1.0 + (np.linalg.norm(v) / R) ** 2)
19
20
21def map_x(x, gain, R, K, theta, limiter):
22 # Centered affine map: x0 is an exact fixed point, so perturbation
23 # dynamics are governed by the displayed Jacobian at x0.
24 Q = rot(theta) @ np.diag([K, 1.0])
25 x0 = np.array([1.2, 0.0])
26 return x0 + gain * Q @ (limiter(x, R) - limiter(x0, R))
27
28
29def fd_jacobian(fun, x, h=1e-6):
30 J = np.zeros((2, 2))
31 for j in range(2):
32 xp, xm = x.copy(), x.copy()
33 xp[j] += h
34 xm[j] -= h
35 J[:, j] = (fun(xp) - fun(xm)) / (2.0 * h)
36 return J
37
38
39def analytic_jacobian(R, r, K, theta, kind):
40 t = r / R
41 Q = rot(theta) @ np.diag([K, 1.0])
42 if kind == "exact":
43 # radial/tangential derivatives of projection outside the ball
44 L = np.diag([0.0, 1.0 / t])
45 else:
46 # S(v)=v/sqrt(1+(||v||/R)^2)
47 radial = (1.0 + t * t) ** (-1.5)
48 tangential = (1.0 + t * t) ** (-0.5)
49 L = np.diag([radial, tangential])
50 return Q @ L
51
52
53def rho(J):
54 return float(np.max(np.abs(np.linalg.eigvals(J))))
55
56
57def complex_pair(J):
58 ev = np.linalg.eigvals(J)
59 return bool(abs(ev[0].imag) > 1e-7 and abs(ev[1].imag) > 1e-7)
60
61
62def predicted_exact_boundary(R, r, theta):
63 # For Q=Rot(theta)diag(K,1), exact projection is rank one and
64 # rho(g Q DP)=g*|cos(theta)|/(r/R), independent of K.
65 return (r / R) / abs(math.cos(theta))
66
67
68def predicted_smooth_boundary(R, r, K):
69 t = r / R
70 a = (1.0 + t * t) ** (-1.5)
71 s = (1.0 + t * t) ** (-0.5)
72 # In the complex-eigenvalue regime, rho(g Q DS)=g*sqrt(K*a*s).
73 return 1.0 / math.sqrt(K * a * s)
74
75
76def trajectory(gain, R, K, theta, kind, n=80, perturb=1e-5):
77 limiter = exact if kind == "exact" else smooth
78 x = np.array([1.2 + perturb, 0.0])
79 norms = []
80 for _ in range(n):
81 norms.append(float(np.linalg.norm(x)))
82 x = map_x(x, gain, R, K, theta, limiter)
83 if not np.all(np.isfinite(x)) or np.linalg.norm(x) > 1e12:
84 norms.extend([float("inf")] * (n - len(norms)))
85 break
86 return norms
87
88
89def main():
90 np.random.seed(7)
91 R, r, theta = 1.0, 1.2, 1.2
92 x0 = np.array([r, 0.0])
93 Ks = [1.0, 2.0, 4.0, 8.0]
94 rows = []
95
96 # Core math sanity check: analytic Jacobians versus finite differences.
97 fd_errors = {}
98 for kind, limiter in [("exact", exact), ("smooth", smooth)]:
99 for K in [2.0, 4.0]:
100 f = lambda x: map_x(x, 1.0, R, K, theta, limiter)
101 Ja = analytic_jacobian(R, r, K, theta, kind)
102 Jfd = fd_jacobian(f, x0)
103 fd_errors[f"{kind}_K{int(K)}"] = float(np.max(np.abs(Ja - Jfd)))
104
105 # Prediction 1: exact threshold independent of anisotropy K.
106 exact_pred = predicted_exact_boundary(R, r, theta)
107 exact_obs = []
108 for K in Ks:
109 J = analytic_jacobian(R, r, K, theta, "exact")
110 exact_obs.append({"K": K, "predicted_gain": exact_pred,
111 "observed_gain_from_rho": 1.0 / rho(J)})
112
113 # Prediction 2: smooth crossing scales as K^(-1/2), and is complex.
114 smooth_rows = []
115 for K in Ks:
116 pred = predicted_smooth_boundary(R, r, K)
117 Junit = analytic_jacobian(R, r, K, theta, "smooth")
118 observed = 1.0 / rho(Junit)
119 ev = np.linalg.eigvals(pred * Junit)
120 smooth_rows.append({"K": K, "predicted_gain": pred,
121 "observed_gain_from_rho": observed,
122 "complex_at_crossing": complex_pair(pred * Junit),
123 "eigenvalues_at_crossing": [[float(z.real), float(z.imag)] for z in ev]})
124
125 # Prediction 3: increasing radius moves both boundaries linearly in r/R
126 # for exact projection, while the smooth boundary follows the analytic
127 # nonlinear formula; compare a radius sweep directly.
128 radius_rows = []
129 for rr in [0.8, 1.0, 1.2, 1.6, 2.0]:
130 ep = predicted_exact_boundary(R, rr, theta)
131 sp = predicted_smooth_boundary(R, rr, 4.0)
132 radius_rows.append({"r_over_R": rr / R, "exact_predicted": ep,
133 "smooth_predicted_K4": sp})
134
135 # A direct continuation-style grid demonstrates the guard interval.
136 K = 4.0
137 ep, sp = exact_pred, predicted_smooth_boundary(R, r, K)
138 grid = np.linspace(0.8 * sp, 1.05 * ep, 25)
139 for g in grid:
140 Jp = g * analytic_jacobian(R, r, K, theta, "exact")
141 Js = g * analytic_jacobian(R, r, K, theta, "smooth")
142 rows.append({"gain": float(g), "rho_exact": rho(Jp),
143 "rho_smooth": rho(Js), "smooth_complex": complex_pair(Js)})
144
145 # Perturbed trajectories: choose a gain strictly between the smooth and
146 # exact crossings, where the predicted artificial instability should occur.
147 middle = 0.5 * (sp + ep)
148 traj = {kind: trajectory(middle, R, K, theta, kind) for kind in ["exact", "smooth"]}
149 result = {
150 "setup": {"R": R, "r": r, "theta": theta, "Ks": Ks},
151 "finite_difference_max_errors": fd_errors,
152 "prediction_1_exact_boundary": exact_obs,
153 "prediction_2_smooth_complex_crossing": smooth_rows,
154 "prediction_3_radius_sweep": radius_rows,
155 "continuation_grid_K4": rows,
156 "trajectory_gain_between_boundaries": middle,
157 "trajectory_final_norms": {k: v[-1] for k, v in traj.items()},
158 "trajectory_max_norms": {k: float(np.max(v)) for k, v in traj.items()},
159 "trajectory_norms": traj,
160 "guard_interval": {"smooth_crossing": sp, "exact_crossing": ep,
161 "width": ep - sp}
162 }
163 Path("results.json").write_text(json.dumps(result, indent=2))
164 print(json.dumps({"fd_errors": fd_errors, "exact_boundary": exact_pred,
165 "smooth_K4_boundary": sp, "smooth_K4_complex": smooth_rows[2]["complex_at_crossing"],
166 "guard_width": ep-sp, "trajectory_final_norms": result["trajectory_final_norms"]}, indent=2))
167
168
169if __name__ == "__main__":
170 main()