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.
144 lines
4.5 KiB
Python
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)
|