core/evals/analogical_transfer/harness.py
Shay efa84002cd 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.
2026-07-12 15:27:20 -07:00

188 lines
5.9 KiB
Python

"""Analogical transfer validation harness (ADR-0240)."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Sequence
import numpy as np
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
from core.physics.surprise import (
SurpriseResidualError,
dual_procrustes_surprise,
surprise_residual,
)
@dataclass(frozen=True, slots=True)
class TransferCase:
case_id: str
source_domain: str
target_domain: str
source: np.ndarray
target: np.ndarray
novel_query: np.ndarray
expected_novel: np.ndarray
@dataclass(frozen=True, slots=True)
class TransferResult:
case_id: str
residual: float
goldtether_before: float
goldtether_after: float
correct: bool
refused: bool
reason: str
@dataclass(frozen=True, slots=True)
class AnalogicalTransferReport:
results: tuple[TransferResult, ...]
counts: dict[str, int]
max_residual: float
wrong: int
@property
def all_correct_or_refused(self) -> bool:
return self.wrong == 0
def _identity() -> np.ndarray:
v = np.zeros(N_COMPONENTS, dtype=np.float64)
v[0] = 1.0
return v
def make_fixture_pair() -> TransferCase:
src = _identity()
R = make_rotor_from_angle(0.7, bivector_idx=6)
tgt = versor_apply(R, src)
novel_q = unitize_versor(make_rotor_from_angle(0.3, bivector_idx=7))
expected = versor_apply(R, novel_q)
return TransferCase(
case_id="fixture-rotation-transfer-v1",
source_domain="domain_a_geometry",
target_domain="domain_b_geometry",
source=src,
target=tgt,
novel_query=novel_q,
expected_novel=expected,
)
def run_analogical_transfer(
cases: Sequence[TransferCase],
*,
residual_threshold: float = 0.35,
goldtether: GoldTetherMonitor | None = None,
) -> AnalogicalTransferReport:
"""Learn map source→target, apply to novel_query; gate with residual + GoldTether."""
mon = goldtether or GoldTetherMonitor()
results: list[TransferResult] = []
counts = {"correct": 0, "wrong": 0, "refused": 0}
for case in cases:
gt_before = mon.residual(case.novel_query)
try:
V, proc_r = conformal_procrustes(case.source, case.target)
mapped = versor_apply(V, case.novel_query)
residual = float(np.linalg.norm(mapped - case.expected_novel))
residual = min(residual, procrustes_residual(case.novel_query, case.expected_novel, V))
closed = versor_condition(mapped) < 1e-6 and versor_condition(V) < 1e-6
gt_after = mon.residual(mapped)
except ValueError as exc:
results.append(
TransferResult(
case_id=case.case_id,
residual=float("inf"),
goldtether_before=gt_before,
goldtether_after=gt_before,
correct=False,
refused=True,
reason=f"refused:{exc}",
)
)
counts["refused"] += 1
continue
basis = np.column_stack([_identity(), case.source])
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:
results.append(
TransferResult(
case_id=case.case_id,
residual=residual,
goldtether_before=gt_before,
goldtether_after=gt_after,
correct=False,
refused=True,
reason="closure_failed",
)
)
counts["refused"] += 1
continue
# GoldTether residual must not increase (package acceptance criterion)
gt_ok = gt_after <= gt_before + 1e-9
if residual <= residual_threshold and gt_ok:
mon.update(mapped, epistemic_elevation=True)
results.append(
TransferResult(
case_id=case.case_id,
residual=residual,
goldtether_before=gt_before,
goldtether_after=gt_after,
correct=True,
refused=False,
reason="transfer_ok",
)
)
counts["correct"] += 1
else:
results.append(
TransferResult(
case_id=case.case_id,
residual=residual,
goldtether_before=gt_before,
goldtether_after=gt_after,
correct=False,
refused=False,
reason=(
"goldtether_increased"
if not gt_ok
else f"residual_above_threshold sur={sur_n:.3g} dual={dual['procrustes_residual']:.3g}"
),
)
)
counts["wrong"] += 1
max_res = max((r.residual for r in results if np.isfinite(r.residual)), default=0.0)
return AnalogicalTransferReport(
results=tuple(results),
counts=counts,
max_residual=float(max_res),
wrong=int(counts["wrong"]),
)