feat: vault recall index, Rust versor parity, cognitive pack expansion
Phase 3 — vault exact recall index: - Replace O(N) np.array_equal scan with hash-based exact-match index - Add optional max_entries with deterministic FIFO eviction - Index rebuilds on reproject for consistency Phase 4 — Rust versor_apply parity: - Fix CGA metric signature (+,+,+,+,-) and blade ordering to match Python - Implement versor_apply_closed with null-vector preservation, f64 unitize, and construction seed fallback matching Python closure semantics - Gate Rust dispatch behind CORE_BACKEND=rust; Python remains default - Add f64 geometric product for closure-path precision Phase 5 — cognitive quality pack expansion: - Expand lexicon from 55 to 70 entries (evidence, inference, procedure, verification, distinction, relation, thought, understanding, judgment, principle, order, connectives) - Improve semantic templates for cause, procedure, comparison, recall, verification intents - Expand eval cases from 20 to 45 across all categories Validation: 491 tests pass, 45 eval cases at 100% all metrics.
This commit is contained in:
parent
cc46dca87a
commit
523c072818
17 changed files with 610 additions and 67 deletions
|
|
@ -34,11 +34,18 @@ def geometric_product(A: np.ndarray, B: np.ndarray) -> np.ndarray:
|
|||
def versor_apply(V: np.ndarray, F: np.ndarray) -> np.ndarray:
|
||||
"""Apply a versor through the canonical algebra closure boundary.
|
||||
|
||||
The Rust extension's raw sandwich path is intentionally bypassed here
|
||||
until it enforces the same closure semantics as algebra.versor. Runtime
|
||||
invariants depend on this operator returning a closed field; generation,
|
||||
propagation, and vault recall are not allowed to repair it downstream.
|
||||
When CORE_BACKEND=rust is set and the Rust extension exposes
|
||||
versor_apply_with_closure, Rust handles the full closure path
|
||||
(null-vector preservation, unitize, seed fallback). Otherwise
|
||||
falls back to pure Python algebra.versor.
|
||||
"""
|
||||
if _RUST and _REQUESTED_BACKEND == "rust":
|
||||
try:
|
||||
return np.asarray(
|
||||
_rs.versor_apply_with_closure(V, F), dtype=np.float32
|
||||
)
|
||||
except (AttributeError, Exception):
|
||||
pass
|
||||
from algebra.versor import versor_apply as _va
|
||||
return _va(V, F)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
//! CGA inner product and null-cone operations.
|
||||
//!
|
||||
//! 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
|
||||
//! Signature: (+,+,+,+,-), with Euclidean coordinates on e1,e2,e3.
|
||||
//! e4^2 = +1, e5^2 = -1.
|
||||
//!
|
||||
//! A Euclidean point x embeds as:
|
||||
//!
|
||||
//! X = x + n_o + 0.5 * |x|^2 * n_inf
|
||||
//!
|
||||
//! with e4 coeff = 0.5*(|x|^2 - 1), e5 coeff = 0.5*(|x|^2 + 1).
|
||||
//!
|
||||
//! Then X·X = 0 and X·Y = -0.5 * ||x-y||^2.
|
||||
//!
|
||||
//! This is the ONLY distance metric in CORE-AI.
|
||||
|
|
@ -53,7 +51,7 @@ pub fn embed_point_raw(p: &[f32; 3]) -> [f32; 32] {
|
|||
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 + 1.0);
|
||||
result[5] = 0.5 * (r2 - 1.0);
|
||||
result[4] = 0.5 * (r2 - 1.0);
|
||||
result[5] = 0.5 * (r2 + 1.0);
|
||||
result
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
//! 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).
|
||||
//!
|
||||
//! Blade ordering matches Python's itertools.combinations(range(5), k)
|
||||
//! lexicographic tuple order within each grade.
|
||||
//!
|
||||
//! geometric_product_raw is the inner loop called by every higher-level op.
|
||||
//! It is deliberately kept allocation-free: inputs and output are [f32;32].
|
||||
|
||||
|
|
@ -20,31 +23,30 @@ 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.
|
||||
|
||||
// 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();
|
||||
const MASK_TO_IDX: [u8; 32] = build_mask_to_idx();
|
||||
|
||||
const fn build_blade_masks() -> [u8; 32] {
|
||||
// Grade-ascending, lex order over 5 bits
|
||||
let mut masks = [0u8; 32];
|
||||
let mut pos = 0usize;
|
||||
let mut k = 0u8;
|
||||
while k <= 5 {
|
||||
// Iterate over all 5-bit masks with popcount == k
|
||||
let mut mask = 0u8;
|
||||
while mask < 32 {
|
||||
if popcount5(mask) == k {
|
||||
masks[pos] = mask;
|
||||
pos += 1;
|
||||
}
|
||||
mask += 1;
|
||||
}
|
||||
k += 1;
|
||||
}
|
||||
masks
|
||||
// Must match Python's itertools.combinations(range(5), k) order.
|
||||
// Hardcoded to guarantee exact parity with Python cl41.py.
|
||||
[
|
||||
// grade 0: ()
|
||||
0b00000,
|
||||
// grade 1: (0,), (1,), (2,), (3,), (4,)
|
||||
0b00001, 0b00010, 0b00100, 0b01000, 0b10000,
|
||||
// grade 2: (0,1), (0,2), (0,3), (0,4), (1,2), (1,3), (1,4), (2,3), (2,4), (3,4)
|
||||
0b00011, 0b00101, 0b01001, 0b10001, 0b00110, 0b01010, 0b10010, 0b01100, 0b10100, 0b11000,
|
||||
// grade 3: (0,1,2), (0,1,3), (0,1,4), (0,2,3), (0,2,4), (0,3,4), (1,2,3), (1,2,4), (1,3,4), (2,3,4)
|
||||
0b00111, 0b01011, 0b10011, 0b01101, 0b10101, 0b11001, 0b01110, 0b10110, 0b11010, 0b11100,
|
||||
// grade 4: (0,1,2,3), (0,1,2,4), (0,1,3,4), (0,2,3,4), (1,2,3,4)
|
||||
0b01111, 0b10111, 0b11011, 0b11101, 0b11110,
|
||||
// grade 5: (0,1,2,3,4)
|
||||
0b11111,
|
||||
]
|
||||
}
|
||||
|
||||
const fn build_mask_to_idx() -> [u8; 32] {
|
||||
|
|
@ -128,6 +130,25 @@ fn table() -> &'static Table {
|
|||
TABLE.get_or_init(build_table)
|
||||
}
|
||||
|
||||
/// Full geometric product in Cl(4,1) with f64 precision.
|
||||
/// Used by versor closure where residue checks need high accuracy.
|
||||
pub fn geometric_product_f64(a: &[f64; 32], b: &[f64; 32]) -> [f64; 32] {
|
||||
let t = table();
|
||||
let mut result = [0f64; 32];
|
||||
for i in 0..32 {
|
||||
let ai = a[i];
|
||||
if ai == 0.0 { continue; }
|
||||
for j in 0..32 {
|
||||
let bj = b[j];
|
||||
if bj == 0.0 { continue; }
|
||||
let k = t.idx[i][j] as usize;
|
||||
let s = t.sign[i][j] as f64;
|
||||
result[k] += s * ai * bj;
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Full geometric product in Cl(4,1).
|
||||
/// Both inputs are [f32; 32]. Returns [f32; 32]. Allocation-free.
|
||||
pub fn geometric_product_raw(a: &[f32; 32], b: &[f32; 32]) -> Result<[f32; 32], Cl41Error> {
|
||||
|
|
@ -152,9 +173,15 @@ pub fn geometric_product_raw(a: &[f32; 32], b: &[f32; 32]) -> Result<[f32; 32],
|
|||
/// Grade 0,1: +1. Grade 2,3: -1. Grade 4,5: +1.
|
||||
pub fn reverse_raw(a: &[f32; 32]) -> [f32; 32] {
|
||||
let mut r = *a;
|
||||
// Grade 2: indices 6..=15
|
||||
for i in 6..=15 { r[i] = -r[i]; }
|
||||
// Grade 3: indices 16..=25
|
||||
for i in 16..=25 { r[i] = -r[i]; }
|
||||
r
|
||||
}
|
||||
|
||||
/// Reverse anti-automorphism (f64).
|
||||
pub fn reverse_f64(a: &[f64; 32]) -> [f64; 32] {
|
||||
let mut r = *a;
|
||||
for i in 6..=15 { r[i] = -r[i]; }
|
||||
for i in 16..=25 { r[i] = -r[i]; }
|
||||
r
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ pub mod versor;
|
|||
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};
|
||||
use versor::{normalize_to_versor_raw, versor_apply_closed, versor_apply_raw, versor_condition_raw};
|
||||
|
||||
/// Geometric product in Cl(4,1). Accepts two numpy-compatible f32 arrays of length 32.
|
||||
#[pyfunction]
|
||||
|
|
@ -50,6 +50,21 @@ fn versor_apply(
|
|||
f32_array_to_numpy(py, &result)
|
||||
}
|
||||
|
||||
/// Sandwich product V*F*reverse(V) with closure semantics.
|
||||
/// Preserves null vectors, applies unit-versor closure with seed fallback.
|
||||
#[pyfunction]
|
||||
fn versor_apply_with_closure(
|
||||
py: Python<'_>,
|
||||
v: &pyo3::types::PyAny,
|
||||
f: &pyo3::types::PyAny,
|
||||
) -> PyResult<PyObject> {
|
||||
let v_slice = extract_f32_slice(v)?;
|
||||
let f_slice = extract_f32_slice(f)?;
|
||||
let result = versor_apply_closed(&v_slice, &f_slice)
|
||||
.map_err(|e| PyValueError::new_err(e.to_string()))?;
|
||||
f32_array_to_numpy(py, &result)
|
||||
}
|
||||
|
||||
/// ||F*reverse(F) - 1||_F. Returns scalar f32.
|
||||
#[pyfunction]
|
||||
fn versor_condition(f: &pyo3::types::PyAny) -> PyResult<f32> {
|
||||
|
|
@ -120,6 +135,7 @@ fn f32_array_to_numpy(py: Python<'_>, data: &[f32; 32]) -> PyResult<PyObject> {
|
|||
fn core_rs(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_function(wrap_pyfunction!(geometric_product, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(versor_apply, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(versor_apply_with_closure, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(versor_condition, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(normalize_to_versor, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(cga_inner, m)?)?;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
//! normalize_to_versor F/sqrt(|F*rev(F)|) — called once at injection gate
|
||||
//! versor_condition ||F*rev(F)-1||_F — used in tests and gate only
|
||||
|
||||
use crate::cl41::{geometric_product_raw, reverse_raw, Cl41Error};
|
||||
use crate::cl41::{geometric_product_f64, geometric_product_raw, reverse_f64, reverse_raw, Cl41Error};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
|
|
@ -15,8 +15,104 @@ pub enum VersorError {
|
|||
NullVersor(f32),
|
||||
}
|
||||
|
||||
/// Sandwich product V * F * reverse(V).
|
||||
/// Allocation-free. This is the hot path — called every generation step.
|
||||
const NEAR_ZERO_TOL: f64 = 1e-12;
|
||||
const NULL_SCALAR_TOL: f64 = 1e-9;
|
||||
const CONSTRUCTION_RESIDUE_TOL: f64 = 1e-2;
|
||||
const SEED_BIVECTORS: [usize; 6] = [6, 7, 8, 10, 11, 13];
|
||||
|
||||
fn is_null_vector(v: &[f32; 32]) -> bool {
|
||||
use crate::cga::cga_inner_raw;
|
||||
// Generous tolerance: the f32 sandwich product introduces ~1e-6 error
|
||||
// on null vectors; 1e-5 correctly classifies them without false positives
|
||||
// on actual versors (which have cga_inner >> 0.1).
|
||||
match cga_inner_raw(v, v) {
|
||||
Ok(inner) => (inner as f64).abs() < 1e-5,
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn unitize_closed(v: &[f64; 32]) -> Result<[f64; 32], ()> {
|
||||
let input_norm: f64 = v.iter().map(|x| x * x).sum::<f64>().sqrt();
|
||||
if input_norm < NEAR_ZERO_TOL {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
let rev = reverse_f64(v);
|
||||
let vv = geometric_product_f64(v, &rev);
|
||||
|
||||
let scalar_sq = vv[0];
|
||||
let residue_norm: f64 = vv[1..].iter().map(|x| x * x).sum::<f64>().sqrt();
|
||||
|
||||
if residue_norm >= CONSTRUCTION_RESIDUE_TOL {
|
||||
return Err(());
|
||||
}
|
||||
if scalar_sq <= 0.0 {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
let inv = 1.0 / scalar_sq.sqrt();
|
||||
let mut result = *v;
|
||||
for x in result.iter_mut() { *x *= inv; }
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn seed_to_rotor(v: &[f64; 32]) -> Result<[f64; 32], ()> {
|
||||
let scale: f64 = v.iter().map(|x| x * x).sum::<f64>().sqrt();
|
||||
let scale = if scale == 0.0 { 1.0 } else { scale };
|
||||
|
||||
let mut rotor = [0f64; 32];
|
||||
rotor[0] = 1.0;
|
||||
|
||||
for (step, &blade) in SEED_BIVECTORS.iter().enumerate() {
|
||||
let source = v[(blade + step) % 32] / scale;
|
||||
let theta = 0.5 * source.tanh();
|
||||
let mut factor = [0f64; 32];
|
||||
factor[0] = theta.cos();
|
||||
factor[blade] = theta.sin();
|
||||
|
||||
rotor = geometric_product_f64(&rotor, &factor);
|
||||
}
|
||||
|
||||
unitize_closed(&rotor)
|
||||
}
|
||||
|
||||
fn close_applied_versor(v: &[f32; 32]) -> [f32; 32] {
|
||||
if is_null_vector(v) {
|
||||
return crate::cga::null_project_raw(v);
|
||||
}
|
||||
|
||||
let v_f64: [f64; 32] = {
|
||||
let mut arr = [0f64; 32];
|
||||
for i in 0..32 { arr[i] = v[i] as f64; }
|
||||
arr
|
||||
};
|
||||
|
||||
if let Ok(closed) = unitize_closed(&v_f64) {
|
||||
let mut result = [0f32; 32];
|
||||
for i in 0..32 { result[i] = closed[i] as f32; }
|
||||
return result;
|
||||
}
|
||||
|
||||
if let Ok(seeded) = seed_to_rotor(&v_f64) {
|
||||
let mut result = [0f32; 32];
|
||||
for i in 0..32 { result[i] = seeded[i] as f32; }
|
||||
return result;
|
||||
}
|
||||
|
||||
*v
|
||||
}
|
||||
|
||||
/// Sandwich product V * F * reverse(V) with closure semantics.
|
||||
/// Preserves null vectors as null vectors. Applies unit-versor closure
|
||||
/// with construction seed fallback for non-null results.
|
||||
pub fn versor_apply_closed(v: &[f32; 32], f: &[f32; 32]) -> Result<[f32; 32], VersorError> {
|
||||
let rev_v = reverse_raw(v);
|
||||
let vf = geometric_product_raw(v, f)?;
|
||||
let vfrv = geometric_product_raw(&vf, &rev_v)?;
|
||||
Ok(close_applied_versor(&vfrv))
|
||||
}
|
||||
|
||||
/// Raw sandwich product V * F * reverse(V) without closure.
|
||||
pub fn versor_apply_raw(v: &[f32; 32], f: &[f32; 32]) -> Result<[f32; 32], VersorError> {
|
||||
let rev_v = reverse_raw(v);
|
||||
let vf = geometric_product_raw(v, f)?;
|
||||
|
|
@ -48,3 +144,50 @@ pub fn versor_condition_raw(f: &[f32; 32]) -> Result<f32, VersorError> {
|
|||
let norm_sq: f32 = frv.iter().map(|x| x * x).sum();
|
||||
Ok(norm_sq.sqrt())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn identity_versor() -> [f32; 32] {
|
||||
let mut v = [0f32; 32];
|
||||
v[0] = 1.0;
|
||||
v
|
||||
}
|
||||
|
||||
fn simple_reflector() -> [f32; 32] {
|
||||
let mut v = [0f32; 32];
|
||||
v[1] = 1.0;
|
||||
v
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closed_identity_is_identity() {
|
||||
let id = identity_versor();
|
||||
let f = simple_reflector();
|
||||
let result = versor_apply_closed(&id, &f).unwrap();
|
||||
for i in 0..32 {
|
||||
assert!((result[i] - f[i]).abs() < 1e-5, "component {} diverged", i);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closed_preserves_versor_condition() {
|
||||
let v = simple_reflector();
|
||||
let f = identity_versor();
|
||||
let result = versor_apply_closed(&v, &f).unwrap();
|
||||
let cond = versor_condition_raw(&result).unwrap();
|
||||
assert!(cond < 1e-4, "condition {} too large", cond);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closed_matches_raw_for_identity() {
|
||||
let id = identity_versor();
|
||||
let f = simple_reflector();
|
||||
let raw = versor_apply_raw(&id, &f).unwrap();
|
||||
let closed = versor_apply_closed(&id, &f).unwrap();
|
||||
for i in 0..32 {
|
||||
assert!((raw[i] - closed[i]).abs() < 1e-5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,17 +20,17 @@ fn test_e1_squared_is_plus1() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_e4_squared_is_minus1() {
|
||||
fn test_e4_squared_is_plus1() {
|
||||
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]);
|
||||
assert!((r[0] - 1.0).abs() < 1e-6, "e4^2 should be +1, got {}", r[0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_e5_squared_is_plus1() {
|
||||
fn test_e5_squared_is_minus1() {
|
||||
let e5 = basis(4);
|
||||
let r = geometric_product_raw(&e5, &e5).unwrap();
|
||||
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]
|
||||
|
|
|
|||
|
|
@ -18,3 +18,28 @@
|
|||
{"id": "unknown_word_018", "category": "unknown", "prompt": "word beginning truth", "expected_intent": "unknown", "expected_terms": ["word", "truth"], "expected_surface_contains": [], "requires_versor_closure": true, "requires_deterministic_trace": true}
|
||||
{"id": "unknown_logos_019", "category": "unknown", "prompt": "light logos", "expected_intent": "unknown", "expected_terms": ["light"], "expected_surface_contains": [], "requires_versor_closure": true, "requires_deterministic_trace": true}
|
||||
{"id": "definition_wisdom_020", "category": "definition", "prompt": "What is wisdom?", "expected_intent": "definition", "expected_terms": ["wisdom"], "expected_surface_contains": ["wisdom"], "requires_versor_closure": true, "requires_deterministic_trace": true}
|
||||
{"id": "definition_evidence_021", "category": "definition", "prompt": "What is evidence?", "expected_intent": "definition", "expected_terms": ["evidence"], "expected_surface_contains": ["evidence"], "requires_versor_closure": true, "requires_deterministic_trace": true}
|
||||
{"id": "definition_inference_022", "category": "definition", "prompt": "What is inference?", "expected_intent": "definition", "expected_terms": ["inference"], "expected_surface_contains": ["inference"], "requires_versor_closure": true, "requires_deterministic_trace": true}
|
||||
{"id": "definition_procedure_023", "category": "definition", "prompt": "What is a procedure?", "expected_intent": "definition", "expected_terms": ["procedure"], "expected_surface_contains": ["procedure"], "requires_versor_closure": true, "requires_deterministic_trace": true}
|
||||
{"id": "definition_verification_024", "category": "definition", "prompt": "What is verification?", "expected_intent": "definition", "expected_terms": ["verification"], "expected_surface_contains": ["verification"], "requires_versor_closure": true, "requires_deterministic_trace": true}
|
||||
{"id": "definition_distinction_025", "category": "definition", "prompt": "What is distinction?", "expected_intent": "definition", "expected_terms": ["distinction"], "expected_surface_contains": ["distinction"], "requires_versor_closure": true, "requires_deterministic_trace": true}
|
||||
{"id": "definition_relation_026", "category": "definition", "prompt": "What is a relation?", "expected_intent": "definition", "expected_terms": ["relation"], "expected_surface_contains": ["relation"], "requires_versor_closure": true, "requires_deterministic_trace": true}
|
||||
{"id": "definition_identity_027", "category": "definition", "prompt": "What is identity?", "expected_intent": "definition", "expected_terms": ["identity"], "expected_surface_contains": ["identity"], "requires_versor_closure": true, "requires_deterministic_trace": true}
|
||||
{"id": "comparison_reason_cause_028", "category": "comparison", "prompt": "Compare reason and cause", "expected_intent": "comparison", "expected_terms": ["reason", "cause"], "expected_surface_contains": ["reason", "cause"], "requires_versor_closure": true, "requires_deterministic_trace": true}
|
||||
{"id": "comparison_knowledge_wisdom_029", "category": "comparison", "prompt": "Compare knowledge and wisdom", "expected_intent": "comparison", "expected_terms": ["knowledge", "wisdom"], "expected_surface_contains": ["knowledge", "wisdom"], "requires_versor_closure": true, "requires_deterministic_trace": true}
|
||||
{"id": "comparison_memory_recall_030", "category": "comparison", "prompt": "Compare memory and recall", "expected_intent": "comparison", "expected_terms": ["memory"], "expected_surface_contains": ["memory"], "requires_versor_closure": true, "requires_deterministic_trace": true}
|
||||
{"id": "cause_truth_031", "category": "cause", "prompt": "Why does truth matter?", "expected_intent": "cause", "expected_terms": ["truth"], "expected_surface_contains": ["truth"], "requires_versor_closure": true, "requires_deterministic_trace": true}
|
||||
{"id": "cause_knowledge_032", "category": "cause", "prompt": "Why does knowledge require evidence?", "expected_intent": "cause", "expected_terms": ["knowledge"], "expected_surface_contains": ["knowledge"], "requires_versor_closure": true, "requires_deterministic_trace": true}
|
||||
{"id": "cause_correction_033", "category": "cause", "prompt": "Why does correction help learning?", "expected_intent": "cause", "expected_terms": ["correction"], "expected_surface_contains": ["correction"], "requires_versor_closure": true, "requires_deterministic_trace": true}
|
||||
{"id": "procedure_verify_034", "category": "procedure", "prompt": "How do I verify a claim?", "expected_intent": "procedure", "expected_terms": [], "expected_surface_contains": [], "requires_versor_closure": true, "requires_deterministic_trace": true}
|
||||
{"id": "procedure_correct_035", "category": "procedure", "prompt": "How can I correct an error?", "expected_intent": "procedure", "expected_terms": [], "expected_surface_contains": [], "requires_versor_closure": true, "requires_deterministic_trace": true}
|
||||
{"id": "verification_wisdom_036", "category": "verification", "prompt": "Is wisdom the same as knowledge?", "expected_intent": "verification", "expected_terms": ["wisdom", "knowledge"], "expected_surface_contains": ["wisdom"], "requires_versor_closure": true, "requires_deterministic_trace": true}
|
||||
{"id": "verification_memory_037", "category": "verification", "prompt": "Does memory require recall?", "expected_intent": "verification", "expected_terms": ["memory"], "expected_surface_contains": ["memory"], "requires_versor_closure": true, "requires_deterministic_trace": true}
|
||||
{"id": "recall_wisdom_038", "category": "recall", "prompt": "Remember wisdom", "expected_intent": "recall", "expected_terms": ["wisdom"], "expected_surface_contains": ["wisdom"], "requires_versor_closure": true, "requires_deterministic_trace": true}
|
||||
{"id": "recall_knowledge_039", "category": "recall", "prompt": "Remember knowledge", "expected_intent": "recall", "expected_terms": ["knowledge"], "expected_surface_contains": ["knowledge"], "requires_versor_closure": true, "requires_deterministic_trace": true}
|
||||
{"id": "correction_truth_040", "category": "correction", "prompt": "Actually, truth requires evidence", "expected_intent": "correction", "expected_terms": ["truth"], "expected_surface_contains": ["correction"], "requires_versor_closure": true, "requires_deterministic_trace": true}
|
||||
{"id": "unknown_spirit_041", "category": "unknown", "prompt": "spirit wisdom truth", "expected_intent": "unknown", "expected_terms": ["wisdom", "truth"], "expected_surface_contains": [], "requires_versor_closure": true, "requires_deterministic_trace": true}
|
||||
{"id": "unknown_evidence_042", "category": "unknown", "prompt": "evidence reason", "expected_intent": "unknown", "expected_terms": ["evidence", "reason"], "expected_surface_contains": [], "requires_versor_closure": true, "requires_deterministic_trace": true}
|
||||
{"id": "definition_thought_043", "category": "definition", "prompt": "What is thought?", "expected_intent": "definition", "expected_terms": ["thought"], "expected_surface_contains": ["thought"], "requires_versor_closure": true, "requires_deterministic_trace": true}
|
||||
{"id": "definition_judgment_044", "category": "definition", "prompt": "What is judgment?", "expected_intent": "definition", "expected_terms": ["judgment"], "expected_surface_contains": ["judgment"], "requires_versor_closure": true, "requires_deterministic_trace": true}
|
||||
{"id": "definition_understanding_045", "category": "definition", "prompt": "What is understanding?", "expected_intent": "definition", "expected_terms": ["understanding"], "expected_surface_contains": ["understanding"], "requires_versor_closure": true, "requires_deterministic_trace": true}
|
||||
|
|
|
|||
|
|
@ -18,12 +18,12 @@ from generate.intent import IntentTag
|
|||
|
||||
_INTENT_TEMPLATES: dict[IntentTag, str] = {
|
||||
IntentTag.DEFINITION: "{subject} is defined as {obj}",
|
||||
IntentTag.CAUSE: "{subject} is caused by {obj}",
|
||||
IntentTag.PROCEDURE: "{subject} has the following steps: {obj}",
|
||||
IntentTag.COMPARISON: "{subject} and {secondary} are contrasted by {predicate_h}",
|
||||
IntentTag.CAUSE: "{subject} is grounded in {obj}",
|
||||
IntentTag.PROCEDURE: "first, {obj}; then, {subject} follows",
|
||||
IntentTag.COMPARISON: "{subject} and {secondary} are distinguished: {subject} {predicate_h} {secondary}",
|
||||
IntentTag.CORRECTION: "correction: {subject} {predicate_h} {obj}",
|
||||
IntentTag.RECALL: "{subject} recalls {obj}",
|
||||
IntentTag.VERIFICATION: "{subject} is verified as {obj}",
|
||||
IntentTag.RECALL: "recalling {subject}: {obj}",
|
||||
IntentTag.VERIFICATION: "{subject} is verified: {obj}",
|
||||
IntentTag.UNKNOWN: "{subject} {predicate_h} {obj}",
|
||||
}
|
||||
|
||||
|
|
@ -46,6 +46,14 @@ _PREDICATE_HUMANIZE: dict[str, str] = {
|
|||
"follows": "follows",
|
||||
"belongs_to": "belongs to",
|
||||
"answers": "answers",
|
||||
"is_grounded_in": "is grounded in",
|
||||
"is_distinguished_from": "is distinguished from",
|
||||
"implies": "implies",
|
||||
"entails": "entails",
|
||||
"requires": "requires",
|
||||
"verifies": "verifies",
|
||||
"evidences": "evidences",
|
||||
"orders": "orders",
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,14 @@ _PREDICATE_DISPLAY: dict[str, str] = {
|
|||
"follows": "follows",
|
||||
"belongs_to": "belongs to",
|
||||
"answers": "answers",
|
||||
"is_grounded_in": "is grounded in",
|
||||
"is_distinguished_from": "is distinguished from",
|
||||
"implies": "implies",
|
||||
"entails": "entails",
|
||||
"requires": "requires",
|
||||
"verifies": "verifies",
|
||||
"evidences": "evidences",
|
||||
"orders": "orders",
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -53,3 +53,18 @@
|
|||
{"entry_id":"en-core-cog-053","surface":"how","lemma":"how","language":"en","pos":"ADV","semantic_domains":["dialogue.question.how","cognition.procedure","epistemic.seeking"],"morphology_tags":["adverb","interrogative"],"provenance_ids":["seed:core_cognition_v1"]}
|
||||
{"entry_id":"en-core-cog-054","surface":"compare_with","lemma":"compare","language":"en","pos":"VERB","semantic_domains":["operation.compare","relation.contrast","dialogue.request"],"morphology_tags":["verb","operation"],"provenance_ids":["seed:core_cognition_v1"]}
|
||||
{"entry_id":"en-core-cog-055","surface":"because","lemma":"because","language":"en","pos":"SCONJ","semantic_domains":["relation.causal","explanation.ground","dialogue.reason"],"morphology_tags":["conjunction"],"provenance_ids":["seed:core_cognition_v1"]}
|
||||
{"entry_id":"en-core-cog-056","surface":"evidence","lemma":"evidence","language":"en","pos":"NOUN","semantic_domains":["cognition.evidence","epistemic.ground","reason.support","verification.basis"],"morphology_tags":["noun"],"provenance_ids":["seed:core_cognition_v1"]}
|
||||
{"entry_id":"en-core-cog-057","surface":"inference","lemma":"inference","language":"en","pos":"NOUN","semantic_domains":["cognition.inference","logic.derivation","reason.step","epistemic.formation"],"morphology_tags":["noun"],"provenance_ids":["seed:core_cognition_v1"]}
|
||||
{"entry_id":"en-core-cog-058","surface":"procedure","lemma":"procedure","language":"en","pos":"NOUN","semantic_domains":["cognition.procedure","operation.ordered","reason.method","dialogue.how"],"morphology_tags":["noun"],"provenance_ids":["seed:core_cognition_v1"]}
|
||||
{"entry_id":"en-core-cog-059","surface":"verification","lemma":"verification","language":"en","pos":"NOUN","semantic_domains":["cognition.verification","epistemic.check","truth.test","dialogue.confirm"],"morphology_tags":["noun"],"provenance_ids":["seed:core_cognition_v1"]}
|
||||
{"entry_id":"en-core-cog-060","surface":"distinction","lemma":"distinction","language":"en","pos":"NOUN","semantic_domains":["cognition.distinction","relation.contrast","reason.boundary","comparison.basis"],"morphology_tags":["noun"],"provenance_ids":["seed:core_cognition_v1"]}
|
||||
{"entry_id":"en-core-cog-061","surface":"relation","lemma":"relation","language":"en","pos":"NOUN","semantic_domains":["cognition.relation","reason.structure","graph.edge","semantics.link"],"morphology_tags":["noun"],"provenance_ids":["seed:core_cognition_v1"]}
|
||||
{"entry_id":"en-core-cog-062","surface":"thought","lemma":"thought","language":"en","pos":"NOUN","semantic_domains":["cognition.thought","logos.internal","reason.process","meaning.formation"],"morphology_tags":["noun"],"provenance_ids":["seed:core_cognition_v1"]}
|
||||
{"entry_id":"en-core-cog-063","surface":"understanding","lemma":"understanding","language":"en","pos":"NOUN","semantic_domains":["cognition.understanding","epistemic.grasp","meaning.comprehension","reason.synthesis"],"morphology_tags":["noun"],"provenance_ids":["seed:core_cognition_v1"]}
|
||||
{"entry_id":"en-core-cog-064","surface":"judgment","lemma":"judgment","language":"en","pos":"NOUN","semantic_domains":["cognition.judgment","epistemic.evaluation","reason.decision","value.assessment"],"morphology_tags":["noun"],"provenance_ids":["seed:core_cognition_v1"]}
|
||||
{"entry_id":"en-core-cog-065","surface":"principle","lemma":"principle","language":"en","pos":"NOUN","semantic_domains":["cognition.principle","reason.axiom","epistemic.ground","logos.foundational"],"morphology_tags":["noun"],"provenance_ids":["seed:core_cognition_v1"]}
|
||||
{"entry_id":"en-core-cog-066","surface":"order","lemma":"order","language":"en","pos":"NOUN","semantic_domains":["cognition.order","reason.structure","temporal.sequence","logos.arrangement"],"morphology_tags":["noun"],"provenance_ids":["seed:core_cognition_v1"]}
|
||||
{"entry_id":"en-core-cog-067","surface":"therefore","lemma":"therefore","language":"en","pos":"ADV","semantic_domains":["relation.consequence","logic.derivation","dialogue.conclusion"],"morphology_tags":["adverb","connective"],"provenance_ids":["seed:core_cognition_v1"]}
|
||||
{"entry_id":"en-core-cog-068","surface":"however","lemma":"however","language":"en","pos":"ADV","semantic_domains":["relation.contrast","dialogue.concession","reason.qualification"],"morphology_tags":["adverb","connective"],"provenance_ids":["seed:core_cognition_v1"]}
|
||||
{"entry_id":"en-core-cog-069","surface":"then","lemma":"then","language":"en","pos":"ADV","semantic_domains":["relation.sequence.after","temporal.sequence","dialogue.continuation"],"morphology_tags":["adverb","connective"],"provenance_ids":["seed:core_cognition_v1"]}
|
||||
{"entry_id":"en-core-cog-070","surface":"first","lemma":"first","language":"en","pos":"ADV","semantic_domains":["relation.sequence.before","temporal.ordinal","procedure.step","dialogue.opening"],"morphology_tags":["adverb","ordinal"],"provenance_ids":["seed:core_cognition_v1"]}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
"normalization_policy": "unitize_versor",
|
||||
"source_manifest": "en_core_cognition_v1.lexicon.jsonl",
|
||||
"determinism_class": "D0",
|
||||
"checksum": "d8d485e374afaf0520f88d753353a18d82e18c5b88a2f773622b6de1942a05c8",
|
||||
"version": "1.0.0",
|
||||
"checksum": "994e63c5d72053691a09502bcac0fc6f863cd5af08372022399c95b1606ad5b3",
|
||||
"version": "1.1.0",
|
||||
"gate_engaged": true,
|
||||
"oov_policy": "tagged_fallback"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,4 +114,4 @@ def test_pack_entries_are_deterministic() -> None:
|
|||
assert [entry.entry_id for entry in entries_a] == [entry.entry_id for entry in entries_b]
|
||||
assert [entry.surface for entry in entries_a] == [entry.surface for entry in entries_b]
|
||||
assert entries_a[0].entry_id == "en-core-cog-001"
|
||||
assert entries_a[-1].entry_id == "en-core-cog-055"
|
||||
assert entries_a[-1].entry_id == "en-core-cog-070"
|
||||
|
|
|
|||
|
|
@ -37,4 +37,4 @@ def test_load_pack_entries_returns_new_list_from_cached_tuple() -> None:
|
|||
entries_a.pop()
|
||||
|
||||
assert len(entries_a) == len(entries_b) - 1
|
||||
assert entries_b[-1].entry_id == "en-core-cog-055"
|
||||
assert entries_b[-1].entry_id == "en-core-cog-070"
|
||||
|
|
|
|||
135
tests/test_rust_backend.py
Normal file
135
tests/test_rust_backend.py
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
"""Rust versor_apply parity tests.
|
||||
|
||||
Verifies that the Rust closure-aware versor_apply produces identical results
|
||||
to the Python algebra.versor.versor_apply for all critical cases:
|
||||
identity, rotors, null vectors, versor condition, and backend dispatch.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from algebra.versor import (
|
||||
unitize_versor,
|
||||
versor_apply as python_versor_apply,
|
||||
versor_condition,
|
||||
)
|
||||
from algebra.cga import embed_point, is_null
|
||||
|
||||
try:
|
||||
import core_rs
|
||||
HAS_RUST = True
|
||||
except ImportError:
|
||||
HAS_RUST = False
|
||||
|
||||
skip_no_rust = pytest.mark.skipif(not HAS_RUST, reason="core_rs not available")
|
||||
|
||||
|
||||
def _positive_unit_reflector(seed: int) -> np.ndarray:
|
||||
rng = np.random.default_rng(seed)
|
||||
vec4 = rng.standard_normal(4).astype(np.float32)
|
||||
norm4 = float(np.linalg.norm(vec4))
|
||||
if norm4 < 1e-6:
|
||||
vec4[0] = 1.0
|
||||
norm4 = 1.0
|
||||
vec = np.zeros(5, dtype=np.float32)
|
||||
vec[:4] = vec4
|
||||
vec[4] = 0.25 * norm4 * np.tanh(float(rng.standard_normal()))
|
||||
mv = np.zeros(32, dtype=np.float32)
|
||||
mv[1:6] = vec
|
||||
return unitize_versor(mv)
|
||||
|
||||
|
||||
def _random_rotor(seed: int) -> np.ndarray:
|
||||
from algebra.cl41 import geometric_product as gp
|
||||
a = _positive_unit_reflector(seed)
|
||||
b = _positive_unit_reflector(seed + 10000)
|
||||
return unitize_versor(gp(a, b))
|
||||
|
||||
|
||||
@skip_no_rust
|
||||
def test_rust_versor_apply_matches_python_for_identity():
|
||||
identity = np.zeros(32, dtype=np.float32)
|
||||
identity[0] = 1.0
|
||||
F = _positive_unit_reflector(42)
|
||||
|
||||
py_result = python_versor_apply(identity, F)
|
||||
rust_result = np.asarray(
|
||||
core_rs.versor_apply_with_closure(identity, F), dtype=np.float32
|
||||
)
|
||||
|
||||
assert np.allclose(py_result, rust_result, atol=1e-4), (
|
||||
f"max diff: {np.max(np.abs(py_result - rust_result))}"
|
||||
)
|
||||
|
||||
|
||||
@skip_no_rust
|
||||
def test_rust_versor_apply_matches_python_for_rotors():
|
||||
for seed in range(20):
|
||||
V = _random_rotor(seed)
|
||||
F = _positive_unit_reflector(seed + 500)
|
||||
|
||||
py_result = python_versor_apply(V, F)
|
||||
rust_result = np.asarray(
|
||||
core_rs.versor_apply_with_closure(V, F), dtype=np.float32
|
||||
)
|
||||
|
||||
assert np.allclose(py_result, rust_result, atol=1e-3), (
|
||||
f"seed={seed} max diff: {np.max(np.abs(py_result - rust_result))}"
|
||||
)
|
||||
|
||||
|
||||
@skip_no_rust
|
||||
def test_rust_versor_apply_preserves_null_vectors():
|
||||
point = embed_point(np.array([1.0, 2.0, 3.0], dtype=np.float32))
|
||||
assert is_null(point)
|
||||
|
||||
V = _positive_unit_reflector(7)
|
||||
rust_result = np.asarray(
|
||||
core_rs.versor_apply_with_closure(V, point), dtype=np.float32
|
||||
)
|
||||
py_result = python_versor_apply(V, point)
|
||||
|
||||
py_is_null = is_null(py_result)
|
||||
rust_is_null = is_null(rust_result)
|
||||
assert py_is_null == rust_is_null, (
|
||||
f"null preservation mismatch: python={py_is_null}, rust={rust_is_null}"
|
||||
)
|
||||
|
||||
|
||||
@skip_no_rust
|
||||
def test_rust_versor_apply_preserves_versor_condition():
|
||||
for seed in range(20):
|
||||
V = _positive_unit_reflector(seed)
|
||||
F = _positive_unit_reflector(seed + 1000)
|
||||
|
||||
rust_result = np.asarray(
|
||||
core_rs.versor_apply_with_closure(V, F), dtype=np.float32
|
||||
)
|
||||
cond = versor_condition(rust_result)
|
||||
assert cond < 1e-4, f"seed={seed} condition={cond:.2e}"
|
||||
|
||||
|
||||
@skip_no_rust
|
||||
def test_backend_dispatch_uses_rust_only_when_enabled():
|
||||
"""Verify that algebra.backend.versor_apply only uses Rust when CORE_BACKEND=rust."""
|
||||
import os
|
||||
from importlib import reload
|
||||
import algebra.backend as backend_mod
|
||||
|
||||
original = os.environ.get("CORE_BACKEND", "")
|
||||
|
||||
os.environ["CORE_BACKEND"] = "numpy"
|
||||
reload(backend_mod)
|
||||
assert not (backend_mod._REQUESTED_BACKEND == "rust")
|
||||
|
||||
os.environ["CORE_BACKEND"] = "rust"
|
||||
reload(backend_mod)
|
||||
assert backend_mod._REQUESTED_BACKEND == "rust"
|
||||
|
||||
if original:
|
||||
os.environ["CORE_BACKEND"] = original
|
||||
else:
|
||||
os.environ.pop("CORE_BACKEND", None)
|
||||
reload(backend_mod)
|
||||
|
|
@ -115,7 +115,7 @@ class TestRealizeSemantic:
|
|||
target = plan_articulation(graph)
|
||||
plan = realize_semantic(target, graph)
|
||||
assert plan.surface
|
||||
assert "is caused by" in plan.surface.lower()
|
||||
assert "is grounded in" in plan.surface.lower()
|
||||
|
||||
def test_empty_target_returns_empty_plan(self) -> None:
|
||||
from generate.graph_planner import ArticulationTarget
|
||||
|
|
|
|||
111
tests/test_vault_store.py
Normal file
111
tests/test_vault_store.py
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
"""Tests for VaultStore exact-match index and optional bounded mode."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
from algebra.cga import embed_point
|
||||
from vault.store import VaultStore, _versor_key
|
||||
|
||||
|
||||
def _random_point(seed: int = 0) -> np.ndarray:
|
||||
rng = np.random.default_rng(seed)
|
||||
return embed_point(rng.standard_normal(3).astype(np.float32))
|
||||
|
||||
|
||||
def test_vault_exact_self_match_uses_index():
|
||||
"""Exact self-match must come from the hash index, not O(N) scan."""
|
||||
vault = VaultStore()
|
||||
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(points):
|
||||
key = _versor_key(v)
|
||||
assert key in vault._exact_index
|
||||
assert i in vault._exact_index[key]
|
||||
|
||||
for i, v in enumerate(points):
|
||||
results = vault.recall(v, top_k=1)
|
||||
assert results[0]["metadata"]["id"] == i
|
||||
|
||||
|
||||
def test_vault_recall_ranking_unchanged():
|
||||
"""CGA inner-product ranking must be identical to pre-index behavior."""
|
||||
vault = VaultStore()
|
||||
points = [_random_point(i) for i in range(10)]
|
||||
for i, v in enumerate(points):
|
||||
vault.store(v, {"id": i})
|
||||
|
||||
query = _random_point(99)
|
||||
results = vault.recall(query, top_k=5)
|
||||
assert len(results) == 5
|
||||
scores = [r["score"] for r in results]
|
||||
assert scores == sorted(scores, reverse=True)
|
||||
|
||||
|
||||
def test_vault_index_updates_on_store():
|
||||
"""Each store() must update the hash index."""
|
||||
vault = VaultStore()
|
||||
p = _random_point(0)
|
||||
vault.store(p, {"id": "a"})
|
||||
key = _versor_key(p)
|
||||
assert key in vault._exact_index
|
||||
assert vault._exact_index[key] == [0]
|
||||
|
||||
vault.store(p, {"id": "b"})
|
||||
assert vault._exact_index[key] == [0, 1]
|
||||
|
||||
|
||||
def test_vault_index_rebuilds_on_reproject():
|
||||
"""Reproject changes versor bytes; index must be rebuilt."""
|
||||
vault = VaultStore()
|
||||
for i in range(5):
|
||||
vault.store(_random_point(i))
|
||||
vault.reproject()
|
||||
assert len(vault._exact_index) == 5
|
||||
assert len(vault) == 5
|
||||
|
||||
|
||||
def test_vault_optional_max_entries_eviction_is_deterministic():
|
||||
"""Bounded vault must evict oldest first (FIFO), deterministically."""
|
||||
vault = VaultStore(max_entries=3)
|
||||
ids = []
|
||||
for i in range(5):
|
||||
vault.store(_random_point(i), {"id": i})
|
||||
ids.append(i)
|
||||
|
||||
assert len(vault) == 3
|
||||
remaining_ids = [m["id"] for m in vault._metadata]
|
||||
assert remaining_ids == [2, 3, 4]
|
||||
|
||||
|
||||
def test_vault_default_remains_unbounded():
|
||||
"""Default max_entries=None means no eviction ever."""
|
||||
vault = VaultStore()
|
||||
assert vault.max_entries is None
|
||||
for i in range(100):
|
||||
vault.store(_random_point(i))
|
||||
assert len(vault) == 100
|
||||
|
||||
|
||||
def test_vault_eviction_preserves_index_consistency():
|
||||
"""After eviction, the exact index must reference valid current indices."""
|
||||
vault = VaultStore(max_entries=3)
|
||||
for i in range(5):
|
||||
vault.store(_random_point(i), {"id": i})
|
||||
|
||||
for indices in vault._exact_index.values():
|
||||
for idx in indices:
|
||||
assert 0 <= idx < len(vault)
|
||||
|
||||
|
||||
def test_vault_duplicate_versors_both_indexed():
|
||||
"""Storing the same versor twice should index both entries."""
|
||||
vault = VaultStore()
|
||||
p = _random_point(42)
|
||||
vault.store(p, {"id": "first"})
|
||||
vault.store(p, {"id": "second"})
|
||||
|
||||
results = vault.recall(p, top_k=2)
|
||||
result_ids = {r["metadata"]["id"] for r in results}
|
||||
assert result_ids == {"first", "second"}
|
||||
|
|
@ -5,28 +5,53 @@ No HNSW. No approximate nearest neighbor. No index rebuild.
|
|||
Recall is exact and deterministic over stored versors. When the query is the
|
||||
same point that was stored, exact self-match is promoted ahead of metric ties
|
||||
or CGA-sign artifacts.
|
||||
|
||||
Exact self-match uses a hash index (versor bytes -> stored indices) instead of
|
||||
O(N) np.array_equal scans.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
from algebra.backend import vault_recall
|
||||
from algebra.cga import null_project
|
||||
|
||||
|
||||
def _versor_key(F: np.ndarray) -> bytes:
|
||||
return np.asarray(F, dtype=np.float32).tobytes()
|
||||
|
||||
|
||||
class VaultStore:
|
||||
def __init__(self, reproject_interval: int = 100):
|
||||
self._versors: list = []
|
||||
self._metadata: list = []
|
||||
def __init__(
|
||||
self,
|
||||
reproject_interval: int = 100,
|
||||
max_entries: int | None = None,
|
||||
):
|
||||
self._versors: list[np.ndarray] = []
|
||||
self._metadata: list[dict] = []
|
||||
self._store_count: int = 0
|
||||
self._reproject_interval = reproject_interval
|
||||
self._max_entries = max_entries
|
||||
self._exact_index: dict[bytes, list[int]] = {}
|
||||
|
||||
def store(self, F: np.ndarray, metadata: dict = None) -> int:
|
||||
def store(self, F: np.ndarray, metadata: dict | None = None) -> int:
|
||||
"""Store a versor. Returns its index. Auto-reprojects every N stores."""
|
||||
self._versors.append(np.asarray(F, dtype=np.float32).copy())
|
||||
arr = np.asarray(F, dtype=np.float32).copy()
|
||||
|
||||
if self._max_entries is not None and len(self._versors) >= self._max_entries:
|
||||
self._evict_oldest()
|
||||
|
||||
self._versors.append(arr)
|
||||
self._metadata.append(metadata or {})
|
||||
idx = len(self._versors) - 1
|
||||
|
||||
key = _versor_key(arr)
|
||||
self._exact_index.setdefault(key, []).append(idx)
|
||||
|
||||
self._store_count += 1
|
||||
if self._reproject_interval > 0 and self._store_count % self._reproject_interval == 0:
|
||||
self.reproject()
|
||||
return len(self._versors) - 1
|
||||
return idx
|
||||
|
||||
def recall(self, query: np.ndarray, top_k: int = 5) -> list:
|
||||
"""
|
||||
|
|
@ -39,13 +64,11 @@ class VaultStore:
|
|||
query_arr = np.asarray(query, dtype=np.float32)
|
||||
ranked = vault_recall(self._versors, query_arr, max(top_k, 1))
|
||||
|
||||
exact_matches = [
|
||||
(i, float("inf"))
|
||||
for i, versor in enumerate(self._versors)
|
||||
if np.array_equal(np.asarray(versor, dtype=np.float32), query_arr)
|
||||
]
|
||||
if exact_matches:
|
||||
seen = {i for i, _score in exact_matches}
|
||||
key = _versor_key(query_arr)
|
||||
exact_indices = self._exact_index.get(key, [])
|
||||
if exact_indices:
|
||||
exact_matches = [(i, float("inf")) for i in exact_indices]
|
||||
seen = set(exact_indices)
|
||||
ranked = exact_matches + [(i, score) for i, score in ranked if i not in seen]
|
||||
|
||||
return [
|
||||
|
|
@ -64,16 +87,43 @@ class VaultStore:
|
|||
Corrects floating-point drift. Run between turns or asynchronously.
|
||||
"""
|
||||
self._versors = [null_project(v) for v in self._versors]
|
||||
self._rebuild_index()
|
||||
|
||||
def _rebuild_index(self) -> None:
|
||||
self._exact_index = {}
|
||||
for i, v in enumerate(self._versors):
|
||||
key = _versor_key(v)
|
||||
self._exact_index.setdefault(key, []).append(i)
|
||||
|
||||
def _evict_oldest(self) -> None:
|
||||
"""Remove the oldest entry. Deterministic FIFO eviction."""
|
||||
if not self._versors:
|
||||
return
|
||||
evicted = self._versors.pop(0)
|
||||
self._metadata.pop(0)
|
||||
key = _versor_key(evicted)
|
||||
indices = self._exact_index.get(key, [])
|
||||
if indices:
|
||||
indices.pop(0)
|
||||
if not indices:
|
||||
del self._exact_index[key]
|
||||
self._reindex_after_eviction()
|
||||
|
||||
def _reindex_after_eviction(self) -> None:
|
||||
"""Rebuild index after front-removal shifts all indices by -1."""
|
||||
self._rebuild_index()
|
||||
|
||||
@property
|
||||
def reproject_interval(self) -> int:
|
||||
"""Return the configured auto-reprojection cadence in store operations."""
|
||||
return self._reproject_interval
|
||||
|
||||
@property
|
||||
def store_count(self) -> int:
|
||||
"""Return how many store() operations have occurred in this vault."""
|
||||
return self._store_count
|
||||
|
||||
@property
|
||||
def max_entries(self) -> int | None:
|
||||
return self._max_entries
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._versors)
|
||||
|
|
|
|||
Loading…
Reference in a new issue