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:
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)

View file

@ -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

View file

@ -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 ---

View file

@ -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.

View file

@ -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)

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:
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))

View file

@ -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},), "

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_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(

View file

@ -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]: