From 9723941a387c1c44acd8efdaa8b86b2474be4998 Mon Sep 17 00:00:00 2001 From: Shay Date: Thu, 14 May 2026 10:55:11 -0700 Subject: [PATCH] Fail closed on invalid versor construction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make versor construction fail closed instead of synthesizing hash-derived fallback rotors. - remove pseudo-random construction fallback from unitize_versor - add signed residual helper for +1 field states vs ±1 manifold entries - validate vocab manifold entries with full residuals - document antipodal transition rotor failure contract - add focused invariant tests for versor closure and manifold validation --- algebra/rotor.py | 10 ++ algebra/versor.py | 150 +++++++----------------- tests/test_versor_closure.py | 62 ++++++++-- tests/test_vocab_manifold_invariants.py | 52 ++++++++ vocab/manifold.py | 68 +++++++---- 5 files changed, 197 insertions(+), 145 deletions(-) create mode 100644 tests/test_vocab_manifold_invariants.py diff --git a/algebra/rotor.py b/algebra/rotor.py index 3cc3da56..979879bd 100644 --- a/algebra/rotor.py +++ b/algebra/rotor.py @@ -25,12 +25,22 @@ def word_transition_rotor(A: np.ndarray, B: np.ndarray) -> np.ndarray: encode a position. Call this from algebra-aware field logic; never store the result on a vocabulary structure. + Antipodal or near-antipodal inputs can make 1 + B * reverse(A) null or + near-zero. That is an ill-conditioned transition construction, not a + case for synthetic fallback. unitize_versor() must fail closed, and the + caller must decide whether to skip, terminate, or choose another edge. + Args: A: Source versor, shape (32,), grade-normed to ±1. B: Target versor, shape (32,), grade-normed to ±1. Returns: R: Unitized rotor in Cl(4,1), shape (32,). + + Raises: + ValueError: if the transition rotor is null, near-zero, non-scalar + after multiplication by its reverse, or otherwise cannot be + scaled into a clean +1 operator. """ R = geometric_product(B, reverse(A)) R = R.copy() diff --git a/algebra/versor.py b/algebra/versor.py index 9b3dc06e..a77bae9f 100644 --- a/algebra/versor.py +++ b/algebra/versor.py @@ -1,120 +1,55 @@ -""" -algebra/versor.py — Versor operations for Cl(4,1). - -Normalization doctrine: - - unitize_versor(v) — CONSTRUCTION primitive. - Call this when building rotors, motors, or - manifold entries from raw arrays. It is the - algebra layer's legitimate construction operation. - May be called in: algebra/, persona/, vocab/ (pre-add). - - normalize_to_versor(v) — GATE primitive. Internal to ingest/gate.py. - Normalizes raw holonomy output to a versor at - the injection boundary. Do not call this anywhere - else in production code. It is NOT the same - operation as unitize_versor conceptually — it is - the boundary crossing from raw data into the field. - - FORBIDDEN: calling either function inside propagation, generation, - vault recall, or as a post-hoc repair for a supposedly - closed transition. If you need normalization there, the - algebra is not closed — fix the operator, not the result. -""" - from __future__ import annotations -import hashlib + import numpy as np -from .cl41 import geometric_product, reverse, N_COMPONENTS +from .cl41 import geometric_product, reverse __all__ = [ "unitize_versor", "versor_apply", "versor_condition", - # normalize_to_versor is intentionally NOT in __all__. - # Import it explicitly only if you are ingest/gate.py. + "versor_unit_residual", ] +_CONSTRUCTION_RESIDUE_TOLERANCE = 1e-2 +_NEAR_ZERO_TOLERANCE = 1e-12 + + +def _array_dtype(v: np.ndarray) -> np.dtype: + arr = np.asarray(v) + return arr.dtype if arr.dtype in (np.dtype(np.float32), np.dtype(np.float64)) else np.dtype(np.float32) + + +def _diagnostic_message(prefix: str, *, input_norm: float, scalar_sq: float, residue_norm: float) -> str: + return f"{prefix}: input_norm={input_norm:.6e}, scalar_sq={scalar_sq:.6e}, residue_norm={residue_norm:.6e}" + def unitize_versor(v: np.ndarray) -> np.ndarray: - """ - Construction-time algebra primitive. - - Scale v so that the scalar part of v * reverse(v) equals +1. - Use this when building rotors, motors, or vocabulary entries - from raw computed arrays. - - This is not a repair operation. It is valid only during construction - of new algebraic objects, never as a correction inside propagation. - - Args: - v: shape (N_COMPONENTS,) float32 multivector. - - Returns: - Scaled copy of v satisfying |V * ~V|_scalar ≈ 1. - - Raises: - ValueError: if v is a null, zero, or near-zero multivector. - """ - arr = np.asarray(v) - dtype = arr.dtype if arr.dtype in (np.dtype(np.float32), np.dtype(np.float64)) else np.dtype(np.float32) + dtype = _array_dtype(v) v = np.asarray(v, dtype=dtype) - vv = geometric_product(v, reverse(v)) + input_norm = float(np.linalg.norm(v)) + if input_norm < _NEAR_ZERO_TOLERANCE: + raise ValueError(_diagnostic_message("unitize_versor: near_zero", input_norm=input_norm, scalar_sq=0.0, residue_norm=0.0)) + + vv = geometric_product(v, reverse(v)).astype(dtype) scalar_sq = float(vv[0]) - if float(np.linalg.norm(v)) < 1e-12: - raise ValueError( - "unitize_versor: null, zero, or near-zero multivector; cannot unitize." - ) residue = vv.copy() residue[0] = 0 - if float(np.linalg.norm(residue)) < 1e-7 and scalar_sq > 0: - scale = 1.0 / np.sqrt(scalar_sq) - return (v * scale).astype(dtype) + residue_norm = float(np.linalg.norm(residue)) - digest = hashlib.sha256(np.ascontiguousarray(v).view(np.uint8)).digest() - flat_idx = digest[0] - theta_unit = int.from_bytes(digest[1:5], "big") / 2**32 - theta = 0.05 + theta_unit * (np.pi - 0.1) - sign_idx = int(np.argmax(np.abs(v[1:]))) + 1 - if float(v[sign_idx]) < 0: - theta = -theta - negative_bivectors = (6, 7, 9, 10, 12, 14) - rotor = np.zeros(N_COMPONENTS, dtype=dtype) - rotor[0] = np.cos(theta) - rotor[negative_bivectors[flat_idx % len(negative_bivectors)]] = np.sin(theta) - return rotor.astype(dtype) + if residue_norm >= _CONSTRUCTION_RESIDUE_TOLERANCE: + raise ValueError(_diagnostic_message("unitize_versor: bad_residue", input_norm=input_norm, scalar_sq=scalar_sq, residue_norm=residue_norm)) + + if scalar_sq <= 0.0: + raise ValueError(_diagnostic_message("unitize_versor: bad_scalar", input_norm=input_norm, scalar_sq=scalar_sq, residue_norm=residue_norm)) + + return (v * (1.0 / np.sqrt(scalar_sq))).astype(dtype) def normalize_to_versor(v: np.ndarray) -> np.ndarray: - """ - Gate-only injection primitive. Reserved for ingest/gate.py. - - Do not call this function outside the injection gate. - For construction of algebraic objects, use unitize_versor() instead. - """ - # Implementation is identical to unitize_versor — the distinction - # is semantic and enforced by convention + docs + test rules. return unitize_versor(v) def versor_apply(V: np.ndarray, F: np.ndarray) -> np.ndarray: - """ - Apply versor V to field state F via the sandwich product. - - F' = V * F * reverse(V) - - This is the ONLY way field state changes in production code. - No normalization is applied here. The sandwich product of two - valid versors is always a valid versor — algebraic closure is - the invariant, not runtime monitoring. - - Args: - V: versor operator, shape (N_COMPONENTS,). - F: field state, shape (N_COMPONENTS,). - - Returns: - F': transformed field state, shape (N_COMPONENTS,). - """ dtype = np.result_type(V, F) if dtype not in (np.dtype(np.float32), np.dtype(np.float64)): dtype = np.dtype(np.float32) @@ -123,18 +58,19 @@ def versor_apply(V: np.ndarray, F: np.ndarray) -> np.ndarray: return geometric_product(geometric_product(V, F), reverse(V)).astype(dtype) -def versor_condition(v: np.ndarray) -> float: - """ - Full residual distance from the unit-versor condition. - - Computes ||v * reverse(v) - 1||_F, not a signed scalar shortcut. - Zero means v satisfies the unit-versor condition. Any non-scalar residue - or scalar drift contributes positively to the residual. - """ - v = np.asarray(v) - dtype = v.dtype if v.dtype in (np.dtype(np.float32), np.dtype(np.float64)) else np.dtype(np.float32) +def versor_unit_residual(v: np.ndarray, *, allow_negative: bool = False) -> float: + dtype = _array_dtype(v) v = np.asarray(v, dtype=dtype) vv = geometric_product(v, reverse(v)).astype(dtype) - vv = vv.copy() - vv[0] -= 1.0 - return float(np.linalg.norm(vv)) + plus = vv.copy() + plus[0] -= 1.0 + plus_residual = float(np.linalg.norm(plus)) + if not allow_negative: + return plus_residual + minus = vv.copy() + minus[0] += 1.0 + return min(plus_residual, float(np.linalg.norm(minus))) + + +def versor_condition(v: np.ndarray) -> float: + return versor_unit_residual(v, allow_negative=False) diff --git a/tests/test_versor_closure.py b/tests/test_versor_closure.py index ea16728b..10077775 100644 --- a/tests/test_versor_closure.py +++ b/tests/test_versor_closure.py @@ -4,10 +4,17 @@ It verifies the core algebraic invariant of the entire system. """ import numpy as np +import pytest from hypothesis import given, settings from hypothesis import strategies as st -from algebra.versor import versor_apply, unitize_versor, versor_condition +from algebra.rotor import word_transition_rotor +from algebra.versor import ( + unitize_versor, + versor_apply, + versor_condition, + versor_unit_residual, +) def _positive_unit_reflector(seed=None) -> np.ndarray: @@ -16,8 +23,7 @@ def _positive_unit_reflector(seed=None) -> np.ndarray: The current field action uses V * F * reverse(V), so the operator fixture must satisfy V * reverse(V) = +1, not -1. We therefore keep the fifth - (negative-metric) basis component bounded below the positive four-space - norm before construction-unitizing. + basis component bounded below the positive four-space norm. """ rng = np.random.default_rng(seed) vec4 = rng.standard_normal(4).astype(np.float32) @@ -38,7 +44,6 @@ def _positive_unit_reflector(seed=None) -> np.ndarray: @given(st.integers(min_value=0, max_value=99)) @settings(max_examples=100) def test_versor_apply_preserves_manifold(seed): - """V*F*reverse(V) must be a versor if V and F are positive unit versors.""" V = _positive_unit_reflector(seed) F = _positive_unit_reflector(seed + 1000) result = versor_apply(V, F) @@ -46,18 +51,50 @@ def test_versor_apply_preserves_manifold(seed): assert cond < 1e-4, f"versor_apply broke the manifold: condition={cond:.2e}" -def test_unitize_random_multivector_constructs_versor(): - """ - unitize_versor() is the construction primitive for lifting raw - deterministic coordinates into a valid versor. - """ - raw = np.random.default_rng(0).standard_normal(32).astype(np.float32) +def test_unitize_clean_scalar_constructs_positive_unit_versor(): + raw = np.zeros(32, dtype=np.float32) + raw[0] = 2.0 V = unitize_versor(raw) - assert versor_condition(V) < 1e-5 + assert np.allclose(V[0], 1.0, atol=1e-7) + assert versor_condition(V) < 1e-7 + + +def test_unitize_rejects_non_scalar_residue_instead_of_hash_fallback(): + dirty = np.zeros(32, dtype=np.float32) + dirty[0] = np.sqrt(0.5) + dirty[1] = np.sqrt(0.5) + + with pytest.raises(ValueError, match="bad_residue"): + unitize_versor(dirty) + + +def test_unitize_rejects_non_positive_scalar_norm(): + negative_norm = np.zeros(32, dtype=np.float32) + negative_norm[5] = 1.0 + + with pytest.raises(ValueError, match="bad_scalar"): + unitize_versor(negative_norm) + + +def test_versor_unit_residual_can_accept_signed_manifold_versors(): + negative_norm = np.zeros(32, dtype=np.float32) + negative_norm[5] = 1.0 + + assert versor_condition(negative_norm) > 1.0 + assert versor_unit_residual(negative_norm, allow_negative=True) < 1e-7 + + +def test_word_transition_rotor_fails_closed_for_antipodal_inputs(): + A = np.zeros(32, dtype=np.float32) + A[0] = 1.0 + B = np.zeros(32, dtype=np.float32) + B[0] = -1.0 + + with pytest.raises(ValueError, match="near_zero"): + word_transition_rotor(A, B) def test_composition_closed(): - """Two sequential versor_apply calls stay on the manifold.""" V1 = _positive_unit_reflector(0) V2 = _positive_unit_reflector(1) F = _positive_unit_reflector(2) @@ -67,7 +104,6 @@ def test_composition_closed(): def test_identity_versor(): - """Scalar 1 is a valid versor and applies as identity.""" identity = np.zeros(32, dtype=np.float32) identity[0] = 1.0 F = _positive_unit_reflector(42) diff --git a/tests/test_vocab_manifold_invariants.py b/tests/test_vocab_manifold_invariants.py new file mode 100644 index 00000000..e0a21f65 --- /dev/null +++ b/tests/test_vocab_manifold_invariants.py @@ -0,0 +1,52 @@ +import numpy as np +import pytest + +from vocab.manifold import VocabManifold + + +def _identity() -> np.ndarray: + v = np.zeros(32, dtype=np.float32) + v[0] = 1.0 + return v + + +def _negative_unit_vector() -> np.ndarray: + v = np.zeros(32, dtype=np.float32) + v[5] = 1.0 + return v + + +def _scalar_norm_one_with_non_scalar_residue() -> np.ndarray: + v = np.zeros(32, dtype=np.float32) + v[0] = np.sqrt(0.5) + v[1] = np.sqrt(0.5) + return v + + +def test_manifold_accepts_positive_unit_versor() -> None: + manifold = VocabManifold() + manifold.add("one", _identity()) + assert manifold.index_of("one") == 0 + + +def test_manifold_accepts_negative_unit_versor() -> None: + """Vocabulary manifold entries follow the mathematical ±1 versor contract.""" + manifold = VocabManifold() + manifold.add("negative", _negative_unit_vector()) + assert manifold.index_of("negative") == 0 + + +def test_manifold_rejects_scalar_norm_shortcut_with_non_scalar_residue() -> None: + """Scalar grade-norm near one is insufficient when residue is non-scalar.""" + manifold = VocabManifold() + + with pytest.raises(ValueError, match="non_scalar_residue"): + manifold.add("dirty", _scalar_norm_one_with_non_scalar_residue()) + + +def test_manifold_update_rejects_non_scalar_residue() -> None: + manifold = VocabManifold() + manifold.add("clean", _identity()) + + with pytest.raises(ValueError, match="replacement versor residual"): + manifold.update("clean", _scalar_norm_one_with_non_scalar_residue()) diff --git a/vocab/manifold.py b/vocab/manifold.py index 51921202..e01a2f16 100644 --- a/vocab/manifold.py +++ b/vocab/manifold.py @@ -4,9 +4,10 @@ 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() and at replacement time in update(). +Invariant: every stored versor must satisfy the full Cl(4,1) unit-versor +condition V * reverse(V) ≈ ±1. This rejects non-scalar construction residue, +not merely scalar grade-norm drift, and is enforced at insertion time in +add() and at replacement time in update(). Normalization doctrine for this module: - Raw coordinate vectors (e.g. from external embeddings) must be @@ -30,13 +31,41 @@ dispatches to the Rust extension when available. import numpy as np from algebra.backend import cga_inner from algebra.cl41 import geometric_product, reverse +from algebra.versor import versor_unit_residual from language_packs.schema import MorphologyEntry +_MANIFOLD_RESIDUAL_TOLERANCE = 1e-5 + + +def _versor_diagnostics(v: np.ndarray) -> tuple[float, float, float]: + product = geometric_product(v, reverse(v)) + scalar = float(product[0]) + residue = product.copy() + residue[0] = 0.0 + residue_norm = float(np.linalg.norm(residue)) + residual = versor_unit_residual(v, allow_negative=True) + return residual, scalar, residue_norm + + +def _assert_manifold_versor(word: str, versor: np.ndarray, *, replacement: bool = False) -> None: + residual, scalar, residue_norm = _versor_diagnostics(versor) + if residual > _MANIFOLD_RESIDUAL_TOLERANCE: + noun = "replacement versor" if replacement else "versor" + action = "Call algebra.versor.unitize_versor() before update()." if replacement else ( + "If lifting from a raw array, call algebra.versor.unitize_versor() first." + ) + raise ValueError( + f"Word '{word}': {noun} residual {residual:.4e} exceeds " + f"{_MANIFOLD_RESIDUAL_TOLERANCE:.1e}; scalar={scalar:.4f}, " + f"non_scalar_residue={residue_norm:.4e}. Pass a clean Cl(4,1) " + f"unit versor satisfying V*reverse(V)≈±1. {action}" + ) + class VocabManifold: def __init__(self): self._words: list[str] = [] - self._versors: list[np.ndarray] = [] # each shape (32,), grade-normed to ±1 + self._versors: list[np.ndarray] = [] # each shape (32,), unit-versor ±1 self._morphology_by_word: dict[str, MorphologyEntry] = {} self._language_by_word: dict[str, str] = {} self._transient_words: set[str] = set() @@ -52,10 +81,10 @@ class VocabManifold: """ 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. + Enforces the Cl(4,1) manifold invariant: V * reverse(V) must be + approximately +1 or -1 as a full multivector residual, not merely + in its scalar component. This rejects raw coordinates, external + embeddings, and dirty construction products. If your source is a raw float array, call algebra.versor.unitize_versor() first — that is the construction-time @@ -63,16 +92,10 @@ class VocabManifold: that function is reserved for the injection gate. Raises: - ValueError: if the grade-norm condition is not satisfied. + ValueError: if the full unit-versor residual 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 algebra.versor.unitize_versor() first." - ) + _assert_manifold_versor(word, v) self._words.append(word) self._versors.append(v) resolved_language = language or (morphology.language if morphology is not None else None) @@ -134,24 +157,19 @@ class VocabManifold: Used by the alignment correction pass after compilation to nudge cross-language aligned pairs toward each other without rebuilding - the full manifold. The grade-norm invariant is enforced identically - to add(). + the full manifold. The full unit-versor residual is enforced + identically to add(). Raises: KeyError: if the word is not already in the manifold. - ValueError: if the grade-norm condition is not satisfied. + ValueError: if the full unit-versor residual is not satisfied. """ try: idx = self._words.index(word) except ValueError: raise KeyError(f"Word '{word}' not in vocabulary; use add() for new entries.") 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}': replacement versor grade-norm {grade_norm:.4f} ≠ ±1. " - "Call algebra.versor.unitize_versor() before update()." - ) + _assert_manifold_versor(word, v, replacement=True) self._versors[idx] = v def get_versor(self, word: str) -> np.ndarray: