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:
@@ -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()
|
||||
Reference in New Issue
Block a user