Research-Stack/4-Infrastructure/NoDupeLabs/scripts/chandelier_genus3_descent.py

261 lines
8.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
Chandelier + Genus-3 Descent Simulation
This is a candidate visualization/model for the Research Stack intuition:
chandelier-shaped shrinking basin
+ genus-3 topology
+ damped descent
+ angular momentum / torsion growth
+ blue/red shift trace
+ flash events at phase transitions
It is not proof. It is a physics-shaped sketch that emits traces for later
Graph.lean / torsion / FAMM handling.
"""
from __future__ import annotations
import json
import math
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import List, Tuple
import matplotlib.pyplot as plt
import numpy as np
@dataclass
class FlashEvent:
step: int
time: float
tier_from: int
tier_to: int
delta_energy: float
radius: float
shift_factor: float
def holes() -> np.ndarray:
"""Three topological handles/obstructions in the 2D chart."""
return np.array([
[1.35, 0.15],
[-0.75, 1.10],
[-0.80, -1.15],
], dtype=float)
def chandelier_potential(pos: np.ndarray) -> float:
"""
Shrinking-basin potential.
Existing equations being borrowed:
- harmonic descent toward zero: V ~ r^2
- tier barriers: Gaussian energy shells
- genus-3 obstruction: three repulsive handles/holes
- blue/red shift proxy later uses Δf/f ≈ -ΔΦ/c^2, rescaled
"""
x, y = pos
r = math.hypot(x, y) + 1e-9
# The fundamental rest-at-zero tendency.
bottom_well = 0.45 * r * r
# Chandelier tiers: energy shells that must be crossed.
tier_radii = [3.2, 2.25, 1.35, 0.62]
tier_heights = [0.38, 0.32, 0.26, 0.18]
tier_widths = [0.10, 0.09, 0.075, 0.06]
tiers = 0.0
for radius, height, width in zip(tier_radii, tier_heights, tier_widths):
tiers += height * math.exp(-((r - radius) ** 2) / (2 * width * width))
# Genus-3: three high-energy holes the descent must wind around.
obstruction = 0.0
for hx, hy in holes():
d2 = (x - hx) ** 2 + (y - hy) ** 2
obstruction += 0.30 * math.exp(-d2 / (2 * 0.22 * 0.22))
# Weak 3-fold angular corrugation so the handles matter as routes, not just dots.
theta = math.atan2(y, x)
angular = 0.055 * math.sin(3.0 * theta + 1.7 / (r + 0.22)) * math.exp(-0.25 * r)
return bottom_well + tiers + obstruction + angular
def numerical_gradient(f, pos: np.ndarray, eps: float = 1e-4) -> np.ndarray:
grad = np.zeros_like(pos)
for i in range(len(pos)):
plus = pos.copy()
minus = pos.copy()
plus[i] += eps
minus[i] -= eps
grad[i] = (f(plus) - f(minus)) / (2 * eps)
return grad
def tier_index(radius: float) -> int:
"""Discrete chandelier tiers; crossing them creates flash events."""
if radius > 2.75:
return 4
if radius > 1.80:
return 3
if radius > 1.00:
return 2
if radius > 0.42:
return 1
return 0
def simulate(
steps: int = 2400,
dt: float = 0.018,
damping: float = 0.065,
torsion_drive: float = 0.075,
) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, List[FlashEvent]]:
"""
Damped descent with a torsion term.
Equation family:
m x¨ + γ x˙ + ∇V(x) = torsion_drive * J x˙
J rotates the velocity by 90 degrees. As the basin narrows, conservation-like
angular behavior makes the path wind tighter around the bottom.
"""
pos = np.array([3.75, 0.28], dtype=float)
vel = np.array([-0.04, 0.34], dtype=float)
traj = []
energies = []
torsions = []
shifts = []
flashes: List[FlashEvent] = []
previous_tier = tier_index(np.linalg.norm(pos))
previous_energy = chandelier_potential(pos) + 0.5 * float(np.dot(vel, vel))
ceiling_energy = previous_energy
for step in range(steps):
r = float(np.linalg.norm(pos)) + 1e-9
grad = numerical_gradient(chandelier_potential, pos)
# Rotational/torsion drive. Stronger as radius shrinks.
Jvel = np.array([-vel[1], vel[0]])
torsion_scale = torsion_drive / (r + 0.18)
acc = -grad - damping * vel + torsion_scale * Jvel
vel = vel + acc * dt
pos = pos + vel * dt
kinetic = 0.5 * float(np.dot(vel, vel))
potential = chandelier_potential(pos)
energy = kinetic + potential
# Angular velocity proxy: omega = (r x v)/r^2.
angular_momentum_proxy = pos[0] * vel[1] - pos[1] * vel[0]
omega = angular_momentum_proxy / (r * r)
torsion = abs(omega) / (r + 0.12)
# Blue/red shift proxy from potential drop. c is scaled to 1 for model-space.
# Falling from ceiling -> shift_factor > 1 (blue). Relaxing/rising -> lower.
delta_phi = ceiling_energy - potential
shift_factor = max(0.05, 1.0 + 0.18 * delta_phi)
current_tier = tier_index(float(np.linalg.norm(pos)))
if current_tier != previous_tier:
flashes.append(
FlashEvent(
step=step,
time=step * dt,
tier_from=previous_tier,
tier_to=current_tier,
delta_energy=previous_energy - energy,
radius=float(np.linalg.norm(pos)),
shift_factor=shift_factor,
)
)
previous_tier = current_tier
previous_energy = energy
traj.append(pos.copy())
energies.append(energy)
torsions.append(torsion)
shifts.append(shift_factor)
return np.array(traj), np.array(energies), np.array(torsions), np.array(shifts), flashes
def plot_outputs(traj: np.ndarray, energies: np.ndarray, torsions: np.ndarray, shifts: np.ndarray, flashes: List[FlashEvent], outdir: Path) -> None:
outdir.mkdir(parents=True, exist_ok=True)
xs = np.linspace(-4.2, 4.2, 340)
ys = np.linspace(-4.2, 4.2, 340)
X, Y = np.meshgrid(xs, ys)
Z = np.vectorize(lambda x, y: chandelier_potential(np.array([x, y])))(X, Y)
plt.figure(figsize=(9, 9))
plt.contourf(X, Y, Z, levels=80, cmap="magma")
plt.contour(X, Y, Z, levels=18, colors="white", alpha=0.18, linewidths=0.6)
plt.plot(traj[:, 0], traj[:, 1], color="cyan", linewidth=1.5, alpha=0.9, label="descent path")
for h in holes():
circle = plt.Circle(tuple(h), 0.23, color="black", alpha=0.55)
plt.gca().add_patch(circle)
if flashes:
fp = traj[[f.step for f in flashes if f.step < len(traj)]]
plt.scatter(fp[:, 0], fp[:, 1], s=80, c="gold", edgecolors="white", label="phase flash")
plt.title("Chandelier + Genus-3 Descent: shrinking basin with flash transitions")
plt.xlabel("x chart")
plt.ylabel("y chart")
plt.axis("equal")
plt.legend(loc="upper right")
plt.tight_layout()
plt.savefig(outdir / "chandelier_genus3_descent_map.png", dpi=180)
plt.close()
plt.figure(figsize=(10, 4))
plt.plot(energies, label="total energy")
plt.plot(torsions / max(1e-9, np.max(torsions)), label="torsion normalized")
plt.plot(shifts / max(1e-9, np.max(shifts)), label="blue/red shift proxy normalized")
for f in flashes:
plt.axvline(f.step, color="gold", alpha=0.28)
plt.title("Energy descends; torsion and frequency-shift respond")
plt.xlabel("step")
plt.legend()
plt.tight_layout()
plt.savefig(outdir / "chandelier_genus3_traces.png", dpi=180)
plt.close()
def main() -> None:
outdir = Path("research-stack/models/chandelier_genus3_outputs")
traj, energies, torsions, shifts, flashes = simulate()
plot_outputs(traj, energies, torsions, shifts, flashes, outdir)
report = {
"model_id": "chandelier_genus3_descent_v0",
"status": "HOLD",
"proof_status": "simulation_sketch_not_proof",
"rule": "TV imagery is not proof; this simulation tests whether the geometry intuition produces coherent traces.",
"summary": {
"steps": int(len(traj)),
"initial_energy": float(energies[0]),
"final_energy": float(energies[-1]),
"max_torsion": float(np.max(torsions)),
"final_torsion": float(torsions[-1]),
"max_shift_factor": float(np.max(shifts)),
"flash_count": len(flashes),
},
"flashes": [asdict(f) for f in flashes],
"outputs": [
str(outdir / "chandelier_genus3_descent_map.png"),
str(outdir / "chandelier_genus3_traces.png"),
],
}
outdir.mkdir(parents=True, exist_ok=True)
(outdir / "chandelier_genus3_report.json").write_text(json.dumps(report, indent=2), encoding="utf-8")
print(json.dumps(report["summary"], indent=2))
if __name__ == "__main__":
main()