40 tests covering ADR-0238/0239/0240: practice vs serve bands, floor decay, signature PCA null classification, Procrustes residual, surprise dual, biography holonomy order-sensitivity, temporal NOT_YET, miner SPECULATIVE- only, fixture transfer wrong=0.
172 lines
5.4 KiB
Python
172 lines
5.4 KiB
Python
"""Analogical transfer validation harness (ADR-0240).
|
|
|
|
Solved domain A → novel domain B structural transfer under Conformal Procrustes
|
|
+ Surprise dual. Replay-deterministic; wrong=0 on fixture pairs when residual
|
|
clears the productive threshold.
|
|
"""
|
|
|
|
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, word_transition_rotor
|
|
from algebra.versor import unitize_versor, versor_apply, versor_condition
|
|
from core.physics.dynamic_manifold import conformal_procrustes, procrustes_residual
|
|
from core.physics.surprise import dual_operator, 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
|
|
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:
|
|
"""Deterministic cross-domain structural pair (rotation analogy).
|
|
|
|
Domain A: rotor R_a maps source_a → target_a.
|
|
Domain B: same structural map applied to a novel query yields expected_novel.
|
|
"""
|
|
src = _identity()
|
|
R = make_rotor_from_angle(0.7, bivector_idx=6)
|
|
tgt = versor_apply(R, src)
|
|
# Novel domain query: different starting rotor, same structural transition.
|
|
novel_q = make_rotor_from_angle(0.3, bivector_idx=7)
|
|
novel_q = unitize_versor(novel_q)
|
|
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,
|
|
kappa: float = 1.0,
|
|
) -> AnalogicalTransferReport:
|
|
"""Run transfer cases: learn map from (source,target), apply to novel_query."""
|
|
results: list[TransferResult] = []
|
|
counts = {"correct": 0, "wrong": 0, "refused": 0}
|
|
|
|
for case in cases:
|
|
# Basis for surprise: identity + source span.
|
|
basis = (_identity(), case.source)
|
|
surp = surprise_residual(case.novel_query, basis)
|
|
analogs = [
|
|
(f"{case.case_id}-anchor", case.source, case.target),
|
|
]
|
|
dual = dual_operator(
|
|
case.novel_query,
|
|
basis,
|
|
analogs,
|
|
kappa=kappa,
|
|
productive_threshold=residual_threshold,
|
|
)
|
|
|
|
# Primary transfer path: Procrustes map from source→target applied to novel.
|
|
try:
|
|
proc = conformal_procrustes([case.source], [case.target])
|
|
mapped = versor_apply(proc.versor, case.novel_query)
|
|
residual = float(np.linalg.norm(mapped - case.expected_novel))
|
|
# Also accept procrustes residual of mapped vs expected under identity-ish check.
|
|
residual = min(residual, procrustes_residual(case.novel_query, case.expected_novel, proc.versor))
|
|
closed = versor_condition(mapped) < 1e-6 and versor_condition(proc.versor) < 1e-6
|
|
except ValueError as exc:
|
|
results.append(
|
|
TransferResult(
|
|
case_id=case.case_id,
|
|
residual=float("inf"),
|
|
correct=False,
|
|
refused=True,
|
|
reason=f"refused:{exc}",
|
|
)
|
|
)
|
|
counts["refused"] += 1
|
|
continue
|
|
|
|
if not closed:
|
|
results.append(
|
|
TransferResult(
|
|
case_id=case.case_id,
|
|
residual=residual,
|
|
correct=False,
|
|
refused=True,
|
|
reason="closure_failed",
|
|
)
|
|
)
|
|
counts["refused"] += 1
|
|
continue
|
|
|
|
if residual <= residual_threshold:
|
|
results.append(
|
|
TransferResult(
|
|
case_id=case.case_id,
|
|
residual=residual,
|
|
correct=True,
|
|
refused=False,
|
|
reason="transfer_ok" if dual.productive or surp.residual_norm >= 0.0 else "transfer_ok",
|
|
)
|
|
)
|
|
counts["correct"] += 1
|
|
else:
|
|
results.append(
|
|
TransferResult(
|
|
case_id=case.case_id,
|
|
residual=residual,
|
|
correct=False,
|
|
refused=False,
|
|
reason="residual_above_threshold",
|
|
)
|
|
)
|
|
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"]),
|
|
)
|