clifford_attractor.py
1import os 2import time 3 4import matplotlib 5matplotlib.use('Agg') 6import matplotlib.pyplot as plt 7import matplotlib.font_manager as fm 8import matplotlib.colors as mcolors 9 10import numpy as np 11from numba import njit 12 13@njit(fastmath=True) 14def compute_attractor(xs, ys, a, b, c, d): 15 x, y = 0.1, 0.0 16 for i in range(xs.shape[0]): 17 xn = np.sin(a * y) + c * np.cos(a * x) 18 yn = np.sin(b * x) + d * np.cos(b * y) 19 x, y = xn, yn 20 xs[i] = x 21 ys[i] = y 22 23def main(): 24 libertinus_math_path = os.path.expanduser('~/.local/share/fonts/LibertinusMath-Regular.otf') 25 font_name = 'Libertinus Math' 26 27 if os.path.exists(libertinus_math_path): 28 try: 29 fm.fontManager.addfont(libertinus_math_path) 30 print(f"Registered font: {libertinus_math_path}") 31 matplotlib.rcParams['font.family'] = [font_name, 'serif'] 32 matplotlib.rcParams['mathtext.fontset'] = 'custom' 33 matplotlib.rcParams['mathtext.rm'] = font_name 34 matplotlib.rcParams['mathtext.it'] = font_name 35 matplotlib.rcParams['mathtext.bf'] = font_name 36 print(f"Matplotlib configured to use '{font_name}' for equations.") 37 except Exception as e: 38 print(f"Error registering or configuring {font_name}: {e}") 39 print("Falling back to 'serif' for equation text.") 40 matplotlib.rcParams['font.family'] = ['serif'] 41 matplotlib.rcParams['mathtext.fontset'] = 'cm' 42 else: 43 print(f"Warning: {font_name} font not found at {libertinus_math_path}. Ensure it's installed or check the path.") 44 print("Falling back to 'serif' for equation text.") 45 matplotlib.rcParams['font.family'] = ['serif'] 46 matplotlib.rcParams['mathtext.fontset'] = 'cm' 47 48 49 n_points = 300_000_000 50 burn_in = 10_000 51 a, b, c, d = 1.8, -1.9, 1.0, 1.5 52 53 xs = np.empty(n_points, dtype=np.float64) 54 ys = np.empty(n_points, dtype=np.float64) 55 56 t0 = time.perf_counter() 57 compute_attractor(xs, ys, a, b, c, d) 58 t1 = time.perf_counter() 59 60 xs = xs[burn_in:] 61 ys = ys[burn_in:] 62 63 print(f"Computed {n_points} points in {t1 - t0:.2f} s.") 64 print(f"x ∈ [{xs.min():.3f}, {xs.max():.3f}], y ∈ [{ys.min():.3f}, {ys.max():.3f}]") 65 66 dark_bg = '#111111' 67 fig = plt.figure(figsize=(12, 8), facecolor=dark_bg) 68 ax = fig.add_axes((0, 0, 1, 1), facecolor=dark_bg) 69 x_min, x_max = -2.5, 2.5 70 y_min, y_max = -2.5, 2.5 71 ax.set_xlim(x_min, x_max) 72 ax.set_ylim(y_min, y_max) 73 ax.axis('off') 74 75 H, xedges, yedges = np.histogram2d(xs, ys, bins=2000, range=[[x_min, x_max], [y_min, y_max]]) 76 77 ax.imshow( 78 H.T, 79 origin='lower', 80 extent=[xedges[0], xedges[-1], yedges[0], yedges[-1]], 81 cmap='inferno', 82 norm=mcolors.LogNorm(vmin=1), 83 aspect='auto', 84 interpolation='bilinear' 85 ) 86 87 eq_text = ( 88 r"$x_{t+1} = \sin(%.1f y_t) + %.1f \cos(%.1f x_t)$" "\n" 89 r"$y_{t+1} = \sin(%.1f x_t) + %.1f \cos(%.1f y_t)$" 90 ) % (a, c, a, b, d, b) 91 92 fig.text( 93 0.01, 0.99, eq_text, 94 color='#BF40BF', 95 fontsize=16, 96 va='top', 97 ha='left', 98 alpha=0.8 99 ) 100 101 interval_text = ( 102 r"$x \in [%.3f, %.3f]$" "\n" 103 r"$y \in [%.3f, %.3f]$" 104 ) % (xs.min(), xs.max(), ys.min(), ys.max()) 105 106 fig.text( 107 0.01, 0.92, 108 interval_text, 109 color='#BF40BF', 110 fontsize=14, 111 va='top', 112 ha='left', 113 alpha=0.8 114 ) 115 116 outpath = 'clifford_attractor.png' 117 118 plt.savefig( 119 outpath, 120 dpi=300, 121 bbox_inches='tight', 122 pad_inches=0, 123 facecolor=fig.get_facecolor() 124 ) 125 plt.close(fig) 126 127if __name__ == '__main__': 128 main()