Files
sim-numerics/tests/test_slope_gate.py
T

96 lines
4.1 KiB
Python

"""Gates T1-T5 for the slope-stability gate (PREREG_slope_gate_2026-09-21.md).
The progress gate refused on the wrong currency (bracket progress) and charged APS 4449
against 2806. This gate refuses on the SHAPE of the model instead: two consecutive slope
estimates of opposite sign mean the local model is not established, so the evaluation is
spent halving. A SIGN is compared — no threshold, no factor, no constant.
"""
from __future__ import annotations
import inspect
import math
import re
import pytest
from simroot import pure as P
SMOOTH = [ # simple roots, stable slope sign: the gate must be inert
(lambda t: t ** 3 - 2.0 * t - 5.0, 2.0, 3.0),
(lambda t: math.exp(t) - 5.0, 0.0, 3.0),
(lambda t: t ** 2 - 2.0, 0.5, 2.0),
(lambda t: math.cos(t) - t, 0.0, 1.0),
]
# the bracket must straddle zero AND contain a sign change of the SLOPE: f' = 3t^2 - 1
# vanishes at t = +-0.577, so the local slope estimate flips sign inside (-2, 2).
# (An earlier version used sin(8t)e^-t + 0.05 on (0,1), which does not straddle zero at
# all — f(0) = +0.05, f(1) = +0.41 — and the method rightly refused the call.)
WOBBLY = (lambda t: t ** 3 - t + 0.2, -2.0, 2.0)
def test_T1_the_published_path_is_bit_identical_with_the_gate_off():
for f, a, b in SMOOTH:
for guard in ("credit", "project", "safe", "step"):
base = P.pure_root(f, a, b, 1e-12, 300, similarity="ratio",
scale="derived3", guard=guard)
off = P.pure_root(f, a, b, 1e-12, 300, similarity="ratio",
scale="derived3", guard=guard, prestep="none")
assert base == off, (guard, base, off)
def test_T2_the_gate_carries_no_constant():
src = inspect.getsource(P.pure_root)
start = src.index('if prestep == "slope":')
end = src.index('if step is None and prestep == "progress"', start)
code = "\n".join(line.split("#", 1)[0] for line in src[start:end].splitlines())
literals = set(re.findall(r"(?<![\w.])\d+\.\d+(?![\w.])", code))
assert literals <= {"0.0", "0.5"}, f"constants in the gate: {literals}"
assert ">" in code and "!=" in code # a SIGN comparison, not a magnitude test
def test_T3_the_gate_is_inert_where_the_slope_sign_is_stable():
"""This is what the progress gate could not do — it fired on excellent steps."""
for f, a, b in SMOOTH:
off = P.pure_root(f, a, b, 1e-12, 300, similarity="ratio", scale="derived3",
guard="credit")
on = P.pure_root(f, a, b, 1e-12, 300, similarity="ratio", scale="derived3",
guard="credit", prestep="slope")
assert on[1] == off[1], (off, on) # identical evaluation count
def test_T4_the_gate_fires_where_the_slope_sign_flips():
f, a, b = WOBBLY
st = {}
x, n, ok = P.pure_root(f, a, b, 1e-10, 300, similarity="ratio", scale="derived3",
guard="credit", prestep="slope", stats=st)
assert ok and abs(f(x)) < 1e-6
assert st["pre_refused"] >= 1, st
def test_T5_a_refusal_costs_no_extra_evaluation():
f, a, b = WOBBLY
st = {}
_x, n, ok = P.pure_root(f, a, b, 1e-10, 300, similarity="ratio", scale="derived3",
guard="credit", prestep="slope", stats=st)
assert ok
assert st["pre_refused"] <= n - 2, (st, n)
def test_the_mode_needs_a_slope_to_judge():
with pytest.raises(ValueError):
P.pure_root(lambda t: t, -1.0, 1.0, 1e-10, 100, scale="bracket", prestep="slope")
with pytest.raises(ValueError):
P.pure_root(lambda t: t, -1.0, 1.0, 1e-10, 100, prestep="nonsense")
def test_both_gates_remain_available_and_distinct():
""""progress" is kept as a MEASURED NEGATIVE (APS 4449 vs 2806), not deleted."""
f, a, b = WOBBLY
a1 = P.pure_root(f, a, b, 1e-10, 300, similarity="ratio", scale="derived3",
guard="credit", prestep="progress")
a2 = P.pure_root(f, a, b, 1e-10, 300, similarity="ratio", scale="derived3",
guard="credit", prestep="slope")
assert a1[2] and a2[2]
assert a1 != a2 or a1[1] == a2[1] # they may coincide, but both must run