init: Rust build config, Python dispatch layer, Rust tests
This commit is contained in:
parent
0063259584
commit
f882408e62
10 changed files with 526 additions and 0 deletions
75
algebra/backend.py
Normal file
75
algebra/backend.py
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
"""
|
||||
Backend dispatch: use Rust extension (core_rs) when available,
|
||||
fall back to pure Python (algebra/cl41.py etc.) transparently.
|
||||
|
||||
This module is the single switch. All algebra modules import from here
|
||||
for performance-critical ops. Pure Python is always the fallback —
|
||||
the system is never broken by a missing Rust build.
|
||||
|
||||
Usage:
|
||||
from algebra.backend import geometric_product, versor_apply, cga_inner, vault_recall
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
try:
|
||||
import core_rs as _rs
|
||||
_RUST = True
|
||||
except ImportError:
|
||||
_RUST = False
|
||||
|
||||
|
||||
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:
|
||||
if _RUST:
|
||||
return np.asarray(_rs.versor_apply(V, F), dtype=np.float32)
|
||||
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 normalize_to_versor(F: np.ndarray) -> np.ndarray:
|
||||
if _RUST:
|
||||
return np.asarray(_rs.normalize_to_versor(F), dtype=np.float32)
|
||||
from algebra.versor import normalize_to_versor as _nv
|
||||
return _nv(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 (releases GIL, true multithreaded).
|
||||
Python path: sequential list comprehension.
|
||||
"""
|
||||
if _RUST:
|
||||
results = _rs.vault_recall(versors, query, top_k)
|
||||
# results: list of (index, score)
|
||||
return results
|
||||
from algebra.cga import cga_inner as _ci
|
||||
scores = [(i, _ci(query, v)) for i, v in enumerate(versors)]
|
||||
scores.sort(key=lambda x: -x[1])
|
||||
return scores[:top_k]
|
||||
|
||||
|
||||
def using_rust() -> bool:
|
||||
"""Returns True if the Rust extension is loaded."""
|
||||
return _RUST
|
||||
7
core-rs/build.rs
Normal file
7
core-rs/build.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// build.rs: nothing special needed for PyO3 — maturin handles the rest.
|
||||
// This file exists as an extension point for future build-time codegen
|
||||
// (e.g. generating the full Cl(4,1) multiplication table as a static array
|
||||
// rather than computing it at OnceLock init time).
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=src/");
|
||||
}
|
||||
8
core-rs/pyproject.toml
Normal file
8
core-rs/pyproject.toml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
[build-system]
|
||||
requires = ["maturin>=1.5,<2.0"]
|
||||
build-backend = "maturin"
|
||||
|
||||
[tool.maturin]
|
||||
features = ["extension-module"]
|
||||
module-name = "core_rs"
|
||||
python-source = "python"
|
||||
81
core-rs/src/holonomy.rs
Normal file
81
core-rs/src/holonomy.rs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
//! Holonomy encoder in Rust — the forward+reverse versor walk.
|
||||
//!
|
||||
//! This is in Rust because:
|
||||
//! - Long prompts (100+ tokens) do 200+ geometric products in sequence
|
||||
//! - Each geometric product is O(32^2) = 1024 multiply-adds
|
||||
//! - Python overhead per call makes this 10-50x slower than necessary
|
||||
//! - Rust collapses the entire walk into a single allocation-free loop
|
||||
|
||||
use crate::cl41::{geometric_product_raw, reverse_raw};
|
||||
use crate::versor::normalize_to_versor_raw;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum HolonomyError {
|
||||
#[error("Empty word list")]
|
||||
Empty,
|
||||
#[error("Versor error: {0}")]
|
||||
Versor(String),
|
||||
}
|
||||
|
||||
/// Compute holonomy of a word versor sequence.
|
||||
///
|
||||
/// Forward walk: F = w0 * w1 * ... * wn
|
||||
/// Reverse walk: R = (1-alpha) * rev(wn) * ... * rev(w0)
|
||||
/// Holonomy: H = normalize(F * R)
|
||||
///
|
||||
/// weights: per-word scalars (inverse frequency). If empty, uniform 1.0.
|
||||
/// alpha: blend factor [0,1]. 0.5 recommended.
|
||||
pub fn holonomy_encode_raw(
|
||||
words: &[[f32; 32]],
|
||||
weights: &[f32],
|
||||
alpha: f32,
|
||||
) -> Result<[f32; 32], HolonomyError> {
|
||||
if words.is_empty() {
|
||||
return Err(HolonomyError::Empty);
|
||||
}
|
||||
|
||||
let n = words.len();
|
||||
let use_weights = !weights.is_empty() && weights.len() == n;
|
||||
|
||||
// Forward accumulation
|
||||
let mut scaled = words[0];
|
||||
if use_weights {
|
||||
let w = weights[0];
|
||||
for x in scaled.iter_mut() { *x *= w; }
|
||||
}
|
||||
let mut f = normalize_to_versor_raw(&scaled)
|
||||
.map_err(|e| HolonomyError::Versor(e.to_string()))?;
|
||||
|
||||
for k in 1..n {
|
||||
let mut wk = words[k];
|
||||
if use_weights {
|
||||
let w = weights[k];
|
||||
for x in wk.iter_mut() { *x *= w; }
|
||||
}
|
||||
let wk_norm = normalize_to_versor_raw(&wk)
|
||||
.map_err(|e| HolonomyError::Versor(e.to_string()))?;
|
||||
f = geometric_product_raw(&f, &wk_norm)
|
||||
.map_err(|e| HolonomyError::Versor(e.to_string()))?;
|
||||
}
|
||||
|
||||
// Reverse accumulation with (1-alpha) damping
|
||||
let damp = 1.0 - alpha;
|
||||
let mut last_rev = reverse_raw(&words[n - 1]);
|
||||
for x in last_rev.iter_mut() { *x *= damp; }
|
||||
let mut r = normalize_to_versor_raw(&last_rev)
|
||||
.map_err(|e| HolonomyError::Versor(e.to_string()))?;
|
||||
|
||||
for k in (0..n - 1).rev() {
|
||||
let rev_wk = reverse_raw(&words[k]);
|
||||
let rev_norm = normalize_to_versor_raw(&rev_wk)
|
||||
.map_err(|e| HolonomyError::Versor(e.to_string()))?;
|
||||
r = geometric_product_raw(&rev_norm, &r)
|
||||
.map_err(|e| HolonomyError::Versor(e.to_string()))?;
|
||||
}
|
||||
|
||||
let h = geometric_product_raw(&f, &r)
|
||||
.map_err(|e| HolonomyError::Versor(e.to_string()))?;
|
||||
normalize_to_versor_raw(&h)
|
||||
.map_err(|e| HolonomyError::Versor(e.to_string()))
|
||||
}
|
||||
48
core-rs/src/propagate.rs
Normal file
48
core-rs/src/propagate.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
//! Propagation loop in Rust — tight versor_apply chain.
|
||||
//!
|
||||
//! propagate_n steps runs N versor_apply calls in a single Rust stack frame,
|
||||
//! eliminating Python dispatch overhead for each step.
|
||||
//! Used by generate/stream.py when stepping more than one token at a time
|
||||
//! (e.g. prefill, speculative steps, or batch generation).
|
||||
|
||||
use crate::versor::versor_apply_raw;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum PropagateError {
|
||||
#[error("Versor error during propagation: {0}")]
|
||||
Versor(String),
|
||||
}
|
||||
|
||||
/// Run n versor_apply steps in sequence.
|
||||
/// rotors: slice of n [f32;32] versors to apply in order
|
||||
/// f0: initial field state
|
||||
/// Returns final field state after n steps.
|
||||
pub fn propagate_n_raw(
|
||||
rotors: &[[f32; 32]],
|
||||
f0: &[f32; 32],
|
||||
) -> Result<[f32; 32], PropagateError> {
|
||||
let mut f = *f0;
|
||||
for v in rotors {
|
||||
f = versor_apply_raw(v, &f)
|
||||
.map_err(|e| PropagateError::Versor(e.to_string()))?;
|
||||
}
|
||||
Ok(f)
|
||||
}
|
||||
|
||||
/// Parallel batch propagation: apply the same rotor V to a batch of field states.
|
||||
/// Used for beam search or multi-hypothesis generation.
|
||||
/// Returns new batch of field states.
|
||||
pub fn propagate_batch_raw(
|
||||
v: &[f32; 32],
|
||||
fields: &[[f32; 32]],
|
||||
) -> Result<Vec<[f32; 32]>, PropagateError> {
|
||||
use rayon::prelude::*;
|
||||
fields
|
||||
.par_iter()
|
||||
.map(|f| {
|
||||
versor_apply_raw(v, f)
|
||||
.map_err(|e| PropagateError::Versor(e.to_string()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
48
core-rs/tests/test_cga.rs
Normal file
48
core-rs/tests/test_cga.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::cga::{cga_inner_raw, embed_point_raw, is_null_raw, null_project_raw};
|
||||
|
||||
#[test]
|
||||
fn test_embedded_point_is_null() {
|
||||
let p = [1.0f32, 2.0, 3.0];
|
||||
let x = embed_point_raw(&p);
|
||||
assert!(is_null_raw(&x, 1e-5).unwrap(), "Embedded point should be null");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_origin_is_null() {
|
||||
let x = embed_point_raw(&[0.0, 0.0, 0.0]);
|
||||
assert!(is_null_raw(&x, 1e-5).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cga_inner_symmetry() {
|
||||
let x = embed_point_raw(&[1.0, 0.0, 0.0]);
|
||||
let y = embed_point_raw(&[0.0, 1.0, 0.0]);
|
||||
let xy = cga_inner_raw(&x, &y).unwrap();
|
||||
let yx = cga_inner_raw(&y, &x).unwrap();
|
||||
assert!((xy - yx).abs() < 1e-6, "cga_inner not symmetric");
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
|
||||
#[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");
|
||||
}
|
||||
}
|
||||
69
core-rs/tests/test_cl41.rs
Normal file
69
core-rs/tests/test_cl41.rs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::cl41::{geometric_product_raw, reverse_raw, BLADE_MASKS, MASK_TO_IDX};
|
||||
|
||||
fn basis(i: usize) -> [f32; 32] {
|
||||
let mut v = [0f32; 32];
|
||||
v[1 + i] = 1.0; // grade-1 component
|
||||
v
|
||||
}
|
||||
|
||||
fn scalar(s: f32) -> [f32; 32] {
|
||||
let mut v = [0f32; 32];
|
||||
v[0] = s;
|
||||
v
|
||||
}
|
||||
|
||||
#[test]
|
||||
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() {
|
||||
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]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_e1_e2_anticommute() {
|
||||
let e1 = basis(0);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
#[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() {
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
50
core-rs/tests/test_vault.rs
Normal file
50
core-rs/tests/test_vault.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::vault::vault_recall_raw;
|
||||
use crate::versor::normalize_to_versor_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")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_recall_self() {
|
||||
let versors: Vec<[f32; 32]> = (0..20).map(|i| random_versor(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);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_vault() {
|
||||
let query = random_versor(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 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 results = vault_recall_raw(&versors, &query, 5).unwrap();
|
||||
for w in results.windows(2) {
|
||||
assert!(w[0].1 >= w[1].1, "Scores not descending");
|
||||
}
|
||||
}
|
||||
}
|
||||
59
core-rs/tests/test_versor.rs
Normal file
59
core-rs/tests/test_versor.rs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::versor::{versor_apply_raw, normalize_to_versor_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")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_produces_versor() {
|
||||
for seed in 0..20u64 {
|
||||
let v = random_versor(seed);
|
||||
let cond = versor_condition_raw(&v).unwrap();
|
||||
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);
|
||||
}
|
||||
}
|
||||
81
docs/RUST.md
Normal file
81
docs/RUST.md
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
# Rust Extension (core-rs)
|
||||
|
||||
## Why Rust
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
## 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 |
|
||||
|
||||
## 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 |
|
||||
|
||||
## 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.
|
||||
|
||||
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
|
||||
|
||||
Requires maturin and a Rust toolchain (stable 1.75+).
|
||||
|
||||
```bash
|
||||
cd core-rs
|
||||
pip install maturin
|
||||
maturin develop --release # installs core_rs into current venv
|
||||
```
|
||||
|
||||
Or build a wheel:
|
||||
```bash
|
||||
maturin build --release
|
||||
pip install target/wheels/*.whl
|
||||
```
|
||||
|
||||
Verify Rust backend is active from Python:
|
||||
```python
|
||||
from algebra.backend import using_rust
|
||||
print(using_rust()) # True if core_rs is installed
|
||||
```
|
||||
|
||||
## Running Rust Tests
|
||||
|
||||
```bash
|
||||
cd core-rs
|
||||
cargo test
|
||||
```
|
||||
|
||||
## 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 error types use thiserror — every failure path is a named enum variant,
|
||||
not a string panic.
|
||||
Loading…
Reference in a new issue