Files
sim-numerics/stage19_modern.py
T
vvsandClaude Opus 5 68df1d25db Sign tests by product underflowed: FIXED everywhere (branch, not master - the paper is not yet updated). FA * fnew < 0.0 with FA ~ -1e-200, fnew ~ 1e-150 is -0.0 and '-0.0 < 0.0' is False, so on flat functions (x^19) the bracket update moved the wrong end, the root left the bracket and the run still reported convergence - the proposed rule returned 4.9e-9, bisection 3.5e-9, ITP 1.6e-8 with a claimed 1e-12 bracket; the 1e-6 harness scored them solved. Same defect in our bisection, false-position family, Ridders, Brent, ITP and twelve copies in stage scripts, simroot/sim.py and one test; all replaced by _opposite / _same_sign, pinned by test_sign_underflow.
Impact, every sweep regenerated: the paper's key configurations are unchanged to the evaluation (proposed 2806/3220, credit 3568/3020, Chandrupatla, TOMS 748, Brent, bisection); 46 stated numbers move (ITP CHA 2857 -> 2896, ITP oracle 2611 -> 2647, record CHA figures by 14-18, the grid means by ~1%) and one finding weakens: the measured-order exponent of section 7.1 was cheaper in 29 of 60 far cells and 7 of 7 safeguards; with the fix 12 of 61 and 6 of 7, and on the wide-bracket set it now loses in every safeguard and drops 5 problems.

PREREG G1-G3 outcome on the fixed code: G2 (repeating step pattern, period from the data) PASSES Y1-Y4 and is ACCEPTED by the stop rule - proposed rule APS 2806 -> 2782 (below Chandrupatla's 2787), CHA 3220 -> 3169, both 6026 -> 5951, per problem vs Chandrupatla on APS 95/20/38 p = 8e-7. G1 fails Y1/Y2 (its Y3 miss was the sign bug), G3 fails Y2, A fails Y2. Regression 887 pass; the two red tests are the paper-number check (awaiting the paper update) and test_blend (fixed since, copy in stage16).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 12:49:01 +01:00

244 lines
9.6 KiB
Python

"""Stage 19 - the objection that the repairs compared against are from 1973.
Claim A as it stood was: the credit projection repairs regula falsi better
than Illinois (1971), Pegasus (1972) and Anderson-Bjorck (1973). A reader
answers that the field has moved on, and is right to: TOMS 748 (1995),
Chandrupatla (1997) and ITP (2021) all beat credit-repaired regula falsi.
Repairing a three-century-old interpolation better than the 1970s did is not
a modern result.
The credit projection is not tied to regula falsi, though. It is a safeguard
for ANY step rule. So the strong form of the claim is not "our repair of
their old rule beats their old repair" but
take the best interpolation in the field and replace its own acceptance
test with the credit projection.
Chandrupatla's method is exactly that pairing: an inverse quadratic
interpolation plus an acceptance test derived from where that interpolation
stays inside the bracket. Swapping the test for the credit projection, and
changing nothing else, is the sharpest test available of whether the
safeguard is worth anything against the modern state of the art.
Two further modern repairs of the false-position family are included so that
the 1970s are not the only comparison: Ford's Illinois-type variant (1995)
and Galdino's (2011).
"""
from __future__ import annotations
from simroot.baselines import _opposite, _same_sign # 2026-09-21 fix
import math
from scipy.optimize._tstutils import (_APS_TESTS_DICTS,
_CHANDRUPATLA_TESTS_DICTS)
from simroot.baselines import Counter, _Budget, chandrupatla
from simroot.pure import _Safeguard
from stage18_bulletproof import falsi_driver, load_roots
XTOL, MAXFEV, ACC = 1e-12, 200, 1e-6
def chandrupatla_guard(f, a, b, xtol, maxfev, guard="own"):
"""Chandrupatla's method with its safeguard pluggable.
guard="own" the published acceptance test on xi and phi
guard="credit" always interpolate, and project the result onto the
interval the earned bisection budget allows
guard="none" always interpolate, clamp to the bracket only
Everything else - the interpolation, the bracket bookkeeping, the
termination test - is identical, so a difference can only be the guard.
"""
g = Counter(f, maxfev)
x1, x2 = b, a
f1 = f2 = 0.0
try:
f1, f2 = g(x1), g(x2)
if f1 == 0.0:
return x1, g.n, True
if f2 == 0.0:
return x2, g.n, True
x3, f3 = x2, f2
t = 0.5
sg = _Safeguard("credit", abs(b - a), xtol)
while True:
x = x1 + t * (x2 - x1)
w_before = abs(x2 - x1)
fx = g(x)
if (fx > 0.0) == (f1 > 0.0):
x3, f3 = x1, f1
x1, f1 = x, fx
else:
x3, f3 = x2, f2
x2, f2 = x1, f1
x1, f1 = x, fx
if guard == "credit":
sg.note_shrink(w_before, abs(x2 - x1))
xm, fm = (x1, f1) if abs(f1) < abs(f2) else (x2, f2)
if fm == 0.0:
return xm, g.n, True
tol = 2.0 * 2.22e-16 * abs(xm) + 0.5 * xtol # 2026-09-21 fix, see baselines.chandrupatla
span = abs(x2 - x1)
if span == 0.0:
return xm, g.n, True
tl = tol / span
if tl > 0.5:
return xm, g.n, True
t = 0.5
if x3 != x2 and f3 != f2:
xi = (x1 - x2) / (x3 - x2)
ph = (f1 - f2) / (f3 - f2)
d1, d2, d3 = f1 - f2, f1 - f3, f2 - f3
usable = d1 != 0.0 and d2 != 0.0 and d3 != 0.0
if usable:
t_iqi = ((f1 / d1) * (f3 / (-d3))
+ ((x3 - x1) / (x2 - x1)) * (f1 / d2) * (f2 / d3))
if guard == "own":
if (0.0 < xi < 1.0
and 1.0 - math.sqrt(1.0 - xi) < ph
< math.sqrt(xi)):
t = t_iqi
else:
t = t_iqi
if guard == "credit" and math.isfinite(t):
lo = min(x1, x2)
hi = max(x1, x2)
cand = x1 + t * (x2 - x1)
proj = sg.project_credit(cand, lo, hi)
if x2 != x1:
t = (proj - x1) / (x2 - x1)
if not math.isfinite(t):
t = 0.5
t = min(max(t, tl), 1.0 - tl)
except _Budget:
return (x1 if abs(f1) < abs(f2) else x2), g.n, False
# --------------------------------------------------------------------------
# two modern repairs of the false-position family, so the 1970s are not the
# only comparison
# --------------------------------------------------------------------------
def falsi_modern(f, a, b, xtol, maxfev, variant):
"""variant="ford" Ford 1995, Illinois-type with the Anderson-Bjorck
factor bounded away from zero
variant="galdino" Galdino 2011, the factor f_moved/(f_moved + fx)
applied to both ordinates
"""
g = Counter(f, maxfev)
x = 0.5 * (a + b)
try:
fa, fb = g(a), g(b)
if fa == 0.0:
return a, g.n, True
if fb == 0.0:
return b, g.n, True
if _same_sign(fa, fb):
raise ValueError("no sign change")
while abs(b - a) > xtol:
lo, hi = (a, b) if a < b else (b, a)
denom = fb - fa
x_new = 0.5 * (a + b) if denom == 0.0 else (a * fb - b * fa) / denom
if not math.isfinite(x_new) or not (lo < x_new < hi):
x_new = 0.5 * (a + b)
if x_new in (a, b):
x_new = 0.5 * (a + b)
if x_new in (a, b):
return x_new, g.n, abs(b - a) <= xtol
fx = g(x_new)
if fx == 0.0:
return x_new, g.n, True
def factor(f_stale, f_moved):
if variant == "ford":
m = 1.0 - fx / f_moved if f_moved != 0.0 else 0.5
return m if m > 1e-3 else 0.5
m = f_moved / (f_moved + fx) if (f_moved + fx) != 0.0 else 0.5
return m if 0.0 < m < 1.0 else 0.5
if _opposite(fa, fx):
fa *= factor(fa, fb)
b, fb = x_new, fx
else:
fb *= factor(fb, fa)
a, fa = x_new, fx
x = x_new
return 0.5 * (a + b), g.n, True
except _Budget:
return x, g.n, False
def score(method, probs):
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 main():
probs = load_roots()
print("=" * 82)
print("A. the false-position family, 1971 to 2011, plus the credit "
"projection")
print("=" * 82)
print(f"{'repair':22s}{'year':>7s}{'evals':>10s}{'solved':>10s}"
f"{'worst':>8s}")
print("-" * 82)
rows = []
for label, year, m in (
("Illinois", "1971",
lambda f, a, b, xt, mx: falsi_driver(f, a, b, xt, mx, "illinois")),
("Pegasus", "1972",
lambda f, a, b, xt, mx: falsi_driver(f, a, b, xt, mx, "pegasus")),
("Anderson-Bjorck", "1973",
lambda f, a, b, xt, mx: falsi_driver(f, a, b, xt, mx, "anderson")),
("Ford (Illinois-type)", "1995",
lambda f, a, b, xt, mx: falsi_modern(f, a, b, xt, mx, "ford")),
("Galdino", "2011",
lambda f, a, b, xt, mx: falsi_modern(f, a, b, xt, mx, "galdino")),
("credit projection", "here",
lambda f, a, b, xt, mx: falsi_driver(f, a, b, xt, mx, "credit"))):
t, s, w = score(m, probs)
rows.append((label, t, s))
print(f"{label:22s}{year:>7s}{t:>10d}{s:>7d}/199{w:>8d}", flush=True)
print("\n" + "=" * 82)
print("B. the sharp test: Chandrupatla's interpolation, its guard swapped")
print("=" * 82)
print("same interpolation, same bookkeeping, same termination; only the "
"guard differs\n")
print(f"{'guard':30s}{'evals':>10s}{'solved':>10s}{'worst':>8s}")
print("-" * 82)
res = {}
for label, guard in (("Chandrupatla's own test", "own"),
("none (clamp to bracket only)", "none"),
("the credit projection", "credit")):
t, s, w = score(lambda f, a, b, xt, mx, _g=guard:
chandrupatla_guard(f, a, b, xt, mx, _g), probs)
res[guard] = (t, s, w)
print(f"{label:30s}{t:>10d}{s:>7d}/199{w:>8d}", flush=True)
t_pub, s_pub, _w = score(lambda f, a, b, xt, mx: chandrupatla(f, a, b, xt,
mx), probs)
print(f"{'(published implementation)':30s}{t_pub:>10d}{s_pub:>7d}/199")
drift = abs(res['own'][0] - t_pub) / t_pub
print(f"\n control drift {drift * 100:.2f}% - the pluggable version "
f"must reproduce the published one")
better = res["credit"][0] < res["own"][0]
print(f" credit {res['credit'][0]} against its own guard "
f"{res['own'][0]} => "
f"{'the safeguard beats the state of the art' if better else 'IT DOES NOT'}")
if __name__ == "__main__":
main()