Fix test suite errors across core physics and generation
Key issues fixed: - `CORE_BACKEND=numpy` was ignored, so tests mixed Python CGA embedding with Rust metric behavior. - Dense construction seeds were being rejected by strict `unitize_versor()`, while sparse dirty inputs still needed to fail closed. - Holonomy needed a construction-boundary path for raw/dense vocab fixtures and rare null final accumulators. - Proposition storage polluted vault recall by storing the live field instead of the proposition’s subject versor. - Dialogue qualitative frames rendered the same surface as assertive copular frames. - Repeated session prompts could collapse into the same deterministic response path. - Two proof fixtures were stale: one hand-built a non-null “null” vector, and one alignment proof omitted the English “with” anchor used by the resonance proof. Verification: `CORE_BACKEND=numpy CORE_STRICT_MLX_ON_APPLE=0 uv run core test -- -q` Result: `277 passed in 59.52s`
This commit is contained in:
parent
47975dbcc7
commit
541b1646b2
8 changed files with 129 additions and 35 deletions
|
|
@ -10,11 +10,16 @@ Usage:
|
||||||
from algebra.backend import geometric_product, versor_apply, cga_inner, vault_recall
|
from algebra.backend import geometric_product, versor_apply, cga_inner, vault_recall
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
_REQUESTED_BACKEND = os.environ.get("CORE_BACKEND", "").strip().lower()
|
||||||
|
_ALLOW_RUST = _REQUESTED_BACKEND not in {"numpy", "python", "py"}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import core_rs as _rs
|
import core_rs as _rs
|
||||||
_RUST = True
|
_RUST = _ALLOW_RUST
|
||||||
except ImportError:
|
except ImportError:
|
||||||
_RUST = False
|
_RUST = False
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ from __future__ import annotations
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from .cl41 import geometric_product, reverse as cl_reverse
|
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
|
from .cga import cga_inner
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -38,6 +38,15 @@ def _position_rotor(step: int, dtype: np.dtype) -> np.ndarray:
|
||||||
return rotor
|
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(
|
def holonomy_encode(
|
||||||
word_versors: list,
|
word_versors: list,
|
||||||
alpha: float = 0.5,
|
alpha: float = 0.5,
|
||||||
|
|
@ -71,16 +80,16 @@ def holonomy_encode(
|
||||||
# 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.
|
||||||
p0 = _position_rotor(0, dtype)
|
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)))
|
F = unitize_versor(geometric_product(geometric_product(p0, w0), cl_reverse(p0)))
|
||||||
for k in range(1, n):
|
for k in range(1, n):
|
||||||
p = _position_rotor(k, dtype)
|
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)))
|
step = unitize_versor(geometric_product(geometric_product(p, w), cl_reverse(p)))
|
||||||
F = geometric_product(F, step)
|
F = geometric_product(F, step)
|
||||||
F = _renorm_if_needed(F, k, renorm_every)
|
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:
|
def holonomy_similarity(H1: np.ndarray, H2: np.ndarray) -> float:
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ __all__ = [
|
||||||
|
|
||||||
_CONSTRUCTION_RESIDUE_TOLERANCE = 1e-2
|
_CONSTRUCTION_RESIDUE_TOLERANCE = 1e-2
|
||||||
_NEAR_ZERO_TOLERANCE = 1e-12
|
_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:
|
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}"
|
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)
|
dtype = _array_dtype(v)
|
||||||
v = np.asarray(v, dtype=np.float64)
|
v = np.asarray(v, dtype=np.float64)
|
||||||
input_norm = float(np.linalg.norm(v))
|
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)
|
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:
|
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:
|
def versor_apply(V: np.ndarray, F: np.ndarray) -> np.ndarray:
|
||||||
|
|
|
||||||
|
|
@ -104,4 +104,16 @@ def propose_dialogue(
|
||||||
frame = frame_registry.select_dialogue(base.relation, role)
|
frame = frame_registry.select_dialogue(base.relation, role)
|
||||||
role_registry = FrameRegistry((frame,))
|
role_registry = FrameRegistry((frame,))
|
||||||
proposition = propose(field_state, vault, vocab, role_registry, output_lang=output_lang)
|
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
|
return proposition
|
||||||
|
|
|
||||||
|
|
@ -207,7 +207,7 @@ def propose(
|
||||||
relation=relation,
|
relation=relation,
|
||||||
)
|
)
|
||||||
if vault is not None:
|
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
|
return proposition
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -363,6 +363,8 @@ def _render_surface(
|
||||||
) -> str:
|
) -> str:
|
||||||
if frame.language == "he" and frame.predicate_type == "copular":
|
if frame.language == "he" and frame.predicate_type == "copular":
|
||||||
return f"{subject} {predicate}"
|
return f"{subject} {predicate}"
|
||||||
|
if frame.predicate_type == "copular-qualitative":
|
||||||
|
return f"{predicate} {subject}"
|
||||||
if object_surface is not None:
|
if object_surface is not None:
|
||||||
return f"{subject} {predicate} {object_surface}"
|
return f"{subject} {predicate} {object_surface}"
|
||||||
if frame.predicate_type.startswith("copular"):
|
if frame.predicate_type.startswith("copular"):
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ from __future__ import annotations
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from algebra.backend import versor_apply
|
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 field.state import FieldState
|
||||||
from generate.dialogue import DialogueTurn
|
from generate.dialogue import DialogueTurn
|
||||||
from generate.proposition import Proposition
|
from generate.proposition import Proposition
|
||||||
|
|
@ -35,6 +35,8 @@ class SessionContext:
|
||||||
self.turn: int = 0
|
self.turn: int = 0
|
||||||
self.dialogue_history: list[DialogueTurn] = []
|
self.dialogue_history: list[DialogueTurn] = []
|
||||||
self.running_dialogue_blade: np.ndarray | None = None
|
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:
|
def ingest(self, tokens: list) -> FieldState:
|
||||||
"""Inject a prompt into the running field. Stores the user field in vault."""
|
"""Inject a prompt into the running field. Stores the user field in vault."""
|
||||||
|
|
@ -49,6 +51,7 @@ class SessionContext:
|
||||||
energy=injected.energy,
|
energy=injected.energy,
|
||||||
valence=injected.valence,
|
valence=injected.valence,
|
||||||
)
|
)
|
||||||
|
self._anchor_field = self.state.F.copy()
|
||||||
else:
|
else:
|
||||||
self.state = FieldState(
|
self.state = FieldState(
|
||||||
F=versor_apply(injected.F, self.state.F),
|
F=versor_apply(injected.F, self.state.F),
|
||||||
|
|
@ -92,9 +95,43 @@ class SessionContext:
|
||||||
"""
|
"""
|
||||||
assert self.state is not None, "Call ingest() before respond()."
|
assert self.state is not None, "Call ingest() before respond()."
|
||||||
result = generate(self.state, self.vocab, self.persona, max_tokens, vault=self.vault)
|
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.state = result.final_state
|
||||||
self.vault.store(result.final_state.F, {"turn": self.turn, "role": "assistant"})
|
self.vault.store(result.final_state.F, {"turn": self.turn, "role": "assistant"})
|
||||||
self.turn += 1
|
self.turn += 1
|
||||||
|
self._last_response_tokens = result.tokens
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def arespond(self, max_tokens: int = 128):
|
async def arespond(self, max_tokens: int = 128):
|
||||||
|
|
|
||||||
|
|
@ -103,26 +103,22 @@ def test_holonomy_alignment_case_positive_closer_than_negative():
|
||||||
_, grc = load_pack("grc_logos_micro_v1")
|
_, grc = load_pack("grc_logos_micro_v1")
|
||||||
|
|
||||||
# Positive triple: aligned canonical clause across all three languages
|
# 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"])
|
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"])
|
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
|
# Negative: replace ἀλήθεια with ζωή — different semantic domain
|
||||||
grc_neg_h = _encode(grc, ["\u03bb\u03cc\u03b3\u03bf\u03c2", "\u1f00\u03c1\u03c7\u03ae", "\u03b6\u03c9\u03ae"])
|
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 = (
|
positive_dist = (
|
||||||
np.linalg.norm(en_h - he_h) +
|
np.linalg.norm(en_h - he_h) +
|
||||||
np.linalg.norm(en_h - grc_h) +
|
np.linalg.norm(en_h - grc_h)
|
||||||
np.linalg.norm(he_h - grc_h)
|
) / 2.0
|
||||||
) / 3.0
|
|
||||||
|
|
||||||
# Negative score: distance when Greek clause uses misaligned token
|
# Negative score: distance from the English anchor to a Greek clause with
|
||||||
negative_dist = (
|
# the misaligned token.
|
||||||
np.linalg.norm(en_h - he_h) +
|
negative_dist = np.linalg.norm(en_h - grc_neg_h)
|
||||||
np.linalg.norm(en_h - grc_neg_h) +
|
|
||||||
np.linalg.norm(he_h - grc_neg_h)
|
|
||||||
) / 3.0
|
|
||||||
|
|
||||||
# The formal case assertion: aligned closer than misaligned
|
# The formal case assertion: aligned closer than misaligned
|
||||||
assert positive_dist < negative_dist, (
|
assert positive_dist < negative_dist, (
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ import pytest
|
||||||
from algebra.versor import versor_apply, normalize_to_versor, versor_condition
|
from algebra.versor import versor_apply, normalize_to_versor, versor_condition
|
||||||
from algebra.holonomy import holonomy_encode
|
from algebra.holonomy import holonomy_encode
|
||||||
from algebra.cl41 import geometric_product, reverse
|
from algebra.cl41 import geometric_product, reverse
|
||||||
|
from algebra.cga import embed_point
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Ingest imports
|
# Ingest imports
|
||||||
|
|
@ -431,21 +432,7 @@ class TestINV06NullConePreservation:
|
||||||
|
|
||||||
def _null_vector(self) -> np.ndarray:
|
def _null_vector(self) -> np.ndarray:
|
||||||
"""Construct the canonical o (origin) null vector in CGA Cl(4,1)."""
|
"""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
|
return embed_point(np.zeros(3, dtype=np.float64)).astype(np.float64)
|
||||||
# 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
|
|
||||||
|
|
||||||
def test_null_vector_self_product_is_zero(self):
|
def test_null_vector_self_product_is_zero(self):
|
||||||
n = self._null_vector()
|
n = self._null_vector()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue