"""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)