Shared CGA substrate for the #17 conformal-Procrustes/Kabsch and #16
Cartan-Iwasawa decompositions. Adds algebra/null_point.py and hoists the two
conformal null directions to frozen module constants.
algebra/cga.py
- Add frozen read-only f64 N_O / N_INF constants: the same vectors
embed_point builds inline (origin embeds to N_O; N_INF is fixed by every
Euclidean isometry), so the null-point primitives share one exact sign
definition instead of re-deriving it per call site.
- Fix header-docstring sign typo: n_o = 0.5*(e5 - e4), not 0.5*(e4 - e5).
embed_point was already correct; only the module header disagreed.
algebra/null_point.py (new)
- dilator(scale), translator(a): CGA similarity constructors; both
round-trip through the recoverers.
- recover_dilation(V) -> (scale, D): reads V n_inf rev(V), weight-normalised
so recovery is invariant to a non-unit versor weight (verified vs V -> kV).
- recover_translation(V) -> (a, T): reads V n_o rev(V), projective
dehomogenisation.
- NullPointRecoveryError carries machine-readable reason codes.
- Fail-closed symmetric similarity gate (_require_similarity): BOTH recoverers
now reject non-versors (not_a_versor) and non-similarities (not_similarity,
e.g. transversions). Closes an asymmetry where recover_translation silently
accepted a transversion / non-versor and returned a plausible translation,
violating the module's own wrong=0 contract.
- Orientation-reversing (reflection / det=-1) versors are refused by
recover_dilation with a distinct reason improper_versor, kept separate from
degenerate_scale; recover_translation still accepts them (the origin image is
well defined). conformal_procrustes strips reflections upstream, so this is a
documented boundary, not a silent one.
- Default tol=1e-9 documented: matches f64-exact recovery of a cleanly
assembled versor (~1e-14 round-trip); noisy/SVD callers must pass a wider tol.
tests/test_null_point_primitives.py (new): 33 tests - null-cone/pairing
invariants, constant immutability, constructor round-trips, composed T.D.R peel,
versor-weight invariance, and the full fail-closed matrix (transversion,
non-versor, inversion, reflection asymmetry, non-positive scale, bad vector).
Invariant protected: wrong=0 - no recovery returns a silently wrong value on a
degenerate / non-versor / non-similarity input. Validation: 33/33 new pass;
88 passed / 1 xfailed across the CGA substrate + physics Procrustes consumers
(dynamic_manifold, surprise, versor closure, rotor, holonomy).
Hardened via a 3-lens adversarial verification (soundness / sign-convention /
consumer-contract, each executing counterexample versors, every finding
skeptic-verified): 2 CONFIRMED findings fixed (asymmetric validation gap;
reflection reason conflation); tol-tightness resolved by documentation rather
than a guard-weakening default change.