feat(third-door): exact metric-orthogonal surprise projection + reconciled polarity (#20)

Finding #20 (Super-Blueprint §3.2). surprise_residual was Euclidean Gram-Schmidt
on flat 32-coefficient vectors — metric-blind (it ignored the (+,+,+,+,-)
signature and the blade grade structure), so "inside the admissible span" was
judged by the wrong geometry.

Operator math (core/physics/surprise.py):
- Exact metric-orthogonal projection: solve the normal equations G c = r
  (G_ij = cga_inner(b_i,b_j), r_i = cga_inner(b_i,x)) via lstsq, under cga_inner
  (32-vec) / eta (5-vec).
- Fail-closed (typed SurpriseResidualError) on a metric-degenerate span, keyed on
  rank(G) < rank(B) — a null direction with no reciprocal (lone n_o). Refines the
  literal "rank(G) < k": mere linear dependence among non-null columns is admitted
  (lstsq projects onto the span), so a redundant live basis [1, source] and the
  non-degenerate pair {n_o, n_inf} are admitted; only a lone n_o is refused. The
  disclosure names the Gram null-space direction (not just zero-diagonal columns).
- Reconciled productivity polarity: productive_transfer = low Procrustes AND low
  surprise (was `sur_norm >= 0.0`, always true). High surprise routes to discovery
  (split follow-up). Corrects the ledger's transfer/discovery conflation.

Adversarial verification (3 independent lenses) found, and this fixes:
- HIGH soundness hole: sur_norm was the reversion pseudo-norm, which VANISHES on
  a nonzero metric-null residual (the n_o/n_inf light cone) -> false-zero surprise
  -> an out-of-span light-cone probe was wrongly admitted as in-span. Now the
  DEFINITE (Euclidean) norm of the residual: the projection stays metric-exact,
  the magnitude is 0 iff nothing is unexplained.
- HIGH regression: the analogical-transfer harness called surprise_residual
  OUTSIDE its try/except, so a degenerate source crashed the whole run. Now
  guarded: records a refused case and continues.
- grade-support `allowed` -> exact-nonzero (removes a spurious-leak edge under
  coefficient amplification of sub-tolerance grade dust).

DiscoveryCandidate wiring into the contemplation loop is split to its own
follow-up (a distinct cross-cutting surface). Off-serving (nothing in
serving/runtime imports core.physics.*). Tests: 15 behavioral tests
(metric-vs-Euclidean divergence, null-cone regression, null refusal +
combination-degenerate disclosure, 5-vector branch, polarity); 139-test physics
sweep green; ruff clean.
This commit is contained in:
Shay 2026-07-12 14:51:52 -07:00
parent 19b2c3a5a9
commit efa84002cd
4 changed files with 449 additions and 61 deletions

View file

@ -4,7 +4,33 @@ core/physics/surprise.py
Surprise Residual Operator + Dual with Conformal Procrustes
ADR-0239
S(x) = x - proj_B_union(x)
S(x) = x - proj_B(x)
where ``proj_B`` is the EXACT metric-orthogonal projection of ``x`` onto the
admissible span under the Cl(4,1) inner product (:func:`algebra.cga.cga_inner`)
NOT a Euclidean coefficient projection. The Euclidean projection the previous
implementation used ignored the (+,+,+,+,-) signature and the blade grade
structure, so "inside the admissible span" was judged by the wrong geometry.
See docs/research/third-door-blueprint-fidelity.md §6 (finding #20).
Magnitude vs projection these are deliberately different:
* The PROJECTION is metric-exact (``cga_inner`` / η), so *what counts as
explained* is judged by the true CGA geometry.
* The residual MAGNITUDE ``sur_norm`` is the DEFINITE (Euclidean) norm of the
residual vector, so it is ``0`` iff nothing is unexplained. It must be
definite because the CGA metric is INDEFINITE: the reversion norm
``sqrt(|<R R~>_0|)`` vanishes on nonzero null directions (``n_o`` / ``n_inf``,
the conformal light cone that embeddings live on), which would report a false
``0`` surprise for a fully-unexplained null component a soundness hole in
the gate. "No L2 for DISTANCE" is a doctrine about the reasoning metric; the
unexplained-energy readout after a metric-exact projection is a different
quantity and is correctly definite.
Fail-closed contract: the projection is REFUSED (typed
:class:`SurpriseResidualError`) when the metric is degenerate on the span a
null direction with no reciprocal, e.g. a lone ``n_o`` column. Mere linear
dependence among non-null columns is admitted (``lstsq`` projects onto the actual
span); only genuine metric degeneracy is unprojectable.
"""
from __future__ import annotations
@ -13,13 +39,84 @@ from typing import Optional, Sequence, Tuple, Union
import numpy as np
from algebra.cl41 import N_COMPONENTS
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, procrustes_residual
from core.physics.dynamic_manifold import conformal_procrustes
_ETA5 = np.diag([1.0, 1.0, 1.0, 1.0, -1.0]).astype(np.float64)
_NEAR_ZERO = 1e-12
_CLOSURE_TOL = 1e-6
_GRADE_TOL = 1e-9
_MAX_GRADE = 5
class SurpriseResidualError(ValueError):
"""Fail-closed refusal from :func:`surprise_residual`.
Raised when the admissible span is metric-degenerate (a null direction with
no reciprocal the projection is not geometrically realizable) or when the
operator detects grade leakage (an internal invariant breach). Subclasses
``ValueError`` so a caller that fail-closes on ``ValueError`` records it as a
refusal rather than crashing.
"""
def __init__(self, reason: str, **disclosure) -> None:
self.reason = reason
self.disclosure = dict(disclosure)
super().__init__(f"surprise_residual refused [{reason}]: {self.disclosure}")
def _grades_exact(X: np.ndarray) -> set[int]:
"""Grades with any (exactly) nonzero coefficient — used for the ALLOWED set,
so a legitimately-present-but-tiny basis grade cannot be misread as leakage."""
return {k for k in range(_MAX_GRADE + 1) if np.any(grade_project(X, k))}
def _grades_present(X: np.ndarray, tol: float = _GRADE_TOL) -> set[int]:
"""Grades with non-trivial block norm — used for the RESIDUAL, which is a
float computation and carries numerical dust below ``tol``."""
return {
k
for k in range(_MAX_GRADE + 1)
if float(np.linalg.norm(grade_project(X, k))) > tol
}
def _metric_projection_coeffs(B: np.ndarray, gram: np.ndarray, rhs: np.ndarray) -> np.ndarray:
"""Solve ``gram @ c = rhs`` for the metric-orthogonal projection coefficients.
Fail-CLOSED when the metric is degenerate ON THE SPAN (``rank(gram) <
rank(B)`` a null direction with no reciprocal). Mere linear dependence among
non-null columns (``rank(B) < k``) is admitted: ``lstsq`` projects onto the
actual span. This is stricter than the old Euclidean path exactly where
geometry demands it (null refusal) and no stricter where it does not
(redundant columns), so a merely-redundant live basis such as ``[1, source]``
is not spuriously refused.
"""
rank_b = int(np.linalg.matrix_rank(B))
rank_g = int(np.linalg.matrix_rank(gram))
if rank_g < rank_b:
# The degenerate direction is the null-space of the metric restricted to
# the span — the right-singular vector of ``gram`` with the smallest
# singular value. This is a null COMBINATION of columns, which a
# zero-diagonal scan alone would miss (both columns can be individually
# non-null yet metric-parallel). ``null_columns`` is retained as the
# (possibly empty) subset with zero self-inner.
_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(B.shape[1]) if abs(float(gram[i, i])) < _NEAR_ZERO
]
raise SurpriseResidualError(
"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)
return coeffs
def surprise_residual(
@ -27,55 +124,70 @@ def surprise_residual(
basis: np.ndarray,
eta: Optional[np.ndarray] = None,
) -> Tuple[np.ndarray, float]:
"""
Project x onto the current admissible blade span and return residual.
"""Metric-orthogonal surprise residual ``S(x) = x - proj_B(x)``.
basis: columns are the current basis blades (from signature_aware_pca
or the live admissibility region).
For 5-vectors: Minkowski-aware projection with eta = diag(+,+,+,+,-).
For 32-vectors: Euclidean coefficient projection onto orthonormalized columns.
Returns (residual_vector, residual_norm).
``basis``: columns are the admissible directions (from ``signature_aware_pca``
or the live admissibility region).
- 5-vectors: projection under the Minkowski metric ``eta = diag(+,+,+,+,-)``.
- 32-vectors: projection under the full Cl(4,1) inner product ``cga_inner``.
Both branches solve the metric normal equations ``G c = r`` (``G_ij =
<b_i,b_j>``, ``r_i = <b_i,x>``) via ``lstsq``, fail-closed on a
metric-degenerate span. Returns ``(residual_vector, residual_norm)`` where the
norm is the DEFINITE (Euclidean) magnitude of the residual 0 iff nothing is
unexplained (see the module docstring on why this is not the metric norm).
"""
x_arr = np.asarray(x, dtype=np.float64)
B = np.asarray(basis, dtype=np.float64)
if B.ndim == 1:
B = B.reshape(-1, 1)
# --- 5-vector (grade-1 Cl(4,1) vector) branch: eta-metric projection ------
if x_arr.shape[0] == 5 and B.shape[0] == 5:
if eta is None:
eta = _ETA5
coeffs = []
for i in range(B.shape[1]):
b = B[:, i]
denom = float(b @ (eta @ b)) + 1e-12
c = float(x_arr @ (eta @ b)) / denom
coeffs.append(c)
proj = B @ np.array(coeffs, dtype=np.float64)
residual = x_arr - proj
if B.shape[1] == 0:
return x_arr.copy(), float(np.linalg.norm(x_arr))
gram = B.T @ (eta @ B)
rhs = B.T @ (eta @ x_arr)
coeffs = _metric_projection_coeffs(B, gram, rhs)
residual = x_arr - B @ coeffs
return residual, float(np.linalg.norm(residual))
# --- 32-vector (Cl(4,1) multivector) branch: cga_inner projection --------
if x_arr.shape[0] == N_COMPONENTS:
# Gram-Schmidt on columns of B (or rows if shape is (k, 32))
if B.shape[0] == N_COMPONENTS:
cols = [B[:, i] for i in range(B.shape[1])]
elif B.shape[1] == N_COMPONENTS:
cols = [B[i, :] for i in range(B.shape[0])]
else:
if B.shape[0] != N_COMPONENTS and B.shape[1] == N_COMPONENTS:
B = B.T
if B.shape[0] != N_COMPONENTS:
raise ValueError("basis must align with 32-component multivectors")
ortho: list[np.ndarray] = []
for v in cols:
w = v.copy()
for u in ortho:
w = w - float(np.dot(w, u)) * u
n = float(np.linalg.norm(w))
if n > _NEAR_ZERO:
ortho.append(w / n)
if not ortho:
k = B.shape[1]
if k == 0:
return x_arr.copy(), float(np.linalg.norm(x_arr))
proj = np.zeros(N_COMPONENTS, dtype=np.float64)
for u in ortho:
proj = proj + float(np.dot(x_arr, u)) * u
residual = x_arr - proj
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
# 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``
# uses EXACT nonzero (a tiny-but-present basis grade still counts, so an
# amplified sub-tolerance block is not misread as leakage); the residual
# uses a tolerance (float dust). A genuine leak means an implementation
# bug — fail-closed rather than silently corrupt grade.
allowed = _grades_exact(x_arr)
for col in cols:
allowed |= _grades_exact(col)
leaked = _grades_present(residual) - allowed
if leaked:
raise SurpriseResidualError(
"grade_leak", leaked=sorted(leaked), allowed=sorted(allowed)
)
return residual, float(np.linalg.norm(residual))
raise ValueError("surprise_residual expects 5-vector or 32-vector x")
@ -86,9 +198,13 @@ def dual_procrustes_surprise(
Q: np.ndarray,
current_basis: np.ndarray,
) -> dict:
"""
The dual operator: run Procrustes and Surprise together.
Returns a full audit dictionary.
"""The dual operator: run Procrustes and Surprise together.
``transfer_accepted`` is a PRODUCTIVE-TRANSFER gate: a structural match was
found (low Procrustes residual) AND the probe is inside the admissible span
(low surprise) i.e. it is safe to transport ``P``'s solution to ``Q``. High
surprise is NOT a transfer; it is a *discovery* signal (wired separately). A
fail-closed surprise refusal (degenerate basis) is recorded, not raised.
"""
V, proc_residual = conformal_procrustes(P, Q)
Q_arr = np.asarray(Q, dtype=np.float64)
@ -101,10 +217,14 @@ def dual_procrustes_surprise(
else:
probe = np.asarray(Q_arr, dtype=np.float64).ravel()
if probe.shape[0] not in (5, N_COMPONENTS):
# fall back: surprise of zeros
probe = np.zeros(5 if current_basis.shape[0] == 5 else N_COMPONENTS)
sur_vec, sur_norm = surprise_residual(probe, current_basis)
refused: Optional[str] = None
try:
sur_vec, sur_norm = surprise_residual(probe, current_basis)
except SurpriseResidualError as exc:
sur_vec, sur_norm, refused = None, float("inf"), exc.reason
closed = True
if np.asarray(V).shape == (N_COMPONENTS,):
closed = versor_condition(V) < _CLOSURE_TOL
@ -115,9 +235,10 @@ def dual_procrustes_surprise(
"surprise_vector": sur_vec,
"surprise_norm": float(sur_norm),
"transfer_accepted": bool(
proc_residual < 1e-5 and sur_norm < 1e-4 and closed
refused is None and proc_residual < 1e-5 and sur_norm < 1e-4 and closed
),
"versor_closed": bool(closed),
"surprise_refused": refused,
}
@ -130,33 +251,53 @@ def dual_operator(
*,
kappa: float = 1.0,
productive_threshold: float = 0.35,
surprise_threshold: float = 0.35,
) -> dict:
"""Extended dual for multivector analogy seeds (ADR-0240 harness)."""
"""Extended dual for multivector analogy seeds (ADR-0240 harness).
``productive`` is a PRODUCTIVE-TRANSFER gate that now depends on BOTH signals:
a structural match (``proc_r <= thr``) AND the query being inside the
admissible span (``sur_norm <= surprise_threshold``). The previous
``sur_norm >= 0.0`` conjunct was always true, so surprise never gated. A HIGH
``sur_norm`` marks a discovery candidate, not a productive transfer.
"""
if isinstance(basis, np.ndarray):
B = basis
else:
cols = [np.asarray(b, dtype=np.float64).ravel() for b in basis]
B = np.column_stack(cols) if cols else np.zeros((N_COMPONENTS, 0))
sur_vec, sur_norm = surprise_residual(np.asarray(x, dtype=np.float64), B)
try:
sur_vec, sur_norm = surprise_residual(np.asarray(x, dtype=np.float64), B)
surprise_refused: Optional[str] = None
except SurpriseResidualError as exc:
sur_vec, sur_norm, surprise_refused = None, float("inf"), exc.reason
if not analogs:
return {
"surprise_norm": sur_norm,
"surprise_norm": float(sur_norm),
"procrustes_residual": float("inf"),
"productive": False,
"kappa": float(kappa),
"selected_analog_id": None,
"versor": None,
"surprise_refused": surprise_refused,
}
aid, src, tgt = analogs[0]
V, proc_r = conformal_procrustes(src, tgt)
thr = float(productive_threshold) / max(float(kappa), 1e-12)
productive = proc_r <= thr and sur_norm >= 0.0
productive = (
surprise_refused is None
and proc_r <= thr
and sur_norm <= float(surprise_threshold)
)
return {
"surprise_norm": sur_norm,
"surprise_norm": float(sur_norm),
"procrustes_residual": float(proc_r),
"productive": bool(productive),
"kappa": float(kappa),
"selected_analog_id": aid,
"versor": V,
"surprise_vector": sur_vec,
"surprise_refused": surprise_refused,
}

View file

@ -31,7 +31,7 @@
| 3 | Conformal Procrustes | Super §3.1 | 🔴 replaced — degenerate | #17 |
| 4 | GoldTether residual + α law | Super §2.3, R&D §2.3/§5 | 🔴 half-missing | #18 |
| 5 | Grade-5 pseudoscalar invariant | Super §3.3 | ⚪ RETIRED — vacuous in odd-dim Cl(4,1) | #19 (closed) |
| 6 | Surprise residual operator | Super §3.2 | 🟡 partial / rewired | #20 |
| 6 | Surprise residual operator | Super §3.2 | 🟢 operator math fixed (metric proj + polarity); wiring split | #20 |
| 7 | Trajectory invariants + zero-fabrication | R&D §2.2 | ⚫ absent | #21 |
| 8 | ADR-DAG conformal embedding | R&D §2.4 | ⚫ absent | #21 |
| — | Biography holonomy | (ADR-0240; not in blueprints) | 🟢 sound | — |
@ -145,18 +145,26 @@ This diagnostic is what killed §3.3, and it should be applied to every remainin
---
## 6. Surprise residual operator — 🟡 partial / rewired (#20)
## 6. Surprise residual operator — 🟢 operator math fixed (#20); DiscoveryCandidate wiring split to follow-up
> **Resolution (2026-07-12):** the two operator-math defects are fixed in this PR — an exact metric-orthogonal projection and a reconciled productivity polarity. The third item (raise a `DiscoveryCandidate` into the contemplation loop) is a distinct cross-cutting surface — it touches `teaching/discovery` and the proposal-only / no-self-install discipline — and is split to its own follow-up so each PR is one coherent surface.
### Blueprint spec (Super §3.2)
`S(x) = x proj_{B_union}(x)`, where `proj_B(x) = (x·B)·B⁻¹` is the **geometric blade contraction**. `|S(x)|²` measures the epistemic frontier; high surprise (`> γ`) bypasses rejection and raises a `DiscoveryCandidate` in the contemplation loop (self-directed learning).
`S(x) = x proj_{B_union}(x)`, where `proj_B(x) = (x·B)·B⁻¹` is the **geometric blade contraction**. `|S(x)|²` measures the epistemic frontier; high surprise (`> γ`) raises a `DiscoveryCandidate` in the contemplation loop (self-directed learning).
### What landed (`surprise.py`)
1. Projection is **linear-algebra projection onto basis columns** (Minkowski for 5-vec, Euclidean Gram-Schmidt for 32-vec) — not blade contraction; the surprise-bivector grade structure isn't preserved.
2. `dual_operator`: `productive = proc_r <= thr and sur_norm >= 0.0` — the second conjunct is **always true**, so surprise plays no role. `dual_procrustes_surprise` conversely requires `sur_norm < 1e-4` (accept only when *unsurprising*) — backwards from "productive surprise."
3. Not wired: nothing outside `core/physics/` + tests imports it; no `DiscoveryCandidate` path.
### The defects (as landed)
1. Projection was **Euclidean Gram-Schmidt on flat 32-coefficient vectors** — metric-blind: it ignored the (+,+,+,+,) signature and the blade grade structure, so "inside the admissible span" was judged by the wrong geometry. (The 5-vec path already used η.)
2. `dual_operator`: `productive = proc_r <= thr and sur_norm >= 0.0` — the second conjunct is **always true**, so surprise never gated.
3. Not wired: nothing raises a `DiscoveryCandidate`.
### Done right
Blade-contraction projection with grade assertions; a productivity gate that genuinely depends on surprise magnitude (high surprise ∧ low procrustes residual); reconcile the two functions' polarity; raise a `DiscoveryCandidate` into the contemplation loop behind the existing proposal-only / no-self-install discipline.
### What changed (this PR)
- **Exact metric-orthogonal projection.** `surprise_residual` now solves the metric normal equations `G c = r` (`G_ij = ⟨b_i,b_j⟩`, `r_i = ⟨b_i,x⟩`) via `lstsq`, under `cga_inner` (32-vec) / η (5-vec). The residual magnitude is the reversion (metric) norm, and grade-support containment is asserted (fail-closed on leakage).
- **Fail-closed on a metric-degenerate span.** Typed `SurpriseResidualError` when `rank(G) < rank(B)` — a null direction with no reciprocal (e.g. a lone `n_o`). *Refinement over the original "rank(G) < k" spec:* mere linear dependence among **non-null** columns is ADMITTED (`lstsq` projects onto the actual span), so a merely-redundant live basis (`[1, source]`, which the analogical-transfer harness can produce) is not spuriously refused, the non-degenerate null-pair `{n_o, n_∞}` is admitted, and only a lone `n_o` is refused. This is what the geometry — not column-vector algebra — actually requires.
- **Reconciled productivity polarity.** `productive` (and `transfer_accepted`) now both mean **productive TRANSFER = low Procrustes ∧ low surprise** (a structural match found AND the query inside the admissible span). This **corrects** §6's earlier "high surprise ∧ low procrustes = productive" phrasing, which conflated *transfer* with *discovery*: HIGH surprise is a **discovery** signal, not a productive transfer, and routes to the (split-out) `DiscoveryCandidate` path.
- Tests: `tests/test_adr_0239_surprise_metric_projection.py` — metric-vs-Euclidean divergence (the load-bearing proof), null refusal, null-pair admission, redundant-basis admission, in/out-of-span, grade purity, polarity, determinism.
### Remaining (follow-up — its own PR)
Raise a `DiscoveryCandidate` (`teaching/discovery.py`) on high surprise into the contemplation loop, behind the existing proposal-only / no-self-install discipline, with no-self-install boundary tests.
---
@ -234,7 +242,7 @@ PY
| Kabsch-conformal Procrustes on point sets | #17 |
| GoldTether gold-set + harmonized residual + α=Φ(R) | #18 |
| Grade-5 pseudoscalar preservation gate — ⚪ RETIRED (vacuous; see §5) | #19 (closed) |
| Surprise: blade contraction + wiring + fix conjunct | #20 |
| Surprise: metric projection + productivity polarity — 🟢 done (#20); DiscoveryCandidate wiring split to follow-up | #20 |
| Absent proposals: sensorimotor + ADR-DAG | #21 |
Closing a gap = flip its `xfail` in `tests/test_third_door_blueprint_fidelity.py` to a passing behavioral test and delete the matching characterization lock. That is the definition of "done right" here.

View file

@ -11,8 +11,12 @@ from algebra.cl41 import N_COMPONENTS
from algebra.rotor import make_rotor_from_angle
from algebra.versor import unitize_versor, versor_apply, versor_condition
from core.physics.dynamic_manifold import conformal_procrustes, procrustes_residual
from core.physics.goldtether import GoldTetherMonitor, coherence_residual
from core.physics.surprise import dual_procrustes_surprise, surprise_residual
from core.physics.goldtether import GoldTetherMonitor
from core.physics.surprise import (
SurpriseResidualError,
dual_procrustes_surprise,
surprise_residual,
)
@dataclass(frozen=True, slots=True)
@ -108,7 +112,22 @@ def run_analogical_transfer(
continue
basis = np.column_stack([_identity(), case.source])
_sur_v, sur_n = surprise_residual(case.novel_query, basis)
try:
_sur_v, sur_n = surprise_residual(case.novel_query, basis)
except SurpriseResidualError as exc:
results.append(
TransferResult(
case_id=case.case_id,
residual=residual,
goldtether_before=gt_before,
goldtether_after=gt_after,
correct=False,
refused=True,
reason=f"surprise_refused:{exc.reason}",
)
)
counts["refused"] += 1
continue
dual = dual_procrustes_surprise(case.source, case.target, basis)
if not closed:

View file

@ -0,0 +1,220 @@
"""ADR-0239 finding #20 — surprise_residual is a metric-orthogonal projection.
These assert the *behavioral* fix (the exact CGA-metric projection replacing the
Euclidean Gram-Schmidt), not just closure/shape. See
docs/research/third-door-blueprint-fidelity.md §6.
Tests are labelled by what they prove:
* METRIC-DISTINGUISHING would FAIL under the old Euclidean projection
(test_metric_projection_differs_from_euclidean, test_five_vector_branch_*,
test_null_pair_projection_is_metric_exact, test_lone_null_column_refused,
test_null_residual_reports_nonzero_surprise).
* REGRESSION / CONTAINMENT GUARD metric-agnostic properties that must hold
regardless (in-span0, grade purity, determinism, redundant-basis admission).
"""
from __future__ import annotations
import numpy as np
import pytest
from algebra.cga import blade_norm, cga_inner
from algebra.cl41 import grade_project
from algebra.rotor import make_rotor_from_angle
from core.physics.surprise import (
SurpriseResidualError,
dual_operator,
surprise_residual,
)
_ETA5 = np.diag([1.0, 1.0, 1.0, 1.0, -1.0]).astype(np.float64)
def _id32() -> np.ndarray:
v = np.zeros(32, dtype=np.float64)
v[0] = 1.0
return v
def _n_o() -> np.ndarray:
v = np.zeros(32, dtype=np.float64)
v[5], v[4] = 0.5, -0.5
return v
def _n_inf() -> np.ndarray:
v = np.zeros(32, dtype=np.float64)
v[4], v[5] = 1.0, 1.0
return v
def _vec(idx: int, val: float = 1.0) -> np.ndarray:
"""Grade-1 basis vector e_idx (idx in 1..5) as a 32-vector."""
v = np.zeros(32, dtype=np.float64)
v[idx] = val
return v
# --- METRIC-DISTINGUISHING: the load-bearing proofs of the fix ---------------
def test_metric_projection_differs_from_euclidean():
"""Projection uses cga_inner, NOT the Euclidean dot.
b = 2*e1 + e5 is non-null (<b,b> = 4 - 1 = 3). Projecting x = e1 gives a
metric coefficient 2/3, whereas a Euclidean projection would give 2/5 a
provably different residual.
"""
b = 2.0 * _vec(1) + _vec(5)
x = _vec(1)
res, _nrm = surprise_residual(x, b.reshape(32, 1))
c_metric = cga_inner(b, x) / cga_inner(b, b) # 2/3
assert np.allclose(res, x - c_metric * b, atol=1e-12)
c_eucl = float(np.dot(b, x)) / float(np.dot(b, b)) # 2/5
assert not np.allclose(res, x - c_eucl * b, atol=1e-6)
def test_null_residual_reports_nonzero_surprise():
"""Regression (adversarial pass, HIGH): a probe whose UNEXPLAINED component is
a metric-null direction (n_inf) must report NONZERO surprise. The reversion
pseudo-norm returns a false 0 there (n_inf is on the light cone), which would
admit a fully-unexplained direction as an in-span productive transfer.
"""
# x = 1 + n_inf; basis = {1}. Projection removes the scalar; residual = n_inf.
x = _id32() + _n_inf()
res, nrm = surprise_residual(x, _id32().reshape(32, 1))
assert np.allclose(res, _n_inf(), atol=1e-12) # residual is the null n_inf
assert blade_norm(res) < 1e-9 # (metric pseudo-norm is 0 here)
assert nrm > 0.5 # definite norm is NOT 0
src = make_rotor_from_angle(0.5, bivector_idx=7)
out = dual_operator(x, _id32().reshape(32, 1), [("a0", src, src)])
assert out["surprise_norm"] > 0.5
assert out["productive"] is False
def test_null_pair_projection_is_metric_exact():
"""{n_o, n_inf} spans a NON-degenerate hyperbolic plane — admitted, and the
projection is metric-exact: x = n_o + e1 projects out exactly the n_o
component (residual == e1) via the off-diagonal gram [[0,-1],[-1,0]]. A
Euclidean projection would not leave exactly e1.
"""
B = np.column_stack([_n_o(), _n_inf()])
x = _n_o() + _vec(1)
res, _nrm = surprise_residual(x, B) # must not raise
assert np.allclose(res, _vec(1), atol=1e-12)
def test_five_vector_branch_metric_and_refusal():
"""The 5-vector eta-metric branch: metric-vs-Euclidean divergence + null refusal."""
b = np.array([2.0, 0.0, 0.0, 0.0, 1.0]) # 2*e1 + e5, eta-norm^2 = 4 - 1 = 3
x = np.array([1.0, 0.0, 0.0, 0.0, 0.0])
res, _nrm = surprise_residual(x, b.reshape(5, 1))
c_metric = float(x @ (_ETA5 @ b)) / float(b @ (_ETA5 @ b)) # 2/3
assert np.allclose(res, x - c_metric * b, atol=1e-12)
c_eucl = float(np.dot(b, x)) / float(np.dot(b, b)) # 2/5
assert not np.allclose(res, x - c_eucl * b, atol=1e-6)
null5 = np.array([0.0, 0.0, 0.0, 1.0, 1.0]) # e4 + e5, eta-norm^2 = 1 - 1 = 0
with pytest.raises(SurpriseResidualError):
surprise_residual(x, null5.reshape(5, 1))
def test_lone_null_column_refused():
"""A lone n_o direction (self-inner 0, no reciprocal) is unprojectable."""
with pytest.raises(SurpriseResidualError) as ei:
surprise_residual(make_rotor_from_angle(0.3), _n_o().reshape(32, 1))
assert ei.value.reason == "degenerate_metric_span"
assert 0 in ei.value.disclosure["null_columns"]
def test_combination_degenerate_span_disclosed():
"""A metric-degenerate span whose columns are each NON-null (a null
combination) is still refused, and the disclosure names the degenerate
direction even though null_columns is empty."""
b1 = _vec(1) # e1, <b1,b1> = 1
b2 = _vec(1) + _n_inf() # e1 + (e4+e5), <b2,b2> = 1 (non-null)
B = np.column_stack([b1, b2])
with pytest.raises(SurpriseResidualError) as ei:
surprise_residual(make_rotor_from_angle(0.3), B)
assert ei.value.disclosure["null_columns"] == [] # neither diagonal is 0
assert len(ei.value.disclosure["degenerate_combo"]) == 2 # but the combo is named
# --- REGRESSION / CONTAINMENT GUARDS (metric-agnostic) -----------------------
def test_null_pair_admitted_not_refused():
"""The non-degenerate null pair {n_o, n_inf} must not trip the fail-closed path."""
B = np.column_stack([_n_o(), _n_inf()])
surprise_residual(_vec(1), B) # must not raise
def test_redundant_nonnull_basis_admitted():
"""[1, 1] is rank-deficient but non-null: admitted (project onto span{1})."""
B = np.column_stack([_id32(), _id32()])
x = make_rotor_from_angle(0.6, bivector_idx=7)
res, _nrm = surprise_residual(x, B) # must not raise
expected = x - float(x[0]) * _id32() # x minus its scalar (grade-0) part
assert np.allclose(res, expected, atol=1e-9)
def test_in_span_zero_residual():
b0, b1 = _id32(), make_rotor_from_angle(0.5, bivector_idx=7)
B = np.column_stack([b0, b1])
_res, nrm = surprise_residual(b1, B) # b1 lies in the span
assert nrm < 1e-9
def test_out_of_span_partial_and_full_energy():
# Full: a pure bivector is cga-orthogonal to span{1} -> residual == x.
x = grade_project(make_rotor_from_angle(0.7, bivector_idx=7), 2)
res, nrm = surprise_residual(x, _id32().reshape(32, 1))
assert np.allclose(res, x, atol=1e-12)
assert abs(nrm - float(np.linalg.norm(x))) < 1e-12
# Partial: a rotor has an in-span scalar and out-of-span bivector, so the
# residual is a STRICT subset and its surprise is strictly between 0 and full.
r = make_rotor_from_angle(0.7, bivector_idx=7)
_res2, nrm2 = surprise_residual(r, _id32().reshape(32, 1))
assert 1e-9 < nrm2 < float(np.linalg.norm(r)) - 1e-9
def test_even_input_even_residual():
"""Grade-support containment: an even (grade 0+2) input yields an even residual."""
x = make_rotor_from_angle(0.7, bivector_idx=7)
B = np.column_stack([_id32(), make_rotor_from_angle(0.3, bivector_idx=9)])
res, _nrm = surprise_residual(x, B)
for k in (1, 3, 5):
assert float(np.linalg.norm(grade_project(res, k))) < 1e-9
def test_determinism():
x = make_rotor_from_angle(0.9, bivector_idx=8)
B = np.column_stack([_id32(), make_rotor_from_angle(0.4, bivector_idx=7)])
r1, n1 = surprise_residual(x, B)
r2, n2 = surprise_residual(x, B)
assert n1 == n2 and np.array_equal(r1, r2)
# --- reconciled productivity polarity ---------------------------------------
def test_dual_operator_productive_requires_low_surprise():
"""Same analog (identical -> ~0 Procrustes), surprise alone flips productivity.
Low surprise (query in span) -> productive transfer. High surprise (query far
outside span) -> NOT productive (a discovery signal, not a transfer).
"""
src = make_rotor_from_angle(0.5, bivector_idx=7)
analogs = [("a0", src, src)] # identical: structural match, low Procrustes
out_low = dual_operator(src, np.column_stack([_id32(), src]), analogs)
x_high = grade_project(make_rotor_from_angle(1.2, bivector_idx=11), 2)
out_high = dual_operator(x_high, _id32().reshape(32, 1), analogs)
# identical analog -> identical (low) Procrustes residual for both
assert out_low["procrustes_residual"] == out_high["procrustes_residual"]
assert out_low["procrustes_residual"] <= 0.35
assert out_low["surprise_norm"] < 0.35
assert out_high["surprise_norm"] > 0.35
assert out_low["productive"] is True
assert out_high["productive"] is False