fix: harden session field invariants and eliminate hot-path inefficiencies
- Fix running_dialogue_blade grade explosion: replace outer_product accumulation (which pushed past grade-5 in Cl(4,1), silently zeroing the blade from turn 3 onward) with CGA-inner-oriented blade tracking that preserves grade-2 across arbitrary turn counts. - Add versor_condition guard at session composition boundary: cross-turn field composition via versor_apply now fails closed (threshold 1e-2, matching algebra construction residue tolerance) instead of silently propagating degraded fields into vault and generation. - Replace VaultStore list with deque(maxlen=max_entries): eliminates O(N) list.pop(0) on every bounded eviction; deque auto-evicts in O(1). - Replace O(N) vocab scan in generate/stream.py stop_nodes construction with O(1) try/except index lookup per stop token. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
be6b278dc4
commit
61c55e457d
4 changed files with 66 additions and 39 deletions
|
|
@ -26,6 +26,13 @@ _RECENT_WINDOW = 3
|
|||
_STOP_TOKENS = frozenset({"it", "to", "word"})
|
||||
|
||||
|
||||
def _try_index(vocab, token: str) -> int | None:
|
||||
try:
|
||||
return vocab.index_of(token)
|
||||
except (KeyError, IndexError):
|
||||
return None
|
||||
|
||||
|
||||
def _articulate(vocab, word: str) -> str:
|
||||
morphology_for_word = getattr(vocab, "morphology_for_word", None)
|
||||
if morphology_for_word is None:
|
||||
|
|
@ -207,9 +214,8 @@ def generate(
|
|||
candidates_used = None if candidate_indices is None else len(candidate_indices)
|
||||
|
||||
stop_nodes = frozenset(
|
||||
vocab.index_of(token)
|
||||
for token in _STOP_TOKENS
|
||||
if token in {vocab.get_word_at(i) for i in range(len(vocab))}
|
||||
idx for token in _STOP_TOKENS
|
||||
if (idx := _try_index(vocab, token)) is not None
|
||||
)
|
||||
|
||||
token_budget = min(max_tokens, int(candidates_used)) if candidates_used is not None else max_tokens
|
||||
|
|
@ -265,9 +271,8 @@ async def agenerate(
|
|||
current = state
|
||||
recent_nodes = deque([state.node], maxlen=_RECENT_WINDOW)
|
||||
stop_nodes = frozenset(
|
||||
vocab.index_of(token)
|
||||
for token in _STOP_TOKENS
|
||||
if token in {vocab.get_word_at(i) for i in range(len(vocab))}
|
||||
idx for token in _STOP_TOKENS
|
||||
if (idx := _try_index(vocab, token)) is not None
|
||||
)
|
||||
for _ in range(max_tokens):
|
||||
current, _hits_applied = _recall_state(
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from __future__ import annotations
|
|||
import numpy as np
|
||||
|
||||
from algebra.backend import cga_inner, versor_apply
|
||||
from algebra.cga import outer_product
|
||||
from algebra.versor import versor_condition as _versor_condition
|
||||
from field.state import FieldState
|
||||
from generate.dialogue import DialogueTurn
|
||||
from generate.proposition import Proposition
|
||||
|
|
@ -72,8 +72,14 @@ class SessionContext:
|
|||
valence=injected.valence,
|
||||
)
|
||||
else:
|
||||
composed_F = versor_apply(injected.F, self.state.F)
|
||||
condition = _versor_condition(composed_F)
|
||||
if condition > 1e-2:
|
||||
raise RuntimeError(
|
||||
f"Cross-turn field composition violated versor condition: {condition:.3e}"
|
||||
)
|
||||
candidate = FieldState(
|
||||
F=versor_apply(injected.F, self.state.F),
|
||||
F=composed_F,
|
||||
node=node_idx,
|
||||
step=self.state.step + 1,
|
||||
holonomy=injected.holonomy,
|
||||
|
|
@ -117,7 +123,9 @@ class SessionContext:
|
|||
if self.running_dialogue_blade is None:
|
||||
self.running_dialogue_blade = blade.copy()
|
||||
else:
|
||||
self.running_dialogue_blade = outer_product(self.running_dialogue_blade, blade)
|
||||
alpha = cga_inner(self.running_dialogue_blade, blade)
|
||||
sign = 1.0 if alpha >= 0.0 else -1.0
|
||||
self.running_dialogue_blade = sign * blade
|
||||
return turn
|
||||
|
||||
@property
|
||||
|
|
|
|||
|
|
@ -96,6 +96,35 @@ def test_surface_elaboration_matches_rendered_elaboration_string() -> None:
|
|||
assert "mercy but justice" in sentence.surface
|
||||
|
||||
|
||||
def test_running_dialogue_blade_stays_nonzero_after_three_turns() -> None:
|
||||
"""The running blade must not collapse to zero through grade explosion."""
|
||||
from algebra.rotor import make_rotor_from_angle
|
||||
from generate.proposition import Proposition
|
||||
|
||||
def _prop(slot: int) -> Proposition:
|
||||
relation = make_rotor_from_angle(0.1 * (slot + 1), bivector_idx=6)
|
||||
return Proposition(
|
||||
subject="a", predicate="b", object_=None, surface="a b",
|
||||
frame_id="test",
|
||||
subject_versor=_v(0), predicate_versor=_v(1),
|
||||
object_versor=None, relation=relation,
|
||||
)
|
||||
|
||||
from session.context import SessionContext
|
||||
from language_packs import load_pack
|
||||
_, vocab = load_pack("en_core_cognition_v1")
|
||||
ctx = SessionContext(vocab)
|
||||
|
||||
for i in range(5):
|
||||
ctx.record_dialogue(_prop(i))
|
||||
|
||||
blade = ctx.running_dialogue_blade
|
||||
assert blade is not None
|
||||
assert float(np.linalg.norm(blade)) > 1e-6, (
|
||||
"running_dialogue_blade collapsed to zero after multiple turns"
|
||||
)
|
||||
|
||||
|
||||
def test_unknown_gate_fires_on_empty_vault_without_self_storage() -> None:
|
||||
class EmptyVault:
|
||||
def __len__(self) -> int:
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ O(N) np.array_equal scans.
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import deque
|
||||
|
||||
import numpy as np
|
||||
from algebra.backend import vault_recall
|
||||
from algebra.cga import null_project
|
||||
|
|
@ -27,8 +29,8 @@ class VaultStore:
|
|||
reproject_interval: int = 100,
|
||||
max_entries: int | None = None,
|
||||
):
|
||||
self._versors: list[np.ndarray] = []
|
||||
self._metadata: list[dict] = []
|
||||
self._versors: deque[np.ndarray] = deque(maxlen=max_entries)
|
||||
self._metadata: deque[dict] = deque(maxlen=max_entries)
|
||||
self._store_count: int = 0
|
||||
self._reproject_interval = reproject_interval
|
||||
self._max_entries = max_entries
|
||||
|
|
@ -38,20 +40,20 @@ class VaultStore:
|
|||
"""Store a versor. Returns its index. Auto-reprojects every N stores."""
|
||||
arr = np.asarray(F, dtype=np.float32).copy()
|
||||
|
||||
if self._max_entries is not None and len(self._versors) >= self._max_entries:
|
||||
self._evict_oldest()
|
||||
|
||||
will_evict = self._max_entries is not None and len(self._versors) >= self._max_entries
|
||||
self._versors.append(arr)
|
||||
self._metadata.append(metadata or {})
|
||||
idx = len(self._versors) - 1
|
||||
|
||||
key = _versor_key(arr)
|
||||
self._exact_index.setdefault(key, []).append(idx)
|
||||
if will_evict:
|
||||
self._rebuild_index()
|
||||
else:
|
||||
idx = len(self._versors) - 1
|
||||
key = _versor_key(arr)
|
||||
self._exact_index.setdefault(key, []).append(idx)
|
||||
|
||||
self._store_count += 1
|
||||
if self._reproject_interval > 0 and self._store_count % self._reproject_interval == 0:
|
||||
self.reproject()
|
||||
return idx
|
||||
return len(self._versors) - 1
|
||||
|
||||
def recall(self, query: np.ndarray, top_k: int = 5) -> list:
|
||||
"""
|
||||
|
|
@ -62,7 +64,7 @@ class VaultStore:
|
|||
return []
|
||||
|
||||
query_arr = np.asarray(query, dtype=np.float32)
|
||||
ranked = vault_recall(self._versors, query_arr, max(top_k, 1))
|
||||
ranked = vault_recall(list(self._versors), query_arr, max(top_k, 1))
|
||||
|
||||
key = _versor_key(query_arr)
|
||||
exact_indices = self._exact_index.get(key, [])
|
||||
|
|
@ -86,7 +88,8 @@ class VaultStore:
|
|||
Re-project all stored versors onto the null cone.
|
||||
Corrects floating-point drift. Run between turns or asynchronously.
|
||||
"""
|
||||
self._versors = [null_project(v) for v in self._versors]
|
||||
reprojected = deque((null_project(v) for v in self._versors), maxlen=self._max_entries)
|
||||
self._versors = reprojected
|
||||
self._rebuild_index()
|
||||
|
||||
def _rebuild_index(self) -> None:
|
||||
|
|
@ -95,24 +98,6 @@ class VaultStore:
|
|||
key = _versor_key(v)
|
||||
self._exact_index.setdefault(key, []).append(i)
|
||||
|
||||
def _evict_oldest(self) -> None:
|
||||
"""Remove the oldest entry. Deterministic FIFO eviction."""
|
||||
if not self._versors:
|
||||
return
|
||||
evicted = self._versors.pop(0)
|
||||
self._metadata.pop(0)
|
||||
key = _versor_key(evicted)
|
||||
indices = self._exact_index.get(key, [])
|
||||
if indices:
|
||||
indices.pop(0)
|
||||
if not indices:
|
||||
del self._exact_index[key]
|
||||
self._reindex_after_eviction()
|
||||
|
||||
def _reindex_after_eviction(self) -> None:
|
||||
"""Rebuild index after front-removal shifts all indices by -1."""
|
||||
self._rebuild_index()
|
||||
|
||||
@property
|
||||
def reproject_interval(self) -> int:
|
||||
return self._reproject_interval
|
||||
|
|
|
|||
Loading…
Reference in a new issue