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:
Shay 2026-05-15 17:10:13 -07:00
parent 3b4fa242c6
commit 29f573d176
3 changed files with 457 additions and 56 deletions

View file

@ -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

View file

@ -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,
)

View file

@ -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}"
)