the wide-bracket gap is NOT closed, and now it has an address: the STEP RULE, not the guarantee and not the reading places. fun1 is one function at growing widths — 7/8, 12/12, 19/15, 59/24, 63/44 — so we tie when narrow and lose as it widens, while our observed order is the HIGHER one (3.63 vs 2.02); 59 on a 2e4 bracket is what bisection needs, i.e. one halving per evaluation against their 2.3. With the safeguard removed entirely we still spend 46/57/61/68 against bisection 57/77/45/45 and Chandrupatla 24/44 — worse than bisection on two of them. |f|/|f'| is an absolute length and cannot march across decades; written into the paper as the stated limit. Also fixed a tracer bug that had best_of double-counting evaluations (2993 vs 1487)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
vvs
2026-09-21 00:09:18 +01:00
co-authored by Claude Opus 5
parent 8d20816163
commit 0fb75be3cf
8 changed files with 372 additions and 6 deletions
+105
View File
@@ -0,0 +1,105 @@
"""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, 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, chandrupatla_scipy)
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()