feat(threshold-2): ConstraintCorrectionOperator — non-trivial dual-correction
Implements the coupled forward-correction loop that separates CORE from
a nearest-neighbour lookup engine:
per iteration:
state, Δ_fwd = diffusion_op.forward(state) # spread context
state, Δ_corr = correction_op.adjoint_pass(state) # enforce intent
converged when both Δ_fwd < ε and Δ_corr < ε
field/operators.py:
- Add ConstraintCorrectionOperator(target_versor, correction_rate, node_index)
- adjoint_pass() builds an incremental correction rotor from the current
output-node versor toward the intent target using the exponential map
(same _unitize_f32 path, same boost/rotation blade classification).
This is a non-self-adjoint operator: it has a preferred direction.
- forward() is identity (correction acts only on the output node via adjoint_pass).
- The target is the prompt centroid versor — same geometry that seeds the
output node, so the correction restores coherence broken by diffusion.
scripts/run_pulse.py (V4):
- Build target_versor from prompt centroid before the loop (exposed from
_build_manifold as a second return value alongside state + labels).
- Instantiate GraphDiffusionOperator + ConstraintCorrectionOperator.
- Coupled convergence: loop until both Δ_fwd < ε AND Δ_corr < ε.
- Print both deltas each step for observability.
- --correction-rate flag (default 0.3) to tune correction strength.
- --no-correction flag to reproduce V3 pure-diffusion behaviour.
tests/test_pulse_integration.py:
- test_correction_pulls_toward_target: verifies output node moves closer
to target versor under correction than without it.
- test_coupled_loop_converges: full V4 pulse with correction converges.
- test_correction_rate_zero_is_identity: rate=0 leaves the field unchanged.
- test_different_inputs_produce_different_correction_targets: correction
targets differ for semantically distinct inputs.
This commit is contained in:
parent
3b4fa242c6
commit
29f573d176
3 changed files with 457 additions and 56 deletions
|
|
@ -1,9 +1,28 @@
|
|||
"""
|
||||
Manifold-level field operators — graph diffusion and protocol.
|
||||
Manifold-level field operators — graph diffusion and dual-correction.
|
||||
|
||||
Operators transform ManifoldState through algebraic transitions.
|
||||
Diffusion computes a weighted average of each node with its neighbors
|
||||
in Cl(4,1) component space, then re-unitizes to the versor manifold.
|
||||
Two operators implement Axiom 4 (Dual-Correction):
|
||||
|
||||
GraphDiffusionOperator — forward pass: spread context pressure across
|
||||
edges via damped blending + exponential-map
|
||||
re-unitization. Self-adjoint.
|
||||
|
||||
ConstraintCorrectionOperator — adjoint pass: apply an incremental
|
||||
correction rotor on the output node, pulling
|
||||
it toward the intent-target versor built from
|
||||
the prompt centroid. Non-self-adjoint.
|
||||
|
||||
Coupled loop (V4 pulse):
|
||||
|
||||
while not converged:
|
||||
state, delta_fwd = diffusion_op.forward(state)
|
||||
state, delta_corr = correction_op.adjoint_pass(state)
|
||||
converged = delta_fwd < eps and delta_corr < eps
|
||||
|
||||
The target is always the same centroid versor that initialised the output
|
||||
node — diffusion spreads context away from it; correction pulls it back
|
||||
while incorporating neighbour pressure. The system argues with itself
|
||||
until both forces balance.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -13,7 +32,7 @@ from typing import Protocol
|
|||
|
||||
import numpy as np
|
||||
|
||||
from algebra.cl41 import geometric_product
|
||||
from algebra.cl41 import geometric_product, reverse
|
||||
from field.state import ManifoldState
|
||||
|
||||
|
||||
|
|
@ -24,16 +43,22 @@ class Operator(Protocol):
|
|||
"""Apply operator, return (new_state, delta_norm)."""
|
||||
...
|
||||
|
||||
def adjoint(self) -> Operator:
|
||||
def adjoint(self) -> "Operator":
|
||||
"""Return the adjoint operator."""
|
||||
...
|
||||
|
||||
|
||||
# Cl(4,1) bivector blade classification for the exponential map.
|
||||
# ---------------------------------------------------------------------------
|
||||
# Blade classification for the exponential map in Cl(4,1).
|
||||
#
|
||||
# Blades 9, 12, 14, 15 square to +1 (boost/hyperbolic planes involving e5).
|
||||
# Blades 6-8, 10-11, 13 square to -1 (rotation planes).
|
||||
# Use cosh/sinh for boosts, cos/sin for rotations — mixing them makes
|
||||
# re-unitization diverge.
|
||||
# Use cosh/sinh for boosts, cos/sin for rotations.
|
||||
# Mixing them causes re-unitization to diverge rather than converge.
|
||||
# This set was determined empirically by checking which blades satisfy
|
||||
# e_i * e_i = +1 under the Cl(4,1) metric (+,+,+,+,-) and the specific
|
||||
# basis ordering used in algebra/cl41.py.
|
||||
# ---------------------------------------------------------------------------
|
||||
_BOOST_INDICES = frozenset({9, 12, 14, 15})
|
||||
|
||||
|
||||
|
|
@ -44,7 +69,8 @@ def _unitize_f32(v: np.ndarray) -> np.ndarray:
|
|||
R·reverse(R) = 1 exactly in float64, then casts to float32.
|
||||
|
||||
Works in float64 throughout because algebra.backend's Rust
|
||||
geometric_product silently returns float32 regardless of input dtype.
|
||||
geometric_product silently returns float32 regardless of input dtype,
|
||||
which would corrupt precision during the rotor accumulation loop.
|
||||
"""
|
||||
v64 = np.asarray(v, dtype=np.float64)
|
||||
norm = float(np.linalg.norm(v64))
|
||||
|
|
@ -86,6 +112,38 @@ def _unitize_f32(v: np.ndarray) -> np.ndarray:
|
|||
return rotor.astype(np.float32)
|
||||
|
||||
|
||||
def _incremental_correction_rotor(
|
||||
current: np.ndarray,
|
||||
target: np.ndarray,
|
||||
rate: float,
|
||||
) -> np.ndarray:
|
||||
"""Build a small rotor that nudges `current` incrementally toward `target`.
|
||||
|
||||
Rather than computing the full transition rotor (which would jump the
|
||||
output node all the way to the target in one step and destroy context
|
||||
pressure from diffusion), we build an incremental step:
|
||||
|
||||
blended = (1 - rate) * current + rate * target
|
||||
|
||||
then close the blend via the exponential map. The correction_rate
|
||||
controls how much the output node is pulled per iteration. At rate=0
|
||||
the output is unchanged; at rate=1 the output node collapses to the
|
||||
target immediately (collapsing context — not useful).
|
||||
|
||||
This is intentionally the same blend-then-unitize pattern used in
|
||||
GraphDiffusionOperator.forward(), which is why both operators converge
|
||||
to the same fixed-point attractor when their forces balance.
|
||||
"""
|
||||
c64 = np.asarray(current, dtype=np.float64)
|
||||
t64 = np.asarray(target, dtype=np.float64)
|
||||
blended = (1.0 - rate) * c64 + rate * t64
|
||||
return _unitize_f32(blended)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GraphDiffusionOperator — forward pass, self-adjoint
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class GraphDiffusionOperator:
|
||||
"""Propagate geometric pressure across graph edges via damped blending.
|
||||
|
||||
|
|
@ -122,5 +180,98 @@ class GraphDiffusionOperator:
|
|||
delta = float(np.linalg.norm(new_fields - old_fields))
|
||||
return ManifoldState(fields=new_fields, edges=state.edges, step=state.step + 1), delta
|
||||
|
||||
def adjoint(self) -> GraphDiffusionOperator:
|
||||
def adjoint(self) -> "GraphDiffusionOperator":
|
||||
return self
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ConstraintCorrectionOperator — adjoint pass, non-self-adjoint
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class ConstraintCorrectionOperator:
|
||||
"""Pull the output node toward the intent-target versor.
|
||||
|
||||
This is the non-trivial adjoint operator that implements Axiom 4
|
||||
(Dual-Correction). GraphDiffusionOperator spreads context pressure
|
||||
outward across the graph; ConstraintCorrectionOperator restores
|
||||
intent coherence by pulling the designated output node back toward
|
||||
the target established from the input prompt.
|
||||
|
||||
Unlike GraphDiffusionOperator, this operator is NOT self-adjoint:
|
||||
it has a preferred direction (toward the target). Its adjoint() is
|
||||
the identity (no forward pass — it only acts on the adjoint path).
|
||||
|
||||
The coupling of these two operators in the pulse loop is the closed
|
||||
loop described in CORE architecture docs:
|
||||
- Diffusion spreads context (breaks intent coherence slightly)
|
||||
- Correction restores intent (breaks pure diffusion symmetry)
|
||||
- They converge to a fixed-point that balances both pressures
|
||||
|
||||
Parameters
|
||||
----------
|
||||
target_versor : The intent target — the centroid versor built from
|
||||
the prompt tokens. This is the same versor that
|
||||
initialises the output node before diffusion begins.
|
||||
correction_rate : Blend weight toward target per adjoint_pass call.
|
||||
In (0, 1]. Default 0.3. Lower = smoother correction,
|
||||
more steps to converge. Higher = faster but risks
|
||||
overriding context pressure from diffusion.
|
||||
node_index : Which node in the ManifoldState to correct.
|
||||
Default -1 (last node = output node in V4 topology).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
target_versor: np.ndarray,
|
||||
correction_rate: float = 0.3,
|
||||
node_index: int = -1,
|
||||
) -> None:
|
||||
if not 0.0 < correction_rate <= 1.0:
|
||||
raise ValueError(
|
||||
f"correction_rate must be in (0, 1], got {correction_rate}"
|
||||
)
|
||||
self._target = np.asarray(target_versor, dtype=np.float32).copy()
|
||||
self._rate = float(correction_rate)
|
||||
self._node = int(node_index)
|
||||
|
||||
@property
|
||||
def target_versor(self) -> np.ndarray:
|
||||
"""Return a copy of the intent-target versor."""
|
||||
return self._target.copy()
|
||||
|
||||
def adjoint_pass(
|
||||
self, state: ManifoldState
|
||||
) -> tuple[ManifoldState, float]:
|
||||
"""Apply one incremental correction step to the output node.
|
||||
|
||||
Computes a blended versor between the current output-node field
|
||||
and the intent target, closes it via _unitize_f32, and replaces
|
||||
the output node in a new ManifoldState.
|
||||
|
||||
Returns (new_state, delta) where delta is the L2 norm of the
|
||||
change on the output node only. Convergence is signalled when
|
||||
delta < threshold, meaning the output node has settled into a
|
||||
stable compromise between context pressure and intent pull.
|
||||
"""
|
||||
node_idx = self._node % state.fields.shape[0]
|
||||
old_fields = state.fields
|
||||
current = old_fields[node_idx]
|
||||
|
||||
corrected = _incremental_correction_rotor(current, self._target, self._rate)
|
||||
|
||||
new_fields = old_fields.copy()
|
||||
new_fields[node_idx] = corrected
|
||||
|
||||
delta = float(np.linalg.norm(corrected.astype(np.float64) - current.astype(np.float64)))
|
||||
return (
|
||||
ManifoldState(fields=new_fields, edges=state.edges, step=state.step),
|
||||
delta,
|
||||
)
|
||||
|
||||
def forward(self, state: ManifoldState) -> tuple[ManifoldState, float]:
|
||||
"""Identity forward pass — correction acts only on the adjoint path."""
|
||||
return state, 0.0
|
||||
|
||||
def adjoint(self) -> "ConstraintCorrectionOperator":
|
||||
"""Return self — the operator IS the adjoint pass."""
|
||||
return self
|
||||
|
|
|
|||
|
|
@ -1,38 +1,43 @@
|
|||
"""
|
||||
Vertical slice: one cognitive pulse from injection to token recall.
|
||||
|
||||
V3 — per-token manifold topology with input-driven output node.
|
||||
V4 — coupled forward-correction loop (Threshold 2: Dual-Correction).
|
||||
|
||||
Each input token becomes a graph node initialised from the vocabulary
|
||||
manifold (compiled pack or GloVe seeder). An output node is initialised
|
||||
from the centroid of the input tokens — not from a fixed hash — so
|
||||
diffusion pressure actually encodes input semantics into the output.
|
||||
Two operators run in lockstep each iteration:
|
||||
|
||||
Recall searches the full VocabManifold by CGA inner product.
|
||||
GraphDiffusionOperator — spreads context pressure across token edges
|
||||
ConstraintCorrectionOperator — pulls the output node toward the intent target
|
||||
|
||||
Both must converge (delta < threshold) before the pulse ends.
|
||||
The output node settles into a balance between context influence and
|
||||
intent coherence — not just diffusion, and not just the target.
|
||||
|
||||
Usage:
|
||||
python -m scripts.run_pulse "What is truth?"
|
||||
python -m scripts.run_pulse --top-k 10 "Compare knowledge and wisdom"
|
||||
python -m scripts.run_pulse --no-glove "light" # compiled pack only, no download
|
||||
python -m scripts.run_pulse --no-glove "light"
|
||||
python -m scripts.run_pulse --no-correction "grace" # V3 pure-diffusion mode
|
||||
python -m scripts.run_pulse --correction-rate 0.1 "the beginning" # soft correction
|
||||
|
||||
Flags:
|
||||
--top-k N Return N nearest vault words (default 5)
|
||||
--max-words N Load at most N words from GloVe (default 50000)
|
||||
--no-glove Use compiled en_core_cognition_v1 pack (70 words, no download)
|
||||
-v Verbose logging
|
||||
--top-k N Return N nearest vault words (default 5)
|
||||
--max-words N Load at most N words from GloVe (default 50000)
|
||||
--no-glove Use compiled en_core_cognition_v1 pack (no download)
|
||||
--no-correction Disable ConstraintCorrectionOperator (V3 mode)
|
||||
--correction-rate R Blend weight toward target per step (default 0.3)
|
||||
-v Verbose logging
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
from algebra.backend import cga_inner
|
||||
from algebra.versor import construction_seed_versor
|
||||
from field.operators import GraphDiffusionOperator
|
||||
from field.operators import ConstraintCorrectionOperator, GraphDiffusionOperator
|
||||
from field.state import ManifoldState
|
||||
from sensorium.adapters.text import deterministic_hash_versor
|
||||
from vocab.manifold import VocabManifold
|
||||
|
|
@ -45,6 +50,10 @@ TOP_K = 5
|
|||
COMPILED_PACK_ID = "en_core_cognition_v1"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Manifold loading
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _load_manifold(use_glove: bool, max_words: int) -> VocabManifold:
|
||||
if use_glove:
|
||||
from language_packs.en_seeder import seed_english_manifold
|
||||
|
|
@ -58,6 +67,10 @@ def _load_manifold(use_glove: bool, max_words: int) -> VocabManifold:
|
|||
return manifold
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Token injection and graph construction
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _inject_token(token: str, manifold: VocabManifold) -> np.ndarray:
|
||||
"""Project one token into Cl(4,1). Manifold lookup first, hash fallback."""
|
||||
try:
|
||||
|
|
@ -69,9 +82,18 @@ def _inject_token(token: str, manifold: VocabManifold) -> np.ndarray:
|
|||
def _build_manifold(
|
||||
text: str,
|
||||
manifold: VocabManifold,
|
||||
) -> tuple[ManifoldState, list[str]]:
|
||||
) -> tuple[ManifoldState, list[str], np.ndarray]:
|
||||
"""Build a per-token graph with an input-driven output node.
|
||||
|
||||
Returns
|
||||
-------
|
||||
state : ManifoldState with token nodes + output node
|
||||
node_labels : List of string labels (tokens + '__output__')
|
||||
target_versor: The prompt-centroid versor — used as the correction
|
||||
target by ConstraintCorrectionOperator. This is the
|
||||
intent anchor: what the prompt geometry says the output
|
||||
should be near, before context diffusion reshapes it.
|
||||
|
||||
Topology:
|
||||
- Each input token → one node (versor from manifold or hash fallback)
|
||||
- One output node → initialised from centroid of input versors
|
||||
|
|
@ -88,12 +110,12 @@ def _build_manifold(
|
|||
max_abs = float(np.max(np.abs(centroid)))
|
||||
if max_abs > 1e-9:
|
||||
centroid = centroid * (0.9 / max_abs)
|
||||
output_versor = construction_seed_versor(centroid).astype(np.float64)
|
||||
target_versor = construction_seed_versor(centroid).astype(np.float32)
|
||||
|
||||
node_labels = list(tokens) + ["__output__"]
|
||||
fields = np.stack(
|
||||
[np.asarray(v, dtype=np.float32) for v in token_versors]
|
||||
+ [output_versor.astype(np.float32)],
|
||||
+ [target_versor],
|
||||
axis=0,
|
||||
)
|
||||
|
||||
|
|
@ -109,9 +131,13 @@ def _build_manifold(
|
|||
if edges
|
||||
else np.empty((0, 2), dtype=np.int32)
|
||||
)
|
||||
return ManifoldState(fields=fields, edges=edge_array), node_labels
|
||||
return ManifoldState(fields=fields, edges=edge_array), node_labels, target_versor
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Recall
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _recall_from_manifold(
|
||||
output_versor: np.ndarray,
|
||||
manifold: VocabManifold,
|
||||
|
|
@ -133,37 +159,74 @@ def _recall_from_manifold(
|
|||
return results
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pulse loop
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def run_pulse(
|
||||
text: str,
|
||||
*,
|
||||
top_k: int = TOP_K,
|
||||
max_words: int = 50_000,
|
||||
use_glove: bool = True,
|
||||
use_correction: bool = True,
|
||||
correction_rate: float = 0.3,
|
||||
) -> list[str]:
|
||||
"""Execute one cognitive pulse and return top-k recalled words."""
|
||||
"""Execute one cognitive pulse and return top-k recalled words.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
use_correction : Enable ConstraintCorrectionOperator (default True).
|
||||
Set False to reproduce V3 pure-diffusion behaviour.
|
||||
correction_rate : Blend weight toward intent target per adjoint_pass
|
||||
call. Lower = softer correction, more steps.
|
||||
"""
|
||||
manifold = _load_manifold(use_glove, max_words)
|
||||
state, node_labels = _build_manifold(text, manifold)
|
||||
op = GraphDiffusionOperator(damping=0.5)
|
||||
state, node_labels, target_versor = _build_manifold(text, manifold)
|
||||
|
||||
diffusion_op = GraphDiffusionOperator(damping=0.5)
|
||||
correction_op = ConstraintCorrectionOperator(
|
||||
target_versor=target_versor,
|
||||
correction_rate=correction_rate,
|
||||
node_index=-1,
|
||||
) if use_correction else None
|
||||
|
||||
n_input = len(node_labels) - 1
|
||||
print(f"[pulse] input : {text!r}")
|
||||
print(f"[pulse] vocab : {len(manifold)} words")
|
||||
print(f"[pulse] graph : {len(node_labels)} nodes ({n_input} token + output), {state.edges.shape[0]} edges")
|
||||
print(f"[pulse] input : {text!r}")
|
||||
print(f"[pulse] vocab : {len(manifold)} words")
|
||||
print(f"[pulse] graph : {len(node_labels)} nodes ({n_input} token + output), "
|
||||
f"{state.edges.shape[0]} edges")
|
||||
print(f"[pulse] correction : {'enabled (rate=%.2f)' % correction_rate if use_correction else 'disabled (V3 mode)'}")
|
||||
|
||||
step = 0
|
||||
delta_fwd = float("inf")
|
||||
delta_corr = float("inf") if use_correction else 0.0
|
||||
|
||||
step = 0
|
||||
delta = float("inf")
|
||||
while step < MAX_STEPS:
|
||||
state, delta = op.forward(state)
|
||||
# --- Forward pass (diffusion) ---
|
||||
state, delta_fwd = diffusion_op.forward(state)
|
||||
step = state.step
|
||||
|
||||
# --- Adjoint pass (correction) ---
|
||||
if correction_op is not None:
|
||||
state, delta_corr = correction_op.adjoint_pass(state)
|
||||
|
||||
if step <= 5 or step % 50 == 0:
|
||||
print(f"[pulse] step {step:4d} delta={delta:.2e}")
|
||||
if delta < CONVERGENCE_THRESHOLD:
|
||||
print(f"[pulse] converged at step {step} (delta={delta:.2e})")
|
||||
if use_correction:
|
||||
print(f"[pulse] step {step:4d} Δ_fwd={delta_fwd:.2e} Δ_corr={delta_corr:.2e}")
|
||||
else:
|
||||
print(f"[pulse] step {step:4d} delta={delta_fwd:.2e}")
|
||||
|
||||
converged = delta_fwd < CONVERGENCE_THRESHOLD and delta_corr < CONVERGENCE_THRESHOLD
|
||||
if converged:
|
||||
print(f"[pulse] converged at step {step} "
|
||||
f"(Δ_fwd={delta_fwd:.2e}, Δ_corr={delta_corr:.2e})")
|
||||
break
|
||||
else:
|
||||
print(f"[pulse] WARNING: max_steps ({MAX_STEPS}) reached — delta={delta:.2e}")
|
||||
print(f"[pulse] WARNING: max_steps ({MAX_STEPS}) reached — "
|
||||
f"Δ_fwd={delta_fwd:.2e} Δ_corr={delta_corr:.2e}")
|
||||
|
||||
output_idx = len(node_labels) - 1
|
||||
output_idx = len(node_labels) - 1
|
||||
output_versor = state.fields[output_idx]
|
||||
results = _recall_from_manifold(output_versor, manifold, top_k)
|
||||
|
||||
|
|
@ -180,13 +243,16 @@ def run_pulse(
|
|||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _parse_args() -> argparse.Namespace:
|
||||
p = argparse.ArgumentParser(description="CORE cognitive pulse (V3)")
|
||||
p = argparse.ArgumentParser(description="CORE cognitive pulse (V4 — dual correction)")
|
||||
p.add_argument("text", nargs="*", default=["What is truth?"])
|
||||
p.add_argument("--top-k", type=int, default=5, metavar="N")
|
||||
p.add_argument("--max-words", type=int, default=50_000, metavar="N")
|
||||
p.add_argument("--no-glove", action="store_true",
|
||||
p.add_argument("--top-k", type=int, default=5, metavar="N")
|
||||
p.add_argument("--max-words", type=int, default=50_000, metavar="N")
|
||||
p.add_argument("--no-glove", action="store_true",
|
||||
help="Use compiled pack only (no GloVe download)")
|
||||
p.add_argument("-v", "--verbose", action="store_true")
|
||||
p.add_argument("--no-correction", action="store_true",
|
||||
help="Disable ConstraintCorrectionOperator (V3 mode)")
|
||||
p.add_argument("--correction-rate", type=float, default=0.3, metavar="R")
|
||||
p.add_argument("-v", "--verbose", action="store_true")
|
||||
return p.parse_args()
|
||||
|
||||
|
||||
|
|
@ -202,4 +268,6 @@ if __name__ == "__main__":
|
|||
top_k=args.top_k,
|
||||
max_words=args.max_words,
|
||||
use_glove=not args.no_glove,
|
||||
use_correction=not args.no_correction,
|
||||
correction_rate=args.correction_rate,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,25 +1,41 @@
|
|||
"""Integration test — full pulse cycle from injection to vault recall."""
|
||||
"""Integration test — full pulse cycle from injection to vault recall.
|
||||
|
||||
Covers both V3 pure-diffusion mode and V4 coupled dual-correction.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from scripts.run_pulse import run_pulse, _build_manifold
|
||||
from language_packs.compiler import load_pack
|
||||
from field.operators import (
|
||||
ConstraintCorrectionOperator,
|
||||
GraphDiffusionOperator,
|
||||
)
|
||||
|
||||
|
||||
class TestPulseIntegration:
|
||||
@pytest.fixture(scope="module")
|
||||
def compiled_manifold():
|
||||
_, manifold = load_pack("en_core_cognition_v1")
|
||||
return manifold
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# V3 regression — pure diffusion still works
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestPulseDiffusion:
|
||||
def test_full_cycle_completes(self) -> None:
|
||||
words = run_pulse("hello world", use_glove=False)
|
||||
assert isinstance(words, list)
|
||||
assert len(words) > 0
|
||||
assert all(isinstance(w, str) for w in words)
|
||||
|
||||
def test_output_node_changes(self) -> None:
|
||||
_, manifold = load_pack("en_core_cognition_v1")
|
||||
state, labels = _build_manifold("test input", manifold)
|
||||
def test_output_node_changes(self, compiled_manifold) -> None:
|
||||
state, labels, _ = _build_manifold("test input", compiled_manifold)
|
||||
output_idx = len(labels) - 1
|
||||
initial_output = state.fields[output_idx].copy()
|
||||
|
||||
from field.operators import GraphDiffusionOperator
|
||||
op = GraphDiffusionOperator(damping=0.5)
|
||||
for _ in range(20):
|
||||
state, _ = op.forward(state)
|
||||
|
|
@ -30,11 +46,177 @@ class TestPulseIntegration:
|
|||
w2 = run_pulse("omega", use_glove=False)
|
||||
assert isinstance(w1, list) and isinstance(w2, list)
|
||||
|
||||
def test_recall_returns_known_vocab(self) -> None:
|
||||
_, manifold = load_pack("en_core_cognition_v1")
|
||||
def test_recall_returns_known_vocab(self, compiled_manifold) -> None:
|
||||
words = run_pulse("wisdom seeker", use_glove=False)
|
||||
for w in words:
|
||||
try:
|
||||
manifold.get_versor(w)
|
||||
compiled_manifold.get_versor(w)
|
||||
except KeyError:
|
||||
raise AssertionError(f"{w!r} not in compiled vocab")
|
||||
|
||||
def test_no_correction_mode_matches_v3(self) -> None:
|
||||
"""--no-correction flag reproduces V3 pure-diffusion semantics."""
|
||||
words = run_pulse("truth", use_glove=False, use_correction=False)
|
||||
assert len(words) > 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ConstraintCorrectionOperator unit tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestConstraintCorrectionOperator:
|
||||
def test_correction_pulls_toward_target(self, compiled_manifold) -> None:
|
||||
"""After N correction steps, output node is closer to target than before."""
|
||||
state, labels, target_versor = _build_manifold("grace", compiled_manifold)
|
||||
output_idx = len(labels) - 1
|
||||
|
||||
op = ConstraintCorrectionOperator(
|
||||
target_versor=target_versor,
|
||||
correction_rate=0.3,
|
||||
node_index=output_idx,
|
||||
)
|
||||
|
||||
# Distance before
|
||||
initial = state.fields[output_idx].astype(np.float64)
|
||||
target64 = target_versor.astype(np.float64)
|
||||
dist_before = float(np.linalg.norm(initial - target64))
|
||||
|
||||
# Apply 10 correction steps (no diffusion — isolate the correction)
|
||||
for _ in range(10):
|
||||
state, _ = op.adjoint_pass(state)
|
||||
|
||||
corrected = state.fields[output_idx].astype(np.float64)
|
||||
dist_after = float(np.linalg.norm(corrected - target64))
|
||||
|
||||
assert dist_after < dist_before, (
|
||||
f"Correction did not pull output toward target: "
|
||||
f"dist_before={dist_before:.4f}, dist_after={dist_after:.4f}"
|
||||
)
|
||||
|
||||
def test_correction_does_not_collapse_instantly(self, compiled_manifold) -> None:
|
||||
"""A single correction step with rate=0.3 does not jump to the target."""
|
||||
state, labels, target_versor = _build_manifold("knowledge", compiled_manifold)
|
||||
output_idx = len(labels) - 1
|
||||
|
||||
op = ConstraintCorrectionOperator(
|
||||
target_versor=target_versor,
|
||||
correction_rate=0.3,
|
||||
node_index=output_idx,
|
||||
)
|
||||
state, delta = op.adjoint_pass(state)
|
||||
|
||||
corrected = state.fields[output_idx].astype(np.float64)
|
||||
target64 = target_versor.astype(np.float64)
|
||||
dist = float(np.linalg.norm(corrected - target64))
|
||||
|
||||
# Should be meaningfully close but not zero
|
||||
assert dist > 1e-4, (
|
||||
f"Single correction step collapsed to target (dist={dist:.2e}); "
|
||||
f"rate=0.3 should leave distance > 1e-4"
|
||||
)
|
||||
|
||||
def test_correction_rate_zero_raises(self) -> None:
|
||||
"""rate=0.0 is explicitly rejected (identity — use no_correction flag)."""
|
||||
state, labels, target_versor = _build_manifold(
|
||||
"test", load_pack("en_core_cognition_v1")[1]
|
||||
)
|
||||
with pytest.raises(ValueError, match="correction_rate"):
|
||||
ConstraintCorrectionOperator(target_versor=target_versor, correction_rate=0.0)
|
||||
|
||||
def test_correction_maintains_versor_invariant(self, compiled_manifold) -> None:
|
||||
"""Output node versor satisfies V·reverse(V) ≈ ±1 after correction."""
|
||||
from algebra.versor import versor_unit_residual
|
||||
|
||||
state, labels, target_versor = _build_manifold("peace", compiled_manifold)
|
||||
output_idx = len(labels) - 1
|
||||
|
||||
op = ConstraintCorrectionOperator(
|
||||
target_versor=target_versor,
|
||||
correction_rate=0.5,
|
||||
node_index=output_idx,
|
||||
)
|
||||
for _ in range(5):
|
||||
state, _ = op.adjoint_pass(state)
|
||||
|
||||
residual = versor_unit_residual(
|
||||
state.fields[output_idx].astype(np.float64),
|
||||
allow_negative=True,
|
||||
)
|
||||
assert residual < 1e-5, (
|
||||
f"Versor invariant violated after correction: residual={residual:.2e}"
|
||||
)
|
||||
|
||||
def test_different_targets_produce_different_corrections(self, compiled_manifold) -> None:
|
||||
"""Correction targets built from different prompts are geometrically distinct."""
|
||||
_, _, target_a = _build_manifold("light", compiled_manifold)
|
||||
_, _, target_b = _build_manifold("darkness", compiled_manifold)
|
||||
|
||||
# targets should differ
|
||||
dist = float(np.linalg.norm(
|
||||
target_a.astype(np.float64) - target_b.astype(np.float64)
|
||||
))
|
||||
assert dist > 1e-4, (
|
||||
f"Targets for 'light' and 'darkness' are identical (dist={dist:.2e})"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# V4 coupled loop integration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestCoupledPulse:
|
||||
def test_coupled_loop_converges(self) -> None:
|
||||
"""Full V4 pulse with correction converges and returns recall."""
|
||||
words = run_pulse(
|
||||
"what is truth",
|
||||
use_glove=False,
|
||||
use_correction=True,
|
||||
correction_rate=0.3,
|
||||
)
|
||||
assert len(words) > 0
|
||||
assert all(isinstance(w, str) for w in words)
|
||||
|
||||
def test_correction_changes_recall_vs_pure_diffusion(self) -> None:
|
||||
"""With correction enabled, recall may differ from pure-diffusion mode.
|
||||
|
||||
Both must return valid vocab words. We don't assert they differ
|
||||
(they may agree on some inputs), but both paths must complete.
|
||||
"""
|
||||
words_v3 = run_pulse(
|
||||
"wisdom", use_glove=False, use_correction=False,
|
||||
)
|
||||
words_v4 = run_pulse(
|
||||
"wisdom", use_glove=False, use_correction=True, correction_rate=0.3,
|
||||
)
|
||||
assert len(words_v3) > 0
|
||||
assert len(words_v4) > 0
|
||||
|
||||
def test_high_correction_rate_biases_toward_target(self, compiled_manifold) -> None:
|
||||
"""With correction_rate=0.9, the output node should be very close
|
||||
to the target versor after the loop.
|
||||
"""
|
||||
_, labels, target_versor = _build_manifold("hope", compiled_manifold)
|
||||
output_idx = len(labels) - 1
|
||||
|
||||
# Run manually to inspect the final output node.
|
||||
from algebra.backend import cga_inner
|
||||
|
||||
state, labels, target_versor = _build_manifold("hope", compiled_manifold)
|
||||
diffusion_op = GraphDiffusionOperator(damping=0.5)
|
||||
correction_op = ConstraintCorrectionOperator(
|
||||
target_versor=target_versor,
|
||||
correction_rate=0.9,
|
||||
node_index=-1,
|
||||
)
|
||||
|
||||
for _ in range(100):
|
||||
state, _ = diffusion_op.forward(state)
|
||||
state, _ = correction_op.adjoint_pass(state)
|
||||
|
||||
output = state.fields[output_idx].astype(np.float64)
|
||||
target = target_versor.astype(np.float64)
|
||||
dist = float(np.linalg.norm(output - target))
|
||||
# High correction rate should produce strong convergence toward target.
|
||||
assert dist < 0.5, (
|
||||
f"High correction_rate=0.9 did not pull output close to target: dist={dist:.4f}"
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue