baseline: the published sim-numerics artifact as it stands (490 tests, verify_article.py checks all 72 paper numbers against sweep_full.json)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
"""Tests for the ANALYSIS code in stage4_why.
|
||||
|
||||
The conclusions about why the method wins and loses rest entirely on three
|
||||
pieces of machinery here - the phase boundary, the bracket replay, and the
|
||||
observed order - plus the `stats` instrumentation added to brent and
|
||||
pure_root. None of it was covered, which made the explanation the least
|
||||
verified part of the project.
|
||||
"""
|
||||
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.baselines import bisection, brent
|
||||
from simroot.pure import GUARDS, pure_root
|
||||
from stage4_why import (observed_order, ours, phase_boundary, phases,
|
||||
replay_bracket, traced)
|
||||
|
||||
XTOL = 1e-12
|
||||
MAXFEV = 200
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# the phase boundary
|
||||
# --------------------------------------------------------------------------
|
||||
@pytest.mark.parametrize("width", [1.0, 5.0, 41.0, 1e4, 1e8, 1e12])
|
||||
def test_phase_boundary_splits_bisection_evenly(width):
|
||||
"""The boundary is defined as the log-midpoint of the journey, so
|
||||
bisection - which halves at a constant rate - must spend the same number
|
||||
of evaluations either side of it, on a bracket of any width. If this
|
||||
fails, the approach/endgame table means nothing."""
|
||||
a, b = 0.0, width
|
||||
root = width * 0.3 # away from the midpoint, so nothing is by luck
|
||||
f = lambda x: x - root
|
||||
app, end, tot = phases(bisection, f, a, b, root)
|
||||
assert app + end == tot
|
||||
assert abs(app - end) <= 4, f"width {width}: {app} vs {end}"
|
||||
|
||||
|
||||
def test_phase_boundary_is_the_geometric_mean_of_width_and_attainable_tol():
|
||||
# near the origin the attainable tolerance is xtol itself
|
||||
assert phase_boundary(1e12, 0.0, 1e-12) == pytest.approx(1.0)
|
||||
assert phase_boundary(4.0, 0.0, 1e-12) == pytest.approx(2e-6)
|
||||
# far from it, floating-point spacing is the real floor
|
||||
assert phase_boundary(1e12, 1e12, 1e-12) > 1e3
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# the bracket replay
|
||||
# --------------------------------------------------------------------------
|
||||
def test_replay_scores_bisection_at_exactly_one_half():
|
||||
"""End-to-end check of the reconstruction: every bisection probe sits at
|
||||
the midpoint and removes exactly half, by construction. Any error in the
|
||||
replay shows up here immediately."""
|
||||
f = lambda x: x * x - 2.0
|
||||
g, xs = traced(f)
|
||||
bisection(g, 0.0, 2.0, 1e-9, MAXFEV)
|
||||
pos, shrinks, skipped = replay_bracket(f, 0.0, 2.0, xs[2:])
|
||||
assert skipped == 0
|
||||
assert pos, "no probes replayed"
|
||||
for t in pos:
|
||||
assert t == pytest.approx(0.5, abs=1e-12)
|
||||
for s in shrinks:
|
||||
assert s == pytest.approx(0.5, abs=1e-12)
|
||||
|
||||
|
||||
def test_replay_counts_probes_outside_the_bracket():
|
||||
"""A probe outside the proven bracket must be counted as skipped, not
|
||||
silently dropped - dropping them flatters whoever probes outside."""
|
||||
f = lambda x: x - 1.0
|
||||
# 5.0 is outside [0, 2]; 1.5 and 0.5 are inside
|
||||
pos, shrinks, skipped = replay_bracket(f, 0.0, 2.0, [1.5, 5.0, 0.5])
|
||||
assert skipped == 1
|
||||
assert len(pos) == len(shrinks) == 2
|
||||
|
||||
|
||||
def test_replay_shrinks_are_fractions():
|
||||
f = lambda x: math.cos(x) - x
|
||||
g, xs = traced(f)
|
||||
ours(g, 0.0, 1.0, XTOL, MAXFEV)
|
||||
_pos, shrinks, _sk = replay_bracket(f, 0.0, 1.0, xs[2:])
|
||||
assert shrinks
|
||||
for s in shrinks:
|
||||
assert 0.0 <= s <= 1.0
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# the observed order
|
||||
# --------------------------------------------------------------------------
|
||||
def test_observed_order_recovers_the_known_orders():
|
||||
"""The whole explanation of the endgame loss is the measured order. It
|
||||
must reproduce the textbook values for methods whose order is known."""
|
||||
f = lambda x: x * x - 2.0
|
||||
root = math.sqrt(2.0)
|
||||
p_ours = observed_order(ours, f, 0.0, 2.0, root)
|
||||
p_brent = observed_order(brent, f, 0.0, 2.0, root)
|
||||
p_bis = observed_order(bisection, f, 0.0, 2.0, root)
|
||||
assert 1.3 < p_ours < 1.9, p_ours # secant, 1.618
|
||||
assert p_brent > p_ours # inverse quadratic, 1.839
|
||||
assert p_bis < 1.3, p_bis # linear
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# the instrumentation must not perturb what it measures
|
||||
# --------------------------------------------------------------------------
|
||||
PROBS = [
|
||||
(lambda x: x * x - 2.0, 0.0, 2.0),
|
||||
(lambda x: math.exp(-x) * (x - 1.0), -1.0, 40.0),
|
||||
(lambda x: x ** 10 - 1.0, 0.0, 1.3),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("f,a,b", PROBS)
|
||||
def test_brent_stats_do_not_change_brent(f, a, b):
|
||||
plain = brent(f, a, b, XTOL, MAXFEV)
|
||||
st = {}
|
||||
withstats = brent(f, a, b, XTOL, MAXFEV, stats=st)
|
||||
assert plain == withstats
|
||||
assert 0 <= st["bisect"] <= plain[1]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("guard", GUARDS)
|
||||
@pytest.mark.parametrize("f,a,b", PROBS)
|
||||
def test_pure_stats_do_not_change_pure_root(f, a, b, guard):
|
||||
plain = pure_root(f, a, b, XTOL, MAXFEV, guard=guard)
|
||||
st = {}
|
||||
withstats = pure_root(f, a, b, XTOL, MAXFEV, guard=guard, stats=st)
|
||||
assert plain == withstats
|
||||
assert 0 <= st["forced"] <= plain[1]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("f,a,b", PROBS)
|
||||
def test_an_unguarded_run_forces_nothing(f, a, b):
|
||||
st = {}
|
||||
pure_root(f, a, b, XTOL, MAXFEV, guard="none", stats=st)
|
||||
assert st["forced"] == 0
|
||||
|
||||
|
||||
def test_the_bracket_guard_actually_fires_where_it_is_needed():
|
||||
"""The flat problem is the one that defeats every interpolation-only
|
||||
rule; the safeguard must be doing visible work there."""
|
||||
f = lambda x: math.exp(-x) * (x - 1.0)
|
||||
st = {}
|
||||
pure_root(f, -1.0, 40.0, XTOL, MAXFEV, guard="bracket", stats=st)
|
||||
assert st["forced"] > 0
|
||||
@@ -0,0 +1,91 @@
|
||||
"""Active noise control: the step size of an NLMS canceller.
|
||||
|
||||
The third domain, and the third instance of the same pattern. What is pinned
|
||||
here is the pattern and the mistake, not a victory.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from anc.bench import SCENARIOS, make_primary_path, make_secondary_path, run_nlms
|
||||
from anc.rules import Fixed, InSampleMeasured, KwongJohnston, Measured
|
||||
|
||||
N, SEEDS = 8000, 4
|
||||
|
||||
|
||||
SNRS = (30.0, 15.0)
|
||||
|
||||
|
||||
def sweep(rule, snrs=SNRS):
|
||||
"""Both noise levels, as the experiment uses.
|
||||
|
||||
Measuring at 30 dB alone is the wrong test and was: with almost no
|
||||
measurement noise there is nothing for a safety factor to hedge against,
|
||||
the exact step mu = 1 is near optimal, and any hedging costs a little.
|
||||
The whole point of the rule appears at 15 dB. A claim about the grid has
|
||||
to be tested on the grid."""
|
||||
vals = []
|
||||
for snr in snrs:
|
||||
for s in range(SEEDS):
|
||||
for _sname, gen in SCENARIOS:
|
||||
rng = np.random.default_rng(1000 + s)
|
||||
noise = gen(N, rng)
|
||||
nr = run_nlms(noise, make_primary_path(rng),
|
||||
make_secondary_path(rng), rule, snr_db=snr,
|
||||
seed=s)
|
||||
if math.isfinite(nr):
|
||||
vals.append(nr)
|
||||
return sum(vals) / len(vals), min(vals), len(vals)
|
||||
|
||||
|
||||
def test_the_in_sample_observable_is_uninformative():
|
||||
"""The mistake, pinned. NLMS with mu = 1 drives the a posteriori error of
|
||||
the current sample to exactly zero, so a rule that measures it reads 'the
|
||||
model was perfect' and proposes mu = 1 for ever. It is therefore the
|
||||
fixed mu = 1 rule under another name, and the numbers say so."""
|
||||
a, _lo, _n = sweep(InSampleMeasured())
|
||||
b, _lo2, _n2 = sweep(Fixed(1.0))
|
||||
assert abs(a - b) < 0.1, (a, b)
|
||||
|
||||
|
||||
def test_measuring_out_of_sample_is_better_than_measuring_in_sample():
|
||||
"""The fix: what mu hedges against is the part of the error the filter
|
||||
cannot cancel, and that is observable as the floor the error keeps
|
||||
hitting - not as the residual of the sample just fitted."""
|
||||
good, _lo, _n = sweep(Measured())
|
||||
bad, _lo2, _n2 = sweep(InSampleMeasured())
|
||||
assert good > bad, (good, bad)
|
||||
|
||||
|
||||
def test_the_measured_rule_beats_the_four_constant_scheme():
|
||||
"""Kwong-Johnston is the classical variable-step-size rule and carries
|
||||
four constants. This is the comparison the transfer is about."""
|
||||
ours, _lo, _n = sweep(Measured())
|
||||
kj, _lo2, _n2 = sweep(KwongJohnston())
|
||||
assert ours > kj + 3.0, (ours, kj)
|
||||
|
||||
|
||||
def test_and_loses_to_one_well_chosen_constant():
|
||||
"""The other half, and the same in all three domains: measuring the
|
||||
constant beats the elaborate multi-constant schemes and does not beat a
|
||||
single constant chosen correctly for the benchmark."""
|
||||
ours, _lo, _n = sweep(Measured())
|
||||
best_fixed, _lo2, _n2 = sweep(Fixed(0.5))
|
||||
assert best_fixed > ours, (best_fixed, ours)
|
||||
assert best_fixed - ours < 3.0, (best_fixed, ours)
|
||||
|
||||
|
||||
def test_no_rule_diverges():
|
||||
"""A step size that is too large makes the filter blow up; none of these
|
||||
may."""
|
||||
for rule in (Fixed(0.1), Fixed(0.5), Fixed(1.0), KwongJohnston(),
|
||||
Measured()):
|
||||
_m, _lo, n = sweep(rule)
|
||||
assert n == SEEDS * len(SCENARIOS) * len(SNRS), (rule.name, n)
|
||||
@@ -0,0 +1,89 @@
|
||||
"""The paper's numbers are part of the regression.
|
||||
|
||||
A table transcribed by hand drifts from its data the first time the data is
|
||||
regenerated. verify_article.py asserts all 72 stated numbers against
|
||||
sweep_full.json; this makes that check part of the suite, so a re-run of the
|
||||
sweep that moves a number fails the build instead of quietly invalidating the
|
||||
paper.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# the other end of the trade the paper states: better average, one constant
|
||||
PROJ_LABEL = "pure.ratio.derived3.project"
|
||||
|
||||
|
||||
@pytest.mark.skipif(not os.path.exists(os.path.join(ROOT, "sweep_full.json")),
|
||||
reason="sweep_full.json not present; run sweep_full.py")
|
||||
def test_every_number_in_the_paper_matches_the_sweep():
|
||||
r = subprocess.run([sys.executable, "verify_article.py"], cwd=ROOT,
|
||||
capture_output=True, text=True, timeout=600)
|
||||
tail = "\n".join(r.stdout.strip().splitlines()[-25:])
|
||||
assert r.returncode == 0, f"the paper disagrees with the data:\n{tail}"
|
||||
assert "every number stated in the paper matches" in r.stdout
|
||||
assert r.stdout.count(" OK ") >= 72, r.stdout.count(" OK ")
|
||||
|
||||
|
||||
@pytest.mark.skipif(not os.path.exists(os.path.join(ROOT, "sweep_full.json")),
|
||||
reason="sweep_full.json not present")
|
||||
def test_the_headline_claim_is_what_the_paper_says_it_is():
|
||||
"""Guard against the one mistake that would matter most: the abstract
|
||||
claiming a place the data does not support.
|
||||
|
||||
It caught exactly that. Until Chandrupatla's own method was added - a
|
||||
reader asked why the owner of half the benchmark was missing - this test
|
||||
asserted that a configuration of ours came first, and it passed only
|
||||
because the method that beats us was absent from the field. The claim it
|
||||
now guards is the corrected one: we are not first, Chandrupatla is, and
|
||||
our margin behind it on the canonical set is under one percent."""
|
||||
import json
|
||||
|
||||
from report_sweep import PROPOSED, rank
|
||||
with open(os.path.join(ROOT, "sweep_full.json"), encoding="utf8") as fh:
|
||||
rows = json.load(fh)
|
||||
by = {r["label"]: r for r in rows}
|
||||
assert "chandrupatla" in by, "the sweep predates the missing baselines"
|
||||
|
||||
aps = rank(rows, "APS")
|
||||
assert aps[0]["label"] == "chandrupatla", aps[0]["label"]
|
||||
|
||||
def both(lbl):
|
||||
return by[lbl]["APS"]["total"] + by[lbl]["CHA"]["total"]
|
||||
|
||||
# What the paper may claim for the credit method, and nothing more.
|
||||
# It is NOT within a percent of the best on Alefeld-Potra-Shi - that was
|
||||
# true of the projection variant, which carries a constant. Buying
|
||||
# constant-freedom and a near-bisection worst case costs average
|
||||
# performance, and the test pins the direction of that trade so the
|
||||
# abstract cannot quietly reclaim the projection variant's numbers.
|
||||
assert both(PROPOSED) > both("chandrupatla"), "behind Chandrupatla"
|
||||
for weaker in ("toms748", "brent", "itp", "ridders", "bisection"):
|
||||
assert both(PROPOSED) < both(weaker), weaker
|
||||
|
||||
# On the canonical set alone it is behind the classical methods; saying
|
||||
# otherwise would be quoting the projection variant.
|
||||
assert by[PROPOSED]["APS"]["total"] > by["toms748"]["APS"]["total"]
|
||||
assert by[PROPOSED]["APS"]["total"] > by["brent"]["APS"]["total"]
|
||||
|
||||
# The claim it does own: the worst single problem, over the problems it
|
||||
# solves, is close to bisection's and far below the classical methods'.
|
||||
def worst(lbl):
|
||||
w = 0
|
||||
for setname in ("APS", "CHA"):
|
||||
for n, ok in by[lbl][setname]["per"].values():
|
||||
if ok:
|
||||
w = max(w, n)
|
||||
return w
|
||||
|
||||
assert worst(PROPOSED) < 100, worst(PROPOSED)
|
||||
assert worst(PROPOSED) < worst("brent")
|
||||
assert worst(PROPOSED) < worst("toms748")
|
||||
assert worst(PROPOSED) < worst(PROJ_LABEL), (worst(PROPOSED),
|
||||
worst(PROJ_LABEL))
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Grafting similarity onto existing methods: where it works and where not.
|
||||
|
||||
The project's one clear success was a graft - the credit safeguard turned
|
||||
regula falsi, the secant rule and inverse quadratic interpolation into methods
|
||||
that solve 193 of 199 with a near-bisection worst case. This file pins the
|
||||
graft that FAILED, and the boundary the two of them mark out.
|
||||
"""
|
||||
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.baselines import brent, chandrupatla
|
||||
from stage16_blend import brent_blend, chandrupatla_blend, load, measure
|
||||
|
||||
PROBS = load()
|
||||
|
||||
|
||||
def both(m):
|
||||
t, ok, worst = measure(m, PROBS)
|
||||
return t["APS"] + t["CHA"], ok["APS"] + ok["CHA"], worst
|
||||
|
||||
|
||||
def test_the_controls_reproduce_the_published_methods_exactly():
|
||||
"""Without this the experiment is void: a blend must be compared against
|
||||
the same code with the blend switched off, not against a different
|
||||
implementation."""
|
||||
pub_b = both(lambda f, a, b, xt, mx: brent(f, a, b, xt, mx))
|
||||
off_b = both(lambda f, a, b, xt, mx: brent_blend(f, a, b, xt, mx,
|
||||
mode="off"))
|
||||
assert pub_b == off_b, (pub_b, off_b)
|
||||
pub_c = both(lambda f, a, b, xt, mx: chandrupatla(f, a, b, xt, mx))
|
||||
off_c = both(lambda f, a, b, xt, mx: chandrupatla_blend(f, a, b, xt, mx,
|
||||
mode="off"))
|
||||
assert pub_c == off_c, (pub_c, off_c)
|
||||
|
||||
|
||||
def test_softening_brents_acceptance_test_makes_it_worse():
|
||||
base, _s, _w = both(lambda f, a, b, xt, mx: brent(f, a, b, xt, mx))
|
||||
blend, _s2, _w2 = both(lambda f, a, b, xt, mx: brent_blend(f, a, b, xt, mx,
|
||||
mode="blend"))
|
||||
assert blend > 1.2 * base, (base, blend)
|
||||
|
||||
|
||||
def test_softening_chandrupatlas_acceptance_test_makes_it_worse():
|
||||
base, s_base, _w = both(lambda f, a, b, xt, mx: chandrupatla(f, a, b, xt,
|
||||
mx))
|
||||
blend, s_blend, _w2 = both(
|
||||
lambda f, a, b, xt, mx: chandrupatla_blend(f, a, b, xt, mx,
|
||||
mode="blend"))
|
||||
assert blend > 1.2 * base, (base, blend)
|
||||
assert s_blend < s_base, (s_base, s_blend) # and it breaks problems
|
||||
|
||||
|
||||
def test_projecting_onto_brents_bound_also_fails():
|
||||
"""The move that worked inside our own method - project, do not replace -
|
||||
does not transfer to somebody else's acceptance condition. Clamping to
|
||||
the boundary produces the most aggressive still-permitted step, which is
|
||||
exactly where the interpolation has stopped carrying information."""
|
||||
base, s_base, _w = both(lambda f, a, b, xt, mx: brent(f, a, b, xt, mx))
|
||||
proj, s_proj, _w2 = both(lambda f, a, b, xt, mx: brent_blend(
|
||||
f, a, b, xt, mx, mode="project"))
|
||||
assert proj > base, (base, proj)
|
||||
assert s_proj < s_base, (s_base, s_proj)
|
||||
|
||||
|
||||
def test_the_boundary_this_marks_out():
|
||||
"""Three failures against one success, and they differ in kind.
|
||||
|
||||
The safeguard graft succeeded where the quantity being controlled is a
|
||||
GRADED QUALITY - how much margin to leave, how bold a step to take. The
|
||||
blend graft failed where the quantity is a VALIDITY CONDITION - whether
|
||||
the interpolation model applies at all. Near such a boundary a point is
|
||||
not partly good; it is where the model has just stopped holding, which is
|
||||
why Brent and Chandrupatla discard it rather than shrink it."""
|
||||
from stage14_is_anything_reusable import RULES, load as load14, measure as m14
|
||||
p14 = load14()
|
||||
for name, rule in RULES:
|
||||
t_raw, s_raw, _w = m14(rule, False, p14)
|
||||
t_g, s_g, _w2 = m14(rule, True, p14)
|
||||
assert s_g > s_raw and t_g < t_raw, name # the graft that works
|
||||
base, _s, _w = both(lambda f, a, b, xt, mx: brent(f, a, b, xt, mx))
|
||||
blend, _s2, _w2 = both(lambda f, a, b, xt, mx: brent_blend(
|
||||
f, a, b, xt, mx, mode="blend"))
|
||||
assert blend > base # the graft that does not
|
||||
@@ -0,0 +1,161 @@
|
||||
"""The one claim that survives the objection a reader raises first.
|
||||
|
||||
Everything else in this project is a tie, a loss, or a negative result. This
|
||||
file pins the exception and, just as important, pins its scope: it is a claim
|
||||
about REPAIRING regula falsi, not about being the best root finder.
|
||||
"""
|
||||
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.baselines import (anderson_bjorck, brent, chandrupatla, illinois,
|
||||
pegasus, regula_falsi)
|
||||
from stage18_bulletproof import falsi_driver, load_roots
|
||||
|
||||
XTOL, MAXFEV, ACC = 1e-12, 200, 1e-6
|
||||
PROBS = load_roots()
|
||||
REPAIRS = ("none", "illinois", "pegasus", "anderson", "credit")
|
||||
|
||||
|
||||
def score(method):
|
||||
total = solved = worst = 0
|
||||
for f, a, b, root in PROBS:
|
||||
try:
|
||||
x, n, conv = method(f, a, b, XTOL, MAXFEV)
|
||||
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:
|
||||
worst = max(worst, n)
|
||||
return total, solved, worst
|
||||
|
||||
|
||||
def by_repair(repair):
|
||||
return score(lambda f, a, b, xt, mx: falsi_driver(f, a, b, xt, mx, repair))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# the driver cannot be handicapping the competitors
|
||||
# --------------------------------------------------------------------------
|
||||
@pytest.mark.parametrize("repair,independent", [
|
||||
("none", regula_falsi), ("illinois", illinois), ("pegasus", pegasus),
|
||||
("anderson", anderson_bjorck)])
|
||||
def test_two_independent_implementations_agree_exactly(repair, independent):
|
||||
"""The classical repairs exist twice in this repository, written at
|
||||
different times against different structures: `_false_position_family` in
|
||||
simroot.baselines, and the pluggable driver in stage18. They agree to the
|
||||
evaluation, which is what removes 'your driver favours your repair' from
|
||||
the table."""
|
||||
a = score(independent)
|
||||
b = by_repair(repair)
|
||||
assert a[:2] == b[:2], (repair, a, b)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# the claim
|
||||
# --------------------------------------------------------------------------
|
||||
def test_credit_beats_every_classical_repair_on_both_axes_at_once():
|
||||
"""Same interpolation, same driver, same budget; only the repair differs.
|
||||
Against Illinois (1971), Pegasus (1972) and Anderson-Bjorck (1973)."""
|
||||
c_total, c_solved, c_worst = by_repair("credit")
|
||||
for repair in ("illinois", "pegasus", "anderson"):
|
||||
t, s, w = by_repair(repair)
|
||||
assert c_solved > s, (repair, c_solved, s)
|
||||
assert c_worst < w, (repair, c_worst, w)
|
||||
assert c_total < min(by_repair(r)[0] for r in ("illinois", "pegasus",
|
||||
"anderson"))
|
||||
|
||||
|
||||
def test_no_classical_repair_gets_near_the_solved_count():
|
||||
"""193 of 199 is what bisection, Chandrupatla and TOMS 748 achieve; the
|
||||
best classical repair of regula falsi reaches 176."""
|
||||
_t, c_solved, _w = by_repair("credit")
|
||||
assert c_solved == 193, c_solved
|
||||
for repair in ("none", "illinois", "pegasus", "anderson"):
|
||||
assert by_repair(repair)[1] <= 176, repair
|
||||
|
||||
|
||||
def test_the_advantage_is_not_an_artefact_of_aggregating_two_sets():
|
||||
"""A reader will ask whether the win comes from one set. On the
|
||||
wide-bracket set the credit repair halves the cost and doubles the solved
|
||||
count; on the canonical set it matches Illinois's solved count at lower
|
||||
cost. Anderson-Bjorck is cheaper there, and fails eight problems the
|
||||
credit repair solves - that trade is stated, not hidden."""
|
||||
from scipy.optimize._tstutils import (_APS_TESTS_DICTS,
|
||||
_CHANDRUPATLA_TESTS_DICTS)
|
||||
|
||||
def per_set(repair, dicts, chand):
|
||||
total = solved = 0
|
||||
for d in dicts:
|
||||
a, b = float(d["bracket"][0]), float(d["bracket"][1])
|
||||
args = () if chand else d.get("args", ())
|
||||
f = (lambda ff, aa: lambda t: ff(t, *aa))(d["f"], args)
|
||||
try:
|
||||
x, n, conv = falsi_driver(f, a, b, XTOL, MAXFEV, repair)
|
||||
except Exception:
|
||||
x, n, conv = float("nan"), MAXFEV, False
|
||||
ok = bool(conv and math.isfinite(x)
|
||||
and abs(x - float(d["root"])) <= ACC)
|
||||
total += n if ok else MAXFEV
|
||||
solved += ok
|
||||
return total, solved
|
||||
|
||||
for repair in ("illinois", "pegasus", "anderson"):
|
||||
c_t, c_s = per_set("credit", _CHANDRUPATLA_TESTS_DICTS, True)
|
||||
t, s = per_set(repair, _CHANDRUPATLA_TESTS_DICTS, True)
|
||||
assert c_t < 0.7 * t, (repair, c_t, t) # halves the cost
|
||||
assert c_s >= 1.7 * s, (repair, c_s, s) # doubles the solved
|
||||
|
||||
|
||||
def test_the_scope_of_the_claim_is_repairs_not_the_field():
|
||||
"""The credit repair of regula falsi is still beaten by Brent and by
|
||||
Chandrupatla. The claim must never be stated as 'the best root finder'."""
|
||||
c_total, _s, _w = by_repair("credit")
|
||||
for better in (brent, chandrupatla):
|
||||
t, _s2, _w2 = score(lambda f, a, b, xt, mx, _m=better: _m(f, a, b, xt,
|
||||
mx))
|
||||
assert t < c_total, (better.__name__, t, c_total)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# the claim that does NOT survive, pinned so it is not overstated
|
||||
# --------------------------------------------------------------------------
|
||||
def test_the_anc_claim_is_not_uniform_and_must_be_stated_as_such():
|
||||
"""Against a Kwong-Johnston oracle tuned per scenario the measured step
|
||||
size wins on the mean (19.14 dB against 17.51) but on only four of ten
|
||||
cells, and the mean is carried by two of them. It is a real result and
|
||||
it is not bullet-proof; this test exists so the difference is not blurred."""
|
||||
import numpy as np
|
||||
|
||||
from anc.bench import SCENARIOS, make_primary_path, make_secondary_path, \
|
||||
run_nlms
|
||||
from anc.rules import Measured
|
||||
from stage18_bulletproof import KJTuned, KJ_GRID
|
||||
|
||||
wins = cells = 0
|
||||
for _sname, gen in SCENARIOS:
|
||||
for snr in (30.0, 15.0):
|
||||
def sc(rule):
|
||||
vals = []
|
||||
for s in range(3):
|
||||
rng = np.random.default_rng(1000 + s)
|
||||
noise = gen(8000, rng)
|
||||
nr = run_nlms(noise, make_primary_path(rng),
|
||||
make_secondary_path(rng), rule,
|
||||
snr_db=snr, seed=s)
|
||||
if math.isfinite(nr):
|
||||
vals.append(nr)
|
||||
return sum(vals) / len(vals)
|
||||
ours = sc(Measured())
|
||||
best_kj = max(sc(KJTuned(a, g)) for a, g in KJ_GRID)
|
||||
cells += 1
|
||||
wins += ours > best_kj
|
||||
assert 0 < wins < cells, (wins, cells)
|
||||
@@ -0,0 +1,246 @@
|
||||
"""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
|
||||
@@ -0,0 +1,111 @@
|
||||
"""The objection that the comparison stopped at 1973, and what it settled.
|
||||
|
||||
Two things came out of running it, and they scope the claim exactly:
|
||||
|
||||
the credit projection is the best repair known for the false-position
|
||||
family - better than 1971, 1972, 1973, 1995 and 2011 on evaluations,
|
||||
solved count and worst case at once, and the only one that reaches the
|
||||
193-of-199 ceiling;
|
||||
|
||||
and it is NOT better than a purpose-built acceptance test: swapped into
|
||||
Chandrupatla's own interpolation it costs 7627 where his test costs 5209.
|
||||
|
||||
So the safeguard is a good DEFAULT for a rule that has none, not a
|
||||
replacement for one that has a good one. Both halves are pinned.
|
||||
"""
|
||||
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.baselines import chandrupatla
|
||||
from stage18_bulletproof import falsi_driver, load_roots
|
||||
from stage19_modern import chandrupatla_guard, falsi_modern, score
|
||||
|
||||
PROBS = load_roots()
|
||||
XTOL, MAXFEV = 1e-12, 200
|
||||
|
||||
|
||||
def falsi(repair):
|
||||
return score(lambda f, a, b, xt, mx: falsi_driver(f, a, b, xt, mx, repair),
|
||||
PROBS)
|
||||
|
||||
|
||||
def modern(variant):
|
||||
return score(lambda f, a, b, xt, mx: falsi_modern(f, a, b, xt, mx, variant),
|
||||
PROBS)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# the half that holds
|
||||
# --------------------------------------------------------------------------
|
||||
@pytest.mark.parametrize("name,getter", [
|
||||
("illinois-1971", lambda: falsi("illinois")),
|
||||
("pegasus-1972", lambda: falsi("pegasus")),
|
||||
("anderson-1973", lambda: falsi("anderson")),
|
||||
("ford-1995", lambda: modern("ford")),
|
||||
("galdino-2011", lambda: modern("galdino")),
|
||||
])
|
||||
def test_credit_beats_every_published_repair_on_all_three_axes(name, getter):
|
||||
c_t, c_s, c_w = falsi("credit")
|
||||
t, s, w = getter()
|
||||
assert c_t < t, (name, c_t, t)
|
||||
assert c_s > s, (name, c_s, s)
|
||||
assert c_w < w, (name, c_w, w)
|
||||
|
||||
|
||||
def test_no_published_repair_reaches_the_ceiling():
|
||||
"""193 of 199 is what bisection, Chandrupatla and TOMS 748 reach. Fifty
|
||||
years of repairs of the false-position family stop at 176."""
|
||||
assert falsi("credit")[1] == 193
|
||||
for g in (lambda: falsi("illinois"), lambda: falsi("pegasus"),
|
||||
lambda: falsi("anderson"), lambda: modern("ford"),
|
||||
lambda: modern("galdino")):
|
||||
assert g()[1] <= 176
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# the half that does not - pinned so the claim is never overstated
|
||||
# --------------------------------------------------------------------------
|
||||
def test_the_pluggable_chandrupatla_reproduces_the_published_one():
|
||||
"""Without this the swap below compares against a different algorithm."""
|
||||
a = score(lambda f, x, y, xt, mx: chandrupatla_guard(f, x, y, xt, mx,
|
||||
"own"), PROBS)
|
||||
b = score(lambda f, x, y, xt, mx: chandrupatla(f, x, y, xt, mx), PROBS)
|
||||
assert a[:2] == b[:2], (a, b)
|
||||
|
||||
|
||||
def test_credit_rescues_chandrupatlas_interpolation_but_loses_to_his_test():
|
||||
"""The sharp test. Same interpolation, same bookkeeping, only the guard
|
||||
differs. Credit turns an unguarded rule that solves 165 into one that
|
||||
solves 193 - and Chandrupatla's own acceptance test does the same job for
|
||||
a third fewer evaluations."""
|
||||
own = score(lambda f, a, b, xt, mx: chandrupatla_guard(f, a, b, xt, mx,
|
||||
"own"), PROBS)
|
||||
cred = score(lambda f, a, b, xt, mx: chandrupatla_guard(f, a, b, xt, mx,
|
||||
"credit"), PROBS)
|
||||
none = score(lambda f, a, b, xt, mx: chandrupatla_guard(f, a, b, xt, mx,
|
||||
"none"), PROBS)
|
||||
assert cred[1] > none[1] and cred[0] < none[0] # it rescues
|
||||
assert own[0] < cred[0] # and is beaten
|
||||
assert own[1] == cred[1] == 193
|
||||
|
||||
|
||||
def test_the_scope_sentence_the_paper_may_use():
|
||||
"""A default safeguard for a rule that has none - not a replacement for a
|
||||
purpose-built one. Both directions asserted together so neither can be
|
||||
quoted alone."""
|
||||
c_t, c_s, _cw = falsi("credit")
|
||||
best_published = min(falsi(r)[0] for r in ("illinois", "pegasus",
|
||||
"anderson"))
|
||||
best_published = min(best_published, modern("ford")[0],
|
||||
modern("galdino")[0])
|
||||
assert c_t < best_published # best repair known
|
||||
own = score(lambda f, a, b, xt, mx: chandrupatla_guard(f, a, b, xt, mx,
|
||||
"own"), PROBS)
|
||||
assert own[0] < c_t # and not the best guard
|
||||
@@ -0,0 +1,102 @@
|
||||
"""The second domain: adaptive step-size control for ODEs.
|
||||
|
||||
What is being tested is the transfer, not the integrator: everything except
|
||||
the controller is shared, so a difference can only come from the controller.
|
||||
"""
|
||||
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 odestep.controllers import (ALL, Classical, Elementary, GustafssonPI,
|
||||
MeasuredSafety)
|
||||
from odestep.driver import integrate, validate_against_scipy
|
||||
from stage15_ode import PROBLEMS, TOLS, reference, scaled_err
|
||||
|
||||
|
||||
def test_the_driver_reproduces_the_library():
|
||||
"""The instrument before the experiment.
|
||||
|
||||
The first version of this work carried a hand-typed Dormand-Prince
|
||||
tableau and validated it by measuring convergence orders. That was both
|
||||
an unnecessary risk and, as it turned out, a material one: with the
|
||||
hand-written tableau the measured-safety controller appeared to beat the
|
||||
five-constant PI controller by 8%, and with SciPy's it is 2.6% behind.
|
||||
The integrator is now SciPy's throughout and the check is direct."""
|
||||
for name, mine, ref, endpoint in validate_against_scipy():
|
||||
assert abs(mine - ref) / ref < 0.05, (name, mine, ref)
|
||||
assert endpoint < 1e-6, (name, endpoint)
|
||||
|
||||
|
||||
def test_every_controller_solves_an_easy_problem():
|
||||
f = lambda t, y: [-y[0]]
|
||||
for c in ALL:
|
||||
r = integrate(f, 0.0, 10.0, [1.0], c, atol=1e-8, rtol=1e-8)
|
||||
assert r["reached_end"], c.name
|
||||
assert abs(r["y"][0] - math.exp(-10.0)) < 1e-6, c.name
|
||||
|
||||
|
||||
def test_the_constant_free_baseline_is_the_one_to_beat():
|
||||
"""The elementary controller is what the error model alone dictates and
|
||||
what every practical code refuses to use. If it were not much worse,
|
||||
there would be nothing for a safety factor - chosen or measured - to buy."""
|
||||
f = lambda t, y: [y[1], 5.0 * (1 - y[0] * y[0]) * y[1] - y[0]]
|
||||
elem = integrate(f, 0.0, 25.0, [2.0, 0.0], Elementary(), atol=1e-8,
|
||||
rtol=1e-8)
|
||||
meas = integrate(f, 0.0, 25.0, [2.0, 0.0], MeasuredSafety(), atol=1e-8,
|
||||
rtol=1e-8)
|
||||
# margins measured with SciPy's integrator. An earlier version demanded
|
||||
# a factor of two on evaluations and five on rejections; those came from
|
||||
# a hand-written tableau and are not what the library produces.
|
||||
assert elem["nfev"] > 1.2 * meas["nfev"], (elem["nfev"], meas["nfev"])
|
||||
assert elem["rejected"] > 1.5 * meas["rejected"], (elem["rejected"],
|
||||
meas["rejected"])
|
||||
|
||||
|
||||
def test_where_the_measured_safety_factor_actually_stands():
|
||||
"""The honest position, over the whole grid, with SciPy's integrator.
|
||||
|
||||
It is NOT ahead of the PI controller - an earlier version of this test
|
||||
asserted that, on numbers produced by a hand-written tableau. What it is:
|
||||
level with a five-constant controller to within a few percent while
|
||||
having none, and clearly ahead of the constant-free elementary rule that
|
||||
every practical code refuses to use. It is behind the three-constant
|
||||
classical controller and the test says so."""
|
||||
tot = {c.name: 0 for c in ALL}
|
||||
fails = {c.name: 0 for c in ALL}
|
||||
refs = {name: (exact if exact is not None else reference(f, t0, t1, y0))
|
||||
for name, f, t0, t1, y0, exact in PROBLEMS}
|
||||
for tol in TOLS:
|
||||
for name, f, t0, t1, y0, _exact in PROBLEMS:
|
||||
for c in ALL:
|
||||
r = integrate(f, t0, t1, y0, c, atol=tol, rtol=tol)
|
||||
err = scaled_err(r["y"], refs[name], tol, tol)
|
||||
bad = (not r["reached_end"]) or (math.isfinite(err)
|
||||
and err > 1000.0)
|
||||
tot[c.name] += r["nfev"]
|
||||
fails[c.name] += bad
|
||||
# nobody wins by failing more
|
||||
assert len(set(fails.values())) == 1, fails
|
||||
m = tot[MeasuredSafety().name]
|
||||
pi = tot[GustafssonPI().name]
|
||||
# level with the five-constant PI controller, within five percent
|
||||
assert abs(m - pi) / pi < 0.05, (m, pi)
|
||||
# clearly ahead of the constant-free rule the error model alone dictates
|
||||
assert m < 0.9 * tot[Elementary().name], (m, tot[Elementary().name])
|
||||
# and honestly behind the three-constant classical one
|
||||
assert m > tot[Classical().name], (m, tot[Classical().name])
|
||||
|
||||
|
||||
def test_the_controller_holds_no_unused_state():
|
||||
"""It was called a credit controller while banking a margin that took no
|
||||
part in any decision - the ledger was vestigial and the name unearned.
|
||||
Nothing may accumulate here that the step factor does not read."""
|
||||
c = MeasuredSafety()
|
||||
c.reset()
|
||||
fields = set(vars(c))
|
||||
assert fields == {"predicted", "delta"}, fields
|
||||
@@ -0,0 +1,144 @@
|
||||
"""Pins the phase detector as REFUTED.
|
||||
|
||||
The rho correction was derived, implemented and measured; it failed on all
|
||||
three criteria set before it was run. These tests exist so nobody adopts it
|
||||
later on the strength of the derivation alone, and so that if someone repairs
|
||||
it the tests fail loudly and the claim gets re-examined.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
from scipy.optimize._tstutils import _CHANDRUPATLA_TESTS_DICTS
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from simroot.pure import pure_root
|
||||
from stage5_three_point import PROBLEMS, order_of
|
||||
|
||||
XTOL = 1e-12
|
||||
MAXFEV = 200
|
||||
ACC = 1e-6
|
||||
|
||||
|
||||
def total(scale, sim="ratio"):
|
||||
t = 0
|
||||
for _n, f, a, b, root in PROBLEMS:
|
||||
x, n, conv = pure_root(f, a, b, XTOL, MAXFEV, similarity=sim,
|
||||
scale=scale, guard="project")
|
||||
t += n if (conv and abs(x - root) <= 1e-8) else MAXFEV
|
||||
return t
|
||||
|
||||
|
||||
def med_order(scale, sim="ratio"):
|
||||
ps = [order_of(lambda f_, a_, b_, xt, mx: pure_root(
|
||||
f_, a_, b_, XTOL, MAXFEV, similarity=sim, scale=scale, guard="project"),
|
||||
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]
|
||||
return sorted(ps)[len(ps) // 2] if ps else float("nan")
|
||||
|
||||
|
||||
def test_rho_makes_the_standard_problems_worse():
|
||||
plain, rho = total("derived3"), total("derived3_rho")
|
||||
assert rho > plain * 1.4, (plain, rho)
|
||||
|
||||
|
||||
def test_rho_destroys_the_order_it_was_meant_to_preserve():
|
||||
"""The correction was supposed to vanish near the root. It does not."""
|
||||
assert med_order("derived3") > 1.7
|
||||
assert med_order("derived3_rho") < 1.3
|
||||
|
||||
|
||||
def test_rho_does_not_capture_the_family_it_was_built_for():
|
||||
"""fun3/fun4/fun5 are where cbrt beats us 2.6x. The detector was built to
|
||||
recover that and recovers essentially none of it."""
|
||||
fam = [d for d in _CHANDRUPATLA_TESTS_DICTS
|
||||
if d["ID"].startswith(("fun3.", "fun4.", "fun5."))]
|
||||
|
||||
def t(sim, scale):
|
||||
s = 0
|
||||
for d in fam:
|
||||
a, b = float(d["bracket"][0]), float(d["bracket"][1])
|
||||
x, n, conv = pure_root(d["f"], a, b, XTOL, MAXFEV,
|
||||
similarity=sim, scale=scale, guard="project")
|
||||
s += n if (conv and abs(x - float(d["root"])) <= ACC) else MAXFEV
|
||||
return s
|
||||
|
||||
plain, rho, cb = t("ratio", "derived3"), t("ratio", "derived3_rho"), t("cbrt", "derived3")
|
||||
assert cb < 0.6 * plain, (cb, plain) # the regime is real
|
||||
assert rho > 0.9 * plain, (rho, plain) # the detector does not reach it
|
||||
|
||||
|
||||
def test_the_refuted_scales_are_still_reachable_and_still_converge():
|
||||
"""Refuted is not broken: the experiment must stay reproducible."""
|
||||
for scale in ("derived_rho", "derived3_rho"):
|
||||
x, n, conv = pure_root(lambda t: t * t - 2.0, 0.0, 2.0, XTOL, MAXFEV,
|
||||
scale=scale, guard="project")
|
||||
assert conv and abs(x - math.sqrt(2.0)) <= 1e-8
|
||||
|
||||
|
||||
def test_the_proposed_configuration_uses_neither_refuted_variant():
|
||||
"""Guards against a refuted variant quietly becoming the default."""
|
||||
from report_sweep import PROPOSED
|
||||
assert "_rho" not in PROPOSED
|
||||
assert "_m" not in PROPOSED
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# the m correction: derivation sound, estimator not
|
||||
# --------------------------------------------------------------------------
|
||||
def test_m_preserves_the_order_and_the_small_set_which_is_why_it_deceives():
|
||||
"""Unlike rho, the m correction passes both of the tests a hand-picked
|
||||
set can show. Pinned precisely because that is what makes it dangerous."""
|
||||
assert med_order("derived3_m") > 1.7
|
||||
assert total("derived3_m") <= total("derived3")
|
||||
|
||||
|
||||
def test_m_loses_on_the_full_benchmark():
|
||||
"""And fails the only test that matters. If this ever flips, the
|
||||
estimator has been improved and the refutation must be revisited."""
|
||||
import json
|
||||
if not os.path.exists(os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||
"sweep_full.json")):
|
||||
pytest.skip("sweep not present")
|
||||
from scipy.optimize._tstutils import _APS_TESTS_DICTS
|
||||
plain = m = 0
|
||||
for d in _APS_TESTS_DICTS:
|
||||
a, b = float(d["bracket"][0]), float(d["bracket"][1])
|
||||
args = d.get("args", ())
|
||||
f = (lambda ff, aa: lambda t: ff(t, *aa))(d["f"], args)
|
||||
root = float(d["root"])
|
||||
for scale, acc in (("derived3", "p"), ("derived3_m", "m")):
|
||||
try:
|
||||
x, n, conv = pure_root(f, a, b, XTOL, MAXFEV,
|
||||
similarity="ratio", scale=scale,
|
||||
guard="project")
|
||||
except Exception:
|
||||
x, n, conv = float("nan"), MAXFEV, False
|
||||
good = conv and math.isfinite(x) and abs(x - root) <= ACC
|
||||
if acc == "p":
|
||||
plain += n if good else MAXFEV
|
||||
else:
|
||||
m += n if good else MAXFEV
|
||||
assert m > plain, (m, plain)
|
||||
|
||||
|
||||
def test_the_local_order_estimator_declines_when_the_model_does_not_apply():
|
||||
"""theta outside (0,1) means the power-law picture does not describe the
|
||||
data; the estimator must return 1 rather than clamp a value into range."""
|
||||
from simroot.pure import _local_order
|
||||
# a straight line: f'' = 0, theta = 0, m must be exactly 1
|
||||
lin = lambda t: 3.0 * t - 6.0
|
||||
assert _local_order(3.0, lin(3.0), 2.0, lin(2.0), 1.0, lin(1.0)) == 1.0
|
||||
# missing third point
|
||||
assert _local_order(1.0, 1.0, 0.5, 0.5, None, None) == 1.0
|
||||
# repeated nodes
|
||||
assert _local_order(1.0, 1.0, 1.0, 1.0, 1.0, 1.0) == 1.0
|
||||
# a double root at 0: f = t^2 has theta = 1/2, so m = 2
|
||||
sq = lambda t: t * t
|
||||
m = _local_order(1e-3, sq(1e-3), 2e-3, sq(2e-3), 3e-3, sq(3e-3))
|
||||
assert abs(m - 2.0) < 1e-6, m
|
||||
@@ -0,0 +1,142 @@
|
||||
"""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)
|
||||
@@ -0,0 +1,177 @@
|
||||
"""Regression suite for simroot.pure and for the benchmark harness.
|
||||
|
||||
Each audited defect is pinned as a test, then the contracts the method is
|
||||
allowed to claim in the paper - and only those.
|
||||
"""
|
||||
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.baselines import bisection, itp
|
||||
from simroot.pure import GUARDS, PURE_SIMS, pure_root
|
||||
|
||||
XTOL = 1e-12
|
||||
MAXFEV = 200
|
||||
ACC = 1e-6
|
||||
|
||||
SMALL = [
|
||||
("x^2-2", lambda x: x * x - 2.0, 0.0, 2.0, math.sqrt(2.0)),
|
||||
("cos-x", lambda x: math.cos(x) - x, 0.0, 1.0, 0.7390851332151607),
|
||||
("cubic", lambda x: x ** 3 - x - 2.0, 1.0, 2.0, 1.5213797068045676),
|
||||
("x^10-1", lambda x: x ** 10 - 1.0, 0.0, 1.3, 1.0),
|
||||
("flat", lambda x: math.exp(-x) * (x - 1.0), -1.0, 40.0, 1.0),
|
||||
("atan", lambda x: math.atan(x), -1.0, 3.0, 0.0),
|
||||
]
|
||||
|
||||
# the guards that are ALLOWED to claim a worst-case bound
|
||||
SOUND_GUARDS = ("bracket", "both")
|
||||
|
||||
|
||||
def _problems(dicts, chandrupatla=False):
|
||||
out = []
|
||||
for d in dicts:
|
||||
a, b = float(d["bracket"][0]), float(d["bracket"][1])
|
||||
args = () if chandrupatla else d.get("args", ())
|
||||
f = (lambda ff, aa: lambda x: ff(x, *aa))(d["f"], args)
|
||||
out.append((d["ID"], f, a, b, float(d["root"])))
|
||||
return out
|
||||
|
||||
|
||||
APS = _problems(_APS_TESTS_DICTS)
|
||||
CHA = _problems(_CHANDRUPATLA_TESTS_DICTS, chandrupatla=True)
|
||||
|
||||
|
||||
def two_x_bisection_bound(a, b):
|
||||
return 2 * math.ceil(math.log2(abs(b - a) / XTOL)) + 4
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# audited defects, pinned
|
||||
# --------------------------------------------------------------------------
|
||||
@pytest.mark.parametrize("guard", SOUND_GUARDS)
|
||||
def test_bug_pending_flag_gave_three_x_not_two_x(guard):
|
||||
"""The bracket guard cleared `pending` on the halving branch and then
|
||||
returned early, so TWO similarity steps were granted after every halving
|
||||
instead of one - a 3x bisection guarantee dressed up as 2x. Chandrupatla
|
||||
fun4.3 caught it: 139 evaluations against a bound of 114."""
|
||||
d = next(x for x in _CHANDRUPATLA_TESTS_DICTS if x["ID"] == "fun4.3")
|
||||
a, b = float(d["bracket"][0]), float(d["bracket"][1])
|
||||
x, n, conv = pure_root(d["f"], a, b, XTOL, MAXFEV,
|
||||
similarity="ratio", scale="derived", guard=guard)
|
||||
assert conv and abs(x - float(d["root"])) <= ACC
|
||||
assert n <= two_x_bisection_bound(a, b), f"{n} evaluations, 3x regression"
|
||||
|
||||
|
||||
def test_step_guard_makes_no_worst_case_claim():
|
||||
"""The step guard is cheap but unsound: a sequence creeping towards a
|
||||
non-root with halving steps never trips it. The suite must NOT assert a
|
||||
bound for it - this test exists so that nobody adds one later without
|
||||
noticing that the benchmark reports real violations."""
|
||||
violations = 0
|
||||
for _pid, f, a, b, root in CHA:
|
||||
x, n, conv = pure_root(f, a, b, XTOL, MAXFEV, similarity="ratio",
|
||||
scale="derived", guard="step")
|
||||
if conv and abs(x - root) <= ACC and n > two_x_bisection_bound(a, b):
|
||||
violations += 1
|
||||
assert violations > 0, ("the step guard no longer violates the 2x bound; "
|
||||
"if that is a real improvement, promote it to "
|
||||
"SOUND_GUARDS and prove it")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# contracts
|
||||
# --------------------------------------------------------------------------
|
||||
@pytest.mark.parametrize("guard", SOUND_GUARDS)
|
||||
def test_sound_guards_respect_the_bound_on_every_benchmark_problem(guard):
|
||||
"""The only worst-case claim the paper may make."""
|
||||
bad = []
|
||||
for pid, f, a, b, root in APS + CHA:
|
||||
x, n, conv = pure_root(f, a, b, XTOL, MAXFEV, similarity="ratio",
|
||||
scale="derived", guard=guard)
|
||||
if conv and abs(x - root) <= ACC and n > two_x_bisection_bound(a, b):
|
||||
bad.append((pid, n, two_x_bisection_bound(a, b)))
|
||||
assert not bad, f"{guard} exceeded the 2x bound on {bad[:5]}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sim", sorted(PURE_SIMS))
|
||||
@pytest.mark.parametrize("prob", SMALL, ids=[p[0] for p in SMALL])
|
||||
def test_derived_scale_with_a_sound_guard_always_converges(sim, prob):
|
||||
_, f, a, b, root = prob
|
||||
x, n, conv = pure_root(f, a, b, XTOL, 400, similarity=sim,
|
||||
scale="derived", guard="both")
|
||||
assert conv, f"pure.{sim}.both did not converge"
|
||||
assert abs(x - root) <= 1e-8
|
||||
|
||||
|
||||
@pytest.mark.parametrize("guard", GUARDS)
|
||||
def test_never_leaves_the_bracket(guard):
|
||||
for _pid, f, a, b, _root in SMALL:
|
||||
x, _n, _c = pure_root(f, a, b, XTOL, MAXFEV, guard=guard)
|
||||
assert min(a, b) - 1e-9 <= x <= max(a, b) + 1e-9
|
||||
|
||||
|
||||
def test_rejects_unknown_settings_loudly():
|
||||
f = lambda t: t * t - 2.0
|
||||
with pytest.raises(ValueError):
|
||||
pure_root(f, 0.0, 2.0, XTOL, MAXFEV, similarity="no-such")
|
||||
with pytest.raises(ValueError):
|
||||
pure_root(f, 0.0, 2.0, XTOL, MAXFEV, scale="no-such")
|
||||
with pytest.raises(ValueError):
|
||||
pure_root(f, 0.0, 2.0, XTOL, MAXFEV, guard="no-such")
|
||||
with pytest.raises(ValueError):
|
||||
pure_root(f, 3.0, 4.0, XTOL, MAXFEV) # no sign change
|
||||
|
||||
|
||||
def test_budget_is_never_exceeded():
|
||||
for budget in (0, 1, 2, 5, 30):
|
||||
for guard in GUARDS:
|
||||
_x, n, _c = pure_root(lambda t: math.exp(-t) * (t - 1.0),
|
||||
-1.0, 40.0, XTOL, budget, guard=guard)
|
||||
assert n <= budget
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# the competitor must be sound too, or the benchmark is worthless
|
||||
# --------------------------------------------------------------------------
|
||||
@pytest.mark.parametrize("n0", [0, 1, 2, 5])
|
||||
def test_itp_respects_its_own_published_guarantee(n0):
|
||||
"""ITP's theorem: it never needs more iterations than bisection plus n0.
|
||||
Checking the theorem is the strongest validation available for the
|
||||
implementation without a reference to diff against.
|
||||
|
||||
The permitted count is n_half + n0 + 1, not n_half + n0: the induction
|
||||
gives bracket_j <= 2^(n_max-j+1)*eps, so the width first drops to 2*eps at
|
||||
j = n_max, and the iterations actually run j = 0 .. n_max, which is
|
||||
n_max + 1 of them. Stating it as n_max flagged 3 of 154 problems by
|
||||
exactly one iteration - the signature of an indexing mistake in the test
|
||||
rather than of a broken implementation."""
|
||||
bad = []
|
||||
for pid, f, a, b, root in APS:
|
||||
if not a < b:
|
||||
continue
|
||||
allowed = math.ceil(math.log2((b - a) / XTOL)) + n0 + 1
|
||||
x, n, conv = itp(f, a, b, XTOL, 400, kappa1=0.2 / (b - a), n0=n0)
|
||||
if conv and abs(x - root) <= ACC and (n - 2) > allowed:
|
||||
bad.append((pid, n - 2, allowed))
|
||||
assert not bad, f"ITP broke its own bound on {bad[:5]}"
|
||||
|
||||
|
||||
def test_bisection_is_the_reference_ceiling_everyone_beats():
|
||||
"""Sanity check on the harness: if the new method were not actually
|
||||
solving anything, this is the comparison that would expose it."""
|
||||
tot_pure = tot_bis = 0
|
||||
for _pid, f, a, b, root in APS:
|
||||
_x, n1, c1 = pure_root(f, a, b, XTOL, MAXFEV, similarity="ratio",
|
||||
scale="derived", guard="both")
|
||||
_y, n2, c2 = bisection(f, a, b, XTOL, MAXFEV)
|
||||
tot_pure += n1 if c1 else MAXFEV
|
||||
tot_bis += n2 if c2 else MAXFEV
|
||||
assert tot_pure < tot_bis, (tot_pure, tot_bis)
|
||||
@@ -0,0 +1,107 @@
|
||||
"""Pins the regime findings of stage 7.
|
||||
|
||||
Three conclusions were drawn from that analysis and used to decide what to do
|
||||
next. Each is asserted here so that a re-run of the sweep which moves them
|
||||
fails the build instead of quietly invalidating the conclusion.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, ROOT)
|
||||
|
||||
SWEEP = os.path.join(ROOT, "sweep_full.json")
|
||||
pytestmark = pytest.mark.skipif(not os.path.exists(SWEEP),
|
||||
reason="sweep_full.json not present")
|
||||
|
||||
SCALES = ("start", "bracket", "derived", "derived3")
|
||||
MAXFEV = 200
|
||||
|
||||
|
||||
def load():
|
||||
with open(SWEEP, encoding="utf8") as fh:
|
||||
return {r["label"]: r for r in json.load(fh)}
|
||||
|
||||
|
||||
def cost(rows, sim, scale, pid):
|
||||
r = rows[f"pure.{sim}.{scale}.project"]
|
||||
d = r["APS"]["per"].get(pid) or r["CHA"]["per"].get(pid)
|
||||
return d[0] if d[1] else MAXFEV
|
||||
|
||||
|
||||
def all_pids(rows):
|
||||
r = rows["pure.ratio.derived3.project"]
|
||||
return list(r["APS"]["per"]) + list(r["CHA"]["per"])
|
||||
|
||||
|
||||
def test_the_scale_axis_has_little_headroom():
|
||||
"""5.3%. If this ever grows a lot, the scale is worth revisiting; as it
|
||||
stands it is not where the regimes live."""
|
||||
rows = load()
|
||||
pids = all_pids(rows)
|
||||
ours = sum(cost(rows, "ratio", "derived3", p) for p in pids)
|
||||
oracle = sum(min(cost(rows, "ratio", s, p) for s in SCALES) for p in pids)
|
||||
head = (ours - oracle) / ours
|
||||
assert 0.0 <= head < 0.10, head
|
||||
|
||||
|
||||
def test_cbrt_beats_us_consistently_on_the_fun4_family():
|
||||
"""The signature of a real regime rather than luck: the same function at
|
||||
five different parameters all prefer the same similarity, by a wide
|
||||
margin. This is the finding that keeps the line open."""
|
||||
rows = load()
|
||||
fam = [f"fun4.{i}" for i in range(1, 6)]
|
||||
better = 0
|
||||
for pid in fam:
|
||||
ours = cost(rows, "ratio", "derived3", pid)
|
||||
cb = cost(rows, "cbrt", "derived3", pid)
|
||||
if cb < ours:
|
||||
better += 1
|
||||
assert better == 5, f"only {better}/5 of the fun4 family prefer cbrt"
|
||||
tot_ours = sum(cost(rows, "ratio", "derived3", p) for p in fam)
|
||||
tot_cb = sum(cost(rows, "cbrt", "derived3", p) for p in fam)
|
||||
assert tot_cb < 0.6 * tot_ours, (tot_cb, tot_ours)
|
||||
|
||||
|
||||
def test_switching_by_benchmark_set_is_worse_than_not_switching():
|
||||
"""The finding that closed the obvious exploit: cbrt wins the fun3/4/5
|
||||
families but loses the rest of Chandrupatla, so a set-level switch costs
|
||||
more than a single fixed choice. Any future 'regime' proposal has to beat
|
||||
this, not merely beat one family."""
|
||||
rows = load()
|
||||
single = (rows["pure.ratio.derived3.project"]["APS"]["total"]
|
||||
+ rows["pure.ratio.derived3.project"]["CHA"]["total"])
|
||||
switched = (rows["pure.ratio.derived3.project"]["APS"]["total"]
|
||||
+ rows["pure.cbrt.derived3.project"]["CHA"]["total"])
|
||||
assert switched > single, (switched, single)
|
||||
|
||||
|
||||
def test_the_proposed_similarity_wins_most_problems_outright():
|
||||
"""155 of 199. The per-problem oracle over similarities is an upper
|
||||
bound on regime detection, not a method, and most of the field is already
|
||||
ours without it."""
|
||||
rows = load()
|
||||
pids = all_pids(rows)
|
||||
sims = ("ratio", "sqrt", "cbrt", "rational", "ratio_soft", "exp_abs",
|
||||
"rat_sq", "exp_sq", "cosine")
|
||||
wins = 0
|
||||
for p in pids:
|
||||
c = {s: cost(rows, s, "derived3", p) for s in sims}
|
||||
if c["ratio"] == min(c.values()):
|
||||
wins += 1
|
||||
assert wins >= 150, wins
|
||||
|
||||
|
||||
def test_stage7_runs():
|
||||
"""The analysis script itself must still execute against the current
|
||||
sweep file."""
|
||||
import subprocess
|
||||
r = subprocess.run([sys.executable, "stage7_regimes.py"], cwd=ROOT,
|
||||
capture_output=True, text=True, timeout=600)
|
||||
assert r.returncode == 0, r.stderr[-800:]
|
||||
assert "SETTLING THE TWO SUSPICIONS" in r.stdout
|
||||
@@ -0,0 +1,77 @@
|
||||
"""A prediction registered before the run, and falsified by it.
|
||||
|
||||
The hypothesis was that a method which wins in some places and loses in others
|
||||
must have a regime where it wins outright, and that the regime is where the
|
||||
quantity the constant approximates CHANGES during the run - because one
|
||||
constant cannot be right at two different noise levels while a measured value
|
||||
tracks.
|
||||
|
||||
It was stated before measuring, with the falsification condition attached: the
|
||||
measured rule had to lose every constant-noise cell and win every
|
||||
varying-noise cell. It lost all twelve.
|
||||
|
||||
The first version of the test could not have confirmed it either way - noise
|
||||
reduction was scored on the second half of the run, by which time every ramp
|
||||
and step had already arrived, so the metric never saw a transition. That flaw
|
||||
was found and fixed before the conclusion was drawn, and the conclusion did
|
||||
not change.
|
||||
|
||||
This file exists so the line is not reopened without new evidence.
|
||||
"""
|
||||
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 anc.rules import Fixed, Measured
|
||||
from stage20_prediction import (FLAT_5, FLAT_30, STEP_25, SWING_25, sig_cabin,
|
||||
sig_tonal, score)
|
||||
|
||||
SEEDS = 3
|
||||
|
||||
|
||||
def test_the_metric_window_sees_the_transition():
|
||||
"""The flaw that would have produced a false falsification. A varying
|
||||
profile must not score the same as a constant one at its final level."""
|
||||
varying = score(Fixed(0.1), sig_tonal, SWING_25, seeds=SEEDS)
|
||||
final_level = score(Fixed(0.1), sig_tonal, FLAT_5, seeds=SEEDS)
|
||||
assert varying > final_level + 2.0, (varying, final_level)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("signal", [sig_tonal, sig_cabin],
|
||||
ids=["tonal", "cabin"])
|
||||
@pytest.mark.parametrize("profile,kind", [(FLAT_30, "control"),
|
||||
(FLAT_5, "control"),
|
||||
(SWING_25, "prediction"),
|
||||
(STEP_25, "prediction")],
|
||||
ids=["flat30", "flat5", "ramp", "step"])
|
||||
def test_an_oracle_constant_beats_the_measured_rule_everywhere(signal, profile,
|
||||
kind):
|
||||
"""The registered prediction required the measured rule to WIN the two
|
||||
'prediction' cells. It does not, on either signal. A single constant,
|
||||
chosen with knowledge of the scenario, is ahead even when the noise level
|
||||
swings by 25 dB inside the run."""
|
||||
ms = score(Measured(), signal, profile, seeds=SEEDS)
|
||||
best = max(score(Fixed(mu), signal, profile, seeds=SEEDS)
|
||||
for mu in (0.05, 0.1, 0.2, 0.5, 1.0))
|
||||
assert best > ms, (kind, best, ms)
|
||||
|
||||
|
||||
def test_the_conclusion_this_licenses():
|
||||
"""Winning somewhere and losing elsewhere does not imply a regime of
|
||||
outright victory exists; it is equally consistent with a middling
|
||||
generalist. On this evidence there is no such regime, and the claim the
|
||||
work may make is the scoped one in test_modern_repairs.py."""
|
||||
losses = 0
|
||||
for signal in (sig_tonal, sig_cabin):
|
||||
for profile in (FLAT_30, FLAT_5, SWING_25, STEP_25):
|
||||
ms = score(Measured(), signal, profile, seeds=SEEDS)
|
||||
best = max(score(Fixed(mu), signal, profile, seeds=SEEDS)
|
||||
for mu in (0.05, 0.2, 1.0))
|
||||
losses += ms < best
|
||||
assert losses == 8, losses
|
||||
@@ -0,0 +1,95 @@
|
||||
"""The relaxation parameter: the fourth domain, and the fourth time the same
|
||||
pattern appears.
|
||||
|
||||
omega in SOR is the oldest hand-tuned step length in numerical computing. The
|
||||
similarity form - omega = 1 + cos between successive corrections - lands
|
||||
exactly in the admissible interval [1, 2] with nothing chosen, and it beats
|
||||
Gauss-Seidel five-fold. It loses to a swept omega two-fold.
|
||||
|
||||
A second map, derived from the theory rather than from the geometry, was tried
|
||||
and is worse than the first. Both are pinned: the working one so it is not
|
||||
lost, the refuted one so it is not retried.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from relax.sor import (ContractionOmega, Fixed, SimilarityOmega, poisson_rhs,
|
||||
residual, solve, sweep_gs)
|
||||
from stage23_relaxation import anisotropic, convection, theory_check
|
||||
|
||||
N = 14
|
||||
F = poisson_rhs(N)
|
||||
W_OPT = 2.0 / (1.0 + math.sin(math.pi / (N + 1)))
|
||||
|
||||
|
||||
def test_the_sweep_reproduces_the_sor_theory():
|
||||
"""The instrument before the experiment: for the model problem the SOR
|
||||
spectral radius is exactly omega* - 1."""
|
||||
_w, theory, measured = theory_check(16)
|
||||
assert abs(theory - measured) < 0.02, (theory, measured)
|
||||
|
||||
|
||||
def test_the_similarity_omega_lands_in_the_admissible_interval():
|
||||
"""Range of the similarity and convergence condition of the method
|
||||
coincide, which is why nothing had to be clipped by hand."""
|
||||
r = SimilarityOmega()
|
||||
r.reset()
|
||||
d1 = np.array([1.0, 2.0, 3.0])
|
||||
assert r.next_omega(None, None) == 1.0
|
||||
assert r.next_omega(d1, d1) == pytest.approx(2.0) # identical
|
||||
assert r.next_omega(d1, -d1) == pytest.approx(1.0) # opposed
|
||||
assert r.next_omega(np.array([1.0, 0.0, 0.0]),
|
||||
np.array([0.0, 1.0, 0.0])) == pytest.approx(1.0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("label,coeff", [
|
||||
("poisson", None), ("aniso", anisotropic(N, 0.1))])
|
||||
def test_similarity_beats_gauss_seidel_by_a_wide_margin(label, coeff):
|
||||
gs, _ = solve(F, Fixed(1.0), coeff=coeff)
|
||||
sim, _ = solve(F, SimilarityOmega(), coeff=coeff)
|
||||
assert sim * 3 < gs, (label, sim, gs)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("label,coeff", [
|
||||
("poisson", None), ("aniso", anisotropic(N, 0.1))])
|
||||
def test_and_loses_to_a_swept_omega(label, coeff):
|
||||
"""The other half, stated so the claim is never inflated."""
|
||||
sim, _ = solve(F, SimilarityOmega(), coeff=coeff)
|
||||
best = min(solve(F, Fixed(w), coeff=coeff)[0]
|
||||
for w in (1.2, 1.4, 1.6, 1.8, W_OPT))
|
||||
assert best < sim, (label, best, sim)
|
||||
|
||||
|
||||
def test_the_similarity_rule_over_relaxes():
|
||||
"""The diagnosis, not just the outcome: the cosine between successive SOR
|
||||
corrections sits near 1, so 1 + cos saturates near 2 and the rule picks a
|
||||
larger omega than the optimum."""
|
||||
_sim, mean_w = solve(F, SimilarityOmega())
|
||||
assert mean_w > W_OPT, (mean_w, W_OPT)
|
||||
|
||||
|
||||
def test_the_contraction_map_is_refuted_and_worse():
|
||||
"""The second attempt, pinned so it is not tried again. Undamped
|
||||
feedback pins omega at the limit from the first sweep."""
|
||||
sim, _ = solve(F, SimilarityOmega())
|
||||
con, mean_w = solve(F, ContractionOmega())
|
||||
assert con > sim, (con, sim)
|
||||
assert mean_w > 1.9, mean_w
|
||||
|
||||
|
||||
def test_convection_is_where_it_actually_hurts():
|
||||
"""On a strongly non-symmetric problem Gauss-Seidel is already fast and
|
||||
the rule makes it slower - the one case where the similarity form is not
|
||||
merely second but harmful."""
|
||||
coeff = convection(N, 10.0)
|
||||
gs, _ = solve(F, Fixed(1.0), coeff=coeff)
|
||||
sim, _ = solve(F, SimilarityOmega(), coeff=coeff)
|
||||
assert sim > gs, (sim, gs)
|
||||
@@ -0,0 +1,82 @@
|
||||
"""The credit safeguard applied to step rules it was not designed for.
|
||||
|
||||
This is the finding that decides what the work is about. As a
|
||||
one-dimensional root finder the similarity method is subsumed by
|
||||
Chandrupatla 1997 on every axis measured. The safeguard is not: it is
|
||||
independent of the similarity framing and it converts unsafe interpolating
|
||||
rules into safe ones without a constant and without a cost.
|
||||
"""
|
||||
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.baselines import chandrupatla
|
||||
from stage14_is_anything_reusable import RULES, load, measure
|
||||
|
||||
PROBS = load()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name,rule", RULES, ids=[n for n, _ in RULES])
|
||||
def test_the_guard_makes_an_unsafe_rule_solve_everything(name, rule):
|
||||
"""Every rule, guarded, must reach the 193 of 199 that is the most any
|
||||
method here achieves - including bisection and Chandrupatla."""
|
||||
_t_raw, s_raw, _w_raw = measure(rule, False, PROBS)
|
||||
_t_g, s_g, _w_g = measure(rule, True, PROBS)
|
||||
assert s_g == 193, (name, s_g)
|
||||
assert s_g > s_raw, (name, s_raw, s_g)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name,rule", RULES, ids=[n for n, _ in RULES])
|
||||
def test_the_guard_is_free_or_better(name, rule):
|
||||
"""Safety normally costs evaluations. Here it saves them, because the
|
||||
failures it prevents were being charged the full budget."""
|
||||
t_raw, _s, _w = measure(rule, False, PROBS)
|
||||
t_g, _s2, _w2 = measure(rule, True, PROBS)
|
||||
assert t_g < t_raw, (name, t_raw, t_g)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name,rule", RULES, ids=[n for n, _ in RULES])
|
||||
def test_the_guarded_worst_case_is_at_bisection_level(name, rule):
|
||||
"""The point of a safeguard. Bisection's worst problem here costs 82
|
||||
evaluations; every guarded rule must be in that neighbourhood, where the
|
||||
raw rules run to 135, 178 and the 200-evaluation cap."""
|
||||
_t, _s, w_g = measure(rule, True, PROBS)
|
||||
assert w_g <= 90, (name, w_g)
|
||||
|
||||
|
||||
def test_the_similarity_method_is_subsumed_by_chandrupatla():
|
||||
"""Stated as a test so the paper cannot drift back to claiming otherwise.
|
||||
Chandrupatla is better on total, on the worst case, and on stability,
|
||||
with the same number of constants (none) and the same 193 solved."""
|
||||
from simroot.pure import pure_root
|
||||
XTOL, MAXFEV, ACC = 1e-12, 200, 1e-6
|
||||
ours_t = ours_w = ours_s = 0
|
||||
cha_t = cha_w = cha_s = 0
|
||||
for f, a, b, root in PROBS:
|
||||
for meth, which in ((lambda: pure_root(f, a, b, XTOL, MAXFEV,
|
||||
similarity="ratio",
|
||||
scale="derived3",
|
||||
guard="credit"), "o"),
|
||||
(lambda: chandrupatla(f, a, b, XTOL, MAXFEV), "c")):
|
||||
try:
|
||||
x, n, conv = meth()
|
||||
except Exception:
|
||||
x, n, conv = float("nan"), MAXFEV, False
|
||||
ok = bool(conv and math.isfinite(x) and abs(x - root) <= ACC)
|
||||
if which == "o":
|
||||
ours_t += n if ok else MAXFEV
|
||||
ours_s += ok
|
||||
ours_w = max(ours_w, n) if ok else ours_w
|
||||
else:
|
||||
cha_t += n if ok else MAXFEV
|
||||
cha_s += ok
|
||||
cha_w = max(cha_w, n) if ok else cha_w
|
||||
assert cha_s == ours_s == 193
|
||||
assert cha_t < ours_t, (cha_t, ours_t)
|
||||
assert cha_w <= ours_w, (cha_w, ours_w)
|
||||
@@ -0,0 +1,177 @@
|
||||
"""Pins the corrections the revision made.
|
||||
|
||||
Each of these replaced a statement in the paper that was wrong or too broad.
|
||||
They are asserted here so a later edit cannot quietly restore the old claim.
|
||||
"""
|
||||
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 PURE_SIMS, pure_root
|
||||
from stage5_three_point import PROBLEMS, order_of
|
||||
|
||||
R2 = math.sqrt(2.0)
|
||||
F = lambda x: x * x - 2.0
|
||||
XTOL, MAXFEV = 1e-12, 200
|
||||
|
||||
|
||||
def iterate(S, n, sigma=2.0, x0=0.0, xfar=2.0):
|
||||
x, errs = x0, []
|
||||
for _ in range(n):
|
||||
x = x + (1.0 - S(F(x) / sigma)) * (xfar - x)
|
||||
errs.append(abs(x - R2))
|
||||
return errs
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# the naming contradiction: the proposed similarity is the tent, and its
|
||||
# degeneracy is a property of the CHOSEN scale only
|
||||
# --------------------------------------------------------------------------
|
||||
def test_the_tent_is_captured_under_the_chosen_scale():
|
||||
"""Table 1's last row. Not a slow rate - capture at the far end on the
|
||||
first step."""
|
||||
e = iterate(PURE_SIMS["ratio"], 200)
|
||||
assert abs(e[0] - e[-1]) < 1e-15, (e[0], e[-1])
|
||||
assert abs(e[0] - 0.5858) < 1e-3, e[0]
|
||||
|
||||
|
||||
def test_the_tent_is_the_best_similarity_under_the_derived_scale():
|
||||
"""The same function, the other scale. If these two tests ever agree in
|
||||
sign, the paper's central thesis - that the content is in the scale - has
|
||||
been broken."""
|
||||
tot = {}
|
||||
for sim in PURE_SIMS:
|
||||
s = 0
|
||||
for _n, f, a, b, root in PROBLEMS:
|
||||
x, n, conv = pure_root(f, a, b, XTOL, MAXFEV, similarity=sim,
|
||||
scale="derived3", guard="project")
|
||||
s += n if (conv and abs(x - root) <= 1e-8) else MAXFEV
|
||||
tot[sim] = s
|
||||
assert min(tot, key=lambda k: tot[k]) == "ratio", tot
|
||||
|
||||
|
||||
def test_the_tent_never_clips_under_the_derived_scale():
|
||||
"""Why the degeneracy disappears: |f|/sigma stays below 1, so the
|
||||
truncation in max(0, 1-|z|) never engages."""
|
||||
from simroot.pure import _slope3
|
||||
a, b = 0.0, 2.0
|
||||
fa, fb = F(a), F(b)
|
||||
A, FA, B = a, fa, b
|
||||
x, fx, xp, fp = a, fa, b, fb
|
||||
xp2 = fp2 = None
|
||||
clipped = steps = 0
|
||||
for _ in range(40):
|
||||
if abs(B - A) <= XTOL:
|
||||
break
|
||||
far = B if abs(B - x) >= abs(A - x) else A
|
||||
sc = abs(_slope3(x, fx, xp, fp, xp2, fp2)) * abs(far - x)
|
||||
if sc == 0.0:
|
||||
break
|
||||
steps += 1
|
||||
clipped += abs(fx) / sc >= 1.0
|
||||
xn = x + min(1.0, abs(fx) / sc) * (far - x)
|
||||
if not (min(A, B) < xn < max(A, B)):
|
||||
xn = 0.5 * (A + B)
|
||||
fn = F(xn)
|
||||
xp2, fp2, xp, fp, x, fx = xp, fp, x, fx, xn, fn
|
||||
if FA * fn < 0.0:
|
||||
B = xn
|
||||
else:
|
||||
A, FA = xn, fn
|
||||
assert steps > 0
|
||||
assert clipped == 0, f"{clipped} of {steps} steps clipped"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Proposition 1 is specific; Proposition 1' locates the boundary
|
||||
# --------------------------------------------------------------------------
|
||||
def test_only_the_ratio_form_reproduces_regula_falsi():
|
||||
a, b = 1.0, 4.0
|
||||
fa, fb = F(a), F(b)
|
||||
falsi = (a * fb - b * fa) / (fb - fa)
|
||||
matches = []
|
||||
for name, S in (("rational", PURE_SIMS["rational"]),
|
||||
("exp_abs", PURE_SIMS["exp_abs"]),
|
||||
("rat_sq", PURE_SIMS["rat_sq"]),
|
||||
("cosine", PURE_SIMS["cosine"]),
|
||||
("ratio", PURE_SIMS["ratio"])):
|
||||
pt = a + (1.0 - S(F(a) / abs(fb))) * (b - a)
|
||||
if abs(pt - falsi) < 1e-12:
|
||||
matches.append(name)
|
||||
assert matches == ["rational"], matches
|
||||
|
||||
|
||||
def test_the_bracket_end_difference_gives_regula_falsi_exactly():
|
||||
"""Proposition 1'. This is why 'most recent' in the definition of the
|
||||
divided difference is load-bearing."""
|
||||
a, b = 1.0, 4.0
|
||||
fa, fb = F(a), F(b)
|
||||
falsi = (a * fb - b * fa) / (fb - fa)
|
||||
sigma = abs((fb - fa) / (b - a)) * abs(b - a)
|
||||
pt = a + min(1.0, abs(fa) / sigma) * (b - a)
|
||||
assert pt == falsi
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Table 1's c = 0 rows: a sublinear law with a predicted coefficient
|
||||
# --------------------------------------------------------------------------
|
||||
@pytest.mark.parametrize("sim,want", [("rat_sq", 0.8536), ("exp_sq", 0.8536),
|
||||
("cosine", 1.7071)])
|
||||
def test_the_quadratic_similarities_are_sublinear_with_a_predicted_constant(sim, want):
|
||||
S = PURE_SIMS[sim]
|
||||
h = 1e-4
|
||||
c2 = (1.0 - S(h)) / (h * h)
|
||||
fp, D, sigma = 2.0 * R2, 2.0 - R2, 2.0
|
||||
predicted = sigma * sigma / (c2 * fp * fp * D)
|
||||
assert abs(predicted - want) < 1e-3, predicted
|
||||
e = iterate(S, 4001)
|
||||
assert abs(4000 * e[4000] - predicted) < 0.02, (4000 * e[4000], predicted)
|
||||
|
||||
|
||||
def test_there_is_no_rate_to_measure_for_those_rows():
|
||||
"""The old table reported 0.93-0.94 as a 'measured rate'; the ratio
|
||||
creeps to 1, so that number was the window's end, not a rate."""
|
||||
e = iterate(PURE_SIMS["rat_sq"], 4001)
|
||||
ratios = [e[n] / e[n - 1] for n in (10, 100, 1000, 4000)]
|
||||
assert ratios == sorted(ratios), ratios
|
||||
assert ratios[0] < 0.94 < ratios[-1]
|
||||
assert ratios[-1] > 0.999
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# the order is 2, not Muller's 1.84
|
||||
# --------------------------------------------------------------------------
|
||||
def test_the_three_point_order_is_two():
|
||||
ps = [order_of(lambda f_, a_, b_, xt, mx: pure_root(
|
||||
f_, a_, b_, XTOL, MAXFEV, scale="derived3", guard="project"),
|
||||
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 = sorted(ps)[len(ps) // 2]
|
||||
assert abs(med - 2.0) < 0.1, med
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# the two evaluation-count conventions must both be reported and must differ
|
||||
# in the direction the paper says
|
||||
# --------------------------------------------------------------------------
|
||||
@pytest.mark.skipif(not os.path.exists(os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||
"sweep_full.json")), reason="sweep not present")
|
||||
def test_charged_totals_are_what_section_5_reports():
|
||||
import json
|
||||
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
with open(os.path.join(root, "sweep_full.json"), encoding="utf8") as fh:
|
||||
rows = {r["label"]: r for r in json.load(fh)}
|
||||
# brent is now run at the better of ours and scipy.brentq per problem,
|
||||
# which is why it is 6988 rather than the 6991 of our implementation alone
|
||||
for lbl, want in (("pure.ratio.derived3.project", 6026),
|
||||
("brent", 6988), ("bisection", 10620),
|
||||
("chandrupatla", 5208)):
|
||||
got = rows[lbl]["APS"]["total"] + rows[lbl]["CHA"]["total"]
|
||||
assert got == want, (lbl, got, want)
|
||||
@@ -0,0 +1,105 @@
|
||||
"""The similarity family for the NLMS step size, and what it settled.
|
||||
|
||||
Two corrections came from outside and both were right:
|
||||
|
||||
1. mu should be a SIMILARITY, not a variance estimate. The filter can only
|
||||
remove what correlates with its input, so the quantity is the cosine
|
||||
between the error and the filtered reference. It beats the variance
|
||||
formulation on eleven of twelve cells.
|
||||
|
||||
2. Testing one similarity function is as incomplete here as it would have
|
||||
been in the root-finding work. The whole family must be run, and when it
|
||||
is, the best member is not the cosine.
|
||||
|
||||
And the law of the first domain - that the slope of (1 - S) at the origin
|
||||
decides, not the shape of S - orders this family too, monotonically and
|
||||
without exception. That is a prediction made in one domain and confirmed in
|
||||
another.
|
||||
|
||||
What it did not settle: even the best member loses to a constant tuned per
|
||||
scenario, on all twelve cells.
|
||||
"""
|
||||
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 anc.rules import CosineSimilarity, Fixed, Measured
|
||||
from simroot.pure import PURE_SIMS
|
||||
from stage20_prediction import FLAT_15, FLAT_30, STEP_25, sig_cabin, sig_tonal, \
|
||||
score
|
||||
|
||||
SEEDS = 3
|
||||
CELLS = [(sig_tonal, FLAT_30), (sig_tonal, STEP_25),
|
||||
(sig_cabin, FLAT_15), (sig_cabin, STEP_25)]
|
||||
|
||||
|
||||
def make(name):
|
||||
r = CosineSimilarity()
|
||||
r.S = None if name is None else PURE_SIMS[name]
|
||||
return r
|
||||
|
||||
|
||||
def mean(rule):
|
||||
vals = [score(rule, gen, prof, seeds=SEEDS) for gen, prof in CELLS]
|
||||
return sum(vals) / len(vals)
|
||||
|
||||
|
||||
def test_the_similarity_formulation_beats_the_variance_one():
|
||||
"""Correction 1. mu as 'how much of the error resembles what the filter
|
||||
can remove' against mu as 'one minus the noise-power fraction'.
|
||||
|
||||
Stated carefully, because the first version of this test was not: what
|
||||
beats the variance rule on eleven of twelve cells is the best member of
|
||||
the FAMILY, not the raw cosine. The raw cosine can lose to it - on this
|
||||
subset it does, 14.04 against 14.32 - which is precisely why correction 2
|
||||
(run the whole family) was needed before the claim could be made at all.
|
||||
"""
|
||||
best_member = max(mean(make(n)) for n in (None, "ratio", "exp_abs",
|
||||
"rational", "ratio_soft",
|
||||
"sqrt", "cbrt"))
|
||||
assert best_member > mean(Measured()), best_member
|
||||
|
||||
|
||||
def test_the_raw_cosine_is_not_the_best_member():
|
||||
"""Correction 2. A single member is not the family: the identity
|
||||
S(d) = 1 - d is beaten by members with a steeper approach to 1."""
|
||||
raw = mean(make(None))
|
||||
best = max(mean(make(n)) for n in ("sqrt", "cbrt", "ratio_soft"))
|
||||
assert best > raw, (best, raw)
|
||||
|
||||
|
||||
def test_two_code_paths_for_the_same_function_agree():
|
||||
"""S = None takes mu = |cos| directly; S = ratio computes
|
||||
max(0, 1 - d) with d = 1 - |cos|. They are the same function on [0,1]
|
||||
and reach it by different code, so they must score identically."""
|
||||
a = mean(make(None))
|
||||
b = mean(make("ratio"))
|
||||
assert abs(a - b) < 1e-9, (a, b)
|
||||
|
||||
|
||||
def test_the_law_of_the_first_domain_orders_this_family_too():
|
||||
"""The prediction carried across domains: slope of (1 - S) at 0 decides.
|
||||
Every slope-1 member must beat every slope-0 member."""
|
||||
slope1 = [mean(make(n)) for n in ("ratio", "exp_abs", "rational")]
|
||||
slope0 = [mean(make(n)) for n in ("rat_sq", "exp_sq", "cosine")]
|
||||
assert min(slope1) > max(slope0), (slope1, slope0)
|
||||
|
||||
|
||||
def test_and_it_still_loses_to_a_tuned_constant():
|
||||
"""The half that did not work, pinned so the claim is never overstated.
|
||||
The competitor knows the scenario; no deployable controller does - and it
|
||||
is still ahead."""
|
||||
losses = 0
|
||||
for gen, prof in CELLS:
|
||||
best_member = max(score(make(n), gen, prof, seeds=SEEDS)
|
||||
for n in (None, "cbrt", "sqrt", "ratio"))
|
||||
oracle = max(score(Fixed(mu), gen, prof, seeds=SEEDS)
|
||||
for mu in (0.05, 0.1, 0.2, 0.5, 1.0))
|
||||
losses += best_member < oracle
|
||||
assert losses == len(CELLS), losses
|
||||
@@ -0,0 +1,234 @@
|
||||
"""Regression suite for the similarity root finders and the baselines.
|
||||
|
||||
Every bug found in a static audit becomes a test here (project rule), so the
|
||||
four audited defects are pinned first, then the behavioural contracts.
|
||||
"""
|
||||
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.baselines import (BASELINES, bisection, brent, illinois, itp,
|
||||
regula_falsi, ridders, toms748)
|
||||
from simroot.sim import SIMILARITIES, sim_root
|
||||
from stage0b_pure_similarity import SIMS_01, pure_similarity_step
|
||||
from stage0d_derived_similarity import FAST_SIMS, derived_step, pure_step
|
||||
|
||||
XTOL = 1e-12
|
||||
MAXFEV = 300
|
||||
|
||||
PROBLEMS = [
|
||||
("x^2-2", lambda x: x * x - 2.0, 0.0, 2.0, math.sqrt(2.0)),
|
||||
("cos-x", lambda x: math.cos(x) - x, 0.0, 1.0, 0.7390851332151607),
|
||||
("exp-2", lambda x: math.exp(x) - 2.0, 0.0, 2.0, math.log(2.0)),
|
||||
("cubic", lambda x: x ** 3 - x - 2.0, 1.0, 2.0, 1.5213797068045676),
|
||||
("x^10-1", lambda x: x ** 10 - 1.0, 0.0, 1.3, 1.0),
|
||||
("flat", lambda x: math.exp(-x) * (x - 1.0), -1.0, 40.0, 1.0),
|
||||
("atan", lambda x: math.atan(x), -1.0, 3.0, 0.0),
|
||||
]
|
||||
|
||||
GUARDED_SIM = [(s, p) for s in SIMILARITIES for p in ("point", "pair", "pair2d")]
|
||||
|
||||
# Interpolation-only methods: no bisection safeguard, hence no worst-case
|
||||
# guarantee. They are in the suite as references, not as claims.
|
||||
NO_GUARANTEE = {"regula_falsi", "pegasus"}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# audited defects, pinned
|
||||
# --------------------------------------------------------------------------
|
||||
@pytest.mark.parametrize("name", sorted(BASELINES))
|
||||
def test_bug1_exhausted_budget_is_an_honest_failure(name):
|
||||
"""A budget that runs out on the very first evaluation must return
|
||||
converged=False, never raise UnboundLocalError."""
|
||||
method = BASELINES[name]
|
||||
for budget in (0, 1, 2):
|
||||
x, n, conv = method(lambda t: t * t - 2.0, 0.0, 2.0, XTOL, budget)
|
||||
assert n <= budget
|
||||
assert math.isfinite(x)
|
||||
assert conv is False or n <= budget
|
||||
|
||||
|
||||
def test_bug2_itp_stops_at_the_same_bracket_as_everyone_else():
|
||||
"""ITP is natively written against a half-width epsilon. If xtol were fed
|
||||
in as epsilon, ITP would stop at twice the bracket width of its
|
||||
competitors and win the benchmark on the stopping rule. The final
|
||||
accuracy must be comparable to the other methods, not 2x looser."""
|
||||
f = lambda t: t * t - 2.0
|
||||
root = math.sqrt(2.0)
|
||||
xtol = 1e-8
|
||||
x_itp, _, c1 = itp(f, 0.0, 2.0, xtol, MAXFEV)
|
||||
x_bis, _, c2 = bisection(f, 0.0, 2.0, xtol, MAXFEV)
|
||||
assert c1 and c2
|
||||
assert abs(x_itp - root) <= xtol
|
||||
assert abs(x_bis - root) <= xtol
|
||||
|
||||
|
||||
def test_bug3_itp_rejects_a_reversed_bracket_loudly():
|
||||
with pytest.raises(ValueError):
|
||||
itp(lambda t: t * t - 2.0, 2.0, 0.0, XTOL, MAXFEV)
|
||||
|
||||
|
||||
def test_bug4_stagnation_is_not_reported_as_success():
|
||||
"""sim_root used to return converged=True when it could no longer split
|
||||
the bracket, regardless of whether the tolerance had been met. A
|
||||
tolerance far below what float can represent on this bracket must not
|
||||
come back as a success."""
|
||||
f = lambda t: math.exp(-t) * (t - 1.0)
|
||||
x, n, conv = sim_root(f, -1.0, 40.0, 1e-300, 400,
|
||||
similarity="inv", prototypes="pair2d", guard=True)
|
||||
if not conv:
|
||||
return # honest failure - fine
|
||||
assert abs(x - 1.0) <= 1e-8, "claimed success while stagnated"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# behavioural contracts
|
||||
# --------------------------------------------------------------------------
|
||||
@pytest.mark.parametrize("name", sorted(BASELINES))
|
||||
@pytest.mark.parametrize("prob", PROBLEMS, ids=[p[0] for p in PROBLEMS])
|
||||
def test_baselines_find_the_root(name, prob):
|
||||
_, f, a, b, root = prob
|
||||
x, n, conv = BASELINES[name](f, a, b, XTOL, MAXFEV)
|
||||
if name in NO_GUARANTEE:
|
||||
# These carry no bisection safeguard and therefore no worst-case
|
||||
# bound; the literature says so and the flat problem shows it. What
|
||||
# is required of them is only that they stay honest about it.
|
||||
assert n <= MAXFEV
|
||||
assert math.isfinite(x)
|
||||
if not conv:
|
||||
return
|
||||
assert conv, f"{name} did not converge"
|
||||
assert abs(x - root) <= 1e-8, f"{name} converged to the wrong point"
|
||||
assert n <= MAXFEV
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sim,proto", GUARDED_SIM,
|
||||
ids=[f"{s}.{p}" for s, p in GUARDED_SIM])
|
||||
@pytest.mark.parametrize("prob", PROBLEMS, ids=[p[0] for p in PROBLEMS])
|
||||
def test_guarded_sim_never_fails(sim, proto, prob):
|
||||
"""The parameter-free safeguard is the whole reason it exists: with it on,
|
||||
no similarity function and no prototype level may fail on any problem."""
|
||||
_, f, a, b, root = prob
|
||||
x, n, conv = sim_root(f, a, b, XTOL, MAXFEV,
|
||||
similarity=sim, prototypes=proto, guard=True)
|
||||
assert conv, f"SIM.{proto}.{sim} did not converge"
|
||||
assert abs(x - root) <= 1e-8
|
||||
assert n <= MAXFEV
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sim,proto", GUARDED_SIM,
|
||||
ids=[f"{s}.{p}" for s, p in GUARDED_SIM])
|
||||
@pytest.mark.parametrize("prob", PROBLEMS, ids=[p[0] for p in PROBLEMS])
|
||||
def test_guard_bounds_the_worst_case_by_twice_bisection(sim, proto, prob):
|
||||
"""The safeguard pays back one bisection step for every similarity step
|
||||
that failed to halve the bracket, so the worst case is bounded by twice
|
||||
the BISECTION BOUND.
|
||||
|
||||
The comparison must be against the bound ceil(log2(w/xtol)), not against
|
||||
what bisection happened to spend on this problem: on atan over [-1, 3]
|
||||
bisection lands exactly on the root in 4 evaluations, which is luck, not
|
||||
a worst case, and comparing against it asserts something that was never
|
||||
claimed."""
|
||||
_, f, a, b, _ = prob
|
||||
n_bound = math.ceil(math.log2(abs(b - a) / XTOL))
|
||||
_, n_sim, conv = sim_root(f, a, b, XTOL, MAXFEV,
|
||||
similarity=sim, prototypes=proto, guard=True)
|
||||
assert conv
|
||||
assert n_sim <= 2 * n_bound + 4, f"{n_sim} > 2*{n_bound}+4"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("prob", PROBLEMS, ids=[p[0] for p in PROBLEMS])
|
||||
def test_every_method_stays_inside_the_initial_bracket(prob):
|
||||
"""A bracketing method must never return a point outside the bracket it
|
||||
was given, converged or not."""
|
||||
_, f, a, b, _ = prob
|
||||
lo, hi = min(a, b), max(a, b)
|
||||
for name, m in BASELINES.items():
|
||||
x, _, _ = m(f, a, b, XTOL, MAXFEV)
|
||||
assert lo - 1e-9 <= x <= hi + 1e-9, f"{name} left the bracket"
|
||||
for sim, proto in GUARDED_SIM:
|
||||
x, _, _ = sim_root(f, a, b, XTOL, MAXFEV, similarity=sim,
|
||||
prototypes=proto, guard=True)
|
||||
assert lo - 1e-9 <= x <= hi + 1e-9, f"SIM.{proto}.{sim} left the bracket"
|
||||
|
||||
|
||||
def test_the_pure_two_point_similarity_step_is_regula_falsi_exactly():
|
||||
"""The stage-0 identity, as an executable claim rather than an assertion
|
||||
in prose: normalising the similarity by the two bracket ends reproduces
|
||||
the false-position formula bit for bit."""
|
||||
for fa, fb in ((-5.436, 1.6e-16), (-1.0, 3.0), (2.5, -0.125), (-1e-9, 7.0)):
|
||||
a, b = -1.0, 40.0
|
||||
s_a = abs(fb) / (abs(fa) + abs(fb))
|
||||
s_b = abs(fa) / (abs(fa) + abs(fb))
|
||||
x_sim = s_a * a + s_b * b
|
||||
x_falsi = (a * fb - b * fa) / (fb - fa)
|
||||
assert abs(x_sim - x_falsi) <= 1e-12 * max(1.0, abs(x_falsi))
|
||||
|
||||
|
||||
def test_similarity_shape_predicts_the_contraction_rate():
|
||||
"""The stage-0c law: rate = |1 - c*|f'(r)|*D/scale| with c the slope of
|
||||
(1 - S) at zero. Pinned for the two linear-approach similarities, where
|
||||
it was measured to four digits."""
|
||||
f = lambda t: t * t - 2.0
|
||||
root, fp, D, scale = math.sqrt(2.0), 2.0 * math.sqrt(2.0), 2.0 - math.sqrt(2.0), 2.0
|
||||
predicted = abs(1.0 - 1.0 * fp * D / scale)
|
||||
for sim in ("rational", "exp_abs"):
|
||||
S = SIMS_01[sim]
|
||||
x, errs = 0.0, []
|
||||
for _ in range(14):
|
||||
x = x + (1.0 - S(abs(f(x)) / scale)) * (2.0 - x)
|
||||
errs.append(abs(x - root))
|
||||
win = [errs[i + 1] / errs[i] for i in range(len(errs) - 1) if errs[i] > 1e-11]
|
||||
measured = sum(win[-3:]) / len(win[-3:])
|
||||
assert abs(measured - predicted) < 1e-3, (sim, measured, predicted)
|
||||
|
||||
|
||||
def test_quadratic_approach_similarities_stagnate():
|
||||
"""The other half of the same law: a similarity reaching 1 quadratically
|
||||
has c = 0, so the predicted rate is 1 and the iteration must crawl."""
|
||||
f = lambda t: t * t - 2.0
|
||||
root = math.sqrt(2.0)
|
||||
for sim in ("rat_sq", "exp_sq", "cosine"):
|
||||
S = SIMS_01[sim]
|
||||
x = 0.0
|
||||
for _ in range(14):
|
||||
x = x + (1.0 - S(abs(f(x)) / 2.0)) * (2.0 - x)
|
||||
assert abs(x - root) > 1e-3, f"{sim} was expected to stagnate"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sim", sorted(FAST_SIMS))
|
||||
@pytest.mark.parametrize("prob", PROBLEMS, ids=[p[0] for p in PROBLEMS])
|
||||
def test_derived_scale_converges_where_chosen_scale_need_not(sim, prob):
|
||||
"""The data-derived local scale plus the parameter-free safeguard must
|
||||
always converge; the chosen-scale version carries no such promise and is
|
||||
only required not to crash."""
|
||||
_, f, a, b, root = prob
|
||||
x, n, conv = derived_step(f, a, b, XTOL, MAXFEV, sim=sim)
|
||||
assert conv, f"derived.{sim} did not converge"
|
||||
assert abs(x - root) <= 1e-8
|
||||
x2, n2, _ = pure_step(f, a, b, XTOL, MAXFEV, sim)
|
||||
assert math.isfinite(x2) and n2 <= MAXFEV
|
||||
|
||||
|
||||
def test_counter_never_exceeds_the_budget():
|
||||
for budget in (3, 7, 25):
|
||||
for name, m in BASELINES.items():
|
||||
_, n, _ = m(lambda t: math.exp(-t) * (t - 1.0), -1.0, 40.0,
|
||||
XTOL, budget)
|
||||
assert n <= budget, f"{name} spent {n} of {budget}"
|
||||
_, n, _ = sim_root(lambda t: math.exp(-t) * (t - 1.0), -1.0, 40.0,
|
||||
XTOL, budget, prototypes="pair2d", guard=True)
|
||||
assert n <= budget
|
||||
|
||||
|
||||
def test_root_already_at_an_endpoint_costs_two_evaluations():
|
||||
for name, m in BASELINES.items():
|
||||
x, n, conv = m(lambda t: t - 1.0, 1.0, 5.0, XTOL, MAXFEV)
|
||||
assert conv and abs(x - 1.0) <= 1e-8, name
|
||||
assert n <= 3, f"{name} spent {n} on a root sitting on the bracket end"
|
||||
@@ -0,0 +1,124 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user