Files
sim-numerics/diag_cha_phases.py
T
vvsandClaude Opus 5 1b6c626f67 Valeri's decisions (1) competitors from the library, (2) variant a. (1) Chandrupatla is scipy.optimize.elementwise.find_root, Brent is brentq, no more per-problem best_of. Our Chandrupatla stopped on a bracket twice as wide as every other method (the factor the protocol corrects for ITP); fixed (+0.5*xtol) in baselines.py and in the two copies found by grep (stage16_blend, stage19_modern) and the traced mirror, and pinned: ours == library on every problem. Regenerating sweep_full.json changed exactly three rows - chandrupatla APS 2785->2787, CHA 2423->2441, brent CHA 4099->4115 - and nothing else (diff_sweeps.py: 0 unexpected).
(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>
2026-09-21 10:53:00 +01:00

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