247 lines
9.9 KiB
Python
247 lines
9.9 KiB
Python
"""The credit safeguard: the budget earned instead of scheduled.
|
|
|
|
The scheduled projection needs a multiplier, and the argument for 2 was
|
|
circular. This one banks the surplus a step delivers beyond a halving and
|
|
lends only what has been banked, so the only constant left is the endowment,
|
|
and one bit is the smallest integer for which the rule is not degenerate.
|
|
|
|
What must hold, and is asserted here: the stronger guarantee is real, the
|
|
endowment is minimal, and the cost of the stronger guarantee is stated rather
|
|
than hidden.
|
|
"""
|
|
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 _Safeguard, pure_root
|
|
|
|
XTOL, MAXFEV, ACC = 1e-12, 200, 1e-6
|
|
|
|
|
|
def problems():
|
|
out = []
|
|
for d in _APS_TESTS_DICTS:
|
|
a, b = d["bracket"]
|
|
args = d.get("args", ())
|
|
out.append(((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(((lambda ff: lambda x: ff(x))(d["f"]),
|
|
float(a), float(b), float(d["root"])))
|
|
return out
|
|
|
|
|
|
ALL = problems()
|
|
|
|
|
|
def sweep(guard):
|
|
total = solved = over = 0
|
|
for f, a, b, root in ALL:
|
|
try:
|
|
x, n, conv = pure_root(f, a, b, XTOL, MAXFEV, similarity="ratio",
|
|
scale="derived3", guard=guard)
|
|
except Exception:
|
|
x, n, conv = float("nan"), MAXFEV, False
|
|
ok = bool(conv and math.isfinite(x) and abs(x - root) <= ACC)
|
|
total += n if ok else MAXFEV
|
|
solved += ok
|
|
if ok and n > math.ceil(math.log2(abs(b - a) / XTOL)) + 4:
|
|
over += 1
|
|
return total, solved, over
|
|
|
|
|
|
def test_the_credit_guard_meets_the_near_minimal_bound_everywhere():
|
|
"""bisection + one step, on all 199 problems. The scheduled projection
|
|
is allowed twice that and uses it on 19 of them."""
|
|
_t, solved, over = sweep("credit")
|
|
assert solved == 193, solved
|
|
assert over == 0, over
|
|
|
|
|
|
def test_the_scheduled_projection_really_does_use_its_larger_budget():
|
|
"""If this ever drops to zero the two guards have converged and the
|
|
multiplier stopped mattering - which would be worth knowing."""
|
|
_t, solved, over = sweep("project")
|
|
assert solved == 193, solved
|
|
assert over > 10, over
|
|
|
|
|
|
def test_the_stronger_guarantee_costs_and_the_cost_is_stated():
|
|
"""No free lunch: the paper must not claim the credit rule is both
|
|
cheaper and safer."""
|
|
t_credit, _s, _o = sweep("credit")
|
|
t_sched, _s2, _o2 = sweep("project")
|
|
assert t_credit > t_sched, (t_credit, t_sched)
|
|
assert (t_credit - t_sched) / t_sched < 0.15, (t_credit, t_sched)
|
|
|
|
|
|
def test_a_zero_endowment_is_degenerate():
|
|
"""Why one bit is the minimum: with no endowment the allowance on the
|
|
first step is exactly half the bracket, which forces the midpoint."""
|
|
sg = _Safeguard("credit", 1.0, XTOL)
|
|
assert sg.credit == 1.0
|
|
sg.credit = 0.0
|
|
assert sg.project_credit(0.9, 0.0, 1.0) == pytest.approx(0.5)
|
|
# with the endowment the same step is left alone
|
|
sg.credit = 1.0
|
|
assert sg.project_credit(0.9, 0.0, 1.0) == pytest.approx(0.9)
|
|
|
|
|
|
def test_credit_never_goes_negative_and_banks_surplus():
|
|
sg = _Safeguard("credit", 1.0, XTOL)
|
|
sg.credit = 0.0
|
|
sg.note_shrink(1.0, 1.0) # no shrink at all
|
|
assert sg.credit == 0.0 # clamped, never a debt
|
|
sg.note_shrink(1.0, 0.125) # three bits of shrink
|
|
assert sg.credit == pytest.approx(2.0)
|
|
|
|
|
|
@pytest.mark.parametrize("c0,steps_over", [(0.25, 1), (0.5, 1), (1.0, 1),
|
|
(2.0, 2), (3.0, 3)])
|
|
def test_the_guarantee_is_bisection_plus_ceil_of_the_endowment(c0, steps_over):
|
|
"""The relation that makes the endowment determined rather than chosen."""
|
|
worst = 0
|
|
for f, a, b, root in ALL:
|
|
try:
|
|
x, n, conv = pure_root(f, a, b, XTOL, MAXFEV, similarity="ratio",
|
|
scale="derived3", guard="credit",
|
|
endowment=c0)
|
|
except Exception:
|
|
continue
|
|
if conv and math.isfinite(x) and abs(x - root) <= ACC:
|
|
half = math.ceil(math.log2(abs(b - a) / XTOL))
|
|
worst = max(worst, (n - 2) - half)
|
|
assert worst == steps_over, (c0, worst, steps_over)
|
|
|
|
|
|
def test_one_bit_is_the_cheapest_endowment_that_promises_one_step():
|
|
"""Why C0 = 1 is forced and not picked: every C0 in (0,1] gives the same
|
|
one-step guarantee, and 1 is the most permissive of them, hence the
|
|
cheapest. If a smaller endowment ever became cheaper, the argument that
|
|
the constant is determined would collapse."""
|
|
def cost(c0):
|
|
t = 0
|
|
for f, a, b, root in ALL:
|
|
try:
|
|
x, n, conv = pure_root(f, a, b, XTOL, MAXFEV,
|
|
similarity="ratio", scale="derived3",
|
|
guard="credit", endowment=c0)
|
|
except Exception:
|
|
x, n, conv = float("nan"), MAXFEV, False
|
|
ok = conv and math.isfinite(x) and abs(x - root) <= ACC
|
|
t += n if ok else MAXFEV
|
|
return t
|
|
|
|
c_one = cost(1.0)
|
|
for smaller in (0.1, 0.25, 0.5):
|
|
assert cost(smaller) > c_one, smaller
|
|
|
|
|
|
def test_the_scheduled_multiplier_has_no_such_determination():
|
|
"""The contrast that decides which safeguard the paper should propose.
|
|
The multiplicative family has no smallest non-trivial member: 1 is
|
|
degenerate and everything above it is admissible with a different cost, so
|
|
the multiplier is genuinely free."""
|
|
def cost(fac):
|
|
t = 0
|
|
for f, a, b, root in ALL:
|
|
try:
|
|
x, n, conv = pure_root(f, a, b, XTOL, MAXFEV,
|
|
similarity="ratio", scale="derived3",
|
|
guard="project", budget_factor=fac)
|
|
except Exception:
|
|
x, n, conv = float("nan"), MAXFEV, False
|
|
ok = conv and math.isfinite(x) and abs(x - root) <= ACC
|
|
t += n if ok else MAXFEV
|
|
return t
|
|
|
|
degenerate = cost(1.0)
|
|
assert degenerate > cost(2.0) # 1 is degenerate
|
|
assert cost(1.25) < cost(2.0) # and 2 is not the best either
|
|
|
|
|
|
def test_the_endowment_cannot_be_earned_and_so_cannot_be_computed():
|
|
"""Why C0 is a licence rather than a quantity to be derived from the run.
|
|
|
|
Credit is earned only by a step that beats a halving, and such a step is
|
|
permitted only when credit already exists. Starting from zero the
|
|
mechanism is locked: the allowance is exactly half the bracket, which
|
|
forces the midpoint, which delivers exactly one bit, which banks nothing.
|
|
The method is then bisection and stays bisection - so no amount of running
|
|
can produce the evidence that would justify a larger endowment."""
|
|
sg = _Safeguard("credit", 1.0, XTOL)
|
|
sg.credit = 0.0
|
|
probe = None
|
|
for _ in range(30):
|
|
probe = sg.project_credit(0.99, 0.0, 1.0) # a bold step is proposed
|
|
assert probe == pytest.approx(0.5) # ...and always refused
|
|
sg.note_shrink(1.0, max(probe, 1.0 - probe))
|
|
assert sg.credit == 0.0 # nothing is ever banked
|
|
|
|
# end to end: zero endowment reproduces bisection's cost
|
|
from simroot.baselines import bisection
|
|
t_zero = t_bis = 0
|
|
for f, a, b, root in ALL:
|
|
for meth, acc in ((lambda: pure_root(f, a, b, XTOL, MAXFEV,
|
|
similarity="ratio",
|
|
scale="derived3", guard="credit",
|
|
endowment=0.0), "z"),
|
|
(lambda: bisection(f, a, b, XTOL, MAXFEV), "b")):
|
|
try:
|
|
x, n, conv = meth()
|
|
except Exception:
|
|
x, n, conv = float("nan"), MAXFEV, False
|
|
ok = conv and math.isfinite(x) and abs(x - root) <= ACC
|
|
if acc == "z":
|
|
t_zero += n if ok else MAXFEV
|
|
else:
|
|
t_bis += n if ok else MAXFEV
|
|
assert abs(t_zero - t_bis) / t_bis < 0.01, (t_zero, t_bis)
|
|
|
|
|
|
def test_a_large_credit_cannot_overflow_the_exponent():
|
|
"""Found by the algorithm audit. The allowance is w * 2^(C-1) and C
|
|
accumulates the log of every shrink without bound, so the exponentiation
|
|
overflowed a double at C ~ 1025 and the OverflowError escaped - only the
|
|
budget exception is caught in pure_root. It is reachable: sixty
|
|
nine-decade steps reach C = 1012, and a bracket of width 1e300 with a
|
|
tolerance of 1e-8 has about 1023 bits of shrink available."""
|
|
sg = _Safeguard("credit", 1.0, XTOL)
|
|
for c in (1.0, 100.0, 1024.0, 1100.0, 1e6):
|
|
sg.credit = c
|
|
assert sg.project_credit(0.9, 0.0, 1.0) == 0.9, c
|
|
# and the accumulation that gets there must not raise either
|
|
sg.credit = 1.0
|
|
w = 1.0
|
|
for _ in range(60):
|
|
sg.note_shrink(w, w * 1e-9)
|
|
w *= 1e-9
|
|
assert sg.credit > 1000.0
|
|
assert sg.project_credit(0.9, 0.0, 1.0) == 0.9
|
|
|
|
|
|
def test_a_very_wide_bracket_is_solved_end_to_end():
|
|
"""The same defect, reached through the public entry point."""
|
|
x, n, conv = pure_root(lambda t: t - 1.0, -1e150, 1e150, 1e-12, 4000,
|
|
similarity="ratio", scale="derived3",
|
|
guard="credit")
|
|
assert conv and abs(x - 1.0) < 1e-6, (x, n, conv)
|
|
|
|
|
|
def test_credit_needs_no_tolerance_at_all():
|
|
"""Unlike the scheduled version it reads only widths, so there is no
|
|
n_max, no eps, and nothing fixed in advance from the initial bracket."""
|
|
f = lambda t: t * t - 2.0
|
|
x, _n, conv = pure_root(f, 0.0, 2.0, XTOL, MAXFEV, similarity="ratio",
|
|
scale="derived3", guard="credit")
|
|
assert conv and abs(x - math.sqrt(2.0)) <= 1e-8
|