Files
sim-numerics/stage19_modern.py
T
vvsandClaude Opus 5 1b6c626f67 Valeri's decisions (1) competitors from the library, (2) variant a. (1) Chandrupatla is scipy.optimize.elementwise.find_root, Brent is brentq, no more per-problem best_of. Our Chandrupatla stopped on a bracket twice as wide as every other method (the factor the protocol corrects for ITP); fixed (+0.5*xtol) in baselines.py and in the two copies found by grep (stage16_blend, stage19_modern) and the traced mirror, and pinned: ours == library on every problem. Regenerating sweep_full.json changed exactly three rows - chandrupatla APS 2785->2787, CHA 2423->2441, brent CHA 4099->4115 - and nothing else (diff_sweeps.py: 0 unexpected).
(2) The proposed method is again the scheduled projection (2806/3220/6026), its budget multiplier DECLARED as the one free constant: the canonical total is the same for every multiplier 1.25-4, the wide-bracket one is not. The earned projection is the constant-free variant (3568/3020/6588). What the old tables hid: problem by problem the proposed rule beats Chandrupatla on the canonical set 87/24/42, exact sign test p = 9e-5, while trailing him by 0.7% in total; on the wide-bracket set he is genuinely stronger (26 to 9, p = 0.006). Gap decomposition recomputed with the library: 244 (was 262), 160 of it on the flat families (two thirds), fun1 58 (a quarter, not a third as the abstract said).

Also corrected on the way: the 'subsumed by Chandrupatla' pin claimed he is better on the worst single problem - true only with the looser tolerance (84 against the credit rule's 83 once fixed); §8.3 still called zero slack degenerate, which Table 1a refutes; §5.2's Brent figure is our pure-Python implementation and now says so. verify_article.py 286/286, 802 tests pass twice.

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

242 lines
9.5 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
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 fa * fb > 0.0:
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 fa * fx < 0.0:
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()