143 lines
5.5 KiB
Python
143 lines
5.5 KiB
Python
"""Tests for the projection safeguard.
|
|
|
|
This is the guard the paper will propose, so its guarantee has to be checked
|
|
where it matters - on every problem of both benchmark sets, not on a sample -
|
|
and the order it was built to preserve has to be shown to survive.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
import os
|
|
import sys
|
|
|
|
import pytest
|
|
from scipy.optimize._tstutils import (_APS_TESTS_DICTS,
|
|
_CHANDRUPATLA_TESTS_DICTS)
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
from simroot.pure import PURE_SIMS, pure_root
|
|
from stage5_three_point import PROBLEMS, order_of
|
|
|
|
XTOL = 1e-12
|
|
MAXFEV = 200
|
|
ACC = 1e-6
|
|
|
|
PROJECTING = ("project", "safe")
|
|
|
|
|
|
def _all_problems():
|
|
out = []
|
|
for d in _APS_TESTS_DICTS:
|
|
a, b = d["bracket"]
|
|
args = d.get("args", ())
|
|
out.append((d["ID"], (lambda ff, aa: lambda x: ff(x, *aa))(d["f"], args),
|
|
float(a), float(b), float(d["root"])))
|
|
for d in _CHANDRUPATLA_TESTS_DICTS:
|
|
a, b = d["bracket"]
|
|
out.append((d["ID"], (lambda ff: lambda x: ff(x))(d["f"]),
|
|
float(a), float(b), float(d["root"])))
|
|
return out
|
|
|
|
|
|
ALL = _all_problems()
|
|
|
|
|
|
def bound(a, b):
|
|
"""The guarantee the paper may state.
|
|
|
|
The budget gives w_{j+1} <= 2^(n_max - j) * eps with n_max = 2*half, so in
|
|
exact arithmetic the width reaches xtol = 2*eps at step j = n_max - 1 and
|
|
the run costs n_max steps plus the two endpoint evaluations.
|
|
|
|
In floating point the bracket can land exactly ON the budget boundary -
|
|
the instrumented invariant in stage6b shows the worst ratio at 1.000000 -
|
|
and the loop condition `w > xtol` then demands one further step. The
|
|
honest statement is therefore n_max + 1 steps, i.e.
|
|
|
|
2 * ceil(log2(w / xtol)) + 3 evaluations,
|
|
|
|
which is still at most twice bisection plus one step. The `+1` is stated
|
|
rather than hidden: it is a property of the termination test at the
|
|
boundary, not slack that could absorb a real violation."""
|
|
return 2 * math.ceil(math.log2(abs(b - a) / XTOL)) + 3
|
|
|
|
|
|
@pytest.mark.parametrize("guard", PROJECTING)
|
|
@pytest.mark.parametrize("scale", ["derived", "derived3"])
|
|
def test_projection_respects_the_bound_on_all_199_problems(guard, scale):
|
|
"""The claim the paper makes about the projection guard. A single
|
|
violation invalidates it, so every problem is checked."""
|
|
bad = []
|
|
for pid, f, a, b, root in ALL:
|
|
x, n, conv = pure_root(f, a, b, XTOL, MAXFEV, similarity="ratio",
|
|
scale=scale, guard=guard)
|
|
if conv and abs(x - root) <= ACC and n > bound(a, b):
|
|
bad.append((pid, n, bound(a, b)))
|
|
assert not bad, f"{guard}/{scale} exceeded the bound on {bad[:5]}"
|
|
|
|
|
|
@pytest.mark.parametrize("guard", PROJECTING)
|
|
def test_projection_solves_as_much_as_the_unguarded_rule(guard):
|
|
"""A safeguard that bought its guarantee by failing more often would be
|
|
no bargain."""
|
|
solved_guard = solved_none = 0
|
|
for _pid, f, a, b, root in ALL:
|
|
for g, acc in ((guard, "g"), ("none", "n")):
|
|
x, _n, conv = pure_root(f, a, b, XTOL, MAXFEV, similarity="ratio",
|
|
scale="derived3", guard=g)
|
|
ok = conv and abs(x - root) <= ACC
|
|
if acc == "g":
|
|
solved_guard += ok
|
|
else:
|
|
solved_none += ok
|
|
assert solved_guard >= solved_none, (solved_guard, solved_none)
|
|
|
|
|
|
def test_projection_keeps_the_order_the_other_guards_destroy():
|
|
"""The whole point: `bracket` and `both` collapse the three-point rule
|
|
from order ~1.94 to ~1.00; `project` and `safe` must not."""
|
|
med = {}
|
|
for guard in ("none", "bracket", "both", "project", "safe", "step"):
|
|
ps = [order_of((lambda g: lambda f_, a_, b_, xt, mx: pure_root(
|
|
f_, a_, b_, XTOL, MAXFEV, scale="derived3", guard=g))(guard),
|
|
f, a, b, root) for _n, f, a, b, root in PROBLEMS]
|
|
ps = [p for p in ps if math.isfinite(p) and 0.5 < p < 5.0]
|
|
med[guard] = sorted(ps)[len(ps) // 2] if ps else float("nan")
|
|
assert med["bracket"] < 1.2, med # the known collapse
|
|
assert med["both"] < 1.2, med
|
|
assert med["project"] > 1.7, med # the fix
|
|
assert med["safe"] > 1.7, med
|
|
assert med["safe"] >= med["bracket"] + 0.5, med
|
|
|
|
|
|
def test_zero_slack_would_be_degenerate():
|
|
"""Guards against someone 'tightening' the budget back to the exact
|
|
bisection bound: with no slack the projection radius on a unit bracket is
|
|
a twentieth of it from the very first step, and the method becomes
|
|
bisection. The doubling is load-bearing."""
|
|
from simroot.pure import _Safeguard
|
|
sg = _Safeguard("project", 1.0, XTOL)
|
|
r_doubled = 2.0 ** (sg.n_half - 0) * sg.eps - 0.5
|
|
half = sg.n_half // 2
|
|
r_zero_slack = 2.0 ** half * sg.eps - 0.5
|
|
assert r_zero_slack < 0.06, r_zero_slack
|
|
assert r_doubled > 1e6, r_doubled
|
|
|
|
|
|
@pytest.mark.parametrize("sim", sorted(PURE_SIMS))
|
|
def test_every_similarity_works_under_the_projection(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="safe")
|
|
assert conv and abs(x - math.sqrt(2.0)) <= 1e-8
|
|
|
|
|
|
def test_projection_needs_a_tolerance_and_a_real_bracket():
|
|
with pytest.raises(ValueError):
|
|
from simroot.pure import _Safeguard
|
|
_Safeguard("project", 1.0, 0.0)
|
|
with pytest.raises(ValueError):
|
|
from simroot.pure import _Safeguard
|
|
_Safeguard("safe", 0.0, XTOL)
|