Files
sim-numerics/tests/test_revision_points.py
T

178 lines
6.9 KiB
Python

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