"""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"]), )