125 lines
4.8 KiB
Python
125 lines
4.8 KiB
Python
"""Tests for the three-point scale.
|
|
|
|
The claim being pinned: a divided difference is a COMPUTATION, not a fitted
|
|
coefficient, so `derived3` introduces no parameter; and it must actually lift
|
|
the order, otherwise the stage-4 explanation of the endgame deficit is wrong.
|
|
"""
|
|
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.pure import GUARDS, PURE_SIMS, _slope3, pure_root
|
|
from stage5_three_point import PROBLEMS, order_of
|
|
|
|
XTOL = 1e-12
|
|
MAXFEV = 300
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# the divided difference itself
|
|
# --------------------------------------------------------------------------
|
|
@pytest.mark.parametrize("q", [
|
|
lambda t: 3.0 * t * t - 2.0 * t + 5.0,
|
|
lambda t: -1.5 * t * t + 0.25 * t - 7.0,
|
|
lambda t: t * t,
|
|
])
|
|
def test_slope3_is_exact_on_a_quadratic(q):
|
|
"""The derivative of the parabola through three points of a quadratic is
|
|
that quadratic's derivative, exactly. If this drifts, the order gain is
|
|
not what it is claimed to be."""
|
|
h = 1e-3
|
|
for x2 in (-2.0, 0.3, 4.0):
|
|
x1, x0 = x2 - h, x2 - 2.0 * h
|
|
s = _slope3(x2, q(x2), x1, q(x1), x0, q(x0))
|
|
exact = (q(x2 + 1e-7) - q(x2 - 1e-7)) / 2e-7
|
|
assert s == pytest.approx(exact, rel=1e-6), (x2, s, exact)
|
|
|
|
|
|
def test_slope3_is_exact_on_a_line():
|
|
f = lambda t: 4.0 * t - 1.0
|
|
s = _slope3(2.0, f(2.0), 1.0, f(1.0), 0.0, f(0.0))
|
|
assert s == pytest.approx(4.0)
|
|
|
|
|
|
def test_slope3_falls_back_to_two_points_when_the_third_is_missing():
|
|
f = lambda t: t * t
|
|
two = (f(2.0) - f(1.0)) / (2.0 - 1.0)
|
|
assert _slope3(2.0, f(2.0), 1.0, f(1.0), None, None) == pytest.approx(two)
|
|
# a repeated node must not divide by zero
|
|
assert math.isfinite(_slope3(2.0, f(2.0), 1.0, f(1.0), 1.0, f(1.0)))
|
|
assert math.isfinite(_slope3(2.0, f(2.0), 1.0, f(1.0), 2.0, f(2.0)))
|
|
|
|
|
|
def test_slope3_never_returns_nan_or_inf_on_degenerate_input():
|
|
for args in [(1.0, 0.0, 1.0, 0.0, 1.0, 0.0),
|
|
(1.0, 1e308, 0.5, -1e308, 0.0, 1e308),
|
|
(0.0, 0.0, 0.0, 0.0, 0.0, 0.0)]:
|
|
s = _slope3(*args)
|
|
assert math.isfinite(s) or s == 0.0
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# the method built on it
|
|
# --------------------------------------------------------------------------
|
|
@pytest.mark.parametrize("guard", GUARDS)
|
|
@pytest.mark.parametrize("prob", PROBLEMS, ids=[p[0] for p in PROBLEMS])
|
|
def test_derived3_converges(prob, guard):
|
|
_, f, a, b, root = prob
|
|
x, n, conv = pure_root(f, a, b, XTOL, MAXFEV, scale="derived3", guard=guard)
|
|
assert conv, f"derived3.{guard} did not converge"
|
|
assert abs(x - root) <= 1e-8
|
|
assert n <= MAXFEV
|
|
|
|
|
|
@pytest.mark.parametrize("sim", sorted(PURE_SIMS))
|
|
def test_derived3_works_for_every_similarity(sim):
|
|
f = lambda t: t * t - 2.0
|
|
x, n, conv = pure_root(f, 0.0, 2.0, XTOL, MAXFEV, similarity=sim,
|
|
scale="derived3", guard="both")
|
|
assert conv and abs(x - math.sqrt(2.0)) <= 1e-8
|
|
|
|
|
|
def test_three_points_raise_the_order_above_two_points():
|
|
"""The whole point of the change. Measured on the problems where an
|
|
order can be estimated at all."""
|
|
two, three = [], []
|
|
for _n, f, a, b, root in PROBLEMS:
|
|
p2 = order_of(lambda f_, a_, b_, xt, mx: pure_root(
|
|
f_, a_, b_, XTOL, MAXFEV, scale="derived", guard="step"),
|
|
f, a, b, root)
|
|
p3 = order_of(lambda f_, a_, b_, xt, mx: pure_root(
|
|
f_, a_, b_, XTOL, MAXFEV, scale="derived3", guard="step"),
|
|
f, a, b, root)
|
|
if math.isfinite(p2) and math.isfinite(p3):
|
|
two.append(p2)
|
|
three.append(p3)
|
|
assert len(two) >= 5
|
|
m2 = sorted(two)[len(two) // 2]
|
|
m3 = sorted(three)[len(three) // 2]
|
|
assert 1.4 < m2 < 1.8, m2 # secant
|
|
assert m3 > m2 + 0.15, (m2, m3) # a real lift, not noise
|
|
|
|
|
|
def test_the_sound_guard_still_costs_the_order():
|
|
"""Honest pin of the tension found in stage 5: `both` carries the 2x
|
|
bisection bound and, on a method that shrinks the ERROR faster than the
|
|
BRACKET, that bound forces a bisection every other step and flattens the
|
|
order. This is a property of the guarantee, not a defect - but if
|
|
someone later claims high order AND the 2x bound from the same
|
|
configuration, this test says otherwise."""
|
|
f = lambda t: t * t - 2.0
|
|
root = math.sqrt(2.0)
|
|
p_step = order_of(lambda f_, a_, b_, xt, mx: pure_root(
|
|
f_, a_, b_, XTOL, MAXFEV, scale="derived3", guard="step"),
|
|
f, 0.0, 2.0, root)
|
|
p_both = order_of(lambda f_, a_, b_, xt, mx: pure_root(
|
|
f_, a_, b_, XTOL, MAXFEV, scale="derived3", guard="both"),
|
|
f, 0.0, 2.0, root)
|
|
assert p_step > p_both, (p_step, p_both)
|