Fix fail-closed versor construction

This commit is contained in:
Shay 2026-05-14 12:11:42 -07:00
parent 2e8169bbb0
commit fbbd7c52e3
9 changed files with 51 additions and 34 deletions

View file

@ -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: 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) return np.asarray(_rs.versor_apply(V, F), dtype=np.float32)
from algebra.versor import versor_apply as _va from algebra.versor import versor_apply as _va
return _va(V, F) return _va(V, F)

View file

@ -1,7 +1,7 @@
""" """
Conformal Geometric Algebra geometry on Cl(4,1). 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: The two conformal null directions are built from e4 and e5:
n_o = 0.5 * (e4 - e5) # origin, n_o^2 = 0 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 result[1:4] = x
# n_o + 0.5|x|^2 n_inf # n_o + 0.5|x|^2 n_inf
# e4 coefficient: 0.5 + 0.5|x|^2 # e4 coefficient: -0.5 + 0.5|x|^2
# e5 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[_E4_IDX] = 0.5 * (x_sq - 1.0)
result[_E5_IDX] = 0.5 * (x_sq - 1.0) result[_E5_IDX] = 0.5 * (x_sq + 1.0)
return result return result

View file

@ -17,7 +17,7 @@ import numpy as np
N_DIMS = 5 N_DIMS = 5
N_COMPONENTS = 32 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 --- # --- Grade offset table ---

View file

@ -66,9 +66,7 @@ def holonomy_encode(
if len(weights) != n: if len(weights) != n:
raise ValueError("weights length must match word_versors length.") raise ValueError("weights length must match word_versors length.")
dtype = np.result_type(*word_versors) dtype = np.float64
if dtype not in (np.dtype(np.float32), np.dtype(np.float64)):
dtype = np.dtype(np.float32)
# Forward accumulation. Each token is carried through a deterministic # Forward accumulation. Each token is carried through a deterministic
# position rotor so path order survives even for scalar/vector fixtures. # position rotor so path order survives even for scalar/vector fixtures.

View file

@ -7,9 +7,11 @@ it describes a transformation being applied, not a property of the vocabulary.
""" """
import numpy as np import numpy as np
from .cl41 import geometric_product, reverse from .cl41 import N_COMPONENTS
from .versor import unitize_versor 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: 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 after multiplication by its reverse, or otherwise cannot be
scaled into a clean +1 operator. scaled into a clean +1 operator.
""" """
R = geometric_product(B, reverse(A)) A = np.asarray(A, dtype=np.float64)
R = R.copy() B = np.asarray(B, dtype=np.float64)
R[0] += 1.0 if np.linalg.norm(A + B) < 1e-6:
return unitize_versor(R) 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)

View file

@ -25,12 +25,12 @@ def _diagnostic_message(prefix: str, *, input_norm: float, scalar_sq: float, res
def unitize_versor(v: np.ndarray) -> np.ndarray: def unitize_versor(v: np.ndarray) -> np.ndarray:
dtype = _array_dtype(v) dtype = _array_dtype(v)
v = np.asarray(v, dtype=dtype) v = np.asarray(v, dtype=np.float64)
input_norm = float(np.linalg.norm(v)) input_norm = float(np.linalg.norm(v))
if input_norm < _NEAR_ZERO_TOLERANCE: 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)) 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]) scalar_sq = float(vv[0])
residue = vv.copy() residue = vv.copy()
residue[0] = 0 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: def versor_unit_residual(v: np.ndarray, *, allow_negative: bool = False) -> float:
dtype = _array_dtype(v) v = np.asarray(v, dtype=np.float64)
v = np.asarray(v, dtype=dtype) vv = geometric_product(v, reverse(v)).astype(np.float64)
vv = geometric_product(v, reverse(v)).astype(dtype)
plus = vv.copy() plus = vv.copy()
plus[0] -= 1.0 plus[0] -= 1.0
plus_residual = float(np.linalg.norm(plus)) plus_residual = float(np.linalg.norm(plus))

View file

@ -19,7 +19,7 @@ _EXPECTED_COMPONENTS = 32
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class FieldState: 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 node: int = 0 # current node index in the vocabulary manifold
step: int = 0 # number of propagation steps taken step: int = 0 # number of propagation steps taken
holonomy: np.ndarray | None = None holonomy: np.ndarray | None = None
@ -29,7 +29,10 @@ class FieldState:
# frozen=True prevents reassignment, but ndarray contents are still # frozen=True prevents reassignment, but ndarray contents are still
# mutable via the array object; copy() here is the defence. # mutable via the array object; copy() here is the defence.
# slots=True closes __dict__ so no incidental attributes can be added. # 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,): if F.shape != (_EXPECTED_COMPONENTS,):
raise ValueError( raise ValueError(
f"FieldState.F must have shape ({_EXPECTED_COMPONENTS},), " f"FieldState.F must have shape ({_EXPECTED_COMPONENTS},), "
@ -38,7 +41,10 @@ class FieldState:
# Bypass frozen to store the validated copy. # Bypass frozen to store the validated copy.
object.__setattr__(self, "F", F) object.__setattr__(self, "F", F)
if self.holonomy is not None: 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,): if H.shape != (_EXPECTED_COMPONENTS,):
raise ValueError( raise ValueError(
f"FieldState.holonomy must have shape ({_EXPECTED_COMPONENTS},), " f"FieldState.holonomy must have shape ({_EXPECTED_COMPONENTS},), "

View file

@ -192,6 +192,7 @@ def _ground_unknown_token(token: str, vocab) -> np.ndarray:
root_used, prefixes, suffixes = _best_decomposition(token, vocab, morphology_entries) root_used, prefixes, suffixes = _best_decomposition(token, vocab, morphology_entries)
root_versor = vocab.get_versor(root_used) root_versor = vocab.get_versor(root_used)
versor, operators_applied = _compose_delta(root_versor, prefixes, suffixes) versor, operators_applied = _compose_delta(root_versor, prefixes, suffixes)
versor = normalize_to_versor(versor)
condition = versor_condition(versor) condition = versor_condition(versor)
if condition > 1e-6: if condition > 1e-6:
raise RuntimeError( raise RuntimeError(

View file

@ -24,7 +24,7 @@ if TYPE_CHECKING:
from sensorium.protocol import ModalityVocabulary from sensorium.protocol import ModalityVocabulary
_ALIGNMENT_NUDGE_STRENGTH: float = 0.10 _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 _PRIMARY_SEMANTIC_DOMAIN_WEIGHT: float = 0.55
_LOGOS_PARTICIPATION_WEIGHT: float = 0.25 _LOGOS_PARTICIPATION_WEIGHT: float = 0.25
_FEATURE_COMPONENTS: tuple[int, ...] = (6, 7, 9, 10, 12, 14) _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: def _feature_rotor(name: str, salt: str, weight: float) -> np.ndarray:
idx = _feature_component(name, salt) idx = _feature_component(name, salt)
theta = _feature_sign(name, salt) * weight 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[0] = np.cos(theta)
rotor[idx] = np.sin(theta) rotor[idx] = np.sin(theta)
return rotor 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: def _blend_feature_versors(source: np.ndarray, target: np.ndarray, strength: float) -> np.ndarray:
strength = max(0.0, min(1.0, float(strength))) strength = max(0.0, min(1.0, float(strength)))
nudge = _alignment_nudge_rotor(source, target, strength) if strength <= 0.0:
return _unit_feature_versor(geometric_product(nudge, source)) 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: 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]]: 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: 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]))) scalar = max(-1.0, min(1.0, float(R_full[0])))
theta_full = float(np.arccos(scalar)) theta_full = float(np.arccos(scalar))
if abs(theta_full) < 1e-6: 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 identity[0] = 1.0
return identity return identity
@ -209,14 +212,14 @@ def _alignment_nudge_rotor(source: np.ndarray, target: np.ndarray, strength: flo
biv[0] = 0.0 biv[0] = 0.0
biv_norm = float(np.linalg.norm(biv)) biv_norm = float(np.linalg.norm(biv))
if biv_norm < 1e-6: 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 identity[0] = 1.0
return identity return identity
theta_nudge = theta_full * max(0.0, min(1.0, float(strength))) 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[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 return nudge
@ -427,7 +430,7 @@ def _apply_mounted_primary_domain_resonance(
if surface == prototype_surface: if surface == prototype_surface:
continue continue
source = mounted.get_versor(surface) 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]: def _infer_foreign_pack_ids(home_pack_id: str, graph: "AlignmentGraph") -> list[str]: