(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>
107 lines
4.4 KiB
Python
107 lines
4.4 KiB
Python
"""WHERE in a run the wide-bracket gap to Chandrupatla is spent: approach or endgame?
|
|
|
|
The gap is diffuse across families (+57, +54, +46, +43, +35, ...), so before proposing any
|
|
mechanism this splits every run into the two phases with the paper's OWN definition
|
|
(stage4_why.phase_boundary: the geometric mean of the initial width and the ATTAINABLE
|
|
tolerance) and reports where each method's evaluations go.
|
|
|
|
Problems whose declared root cannot be matched are skipped, and the six scoring-artefact
|
|
problems (aps.13.00, fun7.*) are reported separately rather than silently dropped.
|
|
|
|
python -u diag_cha_phases.py
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
from collections import defaultdict
|
|
|
|
from simroot.baselines import CHANDRUPATLA_BASELINE
|
|
from simroot.baselines import chandrupatla, chandrupatla_scipy
|
|
from simroot.pure import pure_root
|
|
from stage4_why import phase_boundary, traced
|
|
from sweep_full import ACC, MAXFEV, XTOL, load_problems
|
|
|
|
# NOT best_of here: it runs BOTH implementations and returns the cheaper result, so a tracer
|
|
# wrapped around it counts the evaluations of both. Doing that reported Chandrupatla's CHA
|
|
# total as 2993 instead of 1487 and manufactured a 1299-evaluation "endgame win" for us
|
|
# (caught 2026-09-21). Each implementation is traced separately and the cheaper split kept,
|
|
# which is what best_of means for cost.
|
|
CHA_IMPLS = (CHANDRUPATLA_BASELINE,) # the library alone (2026-09-21)
|
|
|
|
|
|
def ours(f, a, b, xtol, maxfev):
|
|
return pure_root(f, a, b, xtol, maxfev, similarity="ratio", scale="derived3",
|
|
guard="safe", prestep="slope", direction="dissimilar")
|
|
|
|
|
|
def phases(method, f, a, b, root):
|
|
"""(approach, endgame, total) evaluations, or None when the run cannot be scored."""
|
|
g, xs = traced(f)
|
|
try:
|
|
x, _n, conv = method(g, a, b, XTOL, MAXFEV)
|
|
except Exception:
|
|
return None
|
|
x = float(x)
|
|
ok = conv and math.isfinite(x) and (abs(x - root) <= ACC or f(x) == 0.0)
|
|
if not ok:
|
|
return None
|
|
target = phase_boundary(abs(b - a), max(abs(a), abs(b)))
|
|
approach = len(xs)
|
|
for i, xi in enumerate(xs):
|
|
if abs(xi - root) <= target:
|
|
approach = i + 1
|
|
break
|
|
return approach, len(xs) - approach, len(xs)
|
|
|
|
|
|
def cha_phases(f, a, b, root):
|
|
"""The cheaper of the two Chandrupatla implementations, each traced on its own."""
|
|
best = None
|
|
for impl in CHA_IMPLS:
|
|
p = phases(impl, f, a, b, root)
|
|
if p is not None and (best is None or p[2] < best[2]):
|
|
best = p
|
|
return best
|
|
|
|
|
|
def main():
|
|
agg = defaultdict(lambda: [0, 0, 0]) # method -> [approach, endgame, total]
|
|
per_fam = defaultdict(lambda: [0, 0, 0, 0]) # family -> [our_app, our_end, ch_app, ch_end]
|
|
skipped = []
|
|
print(f"{'problem':<10} {'ours app/end':>14} {'CH app/end':>12} {'gap app':>8} {'gap end':>8}")
|
|
for s, pid, f, a, b, root in load_problems():
|
|
if s != "CHA":
|
|
continue
|
|
po = phases(ours, f, a, b, root)
|
|
pc = cha_phases(f, a, b, root)
|
|
if po is None or pc is None:
|
|
skipped.append(pid)
|
|
continue
|
|
agg["ours"][0] += po[0]; agg["ours"][1] += po[1]; agg["ours"][2] += po[2]
|
|
agg["chandrupatla"][0] += pc[0]; agg["chandrupatla"][1] += pc[1]
|
|
agg["chandrupatla"][2] += pc[2]
|
|
fam = pid.split(".")[0]
|
|
per_fam[fam][0] += po[0]; per_fam[fam][1] += po[1]
|
|
per_fam[fam][2] += pc[0]; per_fam[fam][3] += pc[1]
|
|
print(f"{pid:<10} {po[0]:6d}/{po[1]:<7d} {pc[0]:5d}/{pc[1]:<6d} "
|
|
f"{po[0] - pc[0]:+8d} {po[1] - pc[1]:+8d}")
|
|
|
|
print(f"\nskipped (not scorable under either criterion): {skipped}")
|
|
for nm, (app, end, tot) in agg.items():
|
|
print(f"{nm:<14} approach {app:5d} endgame {end:5d} total {tot:5d} "
|
|
f"({100.0 * app / max(tot, 1):.0f}% of the cost is approach)")
|
|
d_app = agg["ours"][0] - agg["chandrupatla"][0]
|
|
d_end = agg["ours"][1] - agg["chandrupatla"][1]
|
|
print(f"\nGAP: approach {d_app:+d}, endgame {d_end:+d} "
|
|
f"(total {d_app + d_end:+d})")
|
|
print("\nper family (approach gap / endgame gap):")
|
|
for fam in sorted(per_fam, key=lambda k: -((per_fam[k][0] - per_fam[k][2])
|
|
+ (per_fam[k][1] - per_fam[k][3]))):
|
|
oa, oe, ca, ce = per_fam[fam]
|
|
print(f" {fam:<8} approach {oa - ca:+5d} endgame {oe - ce:+5d}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|