"""Stage 6 - the projection safeguard: guarantee AND order, or neither? The paper's weak point: the configuration that tops the Alefeld-Potra-Shi ranking carries no worst-case bound, and the best configuration that does is 14th, 18% behind Brent. Comparing an unguaranteed method's average against guaranteed ones is not a comparison a reader will accept. The three earlier safeguards all REPLACE the computed step with a midpoint, which throws the computation away and, with the three-point scale, collapses the order from 1.94 to 1.00. The projection guard instead keeps as much of the step as the bisection budget allows: r_j = 2^(n_half - j) * (xtol/2) - (b-a)/2 x = x_sim if |x_sim - x_mid| <= r_j = x_mid + sign(d) * r_j otherwise Because the bracket after a step is at most (b-a)/2 + r_j = 2^(n_half-j)*eps, induction bounds the run by n_half steps: the worst case is EXACTLY bisection. No constant is introduced - ITP's kappa1, kappa2 and n0 all belong to its truncation stage, which repairs regula falsi and which the similarity step does not need. What must be true for this to be worth putting in the paper: (1) zero runs exceed the bisection bound, on all 199 problems; (2) the order stays near 1.94, not 1.00; (3) the total is close to the unguarded configuration's. Each is checked below. This is the SMOKE run. """ from __future__ import annotations import math from simroot.baselines import bisection, brent, itp, toms748 from simroot.pure import GUARDS, pure_root from stage5_three_point import PROBLEMS, order_of XTOL = 1e-12 MAXFEV = 200 def main(): methods = [ ("bisection", lambda f, a, b: bisection(f, a, b, XTOL, MAXFEV)), ("brent", lambda f, a, b: brent(f, a, b, XTOL, MAXFEV)), ("toms748", lambda f, a, b: toms748(f, a, b, XTOL, MAXFEV)), ("itp (3 constants)", lambda f, a, b: itp(f, a, b, XTOL, MAXFEV)), ] for gu in GUARDS: methods.append((f"OURS 3pt .{gu}", (lambda g: lambda f, a, b: pure_root( f, a, b, XTOL, MAXFEV, scale="derived3", guard=g))(gu))) width = max(len(n) for n, _ in methods) + 1 header = (f"{'method':{width}s}" + "".join(f"{p[0][:13]:>15s}" for p in PROBLEMS) + f"{'TOTAL':>8s}{'FAIL':>6s}{'>bisect bound':>15s}") print(header) print("-" * len(header)) for name, fn in methods: cells, total, fails, over = [], 0, 0, 0 for _, f, a, b, root in PROBLEMS: x, n, conv = fn(f, a, b) ok = conv and abs(x - root) <= 1e-8 cells.append(f"{n:>15d}" if ok else f"{'FAIL(' + str(n) + ')':>15s}") total += n fails += 0 if ok else 1 # the bisection bound itself, not twice it: 2 endpoint evaluations # plus ceil(log2(w/xtol)) steps if ok and n > math.ceil(math.log2(abs(b - a) / XTOL)) + 2: over += 1 print(f"{name:{width}s}" + "".join(cells) + f"{total:>8d}{fails:>6d}{over:>15d}") print("\nobserved order (median over the eight problems)") print(f"{'method':22s}{'order':>9s}") print("-" * 33) for gu in GUARDS: ps = [order_of((lambda g: lambda f_, a_, b_, xt, mx: pure_root( f_, a_, b_, XTOL, MAXFEV, scale="derived3", guard=g))(gu), f, a, b, root) for _, 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] if ps else float("nan") print(f"{'OURS 3pt .' + gu:22s}{med:>9.3f}") if __name__ == "__main__": main()