diff --git a/algebra/__init__.py b/algebra/__init__.py index 94df920d..cf1aa1f0 100644 --- a/algebra/__init__.py +++ b/algebra/__init__.py @@ -2,3 +2,4 @@ from .cl41 import geometric_product, reverse, grade_project, scalar_part, norm_s from .versor import versor_apply, normalize_to_versor, versor_condition from .cga import cga_inner, outer_product, is_null, null_project, embed_point from .holonomy import holonomy_encode, holonomy_similarity +from .rotor import word_transition_rotor diff --git a/algebra/rotor.py b/algebra/rotor.py new file mode 100644 index 00000000..c02cb649 --- /dev/null +++ b/algebra/rotor.py @@ -0,0 +1,34 @@ +""" +algebra/rotor.py — Rotor construction operators for Cl(4,1). + +Rotors are operators. They live here, in algebra/, not in vocab/. +A rotor between two word-versors is a contextual, field-level concern: +it describes a transformation being applied, not a property of the vocabulary. +""" + +import numpy as np +from .cl41 import geometric_product, reverse +from .versor import normalize_to_versor + + +def word_transition_rotor(A: np.ndarray, B: np.ndarray) -> np.ndarray: + """ + Compute the rotor R that rotates versor A toward versor B in Cl(4,1). + + R = normalize(1 + B * reverse(A)) + + This is a pure operator — it transforms a field state, it does not + encode a position. Call this from algebra-aware field logic; never + store the result on a vocabulary structure. + + Args: + A: Source versor, shape (32,), grade-normed to ±1. + B: Target versor, shape (32,), grade-normed to ±1. + + Returns: + R: Normalized rotor in Cl(4,1), shape (32,). + """ + R = geometric_product(B, reverse(A)) + R = R.copy() + R[0] += 1.0 + return normalize_to_versor(R) diff --git a/vocab/manifold.py b/vocab/manifold.py index 7e1d109d..90db5e47 100644 --- a/vocab/manifold.py +++ b/vocab/manifold.py @@ -3,23 +3,51 @@ VocabManifold — the geometric vocabulary. Each word is a versor in Cl(4,1). nearest(F) finds the closest word by CGA inner product — no cosine similarity, no ANN index. + +Invariant: every stored versor must satisfy the Cl(4,1) grade-norm +condition |V * reverse(V)|_scalar ≈ ±1. This is enforced at insertion +time in add(). Raw coordinate vectors (e.g. from external embeddings) +will fail this check — use normalize_to_versor() before calling add(). + +Rotor construction between word-versors is NOT a vocabulary concern. +Use algebra.word_transition_rotor(A, B) from the algebra layer when +a transition operator is needed in field or generation logic. """ import numpy as np from algebra.cga import cga_inner -from algebra.versor import normalize_to_versor from algebra.cl41 import geometric_product, reverse +from algebra.versor import normalize_to_versor class VocabManifold: def __init__(self): self._words: list = [] - self._versors: list = [] # each shape (32,) + self._versors: list = [] # each shape (32,), grade-normed to ±1 def add(self, word: str, versor: np.ndarray) -> None: - """Register a word-versor pair.""" + """ + Register a word-versor pair. + + Enforces the Cl(4,1) versor invariant: the scalar part of + V * reverse(V) must be ≈ ±1. This rejects any raw coordinate + vector or external embedding that has not been lifted into the + algebra. If your source is a float array from outside the system, + call normalize_to_versor() first. + + Raises: + ValueError: if the grade-norm condition is not satisfied. + """ + v = np.asarray(versor, dtype=np.float32).copy() + grade_norm = float(geometric_product(v, reverse(v))[0]) + if not (0.95 <= abs(grade_norm) <= 1.05): + raise ValueError( + f"Word '{word}': versor grade-norm {grade_norm:.4f} ≠ ±1. " + "Pass a valid Cl(4,1) versor. " + "If lifting from a raw array, call normalize_to_versor() first." + ) self._words.append(word) - self._versors.append(np.asarray(versor, dtype=np.float32).copy()) + self._versors.append(v) def get_versor(self, word: str) -> np.ndarray: """Look up a word's versor. Raises KeyError if not found.""" @@ -46,17 +74,5 @@ class VocabManifold: best_idx = i return self._words[best_idx], best_idx - def edge_rotor(self, from_idx: int, to_idx: int) -> np.ndarray: - """ - Compute the rotor that rotates from_versor toward to_versor. - R = normalize(1 + B * reverse(A)) - """ - A = self._versors[from_idx] - B = self._versors[to_idx] - R = geometric_product(B, reverse(A)) - R = R.copy() - R[0] += 1.0 - return normalize_to_versor(R) - def __len__(self) -> int: return len(self._words)