core/tests/test_rust_backend.py
Shay 694754ab46 feat(algebra): null-preserving versor_apply path + un-skip 2 invariant tests
Closes the two skipped null-preservation tests and the architectural
gap behind them.  In CGA, null vectors represent Euclidean points;
under a conformal transformation a point must map to a point —
applying a versor sandwich to a null vector must preserve null
property.  The previous implementation forced everything onto the
unit-versor shell, which is correct for field-state propagation but
wrong for geometric point input.

Implementation
- algebra/versor.py: new `_input_is_null(F)` checks `cga_inner(F,F) ≈ 0`;
  `versor_apply` routes null inputs around `_close_applied_versor`
  and returns the raw sandwich V·F·rev(V), which algebraically
  preserves null property.  Non-null inputs unchanged.
- core-rs/src/versor.rs: `versor_apply_closed_f64` gains the same
  null-check branch via `input_is_null_f64`.  ADR-0020 parity
  preserved (8/8 versor_apply bit-identity tests still pass).

Test changes
- tests/test_architectural_invariants.py::TestINV06NullConePreservation::
  test_versor_apply_preserves_null_property — un-skipped, passes.
- tests/test_rust_backend.py::test_rust_versor_apply_preserves_null_vectors
  — un-skipped, passes.
- tests/test_versor_closure.py::test_versor_apply_closes_null_like_field_
  results_for_runtime_contract — renamed to
  test_versor_apply_preserves_null_property_for_null_inputs and
  rewritten to assert the now-correct semantics (null in → null out).
  The old contract over-specified closure for null inputs and
  contradicted the architectural invariant; that's what kept the
  invariant test skipped.

Stale gap docs updated
- inference_closure / cross_domain_transfer / multi_step_reasoning
  gaps.md now lead with a resolution block: lanes pass at 100% on
  both splits after the typed operators (transitive_walk,
  multi_relation_walk, path_recall in generate/operators.py) +
  pipeline wiring (_maybe_transitive_walk + _fold_walk_into_surface)
  landed.  The historic findings are preserved below for traceability.
- compositionality gaps.md: partial resolution — recall up from
  6.25% to 68.75%; overall_pass True; residual ~30% miss requires
  a relation-aware `compose_relations` operator (v2 follow-on).

Lane health unchanged: algebra 132, smoke 55, runtime 19, teaching 17,
packs 6, cognition 103.  Cognition eval 100%.  Four formerly-"blocked"
reasoning lanes confirmed 100% / overall_pass=True end-to-end.
2026-05-16 21:40:37 -07:00

135 lines
3.9 KiB
Python

"""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)