Also fixes a real regression from cc2c499: the derived3_cap branch was inserted in front of the derived3_m note and swallowed its division, so derived3_m silently became derived3 (2806 against 2806) and a refutation recorded in the paper stopped being reproducible. The full pin caught it; a cheap pin now guards it too. And \label{sec:method} never existed while the table on page 7 referenced it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
128 lines
5.6 KiB
Python
128 lines
5.6 KiB
Python
"""Pins for PREREG 2026-09-21 slot 1: the similarity function read BY PLACE.
|
|
|
|
Every claim the mode rests on is pinned here, including the ones that make it falsifiable:
|
|
the default must stay bit-for-bit published, the far reading must BE false position, and
|
|
the order exponent must invert the power law exactly.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
|
|
import pytest
|
|
|
|
from simroot.pure import PURE_SIMS, _local_order, _read_z, pure_root
|
|
|
|
PROBS = (
|
|
(lambda t: t ** 3 - 2.0 * t - 5.0, 1.0, 4.0),
|
|
(lambda t: math.exp(t) - 2.0, 0.0, 3.0),
|
|
(lambda t: (t - 1.0) ** 3, 0.0, 10.0),
|
|
(lambda t: math.sin(t) - 0.5, 0.0, 1.5),
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("f,a,b", PROBS)
|
|
@pytest.mark.parametrize("guard", ("safe", "credit", "project"))
|
|
def test_default_is_bit_for_bit_the_published_method(f, a, b, guard):
|
|
"""R1: the new parameter must not touch the published grid when left alone."""
|
|
ref = pure_root(f, a, b, 1e-12, 200, similarity="ratio", scale="derived3", guard=guard)
|
|
new = pure_root(f, a, b, 1e-12, 200, similarity="ratio", scale="derived3", guard=guard,
|
|
exponent="none")
|
|
assert ref == new
|
|
|
|
|
|
def test_far_reading_with_rational_is_false_position():
|
|
"""R2: Proposition 4 must hold in the CODE, not only in the paper.
|
|
|
|
The first similarity step from x with S = 1/(1+z), z = |f(x)|/|f(far)| must land on the
|
|
secant root of the pair (x, far). If it does not, the (far, none) cell is not the
|
|
control the pre-registration claims it is.
|
|
"""
|
|
f = lambda t: t ** 3 - 2.0 * t - 5.0 # noqa: E731
|
|
a, b = 1.0, 4.0
|
|
x = 0.5 * (a + b)
|
|
# the far end must be the one whose sign differs, otherwise there is no secant root
|
|
# between the pair at all; on this bracket that is a, not b
|
|
far, fx = a, f(x)
|
|
ffar = f(far)
|
|
assert fx * ffar < 0.0
|
|
z = abs(fx) / abs(ffar)
|
|
step = (1.0 - PURE_SIMS["rational"](z)) * (far - x)
|
|
secant = x - fx * (far - x) / (ffar - fx)
|
|
assert step == pytest.approx(secant - x, rel=1e-12)
|
|
|
|
|
|
def test_order_exponent_inverts_the_power_law_exactly():
|
|
"""R3: the whole derivation is z^(1/m) = e/E. Pinned as an identity."""
|
|
for m in (1.0, 2.0, 3.0, 7.0):
|
|
for e, big in ((0.1, 2.0), (1.0, 5.0), (1e-3, 1.0)):
|
|
z = (e / big) ** m
|
|
got = _read_z(z, 1.0, "order", m)
|
|
assert got == pytest.approx(e / big, rel=1e-9)
|
|
|
|
|
|
def test_order_exponent_is_inert_at_order_one():
|
|
"""A simple root must leave the reading untouched, or the endgame is not preserved."""
|
|
for z in (1e-9, 0.3, 1.0, 12.0):
|
|
assert _read_z(z, 1.0, "order", 1.0) == z
|
|
assert _read_z(z, 1.0, "order", None) == z
|
|
assert _read_z(z, 1.0, "none", 5.0) == z
|
|
|
|
|
|
def test_local_order_reads_a_cube_low_by_a_fixed_bias():
|
|
"""MEASURED, and pinned as measured: on a pure cube the three-point estimator returns
|
|
2.830, not 3. The parabola through three points carries only the second difference, so
|
|
a cubic's order is under-read by 5.7% even in the ideal noiseless case. The bias is
|
|
towards 1, i.e. towards the uncorrected reading, which is the safe direction — but it
|
|
is a bias and the paper must say so rather than claim the estimator is exact."""
|
|
f = lambda t: t ** 3 # noqa: E731
|
|
xs = (0.30, 0.31, 0.32)
|
|
m = _local_order(xs[2], f(xs[2]), xs[1], f(xs[1]), xs[0], f(xs[0]))
|
|
assert m == pytest.approx(2.83, rel=0.02)
|
|
assert 1.0 < m < 3.0
|
|
|
|
|
|
def test_step_fraction_can_never_leave_the_bracket():
|
|
"""L5: under S = 1/(1+z) the fraction z/(1+z) is at most 1, so the step never passes
|
|
the far end. At z >= 1/eps it SATURATES at exactly 1 in floating point — the step
|
|
lands ON the end, which the loop already turns into a bisection because x_new must be
|
|
strictly inside. Pinned so the saturation is a documented boundary, not a surprise."""
|
|
for z in (0.0, 1e-12, 1.0, 1e6):
|
|
assert 0.0 <= 1.0 - PURE_SIMS["rational"](z) < 1.0
|
|
assert 1.0 - PURE_SIMS["rational"](1e300) == 1.0
|
|
|
|
|
|
@pytest.mark.parametrize("f,a,b", PROBS)
|
|
def test_far_and_order_still_solve_and_stay_in_the_bracket(f, a, b):
|
|
ok_any = False
|
|
for ex in ("none", "order"):
|
|
x, n, conv = pure_root(f, a, b, 1e-10, 300, similarity="rational", scale="far",
|
|
guard="safe", exponent=ex)
|
|
assert min(a, b) <= x <= max(a, b)
|
|
assert n <= 300
|
|
ok_any = ok_any or conv
|
|
assert ok_any
|
|
|
|
|
|
def test_derived3_m_actually_divides_by_the_order():
|
|
"""REGRESSION, 2026-09-21. The derived3_cap branch was inserted in front of the
|
|
derived3_m note and swallowed its division, so derived3_m became a silent alias of
|
|
derived3 (2806 against 2806) and a refutation recorded in the paper stopped being
|
|
reproducible. One cheap problem where the order is genuinely above 1 is enough to
|
|
catch it, so the guard does not depend on the full 154-problem pin."""
|
|
f = lambda t: (t - 1.0) ** 3 # noqa: E731
|
|
plain = pure_root(f, 0.0, 10.0, 1e-10, 300, similarity="ratio", scale="derived3",
|
|
guard="project")
|
|
with_m = pure_root(f, 0.0, 10.0, 1e-10, 300, similarity="ratio", scale="derived3_m",
|
|
guard="project")
|
|
assert plain != with_m
|
|
|
|
|
|
def test_bad_combinations_are_refused_not_silently_reinterpreted():
|
|
with pytest.raises(ValueError):
|
|
pure_root(lambda t: t, 0.0, 1.0, 1e-9, 50, exponent="cube")
|
|
with pytest.raises(ValueError):
|
|
# two-point scale has no second difference to read the order from
|
|
pure_root(lambda t: t, 0.0, 1.0, 1e-9, 50, scale="derived", exponent="order")
|
|
with pytest.raises(ValueError):
|
|
pure_root(lambda t: t, 0.0, 1.0, 1e-9, 50, scale="nonesuch")
|