core/field/operators.py
Shay eb30c75810 feat: Full Proof — surface realizer join, Rust diffusion parity, benchmark harness
Surface realizer join: pulse output_versor → vault recall → ground_graph fills
<pending> obj slots with recalled words → realize_semantic produces deterministic
sentences. PulseResult replaces bare word list. Every intent type surfaces.

Rust backend parity: unitize_f32 (exponential-map with boost/rotation blade
distinction) and graph_diffusion_step now in core-rs. Python dispatches through
algebra.backend, falls back transparently. 37x speedup on 200-step diffusion.

Benchmark harness (core bench): determinism (100% trace stability), latency
(~150ms median), backend speedup, versor closure audit (0 violations across all
intermediate states), convergence proof (41/45 exact, 4 bounded oscillation),
realizer coverage (8/8 intent types).

Proof property tests (31 tests): Rust/Python parity, pulse determinism across
prompts, V3 convergence for 10+ topologies, coupled V4 output validity, realizer
coverage per intent, versor closure at every intermediate step.

CLI: core pulse, core bench, core test --suite pulse, core test --suite proof.
Fix test_correction_pulls_toward_target (diffuse first, then correct).
2026-05-15 17:39:14 -07:00

289 lines
11 KiB
Python

"""
Manifold-level field operators — graph diffusion and dual-correction.
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
from collections import defaultdict
from typing import Protocol
import numpy as np
from algebra.backend import (
diffusion_step as _rust_diffusion_step,
unitize_expmap as _rust_unitize,
)
from algebra.cl41 import geometric_product, reverse
from field.state import ManifoldState
class Operator(Protocol):
"""Protocol for manifold field operators."""
def forward(self, state: ManifoldState) -> tuple[ManifoldState, float]:
"""Apply operator, return (new_state, delta_norm)."""
...
def adjoint(self) -> "Operator":
"""Return the adjoint operator."""
...
# ---------------------------------------------------------------------------
# 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 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})
def _unitize_f32(v: np.ndarray) -> np.ndarray:
"""Unitize a multivector to versor condition via the exponential map.
Builds a proper rotor from the bivector content, ensuring
R·reverse(R) = 1 exactly in float64, then casts to float32.
Uses the Rust backend when available for the hot path.
"""
rust_result = _rust_unitize(np.asarray(v, dtype=np.float32))
if rust_result is not None:
return rust_result
v64 = np.asarray(v, dtype=np.float64)
norm = float(np.linalg.norm(v64))
if norm < 1e-12:
out = np.zeros(32, dtype=np.float32)
out[0] = 1.0
return out
bv = v64[6:16]
bv_norm = float(np.linalg.norm(bv))
if bv_norm < 1e-14:
out = np.zeros(32, dtype=np.float32)
out[0] = 1.0 if v64[0] >= 0 else -1.0
return out
angle = np.arctan2(bv_norm, abs(float(v64[0])))
rotor = np.zeros(32, dtype=np.float64)
rotor[0] = 1.0
for i in range(10):
w = float(bv[i]) / bv_norm
if abs(w) < 1e-14:
continue
theta = angle * w
factor = np.zeros(32, dtype=np.float64)
blade_idx = 6 + i
if blade_idx in _BOOST_INDICES:
factor[0] = np.cosh(theta)
factor[blade_idx] = np.sinh(theta)
else:
factor[0] = np.cos(theta)
factor[blade_idx] = np.sin(theta)
rotor = geometric_product(rotor, factor)
if v64[0] < 0:
rotor = -rotor
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.
Self-adjoint: adjoint() returns self (symmetric diffusion).
For each node, computes a linear blend with its neighbors in the
32-component multivector space, then re-projects to the versor
manifold via the exponential map. The damping factor controls
the blend weight: 0 = no change, 1 = replace with neighbor average.
"""
def __init__(self, damping: float = 0.5) -> None:
if not 0.0 < damping <= 1.0:
raise ValueError(f"damping must be in (0, 1], got {damping}")
self._damping = damping
def forward(self, state: ManifoldState) -> tuple[ManifoldState, float]:
# Try Rust batch path first
rust_result = _rust_diffusion_step(state.fields, state.edges, self._damping)
if rust_result is not None:
new_fields, delta = rust_result
return ManifoldState(fields=new_fields, edges=state.edges, step=state.step + 1), delta
old_fields = state.fields
neighbors: dict[int, list[int]] = defaultdict(list)
for edge_idx in range(state.edges.shape[0]):
src, dst = int(state.edges[edge_idx, 0]), int(state.edges[edge_idx, 1])
neighbors[dst].append(src)
new_fields = old_fields.copy()
for node, srcs in neighbors.items():
f = old_fields[node].astype(np.float64)
neighbor_avg = np.mean(
[old_fields[s].astype(np.float64) for s in srcs], axis=0,
)
blended = (1.0 - self._damping) * f + self._damping * neighbor_avg
new_fields[node] = _unitize_f32(blended)
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":
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