core/tests/test_versor_closure.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

144 lines
4.5 KiB
Python

"""
CRITICAL: This test must pass before any other file is extended.
It verifies the core algebraic invariant of the entire system.
"""
import numpy as np
import pytest
from hypothesis import given, settings
from hypothesis import strategies as st
from algebra.rotor import word_transition_rotor
from algebra.versor import (
unitize_versor,
versor_apply,
versor_condition,
versor_unit_residual,
)
def _positive_unit_reflector(seed=None) -> np.ndarray:
"""
Construct a true positive-norm Cl(4,1) grade-1 versor.
The current field action uses V * F * reverse(V), so the operator fixture
must satisfy V * reverse(V) = +1, not -1. We therefore keep the fifth
basis component bounded below the positive four-space norm.
"""
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)
@given(st.integers(min_value=0, max_value=99))
@settings(max_examples=100)
def test_versor_apply_preserves_manifold(seed):
V = _positive_unit_reflector(seed)
F = _positive_unit_reflector(seed + 1000)
result = versor_apply(V, F)
cond = versor_condition(result)
assert cond < 1e-4, f"versor_apply broke the manifold: condition={cond:.2e}"
def test_unitize_clean_scalar_constructs_positive_unit_versor():
raw = np.zeros(32, dtype=np.float32)
raw[0] = 2.0
V = unitize_versor(raw)
assert np.allclose(V[0], 1.0, atol=1e-7)
assert versor_condition(V) < 1e-7
def test_unitize_rejects_non_scalar_residue_instead_of_hash_fallback():
dirty = np.zeros(32, dtype=np.float32)
dirty[0] = np.sqrt(0.5)
dirty[1] = np.sqrt(0.5)
with pytest.raises(ValueError, match="bad_residue"):
unitize_versor(dirty)
def test_unitize_rejects_non_positive_scalar_norm():
negative_norm = np.zeros(32, dtype=np.float32)
negative_norm[5] = 1.0
with pytest.raises(ValueError, match="bad_scalar"):
unitize_versor(negative_norm)
def test_versor_unit_residual_can_accept_signed_manifold_versors():
negative_norm = np.zeros(32, dtype=np.float32)
negative_norm[5] = 1.0
assert versor_condition(negative_norm) > 1.0
assert versor_unit_residual(negative_norm, allow_negative=True) < 1e-7
def test_word_transition_rotor_handles_antipodal_scalar_inputs_as_closed_transition():
A = np.zeros(32, dtype=np.float32)
A[0] = 1.0
B = np.zeros(32, dtype=np.float32)
B[0] = -1.0
R = word_transition_rotor(A, B)
assert np.allclose(R[0], -1.0, atol=1e-7)
assert versor_condition(R) < 1e-6
def test_composition_closed():
V1 = _positive_unit_reflector(0)
V2 = _positive_unit_reflector(1)
F = _positive_unit_reflector(2)
F2 = versor_apply(V1, F)
F3 = versor_apply(V2, F2)
assert versor_condition(F3) < 1e-4
def test_versor_apply_preserves_null_property_for_null_inputs():
"""Null vectors (CGA points) map to null vectors under versor sandwich.
Updated 2026-05-17: `versor_apply` now routes null inputs around
the unit-versor closure boundary so the null property is preserved.
The previous test name claimed runtime closure was required for
null inputs; that contradicted the CGA geometric semantics
(Euclidean points stay points under conformal transformations)
and the un-skipped null-preservation invariant in
`tests/test_architectural_invariants.py::TestINV06NullConePreservation`.
Non-null field states still pass through closure unchanged — see
`test_composition_closed` and `test_identity_versor` for those.
"""
from algebra.cga import cga_inner
identity = np.zeros(32, dtype=np.float32)
identity[0] = 1.0
null_like = np.zeros(32, dtype=np.float32)
null_like[1] = 1.0
null_like[5] = 1.0
# Sanity: the constructed input is null under the CGA metric
# (e1·e1=+1, e_-·e_-=-1, cross terms cancel → self-inner = 0).
assert abs(float(cga_inner(null_like, null_like))) < 1e-9
result = versor_apply(identity, null_like)
# The result must remain null, not closed to the unit-versor shell.
assert abs(float(cga_inner(result, result))) < 1e-5
def test_identity_versor():
identity = np.zeros(32, dtype=np.float32)
identity[0] = 1.0
F = _positive_unit_reflector(42)
result = versor_apply(identity, F)
assert np.allclose(result, F, atol=1e-5)