diff --git a/core/physics/relation_compiler.py b/core/physics/relation_compiler.py new file mode 100644 index 00000000..fa03b4f7 --- /dev/null +++ b/core/physics/relation_compiler.py @@ -0,0 +1,91 @@ +"""core.physics.relation_compiler — affine relation → constraint Hamiltonian (ADR-0249 P2). + +Compiles a single affine relation ``output = scale·input + offset`` (Tier-1: +scale > 0, offset ∈ ℝ, input a known quantity) into a quadratic-well +ProblemHamiltonian whose ground state is the null point encoding the output. + +Anti-hollow (spike §4.1): the compiler NEVER evaluates ``scale·input + offset`` +in Python. It embeds the input as a null point (P1) and applies the relation's +*structure* as versor operators — a dilator for the scale, a translator for the +offset — so the arithmetic is performed by the substrate's geometric product, +not by the compiler. The returned Hamiltonian is a bare geometric constraint +(a projector well); it carries no answer and no relation coefficients. Only +relaxation + projective readback recover the output. + +Reuses the ratified ``compile_quadratic_well`` + ``HamiltonianCompileError`` +contracts (spike §4.3/§4.4); fail-closed on non-finite coefficients and on +non-positive scale (outside the positive-dilation Tier-1 envelope). This is the +single-relation primitive; multi-step chaining (previously-certified state as +the next input) is the P4 turn-program compiler. Serve-quarantined (A-04): +``core/physics/`` is never imported by ``chat/runtime.py``. +""" +from __future__ import annotations + +import math + +import numpy as np + +from core.physics.cognitive_lifecycle import ( + HamiltonianCompileError, + ProblemHamiltonian, + compile_quadratic_well, +) +from core.physics.quantity_kernel import ( + dilate_quantity, + embed_quantity, + translate_quantity, +) + +__all__ = ["compile_affine_relation", "affine_relaxation_start"] + +# Below this the transported target has collapsed and cannot be unit-normalized. +_MIN_TARGET_NORM = 1e-12 + + +def _finite(value: float, *, what: str) -> float: + v = float(value) + if not math.isfinite(v): + raise HamiltonianCompileError(f"{what}_not_finite") + return v + + +def affine_relaxation_start(input_quantity: float) -> np.ndarray: + """Unit-norm null point of the KNOWN input — the natural relaxation start. + + Decodes to the input (a given of the relation), never to the answer, so + exposing it is not a hollow leak. + """ + q = _finite(input_quantity, what="input") + psi = embed_quantity(q) + return (psi / np.linalg.norm(psi)).astype(np.float64) + + +def compile_affine_relation( + input_quantity: float, + *, + scale: float, + offset: float, + curvature: float = 1.0, +) -> ProblemHamiltonian: + """``output = scale·input + offset`` as a quadratic-well constraint Hamiltonian. + + ``scale`` > 0 (positive-dilation Tier-1 envelope); ``offset`` any finite + real. ``curvature`` is validated by ``compile_quadratic_well``. + """ + q = _finite(input_quantity, what="input") + s = _finite(scale, what="scale") + o = _finite(offset, what="offset") + if s <= 0.0: + raise HamiltonianCompileError("scale_not_positive", scale=s) + + # Relation structure as versors — never ``s*q + o`` in Python. Dilation + # scales by e^{-alpha}, so multiplying by s needs alpha = -ln(s); the + # substrate's geometric product performs the actual arithmetic. + psi_scaled = dilate_quantity(embed_quantity(q), -math.log(s)) + psi_target = translate_quantity(psi_scaled, o) + + norm = float(np.linalg.norm(psi_target)) + if norm < _MIN_TARGET_NORM: + raise HamiltonianCompileError("degenerate_affine_target", norm=norm) + unit_target = (psi_target / norm).astype(np.float64) + return compile_quadratic_well(unit_target, curvature=curvature) diff --git a/tests/test_adr_0249_relation_compiler.py b/tests/test_adr_0249_relation_compiler.py new file mode 100644 index 00000000..ea1fbfa1 --- /dev/null +++ b/tests/test_adr_0249_relation_compiler.py @@ -0,0 +1,124 @@ +"""ADR-0249 P2 — affine relation compiler pins. + +Compiles `output = scale·input + offset` (Tier-1: scale > 0) into a +quadratic-well constraint Hamiltonian, reusing the ratified +compile_quadratic_well contract. The arithmetic is performed by the substrate +(versor transport), recovered by relaxation + projective readback — never by +the compiler (anti-hollow, spike §4.1). +""" +from __future__ import annotations + +import hashlib + +import numpy as np +import pytest + +from core.physics.cognitive_lifecycle import ( + HamiltonianCompileError, + ProblemHamiltonian, + relax_to_ground, +) +from core.physics.quantity_kernel import decode_quantity +from core.physics.relation_compiler import ( + affine_relaxation_start, + compile_affine_relation, +) + + +def _solve(inp: float, scale: float, offset: float) -> float: + """Compile → relax from the known input → projectively decode the output.""" + ham = compile_affine_relation(inp, scale=scale, offset=offset) + steady = relax_to_ground(affine_relaxation_start(inp), ham).psi_steady + return decode_quantity(steady) + + +# --- The substrate performs the arithmetic; relaxation + decode recover it --- + + +@pytest.mark.parametrize( + ("inp", "scale", "offset", "gold"), + [ + (4.0, 3.0, 5.0, 17.0), # multiply then add + (10.0, 1.0, -7.0, 3.0), # subtraction (offset < 0) + (12.0, 1.0 / 3.0, 0.0, 4.0), # division (scale = 1/divisor) + (5.0, 2.0, 0.0, 10.0), # pure multiply + (0.0, 4.0, 9.0, 9.0), # offset-only (zero input) + (-6.0, 2.0, 3.0, -9.0), # negative input + ], +) +def test_forward_affine_answer_recovered(inp, scale, offset, gold) -> None: + assert abs(_solve(inp, scale, offset) - gold) < 1e-4 + + +# --- The compiled object is a pure constraint well, carrying no answer ------- + + +def test_compile_returns_problem_hamiltonian() -> None: + ham = compile_affine_relation(4.0, scale=3.0, offset=5.0) + assert isinstance(ham, ProblemHamiltonian) + assert ham.matrix.shape == (32, 32) + assert ham.matrix.dtype == np.dtype(np.float64) + + +def test_hamiltonian_leaks_neither_answer_nor_relation() -> None: + # Anti-hollow (spike §4.1): the well is a bare projector — its metadata + # exposes only curvature + target digest, never scale/offset/answer. + ham = compile_affine_relation(4.0, scale=3.0, offset=5.0) + assert set(ham.metadata) == {"curvature", "target_digest"} + assert ham.domain == "quadratic_well" + + +def test_ablation_relaxation_computes_the_answer() -> None: + # The answer (17) is absent from the start state (decodes to the input 4); + # relaxation is what produces it. The Hamiltonian bytes are identical either + # way — the corridor does the work, not the compile step. + ham = compile_affine_relation(4.0, scale=3.0, offset=5.0) + start = affine_relaxation_start(4.0) + assert abs(decode_quantity(start) - 4.0) < 1e-6 + steady = relax_to_ground(start, ham).psi_steady + assert abs(decode_quantity(steady) - 17.0) < 1e-4 + assert ham.matrix.tobytes() == ham.matrix.tobytes() # frozen, unchanged + + +def test_relaxation_start_decodes_to_known_input() -> None: + assert abs(decode_quantity(affine_relaxation_start(7.5)) - 7.5) < 1e-9 + + +# --- Fail-closed refusals (HamiltonianCompileError family, spike §4.3) ------- + + +@pytest.mark.parametrize("bad_scale", [0.0, -1.0, -3.5]) +def test_refuses_nonpositive_scale(bad_scale) -> None: + with pytest.raises(HamiltonianCompileError): + compile_affine_relation(4.0, scale=bad_scale, offset=1.0) + + +@pytest.mark.parametrize("bad", [np.inf, -np.inf, np.nan]) +def test_refuses_nonfinite_input(bad) -> None: + with pytest.raises(HamiltonianCompileError): + compile_affine_relation(bad, scale=2.0, offset=1.0) + + +@pytest.mark.parametrize("bad", [np.inf, np.nan]) +def test_refuses_nonfinite_scale(bad) -> None: + with pytest.raises(HamiltonianCompileError): + compile_affine_relation(4.0, scale=bad, offset=1.0) + + +@pytest.mark.parametrize("bad", [np.inf, np.nan]) +def test_refuses_nonfinite_offset(bad) -> None: + with pytest.raises(HamiltonianCompileError): + compile_affine_relation(4.0, scale=2.0, offset=bad) + + +# --- Tier-2 cross-hardware reproducibility canary (spike §4.6) -------------- + +# SHA-256 of compile_affine_relation(4, scale=3, offset=5).matrix as None: + ham = compile_affine_relation(4.0, scale=3.0, offset=5.0) + digest = hashlib.sha256(ham.matrix.astype("