diff --git a/algebra/backend.py b/algebra/backend.py index ef65fd74..cdc73b80 100644 --- a/algebra/backend.py +++ b/algebra/backend.py @@ -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) diff --git a/core-rs/src/cga.rs b/core-rs/src/cga.rs index a3c774f4..a5316b65 100644 --- a/core-rs/src/cga.rs +++ b/core-rs/src/cga.rs @@ -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 } diff --git a/core-rs/src/cl41.rs b/core-rs/src/cl41.rs index 166a42f7..edb52d2e 100644 --- a/core-rs/src/cl41.rs +++ b/core-rs/src/cl41.rs @@ -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 } diff --git a/core-rs/src/lib.rs b/core-rs/src/lib.rs index fb605181..5da81c45 100644 --- a/core-rs/src/lib.rs +++ b/core-rs/src/lib.rs @@ -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 { + 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 { @@ -120,6 +135,7 @@ fn f32_array_to_numpy(py: Python<'_>, data: &[f32; 32]) -> PyResult { 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)?)?; diff --git a/core-rs/src/versor.rs b/core-rs/src/versor.rs index ed454c57..09b241da 100644 --- a/core-rs/src/versor.rs +++ b/core-rs/src/versor.rs @@ -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::().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::().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::().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 { 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); + } + } +} diff --git a/core-rs/tests/test_cl41.rs b/core-rs/tests/test_cl41.rs index 420beed8..fdb76bcc 100644 --- a/core-rs/tests/test_cl41.rs +++ b/core-rs/tests/test_cl41.rs @@ -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] diff --git a/evals/cognition_cases.jsonl b/evals/cognition_cases.jsonl index a3e40a06..13fb56d0 100644 --- a/evals/cognition_cases.jsonl +++ b/evals/cognition_cases.jsonl @@ -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} diff --git a/generate/semantic_templates.py b/generate/semantic_templates.py index 11ee5678..fd152557 100644 --- a/generate/semantic_templates.py +++ b/generate/semantic_templates.py @@ -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", } diff --git a/generate/templates.py b/generate/templates.py index bbdf09e1..f609f9a1 100644 --- a/generate/templates.py +++ b/generate/templates.py @@ -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", } diff --git a/language_packs/data/en_core_cognition_v1/lexicon.jsonl b/language_packs/data/en_core_cognition_v1/lexicon.jsonl index d07c4ca7..e014c320 100644 --- a/language_packs/data/en_core_cognition_v1/lexicon.jsonl +++ b/language_packs/data/en_core_cognition_v1/lexicon.jsonl @@ -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"]} diff --git a/language_packs/data/en_core_cognition_v1/manifest.json b/language_packs/data/en_core_cognition_v1/manifest.json index de7c7b14..cd14da53 100644 --- a/language_packs/data/en_core_cognition_v1/manifest.json +++ b/language_packs/data/en_core_cognition_v1/manifest.json @@ -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" } diff --git a/tests/test_core_semantic_seed_pack.py b/tests/test_core_semantic_seed_pack.py index 4bf5d6c9..60392bf9 100644 --- a/tests/test_core_semantic_seed_pack.py +++ b/tests/test_core_semantic_seed_pack.py @@ -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" diff --git a/tests/test_language_pack_cache.py b/tests/test_language_pack_cache.py index 16ff67d3..b41d9f2b 100644 --- a/tests/test_language_pack_cache.py +++ b/tests/test_language_pack_cache.py @@ -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" diff --git a/tests/test_rust_backend.py b/tests/test_rust_backend.py new file mode 100644 index 00000000..068c40b1 --- /dev/null +++ b/tests/test_rust_backend.py @@ -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) diff --git a/tests/test_semantic_realizer_integration.py b/tests/test_semantic_realizer_integration.py index afa352b0..4292bbfd 100644 --- a/tests/test_semantic_realizer_integration.py +++ b/tests/test_semantic_realizer_integration.py @@ -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 diff --git a/tests/test_vault_store.py b/tests/test_vault_store.py new file mode 100644 index 00000000..d15c31f1 --- /dev/null +++ b/tests/test_vault_store.py @@ -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"} diff --git a/vault/store.py b/vault/store.py index af06b0de..00df4c99 100644 --- a/vault/store.py +++ b/vault/store.py @@ -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)