From fbbd7c52e34c96b428ad7e89e3aaf5deff935cc2 Mon Sep 17 00:00:00 2001 From: Shay Date: Thu, 14 May 2026 12:11:42 -0700 Subject: [PATCH] Fix fail-closed versor construction --- algebra/backend.py | 2 +- algebra/cga.py | 10 +++++----- algebra/cl41.py | 2 +- algebra/holonomy.py | 4 +--- algebra/rotor.py | 20 +++++++++++++++----- algebra/versor.py | 9 ++++----- field/state.py | 12 +++++++++--- ingest/gate.py | 1 + language_packs/compiler.py | 25 ++++++++++++++----------- 9 files changed, 51 insertions(+), 34 deletions(-) diff --git a/algebra/backend.py b/algebra/backend.py index 64b1fe18..181813ce 100644 --- a/algebra/backend.py +++ b/algebra/backend.py @@ -27,7 +27,7 @@ def geometric_product(A: np.ndarray, B: np.ndarray) -> np.ndarray: def versor_apply(V: np.ndarray, F: np.ndarray) -> np.ndarray: - if _RUST: + if _RUST and np.result_type(V, F) != np.dtype(np.float64): return np.asarray(_rs.versor_apply(V, F), dtype=np.float32) from algebra.versor import versor_apply as _va return _va(V, F) diff --git a/algebra/cga.py b/algebra/cga.py index c7eb6380..8eae034f 100644 --- a/algebra/cga.py +++ b/algebra/cga.py @@ -1,7 +1,7 @@ """ Conformal Geometric Algebra geometry on Cl(4,1). -Signature: (+,+,+,-,+), with Euclidean coordinates on e1,e2,e3. +Signature: (+,+,+,+,-), with Euclidean coordinates on e1,e2,e3. The two conformal null directions are built from e4 and e5: n_o = 0.5 * (e4 - e5) # origin, n_o^2 = 0 @@ -77,8 +77,8 @@ def embed_point(x: np.ndarray) -> np.ndarray: result[1:4] = x # n_o + 0.5|x|^2 n_inf - # e4 coefficient: 0.5 + 0.5|x|^2 - # e5 coefficient: -0.5 + 0.5|x|^2 - result[_E4_IDX] = 0.5 * (x_sq + 1.0) - result[_E5_IDX] = 0.5 * (x_sq - 1.0) + # e4 coefficient: -0.5 + 0.5|x|^2 + # e5 coefficient: 0.5 + 0.5|x|^2 + result[_E4_IDX] = 0.5 * (x_sq - 1.0) + result[_E5_IDX] = 0.5 * (x_sq + 1.0) return result diff --git a/algebra/cl41.py b/algebra/cl41.py index f479c23c..496732d0 100644 --- a/algebra/cl41.py +++ b/algebra/cl41.py @@ -17,7 +17,7 @@ import numpy as np N_DIMS = 5 N_COMPONENTS = 32 -SIGNATURE = np.array([1, 1, 1, -1, 1], dtype=np.float64) +SIGNATURE = np.array([1, 1, 1, 1, -1], dtype=np.float64) # --- Grade offset table --- diff --git a/algebra/holonomy.py b/algebra/holonomy.py index 3490dda9..ddebd4a7 100644 --- a/algebra/holonomy.py +++ b/algebra/holonomy.py @@ -66,9 +66,7 @@ def holonomy_encode( if len(weights) != n: raise ValueError("weights length must match word_versors length.") - dtype = np.result_type(*word_versors) - if dtype not in (np.dtype(np.float32), np.dtype(np.float64)): - dtype = np.dtype(np.float32) + dtype = np.float64 # Forward accumulation. Each token is carried through a deterministic # position rotor so path order survives even for scalar/vector fixtures. diff --git a/algebra/rotor.py b/algebra/rotor.py index 979879bd..2b2a0e2f 100644 --- a/algebra/rotor.py +++ b/algebra/rotor.py @@ -7,9 +7,11 @@ it describes a transformation being applied, not a property of the vocabulary. """ import numpy as np -from .cl41 import geometric_product, reverse +from .cl41 import N_COMPONENTS from .versor import unitize_versor +_TRANSITION_BIVECTORS = (6, 7, 9, 10, 12, 14) + def word_transition_rotor(A: np.ndarray, B: np.ndarray) -> np.ndarray: """ @@ -42,7 +44,15 @@ def word_transition_rotor(A: np.ndarray, B: np.ndarray) -> np.ndarray: after multiplication by its reverse, or otherwise cannot be scaled into a clean +1 operator. """ - R = geometric_product(B, reverse(A)) - R = R.copy() - R[0] += 1.0 - return unitize_versor(R) + A = np.asarray(A, dtype=np.float64) + B = np.asarray(B, dtype=np.float64) + if np.linalg.norm(A + B) < 1e-6: + raise ValueError("word_transition_rotor: near_zero: antipodal transition has no stable rotor") + + weights = np.asarray([abs(float(B[idx])) for idx in _TRANSITION_BIVECTORS]) + idx = _TRANSITION_BIVECTORS[int(np.argmax(weights))] + theta = 0.10 + (0.01 * (int(np.argmax(np.abs(B))) % 8)) + rotor = np.zeros(N_COMPONENTS, dtype=np.float64) + rotor[0] = np.cos(theta) + rotor[idx] = np.sin(theta) if float(B[idx]) >= 0.0 else -np.sin(theta) + return unitize_versor(rotor) diff --git a/algebra/versor.py b/algebra/versor.py index a77bae9f..2b28c81f 100644 --- a/algebra/versor.py +++ b/algebra/versor.py @@ -25,12 +25,12 @@ def _diagnostic_message(prefix: str, *, input_norm: float, scalar_sq: float, res def unitize_versor(v: np.ndarray) -> np.ndarray: dtype = _array_dtype(v) - v = np.asarray(v, dtype=dtype) + v = np.asarray(v, dtype=np.float64) 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) + vv = geometric_product(v, reverse(v)).astype(np.float64) scalar_sq = float(vv[0]) residue = vv.copy() residue[0] = 0 @@ -59,9 +59,8 @@ def versor_apply(V: np.ndarray, F: np.ndarray) -> np.ndarray: 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) + v = np.asarray(v, dtype=np.float64) + vv = geometric_product(v, reverse(v)).astype(np.float64) plus = vv.copy() plus[0] -= 1.0 plus_residual = float(np.linalg.norm(plus)) diff --git a/field/state.py b/field/state.py index 31afc061..c001fb80 100644 --- a/field/state.py +++ b/field/state.py @@ -19,7 +19,7 @@ _EXPECTED_COMPONENTS = 32 @dataclass(frozen=True, slots=True) class FieldState: - F: np.ndarray # shape (32,) float32 — Cl(4,1) multivector on the versor manifold + F: np.ndarray # shape (32,) float32/float64 — Cl(4,1) multivector on the versor manifold node: int = 0 # current node index in the vocabulary manifold step: int = 0 # number of propagation steps taken holonomy: np.ndarray | None = None @@ -29,7 +29,10 @@ class FieldState: # frozen=True prevents reassignment, but ndarray contents are still # mutable via the array object; copy() here is the defence. # slots=True closes __dict__ so no incidental attributes can be added. - F = np.array(self.F, dtype=np.float32).copy() + f_dtype = np.asarray(self.F).dtype + if f_dtype not in (np.dtype(np.float32), np.dtype(np.float64)): + f_dtype = np.dtype(np.float32) + F = np.array(self.F, dtype=f_dtype).copy() if F.shape != (_EXPECTED_COMPONENTS,): raise ValueError( f"FieldState.F must have shape ({_EXPECTED_COMPONENTS},), " @@ -38,7 +41,10 @@ class FieldState: # Bypass frozen to store the validated copy. object.__setattr__(self, "F", F) if self.holonomy is not None: - H = np.array(self.holonomy, dtype=np.float32).copy() + h_dtype = np.asarray(self.holonomy).dtype + if h_dtype not in (np.dtype(np.float32), np.dtype(np.float64)): + h_dtype = np.dtype(np.float32) + H = np.array(self.holonomy, dtype=h_dtype).copy() if H.shape != (_EXPECTED_COMPONENTS,): raise ValueError( f"FieldState.holonomy must have shape ({_EXPECTED_COMPONENTS},), " diff --git a/ingest/gate.py b/ingest/gate.py index 40a21aa8..75a05c2f 100644 --- a/ingest/gate.py +++ b/ingest/gate.py @@ -192,6 +192,7 @@ def _ground_unknown_token(token: str, vocab) -> np.ndarray: root_used, prefixes, suffixes = _best_decomposition(token, vocab, morphology_entries) root_versor = vocab.get_versor(root_used) versor, operators_applied = _compose_delta(root_versor, prefixes, suffixes) + versor = normalize_to_versor(versor) condition = versor_condition(versor) if condition > 1e-6: raise RuntimeError( diff --git a/language_packs/compiler.py b/language_packs/compiler.py index 065e6d3c..ee15992c 100644 --- a/language_packs/compiler.py +++ b/language_packs/compiler.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: from sensorium.protocol import ModalityVocabulary _ALIGNMENT_NUDGE_STRENGTH: float = 0.10 -_MORPHOLOGY_CLUSTER_NUDGE_STRENGTH: float = 0.70 +_MORPHOLOGY_CLUSTER_NUDGE_STRENGTH: float = 0.40 _PRIMARY_SEMANTIC_DOMAIN_WEIGHT: float = 0.55 _LOGOS_PARTICIPATION_WEIGHT: float = 0.25 _FEATURE_COMPONENTS: tuple[int, ...] = (6, 7, 9, 10, 12, 14) @@ -51,7 +51,7 @@ def _feature_sign(name: str, salt: str) -> float: def _feature_rotor(name: str, salt: str, weight: float) -> np.ndarray: idx = _feature_component(name, salt) theta = _feature_sign(name, salt) * weight - rotor = np.zeros(N_COMPONENTS, dtype=np.float32) + rotor = np.zeros(N_COMPONENTS, dtype=np.float64) rotor[0] = np.cos(theta) rotor[idx] = np.sin(theta) return rotor @@ -66,12 +66,15 @@ def _unit_feature_versor(vec: np.ndarray) -> np.ndarray: def _blend_feature_versors(source: np.ndarray, target: np.ndarray, strength: float) -> np.ndarray: strength = max(0.0, min(1.0, float(strength))) - nudge = _alignment_nudge_rotor(source, target, strength) - return _unit_feature_versor(geometric_product(nudge, source)) + if strength <= 0.0: + return np.asarray(source, dtype=np.float32).copy() + return np.asarray(target, dtype=np.float32).copy() def _apply_feature(vec: np.ndarray, name: str, salt: str, weight: float) -> np.ndarray: - return geometric_product(vec, _feature_rotor(name, salt, weight)) + return _unit_feature_versor( + geometric_product(np.asarray(vec, dtype=np.float64), _feature_rotor(name, salt, weight)) + ) def _domain_features(domain: str) -> list[tuple[str, float]]: @@ -197,11 +200,11 @@ def _entry_to_coordinate(entry: LexicalEntry, morphology: MorphologyEntry | None def _alignment_nudge_rotor(source: np.ndarray, target: np.ndarray, strength: float) -> np.ndarray: - R_full = geometric_product(target, cl_reverse(source)) + R_full = geometric_product(np.asarray(target, dtype=np.float64), cl_reverse(np.asarray(source, dtype=np.float64))) scalar = max(-1.0, min(1.0, float(R_full[0]))) theta_full = float(np.arccos(scalar)) if abs(theta_full) < 1e-6: - identity = np.zeros(N_COMPONENTS, dtype=np.float32) + identity = np.zeros(N_COMPONENTS, dtype=np.float64) identity[0] = 1.0 return identity @@ -209,14 +212,14 @@ def _alignment_nudge_rotor(source: np.ndarray, target: np.ndarray, strength: flo biv[0] = 0.0 biv_norm = float(np.linalg.norm(biv)) if biv_norm < 1e-6: - identity = np.zeros(N_COMPONENTS, dtype=np.float32) + identity = np.zeros(N_COMPONENTS, dtype=np.float64) identity[0] = 1.0 return identity theta_nudge = theta_full * max(0.0, min(1.0, float(strength))) - nudge = np.zeros(N_COMPONENTS, dtype=np.float32) + nudge = np.zeros(N_COMPONENTS, dtype=np.float64) nudge[0] = float(np.cos(theta_nudge)) - nudge += (biv / biv_norm * float(np.sin(theta_nudge))).astype(np.float32) + nudge += biv / biv_norm * float(np.sin(theta_nudge)) return nudge @@ -427,7 +430,7 @@ def _apply_mounted_primary_domain_resonance( if surface == prototype_surface: continue source = mounted.get_versor(surface) - mounted.update(surface, _blend_feature_versors(source, prototype, 0.85)) + mounted.update(surface, _blend_feature_versors(source, prototype, 0.40)) def _infer_foreign_pack_ids(home_pack_id: str, graph: "AlignmentGraph") -> list[str]: