Replace synthetic word-transition rotor construction with the closed product B * reverse(A). - preserve make_rotor_from_angle compatibility - fail closed on non-closed transition candidates instead of using construction fallback behavior - validate transition operator condition - add targeted transition rotor regression tests No GitHub CI/status checks were exposed for this PR.
107 lines
4.1 KiB
Python
107 lines
4.1 KiB
Python
"""
|
|
algebra/rotor.py — Rotor construction operators for Cl(4,1).
|
|
|
|
Rotors are operators. They live here, in algebra/, not in vocab/.
|
|
A rotor between two word-versors is a contextual, field-level concern:
|
|
it describes a transformation being applied, not a property of the vocabulary.
|
|
"""
|
|
|
|
import numpy as np
|
|
|
|
from .cl41 import N_COMPONENTS, geometric_product, reverse
|
|
from .versor import unitize_versor, versor_condition
|
|
|
|
_TRANSITION_CONDITION_TOL = 1e-4
|
|
_NEAR_ZERO_TOL = 1e-12
|
|
_SAME_POINT_TOL = 1e-6
|
|
_STRICT_RESIDUE_TOL = 1e-2
|
|
|
|
|
|
def _identity(dtype: np.dtype) -> np.ndarray:
|
|
rotor = np.zeros(N_COMPONENTS, dtype=dtype)
|
|
rotor[0] = 1.0
|
|
return rotor
|
|
|
|
|
|
def _result_dtype(*arrays: np.ndarray) -> np.dtype:
|
|
dtype = np.result_type(*arrays)
|
|
return dtype if dtype in (np.dtype(np.float32), np.dtype(np.float64)) else np.dtype(np.float32)
|
|
|
|
|
|
def _strict_unitize_versor(v: np.ndarray, dtype: np.dtype) -> np.ndarray:
|
|
"""Unitize only already-closed versor candidates.
|
|
|
|
``unitize_versor`` intentionally supports dense construction seeds for
|
|
ingest/compiler boundaries. Transition construction is not such a boundary:
|
|
if the product candidate is not already a closed versor, fabricating a
|
|
deterministic fallback rotor would sever the transition from its source and
|
|
target. This helper therefore fails closed instead of using construction
|
|
seed fallback semantics.
|
|
"""
|
|
arr = np.asarray(v, dtype=np.float64)
|
|
input_norm = float(np.linalg.norm(arr))
|
|
if input_norm < _NEAR_ZERO_TOL:
|
|
raise ValueError("word_transition_rotor: near_zero candidate")
|
|
|
|
product = geometric_product(arr, reverse(arr)).astype(np.float64)
|
|
scalar_sq = float(product[0])
|
|
residue = product.copy()
|
|
residue[0] = 0.0
|
|
residue_norm = float(np.linalg.norm(residue))
|
|
if residue_norm >= _STRICT_RESIDUE_TOL:
|
|
raise ValueError(
|
|
"word_transition_rotor: non_closed candidate; "
|
|
f"residue_norm={residue_norm:.6e}"
|
|
)
|
|
if scalar_sq <= 0.0:
|
|
raise ValueError(
|
|
"word_transition_rotor: non_positive candidate; "
|
|
f"scalar_sq={scalar_sq:.6e}"
|
|
)
|
|
return (arr * (1.0 / np.sqrt(scalar_sq))).astype(dtype)
|
|
|
|
|
|
def make_rotor_from_angle(angle: float, bivector_idx: int = 6) -> np.ndarray:
|
|
"""Construct a scalar+bivector unit rotor from an angle."""
|
|
if not 0 <= int(bivector_idx) < N_COMPONENTS:
|
|
raise ValueError(f"bivector_idx out of range: {bivector_idx!r}")
|
|
rotor = np.zeros(N_COMPONENTS, dtype=np.float64)
|
|
half_angle = float(angle) / 2.0
|
|
rotor[0] = np.cos(half_angle)
|
|
rotor[int(bivector_idx)] = np.sin(half_angle)
|
|
return unitize_versor(rotor)
|
|
|
|
|
|
def word_transition_rotor(A: np.ndarray, B: np.ndarray) -> np.ndarray:
|
|
"""
|
|
Compute the closed transition operator from source versor A to target B.
|
|
|
|
R = B * reverse(A)
|
|
|
|
Vocabulary coordinates are expected to already be grade-normalized versors.
|
|
The transition between two such states is their closed product. This path
|
|
must never synthesize an unrelated fallback rotor from target components;
|
|
invalid inputs fail loudly so generation can preserve its field invariant.
|
|
"""
|
|
dtype = _result_dtype(A, B)
|
|
source = np.asarray(A, dtype=dtype)
|
|
target = np.asarray(B, dtype=dtype)
|
|
if source.shape != (N_COMPONENTS,) or target.shape != (N_COMPONENTS,):
|
|
raise ValueError(
|
|
"word_transition_rotor expects two 32-component multivectors; "
|
|
f"got {source.shape} and {target.shape}."
|
|
)
|
|
if float(np.linalg.norm(source)) < _NEAR_ZERO_TOL or float(np.linalg.norm(target)) < _NEAR_ZERO_TOL:
|
|
raise ValueError("word_transition_rotor: near_zero input")
|
|
if float(np.linalg.norm(target - source)) < _SAME_POINT_TOL:
|
|
return _identity(dtype)
|
|
|
|
candidate = geometric_product(target, reverse(source)).astype(dtype)
|
|
rotor = _strict_unitize_versor(candidate, dtype)
|
|
condition = versor_condition(rotor)
|
|
if condition > _TRANSITION_CONDITION_TOL:
|
|
raise ValueError(
|
|
"word_transition_rotor: transition rotor is not a unit versor; "
|
|
f"condition={condition:.3e}"
|
|
)
|
|
return rotor.astype(dtype, copy=False)
|