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:
|
||||
if _RUST and np.result_type(V, F) != np.dtype(np.float64):
|
||||
return np.asarray(_rs.versor_apply(V, F), dtype=np.float32)
|
||||
"""Apply a versor through the canonical algebra closure boundary.
|
||||
|
||||
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
|
||||
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:
|
||||
dtype = np.result_type(V, F)
|
||||
if dtype not in (np.dtype(np.float32), np.dtype(np.float64)):
|
||||
dtype = np.dtype(np.float32)
|
||||
V = np.asarray(V, 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:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, replace
|
||||
import re
|
||||
from collections.abc import Sequence
|
||||
from typing import List
|
||||
|
|
@ -369,19 +369,19 @@ class ChatRuntime:
|
|||
)
|
||||
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(
|
||||
articulation,
|
||||
result.tokens,
|
||||
role=dialogue_role,
|
||||
)
|
||||
walk_surface = sentence_plan.surface
|
||||
|
||||
surface = _terminate_surface(
|
||||
articulation.surface,
|
||||
role=dialogue_role,
|
||||
output_language=self.config.output_language,
|
||||
)
|
||||
articulation_surface = surface
|
||||
articulation_surface = articulation.surface
|
||||
vault_hits = int(result.vault_hits)
|
||||
|
||||
turn_event = TurnEvent(
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
Structured proposition generation.
|
||||
|
||||
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
|
||||
CGA inner product against that relation; vocabulary points then instantiate
|
||||
the frame slots.
|
||||
prompt and field form a relation blade; a frame is selected by exact CGA
|
||||
inner product against that relation; vocabulary points then instantiate the
|
||||
frame slots.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -142,8 +142,8 @@ def propose(
|
|||
) -> Proposition:
|
||||
"""Generate one structured proposition from the live field."""
|
||||
prompt = _prompt_versor(field_state)
|
||||
relation = outer_product(prompt, field_state.F)
|
||||
frame = frame_registry.select(relation)
|
||||
frame_relation = _frame_query_relation(field_state)
|
||||
frame = frame_registry.select(frame_relation)
|
||||
candidate_indices = _candidate_indices_for_language(vocab, output_lang)
|
||||
|
||||
subject_word, subject_idx = _nearest_content_word(
|
||||
|
|
@ -160,6 +160,12 @@ def propose(
|
|||
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_versor: np.ndarray | None = None
|
||||
if _frame_wants_object(frame):
|
||||
|
|
@ -183,8 +189,8 @@ def propose(
|
|||
object_=object_surface,
|
||||
surface=surface,
|
||||
frame_id=frame.frame_id,
|
||||
subject_versor=vocab.get_versor_at(subject_idx),
|
||||
predicate_versor=vocab.get_versor_at(predicate_idx),
|
||||
subject_versor=subject_versor,
|
||||
predicate_versor=predicate_versor,
|
||||
object_versor=object_versor,
|
||||
relation=relation,
|
||||
)
|
||||
|
|
@ -275,6 +281,15 @@ def _prompt_versor(field_state: FieldState) -> np.ndarray:
|
|||
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(
|
||||
vocab,
|
||||
query: np.ndarray,
|
||||
|
|
@ -288,14 +303,29 @@ def _nearest_content_word(
|
|||
if _has_word(vocab, surface)
|
||||
}
|
||||
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:
|
||||
selected = _nearest_by_pos(vocab, query, blocked, preferred_pos, candidate_indices)
|
||||
if selected is not None:
|
||||
return selected
|
||||
try:
|
||||
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)
|
||||
return _nearest_by_cga(vocab, query, blocked, candidates)
|
||||
|
||||
|
||||
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(
|
||||
|
|
@ -308,6 +338,7 @@ def _nearest_by_pos(
|
|||
best_score = -np.inf
|
||||
best: tuple[str, int] | None = None
|
||||
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:
|
||||
if idx in blocked:
|
||||
continue
|
||||
|
|
@ -317,7 +348,7 @@ def _nearest_by_pos(
|
|||
pos = None if morphology is None else dict(morphology.inflection).get("pos")
|
||||
if pos not in preferred_pos:
|
||||
continue
|
||||
score = cga_inner(query, vocab.get_versor_at(idx))
|
||||
score = cga_inner(vocab.get_versor_at(idx), query_arr)
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best = (word, idx)
|
||||
|
|
|
|||
Loading…
Reference in a new issue