"""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()