151 lines
5.7 KiB
Python
151 lines
5.7 KiB
Python
"""Tests for the ANALYSIS code in stage4_why.
|
|
|
|
The conclusions about why the method wins and loses rest entirely on three
|
|
pieces of machinery here - the phase boundary, the bracket replay, and the
|
|
observed order - plus the `stats` instrumentation added to brent and
|
|
pure_root. None of it was covered, which made the explanation the least
|
|
verified part of the project.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
import os
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
from simroot.baselines import bisection, brent
|
|
from simroot.pure import GUARDS, pure_root
|
|
from stage4_why import (observed_order, ours, phase_boundary, phases,
|
|
replay_bracket, traced)
|
|
|
|
XTOL = 1e-12
|
|
MAXFEV = 200
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# the phase boundary
|
|
# --------------------------------------------------------------------------
|
|
@pytest.mark.parametrize("width", [1.0, 5.0, 41.0, 1e4, 1e8, 1e12])
|
|
def test_phase_boundary_splits_bisection_evenly(width):
|
|
"""The boundary is defined as the log-midpoint of the journey, so
|
|
bisection - which halves at a constant rate - must spend the same number
|
|
of evaluations either side of it, on a bracket of any width. If this
|
|
fails, the approach/endgame table means nothing."""
|
|
a, b = 0.0, width
|
|
root = width * 0.3 # away from the midpoint, so nothing is by luck
|
|
f = lambda x: x - root
|
|
app, end, tot = phases(bisection, f, a, b, root)
|
|
assert app + end == tot
|
|
assert abs(app - end) <= 4, f"width {width}: {app} vs {end}"
|
|
|
|
|
|
def test_phase_boundary_is_the_geometric_mean_of_width_and_attainable_tol():
|
|
# near the origin the attainable tolerance is xtol itself
|
|
assert phase_boundary(1e12, 0.0, 1e-12) == pytest.approx(1.0)
|
|
assert phase_boundary(4.0, 0.0, 1e-12) == pytest.approx(2e-6)
|
|
# far from it, floating-point spacing is the real floor
|
|
assert phase_boundary(1e12, 1e12, 1e-12) > 1e3
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# the bracket replay
|
|
# --------------------------------------------------------------------------
|
|
def test_replay_scores_bisection_at_exactly_one_half():
|
|
"""End-to-end check of the reconstruction: every bisection probe sits at
|
|
the midpoint and removes exactly half, by construction. Any error in the
|
|
replay shows up here immediately."""
|
|
f = lambda x: x * x - 2.0
|
|
g, xs = traced(f)
|
|
bisection(g, 0.0, 2.0, 1e-9, MAXFEV)
|
|
pos, shrinks, skipped = replay_bracket(f, 0.0, 2.0, xs[2:])
|
|
assert skipped == 0
|
|
assert pos, "no probes replayed"
|
|
for t in pos:
|
|
assert t == pytest.approx(0.5, abs=1e-12)
|
|
for s in shrinks:
|
|
assert s == pytest.approx(0.5, abs=1e-12)
|
|
|
|
|
|
def test_replay_counts_probes_outside_the_bracket():
|
|
"""A probe outside the proven bracket must be counted as skipped, not
|
|
silently dropped - dropping them flatters whoever probes outside."""
|
|
f = lambda x: x - 1.0
|
|
# 5.0 is outside [0, 2]; 1.5 and 0.5 are inside
|
|
pos, shrinks, skipped = replay_bracket(f, 0.0, 2.0, [1.5, 5.0, 0.5])
|
|
assert skipped == 1
|
|
assert len(pos) == len(shrinks) == 2
|
|
|
|
|
|
def test_replay_shrinks_are_fractions():
|
|
f = lambda x: math.cos(x) - x
|
|
g, xs = traced(f)
|
|
ours(g, 0.0, 1.0, XTOL, MAXFEV)
|
|
_pos, shrinks, _sk = replay_bracket(f, 0.0, 1.0, xs[2:])
|
|
assert shrinks
|
|
for s in shrinks:
|
|
assert 0.0 <= s <= 1.0
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# the observed order
|
|
# --------------------------------------------------------------------------
|
|
def test_observed_order_recovers_the_known_orders():
|
|
"""The whole explanation of the endgame loss is the measured order. It
|
|
must reproduce the textbook values for methods whose order is known."""
|
|
f = lambda x: x * x - 2.0
|
|
root = math.sqrt(2.0)
|
|
p_ours = observed_order(ours, f, 0.0, 2.0, root)
|
|
p_brent = observed_order(brent, f, 0.0, 2.0, root)
|
|
p_bis = observed_order(bisection, f, 0.0, 2.0, root)
|
|
assert 1.3 < p_ours < 1.9, p_ours # secant, 1.618
|
|
assert p_brent > p_ours # inverse quadratic, 1.839
|
|
assert p_bis < 1.3, p_bis # linear
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# the instrumentation must not perturb what it measures
|
|
# --------------------------------------------------------------------------
|
|
PROBS = [
|
|
(lambda x: x * x - 2.0, 0.0, 2.0),
|
|
(lambda x: math.exp(-x) * (x - 1.0), -1.0, 40.0),
|
|
(lambda x: x ** 10 - 1.0, 0.0, 1.3),
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize("f,a,b", PROBS)
|
|
def test_brent_stats_do_not_change_brent(f, a, b):
|
|
plain = brent(f, a, b, XTOL, MAXFEV)
|
|
st = {}
|
|
withstats = brent(f, a, b, XTOL, MAXFEV, stats=st)
|
|
assert plain == withstats
|
|
assert 0 <= st["bisect"] <= plain[1]
|
|
|
|
|
|
@pytest.mark.parametrize("guard", GUARDS)
|
|
@pytest.mark.parametrize("f,a,b", PROBS)
|
|
def test_pure_stats_do_not_change_pure_root(f, a, b, guard):
|
|
plain = pure_root(f, a, b, XTOL, MAXFEV, guard=guard)
|
|
st = {}
|
|
withstats = pure_root(f, a, b, XTOL, MAXFEV, guard=guard, stats=st)
|
|
assert plain == withstats
|
|
assert 0 <= st["forced"] <= plain[1]
|
|
|
|
|
|
@pytest.mark.parametrize("f,a,b", PROBS)
|
|
def test_an_unguarded_run_forces_nothing(f, a, b):
|
|
st = {}
|
|
pure_root(f, a, b, XTOL, MAXFEV, guard="none", stats=st)
|
|
assert st["forced"] == 0
|
|
|
|
|
|
def test_the_bracket_guard_actually_fires_where_it_is_needed():
|
|
"""The flat problem is the one that defeats every interpolation-only
|
|
rule; the safeguard must be doing visible work there."""
|
|
f = lambda x: math.exp(-x) * (x - 1.0)
|
|
st = {}
|
|
pure_root(f, -1.0, 40.0, XTOL, MAXFEV, guard="bracket", stats=st)
|
|
assert st["forced"] > 0
|