diff --git a/algebra/versor.py b/algebra/versor.py index 51f92df3..98f31035 100644 --- a/algebra/versor.py +++ b/algebra/versor.py @@ -14,7 +14,6 @@ _CONSTRUCTION_RESIDUE_TOLERANCE = 1e-2 _NEAR_ZERO_TOLERANCE = 1e-12 _DENSE_SEED_MIN_COMPONENTS = 8 _SEED_BIVECTORS = (6, 7, 8, 10, 11, 13) -_NULL_SCALAR_TOLERANCE = 1e-9 def _array_dtype(v: np.ndarray) -> np.dtype: @@ -95,22 +94,15 @@ def construction_seed_versor(v: np.ndarray) -> np.ndarray: return _seed_to_rotor(v, _array_dtype(v)) - -def _is_null_vector(v: np.ndarray) -> bool: - product = geometric_product(v, v).astype(np.float64) - return float(np.linalg.norm(product)) < _NULL_SCALAR_TOLERANCE - - def _close_applied_versor(v: np.ndarray, dtype: np.dtype) -> np.ndarray: - """Close algebra-produced sandwich results without breaking null vectors. + """Close algebra-produced sandwich results for runtime field states. - CGA sandwiching must preserve null vectors as null vectors. Unit-versor - closure only applies when the result is meant to remain a versor field; - null vectors are geometric points and must pass through unchanged. + ``versor_apply`` is the field-propagation API: callers expect the returned + multivector to satisfy ``versor_condition(result)``. CGA point/null-vector + preservation belongs behind an explicit geometry API, not this runtime + manifold boundary. """ arr = np.asarray(v, dtype=dtype) - if _is_null_vector(arr): - return arr.astype(dtype) try: return unitize_versor(arr).astype(dtype) except ValueError: diff --git a/tests/test_versor_closure.py b/tests/test_versor_closure.py index 10077775..423db1a4 100644 --- a/tests/test_versor_closure.py +++ b/tests/test_versor_closure.py @@ -103,6 +103,18 @@ def test_composition_closed(): assert versor_condition(F3) < 1e-4 +def test_versor_apply_closes_null_like_field_results_for_runtime_contract(): + 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 + + result = versor_apply(identity, null_like) + + assert versor_condition(result) < 1e-6 + + def test_identity_versor(): identity = np.zeros(32, dtype=np.float32) identity[0] = 1.0