fix(W-015): replace _slerp_toward with rotor-geodesic anchor pull (#255)
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.
This commit is contained in:
parent
bf3baa8bfa
commit
11c91581e8
6 changed files with 54 additions and 40 deletions
|
|
@ -38,8 +38,8 @@ is a CI failure (`.github/workflows/lane-shas.yml`).
|
|||
| ADR-0093 | `domain_contract_validation` | All ratified packs satisfy the 9 ADR-0091 contract predicates | `evals/domain_contract_validation/results/v1_dev.json` | `98ace04e3f02bbc5a8ad655bb6593c3f1ee64cb67014f1122fe6c3c85f48d22f` |
|
||||
| ADR-0095 | `miner_loop_closure` | Miner-sourced proposals route through single reviewed teaching path | `evals/miner_loop_closure/results/v1_dev.json` | `9f071733abe7dcacf759f928548ce738fb639af3fd6e4c621a651b306d7e77ce` |
|
||||
| ADR-0096 | `fabrication_control_summary` | Phantom endpoints / cross-pack non-bridges / sibling collapses refuse | `evals/fabrication_control/results/v1_summary.json` | `01e1b6b711141f2b4a14551d7df3ea482d8d6dd7b364a25c509f4f8d08cda8a8` |
|
||||
| ADR-0098 | `demo_composition` | Demos compose from shipped modules; no parallel mechanism | `evals/demo_composition/results/v1_dev.json` | `403be13bd666389bdf1caa1b97382753e531ad72686c5237f64a5e23bc702ac4` |
|
||||
| ADR-0099 | `public_demo` | Public showcase runs deterministically under 30s; all claims supported | `evals/public_demo/results/v1_dev.json` | `acd51d0c17948030aa2843309a3729c6f825959e2e0cbbc477e07de8269268a4` |
|
||||
| ADR-0098 | `demo_composition` | Demos compose from shipped modules; no parallel mechanism | `evals/demo_composition/results/v1_dev.json` | `3a3d09f3a87462737e615c2dd3481b9e13e5ff8fadee0043c37873494ded556d` |
|
||||
| ADR-0099 | `public_demo` | Public showcase runs deterministically under 30s; all claims supported | `evals/public_demo/results/v1_dev.json` | `888ddd0d12635d709f91898cf06601062168694d870f776d2b0b7710e5d68cbd` |
|
||||
| ADR-0104 | `curriculum_loop_closure` | Curriculum-sourced proposals route through single reviewed teaching path | `evals/curriculum_loop_closure/results/v1_dev.json` | `b46d56b2d209172cc3ffaf3776dc8dcfe55093f13587c5cb67372be6dfa23e8d` |
|
||||
|
||||
## Verification
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
"details": {
|
||||
"all_claims_supported_a": true,
|
||||
"all_claims_supported_b": true,
|
||||
"sha256": "deb3af8fab8f3ff9cf7468efd7a403b60c825a67b76a5c07ab95147f79bd2e9a"
|
||||
"sha256": "6a02197c0d9fae0eb041aaed9d8c864291477c066ec915ca403cf774c301f7d3"
|
||||
},
|
||||
"divergence": null,
|
||||
"passed": true
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
{
|
||||
"case_id": "determinism_run_to_run_byte_equality",
|
||||
"details": {
|
||||
"sha256": "48fc9cba028d5d8e8e098d71cd25a140861fcf380c1214612358bce433f79996"
|
||||
"sha256": "58e4ddec73aba31b0e193d573921cd85df7fd225ba68716eeef1517b635c6141"
|
||||
},
|
||||
"divergence": null,
|
||||
"passed": true
|
||||
|
|
|
|||
|
|
@ -38,8 +38,8 @@ PINNED_SHAS: dict[str, str] = {
|
|||
"curriculum_loop_closure": "b46d56b2d209172cc3ffaf3776dc8dcfe55093f13587c5cb67372be6dfa23e8d",
|
||||
"domain_contract_validation": "98ace04e3f02bbc5a8ad655bb6593c3f1ee64cb67014f1122fe6c3c85f48d22f",
|
||||
"fabrication_control_summary": "01e1b6b711141f2b4a14551d7df3ea482d8d6dd7b364a25c509f4f8d08cda8a8",
|
||||
"demo_composition": "403be13bd666389bdf1caa1b97382753e531ad72686c5237f64a5e23bc702ac4",
|
||||
"public_demo": "acd51d0c17948030aa2843309a3729c6f825959e2e0cbbc477e07de8269268a4",
|
||||
"demo_composition": "3a3d09f3a87462737e615c2dd3481b9e13e5ff8fadee0043c37873494ded556d",
|
||||
"public_demo": "888ddd0d12635d709f91898cf06601062168694d870f776d2b0b7710e5d68cbd",
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ from __future__ import annotations
|
|||
import numpy as np
|
||||
|
||||
from algebra.backend import cga_inner, versor_apply
|
||||
from algebra.versor import unitize_versor, versor_condition as _versor_condition
|
||||
from algebra.rotor import rotor_power, word_transition_rotor
|
||||
from algebra.versor import versor_condition as _versor_condition
|
||||
from field.state import FieldState
|
||||
from generate.dialogue import DialogueTurn
|
||||
from generate.proposition import Proposition
|
||||
|
|
@ -35,35 +36,6 @@ _BLADE_EMA_ALPHA: float = 0.15
|
|||
_ANCHOR_PULL_ALPHA: float = 0.05
|
||||
|
||||
|
||||
def _slerp_toward(
|
||||
F: np.ndarray,
|
||||
target: np.ndarray,
|
||||
alpha: float,
|
||||
) -> np.ndarray:
|
||||
"""Spherical-linear interpolation of F toward target by fraction alpha.
|
||||
|
||||
When the inner product is near ±1 (nearly parallel/antiparallel versors),
|
||||
falls back to linear interpolation to avoid numerical instability.
|
||||
"""
|
||||
f_norm = float(np.linalg.norm(F))
|
||||
t_norm = float(np.linalg.norm(target))
|
||||
if f_norm < 1e-10 or t_norm < 1e-10:
|
||||
return F
|
||||
f_unit = F / f_norm
|
||||
t_unit = target / t_norm
|
||||
cos_theta = float(np.clip(np.dot(f_unit.ravel(), t_unit.ravel()), -1.0, 1.0))
|
||||
theta = float(np.arccos(abs(cos_theta)))
|
||||
if theta < 1e-6:
|
||||
# Nearly parallel — linear blend is numerically identical
|
||||
result = (1.0 - alpha) * F + alpha * target
|
||||
else:
|
||||
sin_theta = float(np.sin(theta))
|
||||
w_f = float(np.sin((1.0 - alpha) * theta)) / sin_theta
|
||||
w_t = float(np.sin(alpha * theta)) / sin_theta
|
||||
result = w_f * F + w_t * target
|
||||
return np.asarray(result, dtype=F.dtype)
|
||||
|
||||
|
||||
class SessionContext:
|
||||
def __init__(self, vocab, persona=None, vault=None, vault_reproject_interval: int = 100):
|
||||
self.vocab = vocab
|
||||
|
|
@ -221,19 +193,29 @@ class SessionContext:
|
|||
)
|
||||
|
||||
def _anchor_pull(self, field_state: FieldState) -> FieldState:
|
||||
"""Drift fix 3: mild slerp toward the session anchor field.
|
||||
"""Drift fix 3: mild rotor-geodesic pull toward the session anchor field.
|
||||
|
||||
Applied after hemisphere correction. Provides continuous conjugate
|
||||
correction against slow angular drift that stays within the hemisphere
|
||||
but gradually moves away from the session concept attractor.
|
||||
|
||||
Computes the transition rotor R = anchor * reverse(F), scales it to
|
||||
R^α via rotor_power (stays on the versor manifold by construction), and
|
||||
applies it via versor_apply. This replaces the previous _slerp_toward
|
||||
approach, which interpolated on S^31 rather than on the Spin sub-manifold
|
||||
and required a post-hoc unitize_versor to repair the manifold violation.
|
||||
|
||||
α=0.05 is intentionally mild — it corrects accumulated drift over many
|
||||
turns without distorting single-turn response fields.
|
||||
"""
|
||||
if self._anchor_field is None:
|
||||
return field_state
|
||||
pulled_F = _slerp_toward(field_state.F, self._anchor_field, _ANCHOR_PULL_ALPHA)
|
||||
pulled_F = unitize_versor(pulled_F)
|
||||
try:
|
||||
R = word_transition_rotor(field_state.F, self._anchor_field)
|
||||
except ValueError:
|
||||
return field_state
|
||||
R_step = rotor_power(R, _ANCHOR_PULL_ALPHA)
|
||||
pulled_F = versor_apply(R_step, field_state.F)
|
||||
return FieldState(
|
||||
F=pulled_F,
|
||||
node=field_state.node,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||
import numpy as np
|
||||
|
||||
from algebra.backend import cga_inner
|
||||
from algebra.versor import unitize_versor
|
||||
from algebra.versor import unitize_versor, versor_condition
|
||||
from session.context import SessionContext
|
||||
from vocab.manifold import VocabManifold
|
||||
|
||||
|
|
@ -106,3 +106,35 @@ def test_repeated_prompt_accumulates_field_and_stays_prompt_coherent() -> None:
|
|||
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."
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue