core/core/physics/dynamic_manifold.py
Shay 44f7258b16
Some checks failed
smoke / smoke (-m "not quarantine") (pull_request) Successful in 5m57s
lane-shas / verify pinned lane SHAs (pull_request) Failing after 1h43m22s
fix(algebra): P11a physics hot paths via algebra.backend (Rust-ready)
Stop wave/Third-Door physics from bypassing native dispatch:

- Route geometric_product / versor_apply / versor_condition / cga_inner
  through algebra.backend in wave_manifold, goldtether, trajectory,
  dynamic_manifold, surprise, holographic_vault, atlas_packing,
  biography, self_authorship.
- Backend: dtype-aware Rust use — f32 workloads use core_rs; f64 wave
  residual pins keep Python SOT until f64 GP parity exists. Coerce
  arrays for PyO3 bindings; fail soft to Python.
- AST hygiene pin: tests/test_physics_backend_dispatch_hygiene.py
- Docs: RUST.md, runtime_contracts, fidelity (ADR-0235 / UMA hygiene).

Verified: wave + cohesion suites green default and CORE_BACKEND=rust
(with core_rs built). MLX still exploratory off-serve.
2026-07-14 21:08:02 -07:00

872 lines
32 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
core/physics/dynamic_manifold.py
Signature-aware PCA + Conformal Procrustes + Cartan-Iwasawa extraction
ADR-0239
Geometry-first, dual-corrected, null-vector safe.
Wired to live algebra/* (no scipy, no placeholder-only path).
"""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from typing import Optional, Sequence, Tuple, Union
import numpy as np
from algebra.backend import geometric_product, versor_condition
from algebra.cga import is_null
from algebra.cl41 import N_COMPONENTS, grade_project, reverse
from algebra.null_point import (
NullPointRecoveryError,
dilator,
recover_dilation,
recover_translation,
translator,
)
from algebra.rotor import rotor_power, word_transition_rotor
_CLOSURE_TOL = 1e-6
_NEAR_ZERO = 1e-12
_NULL_TOL = 1e-9
_PROCRUSTES_WEIGHT_TOL = 1e-8
_CONJUGACY_RES_TOL = 1e-5
_CONJUGACY_MAX_STEPS = 48
# Grade-2 blade indices (e1∧e2 … spanning the 10-plane bivector of Cl(4,1)).
_BIVECTOR_PLANES = tuple(range(6, 16))
# Cl(4,1) metric on Euclidean+conformal R^5
_ETA5 = np.diag([1.0, 1.0, 1.0, 1.0, -1.0]).astype(np.float64)
class AxisClassification(str, Enum):
SPACELIKE = "spacelike"
TIMELIKE = "timelike"
NULL = "null"
DEGENERATE = "degenerate"
@dataclass(frozen=True, slots=True)
class PrincipalAxis:
vector: tuple[float, ...]
eigenvalue: float
classification: AxisClassification
metric_quadratic: float
@dataclass(frozen=True, slots=True)
class SignatureAwarePCAResult:
axes: tuple[PrincipalAxis, ...]
basis_matrix: np.ndarray # shape (5, n) or (32, n)
n_null: int
n_spacelike: int
n_timelike: int
n_degenerate: int
@dataclass(frozen=True, slots=True)
class ConformalProcrustesResult:
versor: np.ndarray
residual_norm: float
n_pairs: int
pair_residuals: tuple[float, ...]
@dataclass(frozen=True, slots=True)
class CartanIwasawaFactors:
"""Rotor · Translator · Dilator (K/A/N style factors on the multivector)."""
R: np.ndarray
T: np.ndarray
D: np.ndarray
reconstruction_residual: float
def _identity32() -> np.ndarray:
out = np.zeros(N_COMPONENTS, dtype=np.float64)
out[0] = 1.0
return out
def _strict_close_versor(V: np.ndarray, *, name: str) -> np.ndarray:
"""Rescale a true versor to unit weight; never seed-fabricate a rotor.
A versor satisfies ``V·rev(V) = scalar``. If the product is not scalar, or
the scalar is non-positive, raise ``ValueError`` (fail-closed). This is the
construction-boundary closer for CartanIwasawa — distinct from
:func:`unitize_versor`, which may map dense seeds onto the manifold.
"""
arr = np.asarray(V, dtype=np.float64)
if arr.shape != (N_COMPONENTS,):
raise ValueError(f"{name}: expected shape ({N_COMPONENTS},), got {arr.shape}")
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 >= 1e-2 or scalar_sq <= 0.0:
raise ValueError(
f"{name}: input not a versor "
f"(residue_norm={residue_norm:.3e}, scalar_sq={scalar_sq:.3e})"
)
return (arr * (1.0 / np.sqrt(scalar_sq))).astype(np.float64)
def _identity5() -> np.ndarray:
return np.eye(5, dtype=np.float64)
def signature_aware_pca(
X: np.ndarray,
target_grade: int = 3,
) -> np.ndarray:
"""
Metric-preserving principal axes on the Cl(4,1) null cone.
X: shape (5, K) of conformal (null) vectors.
Returns: shape (5, target_grade) real, pseudo-orthogonal basis.
CRITICAL FIX (Terra + Grok mastery): genuine null vectors are CLASSIFIED
and retained. They are never silently skipped.
"""
X_arr = np.asarray(X, dtype=np.float64)
if X_arr.ndim != 2 or X_arr.shape[0] != 5:
raise ValueError("signature_aware_pca expects X with shape (5, K)")
K = X_arr.shape[1]
A = (X_arr @ X_arr.T) / max(K, 1)
M = _ETA5 @ A
eigenvalues, eigenvectors = np.linalg.eig(M)
real_idx = np.argsort(np.real(eigenvalues))[::-1]
sorted_vecs = np.real(eigenvectors[:, real_idx])
basis: list[np.ndarray] = []
for i in range(sorted_vecs.shape[1]):
v = sorted_vecs[:, i].copy()
for u in basis:
denom = float(u @ (_ETA5 @ u)) + 1e-12
proj = float(v @ (_ETA5 @ u)) / denom
v = v - proj * u
nrm2 = float(v @ (_ETA5 @ v))
if abs(nrm2) < _NULL_TOL:
# GENUINE NULL VECTOR — keep as-is (the fix)
if float(np.linalg.norm(v)) > _NEAR_ZERO:
basis.append(v)
else:
nrm = float(np.sqrt(abs(nrm2)))
if nrm > _NEAR_ZERO:
basis.append(v / nrm)
if len(basis) == int(target_grade):
break
if not basis:
raise ValueError("signature_aware_pca produced empty basis")
return np.column_stack(basis)
def signature_aware_pca_report(
X: np.ndarray,
target_grade: int = 3,
) -> SignatureAwarePCAResult:
"""PCA with explicit null/spacelike/timelike classification counts."""
basis = signature_aware_pca(X, target_grade=target_grade)
axes: list[PrincipalAxis] = []
counts = {c: 0 for c in AxisClassification}
for j in range(basis.shape[1]):
v = basis[:, j]
q = float(v @ (_ETA5 @ v))
if float(np.linalg.norm(v)) < _NEAR_ZERO:
cls = AxisClassification.DEGENERATE
elif abs(q) < _NULL_TOL:
cls = AxisClassification.NULL
elif q > 0.0:
cls = AxisClassification.SPACELIKE
else:
cls = AxisClassification.TIMELIKE
counts[cls] += 1
axes.append(
PrincipalAxis(
vector=tuple(float(x) for x in v),
eigenvalue=q,
classification=cls,
metric_quadratic=q,
)
)
return SignatureAwarePCAResult(
axes=tuple(axes),
basis_matrix=basis,
n_null=counts[AxisClassification.NULL],
n_spacelike=counts[AxisClassification.SPACELIKE],
n_timelike=counts[AxisClassification.TIMELIKE],
n_degenerate=counts[AxisClassification.DEGENERATE],
)
def procrustes_residual(
source: np.ndarray,
target: np.ndarray,
versor: np.ndarray,
) -> float:
"""Dedicated Procrustes residual: sandwich for 32-vecs, linear map for 5-vecs."""
s = np.asarray(source, dtype=np.float64)
t = np.asarray(target, dtype=np.float64)
V = np.asarray(versor, dtype=np.float64)
if s.shape == (N_COMPONENTS,) and V.shape == (N_COMPONENTS,):
# Raw sandwich — not versor_apply (that unitizes non-null images).
mapped = _raw_sandwich(V, s)
return float(np.linalg.norm(mapped - t))
# 5-vector conformal points: Frobenius after linear map if V is 5x5
if s.shape == (5,) and V.shape == (5, 5):
return float(np.linalg.norm(V @ s - t))
if s.ndim == 2 and s.shape[0] == 5 and V.shape == (5, 5) and s.shape == t.shape:
return _projective_cloud_residual(V @ s, t)
return float(np.linalg.norm(s - t))
def so3_matrix_to_rotor(R3: np.ndarray) -> np.ndarray:
"""SO(3) matrix → Cl(4,1) rotor whose sandwich acts as ``R3`` on e1..e3.
Shepperd quaternion extraction, then even-grade embed::
rotor[0] = q0
rotor[10] = q1 # e2∧e3
rotor[7] = -q2 # -e1∧e3
rotor[6] = q3 # e1∧e2
Unitize once at this construction boundary. Shepperd is applied to ``R3.T``
so the sandwich product of this algebra implements active ``R3`` (the raw
Shepperd(R) quaternion sandwiches as ``R.T`` under Cl(4,1) GP convention).
"""
R = np.asarray(R3, dtype=np.float64)
if R.shape != (3, 3) or not np.all(np.isfinite(R)):
raise ValueError(f"so3_matrix_to_rotor expects finite (3,3), got {R.shape}")
q0, q1, q2, q3 = _shepperd_quaternion(R.T)
rotor = np.zeros(N_COMPONENTS, dtype=np.float64)
rotor[0] = q0
rotor[10] = q1 # e2∧e3
rotor[7] = -q2 # -e1∧e3
rotor[6] = q3 # e1∧e2
return _strict_close_versor(rotor, name="so3_matrix_to_rotor")
def _shepperd_quaternion(R: np.ndarray) -> tuple[float, float, float, float]:
"""Shepperd's method: robust SO(3) → unit quaternion (q0, q1, q2, q3)."""
R = np.asarray(R, dtype=np.float64)
tr = float(R[0, 0] + R[1, 1] + R[2, 2])
if tr > 0.0:
S = np.sqrt(tr + 1.0) * 2.0
q0 = 0.25 * S
q1 = (R[2, 1] - R[1, 2]) / S
q2 = (R[0, 2] - R[2, 0]) / S
q3 = (R[1, 0] - R[0, 1]) / S
elif R[0, 0] > R[1, 1] and R[0, 0] > R[2, 2]:
S = np.sqrt(1.0 + R[0, 0] - R[1, 1] - R[2, 2]) * 2.0
q0 = (R[2, 1] - R[1, 2]) / S
q1 = 0.25 * S
q2 = (R[0, 1] + R[1, 0]) / S
q3 = (R[0, 2] + R[2, 0]) / S
elif R[1, 1] > R[2, 2]:
S = np.sqrt(1.0 + R[1, 1] - R[0, 0] - R[2, 2]) * 2.0
q0 = (R[0, 2] - R[2, 0]) / S
q1 = (R[0, 1] + R[1, 0]) / S
q2 = 0.25 * S
q3 = (R[1, 2] + R[2, 1]) / S
else:
S = np.sqrt(1.0 + R[2, 2] - R[0, 0] - R[1, 1]) * 2.0
q0 = (R[1, 0] - R[0, 1]) / S
q1 = (R[0, 2] + R[2, 0]) / S
q2 = (R[1, 2] + R[2, 1]) / S
q3 = 0.25 * S
return float(q0), float(q1), float(q2), float(q3)
def _raw_sandwich(V: np.ndarray, X: np.ndarray) -> np.ndarray:
"""Raw f64 sandwich ``V X rev(V)`` — no unitize (construction / adjoint path)."""
V = np.asarray(V, dtype=np.float64)
X = np.asarray(X, dtype=np.float64)
return geometric_product(geometric_product(V, X), reverse(V))
def grade1_sandwich_adjoint(V32: np.ndarray) -> np.ndarray:
"""5×5 matrix of the grade-1 sandwich outermorphism of unit versor ``V32``.
Column ``j`` is the grade-1 part of ``V e_{j+1} rev(V)`` (basis e1..e5).
Built via raw sandwich — never ``versor_apply`` on non-null basis vectors.
"""
V = np.asarray(V32, dtype=np.float64)
if V.shape != (N_COMPONENTS,):
raise ValueError(f"grade1_sandwich_adjoint expects 32-vector, got {V.shape}")
M = np.zeros((5, 5), dtype=np.float64)
for j in range(5):
ej = np.zeros(N_COMPONENTS, dtype=np.float64)
ej[j + 1] = 1.0
out = _raw_sandwich(V, ej)
M[:, j] = out[1:6]
return M
def _dehomogenize_cloud(
P: np.ndarray,
*,
tol: float = _PROCRUSTES_WEIGHT_TOL,
) -> tuple[np.ndarray, np.ndarray]:
"""Projective dehomogenization of (5,K) conformal columns → (3,K') Euclidean.
``x = P[0:3] / w`` with ``w = P[4] - P[3]`` (e5 e4). Columns with
``|w| < tol`` are dropped. Returns ``(X_3xKprime, keep_mask)``.
"""
P = np.asarray(P, dtype=np.float64)
if P.ndim != 2 or P.shape[0] != 5:
raise ValueError(f"dehomogenize expects (5,K), got {P.shape}")
w = P[4, :] - P[3, :]
keep = np.abs(w) >= tol
if not np.any(keep):
raise ValueError(
"conformal_procrustes: all points have degenerate conformal weight "
f"(|e5-e4| < {tol:g})"
)
X = P[:3, keep] / w[keep]
return X, keep
def _projective_cloud_residual(mapped: np.ndarray, Q: np.ndarray) -> float:
"""Weight-normalized Frobenius residual on (5,K) clouds, mean over K.
Dilation changes homogeneous weight, so raw ``||M@P Q||`` is large even
when dehomogenized Euclidean images match. Normalize each column by its
n_o weight ``w = e5 e4`` before comparing.
"""
mapped = np.asarray(mapped, dtype=np.float64)
Q = np.asarray(Q, dtype=np.float64)
K = mapped.shape[1]
if K == 0:
return 0.0
wm = mapped[4, :] - mapped[3, :]
wq = Q[4, :] - Q[3, :]
acc = 0.0
n = 0
for k in range(K):
if abs(wm[k]) < _PROCRUSTES_WEIGHT_TOL or abs(wq[k]) < _PROCRUSTES_WEIGHT_TOL:
continue
diff = mapped[:, k] / wm[k] - Q[:, k] / wq[k]
acc += float(np.dot(diff, diff))
n += 1
if n == 0:
raise ValueError("conformal_procrustes: no finite-weight columns for residual")
return float(np.sqrt(acc) / n)
def _kabsch_similarity(
X: np.ndarray,
Y: np.ndarray,
*,
tol: float = _PROCRUSTES_WEIGHT_TOL,
) -> tuple[float, np.ndarray, np.ndarray]:
"""Umeyama/Kabsch similarity: ``Y ≈ s R X + t`` with ``det(R)=+1``.
Returns ``(s, R3, t)``. Source-degenerate scale → ``s=1``.
"""
X = np.asarray(X, dtype=np.float64)
Y = np.asarray(Y, dtype=np.float64)
if X.shape != Y.shape or X.ndim != 2 or X.shape[0] != 3:
raise ValueError(f"Kabsch expects matching (3,K) clouds, got {X.shape}/{Y.shape}")
K = X.shape[1]
if K == 0:
raise ValueError("Kabsch requires at least one point")
mu_x = X.mean(axis=1)
mu_y = Y.mean(axis=1)
Xc = X - mu_x[:, None]
Yc = Y - mu_y[:, None]
sig_x2 = float(np.sum(Xc * Xc))
sig_y2 = float(np.sum(Yc * Yc))
if sig_x2 <= tol:
s = 1.0
else:
s = float(np.sqrt(sig_y2 / sig_x2))
H = Xc @ Yc.T
U, _S, Vt = np.linalg.svd(H)
R3 = Vt.T @ U.T
if np.linalg.det(R3) < 0.0:
# Strip reflection (force proper rotation).
Vt = Vt.copy()
Vt[-1, :] *= -1.0
R3 = Vt.T @ U.T
t = mu_y - s * (R3 @ mu_x)
return s, R3, t
def _assemble_similarity_versor(
s: float,
R3: np.ndarray,
t: np.ndarray,
) -> np.ndarray:
"""Assemble ``V = T(t) * D(s) * R`` (Euclidean similarity ``x ↦ s R x + t``)."""
s = float(s)
if not np.isfinite(s) or s <= 0.0:
raise ValueError(f"conformal_procrustes: non-positive scale {s}")
R_mv = so3_matrix_to_rotor(R3)
D = dilator(s) if abs(s - 1.0) > _NEAR_ZERO else _identity32()
T = translator(np.asarray(t, dtype=np.float64))
V = geometric_product(geometric_product(T, D), R_mv)
# Construction-boundary strict close (no seed-to-rotor fabrication).
return _strict_close_versor(V, name="assemble_similarity_versor")
def _kabsch_conformal_from_5clouds(
P: np.ndarray,
Q: np.ndarray,
*,
tol: float = _PROCRUSTES_WEIGHT_TOL,
) -> tuple[np.ndarray, np.ndarray, float]:
"""Full Kabsch-conformal on (5,K) clouds → ``(V32, M5x5, residual)``."""
P = np.asarray(P, dtype=np.float64)
Q = np.asarray(Q, dtype=np.float64)
if P.shape != Q.shape or P.ndim != 2 or P.shape[0] != 5:
raise ValueError(f"expected matching (5,K) clouds, got {P.shape}/{Q.shape}")
K = P.shape[1]
if K == 0:
V = _identity32()
return V, grade1_sandwich_adjoint(V), 0.0
X, keep_p = _dehomogenize_cloud(P, tol=tol)
Y, keep_q = _dehomogenize_cloud(Q, tol=tol)
keep = keep_p & keep_q
if not np.any(keep):
raise ValueError("conformal_procrustes: no paired finite-weight columns")
# Re-dehomogenize with joint mask (weights already gated).
wP = P[4, keep] - P[3, keep]
wQ = Q[4, keep] - Q[3, keep]
X = P[:3, keep] / wP
Y = Q[:3, keep] / wQ
s, R3, t = _kabsch_similarity(X, Y, tol=tol)
V32 = _assemble_similarity_versor(s, R3, t)
cond = versor_condition(V32)
if cond >= _CLOSURE_TOL:
raise ValueError(f"conformal_procrustes: assembled versor not closed ({cond:.3e})")
M = grade1_sandwich_adjoint(V32)
residual = _projective_cloud_residual(M @ P, Q)
return V32, M, residual
def _is_grade1_null(mv: np.ndarray, *, tol: float = 1e-6) -> bool:
"""True iff ``mv`` is (numerically) a grade-1 null vector (CGA point)."""
mv = np.asarray(mv, dtype=np.float64)
if mv.shape != (N_COMPONENTS,):
return False
off_g1 = float(np.linalg.norm(mv) - np.linalg.norm(mv[1:6]))
# Cheaper: non-grade-1 mass.
g1 = grade_project(mv, 1)
if float(np.linalg.norm(mv - g1)) > tol * max(1.0, float(np.linalg.norm(mv))):
return False
if float(np.linalg.norm(g1)) < _NEAR_ZERO:
return False
return bool(is_null(mv, tol=tol))
def _mv_to_5(mv: np.ndarray) -> np.ndarray:
return np.asarray(mv, dtype=np.float64)[1:6].copy()
def _left_gp_matrix(A: np.ndarray) -> np.ndarray:
"""Matrix L with ``L @ vec(B) = vec(A * B)``."""
A = np.asarray(A, dtype=np.float64)
L = np.zeros((N_COMPONENTS, N_COMPONENTS), dtype=np.float64)
for j in range(N_COMPONENTS):
ej = np.zeros(N_COMPONENTS, dtype=np.float64)
ej[j] = 1.0
L[:, j] = geometric_product(A, ej)
return L
def _right_gp_matrix(A: np.ndarray) -> np.ndarray:
"""Matrix R with ``R @ vec(B) = vec(B * A)``."""
A = np.asarray(A, dtype=np.float64)
R = np.zeros((N_COMPONENTS, N_COMPONENTS), dtype=np.float64)
for j in range(N_COMPONENTS):
ej = np.zeros(N_COMPONENTS, dtype=np.float64)
ej[j] = 1.0
R[:, j] = geometric_product(ej, A)
return R
def _strict_unitize_candidate(v: np.ndarray, *, tol: float = 1e-5) -> Optional[np.ndarray]:
"""Unitize only if ``v·rev(v)`` is already a positive scalar (no seed fallback)."""
v = np.asarray(v, dtype=np.float64)
if float(np.linalg.norm(v)) < _NEAR_ZERO:
return None
vv = geometric_product(v, reverse(v))
off = float(np.linalg.norm(vv[1:]))
sc = float(vv[0])
if off > tol * max(1.0, abs(sc)) or sc <= 0.0:
return None
return (v / np.sqrt(sc)).astype(np.float64)
def _exp_bivector(B: np.ndarray) -> np.ndarray:
"""``exp(B)`` series for a pure bivector (construction path); strict-close at end."""
B = np.asarray(B, dtype=np.float64)
term = _identity32()
out = term.copy()
for k in range(1, 48):
term = geometric_product(term, B) / float(k)
out = out + term
if float(np.linalg.norm(term)) < 1e-18:
break
return _strict_close_versor(out, name="exp_bivector")
def _field_conjugacy_versor(
sources: Sequence[np.ndarray],
targets: Sequence[np.ndarray],
*,
max_steps: int = _CONJUGACY_MAX_STEPS,
tol: float = _CONJUGACY_RES_TOL,
) -> tuple[np.ndarray, float]:
"""Recover unit versor ``W`` with raw sandwich ``W·F_A·rev(W) ≈ F_B``.
1. Build stacked linear conjugacy constraints ``W F_A F_B W = 0``; the
nullspace contains all conjugators (plus centralizer junk).
2. Try strict-unitize of ± null singular vectors as candidates.
3. Multiplicative Lie-algebra GaussNewton on Spin (left updates
``W ← exp(B) W``) minimizing mean raw-sandwich residual.
Returns the best closed versor with an **honest residual** (may stay large
when no conjugator exists, e.g. sandwich cannot map ``I → non-I``). Callers
gate on residual — residual-honest, not raise-on-failure. Never left-
composition via ``word_transition_rotor``; never ``versor_apply`` (which
unitizes non-null images).
"""
pairs = [
(np.asarray(s, dtype=np.float64), np.asarray(t, dtype=np.float64))
for s, t in zip(sources, targets)
]
for i, (s, t) in enumerate(pairs):
if s.shape != (N_COMPONENTS,) or t.shape != (N_COMPONENTS,):
raise ValueError(f"pair[{i}] must be 32-component multivectors")
# Linear conjugacy nullspace (design step); used for candidates + audit.
blocks = [_right_gp_matrix(s) - _left_gp_matrix(t) for s, t in pairs]
Mat = np.vstack(blocks)
_u, svals, vh = np.linalg.svd(Mat, full_matrices=True)
null_dim = int(np.sum(svals < 1e-8))
candidates: list[np.ndarray] = [_identity32()]
if null_dim > 0:
for row in vh[-null_dim:]:
for sgn in (1.0, -1.0):
u = _strict_unitize_candidate(sgn * row)
if u is not None:
candidates.append(u)
def _mean_sandwich(W: np.ndarray) -> float:
acc = 0.0
for s, t in pairs:
acc += float(np.linalg.norm(_raw_sandwich(W, s) - t) ** 2)
return float(np.sqrt(acc / len(pairs)))
best_W = candidates[0]
best_r = _mean_sandwich(best_W)
for c in candidates[1:]:
r = _mean_sandwich(c)
if r < best_r:
best_r = r
best_W = c
if best_r < tol:
cond = versor_condition(best_W)
if cond >= _CLOSURE_TOL:
raise ValueError(f"field conjugacy versor not closed: {cond:.3e}")
return best_W, best_r
# Multiplicative GN on Spin from best candidate (usually identity).
W = best_W.copy()
n = len(pairs)
for _step in range(max_steps):
rvec = np.zeros(N_COMPONENTS * n, dtype=np.float64)
J = np.zeros((N_COMPONENTS * n, 10), dtype=np.float64)
for i, (Fa, Fb) in enumerate(pairs):
cur = _raw_sandwich(W, Fa)
delta = Fb - cur
rvec[N_COMPONENTS * i : N_COMPONENTS * (i + 1)] = delta
for j, plane in enumerate(_BIVECTOR_PLANES):
B = np.zeros(N_COMPONENTS, dtype=np.float64)
B[plane] = 1.0
# d/dε Ad_{exp(ε E_j) W} Fa |₀ ≈ [E_j, cur]
J[N_COMPONENTS * i : N_COMPONENTS * (i + 1), j] = (
geometric_product(B, cur) - geometric_product(cur, B)
)
r = float(np.linalg.norm(rvec) / np.sqrt(n))
if r < tol:
best_W, best_r = W, r
break
b, _res, _rank, _sv = np.linalg.lstsq(J, rvec, rcond=None)
alpha = 1.0
improved = False
for _ls in range(12):
B = np.zeros(N_COMPONENTS, dtype=np.float64)
B[6:16] = alpha * b
try:
E = _exp_bivector(B)
W_try = _strict_close_versor(
geometric_product(E, W), name="conjugacy_GN"
)
except ValueError:
alpha *= 0.5
continue
r_try = _mean_sandwich(W_try)
if r_try < r:
W = W_try
best_W, best_r = W_try, r_try
improved = True
break
alpha *= 0.5
if not improved:
break
if best_r < tol:
break
cond = versor_condition(best_W)
if cond >= _CLOSURE_TOL:
raise ValueError(f"field conjugacy versor not closed: {cond:.3e}")
# Return best closed versor with honest sandwich residual (may be large when
# no conjugator exists — e.g. sandwich cannot map I → non-I). Callers gate
# on residual; do not fabricate a low residual or raise as "success".
return best_W, best_r
def conformal_procrustes(
P: np.ndarray,
Q: np.ndarray,
max_iter: int = 32,
tol: float = 1e-8,
) -> Tuple[np.ndarray, float]:
"""
Kabsch-conformal Procrustes / field conjugacy (Super-Blueprint §3.1).
Find best versor (or its grade-1 sandwich adjoint) mapping source ``P``
onto target ``Q`` under the **sandwich** ``V·X·rev(V)``.
Accepts:
- ``P,Q`` shape ``(5, K)`` conformal vectors → returns ``(M_5x5, residual)``
where ``M`` is the grade-1 sandwich adjoint of the assembled similarity
versor ``V = T(t)·D(s)·R`` (Kabsch + Umeyama scale, det R = +1).
- ``P,Q`` shape ``(32,)`` or sequences of 32-vectors:
* all grade-1 null (CGA points) → Kabsch on extracted (5,K), returns
**V32** (not 5×5) with sandwich residual;
* otherwise field conjugacy ``W F_A = F_B W`` + sandwich residual,
returns **V32**.
Residual is always a sandwich / projective match residual (never a
left-composition ``word_transition_rotor`` average). Off-serving geometry;
not wired into chat/runtime.
Returns ``(V, residual)`` matching the package contract.
"""
weight_tol = float(tol) if tol is not None else _PROCRUSTES_WEIGHT_TOL
_ = max_iter # reserved; conjugacy uses its own step budget
# Multivector sequence
if isinstance(P, (list, tuple)):
src_list = [np.asarray(p, dtype=np.float64) for p in P]
tgt_list = [np.asarray(q, dtype=np.float64) for q in Q]
if not isinstance(Q, (list, tuple)):
raise ValueError("Q must be a sequence when P is a sequence")
result = _procrustes_multivector_pairs(src_list, tgt_list, tol=weight_tol)
return result.versor, result.residual_norm
P_arr = np.asarray(P, dtype=np.float64)
Q_arr = np.asarray(Q, dtype=np.float64)
if P_arr.shape == (N_COMPONENTS,) and Q_arr.shape == (N_COMPONENTS,):
result = _procrustes_multivector_pairs([P_arr], [Q_arr], tol=weight_tol)
return result.versor, result.residual_norm
if P_arr.ndim == 2 and P_arr.shape[0] == 5 and P_arr.shape == Q_arr.shape:
_V32, M, residual = _kabsch_conformal_from_5clouds(P_arr, Q_arr, tol=weight_tol)
return M, residual
raise ValueError(
"conformal_procrustes expects (5,K) point clouds, 32-vectors, or sequences thereof"
)
def _procrustes_multivector_pairs(
sources: Sequence[np.ndarray],
targets: Sequence[np.ndarray],
*,
tol: float = _PROCRUSTES_WEIGHT_TOL,
) -> ConformalProcrustesResult:
"""32-vector Procrustes: Kabsch on null-point lists, else field conjugacy.
Deletes the old ``word_transition_rotor`` averaging path (left composition).
Residual is always raw sandwich residual (never left-composition).
"""
if len(sources) != len(targets) or not sources:
raise ValueError("sources/targets must be non-empty and equal length")
src_list = [np.asarray(s, dtype=np.float64) for s in sources]
tgt_list = [np.asarray(t, dtype=np.float64) for t in targets]
for i, (s, t) in enumerate(zip(src_list, tgt_list)):
if s.shape != (N_COMPONENTS,) or t.shape != (N_COMPONENTS,):
raise ValueError(f"pair[{i}] must be 32-component multivectors")
# Null-point cloud path: extract (5,K), Kabsch, return V32.
if all(_is_grade1_null(s) and _is_grade1_null(t) for s, t in zip(src_list, tgt_list)):
P = np.column_stack([_mv_to_5(s) for s in src_list])
Q = np.column_stack([_mv_to_5(t) for t in tgt_list])
V32, _M, _proj_r = _kabsch_conformal_from_5clouds(P, Q, tol=tol)
pair_res = tuple(procrustes_residual(s, t, V32) for s, t in zip(src_list, tgt_list))
residual_norm = float(np.sqrt(sum(r * r for r in pair_res) / len(pair_res)))
cond = versor_condition(V32)
if cond >= _CLOSURE_TOL:
raise ValueError(f"Procrustes versor not closed: condition={cond:.3e}")
return ConformalProcrustesResult(
versor=V32,
residual_norm=residual_norm,
n_pairs=len(src_list),
pair_residuals=pair_res,
)
# Field conjugacy / wave polar (ADR-0241 Slice 23): all non-null field paths
# go through WaveManifold (single-pair polar; multi-pair thin conjugacy wrap).
# Null-point clouds already returned above (Kabsch point-cloud path).
from core.physics.wave_manifold import WaveManifold
wave = WaveManifold()
if len(src_list) == 1:
V = wave.wave_analogical_polar(src_list[0], tgt_list[0])
else:
V, _engine_r = wave.wave_field_conjugacy(src_list, tgt_list)
pair_res = tuple(
procrustes_residual(s, t, V) for s, t in zip(src_list, tgt_list)
)
residual_norm = float(np.sqrt(sum(r * r for r in pair_res) / len(pair_res)))
return ConformalProcrustesResult(
versor=V,
residual_norm=residual_norm,
n_pairs=len(src_list),
pair_residuals=pair_res,
)
def cartan_iwasawa_extract(
V: np.ndarray,
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
"""
Factor a conformal versor into Rotor · Translator · Dilator
via explicit Cartan-Iwasawa (BCH-free).
Returns (R, T, D).
For 32-component unit versors: factors live in Cl(4,1) multivector space.
5×5 sandwich-adjoint matrices are **not** supported (fail-closed) — pass the
assembled 32-versor from Kabsch, not the grade-1 adjoint alone.
"""
V_arr = np.asarray(V, dtype=np.float64)
if V_arr.shape == (5, 5):
raise ValueError(
"cartan_iwasawa_extract: 5x5 adjoint path not supported; "
"pass the assembled 32-component versor (V32)"
)
if V_arr.shape != (N_COMPONENTS,):
raise ValueError(f"V must be 32-vector; got {V_arr.shape}")
factors = cartan_iwasawa_factorize(V_arr)
return factors.R, factors.T, factors.D
def cartan_iwasawa_factorize(V: np.ndarray) -> CartanIwasawaFactors:
"""Factor a closed conformal versor into Rotor · Translator · Dilator.
Super-Blueprint §2.2 (null-point peel) for similarities, plus an honest
remainder-as-rotor path for general Spin(4,1) elements that do not fix
infinity (multi-plane rotors that are not Euclidean similarities).
Algorithm
---------
1. Validate V is a 32-vector; at this construction boundary, unitize once
when the input is salvageably open (existing soft threshold); require
final ``versor_condition < 1e-6`` or raise (fail-closed).
2. **Similarity path** — peel via null-point recovery (right-to-left for
reconstruction order ``R * T * D``)::
s, D = recover_dilation(V)
V1 = unitize(V * reverse(D))
a, T = recover_translation(V1)
R = unitize(V1 * reverse(T))
Any :class:`~algebra.null_point.NullPointRecoveryError` (or a unitize
failure after a partial peel) falls through to the general path — never
fabricate broken R/D seeds.
3. **General Spin path** — not a similarity / peel failed: ``R = V``,
``T = I``, ``D = I`` (perfect reconstruction; R carries the full motion).
4. Assert each factor is closed; return residual ``‖R·T·D V‖``.
Off-serving geometry only: not wired into chat/runtime.
"""
V_arr = np.asarray(V, dtype=np.float64)
if V_arr.shape != (N_COMPONENTS,):
raise ValueError(f"V must have shape ({N_COMPONENTS},)")
# Strict construction-boundary close: rescale only when V·rev(V) is already
# scalar (a true versor up to weight). Never seed-to-rotor fabrications.
V_arr = _strict_close_versor(V_arr, name="cartan_iwasawa_factorize")
I = _identity32()
used_peel = False
try:
# Similarity path: Super §2.2 null-point peel (right-peel D then T).
_s, D = recover_dilation(V_arr)
V1 = _strict_close_versor(
geometric_product(V_arr, reverse(D)), name="cartan_peel_D"
)
_a, T = recover_translation(V1)
R = _strict_close_versor(
geometric_product(V1, reverse(T)), name="cartan_peel_T"
)
used_peel = True
except (NullPointRecoveryError, ValueError):
# General Spin(4,1): remainder-as-rotor — honest, exact reconstruction.
R, T, D = V_arr.copy(), I.copy(), I.copy()
recon = geometric_product(geometric_product(R, T), D)
recon_res = float(np.linalg.norm(recon - V_arr))
if used_peel and recon_res >= _CLOSURE_TOL:
# Peel produced closed factors that do not reconstruct — fall back to
# honest Spin remainder rather than hand a wrong factorization downstream.
R, T, D = V_arr.copy(), I.copy(), I.copy()
recon = geometric_product(geometric_product(R, T), D)
recon_res = float(np.linalg.norm(recon - V_arr))
for name, f in (("R", R), ("T", T), ("D", D)):
c = versor_condition(f)
if c >= _CLOSURE_TOL:
# Fail-closed: never return a broken R/T/D seed.
raise ValueError(f"CartanIwasawa factor {name} not closed: {c:.3e}")
return CartanIwasawaFactors(
R=R, T=T, D=D, reconstruction_residual=recon_res
)
def dual_correction_slerp(
source: np.ndarray,
target: np.ndarray,
alpha: float,
) -> np.ndarray:
"""Slerp on CartanIwasawa factors via left composition."""
a = float(alpha)
if a < 0.0 or a > 1.0:
raise ValueError("alpha must be in [0, 1]")
src = np.asarray(source, dtype=np.float64)
tgt = np.asarray(target, dtype=np.float64)
if a <= _NEAR_ZERO:
out = src.copy()
elif a >= 1.0 - _NEAR_ZERO:
out = tgt.copy()
else:
V = word_transition_rotor(src, tgt)
fac = cartan_iwasawa_factorize(V)
R_a = rotor_power(fac.R, a)
T_a = rotor_power(fac.T, a)
D_a = rotor_power(fac.D, a)
V_a = geometric_product(geometric_product(R_a, T_a), D_a)
V_a = _strict_close_versor(V_a, name="dual_correction_slerp")
out = geometric_product(V_a, src).astype(np.float64)
if versor_condition(out) >= _CLOSURE_TOL:
raise ValueError("dual_correction_slerp broke closure")
return out