feat(third-door): Slice-2 wave subsumption (surprise/Procrustes/GoldTether/biography)

Collapse parallel residual/projection paths into WaveManifold:
- surprise 32-vec residual → compute_spectral_leakage
- single non-null Procrustes pair → wave_analogical_polar
- coherence_residual / GoldTether drift → measure_unitary_residual
- biography integrate → unitary lock-in + holonomy_encode
Null-point Kabsch retained. Discovery wiring unchanged (no teaching import).
This commit is contained in:
Shay 2026-07-13 21:08:22 -07:00
parent 231b4c664c
commit 541257f81d
8 changed files with 167 additions and 45 deletions

View file

@ -19,6 +19,7 @@ import numpy as np
from algebra.cl41 import N_COMPONENTS
from algebra.holonomy import holonomy_encode, holonomy_similarity
from algebra.versor import unitize_versor, versor_condition
from core.physics.wave_manifold import WaveManifold
_CLOSURE_TOL = 1e-6
_TELEMETRY_SCHEMA = "biography_holonomy_v1"
@ -69,16 +70,32 @@ def integrate_biography(
"""Integrate ordered identity/session versors into a biography holonomy blade.
Order is load-bearing. Empty trajectory is refused (no confabulated self).
ADR-0241 Slice 2: each trajectory versor and the integrated blade must pass
the wave unitary residual (standing-wave / unitary-propagator lock-in). The
holonomy blade itself remains reconstruction-over-storage via
:func:`holonomy_encode` (no raw experience dump).
"""
if not trajectory:
raise ValueError("biography trajectory must be non-empty")
closed = [_as_versor(v, f"trajectory[{i}]") for i, v in enumerate(trajectory)]
wave = WaveManifold()
for i, v in enumerate(closed):
r = wave.measure_unitary_residual(v)
if r >= _CLOSURE_TOL:
raise ValueError(
f"trajectory[{i}] failed wave unitary residual: {r:.3e}"
)
blade = holonomy_encode(closed, alpha=alpha)
cond = versor_condition(blade)
if cond >= _CLOSURE_TOL:
raise ValueError(f"biography blade not closed: {cond:.3e}")
blade_arr = np.asarray(blade, dtype=np.float64)
r_blade = wave.measure_unitary_residual(blade_arr)
if r_blade >= _CLOSURE_TOL:
raise ValueError(f"biography blade wave unitary residual: {r_blade:.3e}")
return BiographyHolonomyBlade(
blade=np.asarray(blade, dtype=np.float64),
blade=blade_arr,
n_steps=len(closed),
trajectory_hash=_trajectory_hash(closed),
closure=float(cond),

View file

@ -723,10 +723,22 @@ def _procrustes_multivector_pairs(
pair_residuals=pair_res,
)
# Field conjugacy: sandwich residual, stacked multi-pair constraints.
V, residual_norm = _field_conjugacy_versor(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)))
# Field conjugacy / wave polar (ADR-0241 Slice 2): single non-null pair uses
# WaveManifold.wave_analogical_polar as the canonical sandwich conjugator.
# Multi-pair keeps the stacked conjugacy engine (same geometry family).
# Null-point clouds already returned above (Kabsch point-cloud path).
if len(src_list) == 1:
from core.physics.wave_manifold import WaveManifold
V = WaveManifold().wave_analogical_polar(src_list[0], tgt_list[0])
pair_res = (procrustes_residual(src_list[0], tgt_list[0], V),)
residual_norm = float(pair_res[0])
else:
V, residual_norm = _field_conjugacy_versor(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,

View file

@ -31,6 +31,7 @@ import numpy as np
from algebra.cl41 import N_COMPONENTS, geometric_product, reverse
from algebra.rotor import rotor_power, word_transition_rotor
from algebra.versor import versor_condition, versor_unit_residual
from core.physics.wave_manifold import WaveManifold
_CLOSURE_TOL = 1e-6
_NEAR_ZERO = 1e-12
@ -98,11 +99,11 @@ def coherence_residual(F: np.ndarray) -> float:
"""Public one-shot residual for tests and harnesses.
R = || F · reverse(F) 1 ||_F (dual-checked against reverse(F)).
Canonical path (ADR-0241 Slice 2): :meth:`WaveManifold.measure_unitary_residual`
unitary wave amplitude drift, not a parallel residual implementation.
"""
F_arr = _as_mv(F)
r = float(versor_unit_residual(F_arr))
r_rev = float(versor_unit_residual(reverse(F_arr)))
return max(r, r_rev)
return WaveManifold().measure_unitary_residual(_as_mv(F))
@dataclass
@ -202,8 +203,18 @@ class GoldTetherMonitor:
raw :func:`coherence_residual` stays the fail-closed *closure* gate.
"""
F_arr = _as_mv(F)
drift = coherence_residual(F_arr)
drift_term = drift / self.epsilon_drift if self.epsilon_drift > 0.0 else drift
# Unitary amplitude drift (wave substrate) + optional chiral readout.
# Chiral charge is structurally ~0 on real even field-states (#19 family);
# included as a non-negative integrity term so a future non-vacuous spinor
# path can move the residual without a second API.
wave = WaveManifold()
drift = wave.measure_unitary_residual(F_arr)
chiral = abs(float(wave.chiral_charge(F_arr)))
drift_term = (
(drift + chiral) / self.epsilon_drift
if self.epsilon_drift > 0.0
else (drift + chiral)
)
scale = float(np.linalg.norm(F_arr))
if self.gold_invariants and scale > _NEAR_ZERO:
min_dist = min(

View file

@ -43,6 +43,7 @@ from algebra.cga import cga_inner
from algebra.cl41 import N_COMPONENTS, grade_project
from algebra.versor import versor_condition
from core.physics.dynamic_manifold import conformal_procrustes
from core.physics.wave_manifold import WaveManifold, WaveSpectralLeakageError
_ETA5 = np.diag([1.0, 1.0, 1.0, 1.0, -1.0]).astype(np.float64)
_NEAR_ZERO = 1e-12
@ -166,7 +167,9 @@ def surprise_residual(
residual = x_arr - B @ coeffs
return residual, float(np.linalg.norm(residual))
# --- 32-vector (Cl(4,1) multivector) branch: cga_inner projection --------
# --- 32-vector (Cl(4,1) multivector) branch: wave spectral leakage -------
# Canonical path (ADR-0241 Slice 2): metric proj via WaveManifold; no parallel
# Euclidean Gram-Schmidt. Typed degenerate-span refusal preserved.
if x_arr.shape[0] == N_COMPONENTS:
if B.shape[0] != N_COMPONENTS and B.shape[1] == N_COMPONENTS:
B = B.T
@ -176,13 +179,10 @@ def surprise_residual(
if k == 0:
return x_arr.copy(), float(np.linalg.norm(x_arr))
cols = [B[:, i] for i in range(k)]
gram = np.array(
[[cga_inner(cols[i], cols[j]) for j in range(k)] for i in range(k)],
dtype=np.float64,
)
rhs = np.array([cga_inner(cols[i], x_arr) for i in range(k)], dtype=np.float64)
coeffs = _metric_projection_coeffs(B, gram, rhs)
residual = x_arr - B @ coeffs
try:
residual, energy = WaveManifold().compute_spectral_leakage(x_arr, cols)
except WaveSpectralLeakageError as exc:
raise SurpriseResidualError(exc.reason, **exc.disclosure) from exc
# Grade-support containment: residual is a linear combination of x and the
# basis columns, so its grades can only be a subset of theirs. ``allowed``
@ -199,7 +199,7 @@ def surprise_residual(
"grade_leak", leaked=sorted(leaked), allowed=sorted(allowed)
)
return residual, float(np.linalg.norm(residual))
return residual, float(energy)
raise ValueError("surprise_residual expects 5-vector or 32-vector x")

View file

@ -16,7 +16,7 @@ Off-serving until explicit gates; dual-checked unitary residual.
from __future__ import annotations
from typing import Sequence, Tuple
from typing import Any, Sequence, Tuple
import numpy as np
@ -28,6 +28,19 @@ _CLOSURE_TOL = 1e-6
_NEAR_ZERO = 1e-12
_NONSIMPLE_TOL = 1e-6
class WaveSpectralLeakageError(ValueError):
"""Fail-closed spectral leakage (metric-degenerate resonant span).
Mapped to :class:`core.physics.surprise.SurpriseResidualError` at the
surprise boundary so discovery / dual contracts keep a stable error type.
"""
def __init__(self, reason: str, **disclosure: Any) -> None:
self.reason = reason
self.disclosure = dict(disclosure)
super().__init__(f"spectral_leakage refused [{reason}]: {self.disclosure}")
# Unit pseudoscalar I₅ = e1 e2 e3 e4 e5 (central; I² = 1 in Cl(4,1)).
_I5 = np.zeros(N_COMPONENTS, dtype=np.float64)
_I5[31] = 1.0
@ -139,12 +152,21 @@ def _metric_project(
)
rhs = np.array([cga_inner(cols[i], x_arr) for i in range(k)], dtype=np.float64)
# Fail-closed on metric-degenerate span (null direction with no reciprocal).
rank_b = int(np.linalg.matrix_rank(np.column_stack(cols)))
Bmat = np.column_stack(cols)
rank_b = int(np.linalg.matrix_rank(Bmat))
rank_g = int(np.linalg.matrix_rank(gram))
if rank_g < rank_b:
raise ValueError(
f"spectral_leakage: degenerate metric span "
f"(rank_basis={rank_b}, rank_gram={rank_g})"
_u, _sv, vh = np.linalg.svd(gram)
degenerate_combo = [round(float(v), 6) for v in vh[-1]]
null_columns = [
i for i in range(k) if abs(float(gram[i, i])) < _NEAR_ZERO
]
raise WaveSpectralLeakageError(
"degenerate_metric_span",
rank_basis=rank_b,
rank_gram=rank_g,
null_columns=null_columns,
degenerate_combo=degenerate_combo,
)
coeffs, *_ = np.linalg.lstsq(gram, rhs, rcond=None)
projection = np.zeros(N_COMPONENTS, dtype=np.float64)
@ -231,17 +253,17 @@ class WaveManifold:
) -> np.ndarray:
"""Recover sandwich conjugator R with ψ_B ≈ R ψ_A ~R (polar / conjugacy).
Delegates to field conjugacy in :func:`conformal_procrustes` (Kabsch path
when null-points; conjugacy otherwise). Returns a closed unit versor.
Canonical single-field analogy rotor. Uses the field-conjugacy engine in
``dynamic_manifold`` (lazy import avoids import cycle; Procrustes
multi-pair path calls this for single non-null pairs).
"""
# Local import keeps module graph light and avoids cycles at import time.
from core.physics.dynamic_manifold import conformal_procrustes
# Lazy import: dynamic_manifold may call WaveManifold at runtime.
from core.physics.dynamic_manifold import _field_conjugacy_versor
psi_A = _as_mv(psi_A, "ψ_A")
psi_B = _as_mv(psi_B, "ψ_B")
R, _residual = conformal_procrustes(psi_A, psi_B)
R_arr = _as_mv(R, "R_polar")
return _require_closed_rotor(R_arr, name="R_polar")
psi_A_arr = _as_mv(psi_A, "ψ_A")
psi_B_arr = _as_mv(psi_B, "ψ_B")
R, _residual = _field_conjugacy_versor([psi_A_arr], [psi_B_arr])
return _require_closed_rotor(R, name="R_polar")
# --- Chiral spinor charge ------------------------------------------------
@ -266,4 +288,4 @@ class WaveManifold:
)
__all__ = ["WaveManifold"]
__all__ = ["WaveManifold", "WaveSpectralLeakageError"]

View file

@ -1,6 +1,6 @@
# ADR-0241: Wave-Field Driven Hyperbolic Atlas and Resonant Algebraic Cognition
**Status**: Proposed — substrate implemented (`core/physics/wave_manifold.py`); operator subsumption pending (acceptance path: Slice-2 wiring + Joshua review)
**Status**: Proposed — substrate + Slice-2 operator subsumption implemented (`wave_manifold` + surprise/Procrustes/GoldTether/biography delegates); acceptance path: Joshua review + merge
**Date**: 2026-07-13
**Deciders**: Joshua Shay + multi-model R&D
**Traceability**: Issue #14, parent #10

View file

@ -41,7 +41,7 @@
| W2 | Spectral leakage surprise | ADR-0241 §2.4B | 🟢 substrate landed (metric proj) | ADR-0241 |
| W3 | Wave polar analogy (Procrustes upgrade) | ADR-0241 §2.4A | 🟢 substrate landed (conjugacy polar) | ADR-0241 |
| W4 | Unitary residual + chiral charge readout | ADR-0241 §2.4CD | 🟢 substrate landed (Q structural 0 in real Cl(4,1); see §12) | ADR-0241 / #18 |
| W5 | Biography resonant lock-in | ADR-0241 + ADR-0240 | ⚫ not started | ADR-0241 |
| W5 | Biography resonant lock-in | ADR-0241 + ADR-0240 | 🟡 unitary lock-in on integrate; full holographic store deferred | ADR-0241 |
| W6 | `core_ha` deprecation / absorption | deprecation plan | 🟡 docs-only (no live tree) | ADR-0241 |
| — | Biography holonomy | (ADR-0240; not in blueprints) | 🟢 sound (pointwise) | — |
| — | Temporal admissibility gate | (ADR-0240; not in blueprints) | 🟢 sound | — |
@ -251,13 +251,12 @@ PY
---
## 12. Wave-field substrate (ADR-0241) — 🟢 substrate + ⚫ subsumption pending
## 12. Wave-field substrate (ADR-0241) — 🟢 substrate + Slice-2 subsumption
> **Status (2026-07-14):** ADR + deprecation plan + `core/physics/wave_manifold.py`
> landed on `feat/third-door-wave-field-substrate`. Behavioral suite
> `tests/test_adr_0241_wave_manifold.py` is **GREEN**. Operator subsumption
> (surprise / Procrustes / GoldTether / biography **delegate into** wave primitives)
> is still pending Slice 2 — substrate exists; parallel path not yet collapsed.
> + Slice-2 operator subsumption on `feat/third-door-wave-field-substrate`.
> Behavioral suite `tests/test_adr_0241_wave_manifold.py` is **GREEN**. Third-Door
> operators **delegate into** wave primitives (no parallel residual/projection path).
### Spec (ADR-0241)
- Continuous multivector wave-field \(\psi \in Cl(4,1)\) (32-coeff) as the representation layer under Cartan/Procrustes, Surprise, GoldTether, Biography.
@ -274,10 +273,20 @@ PY
- Containment: no serve-path import of `wave_manifold` until explicit gate; physics still never imports teaching.
- #18 bootstrap/prune of \(\mathcal{I}_{gold}\) **stays deferred** while wave GoldTether subsumption lands.
### Slice-2 subsumption map (landed)
| Operator | Delegation |
|----------|------------|
| `surprise_residual` (32-vec) | `WaveManifold.compute_spectral_leakage` |
| `conformal_procrustes` single non-null field pair | `WaveManifold.wave_analogical_polar` |
| Null-point / (5,K) clouds | Kabsch point-cloud path retained (compatibility) |
| `coherence_residual` / GoldTether drift | `WaveManifold.measure_unitary_residual` (+ chiral term in harmonized residual) |
| `integrate_biography` | unitary residual lock-in on trajectory + blade; encode still `holonomy_encode` |
### What is not done
- Subsumption of `surprise` / `dynamic_manifold` / `goldtether` / `biography` into wave primitives (Slice 2).
- Biography resonant lock-in (W5).
- Multi-pair field conjugacy still uses stacked conjugacy engine directly (same family; not dual path).
- Full resonant standing-wave memory / holographic recall store (W5 beyond unitary lock-in).
- Rust/MLX acceleration of exp-map / cross-spectral (optional later).
- #18 gold-set bootstrap/prune still deferred.
---
@ -291,7 +300,7 @@ PY
| Grade-5 pseudoscalar preservation gate — ⚪ RETIRED (vacuous; see §5) | #19 (closed) |
| Surprise: metric projection + productivity polarity + DiscoveryCandidate wiring — 🟢 done | #20 (math #26; wiring #31) |
| Absent proposals: sensorimotor + ADR-DAG | #21 |
| Wave-field substrate (unitary, leakage, polar, chiral) — 🟢 substrate; subsumption pending | ADR-0241 |
| Wave-field substrate + Slice-2 operator subsumption — 🟢 | ADR-0241 |
| `core_ha` deprecation — 🟡 docs-only (no live tree) | ADR-0241 / deprecation plan |
Closing a gap = flip its `xfail` in `tests/test_third_door_blueprint_fidelity.py` (or the ADR-0241 suite) to a passing behavioral test and delete the matching characterization lock. That is the definition of "done right" here.

View file

@ -215,3 +215,54 @@ def test_wave_manifold_module_does_not_import_teaching():
assert not alias.name.startswith("teaching")
if isinstance(node, ast.ImportFrom) and node.module:
assert not node.module.startswith("teaching")
# --- Slice 2: operator subsumption (no parallel path) ----------------------
def test_surprise_residual_delegates_to_wave_spectral_leakage():
"""32-vec surprise residual matches WaveManifold.compute_spectral_leakage."""
from core.physics.surprise import surprise_residual
M = WaveManifold()
mode = _e(1) + 0.5 * _e(3)
mode = mode / float(np.linalg.norm(mode))
x = 0.7 * mode + 0.4 * _e(2)
B = mode.reshape(N_COMPONENTS, 1)
sur_vec, sur_n = surprise_residual(x, B)
leak_vec, leak_n = M.compute_spectral_leakage(x, [mode])
assert np.allclose(sur_vec, leak_vec, atol=1e-12)
assert abs(sur_n - leak_n) < 1e-12
def test_coherence_residual_delegates_to_wave_unitary():
"""GoldTether coherence_residual is WaveManifold.measure_unitary_residual."""
from core.physics.goldtether import coherence_residual
M = WaveManifold()
psi = _unit_rotor(0.42, plane=8)
assert abs(coherence_residual(psi) - M.measure_unitary_residual(psi)) < 1e-15
def test_conformal_procrustes_single_field_uses_wave_polar():
"""Single non-null field Procrustes recovers the same conjugator as wave polar."""
from core.physics.dynamic_manifold import conformal_procrustes
M = WaveManifold()
psi_A = _unit_rotor(0.15, plane=6)
R_true = _unit_rotor(0.55, plane=10)
psi_B = versor_apply(R_true, psi_A)
V_proc, res = conformal_procrustes(psi_A, psi_B)
V_wave = M.wave_analogical_polar(psi_A, psi_B)
# Both must sandwich A → B; residual small.
assert res < 1e-5
err_p = min(
float(np.linalg.norm(versor_apply(V_proc, psi_A) - psi_B)),
float(np.linalg.norm(versor_apply(-V_proc, psi_A) - psi_B)),
)
err_w = min(
float(np.linalg.norm(versor_apply(V_wave, psi_A) - psi_B)),
float(np.linalg.norm(versor_apply(-V_wave, psi_A) - psi_B)),
)
assert err_p < 1e-5
assert err_w < 1e-5