core/algebra/backend.py
Shay b40422e9db perf(rust): versor_apply f64 parity port — 29x over Python, bit-identical
Closes the last open Rust parity gate from ADR-0020.

Kernel: new versor_apply_closed_f64 in core-rs/src/versor.rs performs
the full sandwich V·F·rev(V) + closure in f64, mirroring Python's
algebra.versor.versor_apply + _close_applied_versor exactly:
  - no null-vector early branch (Python doesn't have one)
  - unitize_versor with dense-support seed fallback gate
  - post-unitize versor_condition < 1e-6 recheck
  - seed_to_rotor on failure, passthrough as last resort

PyO3 binding: versor_apply_with_closure_f64 accepts/returns float64
arrays through new extract_f64_slice / f64_array_to_numpy helpers.
algebra/backend.py::versor_apply routes through it under CORE_BACKEND=rust.

Parity gate re-enabled (was skipped pending this port). 8/8 bit-
identical across normalized hot-path + identity-versor cases.

Bench (5000 iters, runtime hot path):
  python: 213.0 us/call
  rust:     7.4 us/call  → 28.8x speedup

All lanes green: algebra 132 (was 124+8skip), smoke 54, runtime 19,
cognition 57, teaching 17, packs 6. Cognition eval 100% across all metrics.

PROGRESS.md updated: versor_apply marked passing; Phase 5 Rust parity
track now 5/5 surfaces gated and enabled.
2026-05-16 20:43:01 -07:00

183 lines
6.5 KiB
Python

"""
Backend dispatch.
Pure Python is the deterministic default. Rust is an explicit opt-in backend
via CORE_BACKEND=rust/core_rs. This avoids silently bypassing Python-side
closure semantics when a local core_rs build happens to be importable.
Usage:
from algebra.backend import geometric_product, versor_apply, cga_inner, vault_recall
"""
import os
import numpy as np
_REQUESTED_BACKEND = os.environ.get("CORE_BACKEND", "").strip().lower()
_ALLOW_RUST = _REQUESTED_BACKEND in {"rust", "core_rs", "rs"}
try:
import core_rs as _rs
_RUST = _ALLOW_RUST
except ImportError:
_RUST = False
def _build_cga_inner_metric() -> np.ndarray:
"""Derive the Cl(4,1) inner-product metric vector from cga_inner.
For Cl(p,q) basis blades, e_i * e_j is scalar only when i == j, so
cga_inner(X, Y) reduces to a diagonal weighted dot product:
cga_inner(X, Y) = sum_i metric[i] * X[i] * Y[i]
where metric[i] = cga_inner(e_i, e_i) is ±1. Computing the metric
once at import time lets vault recall scan via vectorised NumPy
ops while preserving the scalar path's serial reduction order
bit-for-bit.
"""
from algebra.cga import cga_inner as _ci
from algebra.cl41 import N_COMPONENTS
metric = np.zeros(N_COMPONENTS, dtype=np.float32)
for i in range(N_COMPONENTS):
e_i = np.zeros(N_COMPONENTS, dtype=np.float32)
e_i[i] = 1.0
metric[i] = _ci(e_i, e_i)
return metric
_CGA_INNER_METRIC: np.ndarray = _build_cga_inner_metric()
def geometric_product(A: np.ndarray, B: np.ndarray) -> np.ndarray:
if _RUST:
return np.asarray(_rs.geometric_product(A, B), dtype=np.float32)
from algebra.cl41 import geometric_product as _gp
return _gp(A, B)
def versor_apply(V: np.ndarray, F: np.ndarray) -> np.ndarray:
"""Apply a versor through the canonical algebra closure boundary.
The Python implementation is the default source of truth for runtime
closure semantics. The Rust f64 closure path
(`versor_apply_with_closure_f64`) is a bit-identity port of
`algebra.versor.versor_apply` + `_close_applied_versor`; ADR-0020
parity gate `tests/test_versor_apply_rust_parity.py` proves the
swap is safe before this dispatch is enabled.
"""
if _RUST:
try:
Vc = np.ascontiguousarray(V, dtype=np.float64)
Fc = np.ascontiguousarray(F, dtype=np.float64)
return np.asarray(_rs.versor_apply_with_closure_f64(Vc, Fc), dtype=np.float64)
except (AttributeError, Exception):
pass
from algebra.versor import versor_apply as _va
return _va(V, F)
def versor_condition(F: np.ndarray) -> float:
if _RUST:
return float(_rs.versor_condition(F))
from algebra.versor import versor_condition as _vc
return _vc(F)
def cga_inner(X: np.ndarray, Y: np.ndarray) -> float:
if _RUST:
return float(_rs.cga_inner(X, Y))
from algebra.cga import cga_inner as _ci
return _ci(X, Y)
def vault_recall(versors: list, query: np.ndarray, top_k: int = 5) -> list:
"""Top-k CGA inner product recall.
Rust path: parallel Rayon scan when explicitly enabled.
Python path: vectorised exact scan via the diagonal CGA inner-
product metric. Bit-identical to the scalar `cga_inner` path
because the per-versor sum is folded in the same serial component
order; the only thing the vectorisation replaces is the
per-element Python dispatch loop. ADR-0019 Stage 1.
"""
if not versors:
return []
q = np.asarray(query, dtype=np.float32)
M = np.asarray(versors, dtype=np.float32)
if _RUST and M.ndim == 2 and M.shape[1] == 32:
try:
# Pass the (N, 32) numpy buffer directly — the Rust
# binding reads it zero-copy via PyReadonlyArray2 (task
# #35). ascontiguousarray ensures C-contiguous f32
# layout, which the zero-copy slice requires.
Mc = np.ascontiguousarray(M, dtype=np.float32)
qc = np.ascontiguousarray(q, dtype=np.float32)
return _rs.vault_recall(Mc, qc, top_k)
except Exception:
pass
if M.ndim != 2:
# Heterogeneous shapes — fall back to the scalar path rather
# than coerce silently.
scores_list = [(i, float(cga_inner(q, np.asarray(v)))) for i, v in enumerate(versors)]
scores_list.sort(key=lambda x: -x[1])
return scores_list[:top_k]
scores = np.zeros(M.shape[0], dtype=np.float32)
for i in range(M.shape[1]):
scores += (_CGA_INNER_METRIC[i] * M[:, i]) * q[i]
k = min(top_k, scores.shape[0])
if k <= 0:
return []
# argpartition gives unordered top-k; finalize the order with a
# stable sort by descending score, then ascending index for ties
# (mirrors the scalar path's stable enumerate order under
# list.sort with a strict key).
if k < scores.shape[0]:
cand = np.argpartition(-scores, k - 1)[:k]
else:
cand = np.arange(scores.shape[0])
# Stable order: primary key -scores ascending (= score descending),
# tiebreak ascending index to match scalar path's enumerate + stable
# list.sort ordering.
order = np.lexsort((cand, -scores[cand]))
cand = cand[order]
return [(int(i), float(scores[i])) for i in cand]
def unitize_expmap(v: np.ndarray) -> np.ndarray:
"""Unitize a multivector via the Cl(4,1) exponential map.
Distinguishes boost planes (cosh/sinh) from rotation planes (cos/sin).
Returns f32 array of length 32.
"""
if _RUST:
try:
return np.asarray(_rs.unitize_expmap(v), dtype=np.float32)
except (AttributeError, Exception):
pass
return None # caller must fall back to Python implementation
def diffusion_step(
fields: np.ndarray, edges: np.ndarray, damping: float,
) -> tuple[np.ndarray, float] | None:
"""One forward step of graph diffusion via Rust.
Returns (new_fields, delta) or None if Rust is unavailable or not explicitly enabled.
"""
if _RUST:
try:
n_nodes = fields.shape[0]
fields_flat = fields.astype(np.float32).flatten().tolist()
edges_flat = edges.astype(np.int32).flatten().tolist()
new_fields, delta = _rs.diffusion_step(
fields_flat, edges_flat, n_nodes, float(damping),
)
return np.asarray(new_fields, dtype=np.float32), float(delta)
except (AttributeError, Exception):
pass
return None
def using_rust() -> bool:
"""Returns True if the Rust extension is explicitly enabled and loaded."""
return _RUST