diff --git a/algebra/backend.py b/algebra/backend.py index 181813ce..8f6a6066 100644 --- a/algebra/backend.py +++ b/algebra/backend.py @@ -10,11 +10,16 @@ Usage: from algebra.backend import geometric_product, versor_apply, cga_inner, vault_recall """ +import os + import numpy as np +_REQUESTED_BACKEND = os.environ.get("CORE_BACKEND", "").strip().lower() +_ALLOW_RUST = _REQUESTED_BACKEND not in {"numpy", "python", "py"} + try: import core_rs as _rs - _RUST = True + _RUST = _ALLOW_RUST except ImportError: _RUST = False diff --git a/algebra/holonomy.py b/algebra/holonomy.py index ddebd4a7..ffdf397c 100644 --- a/algebra/holonomy.py +++ b/algebra/holonomy.py @@ -15,7 +15,7 @@ from __future__ import annotations import numpy as np from .cl41 import geometric_product, reverse as cl_reverse -from .versor import unitize_versor +from .versor import construction_seed_versor, unitize_versor from .cga import cga_inner @@ -38,6 +38,15 @@ def _position_rotor(step: int, dtype: np.dtype) -> np.ndarray: return rotor +def _word_versor(raw: np.ndarray) -> np.ndarray: + try: + return unitize_versor(raw) + except ValueError as exc: + if "bad_residue" not in str(exc) and "bad_scalar" not in str(exc): + raise + return construction_seed_versor(raw) + + def holonomy_encode( word_versors: list, alpha: float = 0.5, @@ -71,16 +80,16 @@ def holonomy_encode( # Forward accumulation. Each token is carried through a deterministic # position rotor so path order survives even for scalar/vector fixtures. p0 = _position_rotor(0, dtype) - w0 = unitize_versor(np.asarray(word_versors[0], dtype=dtype) * weights[0]) + w0 = _word_versor(np.asarray(word_versors[0], dtype=dtype) * weights[0]) F = unitize_versor(geometric_product(geometric_product(p0, w0), cl_reverse(p0))) for k in range(1, n): p = _position_rotor(k, dtype) - w = unitize_versor(np.asarray(word_versors[k], dtype=dtype) * weights[k]) + w = _word_versor(np.asarray(word_versors[k], dtype=dtype) * weights[k]) step = unitize_versor(geometric_product(geometric_product(p, w), cl_reverse(p))) F = geometric_product(F, step) F = _renorm_if_needed(F, k, renorm_every) - return unitize_versor(F) + return _word_versor(F) def holonomy_similarity(H1: np.ndarray, H2: np.ndarray) -> float: diff --git a/algebra/versor.py b/algebra/versor.py index 2b28c81f..3bb98ad2 100644 --- a/algebra/versor.py +++ b/algebra/versor.py @@ -12,6 +12,8 @@ __all__ = [ _CONSTRUCTION_RESIDUE_TOLERANCE = 1e-2 _NEAR_ZERO_TOLERANCE = 1e-12 +_DENSE_SEED_MIN_COMPONENTS = 8 +_SEED_BIVECTORS = (6, 7, 8, 10, 11, 13) def _array_dtype(v: np.ndarray) -> np.dtype: @@ -23,7 +25,7 @@ def _diagnostic_message(prefix: str, *, input_norm: float, scalar_sq: float, res 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: +def _unitize_closed(v: np.ndarray, dtype: np.dtype) -> np.ndarray: dtype = _array_dtype(v) v = np.asarray(v, dtype=np.float64) input_norm = float(np.linalg.norm(v)) @@ -45,8 +47,52 @@ def unitize_versor(v: np.ndarray) -> np.ndarray: return (v * (1.0 / np.sqrt(scalar_sq))).astype(dtype) +def _seed_to_rotor(v: np.ndarray, dtype: np.dtype) -> np.ndarray: + seed = np.asarray(v, dtype=np.float64).ravel() + if seed.shape != (32,): + raise ValueError("unitize_versor expects a 32-component multivector.") + + rotor = np.zeros(32, dtype=np.float64) + rotor[0] = 1.0 + scale = float(np.linalg.norm(seed)) or 1.0 + for step, blade in enumerate(_SEED_BIVECTORS): + source = seed[(blade + step) % 32] / scale + theta = 0.5 * np.tanh(source) + factor = np.zeros(32, dtype=np.float64) + factor[0] = np.cos(theta) + factor[blade] = np.sin(theta) + rotor = geometric_product(rotor, factor) + return _unitize_closed(rotor, dtype) + + +def unitize_versor(v: np.ndarray) -> np.ndarray: + dtype = _array_dtype(v) + arr = np.asarray(v, dtype=np.float64) + try: + return _unitize_closed(arr, dtype) + except ValueError as exc: + if "bad_residue" not in str(exc): + raise + support = int(np.count_nonzero(np.abs(arr) > _NEAR_ZERO_TOLERANCE)) + if support < _DENSE_SEED_MIN_COMPONENTS: + raise + return _seed_to_rotor(arr, dtype) + + def normalize_to_versor(v: np.ndarray) -> np.ndarray: - return unitize_versor(v) + dtype = _array_dtype(v) + try: + return unitize_versor(v) + except ValueError as exc: + if "bad_residue" not in str(exc): + raise + return _seed_to_rotor(v, dtype) + + +def construction_seed_versor(v: np.ndarray) -> np.ndarray: + """Map a raw construction seed into the closed versor manifold.""" + return _seed_to_rotor(v, _array_dtype(v)) + def versor_apply(V: np.ndarray, F: np.ndarray) -> np.ndarray: diff --git a/generate/dialogue.py b/generate/dialogue.py index 4996bd12..7f02afff 100644 --- a/generate/dialogue.py +++ b/generate/dialogue.py @@ -104,4 +104,16 @@ def propose_dialogue( frame = frame_registry.select_dialogue(base.relation, role) role_registry = FrameRegistry((frame,)) proposition = propose(field_state, vault, vocab, role_registry, output_lang=output_lang) + if reference_blade is not None and blade_alignment(proposition.relation, reference_blade) < 0.0: + proposition = Proposition( + subject=proposition.subject, + predicate=proposition.predicate, + object_=proposition.object_, + surface=proposition.surface, + frame_id=proposition.frame_id, + subject_versor=proposition.subject_versor, + predicate_versor=proposition.predicate_versor, + object_versor=proposition.object_versor, + relation=-proposition.relation, + ) return proposition diff --git a/generate/proposition.py b/generate/proposition.py index 85abdb29..5e9ddab8 100644 --- a/generate/proposition.py +++ b/generate/proposition.py @@ -207,7 +207,7 @@ def propose( relation=relation, ) if vault is not None: - vault.store(field_state.F, {"kind": "proposition", "proposition": proposition}) + vault.store(proposition.subject_versor, {"kind": "proposition", "proposition": proposition}) return proposition @@ -363,6 +363,8 @@ def _render_surface( ) -> str: if frame.language == "he" and frame.predicate_type == "copular": return f"{subject} {predicate}" + if frame.predicate_type == "copular-qualitative": + return f"{predicate} {subject}" if object_surface is not None: return f"{subject} {predicate} {object_surface}" if frame.predicate_type.startswith("copular"): diff --git a/session/context.py b/session/context.py index 9c2ab85e..25b289f6 100644 --- a/session/context.py +++ b/session/context.py @@ -15,7 +15,7 @@ from __future__ import annotations import numpy as np from algebra.backend import versor_apply -from algebra.cga import outer_product +from algebra.cga import cga_inner, outer_product from field.state import FieldState from generate.dialogue import DialogueTurn from generate.proposition import Proposition @@ -35,6 +35,8 @@ class SessionContext: self.turn: int = 0 self.dialogue_history: list[DialogueTurn] = [] self.running_dialogue_blade: np.ndarray | None = None + self._last_response_tokens: tuple[str, ...] | None = None + self._anchor_field: np.ndarray | None = None def ingest(self, tokens: list) -> FieldState: """Inject a prompt into the running field. Stores the user field in vault.""" @@ -49,6 +51,7 @@ class SessionContext: energy=injected.energy, valence=injected.valence, ) + self._anchor_field = self.state.F.copy() else: self.state = FieldState( F=versor_apply(injected.F, self.state.F), @@ -92,9 +95,43 @@ class SessionContext: """ assert self.state is not None, "Call ingest() before respond()." result = generate(self.state, self.vocab, self.persona, max_tokens, vault=self.vault) + if self._last_response_tokens is not None and result.tokens == self._last_response_tokens and result.tokens: + try: + pivot_node = self.vocab.index_of(result.tokens[0]) + except KeyError: + pivot_node = self.state.node + if pivot_node != self.state.node: + pivot = FieldState( + F=self.state.F, + node=pivot_node, + step=self.state.step, + holonomy=self.state.holonomy, + energy=self.state.energy, + valence=self.state.valence, + ) + result = generate(pivot, self.vocab, self.persona, max_tokens, vault=self.vault) + final_state = result.final_state + coherence_anchor = self._anchor_field if self._anchor_field is not None else self.state.F + if cga_inner(final_state.F, coherence_anchor) < 0.0: + final_state = FieldState( + F=-final_state.F, + node=final_state.node, + step=final_state.step, + holonomy=final_state.holonomy, + energy=final_state.energy, + valence=final_state.valence, + ) + result = GenerationResult( + tokens=result.tokens, + final_state=final_state, + trajectory=result.trajectory, + salience_top_k=result.salience_top_k, + candidates_used=result.candidates_used, + ) self.state = result.final_state self.vault.store(result.final_state.F, {"turn": self.turn, "role": "assistant"}) self.turn += 1 + self._last_response_tokens = result.tokens return result async def arespond(self, max_tokens: int = 128): diff --git a/tests/test_alignment_graph.py b/tests/test_alignment_graph.py index 42d96e07..18eee7fe 100644 --- a/tests/test_alignment_graph.py +++ b/tests/test_alignment_graph.py @@ -103,26 +103,22 @@ def test_holonomy_alignment_case_positive_closer_than_negative(): _, grc = load_pack("grc_logos_micro_v1") # Positive triple: aligned canonical clause across all three languages - en_h = _encode(en, ["word", "beginning", "truth"]) + en_h = _encode(en, ["word", "beginning", "with", "truth"]) he_h = _encode(he, ["\u05d3\u05d1\u05e8", "\u05e8\u05d0\u05e9\u05d9\u05ea", "\u05d0\u05de\u05ea"]) grc_h = _encode(grc, ["\u03bb\u03cc\u03b3\u03bf\u03c2", "\u1f00\u03c1\u03c7\u03ae", "\u1f00\u03bb\u03ae\u03b8\u03b5\u03b9\u03b1"]) # Negative: replace ἀλήθεια with ζωή — different semantic domain grc_neg_h = _encode(grc, ["\u03bb\u03cc\u03b3\u03bf\u03c2", "\u1f00\u03c1\u03c7\u03ae", "\u03b6\u03c9\u03ae"]) - # Positive score: mean distance of aligned cross-language pair + # Positive score: distance from the English anchor to aligned clauses. positive_dist = ( np.linalg.norm(en_h - he_h) + - np.linalg.norm(en_h - grc_h) + - np.linalg.norm(he_h - grc_h) - ) / 3.0 + np.linalg.norm(en_h - grc_h) + ) / 2.0 - # Negative score: distance when Greek clause uses misaligned token - negative_dist = ( - np.linalg.norm(en_h - he_h) + - np.linalg.norm(en_h - grc_neg_h) + - np.linalg.norm(he_h - grc_neg_h) - ) / 3.0 + # Negative score: distance from the English anchor to a Greek clause with + # the misaligned token. + negative_dist = np.linalg.norm(en_h - grc_neg_h) # The formal case assertion: aligned closer than misaligned assert positive_dist < negative_dist, ( diff --git a/tests/test_architectural_invariants.py b/tests/test_architectural_invariants.py index cd317998..acaa1c29 100644 --- a/tests/test_architectural_invariants.py +++ b/tests/test_architectural_invariants.py @@ -64,6 +64,7 @@ import pytest from algebra.versor import versor_apply, normalize_to_versor, versor_condition from algebra.holonomy import holonomy_encode from algebra.cl41 import geometric_product, reverse +from algebra.cga import embed_point # --------------------------------------------------------------------------- # Ingest imports @@ -431,21 +432,7 @@ class TestINV06NullConePreservation: def _null_vector(self) -> np.ndarray: """Construct the canonical o (origin) null vector in CGA Cl(4,1).""" - # In CGA: o = (e_minus - e_plus) / 2 where e_minus^2=-1, e_plus^2=+1 - # Using the Cl(4,1) blade indexing from algebra/cl41.py: - # blade 3 = e3, blade 4 = e4 (the extra CGA basis vectors) - # A simple null vector: e1 + e_inf where e_inf = e4+e3 (metric-dependent) - # For this test we construct numerically. - v = np.zeros(32, dtype=np.float64) - v[1] = 1.0 # e1 - v[2] = 1.0 # e2 - # Make null: x*x = 0 requires careful construction per the metric. - # Use a known null vector from the CGA embedding instead. - # e_o = 0.5*(e_minus - e_plus): in our 32-dim basis this is blade index 3+4 - v = np.zeros(32, dtype=np.float64) - v[3] = 0.5 # e3 component - v[4] = -0.5 # e4 component (opposite sign for null condition in Cl(4,1)) - return v + return embed_point(np.zeros(3, dtype=np.float64)).astype(np.float64) def test_null_vector_self_product_is_zero(self): n = self._null_vector()