Activate and verify Rust backend

Add Rust backend CLI controls, fix core-rs build/test configuration, align Rust Cl(4,1)/CGA conventions with Python, and validate core_rs activation.
This commit is contained in:
Shay 2026-05-13 22:23:48 -07:00 committed by GitHub
parent 4ab148149f
commit df9ced7104
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 430 additions and 380 deletions

View file

@ -60,7 +60,7 @@ def vault_recall(versors: list, query: np.ndarray, top_k: int = 5) -> list:
except Exception:
pass
q = np.asarray(query)
scores = [(i, -float(np.sum((q - np.asarray(v)) ** 2))) for i, v in enumerate(versors)]
scores = [(i, float(cga_inner(q, np.asarray(v)))) for i, v in enumerate(versors)]
scores.sort(key=lambda x: -x[1])
return scores[:top_k]

View file

@ -5,10 +5,10 @@ edition = "2021"
[lib]
name = "core_rs"
crate-type = ["cdylib"]
crate-type = ["cdylib", "rlib"]
[dependencies]
pyo3 = { version = "0.21", features = ["extension-module"] }
pyo3 = { version = "0.21" }
rayon = "1.10"
nalgebra = "0.33"
ndarray = { version = "0.16", features = ["rayon"] }

View file

@ -5,4 +5,3 @@ build-backend = "maturin"
[tool.maturin]
features = ["extension-module"]
module-name = "core_rs"
python-source = "python"

View file

@ -1,7 +1,17 @@
//! CGA inner product and null-cone operations.
//!
//! cga_inner(X, Y) = 0.5 * scalar_part(X*Y + Y*X)
//! = -d^2 / 2 for null vectors X, Y
//! Signature: (+,+,+,-,+), with Euclidean coordinates on e1,e2,e3.
//! The conformal null directions are:
//!
//! n_o = 0.5 * (e4 - e5) # origin, n_o^2 = 0
//! n_inf = e4 + e5 # infinity, n_inf^2 = 0
//! n_o · n_inf = -1
//!
//! A Euclidean point x embeds as:
//!
//! X = x + n_o + 0.5 * |x|^2 * n_inf
//!
//! Then X·X = 0 and X·Y = -0.5 * ||x-y||^2.
//!
//! This is the ONLY distance metric in CORE-AI.
@ -20,37 +30,30 @@ pub enum CgaError {
pub fn cga_inner_raw(x: &[f32; 32], y: &[f32; 32]) -> Result<f32, CgaError> {
let xy = geometric_product_raw(x, y)?;
let yx = geometric_product_raw(y, x)?;
// scalar part is index 0
Ok(0.5 * (xy[0] + yx[0]))
}
/// Check if X is on the null cone: |X*X| < tol.
/// Check if X is on the null cone: |X·X| < tol.
pub fn is_null_raw(x: &[f32; 32], tol: f32) -> Result<bool, CgaError> {
Ok(cga_inner_raw(x, x)?.abs() < tol)
}
/// Re-project X onto the null cone.
/// Extract Euclidean components (indices 1-3), recompute e+ = 0.5*|x|^2, e- = 1.
/// Re-project X onto the null cone by extracting Euclidean components
/// and re-embedding with the canonical CGA point map.
pub fn null_project_raw(x: &[f32; 32]) -> [f32; 32] {
let mut result = [0f32; 32];
result[1] = x[1];
result[2] = x[2];
result[3] = x[3];
let x_sq = result[1] * result[1] + result[2] * result[2] + result[3] * result[3];
result[4] = 0.5 * x_sq; // e+ coefficient
result[5] = 1.0; // e- coefficient
result
embed_point_raw(&[x[1], x[2], x[3]])
}
/// Embed a Euclidean point [x, y, z] into the CGA null cone.
/// X = x*e1 + y*e2 + z*e3 + (1/2)|x|^2 * e+ + e-
/// X = x + n_o + 0.5|x|^2 n_inf
/// where n_o = 0.5(e4 - e5), n_inf = e4 + e5.
pub fn embed_point_raw(p: &[f32; 3]) -> [f32; 32] {
let mut result = [0f32; 32];
result[1] = p[0];
result[2] = p[1];
result[3] = p[2];
let r2 = p[0] * p[0] + p[1] * p[1] + p[2] * p[2];
result[4] = 0.5 * r2;
result[5] = 1.0;
result[4] = 0.5 * (r2 + 1.0);
result[5] = 0.5 * (r2 - 1.0);
result
}

View file

@ -1,6 +1,6 @@
//! Cl(4,1) geometric product via fully unrolled precomputed table.
//! Cl(4,1) geometric product via precomputed table.
//!
//! Signature: (+,+,+,+,-). 32-component f32 multivectors.
//! Signature: (+,+,+,-,+). 32-component f32 multivectors.
//! The multiplication table is computed once at program start using
//! const evaluation and stored as two [u8;1024] and [i8;1024] arrays
//! (index and sign for each of the 32x32 blade pairs).
@ -20,10 +20,8 @@ pub enum Cl41Error {
// We encode each blade as a bitmask over 5 bits (bit k = basis vector k+1 present)
// The mapping from bitmask to component index follows grade-ascending, lex order.
const N: usize = 32;
// Signature: e1^2=+1, e2^2=+1, e3^2=+1, e4^2=+1, e5^2=-1
const SIG: [i8; 5] = [1, 1, 1, 1, -1];
// Signature: e1^2=+1, e2^2=+1, e3^2=+1, e4^2=-1, e5^2=+1
const SIG: [i8; 5] = [1, 1, 1, -1, 1];
// Precomputed at compile time via const fn
const BLADE_MASKS: [u8; 32] = build_blade_masks();
@ -68,62 +66,42 @@ const fn popcount5(x: u8) -> u8 {
}
// Multiply two basis blades given as bitmasks. Returns (result_mask, sign).
// Uses bubble-sort on the concatenated index list, tracking swaps and metric contractions.
// The sign is the parity of swaps needed to canonicalize A followed by B,
// multiplied by the metric contractions for repeated basis vectors.
const fn blade_product(a: u8, b: u8) -> (u8, i8) {
// Expand masks into sorted index sequences
let mut seq = [0u8; 10];
let mut len = 0usize;
let mut bit = 0u8;
while bit < 5 {
if (a >> bit) & 1 == 1 { seq[len] = bit; len += 1; }
bit += 1;
}
bit = 0;
while bit < 5 {
if (b >> bit) & 1 == 1 { seq[len] = bit; len += 1; }
bit += 1;
}
let mut sign: i8 = 1;
// Bubble sort + contract duplicates
let mut changed = true;
while changed {
changed = false;
let mut i = 0usize;
while i + 1 < len {
if seq[i] == seq[i + 1] {
// Contract: e_k^2 = SIG[k]
sign *= SIG[seq[i] as usize];
// Remove both elements at i and i+1
let mut j = i;
while j + 2 < len { seq[j] = seq[j + 2]; j += 1; }
len -= 2;
changed = true;
if i > 0 { i -= 1; } // re-check from one step back
} else if seq[i] > seq[i + 1] {
let tmp = seq[i]; seq[i] = seq[i + 1]; seq[i + 1] = tmp;
// Anticommutation sign: every pair (a_i, b_j) with a_i > b_j swaps once.
let mut swaps = 0u8;
let mut ai = 0u8;
while ai < 5 {
if (a >> ai) & 1 == 1 {
let mut bj = 0u8;
while bj < 5 {
if (b >> bj) & 1 == 1 && ai > bj {
swaps += 1;
}
bj += 1;
}
}
ai += 1;
}
if swaps % 2 == 1 {
sign *= -1;
changed = true;
i += 1;
} else {
i += 1;
}
}
}
// Build result mask
let mut result = 0u8;
let mut i = 0usize;
while i < len { result |= 1 << seq[i]; i += 1; }
(result, sign)
// Metric contractions for duplicate basis vectors.
let common = a & b;
let mut bit = 0u8;
while bit < 5 {
if (common >> bit) & 1 == 1 {
sign *= SIG[bit as usize];
}
bit += 1;
}
// Full 32x32 product table — computed once at startup (not const due to complexity)
// TABLE_IDX[i][j] = component index of blade_i * blade_j
// TABLE_SIGN[i][j] = sign (+1 or -1) of blade_i * blade_j
(a ^ b, sign)
}
struct Table {
idx: [[u8; 32]; 32],

View file

@ -8,33 +8,27 @@
//! - vault_recall (parallel top-k scan)
//!
//! All multivectors are f32 arrays of length 32, passed as numpy arrays.
//! Zero-copy: we read directly from numpy buffer pointers.
use pyo3::prelude::*;
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
mod cl41;
mod versor;
mod cga;
mod vault;
pub mod cga;
pub mod cl41;
pub mod vault;
pub mod versor;
use cl41::{geometric_product_raw, reverse_raw};
use versor::{versor_apply_raw, versor_condition_raw, normalize_to_versor_raw};
use cga::cga_inner_raw;
use cl41::geometric_product_raw;
use vault::vault_recall_raw;
use versor::{normalize_to_versor_raw, versor_apply_raw, versor_condition_raw};
// Re-export Python-facing functions
/// Geometric product in Cl(4,1). Accepts two numpy f32 arrays of length 32.
/// Geometric product in Cl(4,1). Accepts two numpy-compatible f32 arrays of length 32.
#[pyfunction]
fn geometric_product<'py>(
py: Python<'py>,
fn geometric_product(
py: Python<'_>,
a: &pyo3::types::PyAny,
b: &pyo3::types::PyAny,
) -> PyResult<pyo3::Bound<'py, pyo3::types::PyAny>> {
let a_buf = a.call_method0("__array__")?;
let b_buf = b.call_method0("__array__")?;
// Extract raw f32 slices via buffer protocol
) -> PyResult<PyObject> {
let a_slice = extract_f32_slice(a)?;
let b_slice = extract_f32_slice(b)?;
let result = geometric_product_raw(&a_slice, &b_slice)
@ -42,13 +36,13 @@ fn geometric_product<'py>(
f32_array_to_numpy(py, &result)
}
/// Sandwich product V*F*reverse(V). Zero-copy on input arrays.
/// Sandwich product V*F*reverse(V).
#[pyfunction]
fn versor_apply<'py>(
py: Python<'py>,
fn versor_apply(
py: Python<'_>,
v: &pyo3::types::PyAny,
f: &pyo3::types::PyAny,
) -> PyResult<pyo3::Bound<'py, pyo3::types::PyAny>> {
) -> PyResult<PyObject> {
let v_slice = extract_f32_slice(v)?;
let f_slice = extract_f32_slice(f)?;
let result = versor_apply_raw(&v_slice, &f_slice)
@ -65,10 +59,10 @@ fn versor_condition(f: &pyo3::types::PyAny) -> PyResult<f32> {
/// Project F onto versor manifold: F / sqrt(|F*rev(F)|).
#[pyfunction]
fn normalize_to_versor<'py>(
py: Python<'py>,
fn normalize_to_versor(
py: Python<'_>,
f: &pyo3::types::PyAny,
) -> PyResult<pyo3::Bound<'py, pyo3::types::PyAny>> {
) -> PyResult<PyObject> {
let f_slice = extract_f32_slice(f)?;
let result = normalize_to_versor_raw(&f_slice)
.map_err(|e| PyValueError::new_err(e.to_string()))?;
@ -84,9 +78,6 @@ fn cga_inner(x: &pyo3::types::PyAny, y: &pyo3::types::PyAny) -> PyResult<f32> {
}
/// Parallel top-k vault recall by CGA inner product.
/// versors: list of numpy f32 arrays (length 32 each)
/// query: numpy f32 array (length 32)
/// Returns: list of (index, score) sorted descending by score.
#[pyfunction]
fn vault_recall(
versors: Vec<&pyo3::types::PyAny>,
@ -96,42 +87,35 @@ fn vault_recall(
let query_slice = extract_f32_slice(query)?;
let mut slices: Vec<[f32; 32]> = Vec::with_capacity(versors.len());
for v in &versors {
let s = extract_f32_slice(v)?;
slices.push(s);
slices.push(extract_f32_slice(v)?);
}
vault_recall_raw(&slices, &query_slice, top_k)
.map_err(|e| PyValueError::new_err(e.to_string()))
}
// --- Helpers ---
fn extract_f32_slice(obj: &pyo3::types::PyAny) -> PyResult<[f32; 32]> {
// Use numpy's buffer protocol for zero-copy read
let np = obj.py().import("numpy")?;
let arr = np.call_method1("asarray", (obj, "float32"))?;
let flat = arr.call_method0("flatten")?;
let list: Vec<f32> = flat.extract()?;
if list.len() != 32 {
return Err(PyValueError::new_err(
format!("Expected array of length 32, got {}", list.len())
));
return Err(PyValueError::new_err(format!(
"Expected array of length 32, got {}",
list.len()
)));
}
let mut out = [0f32; 32];
out.copy_from_slice(&list);
Ok(out)
}
fn f32_array_to_numpy<'py>(
py: Python<'py>,
data: &[f32; 32],
) -> PyResult<pyo3::Bound<'py, pyo3::types::PyAny>> {
fn f32_array_to_numpy(py: Python<'_>, data: &[f32; 32]) -> PyResult<PyObject> {
let np = py.import("numpy")?;
let list: Vec<f32> = data.to_vec();
let arr = np.call_method1("array", (list, "float32"))?;
Ok(arr.into())
Ok(arr.into_py(py))
}
/// Module registration
#[pymodule]
fn core_rs(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(geometric_product, m)?)?;

View file

@ -1,6 +1,4 @@
#[cfg(test)]
mod tests {
use crate::cga::{cga_inner_raw, embed_point_raw, is_null_raw, null_project_raw};
use core_rs::cga::{cga_inner_raw, embed_point_raw, is_null_raw, null_project_raw};
#[test]
fn test_embedded_point_is_null() {
@ -26,23 +24,18 @@ mod tests {
#[test]
fn test_cga_distance_identity() {
// Points at distance 1: cga_inner = -d^2/2 = -0.5
let x = embed_point_raw(&[0.0, 0.0, 0.0]);
let y = embed_point_raw(&[1.0, 0.0, 0.0]);
let inner = cga_inner_raw(&x, &y).unwrap();
assert!((inner - (-0.5)).abs() < 1e-5,
"Expected -0.5 for unit-distance points, got {}", inner);
assert!((inner - (-0.5)).abs() < 1e-5, "Expected -0.5 for unit-distance points, got {}", inner);
}
#[test]
fn test_null_project_restores_null() {
let p = [1.0f32, 2.0, 3.0];
let mut x = embed_point_raw(&p);
// Introduce drift
x[0] += 0.05;
x[7] -= 0.03;
let fixed = null_project_raw(&x);
assert!(is_null_raw(&fixed, 1e-5).unwrap(),
"null_project failed to restore null cone");
}
assert!(is_null_raw(&fixed, 1e-5).unwrap(), "null_project failed to restore null cone");
}

View file

@ -1,10 +1,8 @@
#[cfg(test)]
mod tests {
use crate::cl41::{geometric_product_raw, reverse_raw, BLADE_MASKS, MASK_TO_IDX};
use core_rs::cl41::{geometric_product_raw, reverse_raw};
fn basis(i: usize) -> [f32; 32] {
let mut v = [0f32; 32];
v[1 + i] = 1.0; // grade-1 component
v[1 + i] = 1.0;
v
}
@ -18,16 +16,21 @@ mod tests {
fn test_e1_squared_is_plus1() {
let e1 = basis(0);
let r = geometric_product_raw(&e1, &e1).unwrap();
// e1^2 = +1 (signature index 0 = +1)
assert!((r[0] - 1.0).abs() < 1e-6, "e1^2 should be +1, got {}", r[0]);
}
#[test]
fn test_e5_squared_is_minus1() {
fn test_e4_squared_is_minus1() {
let e4 = basis(3);
let r = geometric_product_raw(&e4, &e4).unwrap();
assert!((r[0] + 1.0).abs() < 1e-6, "e4^2 should be -1, got {}", r[0]);
}
#[test]
fn test_e5_squared_is_plus1() {
let e5 = basis(4);
let r = geometric_product_raw(&e5, &e5).unwrap();
// e5^2 = -1 (signature index 4 = -1)
assert!((r[0] + 1.0).abs() < 1e-6, "e5^2 should be -1, got {}", r[0]);
assert!((r[0] - 1.0).abs() < 1e-6, "e5^2 should be +1, got {}", r[0]);
}
#[test]
@ -36,10 +39,8 @@ mod tests {
let e2 = basis(1);
let e1e2 = geometric_product_raw(&e1, &e2).unwrap();
let e2e1 = geometric_product_raw(&e2, &e1).unwrap();
// e1*e2 = -e2*e1
for i in 0..32 {
assert!((e1e2[i] + e2e1[i]).abs() < 1e-6,
"e1*e2 + e2*e1 != 0 at index {}", i);
assert!((e1e2[i] + e2e1[i]).abs() < 1e-6, "e1*e2 + e2*e1 != 0 at index {}", i);
}
}
@ -53,9 +54,8 @@ mod tests {
#[test]
fn test_reverse_grade2_sign() {
// A grade-2 blade should flip sign under reverse
let mut a = [0f32; 32];
a[6] = 1.0; // first grade-2 component
a[6] = 1.0;
let r = reverse_raw(&a);
assert!((r[6] + 1.0).abs() < 1e-6, "reverse of grade-2 blade should negate");
}
@ -66,4 +66,3 @@ mod tests {
let r = reverse_raw(&e1);
assert!((r[1] - 1.0).abs() < 1e-6, "reverse of grade-1 blade should be unchanged");
}
}

View file

@ -1,50 +1,43 @@
#[cfg(test)]
mod tests {
use crate::vault::vault_recall_raw;
use crate::versor::normalize_to_versor_raw;
use core_rs::vault::vault_recall_raw;
use core_rs::cga::embed_point_raw;
fn random_versor(seed: u64) -> [f32; 32] {
let mut state = seed ^ 0xdeadbeef_cafebabe;
let mut raw = [0f32; 32];
for x in raw.iter_mut() {
state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
*x = ((state >> 33) as f32) / (u32::MAX as f32) * 2.0 - 1.0;
}
normalize_to_versor_raw(&raw).expect("normalize failed")
fn sample_point(seed: u64) -> [f32; 32] {
let x = ((seed * 17 + 3) % 101) as f32 / 10.0;
let y = ((seed * 23 + 7) % 101) as f32 / 10.0;
let z = ((seed * 31 + 11) % 101) as f32 / 10.0;
embed_point_raw(&[x, y, z])
}
#[test]
fn test_recall_self() {
let versors: Vec<[f32; 32]> = (0..20).map(|i| random_versor(i as u64)).collect();
let versors: Vec<[f32; 32]> = (0..20).map(|i| sample_point(i as u64)).collect();
for (i, query) in versors.iter().enumerate() {
let results = vault_recall_raw(&versors, query, 1).unwrap();
assert_eq!(results[0].0, i,
"Versor {} should recall itself as top-1, got {}", i, results[0].0);
assert_eq!(results[0].0, i);
}
}
#[test]
fn test_empty_vault() {
let query = random_versor(0);
let query = sample_point(0);
let results = vault_recall_raw(&[], &query, 5).unwrap();
assert!(results.is_empty());
}
#[test]
fn test_top_k_count() {
let versors: Vec<[f32; 32]> = (0..10).map(|i| random_versor(i as u64)).collect();
let query = random_versor(99);
let versors: Vec<[f32; 32]> = (0..10).map(|i| sample_point(i as u64)).collect();
let query = sample_point(99);
let results = vault_recall_raw(&versors, &query, 3).unwrap();
assert_eq!(results.len(), 3);
}
#[test]
fn test_scores_descending() {
let versors: Vec<[f32; 32]> = (0..10).map(|i| random_versor(i as u64)).collect();
let query = random_versor(99);
let versors: Vec<[f32; 32]> = (0..10).map(|i| sample_point(i as u64)).collect();
let query = sample_point(99);
let results = vault_recall_raw(&versors, &query, 5).unwrap();
for w in results.windows(2) {
assert!(w[0].1 >= w[1].1, "Scores not descending");
}
assert!(w[0].1 >= w[1].1);
}
}

View file

@ -1,16 +1,17 @@
#[cfg(test)]
mod tests {
use crate::versor::{versor_apply_raw, normalize_to_versor_raw, versor_condition_raw};
use core_rs::versor::{normalize_to_versor_raw, versor_apply_raw, versor_condition_raw};
fn random_versor(seed: u64) -> [f32; 32] {
// Simple LCG for deterministic test data — no external deps
let mut state = seed ^ 0xdeadbeef_cafebabe;
let mut raw = [0f32; 32];
for x in raw.iter_mut() {
state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
*x = ((state >> 33) as f32) / (u32::MAX as f32) * 2.0 - 1.0;
}
normalize_to_versor_raw(&raw).expect("normalize failed")
let theta = ((seed * 17 + 3) % 101) as f32 / 100.0;
let mut v = [0f32; 32];
v[0] = theta.cos();
// Choose a bivector with negative square, e.g. e12
// e1^2 = 1, e2^2 = 1 => (e1 e2)^2 = -e1^2 e2^2 = -1
// MASK_TO_IDX for e1e2: e1 is bit 0, e2 is bit 1 => mask 3
// MASK_TO_IDX[3] = 6 (grade 2 starts at 6)
v[6] = theta.sin();
v
}
#[test]
@ -29,8 +30,7 @@ mod tests {
let f = random_versor(seed + 1000);
let result = versor_apply_raw(&v, &f).unwrap();
let cond = versor_condition_raw(&result).unwrap();
assert!(cond < 1e-4,
"versor_apply broke manifold: condition={:.2e} at seed={}", cond, seed);
assert!(cond < 1e-4, "versor_apply broke manifold: condition={:.2e} at seed={}", cond, seed);
}
}
@ -41,8 +41,7 @@ mod tests {
let f = random_versor(42);
let result = versor_apply_raw(&identity, &f).unwrap();
for i in 0..32 {
assert!((result[i] - f[i]).abs() < 1e-5,
"Identity apply changed component {}: {} vs {}", i, result[i], f[i]);
assert!((result[i] - f[i]).abs() < 1e-5, "Identity apply changed component {}: {} vs {}", i, result[i], f[i]);
}
}
@ -56,4 +55,3 @@ mod tests {
let cond = versor_condition_raw(&f3).unwrap();
assert!(cond < 1e-4, "Composition broke manifold: condition={:.2e}", cond);
}
}

View file

@ -3,6 +3,7 @@ from __future__ import annotations
import argparse
import json
import shutil
import subprocess
import sys
from collections.abc import Sequence
@ -18,14 +19,16 @@ _REPO_ROOT = Path(__file__).resolve().parent.parent
if str(_REPO_ROOT) not in sys.path:
sys.path.insert(0, str(_REPO_ROOT))
_CORE_RS_DIR = _REPO_ROOT / "core-rs"
_CORE_RS_MANIFEST = _CORE_RS_DIR / "Cargo.toml"
DESCRIPTION = "CORE versor engine command suite."
EPILOG = "Examples:\n core chat\n core trace \"word beginning truth\"\n core trace --output-language grc --frame-pack grc --json \"logos\"\n core oov covenant\n core pack list\n core pack verify en_minimal_v1\n core test tests/test_alignment_graph.py -q"
EPILOG = "Examples:\n core chat\n core trace \"word beginning truth\"\n core trace --output-language grc --frame-pack grc --json \"logos\"\n core rust status\n core rust build\n core oov covenant\n core pack list\n core pack verify en_minimal_v1\n core test tests/test_alignment_graph.py -q"
def _run(*args: str, check: bool = False) -> int:
def _run(*args: str, check: bool = False, cwd: Path | None = None) -> int:
"""Run a child command and return its exit code."""
completed = subprocess.run(args, check=check, text=True)
completed = subprocess.run(args, check=check, text=True, cwd=cwd)
return int(completed.returncode)
@ -260,6 +263,56 @@ def cmd_pack_verify(args: argparse.Namespace) -> int:
return _run(sys.executable, "-m", "language_packs", "verify", args.pack_id)
def _print_rust_status() -> bool:
from algebra.backend import using_rust
active = using_rust()
print(f"core_rs crate : {_CORE_RS_DIR}")
print(f"cargo manifest: {_CORE_RS_MANIFEST}")
print(f"rust backend : {'active' if active else 'inactive'}")
if active:
import core_rs
print(f"core_rs module: {getattr(core_rs, '__file__', '<built-in>')}")
else:
print("activation : run `core rust build`")
return active
def cmd_rust_status(args: argparse.Namespace) -> int:
"""Print Rust backend activation status."""
return 0 if _print_rust_status() or not args.require_active else 1
def cmd_rust_build(args: argparse.Namespace) -> int:
"""Build/install core_rs into the active Python environment."""
if not _CORE_RS_MANIFEST.exists():
_die(f"core-rs manifest not found: {_CORE_RS_MANIFEST}", code=1)
if shutil.which("uv") is not None:
rc = _run("uv", "pip", "install", "maturin")
if rc != 0:
return rc
cmd = [
sys.executable,
"-m",
"maturin",
"develop",
"--release",
"--manifest-path",
str(_CORE_RS_MANIFEST),
]
if args.skip_auditwheel:
cmd.append("--skip-auditwheel")
return _run(*cmd)
def cmd_rust_test(args: argparse.Namespace) -> int:
"""Run Rust crate tests."""
if shutil.which("cargo") is None:
_die("cargo not found. Install a Rust toolchain first.", code=1)
return _run("cargo", "test", "--release", cwd=_CORE_RS_DIR)
def cmd_doctor(args: argparse.Namespace) -> int:
"""Inspect import/package health for the CLI runtime path."""
checks = [
@ -296,6 +349,10 @@ def cmd_doctor(args: argparse.Namespace) -> int:
print(f" {pack_id}")
else:
print(" none found")
if args.rust:
rust_active = _print_rust_status()
if args.require_rust and not rust_active:
ok = False
return 0 if ok else 1
@ -361,8 +418,25 @@ def build_parser() -> argparse.ArgumentParser:
pack_verify.add_argument("pack_id", help="pack id, e.g. en_minimal_v1")
pack_verify.set_defaults(func=cmd_pack_verify)
rust = subparsers.add_parser(
"rust",
help="build, test, and inspect the Rust backend",
description="build, test, and inspect the Rust backend",
)
rust_sub = rust.add_subparsers(dest="rust_command", metavar="rust-command", required=True)
rust_status = rust_sub.add_parser("status", help="show whether core_rs is active")
rust_status.add_argument("--require-active", action="store_true", help="exit nonzero if core_rs is inactive")
rust_status.set_defaults(func=cmd_rust_status)
rust_build = rust_sub.add_parser("build", help="build/install core_rs with maturin")
rust_build.add_argument("--skip-auditwheel", action="store_true", help="pass --skip-auditwheel to maturin")
rust_build.set_defaults(func=cmd_rust_build)
rust_test = rust_sub.add_parser("test", help="run cargo test --release for core-rs")
rust_test.set_defaults(func=cmd_rust_test)
doctor = subparsers.add_parser("doctor", help="check runtime imports and packaging health")
doctor.add_argument("--packs", action="store_true", help="also list discovered language packs")
doctor.add_argument("--rust", action="store_true", help="also show Rust backend activation status")
doctor.add_argument("--require-rust", action="store_true", help="exit nonzero when --rust shows inactive backend")
doctor.set_defaults(func=cmd_doctor)
return parser

View file

@ -4,78 +4,81 @@
Three operations dominate CORE-AI's runtime:
1. geometric_product — O(32^2) = 1024 multiply-adds per call, called 2-3x per versor_apply
2. vault_recall scan — O(N) cga_inner calls, N = all stored versors, called once per token
3. holonomy_encode — 2 * prompt_length geometric products in sequence
1. `geometric_product` — O(32^2) = 1024 multiply-adds per call, called 2-3x per `versor_apply`
2. `vault_recall` scan — O(N) CGA inner product calls, N = all stored versors, called during generation recall
3. `cga_inner` — called by vocabulary/proposition nearest selection and vault recall
None of these release the Python GIL. Rayon gives vault_recall true multithreaded
parallelism across CPU cores. The geometric product loop is cache-friendly and
compiler-autovectorized when opt-level=3 + lto=true.
None of the Python fallback paths release the Python GIL. Rayon gives `vault_recall` true multithreaded parallelism across CPU cores. The geometric product loop is cache-friendly and compiler-optimized in release mode.
## What is in Rust
| Module | Rust file | Why |
|---|---|---|
| Cl(4,1) product | cl41.rs | Hot inner loop, 1024 MADs, autovectorizable |
| Versor ops | versor.rs | 3x geometric_product per step, allocation-free |
| CGA inner product | cga.rs | Called every token decode and every vault recall |
| Vault top-k scan | vault.rs | Rayon parallel scan — GIL blocks Python threads |
| Holonomy encode | holonomy.rs | 200+ products for long prompts |
| Batch propagation | propagate.rs | Beam search / speculative decode |
| Cl(4,1) product | `cl41.rs` | Hot inner loop, 1024 MADs |
| Versor ops | `versor.rs` | 3x geometric product per field step |
| CGA inner product | `cga.rs` | Called by nearest search and recall |
| Vault top-k scan | `vault.rs` | Rayon parallel scan |
## What stays in Python
| Layer | Why |
|---|---|
| VocabManifold | Word lookup, edge rotor construction — called once per token, not per step |
| SessionContext | Orchestration, not arithmetic |
| FieldState | Plain dataclass |
| PersonaMotor | Motor construction is infrequent |
| `VocabManifold` | Word/morphology/language metadata and exact candidate filtering |
| `SessionContext` | Orchestration, not arithmetic |
| `FieldState` | Plain dataclass |
| `PersonaMotor` | Motor construction is infrequent |
## Zero-Copy Semantics
All f32 arrays are passed as numpy arrays from Python.
The Rust functions receive them as `[f32; 32]` stack arrays — copied once from
the numpy buffer into a stack frame, processed, and returned as a new numpy array.
No heap allocation inside any hot-path function.
The runtime contract is numpy `float32` arrays of length 32. Rust reads them into stack `[f32; 32]` values, executes the hot loop, and returns a new numpy array. The Python fallback remains behaviorally available when `core_rs` is not installed.
For vault_recall, the versors list is iterated via Rayon par_iter with no cloning:
each worker holds a read-only reference to its slice element.
## Build / Activate
## Build
Requires maturin and a Rust toolchain (stable 1.75+).
Requires a Rust toolchain and maturin. Prefer the uv-native flow so the repo does not depend on `pip` being installed inside `.venv`:
```bash
cd core-rs
pip install maturin
maturin develop --release # installs core_rs into current venv
core rust status
core rust test
core rust build
core rust status --require-active
```
Or build a wheel:
Equivalent explicit maturin command:
```bash
maturin build --release
pip install target/wheels/*.whl
uv run --with maturin maturin develop --release --manifest-path core-rs/Cargo.toml
```
Verify Rust backend is active from Python:
```python
from algebra.backend import using_rust
print(using_rust()) # True if core_rs is installed
```bash
uv run python -c "from algebra.backend import using_rust; print(using_rust())"
```
Expected:
```text
True
```
## Running Rust Tests
```bash
cd core-rs
cargo test
core rust test
# or
cargo test --release --manifest-path core-rs/Cargo.toml
```
## Type Safety Contract
All multivectors entering the Rust layer are validated as f32 arrays of length 32
by extract_f32_slice() in lib.rs. Type errors surface immediately as Python
ValueError with a descriptive message rather than silent memory corruption.
All multivectors entering the Rust layer must be numpy-compatible `float32` arrays of length 32. Type errors surface as Python `ValueError` rather than silent memory corruption.
All error types use thiserror — every failure path is a named enum variant,
not a string panic.
## Failure Mode
If `core_rs` is absent or fails to import, `algebra.backend` silently falls back to Python. This keeps the engine correct but not mechanically optimal. Use:
```bash
core doctor --rust --require-rust
```
to fail fast when benchmarking or profiling requires the Rust backend.

View file

@ -12,6 +12,7 @@ def test_top_level_help_exits_without_runtime_import(capsys: pytest.CaptureFixtu
out = capsys.readouterr().out
assert "CORE versor engine command suite" in out
assert "core trace" in out
assert "core rust" in out
def test_trace_help_exits_without_runtime_import(capsys: pytest.CaptureFixture[str]) -> None:
@ -26,6 +27,25 @@ def test_trace_help_exits_without_runtime_import(capsys: pytest.CaptureFixture[s
assert "--json" in out
def test_rust_help_exits_without_building(capsys: pytest.CaptureFixture[str]) -> None:
with pytest.raises(SystemExit) as excinfo:
build_parser().parse_args(["rust", "-h"])
assert excinfo.value.code == 0
out = capsys.readouterr().out
assert "build, test, and inspect the Rust backend" in out
assert "status" in out
assert "build" in out
assert "test" in out
def test_rust_status_reports_backend_state(capsys: pytest.CaptureFixture[str]) -> None:
assert main(["rust", "status"]) in {0, 1}
out = capsys.readouterr().out
assert "core_rs crate" in out
assert "cargo manifest" in out
assert "rust backend" in out
def test_main_without_args_prints_help(capsys: pytest.CaptureFixture[str]) -> None:
assert main([]) == 0
out = capsys.readouterr().out
@ -49,6 +69,12 @@ def test_doctor_imports_runtime_support_modules(capsys: pytest.CaptureFixture[st
assert "OK sensorium" in out
def test_doctor_rust_reports_backend_state(capsys: pytest.CaptureFixture[str]) -> None:
assert main(["doctor", "--rust"]) == 0
out = capsys.readouterr().out
assert "rust backend" in out
def test_trace_formats_real_runtime_payload(capsys: pytest.CaptureFixture[str]) -> None:
assert main(["trace", "--pack", "en_minimal_v1", "word", "beginning", "truth"]) == 0
out = capsys.readouterr().out

View file

@ -1,32 +1,31 @@
import numpy as np
import pytest
from algebra.versor import unitize_versor
from algebra.cga import is_null
from algebra.cga import embed_point, is_null
from vault.store import VaultStore
def _random_versor(seed=0) -> np.ndarray:
def _random_point(seed=0) -> np.ndarray:
rng = np.random.default_rng(seed)
return unitize_versor(rng.standard_normal(32).astype(np.float32))
return embed_point(rng.standard_normal(3).astype(np.float32))
def test_store_and_recall_top1():
"""Each stored versor should recall itself as the top result."""
"""Each stored point should recall itself as the top result."""
vault = VaultStore()
versors = [_random_versor(i) for i in range(20)]
for i, v in enumerate(versors):
points = [_random_point(i) for i in range(20)]
for i, v in enumerate(points):
vault.store(v, {"id": i})
for i, v in enumerate(versors):
for i, v in enumerate(points):
results = vault.recall(v, top_k=1)
assert results[0]["metadata"]["id"] == i, (
f"Versor {i} did not recall itself as top result"
f"Point {i} did not recall itself as top result"
)
def test_recall_empty_vault():
vault = VaultStore()
result = vault.recall(_random_versor(), top_k=5)
result = vault.recall(_random_point(), top_k=5)
assert result == []
@ -34,7 +33,7 @@ def test_reproject_maintains_structure():
"""Reproject should not lose stored entries."""
vault = VaultStore()
for i in range(10):
vault.store(_random_versor(i), {"id": i})
vault.store(_random_point(i), {"id": i})
vault.reproject()
assert len(vault) == 10
@ -42,5 +41,6 @@ def test_reproject_maintains_structure():
def test_vault_len():
vault = VaultStore()
for i in range(5):
vault.store(_random_versor(i))
vault.store(_random_point(i))
assert len(vault) == 5