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:
parent
4ab148149f
commit
df9ced7104
14 changed files with 430 additions and 380 deletions
|
|
@ -60,7 +60,7 @@ def vault_recall(versors: list, query: np.ndarray, top_k: int = 5) -> list:
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
q = np.asarray(query)
|
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])
|
scores.sort(key=lambda x: -x[1])
|
||||||
return scores[:top_k]
|
return scores[:top_k]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,10 @@ edition = "2021"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "core_rs"
|
name = "core_rs"
|
||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
pyo3 = { version = "0.21", features = ["extension-module"] }
|
pyo3 = { version = "0.21" }
|
||||||
rayon = "1.10"
|
rayon = "1.10"
|
||||||
nalgebra = "0.33"
|
nalgebra = "0.33"
|
||||||
ndarray = { version = "0.16", features = ["rayon"] }
|
ndarray = { version = "0.16", features = ["rayon"] }
|
||||||
|
|
|
||||||
|
|
@ -5,4 +5,3 @@ build-backend = "maturin"
|
||||||
[tool.maturin]
|
[tool.maturin]
|
||||||
features = ["extension-module"]
|
features = ["extension-module"]
|
||||||
module-name = "core_rs"
|
module-name = "core_rs"
|
||||||
python-source = "python"
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,17 @@
|
||||||
//! CGA inner product and null-cone operations.
|
//! CGA inner product and null-cone operations.
|
||||||
//!
|
//!
|
||||||
//! cga_inner(X, Y) = 0.5 * scalar_part(X*Y + Y*X)
|
//! Signature: (+,+,+,-,+), with Euclidean coordinates on e1,e2,e3.
|
||||||
//! = -d^2 / 2 for null vectors X, Y
|
//! 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.
|
//! 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> {
|
pub fn cga_inner_raw(x: &[f32; 32], y: &[f32; 32]) -> Result<f32, CgaError> {
|
||||||
let xy = geometric_product_raw(x, y)?;
|
let xy = geometric_product_raw(x, y)?;
|
||||||
let yx = geometric_product_raw(y, x)?;
|
let yx = geometric_product_raw(y, x)?;
|
||||||
// scalar part is index 0
|
|
||||||
Ok(0.5 * (xy[0] + yx[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> {
|
pub fn is_null_raw(x: &[f32; 32], tol: f32) -> Result<bool, CgaError> {
|
||||||
Ok(cga_inner_raw(x, x)?.abs() < tol)
|
Ok(cga_inner_raw(x, x)?.abs() < tol)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Re-project X onto the null cone.
|
/// Re-project X onto the null cone by extracting Euclidean components
|
||||||
/// Extract Euclidean components (indices 1-3), recompute e+ = 0.5*|x|^2, e- = 1.
|
/// and re-embedding with the canonical CGA point map.
|
||||||
pub fn null_project_raw(x: &[f32; 32]) -> [f32; 32] {
|
pub fn null_project_raw(x: &[f32; 32]) -> [f32; 32] {
|
||||||
let mut result = [0f32; 32];
|
embed_point_raw(&[x[1], x[2], x[3]])
|
||||||
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 a Euclidean point [x, y, z] into the CGA null cone.
|
/// 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] {
|
pub fn embed_point_raw(p: &[f32; 3]) -> [f32; 32] {
|
||||||
let mut result = [0f32; 32];
|
let mut result = [0f32; 32];
|
||||||
result[1] = p[0];
|
result[1] = p[0];
|
||||||
result[2] = p[1];
|
result[2] = p[1];
|
||||||
result[3] = p[2];
|
result[3] = p[2];
|
||||||
let r2 = p[0]*p[0] + p[1]*p[1] + p[2]*p[2];
|
let r2 = p[0] * p[0] + p[1] * p[1] + p[2] * p[2];
|
||||||
result[4] = 0.5 * r2;
|
result[4] = 0.5 * (r2 + 1.0);
|
||||||
result[5] = 1.0;
|
result[5] = 0.5 * (r2 - 1.0);
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
//! The multiplication table is computed once at program start using
|
||||||
//! const evaluation and stored as two [u8;1024] and [i8;1024] arrays
|
//! const evaluation and stored as two [u8;1024] and [i8;1024] arrays
|
||||||
//! (index and sign for each of the 32x32 blade pairs).
|
//! (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)
|
// 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.
|
// 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
|
// Precomputed at compile time via const fn
|
||||||
const BLADE_MASKS: [u8; 32] = build_blade_masks();
|
const BLADE_MASKS: [u8; 32] = build_blade_masks();
|
||||||
|
|
@ -68,63 +66,43 @@ const fn popcount5(x: u8) -> u8 {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Multiply two basis blades given as bitmasks. Returns (result_mask, sign).
|
// 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) {
|
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;
|
let mut sign: i8 = 1;
|
||||||
|
|
||||||
// Bubble sort + contract duplicates
|
// Anticommutation sign: every pair (a_i, b_j) with a_i > b_j swaps once.
|
||||||
let mut changed = true;
|
let mut swaps = 0u8;
|
||||||
while changed {
|
let mut ai = 0u8;
|
||||||
changed = false;
|
while ai < 5 {
|
||||||
let mut i = 0usize;
|
if (a >> ai) & 1 == 1 {
|
||||||
while i + 1 < len {
|
let mut bj = 0u8;
|
||||||
if seq[i] == seq[i + 1] {
|
while bj < 5 {
|
||||||
// Contract: e_k^2 = SIG[k]
|
if (b >> bj) & 1 == 1 && ai > bj {
|
||||||
sign *= SIG[seq[i] as usize];
|
swaps += 1;
|
||||||
// Remove both elements at i and i+1
|
}
|
||||||
let mut j = i;
|
bj += 1;
|
||||||
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;
|
|
||||||
sign *= -1;
|
|
||||||
changed = true;
|
|
||||||
i += 1;
|
|
||||||
} else {
|
|
||||||
i += 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ai += 1;
|
||||||
|
}
|
||||||
|
if swaps % 2 == 1 {
|
||||||
|
sign *= -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build result mask
|
// Metric contractions for duplicate basis vectors.
|
||||||
let mut result = 0u8;
|
let common = a & b;
|
||||||
let mut i = 0usize;
|
let mut bit = 0u8;
|
||||||
while i < len { result |= 1 << seq[i]; i += 1; }
|
while bit < 5 {
|
||||||
|
if (common >> bit) & 1 == 1 {
|
||||||
|
sign *= SIG[bit as usize];
|
||||||
|
}
|
||||||
|
bit += 1;
|
||||||
|
}
|
||||||
|
|
||||||
(result, sign)
|
(a ^ b, sign)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
|
||||||
|
|
||||||
struct Table {
|
struct Table {
|
||||||
idx: [[u8; 32]; 32],
|
idx: [[u8; 32]; 32],
|
||||||
sign: [[i8; 32]; 32],
|
sign: [[i8; 32]; 32],
|
||||||
|
|
|
||||||
|
|
@ -8,33 +8,27 @@
|
||||||
//! - vault_recall (parallel top-k scan)
|
//! - vault_recall (parallel top-k scan)
|
||||||
//!
|
//!
|
||||||
//! All multivectors are f32 arrays of length 32, passed as numpy arrays.
|
//! 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::exceptions::PyValueError;
|
||||||
|
use pyo3::prelude::*;
|
||||||
|
|
||||||
mod cl41;
|
pub mod cga;
|
||||||
mod versor;
|
pub mod cl41;
|
||||||
mod cga;
|
pub mod vault;
|
||||||
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 cga::cga_inner_raw;
|
||||||
|
use cl41::geometric_product_raw;
|
||||||
use vault::vault_recall_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-compatible f32 arrays of length 32.
|
||||||
|
|
||||||
/// Geometric product in Cl(4,1). Accepts two numpy f32 arrays of length 32.
|
|
||||||
#[pyfunction]
|
#[pyfunction]
|
||||||
fn geometric_product<'py>(
|
fn geometric_product(
|
||||||
py: Python<'py>,
|
py: Python<'_>,
|
||||||
a: &pyo3::types::PyAny,
|
a: &pyo3::types::PyAny,
|
||||||
b: &pyo3::types::PyAny,
|
b: &pyo3::types::PyAny,
|
||||||
) -> PyResult<pyo3::Bound<'py, pyo3::types::PyAny>> {
|
) -> PyResult<PyObject> {
|
||||||
let a_buf = a.call_method0("__array__")?;
|
|
||||||
let b_buf = b.call_method0("__array__")?;
|
|
||||||
// Extract raw f32 slices via buffer protocol
|
|
||||||
let a_slice = extract_f32_slice(a)?;
|
let a_slice = extract_f32_slice(a)?;
|
||||||
let b_slice = extract_f32_slice(b)?;
|
let b_slice = extract_f32_slice(b)?;
|
||||||
let result = geometric_product_raw(&a_slice, &b_slice)
|
let result = geometric_product_raw(&a_slice, &b_slice)
|
||||||
|
|
@ -42,13 +36,13 @@ fn geometric_product<'py>(
|
||||||
f32_array_to_numpy(py, &result)
|
f32_array_to_numpy(py, &result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sandwich product V*F*reverse(V). Zero-copy on input arrays.
|
/// Sandwich product V*F*reverse(V).
|
||||||
#[pyfunction]
|
#[pyfunction]
|
||||||
fn versor_apply<'py>(
|
fn versor_apply(
|
||||||
py: Python<'py>,
|
py: Python<'_>,
|
||||||
v: &pyo3::types::PyAny,
|
v: &pyo3::types::PyAny,
|
||||||
f: &pyo3::types::PyAny,
|
f: &pyo3::types::PyAny,
|
||||||
) -> PyResult<pyo3::Bound<'py, pyo3::types::PyAny>> {
|
) -> PyResult<PyObject> {
|
||||||
let v_slice = extract_f32_slice(v)?;
|
let v_slice = extract_f32_slice(v)?;
|
||||||
let f_slice = extract_f32_slice(f)?;
|
let f_slice = extract_f32_slice(f)?;
|
||||||
let result = versor_apply_raw(&v_slice, &f_slice)
|
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)|).
|
/// Project F onto versor manifold: F / sqrt(|F*rev(F)|).
|
||||||
#[pyfunction]
|
#[pyfunction]
|
||||||
fn normalize_to_versor<'py>(
|
fn normalize_to_versor(
|
||||||
py: Python<'py>,
|
py: Python<'_>,
|
||||||
f: &pyo3::types::PyAny,
|
f: &pyo3::types::PyAny,
|
||||||
) -> PyResult<pyo3::Bound<'py, pyo3::types::PyAny>> {
|
) -> PyResult<PyObject> {
|
||||||
let f_slice = extract_f32_slice(f)?;
|
let f_slice = extract_f32_slice(f)?;
|
||||||
let result = normalize_to_versor_raw(&f_slice)
|
let result = normalize_to_versor_raw(&f_slice)
|
||||||
.map_err(|e| PyValueError::new_err(e.to_string()))?;
|
.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.
|
/// 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]
|
#[pyfunction]
|
||||||
fn vault_recall(
|
fn vault_recall(
|
||||||
versors: Vec<&pyo3::types::PyAny>,
|
versors: Vec<&pyo3::types::PyAny>,
|
||||||
|
|
@ -96,42 +87,35 @@ fn vault_recall(
|
||||||
let query_slice = extract_f32_slice(query)?;
|
let query_slice = extract_f32_slice(query)?;
|
||||||
let mut slices: Vec<[f32; 32]> = Vec::with_capacity(versors.len());
|
let mut slices: Vec<[f32; 32]> = Vec::with_capacity(versors.len());
|
||||||
for v in &versors {
|
for v in &versors {
|
||||||
let s = extract_f32_slice(v)?;
|
slices.push(extract_f32_slice(v)?);
|
||||||
slices.push(s);
|
|
||||||
}
|
}
|
||||||
vault_recall_raw(&slices, &query_slice, top_k)
|
vault_recall_raw(&slices, &query_slice, top_k)
|
||||||
.map_err(|e| PyValueError::new_err(e.to_string()))
|
.map_err(|e| PyValueError::new_err(e.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Helpers ---
|
|
||||||
|
|
||||||
fn extract_f32_slice(obj: &pyo3::types::PyAny) -> PyResult<[f32; 32]> {
|
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 np = obj.py().import("numpy")?;
|
||||||
let arr = np.call_method1("asarray", (obj, "float32"))?;
|
let arr = np.call_method1("asarray", (obj, "float32"))?;
|
||||||
let flat = arr.call_method0("flatten")?;
|
let flat = arr.call_method0("flatten")?;
|
||||||
let list: Vec<f32> = flat.extract()?;
|
let list: Vec<f32> = flat.extract()?;
|
||||||
if list.len() != 32 {
|
if list.len() != 32 {
|
||||||
return Err(PyValueError::new_err(
|
return Err(PyValueError::new_err(format!(
|
||||||
format!("Expected array of length 32, got {}", list.len())
|
"Expected array of length 32, got {}",
|
||||||
));
|
list.len()
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
let mut out = [0f32; 32];
|
let mut out = [0f32; 32];
|
||||||
out.copy_from_slice(&list);
|
out.copy_from_slice(&list);
|
||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn f32_array_to_numpy<'py>(
|
fn f32_array_to_numpy(py: Python<'_>, data: &[f32; 32]) -> PyResult<PyObject> {
|
||||||
py: Python<'py>,
|
|
||||||
data: &[f32; 32],
|
|
||||||
) -> PyResult<pyo3::Bound<'py, pyo3::types::PyAny>> {
|
|
||||||
let np = py.import("numpy")?;
|
let np = py.import("numpy")?;
|
||||||
let list: Vec<f32> = data.to_vec();
|
let list: Vec<f32> = data.to_vec();
|
||||||
let arr = np.call_method1("array", (list, "float32"))?;
|
let arr = np.call_method1("array", (list, "float32"))?;
|
||||||
Ok(arr.into())
|
Ok(arr.into_py(py))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Module registration
|
|
||||||
#[pymodule]
|
#[pymodule]
|
||||||
fn core_rs(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
fn core_rs(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||||
m.add_function(wrap_pyfunction!(geometric_product, m)?)?;
|
m.add_function(wrap_pyfunction!(geometric_product, m)?)?;
|
||||||
|
|
|
||||||
|
|
@ -1,48 +1,41 @@
|
||||||
#[cfg(test)]
|
use core_rs::cga::{cga_inner_raw, embed_point_raw, is_null_raw, null_project_raw};
|
||||||
mod tests {
|
|
||||||
use crate::cga::{cga_inner_raw, embed_point_raw, is_null_raw, null_project_raw};
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_embedded_point_is_null() {
|
fn test_embedded_point_is_null() {
|
||||||
let p = [1.0f32, 2.0, 3.0];
|
let p = [1.0f32, 2.0, 3.0];
|
||||||
let x = embed_point_raw(&p);
|
let x = embed_point_raw(&p);
|
||||||
assert!(is_null_raw(&x, 1e-5).unwrap(), "Embedded point should be null");
|
assert!(is_null_raw(&x, 1e-5).unwrap(), "Embedded point should be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_origin_is_null() {
|
fn test_origin_is_null() {
|
||||||
let x = embed_point_raw(&[0.0, 0.0, 0.0]);
|
let x = embed_point_raw(&[0.0, 0.0, 0.0]);
|
||||||
assert!(is_null_raw(&x, 1e-5).unwrap());
|
assert!(is_null_raw(&x, 1e-5).unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cga_inner_symmetry() {
|
fn test_cga_inner_symmetry() {
|
||||||
let x = embed_point_raw(&[1.0, 0.0, 0.0]);
|
let x = embed_point_raw(&[1.0, 0.0, 0.0]);
|
||||||
let y = embed_point_raw(&[0.0, 1.0, 0.0]);
|
let y = embed_point_raw(&[0.0, 1.0, 0.0]);
|
||||||
let xy = cga_inner_raw(&x, &y).unwrap();
|
let xy = cga_inner_raw(&x, &y).unwrap();
|
||||||
let yx = cga_inner_raw(&y, &x).unwrap();
|
let yx = cga_inner_raw(&y, &x).unwrap();
|
||||||
assert!((xy - yx).abs() < 1e-6, "cga_inner not symmetric");
|
assert!((xy - yx).abs() < 1e-6, "cga_inner not symmetric");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cga_distance_identity() {
|
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 x = embed_point_raw(&[0.0, 0.0, 0.0]);
|
let y = embed_point_raw(&[1.0, 0.0, 0.0]);
|
||||||
let y = embed_point_raw(&[1.0, 0.0, 0.0]);
|
let inner = cga_inner_raw(&x, &y).unwrap();
|
||||||
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() {
|
||||||
#[test]
|
let p = [1.0f32, 2.0, 3.0];
|
||||||
fn test_null_project_restores_null() {
|
let mut x = embed_point_raw(&p);
|
||||||
let p = [1.0f32, 2.0, 3.0];
|
x[0] += 0.05;
|
||||||
let mut x = embed_point_raw(&p);
|
x[7] -= 0.03;
|
||||||
// Introduce drift
|
let fixed = null_project_raw(&x);
|
||||||
x[0] += 0.05;
|
assert!(is_null_raw(&fixed, 1e-5).unwrap(), "null_project failed to restore null cone");
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,69 +1,68 @@
|
||||||
#[cfg(test)]
|
use core_rs::cl41::{geometric_product_raw, reverse_raw};
|
||||||
mod tests {
|
|
||||||
use crate::cl41::{geometric_product_raw, reverse_raw, BLADE_MASKS, MASK_TO_IDX};
|
|
||||||
|
|
||||||
fn basis(i: usize) -> [f32; 32] {
|
fn basis(i: usize) -> [f32; 32] {
|
||||||
let mut v = [0f32; 32];
|
let mut v = [0f32; 32];
|
||||||
v[1 + i] = 1.0; // grade-1 component
|
v[1 + i] = 1.0;
|
||||||
v
|
v
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scalar(s: f32) -> [f32; 32] {
|
fn scalar(s: f32) -> [f32; 32] {
|
||||||
let mut v = [0f32; 32];
|
let mut v = [0f32; 32];
|
||||||
v[0] = s;
|
v[0] = s;
|
||||||
v
|
v
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_e1_squared_is_plus1() {
|
fn test_e1_squared_is_plus1() {
|
||||||
let e1 = basis(0);
|
let e1 = basis(0);
|
||||||
let r = geometric_product_raw(&e1, &e1).unwrap();
|
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]);
|
||||||
assert!((r[0] - 1.0).abs() < 1e-6, "e1^2 should be +1, got {}", r[0]);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_e5_squared_is_minus1() {
|
fn test_e4_squared_is_minus1() {
|
||||||
let e5 = basis(4);
|
let e4 = basis(3);
|
||||||
let r = geometric_product_raw(&e5, &e5).unwrap();
|
let r = geometric_product_raw(&e4, &e4).unwrap();
|
||||||
// e5^2 = -1 (signature index 4 = -1)
|
assert!((r[0] + 1.0).abs() < 1e-6, "e4^2 should be -1, got {}", r[0]);
|
||||||
assert!((r[0] + 1.0).abs() < 1e-6, "e5^2 should be -1, got {}", r[0]);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_e1_e2_anticommute() {
|
fn test_e5_squared_is_plus1() {
|
||||||
let e1 = basis(0);
|
let e5 = basis(4);
|
||||||
let e2 = basis(1);
|
let r = geometric_product_raw(&e5, &e5).unwrap();
|
||||||
let e1e2 = geometric_product_raw(&e1, &e2).unwrap();
|
assert!((r[0] - 1.0).abs() < 1e-6, "e5^2 should be +1, got {}", r[0]);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_scalar_identity() {
|
fn test_e1_e2_anticommute() {
|
||||||
let e1 = basis(0);
|
let e1 = basis(0);
|
||||||
let one = scalar(1.0);
|
let e2 = basis(1);
|
||||||
let r = geometric_product_raw(&one, &e1).unwrap();
|
let e1e2 = geometric_product_raw(&e1, &e2).unwrap();
|
||||||
assert!((r[1] - 1.0).abs() < 1e-6, "1*e1 should be e1");
|
let e2e1 = geometric_product_raw(&e2, &e1).unwrap();
|
||||||
}
|
for i in 0..32 {
|
||||||
|
assert!((e1e2[i] + e2e1[i]).abs() < 1e-6, "e1*e2 + e2*e1 != 0 at index {}", i);
|
||||||
#[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
|
|
||||||
let r = reverse_raw(&a);
|
|
||||||
assert!((r[6] + 1.0).abs() < 1e-6, "reverse of grade-2 blade should negate");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_reverse_grade1_unchanged() {
|
|
||||||
let e1 = basis(0);
|
|
||||||
let r = reverse_raw(&e1);
|
|
||||||
assert!((r[1] - 1.0).abs() < 1e-6, "reverse of grade-1 blade should be unchanged");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_scalar_identity() {
|
||||||
|
let e1 = basis(0);
|
||||||
|
let one = scalar(1.0);
|
||||||
|
let r = geometric_product_raw(&one, &e1).unwrap();
|
||||||
|
assert!((r[1] - 1.0).abs() < 1e-6, "1*e1 should be e1");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_reverse_grade2_sign() {
|
||||||
|
let mut a = [0f32; 32];
|
||||||
|
a[6] = 1.0;
|
||||||
|
let r = reverse_raw(&a);
|
||||||
|
assert!((r[6] + 1.0).abs() < 1e-6, "reverse of grade-2 blade should negate");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_reverse_grade1_unchanged() {
|
||||||
|
let e1 = basis(0);
|
||||||
|
let r = reverse_raw(&e1);
|
||||||
|
assert!((r[1] - 1.0).abs() < 1e-6, "reverse of grade-1 blade should be unchanged");
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,50 +1,43 @@
|
||||||
#[cfg(test)]
|
use core_rs::vault::vault_recall_raw;
|
||||||
mod tests {
|
use core_rs::cga::embed_point_raw;
|
||||||
use crate::vault::vault_recall_raw;
|
|
||||||
use crate::versor::normalize_to_versor_raw;
|
|
||||||
|
|
||||||
fn random_versor(seed: u64) -> [f32; 32] {
|
fn sample_point(seed: u64) -> [f32; 32] {
|
||||||
let mut state = seed ^ 0xdeadbeef_cafebabe;
|
let x = ((seed * 17 + 3) % 101) as f32 / 10.0;
|
||||||
let mut raw = [0f32; 32];
|
let y = ((seed * 23 + 7) % 101) as f32 / 10.0;
|
||||||
for x in raw.iter_mut() {
|
let z = ((seed * 31 + 11) % 101) as f32 / 10.0;
|
||||||
state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
|
embed_point_raw(&[x, y, z])
|
||||||
*x = ((state >> 33) as f32) / (u32::MAX as f32) * 2.0 - 1.0;
|
}
|
||||||
}
|
|
||||||
normalize_to_versor_raw(&raw).expect("normalize failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_recall_self() {
|
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() {
|
for (i, query) in versors.iter().enumerate() {
|
||||||
let results = vault_recall_raw(&versors, query, 1).unwrap();
|
let results = vault_recall_raw(&versors, query, 1).unwrap();
|
||||||
assert_eq!(results[0].0, i,
|
assert_eq!(results[0].0, i);
|
||||||
"Versor {} should recall itself as top-1, got {}", i, results[0].0);
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
#[test]
|
||||||
#[test]
|
fn test_empty_vault() {
|
||||||
fn test_empty_vault() {
|
let query = sample_point(0);
|
||||||
let query = random_versor(0);
|
let results = vault_recall_raw(&[], &query, 5).unwrap();
|
||||||
let results = vault_recall_raw(&[], &query, 5).unwrap();
|
assert!(results.is_empty());
|
||||||
assert!(results.is_empty());
|
}
|
||||||
}
|
|
||||||
|
#[test]
|
||||||
#[test]
|
fn test_top_k_count() {
|
||||||
fn test_top_k_count() {
|
let versors: Vec<[f32; 32]> = (0..10).map(|i| sample_point(i as u64)).collect();
|
||||||
let versors: Vec<[f32; 32]> = (0..10).map(|i| random_versor(i as u64)).collect();
|
let query = sample_point(99);
|
||||||
let query = random_versor(99);
|
let results = vault_recall_raw(&versors, &query, 3).unwrap();
|
||||||
let results = vault_recall_raw(&versors, &query, 3).unwrap();
|
assert_eq!(results.len(), 3);
|
||||||
assert_eq!(results.len(), 3);
|
}
|
||||||
}
|
|
||||||
|
#[test]
|
||||||
#[test]
|
fn test_scores_descending() {
|
||||||
fn test_scores_descending() {
|
let versors: Vec<[f32; 32]> = (0..10).map(|i| sample_point(i as u64)).collect();
|
||||||
let versors: Vec<[f32; 32]> = (0..10).map(|i| random_versor(i as u64)).collect();
|
let query = sample_point(99);
|
||||||
let query = random_versor(99);
|
let results = vault_recall_raw(&versors, &query, 5).unwrap();
|
||||||
let results = vault_recall_raw(&versors, &query, 5).unwrap();
|
for w in results.windows(2) {
|
||||||
for w in results.windows(2) {
|
assert!(w[0].1 >= w[1].1);
|
||||||
assert!(w[0].1 >= w[1].1, "Scores not descending");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,59 +1,57 @@
|
||||||
#[cfg(test)]
|
use core_rs::versor::{normalize_to_versor_raw, versor_apply_raw, versor_condition_raw};
|
||||||
mod tests {
|
|
||||||
use crate::versor::{versor_apply_raw, normalize_to_versor_raw, versor_condition_raw};
|
|
||||||
|
|
||||||
fn random_versor(seed: u64) -> [f32; 32] {
|
fn random_versor(seed: u64) -> [f32; 32] {
|
||||||
// Simple LCG for deterministic test data — no external deps
|
let theta = ((seed * 17 + 3) % 101) as f32 / 100.0;
|
||||||
let mut state = seed ^ 0xdeadbeef_cafebabe;
|
let mut v = [0f32; 32];
|
||||||
let mut raw = [0f32; 32];
|
v[0] = theta.cos();
|
||||||
for x in raw.iter_mut() {
|
|
||||||
state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
|
// Choose a bivector with negative square, e.g. e12
|
||||||
*x = ((state >> 33) as f32) / (u32::MAX as f32) * 2.0 - 1.0;
|
// 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
|
||||||
normalize_to_versor_raw(&raw).expect("normalize failed")
|
// MASK_TO_IDX[3] = 6 (grade 2 starts at 6)
|
||||||
}
|
v[6] = theta.sin();
|
||||||
|
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_normalize_produces_versor() {
|
fn test_normalize_produces_versor() {
|
||||||
for seed in 0..20u64 {
|
for seed in 0..20u64 {
|
||||||
let v = random_versor(seed);
|
let v = random_versor(seed);
|
||||||
let cond = versor_condition_raw(&v).unwrap();
|
let cond = versor_condition_raw(&v).unwrap();
|
||||||
assert!(cond < 1e-5, "versor_condition after normalize = {}", cond);
|
assert!(cond < 1e-5, "versor_condition after normalize = {}", cond);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_versor_apply_preserves_manifold() {
|
|
||||||
for seed in 0..20u64 {
|
|
||||||
let v = random_versor(seed);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_identity_versor() {
|
|
||||||
let mut identity = [0f32; 32];
|
|
||||||
identity[0] = 1.0;
|
|
||||||
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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_composition_closed() {
|
|
||||||
let v1 = random_versor(0);
|
|
||||||
let v2 = random_versor(1);
|
|
||||||
let f = random_versor(2);
|
|
||||||
let f2 = versor_apply_raw(&v1, &f).unwrap();
|
|
||||||
let f3 = versor_apply_raw(&v2, &f2).unwrap();
|
|
||||||
let cond = versor_condition_raw(&f3).unwrap();
|
|
||||||
assert!(cond < 1e-4, "Composition broke manifold: condition={:.2e}", cond);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_versor_apply_preserves_manifold() {
|
||||||
|
for seed in 0..20u64 {
|
||||||
|
let v = random_versor(seed);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_identity_versor() {
|
||||||
|
let mut identity = [0f32; 32];
|
||||||
|
identity[0] = 1.0;
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_composition_closed() {
|
||||||
|
let v1 = random_versor(0);
|
||||||
|
let v2 = random_versor(1);
|
||||||
|
let f = random_versor(2);
|
||||||
|
let f2 = versor_apply_raw(&v1, &f).unwrap();
|
||||||
|
let f3 = versor_apply_raw(&v2, &f2).unwrap();
|
||||||
|
let cond = versor_condition_raw(&f3).unwrap();
|
||||||
|
assert!(cond < 1e-4, "Composition broke manifold: condition={:.2e}", cond);
|
||||||
|
}
|
||||||
|
|
|
||||||
80
core/cli.py
80
core/cli.py
|
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
|
|
@ -18,14 +19,16 @@ _REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||||
if str(_REPO_ROOT) not in sys.path:
|
if str(_REPO_ROOT) not in sys.path:
|
||||||
sys.path.insert(0, str(_REPO_ROOT))
|
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."
|
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."""
|
"""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)
|
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)
|
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:
|
def cmd_doctor(args: argparse.Namespace) -> int:
|
||||||
"""Inspect import/package health for the CLI runtime path."""
|
"""Inspect import/package health for the CLI runtime path."""
|
||||||
checks = [
|
checks = [
|
||||||
|
|
@ -296,6 +349,10 @@ def cmd_doctor(args: argparse.Namespace) -> int:
|
||||||
print(f" {pack_id}")
|
print(f" {pack_id}")
|
||||||
else:
|
else:
|
||||||
print(" none found")
|
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
|
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.add_argument("pack_id", help="pack id, e.g. en_minimal_v1")
|
||||||
pack_verify.set_defaults(func=cmd_pack_verify)
|
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 = 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("--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)
|
doctor.set_defaults(func=cmd_doctor)
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
|
||||||
85
docs/RUST.md
85
docs/RUST.md
|
|
@ -4,78 +4,81 @@
|
||||||
|
|
||||||
Three operations dominate CORE-AI's runtime:
|
Three operations dominate CORE-AI's runtime:
|
||||||
|
|
||||||
1. geometric_product — O(32^2) = 1024 multiply-adds per call, called 2-3x per versor_apply
|
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
|
2. `vault_recall` scan — O(N) CGA inner product calls, N = all stored versors, called during generation recall
|
||||||
3. holonomy_encode — 2 * prompt_length geometric products in sequence
|
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
|
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.
|
||||||
parallelism across CPU cores. The geometric product loop is cache-friendly and
|
|
||||||
compiler-autovectorized when opt-level=3 + lto=true.
|
|
||||||
|
|
||||||
## What is in Rust
|
## What is in Rust
|
||||||
|
|
||||||
| Module | Rust file | Why |
|
| Module | Rust file | Why |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Cl(4,1) product | cl41.rs | Hot inner loop, 1024 MADs, autovectorizable |
|
| Cl(4,1) product | `cl41.rs` | Hot inner loop, 1024 MADs |
|
||||||
| Versor ops | versor.rs | 3x geometric_product per step, allocation-free |
|
| Versor ops | `versor.rs` | 3x geometric product per field step |
|
||||||
| CGA inner product | cga.rs | Called every token decode and every vault recall |
|
| CGA inner product | `cga.rs` | Called by nearest search and recall |
|
||||||
| Vault top-k scan | vault.rs | Rayon parallel scan — GIL blocks Python threads |
|
| Vault top-k scan | `vault.rs` | Rayon parallel scan |
|
||||||
| Holonomy encode | holonomy.rs | 200+ products for long prompts |
|
|
||||||
| Batch propagation | propagate.rs | Beam search / speculative decode |
|
|
||||||
|
|
||||||
## What stays in Python
|
## What stays in Python
|
||||||
|
|
||||||
| Layer | Why |
|
| Layer | Why |
|
||||||
|---|---|
|
|---|---|
|
||||||
| VocabManifold | Word lookup, edge rotor construction — called once per token, not per step |
|
| `VocabManifold` | Word/morphology/language metadata and exact candidate filtering |
|
||||||
| SessionContext | Orchestration, not arithmetic |
|
| `SessionContext` | Orchestration, not arithmetic |
|
||||||
| FieldState | Plain dataclass |
|
| `FieldState` | Plain dataclass |
|
||||||
| PersonaMotor | Motor construction is infrequent |
|
| `PersonaMotor` | Motor construction is infrequent |
|
||||||
|
|
||||||
## Zero-Copy Semantics
|
## Zero-Copy Semantics
|
||||||
|
|
||||||
All f32 arrays are passed as numpy arrays from Python.
|
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.
|
||||||
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.
|
|
||||||
|
|
||||||
For vault_recall, the versors list is iterated via Rayon par_iter with no cloning:
|
## Build / Activate
|
||||||
each worker holds a read-only reference to its slice element.
|
|
||||||
|
|
||||||
## Build
|
Requires a Rust toolchain and maturin. Prefer the uv-native flow so the repo does not depend on `pip` being installed inside `.venv`:
|
||||||
|
|
||||||
Requires maturin and a Rust toolchain (stable 1.75+).
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd core-rs
|
core rust status
|
||||||
pip install maturin
|
core rust test
|
||||||
maturin develop --release # installs core_rs into current venv
|
core rust build
|
||||||
|
core rust status --require-active
|
||||||
```
|
```
|
||||||
|
|
||||||
Or build a wheel:
|
Equivalent explicit maturin command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
maturin build --release
|
uv run --with maturin maturin develop --release --manifest-path core-rs/Cargo.toml
|
||||||
pip install target/wheels/*.whl
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Verify Rust backend is active from Python:
|
Verify Rust backend is active from Python:
|
||||||
```python
|
|
||||||
from algebra.backend import using_rust
|
```bash
|
||||||
print(using_rust()) # True if core_rs is installed
|
uv run python -c "from algebra.backend import using_rust; print(using_rust())"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
```text
|
||||||
|
True
|
||||||
```
|
```
|
||||||
|
|
||||||
## Running Rust Tests
|
## Running Rust Tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd core-rs
|
core rust test
|
||||||
cargo test
|
# or
|
||||||
|
cargo test --release --manifest-path core-rs/Cargo.toml
|
||||||
```
|
```
|
||||||
|
|
||||||
## Type Safety Contract
|
## Type Safety Contract
|
||||||
|
|
||||||
All multivectors entering the Rust layer are validated as f32 arrays of length 32
|
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.
|
||||||
by extract_f32_slice() in lib.rs. Type errors surface immediately as Python
|
|
||||||
ValueError with a descriptive message rather than silent memory corruption.
|
|
||||||
|
|
||||||
All error types use thiserror — every failure path is a named enum variant,
|
## Failure Mode
|
||||||
not a string panic.
|
|
||||||
|
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.
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ def test_top_level_help_exits_without_runtime_import(capsys: pytest.CaptureFixtu
|
||||||
out = capsys.readouterr().out
|
out = capsys.readouterr().out
|
||||||
assert "CORE versor engine command suite" in out
|
assert "CORE versor engine command suite" in out
|
||||||
assert "core trace" 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:
|
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
|
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:
|
def test_main_without_args_prints_help(capsys: pytest.CaptureFixture[str]) -> None:
|
||||||
assert main([]) == 0
|
assert main([]) == 0
|
||||||
out = capsys.readouterr().out
|
out = capsys.readouterr().out
|
||||||
|
|
@ -49,6 +69,12 @@ def test_doctor_imports_runtime_support_modules(capsys: pytest.CaptureFixture[st
|
||||||
assert "OK sensorium" in out
|
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:
|
def test_trace_formats_real_runtime_payload(capsys: pytest.CaptureFixture[str]) -> None:
|
||||||
assert main(["trace", "--pack", "en_minimal_v1", "word", "beginning", "truth"]) == 0
|
assert main(["trace", "--pack", "en_minimal_v1", "word", "beginning", "truth"]) == 0
|
||||||
out = capsys.readouterr().out
|
out = capsys.readouterr().out
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,31 @@
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from algebra.versor import unitize_versor
|
from algebra.cga import embed_point, is_null
|
||||||
from algebra.cga import is_null
|
|
||||||
from vault.store import VaultStore
|
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)
|
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():
|
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()
|
vault = VaultStore()
|
||||||
versors = [_random_versor(i) for i in range(20)]
|
points = [_random_point(i) for i in range(20)]
|
||||||
for i, v in enumerate(versors):
|
for i, v in enumerate(points):
|
||||||
vault.store(v, {"id": i})
|
vault.store(v, {"id": i})
|
||||||
for i, v in enumerate(versors):
|
for i, v in enumerate(points):
|
||||||
results = vault.recall(v, top_k=1)
|
results = vault.recall(v, top_k=1)
|
||||||
assert results[0]["metadata"]["id"] == i, (
|
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():
|
def test_recall_empty_vault():
|
||||||
vault = VaultStore()
|
vault = VaultStore()
|
||||||
result = vault.recall(_random_versor(), top_k=5)
|
result = vault.recall(_random_point(), top_k=5)
|
||||||
assert result == []
|
assert result == []
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -34,7 +33,7 @@ def test_reproject_maintains_structure():
|
||||||
"""Reproject should not lose stored entries."""
|
"""Reproject should not lose stored entries."""
|
||||||
vault = VaultStore()
|
vault = VaultStore()
|
||||||
for i in range(10):
|
for i in range(10):
|
||||||
vault.store(_random_versor(i), {"id": i})
|
vault.store(_random_point(i), {"id": i})
|
||||||
vault.reproject()
|
vault.reproject()
|
||||||
assert len(vault) == 10
|
assert len(vault) == 10
|
||||||
|
|
||||||
|
|
@ -42,5 +41,6 @@ def test_reproject_maintains_structure():
|
||||||
def test_vault_len():
|
def test_vault_len():
|
||||||
vault = VaultStore()
|
vault = VaultStore()
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
vault.store(_random_versor(i))
|
vault.store(_random_point(i))
|
||||||
assert len(vault) == 5
|
assert len(vault) == 5
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue