From 541257f81de9ea9b1f16dda8169e5ad21f583156 Mon Sep 17 00:00:00 2001 From: Shay Date: Mon, 13 Jul 2026 21:08:22 -0700 Subject: [PATCH] feat(third-door): Slice-2 wave subsumption (surprise/Procrustes/GoldTether/biography) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- core/physics/biography.py | 19 ++++++- core/physics/dynamic_manifold.py | 20 +++++-- core/physics/goldtether.py | 23 +++++--- core/physics/surprise.py | 18 +++---- core/physics/wave_manifold.py | 52 +++++++++++++------ ...hyperbolic-atlas-and-resonant-cognition.md | 2 +- .../research/third-door-blueprint-fidelity.md | 27 ++++++---- tests/test_adr_0241_wave_manifold.py | 51 ++++++++++++++++++ 8 files changed, 167 insertions(+), 45 deletions(-) diff --git a/core/physics/biography.py b/core/physics/biography.py index 8fb07a20..01e09b02 100644 --- a/core/physics/biography.py +++ b/core/physics/biography.py @@ -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), diff --git a/core/physics/dynamic_manifold.py b/core/physics/dynamic_manifold.py index 18f843e1..7969bbf2 100644 --- a/core/physics/dynamic_manifold.py +++ b/core/physics/dynamic_manifold.py @@ -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, diff --git a/core/physics/goldtether.py b/core/physics/goldtether.py index 9415dd51..1255538d 100644 --- a/core/physics/goldtether.py +++ b/core/physics/goldtether.py @@ -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( diff --git a/core/physics/surprise.py b/core/physics/surprise.py index 674a8510..6c66756c 100644 --- a/core/physics/surprise.py +++ b/core/physics/surprise.py @@ -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") diff --git a/core/physics/wave_manifold.py b/core/physics/wave_manifold.py index e2863971..97820032 100644 --- a/core/physics/wave_manifold.py +++ b/core/physics/wave_manifold.py @@ -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"] diff --git a/docs/adr/ADR-0241-wave-field-driven-hyperbolic-atlas-and-resonant-cognition.md b/docs/adr/ADR-0241-wave-field-driven-hyperbolic-atlas-and-resonant-cognition.md index 24d5caf3..4cd31355 100644 --- a/docs/adr/ADR-0241-wave-field-driven-hyperbolic-atlas-and-resonant-cognition.md +++ b/docs/adr/ADR-0241-wave-field-driven-hyperbolic-atlas-and-resonant-cognition.md @@ -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 diff --git a/docs/research/third-door-blueprint-fidelity.md b/docs/research/third-door-blueprint-fidelity.md index 830abbb5..607279bc 100644 --- a/docs/research/third-door-blueprint-fidelity.md +++ b/docs/research/third-door-blueprint-fidelity.md @@ -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.4C–D | 🟢 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. diff --git a/tests/test_adr_0241_wave_manifold.py b/tests/test_adr_0241_wave_manifold.py index 7cafb70f..1570264e 100644 --- a/tests/test_adr_0241_wave_manifold.py +++ b/tests/test_adr_0241_wave_manifold.py @@ -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