Files
sim-numerics/tests/test_three_point.py

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)