import math
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
from typing import Optional, List, Tuple
from ._versorc_math import (
Cl3,
PoincarePoint,
get_e8_roots,
get_lloyd_max_3bit_centroids,
)
def compute_e8_coxeter_basis() -> Tuple[np.ndarray, np.ndarray]:
simple_roots = np.array([
[ 0.5, -0.5, -0.5, -0.5, -0.5, -0.5, -0.5, 0.5],
[ 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[-1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[ 0.0, -1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[ 0.0, 0.0, -1.0, 1.0, 0.0, 0.0, 0.0, 0.0],
[ 0.0, 0.0, 0.0, -1.0, 1.0, 0.0, 0.0, 0.0],
[ 0.0, 0.0, 0.0, 0.0, -1.0, 1.0, 0.0, 0.0],
[ 0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 1.0, 0.0],
], dtype=np.float64)
coxeter_mat = np.eye(8, dtype=np.float64)
for alpha in simple_roots:
refl = np.eye(8, dtype=np.float64) - 2.0 * np.outer(alpha, alpha) / np.dot(alpha, alpha)
coxeter_mat = coxeter_mat @ refl
eigvals, eigvecs = np.linalg.eig(coxeter_mat)
exponents = np.angle(eigvals) * 30.0 / (2.0 * np.pi)
idx = int(np.argmin(np.abs(exponents - 1.0)))
v = eigvecs[:, idx]
u1 = np.real(v)
u2 = np.imag(v)
u1 = u1 / np.linalg.norm(u1)
u2 = u2 - np.dot(u2, u1) * u1
u2 = u2 / np.linalg.norm(u2)
return u1, u2
def plot_lloyd_max_centroids(
samples: Optional[np.ndarray] = None,
bins: int = 80,
title: str = "Clifford 3-Bit Lloyd-Max Centroids & Decision Boundaries",
save_path: Optional[str] = None,
) -> plt.Figure:
centroids = get_lloyd_max_3bit_centroids()
sorted_centroids = np.sort(centroids)
fig, ax = plt.subplots(figsize=(10, 5), dpi=120)
x = np.linspace(-3.5, 3.5, 1000)
pdf = (1.0 / np.sqrt(2 * np.pi)) * np.exp(-0.5 * x**2)
ax.plot(x, pdf, "k-", lw=1.8, label="Gaussian N(0, 1) Reference PDF")
if samples is not None:
flat = np.asarray(samples, dtype=np.float32).ravel()
rms = float(np.sqrt(np.mean(flat**2)))
flat_norm = flat / rms if rms > 1e-6 else flat
ax.hist(
flat_norm,
bins=bins,
density=True,
alpha=0.35,
color="#2b5c8f",
edgecolor="none",
label="Normalized Rotated Distribution R(W) / σ",
)
palette = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b", "#e377c2", "#7f7f7f"]
for i, c in enumerate(sorted_centroids):
ax.axvline(
c,
color=palette[i % len(palette)],
linestyle="--",
lw=1.5,
label=f"Level {i}: {c:+.4f}" if samples is None else None,
)
for i in range(len(sorted_centroids) - 1):
mid = (sorted_centroids[i] + sorted_centroids[i + 1]) / 2.0
ax.axvline(
mid,
color="#888888",
linestyle=":",
lw=0.9,
alpha=0.7,
label="Decision Boundaries" if i == 0 and samples is None else None,
)
ax.set_title(title, fontsize=12, pad=12)
ax.set_xlabel("Normalized Value (Standard Deviations σ)", fontsize=10)
ax.set_ylabel("Probability Density", fontsize=10)
ax.grid(True, linestyle=":", alpha=0.5)
ax.legend(loc="upper right", frameon=True)
plt.tight_layout()
if save_path:
fig.savefig(save_path, dpi=300, bbox_inches="tight")
return fig
def plot_e8_coxeter_plane(
title: str = "E8 Root System Projection on 2D Coxeter Plane (240 Roots, D30 Symmetry)",
save_path: Optional[str] = None,
) -> plt.Figure:
roots = get_e8_roots().astype(np.float64) u1, u2 = compute_e8_coxeter_basis()
proj_x = roots @ u1
proj_y = roots @ u2
fig, ax = plt.subplots(figsize=(8, 8), dpi=150)
fig.patch.set_facecolor("#0f111a")
ax.set_facecolor("#0f111a")
gram = roots @ roots.T
for i in range(240):
neighbors = np.where(np.abs(gram[i] - 1.0) < 1e-4)[0]
for j in neighbors:
if i < j:
ax.plot(
[proj_x[i], proj_x[j]],
[proj_y[i], proj_y[j]],
color="#4e88c7",
alpha=0.06,
lw=0.45,
)
radii = np.sqrt(proj_x**2 + proj_y**2)
ax.scatter(
proj_x,
proj_y,
c=radii,
cmap="plasma",
s=28,
alpha=0.95,
edgecolors="white",
linewidths=0.35,
zorder=5,
)
ax.set_title(title, color="white", fontsize=11, pad=15)
ax.axis("equal")
ax.axis("off")
plt.tight_layout()
if save_path:
fig.savefig(save_path, dpi=300, facecolor=fig.get_facecolor(), bbox_inches="tight")
return fig
def plot_clifford_isotropy_comparison(
original_weights: np.ndarray,
rotated_weights: np.ndarray,
title: str = "Outlier Smearing via Dyadic Walsh Versor Isometry: W vs R(W)",
save_path: Optional[str] = None,
) -> plt.Figure:
orig_flat = np.asarray(original_weights, dtype=np.float32).ravel()
rot_flat = np.asarray(rotated_weights, dtype=np.float32).ravel()
rms = float(np.sqrt(np.mean(rot_flat**2)))
orig_norm = orig_flat / rms if rms > 1e-6 else orig_flat
rot_norm = rot_flat / rms if rms > 1e-6 else rot_flat
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5), dpi=120)
bins = 100
ax1.hist(orig_norm, bins=bins, density=True, alpha=0.5, color="#d9534f", label="Raw Weights W / σ (Outliers)")
ax1.hist(rot_norm, bins=bins, density=True, alpha=0.5, color="#2b5c8f", label="Rotated R(W) / σ (Gaussian)")
ax1.set_title("Probability Density (Linear)", fontsize=11)
ax1.set_xlabel("Normalized Weight (σ units)", fontsize=10)
ax1.set_ylabel("Density", fontsize=10)
ax1.grid(True, linestyle=":", alpha=0.5)
ax1.legend()
ax2.hist(orig_norm, bins=bins, density=True, alpha=0.5, color="#d9534f", log=True, label="Raw W / σ (Extreme Tails)")
ax2.hist(rot_norm, bins=bins, density=True, alpha=0.5, color="#2b5c8f", log=True, label="Rotated R(W) / σ (Bounded)")
for c in np.sort(get_lloyd_max_3bit_centroids()):
ax2.axvline(c, color="#ffaa00", linestyle="--", lw=1.1, alpha=0.8)
ax2.set_title("Probability Density (Logarithmic Tails & Centroids)", fontsize=11)
ax2.set_xlabel("Normalized Weight (σ units)", fontsize=10)
ax2.set_ylabel("Log Density", fontsize=10)
ax2.grid(True, linestyle=":", alpha=0.5)
ax2.legend()
fig.suptitle(title, fontsize=12, y=1.02)
plt.tight_layout()
if save_path:
fig.savefig(save_path, dpi=300, bbox_inches="tight")
return fig
def plot_poincare_disk(
points: Optional[np.ndarray] = None,
title: str = "Poincaré Hyperbolic Disk Model D^2 (Orthogonal Geodesics)",
save_path: Optional[str] = None,
) -> plt.Figure:
fig, ax = plt.subplots(figsize=(6, 6), dpi=120)
theta = np.linspace(0, 2 * np.pi, 500)
ax.plot(np.cos(theta), np.sin(theta), "k-", lw=1.5, label="Boundary S^1 (|z| = 1)")
ax.fill(np.cos(theta), np.sin(theta), color="#f5f7fa", zorder=0)
for r in [0.3, 0.5, 0.7, 0.9]:
ax.plot(r * np.cos(theta), r * np.sin(theta), color="#c0c4cc", linestyle=":", lw=0.8)
if points is not None:
pts = np.asarray(points, dtype=np.float32)
assert pts.shape[1] == 2, "Points must be of shape (N, 2)"
norms = np.linalg.norm(pts, axis=1)
valid_pts = pts[norms < 1.0]
p_objs = [PoincarePoint([float(p[0]), float(p[1])]) for p in valid_pts]
for i in range(len(p_objs)):
for j in range(i + 1, len(p_objs)):
arc = np.array(p_objs[i].geodesic_arc(p_objs[j], 60))
ax.plot(arc[:, 0], arc[:, 1], color="#fd7e14", lw=1.4, alpha=0.75)
ax.scatter(valid_pts[:, 0], valid_pts[:, 1], c="#2b5c8f", s=45, zorder=5, label="Hyperbolic Points")
ax.set_title(title, fontsize=11, pad=10)
ax.set_xlim(-1.1, 1.1)
ax.set_ylim(-1.1, 1.1)
ax.axis("equal")
ax.axis("off")
ax.legend(loc="lower right", frameon=True)
plt.tight_layout()
if save_path:
fig.savefig(save_path, dpi=300, bbox_inches="tight")
return fig
def plot_bivector_blade(
u: np.ndarray,
v: np.ndarray,
origin: Optional[np.ndarray] = None,
title: str = "Geometric Algebra 2-Blade: Bivector u ∧ v",
save_path: Optional[str] = None,
) -> plt.Figure:
u_vec = np.asarray(u, dtype=np.float64).ravel()
v_vec = np.asarray(v, dtype=np.float64).ravel()
assert len(u_vec) == 3 and len(v_vec) == 3, "Vectors u and v must be 3-dimensional"
orig = np.zeros(3, dtype=np.float64) if origin is None else np.asarray(origin, dtype=np.float64).ravel()
u_mv = Cl3.vector(float(u_vec[0]), float(u_vec[1]), float(u_vec[2]))
v_mv = Cl3.vector(float(v_vec[0]), float(v_vec[1]), float(v_vec[2]))
biv = u_mv ^ v_mv
dual_mv = biv.dual()
dual_arr = dual_mv.to_numpy()
dual = np.array([dual_arr[1], dual_arr[2], dual_arr[4]], dtype=np.float64)
area = float(biv.norm())
fig = plt.figure(figsize=(8, 7), dpi=120)
ax = fig.add_subplot(111, projection="3d")
verts = [np.array([orig, orig + u_vec, orig + u_vec + v_vec, orig + v_vec])]
poly = Poly3DCollection(verts, alpha=0.35, facecolor="#2b5c8f", edgecolor="#1a3a5c", lw=1.5)
ax.add_collection3d(poly)
ax.quiver(orig[0], orig[1], orig[2], u_vec[0], u_vec[1], u_vec[2],
color="#d9534f", lw=2.2, arrow_length_ratio=0.1, label=f"1-Blade u ({u_vec[0]:.1f}, {u_vec[1]:.1f}, {u_vec[2]:.1f})")
ax.quiver(orig[0], orig[1], orig[2], v_vec[0], v_vec[1], v_vec[2],
color="#28a745", lw=2.2, arrow_length_ratio=0.1, label=f"1-Blade v ({v_vec[0]:.1f}, {v_vec[1]:.1f}, {v_vec[2]:.1f})")
center = orig + (u_vec + v_vec) / 2.0
if area > 1e-6:
ax.quiver(center[0], center[1], center[2], dual[0], dual[1], dual[2],
color="#6f42c1", lw=2.0, linestyle="--", arrow_length_ratio=0.12,
label=f"Hodge Dual (u ∧ v)I⁻¹ (|B| = {area:.2f})")
all_pts = np.vstack([orig, orig + u_vec, orig + v_vec, orig + u_vec + v_vec, center + dual])
min_xyz = np.min(all_pts, axis=0) - 0.3
max_xyz = np.max(all_pts, axis=0) + 0.3
max_range = np.max(max_xyz - min_xyz) / 2.0
mid_xyz = (max_xyz + min_xyz) / 2.0
ax.set_xlim(mid_xyz[0] - max_range, mid_xyz[0] + max_range)
ax.set_ylim(mid_xyz[1] - max_range, mid_xyz[1] + max_range)
ax.set_zlim(mid_xyz[2] - max_range, mid_xyz[2] + max_range)
ax.set_xlabel("e1", fontsize=10)
ax.set_ylabel("e2", fontsize=10)
ax.set_zlabel("e3", fontsize=10)
ax.set_title(title, fontsize=11, pad=12)
ax.legend(loc="upper left", frameon=True)
plt.tight_layout()
if save_path:
fig.savefig(save_path, dpi=300, bbox_inches="tight")
return fig
def plot_rotor_orbit(
rotation_axis: np.ndarray,
theta_rad: float,
probe_vector: np.ndarray,
num_steps: int = 60,
title: str = "Clifford Rotor Action: v' = R v R~",
save_path: Optional[str] = None,
) -> plt.Figure:
n = np.asarray(rotation_axis, dtype=np.float64).ravel()
n_norm = np.linalg.norm(n)
assert n_norm > 1e-6, "Rotation axis must be non-zero"
n_unit = n / n_norm
v0 = np.asarray(probe_vector, dtype=np.float64).ravel()
assert len(v0) == 3, "Probe vector must be 3-dimensional"
v0_mv = Cl3.vector(float(v0[0]), float(v0[1]), float(v0[2]))
n_mv = Cl3.vector(float(n_unit[0]), float(n_unit[1]), float(n_unit[2]))
b_unit = n_mv.dual()
t_vals = np.linspace(0, theta_rad, num_steps)
orbit = np.zeros((num_steps, 3), dtype=np.float64)
for i, t in enumerate(t_vals):
rotor = Cl3.from_scalar(float(math.cos(t * 0.5))) - b_unit * float(math.sin(t * 0.5))
vt_arr = rotor.sandwich(v0_mv).to_numpy()
orbit[i] = [vt_arr[1], vt_arr[2], vt_arr[4]]
fig = plt.figure(figsize=(8, 7), dpi=120)
ax = fig.add_subplot(111, projection="3d")
ax.plot(orbit[:, 0], orbit[:, 1], orbit[:, 2], color="#fd7e14", lw=2.8, label=f"Rotor Orbit (θ = {np.degrees(theta_rad):.1f}°)")
ax.quiver(0, 0, 0, v0[0], v0[1], v0[2], color="#2b5c8f", lw=2.2, arrow_length_ratio=0.1, label="Initial Vector v(0)")
v_final = orbit[-1]
ax.quiver(0, 0, 0, v_final[0], v_final[1], v_final[2], color="#28a745", lw=2.2, arrow_length_ratio=0.1, label="Rotated v(θ) = R v(0) R~")
ax.quiver(0, 0, 0, n_unit[0], n_unit[1], n_unit[2], color="#6f42c1", lw=1.8, linestyle=":", arrow_length_ratio=0.15, label="Bivector Dual Axis *(B)")
all_pts = np.vstack([np.zeros(3), v0, v_final, n_unit, orbit])
min_xyz = np.min(all_pts, axis=0) - 0.3
max_xyz = np.max(all_pts, axis=0) + 0.3
max_range = np.max(max_xyz - min_xyz) / 2.0
mid_xyz = (max_xyz + min_xyz) / 2.0
ax.set_xlim(mid_xyz[0] - max_range, mid_xyz[0] + max_range)
ax.set_ylim(mid_xyz[1] - max_range, mid_xyz[1] + max_range)
ax.set_zlim(mid_xyz[2] - max_range, mid_xyz[2] + max_range)
ax.set_xlabel("e1", fontsize=10)
ax.set_ylabel("e2", fontsize=10)
ax.set_zlabel("e3", fontsize=10)
ax.set_title(title, fontsize=11, pad=12)
ax.legend(loc="upper left", frameon=True)
plt.tight_layout()
if save_path:
fig.savefig(save_path, dpi=300, bbox_inches="tight")
return fig