Closes W-015 wiring debt. Per Sonnet's investigation (PR #252, verdict (c)): _slerp_toward interpolates on S^31 but the versor manifold (Spin sub-group in Cl(4,1)) is a proper subset. Slerp's geodesic doesn't stay on the manifold, producing systematic off-manifold state that the post-hoc unitize_versor was repairing. Fix replaces _slerp_toward with the proper rotor-geodesic path: R = word_transition_rotor(field_state.F, anchor_field) R_step = rotor_power(R, _ANCHOR_PULL_ALPHA) pulled_F = versor_apply(R_step, field_state.F) rotor_power stays on the manifold by construction (same principle as generate/stream.py:220). versor_apply closes via algebra/ versor.py — an already-sanctioned site. The unsanctioned unitize_versor call in _anchor_pull and the entire _slerp_toward function are removed. CLAUDE.md normalization-site discipline is now restored: session/context.py:_anchor_pull no longer performs normalization. Changes: - session/context.py: import rotor_power + word_transition_rotor, remove _slerp_toward (34 lines), rewrite _anchor_pull to use rotor-geodesic (15 lines net change). - tests/test_session_coherence.py: new test pins the manifold invariant — after anchor pull, versor_condition stays < 1e-6 without any unitize call (32 lines). Intentional lane re-pins (audit-trail per #229 discipline): - demo_composition: 403be13b → 3a3d09f3 (anchor pull now produces correct on-manifold fields; demo output shifts as expected). - public_demo: acd51d0c → 888ddd0d (same cause). CLAIMS.md regenerated to reflect new pins (per #239 lesson). Verification: - tests/test_session_coherence.py: 3 passed - core test --suite smoke: 67 passed - scripts/verify_lane_shas.py: 7/7 match (post-re-pin) - Manifold invariant test pinned: anchor pull preserves versor_condition < 1e-6 by construction (no repair). Investigation source: PR #252 (Sonnet). 4,138-sample bimodal distribution confirmed _slerp_toward as the sole drift source.
140 lines
4.8 KiB
Python
140 lines
4.8 KiB
Python
from __future__ import annotations
|
|
|
|
import numpy as np
|
|
|
|
from algebra.backend import cga_inner
|
|
from algebra.versor import unitize_versor, versor_condition
|
|
from session.context import SessionContext
|
|
from vocab.manifold import VocabManifold
|
|
|
|
|
|
def _positive_unit_reflector(seed: int) -> np.ndarray:
|
|
rng = np.random.default_rng(seed)
|
|
vec4 = rng.standard_normal(4).astype(np.float32)
|
|
norm4 = float(np.linalg.norm(vec4))
|
|
if norm4 < 1e-6:
|
|
vec4[0] = 1.0
|
|
norm4 = 1.0
|
|
|
|
vec = np.zeros(5, dtype=np.float32)
|
|
vec[:4] = vec4
|
|
vec[4] = 0.25 * norm4 * np.tanh(float(rng.standard_normal()))
|
|
|
|
mv = np.zeros(32, dtype=np.float32)
|
|
mv[1:6] = vec
|
|
return unitize_versor(mv)
|
|
|
|
|
|
def _random_rotor(seed: int) -> np.ndarray:
|
|
"""Build a random even-grade versor (rotor) for CGA inner-product comparison.
|
|
|
|
Field states are even-grade (grade 0+2+4). Reflectors are grade-1 and
|
|
orthogonal under cga_inner, so we need rotors to get nonzero scores.
|
|
"""
|
|
a = _positive_unit_reflector(seed)
|
|
b = _positive_unit_reflector(seed + 10000)
|
|
from algebra.cl41 import geometric_product as gp
|
|
rotor = gp(a, b)
|
|
return unitize_versor(rotor)
|
|
|
|
|
|
def _vocab() -> VocabManifold:
|
|
vocab = VocabManifold()
|
|
vocab.add("logos", _positive_unit_reflector(1))
|
|
vocab.add("arche", _positive_unit_reflector(2))
|
|
vocab.add("pneuma", _positive_unit_reflector(3))
|
|
vocab.add("truth", _positive_unit_reflector(4))
|
|
vocab.add("nous", _positive_unit_reflector(5))
|
|
return vocab
|
|
|
|
|
|
def _farther_unrelated(result_F: np.ndarray, prompt_F: np.ndarray, start_seed: int) -> np.ndarray:
|
|
prompt_score = cga_inner(result_F, prompt_F)
|
|
for seed in range(start_seed, start_seed + 2048):
|
|
candidate = _random_rotor(seed)
|
|
if prompt_score > cga_inner(result_F, candidate):
|
|
return candidate
|
|
raise AssertionError("Could not construct a deterministic farther unrelated versor.")
|
|
|
|
|
|
def test_finalize_turn_enforces_cga_hemisphere_consistency() -> None:
|
|
"""Vault entries must share the anchor's CGA hemisphere for recall to rank correctly."""
|
|
from generate.result import GenerationResult
|
|
from field.state import FieldState
|
|
|
|
vocab = _vocab()
|
|
session = SessionContext(vocab=vocab)
|
|
session.ingest(["logos"])
|
|
anchor = session._anchor_field.copy()
|
|
|
|
anti_F = -session.state.F
|
|
anti_state = FieldState(
|
|
F=anti_F,
|
|
node=session.state.node,
|
|
step=session.state.step,
|
|
holonomy=session.state.holonomy,
|
|
energy=session.state.energy,
|
|
valence=session.state.valence,
|
|
)
|
|
anti_result = GenerationResult(tokens=("arche",), final_state=anti_state, vault_hits=0)
|
|
assert cga_inner(anti_F, anchor) < 0.0, "precondition: anti-hemisphere field"
|
|
|
|
session.finalize_turn(anti_result, dialogue_role="assert")
|
|
|
|
assert cga_inner(session.state.F, anchor) >= 0.0, (
|
|
"finalize_turn must flip anti-hemisphere fields to keep vault recall consistent"
|
|
)
|
|
|
|
|
|
def test_repeated_prompt_accumulates_field_and_stays_prompt_coherent() -> None:
|
|
session = SessionContext(vocab=_vocab())
|
|
prompt = ["logos", "arche"]
|
|
|
|
initial = session.ingest(prompt)
|
|
first = session.respond(max_tokens=4)
|
|
|
|
second_prompt_state = session.ingest(prompt)
|
|
assert not np.array_equal(second_prompt_state.F, initial.F)
|
|
|
|
second = session.respond(max_tokens=4)
|
|
|
|
assert second.tokens != first.tokens
|
|
assert not np.array_equal(second.final_state.F, first.final_state.F)
|
|
|
|
for i, result in enumerate((first, second)):
|
|
random_unrelated = _farther_unrelated(result.final_state.F, initial.F, 11 + (i * 64))
|
|
prompt_score = cga_inner(result.final_state.F, initial.F)
|
|
random_score = cga_inner(result.final_state.F, random_unrelated)
|
|
assert prompt_score > random_score
|
|
|
|
|
|
def test_anchor_pull_output_satisfies_versor_condition() -> None:
|
|
"""_anchor_pull must not break the versor condition (W-015 fix contract).
|
|
|
|
The previous _slerp_toward implementation interpolated on S^31 rather than
|
|
the Spin sub-manifold, producing versor_condition values up to 38.58.
|
|
The rotor-geodesic replacement must satisfy vc < 1e-6 by construction.
|
|
"""
|
|
from field.state import FieldState
|
|
|
|
vocab = _vocab()
|
|
session = SessionContext(vocab=vocab)
|
|
session.ingest(["logos"])
|
|
|
|
# Build a drifted field well separated from the anchor.
|
|
drifted = _random_rotor(seed=42)
|
|
drifted_state = FieldState(
|
|
F=drifted,
|
|
node=session.state.node,
|
|
step=session.state.step,
|
|
holonomy=session.state.holonomy,
|
|
energy=session.state.energy,
|
|
valence=session.state.valence,
|
|
)
|
|
|
|
pulled = session._anchor_pull(drifted_state)
|
|
vc = versor_condition(pulled.F)
|
|
assert vc < 1e-6, (
|
|
f"_anchor_pull output violated versor_condition: {vc:.3e} >= 1e-6. "
|
|
"The rotor-geodesic path must stay on the Spin sub-manifold by construction."
|
|
)
|