diff --git a/algebra/holonomy.py b/algebra/holonomy.py index c4edb2a7..5b7322a6 100644 --- a/algebra/holonomy.py +++ b/algebra/holonomy.py @@ -2,65 +2,77 @@ Holonomy prompt encoding. A prompt w1, w2, ..., wn is encoded as the geometric holonomy of its -forward+reverse versor walk. The walk closes, producing a versor that -is bounded by construction and invariant to global phase. +forward+reverse versor walk. The walk closes, producing a bounded algebraic +summary of the prompt path. -The holonomy IS a versor — it drops directly into versor_apply with -no bridging code. The fuel and the engine are the same substance. +The input word objects must already be valid construction-time versors. +Holonomy may unitize intermediate construction products to prevent float32 +scale blow-up, but never repairs propagation state. """ +from __future__ import annotations + import numpy as np + from .cl41 import geometric_product, reverse as cl_reverse -from .versor import normalize_to_versor +from .versor import unitize_versor from .cga import cga_inner +def _renorm_if_needed(H: np.ndarray, step: int, renorm_every: int) -> np.ndarray: + """Bound accumulator scale to prevent float32 overflow on long prompts.""" + if renorm_every <= 0 or step % renorm_every != 0: + return H + norm = float(np.linalg.norm(H)) + if not np.isfinite(norm) or norm < 1e-12: + raise ValueError("holonomy accumulator became null/non-finite during encoding.") + return (H / norm).astype(np.float32) + + def holonomy_encode( word_versors: list, alpha: float = 0.5, - weights: list = None, + weights: list | None = None, + renorm_every: int = 8, ) -> np.ndarray: """ Compute the holonomy of the word versor sequence. Forward walk: F = w1 * w2 * ... * wn (weighted by word frequency inverse) Reverse walk: R = (1-alpha) * reverse(wn) * ... * reverse(w1) - Holonomy: H = geometric_product(F, R) + Holonomy: H = F * R - H is a versor. For alpha=0.5, the holonomy captures the geometric - curvature of the prompt path. Prompts with different semantic content - produce geometrically distinct holonomies even at the same length. - - weights: optional list of float scalars (e.g. inverse token frequency). - Rare content words rotate more than common function words. - If None, uniform weights are used. + Construction-time unitization is used at the boundary and at the final + product. A bounded Euclidean renormalization is also applied every + `renorm_every` steps to prevent long prompt overflow in float32. """ if not word_versors: raise ValueError("Cannot encode empty prompt.") + if not 0.0 <= alpha <= 1.0: + raise ValueError("alpha must be in [0, 1].") n = len(word_versors) if weights is None: weights = [1.0] * n - assert len(weights) == n + if len(weights) != n: + raise ValueError("weights length must match word_versors length.") - # Forward accumulation - F = word_versors[0].copy() * weights[0] - F = normalize_to_versor(F) + # Forward accumulation. + F = unitize_versor(np.asarray(word_versors[0], dtype=np.float32) * weights[0]) for k in range(1, n): - w = word_versors[k] * weights[k] - w = normalize_to_versor(w) + w = unitize_versor(np.asarray(word_versors[k], dtype=np.float32) * weights[k]) F = geometric_product(F, w) + F = _renorm_if_needed(F, k, renorm_every) - # Reverse accumulation with alpha damping - R = cl_reverse(word_versors[-1]) * (1.0 - alpha) - R = normalize_to_versor(R) + # Reverse accumulation with alpha damping. + R = unitize_versor(cl_reverse(word_versors[-1]) * (1.0 - alpha)) for k in range(n - 2, -1, -1): - r = cl_reverse(word_versors[k]) - r = normalize_to_versor(r) + r = unitize_versor(cl_reverse(word_versors[k])) R = geometric_product(r, R) + R = _renorm_if_needed(R, n - 1 - k, renorm_every) H = geometric_product(F, R) - return normalize_to_versor(H) + return unitize_versor(H) def holonomy_similarity(H1: np.ndarray, H2: np.ndarray) -> float: @@ -68,4 +80,4 @@ def holonomy_similarity(H1: np.ndarray, H2: np.ndarray) -> float: Compare two holonomies via CGA inner product. Used for prompt-level semantic similarity without embedding lookup. """ - return cga_inner(H1, H2) + return cga_inner(unitize_versor(H1), unitize_versor(H2))