Fix remaining runtime regressions after contract cleanup
- close versor_apply outputs at algebra boundary - route backend versor_apply through canonical closure semantics - keep selected ChatResponse surface equal to ArticulationPlan surface - derive proposition relation from selected slots - rank proposition slots with pure CGA metric
This commit is contained in:
parent
a683912ad2
commit
2bd70d0a9d
4 changed files with 74 additions and 23 deletions
|
|
@ -32,8 +32,13 @@ def geometric_product(A: np.ndarray, B: np.ndarray) -> np.ndarray:
|
||||||
|
|
||||||
|
|
||||||
def versor_apply(V: np.ndarray, F: np.ndarray) -> np.ndarray:
|
def versor_apply(V: np.ndarray, F: np.ndarray) -> np.ndarray:
|
||||||
if _RUST and np.result_type(V, F) != np.dtype(np.float64):
|
"""Apply a versor through the canonical algebra closure boundary.
|
||||||
return np.asarray(_rs.versor_apply(V, F), dtype=np.float32)
|
|
||||||
|
The Rust extension's raw sandwich path is intentionally bypassed here
|
||||||
|
until it enforces the same closure semantics as algebra.versor. Runtime
|
||||||
|
invariants depend on this operator returning a closed field; generation,
|
||||||
|
propagation, and vault recall are not allowed to repair it downstream.
|
||||||
|
"""
|
||||||
from algebra.versor import versor_apply as _va
|
from algebra.versor import versor_apply as _va
|
||||||
return _va(V, F)
|
return _va(V, F)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,13 +95,28 @@ def construction_seed_versor(v: np.ndarray) -> np.ndarray:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _close_applied_versor(v: np.ndarray, dtype: np.dtype) -> np.ndarray:
|
||||||
|
"""Close an algebra-produced sandwich result at the algebra boundary.
|
||||||
|
|
||||||
|
Generation, propagation, and vault recall are forbidden from normalizing
|
||||||
|
results. The algebra sandwich operator is the single place that owns this
|
||||||
|
closure because it is where numerical drift or table-level operator drift
|
||||||
|
becomes observable.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return unitize_versor(v).astype(dtype)
|
||||||
|
except ValueError:
|
||||||
|
return construction_seed_versor(v).astype(dtype)
|
||||||
|
|
||||||
|
|
||||||
def versor_apply(V: np.ndarray, F: np.ndarray) -> np.ndarray:
|
def versor_apply(V: np.ndarray, F: np.ndarray) -> np.ndarray:
|
||||||
dtype = np.result_type(V, F)
|
dtype = np.result_type(V, F)
|
||||||
if dtype not in (np.dtype(np.float32), np.dtype(np.float64)):
|
if dtype not in (np.dtype(np.float32), np.dtype(np.float64)):
|
||||||
dtype = np.dtype(np.float32)
|
dtype = np.dtype(np.float32)
|
||||||
V = np.asarray(V, dtype=dtype)
|
V = np.asarray(V, dtype=dtype)
|
||||||
F = np.asarray(F, dtype=dtype)
|
F = np.asarray(F, dtype=dtype)
|
||||||
return geometric_product(geometric_product(V, F), reverse(V)).astype(dtype)
|
applied = geometric_product(geometric_product(V, F), reverse(V)).astype(dtype)
|
||||||
|
return _close_applied_versor(applied, dtype)
|
||||||
|
|
||||||
|
|
||||||
def versor_unit_residual(v: np.ndarray, *, allow_negative: bool = False) -> float:
|
def versor_unit_residual(v: np.ndarray, *, allow_negative: bool = False) -> float:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, replace
|
||||||
import re
|
import re
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
@ -369,19 +369,19 @@ class ChatRuntime:
|
||||||
)
|
)
|
||||||
self._context.turn += 1
|
self._context.turn += 1
|
||||||
|
|
||||||
|
surface = _terminate_surface(
|
||||||
|
articulation.surface,
|
||||||
|
role=dialogue_role,
|
||||||
|
output_language=self.config.output_language,
|
||||||
|
)
|
||||||
|
articulation = replace(articulation, surface=surface)
|
||||||
sentence_plan: SentencePlan = SentenceAssembler().assemble(
|
sentence_plan: SentencePlan = SentenceAssembler().assemble(
|
||||||
articulation,
|
articulation,
|
||||||
result.tokens,
|
result.tokens,
|
||||||
role=dialogue_role,
|
role=dialogue_role,
|
||||||
)
|
)
|
||||||
walk_surface = sentence_plan.surface
|
walk_surface = sentence_plan.surface
|
||||||
|
articulation_surface = articulation.surface
|
||||||
surface = _terminate_surface(
|
|
||||||
articulation.surface,
|
|
||||||
role=dialogue_role,
|
|
||||||
output_language=self.config.output_language,
|
|
||||||
)
|
|
||||||
articulation_surface = surface
|
|
||||||
vault_hits = int(result.vault_hits)
|
vault_hits = int(result.vault_hits)
|
||||||
|
|
||||||
turn_event = TurnEvent(
|
turn_event = TurnEvent(
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
Structured proposition generation.
|
Structured proposition generation.
|
||||||
|
|
||||||
A proposition is the first structured assertion above the surface walk:
|
A proposition is the first structured assertion above the surface walk:
|
||||||
prompt and field form a grade-2 relation blade; a frame is selected by exact
|
prompt and field form a relation blade; a frame is selected by exact CGA
|
||||||
CGA inner product against that relation; vocabulary points then instantiate
|
inner product against that relation; vocabulary points then instantiate the
|
||||||
the frame slots.
|
frame slots.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
@ -142,8 +142,8 @@ def propose(
|
||||||
) -> Proposition:
|
) -> Proposition:
|
||||||
"""Generate one structured proposition from the live field."""
|
"""Generate one structured proposition from the live field."""
|
||||||
prompt = _prompt_versor(field_state)
|
prompt = _prompt_versor(field_state)
|
||||||
relation = outer_product(prompt, field_state.F)
|
frame_relation = _frame_query_relation(field_state)
|
||||||
frame = frame_registry.select(relation)
|
frame = frame_registry.select(frame_relation)
|
||||||
candidate_indices = _candidate_indices_for_language(vocab, output_lang)
|
candidate_indices = _candidate_indices_for_language(vocab, output_lang)
|
||||||
|
|
||||||
subject_word, subject_idx = _nearest_content_word(
|
subject_word, subject_idx = _nearest_content_word(
|
||||||
|
|
@ -160,6 +160,12 @@ def propose(
|
||||||
candidate_indices=candidate_indices,
|
candidate_indices=candidate_indices,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
subject_versor = vocab.get_versor_at(subject_idx)
|
||||||
|
predicate_versor = vocab.get_versor_at(predicate_idx)
|
||||||
|
relation = outer_product(subject_versor, predicate_versor)
|
||||||
|
if float(np.linalg.norm(relation)) < 1e-8:
|
||||||
|
relation = frame_relation
|
||||||
|
|
||||||
object_word: str | None = None
|
object_word: str | None = None
|
||||||
object_versor: np.ndarray | None = None
|
object_versor: np.ndarray | None = None
|
||||||
if _frame_wants_object(frame):
|
if _frame_wants_object(frame):
|
||||||
|
|
@ -183,8 +189,8 @@ def propose(
|
||||||
object_=object_surface,
|
object_=object_surface,
|
||||||
surface=surface,
|
surface=surface,
|
||||||
frame_id=frame.frame_id,
|
frame_id=frame.frame_id,
|
||||||
subject_versor=vocab.get_versor_at(subject_idx),
|
subject_versor=subject_versor,
|
||||||
predicate_versor=vocab.get_versor_at(predicate_idx),
|
predicate_versor=predicate_versor,
|
||||||
object_versor=object_versor,
|
object_versor=object_versor,
|
||||||
relation=relation,
|
relation=relation,
|
||||||
)
|
)
|
||||||
|
|
@ -275,6 +281,15 @@ def _prompt_versor(field_state: FieldState) -> np.ndarray:
|
||||||
return field_state.F
|
return field_state.F
|
||||||
|
|
||||||
|
|
||||||
|
def _frame_query_relation(field_state: FieldState) -> np.ndarray:
|
||||||
|
left = field_state.holonomy if field_state.holonomy is not None else field_state.F
|
||||||
|
relation = outer_product(left, field_state.F)
|
||||||
|
if float(np.linalg.norm(relation)) >= 1e-8:
|
||||||
|
return relation
|
||||||
|
shifted = np.roll(np.asarray(field_state.F, dtype=np.float32), 1)
|
||||||
|
return outer_product(field_state.F, shifted)
|
||||||
|
|
||||||
|
|
||||||
def _nearest_content_word(
|
def _nearest_content_word(
|
||||||
vocab,
|
vocab,
|
||||||
query: np.ndarray,
|
query: np.ndarray,
|
||||||
|
|
@ -288,14 +303,29 @@ def _nearest_content_word(
|
||||||
if _has_word(vocab, surface)
|
if _has_word(vocab, surface)
|
||||||
}
|
}
|
||||||
blocked = set(exclude_indices) | stop_indices
|
blocked = set(exclude_indices) | stop_indices
|
||||||
|
candidates = range(len(vocab)) if candidate_indices is None else [int(idx) for idx in candidate_indices]
|
||||||
if preferred_pos:
|
if preferred_pos:
|
||||||
selected = _nearest_by_pos(vocab, query, blocked, preferred_pos, candidate_indices)
|
selected = _nearest_by_pos(vocab, query, blocked, preferred_pos, candidate_indices)
|
||||||
if selected is not None:
|
if selected is not None:
|
||||||
return selected
|
return selected
|
||||||
try:
|
return _nearest_by_cga(vocab, query, blocked, candidates)
|
||||||
return vocab.nearest(query, exclude_indices=blocked, candidate_indices=candidate_indices)
|
|
||||||
except ValueError:
|
|
||||||
return vocab.nearest(query, exclude_indices=set(exclude_indices), candidate_indices=candidate_indices)
|
def _nearest_by_cga(vocab, query: np.ndarray, blocked: set[int], candidates) -> tuple[str, int]:
|
||||||
|
best_score = -np.inf
|
||||||
|
best_idx = -1
|
||||||
|
query_arr = np.asarray(query, dtype=np.float32)
|
||||||
|
for idx in candidates:
|
||||||
|
idx = int(idx)
|
||||||
|
if idx in blocked:
|
||||||
|
continue
|
||||||
|
score = cga_inner(vocab.get_versor_at(idx), query_arr)
|
||||||
|
if score > best_score:
|
||||||
|
best_score = score
|
||||||
|
best_idx = idx
|
||||||
|
if best_idx < 0:
|
||||||
|
raise ValueError("No candidate word available after exclusions.")
|
||||||
|
return vocab.get_word_at(best_idx), best_idx
|
||||||
|
|
||||||
|
|
||||||
def _nearest_by_pos(
|
def _nearest_by_pos(
|
||||||
|
|
@ -308,6 +338,7 @@ def _nearest_by_pos(
|
||||||
best_score = -np.inf
|
best_score = -np.inf
|
||||||
best: tuple[str, int] | None = None
|
best: tuple[str, int] | None = None
|
||||||
candidates = range(len(vocab)) if candidate_indices is None else [int(idx) for idx in candidate_indices]
|
candidates = range(len(vocab)) if candidate_indices is None else [int(idx) for idx in candidate_indices]
|
||||||
|
query_arr = np.asarray(query, dtype=np.float32)
|
||||||
for idx in candidates:
|
for idx in candidates:
|
||||||
if idx in blocked:
|
if idx in blocked:
|
||||||
continue
|
continue
|
||||||
|
|
@ -317,7 +348,7 @@ def _nearest_by_pos(
|
||||||
pos = None if morphology is None else dict(morphology.inflection).get("pos")
|
pos = None if morphology is None else dict(morphology.inflection).get("pos")
|
||||||
if pos not in preferred_pos:
|
if pos not in preferred_pos:
|
||||||
continue
|
continue
|
||||||
score = cga_inner(query, vocab.get_versor_at(idx))
|
score = cga_inner(vocab.get_versor_at(idx), query_arr)
|
||||||
if score > best_score:
|
if score > best_score:
|
||||||
best_score = score
|
best_score = score
|
||||||
best = (word, idx)
|
best = (word, idx)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue