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.
135 lines
3.9 KiB
Python
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)
|