catastrophe/butterfly_catastrophe.py

122 lines
5 KiB
Python
Raw Normal View History

#!/usr/bin/python3
2026-03-25 18:23:53 +00:00
"""
Butterfly Catastrophe Surface ASCII STL Generator
Potential: V(x) = x^6 + a*x^4 + c*x^2 + d*x (a = -3 fixed, b = 0)
Equilibrium: dV/dx = 6x^5 - 12x^3 + 2c*x + d = 0
2026-03-25 21:48:29 +00:00
Rearranged as a single-valued function:
d(x, c) = -(6x^5 - 12x^3 + 2c*x)
= -6x^5 + 12x^3 - 2c*x
2026-03-25 21:48:29 +00:00
The print base is the (x, c) plane; height is d. Every grid point maps to
exactly one vertex no root finding, no branch tracking, no holes.
The fold lines of the bifurcation set appear as ridges where the surface has
zero gradient in x: d/x = -30x^4 + 36x^2 - 2c = 0, i.e. c = 18x^2 - 15x^4.
2026-03-25 18:23:53 +00:00
"""
import numpy as np
import os
2026-03-25 21:48:29 +00:00
A_FIXED = -3.0
# ── Tuning ──────────────────────────────────────────────────────────────────
GRID = 400 # grid resolution — higher = smoother ridges
X_RANGE = (-4.5, 4.5) # state variable x
C_RANGE = (-1.5, 7.5) # control parameter c
D_SCALE = 0.4 # scale factor applied to the computed d height;
2026-03-25 21:48:29 +00:00
# reduce if the model is too tall for your printer
2026-03-25 19:05:53 +00:00
2026-03-25 21:48:29 +00:00
# ── 1. Height function ────────────────────────────────────────────────────────
2026-03-25 18:23:53 +00:00
2026-03-25 21:48:29 +00:00
def d_surface(x, c):
"""d value on the equilibrium manifold: dV/dx = 0 solved for d."""
return (-6*x**5 + 12*x**3 - 2*c*x) * D_SCALE
2026-03-25 21:34:16 +00:00
2026-03-25 21:48:29 +00:00
# ── 2. Build mesh ─────────────────────────────────────────────────────────────
2026-03-25 21:34:16 +00:00
def build_mesh():
2026-03-25 21:48:29 +00:00
x_vals = np.linspace(*X_RANGE, GRID)
c_vals = np.linspace(*C_RANGE, GRID)
2026-03-25 18:23:53 +00:00
2026-03-25 21:48:29 +00:00
# Pre-compute the full height field in one vectorised call
X, C = np.meshgrid(x_vals, c_vals, indexing='ij') # (GRID, GRID)
D = (-6*X**5 + 12*X**3 - 2*C*X) * D_SCALE
2026-03-25 19:05:53 +00:00
2026-03-25 18:23:53 +00:00
triangles = []
2026-03-25 21:34:16 +00:00
for i in range(GRID - 1):
2026-03-25 21:48:29 +00:00
for j in range(GRID - 1):
p00 = (x_vals[i], c_vals[j], D[i, j ])
p10 = (x_vals[i+1], c_vals[j], D[i+1, j ])
p11 = (x_vals[i+1], c_vals[j+1], D[i+1, j+1])
p01 = (x_vals[i], c_vals[j+1], D[i, j+1])
triangles.append((p00, p10, p11))
triangles.append((p00, p11, p01))
2026-03-25 18:23:53 +00:00
return triangles
2026-03-25 21:48:29 +00:00
# ── 3. Flat base ──────────────────────────────────────────────────────────────
2026-03-25 18:23:53 +00:00
2026-03-25 21:48:29 +00:00
def add_base(triangles):
x0, x1 = X_RANGE
c0, c1 = C_RANGE
2026-03-25 21:48:29 +00:00
X, C = np.meshgrid(np.linspace(x0, x1, GRID),
np.linspace(c0, c1, GRID), indexing='ij')
D = (-6*X**5 + 12*X**3 - 2*C*X) * D_SCALE
zb = D.min() - 0.15
zt = zb + 0.15
2026-03-25 18:23:53 +00:00
2026-03-25 21:48:29 +00:00
# Top and bottom faces of base slab
2026-03-25 18:23:53 +00:00
triangles += [
2026-03-25 21:48:29 +00:00
((x0,c0,zt),(x1,c0,zt),(x1,c1,zt)),
((x0,c0,zt),(x1,c1,zt),(x0,c1,zt)),
((x0,c0,zb),(x1,c1,zb),(x1,c0,zb)),
((x0,c0,zb),(x0,c1,zb),(x1,c1,zb)),
2026-03-25 18:23:53 +00:00
]
2026-03-25 21:48:29 +00:00
for (ax,ay),(bx,by) in [
((x0,c0),(x1,c0)), ((x1,c0),(x1,c1)),
((x1,c1),(x0,c1)), ((x0,c1),(x0,c0)),
]:
2026-03-25 18:23:53 +00:00
triangles += [
2026-03-25 21:48:29 +00:00
((ax,ay,zb),(bx,by,zb),(bx,by,zt)),
((ax,ay,zb),(bx,by,zt),(ax,ay,zt)),
2026-03-25 18:23:53 +00:00
]
return triangles
2026-03-25 21:48:29 +00:00
# ── 4. ASCII STL output ───────────────────────────────────────────────────────
2026-03-25 18:23:53 +00:00
def normal(v0, v1, v2):
a = np.subtract(v1, v0)
b = np.subtract(v2, v0)
n = np.cross(a, b)
2026-03-25 21:48:29 +00:00
L = np.linalg.norm(n)
return n / L if L > 1e-14 else np.array([0.0, 0.0, 1.0])
2026-03-25 18:23:53 +00:00
def write_ascii_stl(triangles, filename):
with open(filename, 'w') as f:
f.write('solid butterfly_catastrophe\n')
for tri in triangles:
v0, v1, v2 = [np.array(v, dtype=float) for v in tri]
nx, ny, nz = normal(v0, v1, v2)
f.write(f' facet normal {nx:.6e} {ny:.6e} {nz:.6e}\n')
f.write(' outer loop\n')
for v in (v0, v1, v2):
f.write(f' vertex {v[0]:.6e} {v[1]:.6e} {v[2]:.6e}\n')
f.write(' endloop\n')
f.write(' endfacet\n')
f.write('endsolid butterfly_catastrophe\n')
size_kb = os.path.getsize(filename) / 1024
print(f' Triangles : {len(triangles):,}')
print(f' File size : {size_kb:.0f} KB → {filename}')
2026-03-25 21:48:29 +00:00
# ── 5. Main ───────────────────────────────────────────────────────────────────
2026-03-25 18:23:53 +00:00
if __name__ == '__main__':
2026-03-25 21:48:29 +00:00
print(f'Building butterfly catastrophe surface (grid={GRID})…')
tris = build_mesh()
2026-03-25 18:23:53 +00:00
tris = add_base(tris)
print('Writing ASCII STL…')
write_ascii_stl(tris, 'butterfly_catastrophe.stl')
print('Done.')