A in the method (extrapolate='steps', default 'none' = published bit for bit): NOT accepted into the proposed rule by the declared stop rule - X2 fails. Proposed rule without -> with A: APS 2806 -> 2967 (X2 FAIL), CHA 3220 -> 2925 (X1 pass), flat fun3-6 1823 -> 1524 (X4 pass), guarantee intact, 0 violations in 39 family cells (X3 pass). The per-problem edge over Chandrupatla on APS is lost (87/24/42, p=9e-5 -> 42/62/49, p=0.53).

X5: dissimilar+slope with A reaches 2859+2545 = 5404 over both sets, the best total this line has measured (was 5511; Chandrupatla 5228) - but again by trading the canonical set (2726 -> 2859) for the wide brackets (2785 -> 2545). The one-step replay (APS no harm) did not carry over: its bracket-halving metric rewards overshooting the root, which on a simple root costs superlinear convergence in the full run - the declared one-step limitation, now measured. Among the nine functions A helps only the tent (6026 -> 5892) and slightly ratio_soft.

Also: pin 'strictly fewer evaluations on every flat problem' was wrong for 6(x-2)^5 on [0,10] - both runs are budget-bound (90 of the guaranteed 91); corrected to the measured claim. verify_article 286/286, 836 tests pass twice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
vvs
2026-09-21 12:11:06 +01:00
co-authored by Claude Opus 5
parent a25cffad1d
commit 99be33a632
6 changed files with 293 additions and 9 deletions
+100
View File
@@ -0,0 +1,100 @@
"""Verdicts X1-X5 of PREREG_extrapolate_in_method_2026-09-21.md from sweep_extrap.json.
python -u analyze_extrap.py
"""
from __future__ import annotations
import json
import math
FLAT = ("fun3", "fun4", "fun5", "fun6")
FAMILY = ("project", "safe", "credit")
def load(p):
with open(p, encoding="utf8") as fh:
return json.load(fh)
def key(r):
s = r["spec"]
return (s[1], s[2], s[3], s[5], s[6], s[7])
def fam_sum(r, fams):
return sum((n if ok else 200) for pid, (n, ok) in r["CHA"]["per"].items()
if pid.split(".")[0] in fams)
def sign_p(w, l):
k, n = min(w, l), w + l
return min(1.0, 2.0 * sum(math.comb(n, i) for i in range(k + 1)) / 2.0 ** n) if n else 1.0
def h2h(a, b, s):
w = t = l = 0
for pid, (na, oka) in a[s]["per"].items():
nb, okb = b[s]["per"][pid]
if oka and okb:
w += na < nb
l += na > nb
t += na == nb
return w, t, l
def main():
rows = load("sweep_extrap.json")
full = {r["label"]: r for r in load("sweep_full.json")}
cha = full["chandrupatla"]
pairs = {}
for r in rows:
pairs.setdefault(key(r), {})[r["spec"][9]] = r
readings = {("none", "recent", "distance"): "plain", ("slope", "recent", "distance"): "slope",
("slope", "shape", "distance"): "shape+slope",
("slope", "recent", "dissimilar"): "dissim+slope",
("defect", "recent", "distance"): "defect"}
print("tent, three-point, pairs (without A -> with A): APS | CHA | both | flat fun3-6 sum")
for (sim, sc, gu, pre, pts, dirn), p in sorted(pairs.items()):
if sim != "ratio" or gu not in FAMILY + ("none",):
continue
a, b = p["none"], p["steps"]
rd = readings[(pre, pts, dirn)]
print(f" {rd:<13} {gu:<8} {a['APS']['total']:5d}->{b['APS']['total']:<5d} "
f"{a['CHA']['total']:5d}->{b['CHA']['total']:<5d} "
f"{a['APS']['total'] + a['CHA']['total']:5d}->"
f"{b['APS']['total'] + b['CHA']['total']:<5d} "
f"{fam_sum(a, FLAT):4d}->{fam_sum(b, FLAT):<4d} "
f"solved {b['APS']['solved'] + b['CHA']['solved']} over {b['APS']['over'] + b['CHA']['over']}")
prop = pairs[("ratio", "derived3", "project", "none", "recent", "distance")]
a, b = prop["none"], prop["steps"]
print("\nVERDICTS for the proposed rule (tent, 3-point, projection)")
print(f" X1 CHA < 3220: {b['CHA']['total']} -> {'PASS' if b['CHA']['total'] < 3220 else 'FAIL'}")
print(f" X2 APS <= 2806: {b['APS']['total']} -> {'PASS' if b['APS']['total'] <= 2806 else 'FAIL'}")
fam = [p["steps"] for k, p in pairs.items() if k[2] in FAMILY]
viol = sum(r["APS"]["over"] + r["CHA"]["over"] for r in fam)
low = [r["label"] for r in fam if r["APS"]["solved"] + r["CHA"]["solved"] < 193]
print(f" X3 guarantee family with A: {viol} violations over {len(fam)} cells; "
f"cells solving < 193: {len(low)} -> {'PASS' if viol == 0 and not low else 'FAIL'}")
fa, fb = fam_sum(a, FLAT), fam_sum(b, FLAT)
print(f" X4 flat fun3-6 sum: {fa} -> {fb} -> {'PASS' if fb < fa else 'FAIL'}")
best = min((p["steps"] for p in pairs.values()
if p["steps"]["spec"][3] in FAMILY
and p["steps"]["APS"]["solved"] + p["steps"]["CHA"]["solved"] >= 193),
key=lambda r: r["APS"]["total"] + r["CHA"]["total"])
print(f" X5 best pair with A over both sets: {best['label']} "
f"{best['APS']['total']}+{best['CHA']['total']}="
f"{best['APS']['total'] + best['CHA']['total']} (reference 5511, Chandrupatla 5228)")
for s in ("APS", "CHA"):
w, t, l = h2h(b, cha, s)
print(f" proposed + A vs Chandrupatla, {s}: {w}/{t}/{l} sign p={sign_p(w, l):.2g} "
f"totals {b[s]['total']} vs {cha[s]['total']}")
print("\nnine functions, three-point, projection, no readings (without A -> with A), both sets:")
for (sim, sc, gu, pre, pts, dirn), p in sorted(pairs.items()):
if gu == "project" and pre == "none" and pts == "recent" and dirn == "distance":
a2, b2 = p["none"], p["steps"]
print(f" {sim:<11} {a2['APS']['total'] + a2['CHA']['total']:6d} -> "
f"{b2['APS']['total'] + b2['CHA']['total']:6d}")
if __name__ == "__main__":
main()
+41
View File
@@ -0,0 +1,41 @@
tent, three-point, pairs (without A -> with A): APS | CHA | both | flat fun3-6 sum
defect credit 3545->4134 2949->2926 6494->7060 1044->993 solved 193 over 0
plain credit 3568->4130 3020->2986 6588->7116 1044->988 solved 193 over 0
dissim+slope credit 3857->4223 3002->2947 6859->7170 1043->1002 solved 193 over 0
slope credit 3507->4121 2997->3007 6504->7128 1051->1007 solved 193 over 0
shape+slope credit 3755->4169 3021->3059 6776->7228 1064->1064 solved 193 over 0
defect none 2523->3180 4198->3508 6721->6688 2806->2081 solved 188 over 1
plain none 2806->2967 4104->3471 6910->6438 2707->2070 solved 188 over 0
dissim+slope none 2726->2859 2785->2545 5511->5404 1376->1157 solved 193 over 0
slope none 2848->2971 3075->2735 5923->5706 1710->1331 solved 193 over 0
shape+slope none 2675->2930 5443->5452 8118->8382 4000->4000 solved 173 over 0
defect project 2523->3180 3210->2954 5733->6134 1818->1527 solved 193 over 0
plain project 2806->2967 3220->2925 6026->5892 1823->1524 solved 193 over 0
dissim+slope project 2726->2859 2785->2545 5511->5404 1376->1157 solved 193 over 0
slope project 2848->2971 3074->2735 5922->5706 1709->1331 solved 193 over 0
shape+slope project 2675->2930 3498->3506 6173->6436 2055->2054 solved 193 over 0
defect safe 3131->3207 3368->3386 6499->6593 1943->1929 solved 193 over 0
plain safe 3036->3171 3363->3389 6399->6560 1950->1931 solved 193 over 0
dissim+slope safe 3123->3170 2683->2710 5806->5880 1228->1248 solved 193 over 0
slope safe 3020->3187 2903->2895 5923->6082 1484->1469 solved 193 over 0
shape+slope safe 2527->2986 3108->3223 5635->6209 1629->1687 solved 193 over 0
VERDICTS for the proposed rule (tent, 3-point, projection)
X1 CHA < 3220: 2925 -> PASS
X2 APS <= 2806: 2967 -> FAIL
X3 guarantee family with A: 0 violations over 39 cells; cells solving < 193: 0 -> PASS
X4 flat fun3-6 sum: 1823 -> 1524 -> PASS
X5 best pair with A over both sets: pure+dissim+ext+slope.ratio.derived3.project 2859+2545=5404 (reference 5511, Chandrupatla 5228)
proposed + A vs Chandrupatla, APS: 42/62/49 sign p=0.53 totals 2967 vs 2787
proposed + A vs Chandrupatla, CHA: 11/1/28 sign p=0.0095 totals 2925 vs 2441
nine functions, three-point, projection, no readings (without A -> with A), both sets:
cbrt 17993 -> 18181
cosine 19784 -> 19753
exp_abs 6652 -> 6869
exp_sq 19786 -> 19780
rat_sq 19793 -> 19796
ratio 6026 -> 5892
ratio_soft 9352 -> 9285
rational 6823 -> 7483
sqrt 13381 -> 13633
+24 -1
View File
@@ -542,7 +542,8 @@ def _local_order(x2, f2, x1, f1, x0, f0):
def pure_root(f, a, b, xtol, maxfev, similarity="ratio", scale="derived", def pure_root(f, a, b, xtol, maxfev, similarity="ratio", scale="derived",
guard="step", stats=None, budget_factor=2.0, guard="step", stats=None, budget_factor=2.0,
budget_extra=None, endowment=1.0, space="x", prestep="none", budget_extra=None, endowment=1.0, space="x", prestep="none",
points="recent", direction="distance", exponent="none"): points="recent", direction="distance", exponent="none",
extrapolate="none"):
"""The pure similarity root finder. """The pure similarity root finder.
similarity : key into PURE_SIMS similarity : key into PURE_SIMS
@@ -594,6 +595,8 @@ def pure_root(f, a, b, xtol, maxfev, similarity="ratio", scale="derived",
if scale not in ("derived", "derived3", "derived_rho", "derived3_rho", if scale not in ("derived", "derived3", "derived_rho", "derived3_rho",
"derived3_m", "derived3_cap", "start", "bracket", "far"): "derived3_m", "derived3_cap", "start", "bracket", "far"):
raise ValueError(f"unknown scale {scale!r}") raise ValueError(f"unknown scale {scale!r}")
if extrapolate not in ("none", "steps"):
raise ValueError(f"unknown extrapolate {extrapolate!r}")
if exponent not in EXPONENTS: if exponent not in EXPONENTS:
raise ValueError(f"unknown exponent {exponent!r}") raise ValueError(f"unknown exponent {exponent!r}")
if exponent == "order" and scale not in ("derived3", "derived3_cap", "bracket", "far"): if exponent == "order" and scale not in ("derived3", "derived3_cap", "bracket", "far"):
@@ -863,6 +866,26 @@ def pure_root(f, a, b, xtol, maxfev, similarity="ratio", scale="derived",
step = _log_space_step(S, x, fx, far, slope, c_near, xtol) step = _log_space_step(S, x, fx, far, slope, c_near, xtol)
if step is None: if step is None:
step = (1.0 - S(z_now)) * (far - x) step = (1.0 - S(z_now)) * (far - x)
if extrapolate == "steps":
# PREREG 2026-09-21 (A in the method). The similarity of THIS
# step to the previous displacement: two successive moves the
# same way, of similar length, mean the iterate creeps along a
# geometric series whose remainder is step*S/(1-S) - so the
# step is lengthened by 1/(1-S). On x^m under a Newton step
# S = 1 - 1/m and the factor is m, which lands on the root.
# At a simple root successive steps shrink superlinearly, S ->
# 0 and the step is untouched. Capped at the far end, the same
# bound the step itself has; no constant.
d_prev = x - x_prev
if (step != 0.0 and d_prev != 0.0
and (step > 0.0) == (d_prev > 0.0)):
s_steps = (min(abs(step), abs(d_prev))
/ max(abs(step), abs(d_prev)))
if s_steps < 1.0:
longer = step / (1.0 - s_steps)
reach = abs(far - x)
step = (longer if abs(longer) <= reach
else math.copysign(reach, step))
if stats is not None and "trace" in stats: if stats is not None and "trace" in stats:
# DIAGNOSTIC HOOK, behaviour-neutral: records the state each # DIAGNOSTIC HOOK, behaviour-neutral: records the state each
# step was taken from, so another model can be evaluated on the # step was taken from, so another model can be evaluated on the
File diff suppressed because one or more lines are too long
+49 -8
View File
@@ -85,10 +85,10 @@ def itp_oracle(f, a, b, xtol, maxfev):
return best if best is not None else (0.5 * (a + b), maxfev, False) return best if best is not None else (0.5 * (a + b), maxfev, False)
# Competitors are taken at the better of SciPy's implementation and ours, # Competitors are the LIBRARY wherever SciPy ships the method (since 2026-09-21):
# problem by problem, wherever SciPy ships the method. Ours turned out to be # Brent = brentq, Chandrupatla = elementwise.find_root. Until then they were the
# the cheaper of the two - 6991 against brentq's 7004, 5209 against # per-problem better of the library and ours, which let a tolerance slip in our
# _chandrupatla's 5228 - so quoting the library alone would have flattered us. # Chandrupatla (a bracket twice as wide) leak into the reported numbers.
BASELINES = { BASELINES = {
"bisection": bisection, "regula_falsi": regula_falsi, "bisection": bisection, "regula_falsi": regula_falsi,
"illinois": illinois, "pegasus": pegasus, "illinois": illinois, "pegasus": pegasus,
@@ -115,11 +115,13 @@ def make_method(spec):
points = spec[6] if len(spec) > 6 else "recent" points = spec[6] if len(spec) > 6 else "recent"
direction = spec[7] if len(spec) > 7 else "distance" direction = spec[7] if len(spec) > 7 else "distance"
exponent = spec[8] if len(spec) > 8 else "none" exponent = spec[8] if len(spec) > 8 else "none"
extrapolate = spec[9] if len(spec) > 9 else "none"
def m(f, a, b, xtol, maxfev): def m(f, a, b, xtol, maxfev):
return pure_root(f, a, b, xtol, maxfev, similarity=sim, return pure_root(f, a, b, xtol, maxfev, similarity=sim,
scale=scale, guard=guard, space=space, prestep=prestep, scale=scale, guard=guard, space=space, prestep=prestep,
points=points, direction=direction, exponent=exponent) points=points, direction=direction, exponent=exponent,
extrapolate=extrapolate)
return m return m
@@ -133,6 +135,9 @@ def label(spec):
tag = f"{tag}+shape" tag = f"{tag}+shape"
if len(spec) > 7 and spec[7] != "distance": if len(spec) > 7 and spec[7] != "distance":
tag = f"{tag}+dissim" tag = f"{tag}+dissim"
if len(spec) > 9 and spec[9] != "none":
# the pair (off, on) must be distinguishable in a results file
tag = f"{tag}+ext"
if len(spec) > 8 and spec[8] != "none": if len(spec) > 8 and spec[8] != "none":
# the exponent must be visible in the label: (far, none) is false position and # the exponent must be visible in the label: (far, none) is false position and
# (far, order) is the hypothesis, and a results file that cannot tell them apart # (far, order) is the hypothesis, and a results file that cannot tell them apart
@@ -205,6 +210,39 @@ def build_grid_log(smoke):
for sim in sims for sc in ("derived", "derived3") for gu in guards] for sim in sims for sc in ("derived", "derived3") for gu in guards]
def _preflight_extrap_mode():
"""Fail LOUDLY if extrapolate is not deployed (see _preflight_log_mode)."""
x, _n, ok = pure_root(lambda t: t - 1.0, 0.0, 3.0, 1e-10, 100, similarity="ratio",
scale="derived3", guard="project", extrapolate="steps")
if not (ok and abs(x - 1.0) < 1e-8):
raise SystemExit("extrapolate preflight FAILED: refusing to report zeros")
def build_grid_extrap(smoke):
"""PREREG 2026-09-21: step extrapolation (A) inside the method, every cell as a pair
(none, steps). Tent with every reading and every guard; all nine functions without
readings, so the law across the grid is visible with and without A."""
_preflight_extrap_mode()
guards = ("project", "credit") if smoke else GUARD_MODES
readings = [("none", "recent", "distance")] if smoke else [
("none", "recent", "distance"), ("slope", "recent", "distance"),
("slope", "shape", "distance"), ("slope", "recent", "dissimilar"),
("defect", "recent", "distance")]
specs = []
for gu in guards:
for pre, pts, dirn in readings:
for ex in ("none", "steps"):
specs.append(("pure", "ratio", "derived3", gu, "x", pre, pts, dirn,
"none", ex))
sims = ["sqrt"] if smoke else [s for s in sorted(PURE_SIMS) if s != "ratio"]
for sim in sims:
for gu in guards:
for ex in ("none", "steps"):
specs.append(("pure", sim, "derived3", gu, "x", "none", "recent",
"distance", "none", ex))
return specs
def _preflight_decision_mode(): def _preflight_decision_mode():
"""Fail LOUDLY if the decision readings are not deployed (see _preflight_log_mode).""" """Fail LOUDLY if the decision readings are not deployed (see _preflight_log_mode)."""
for md in ("band", "defect", "simpair"): for md in ("band", "defect", "simpair"):
@@ -335,10 +373,12 @@ def main():
capmode = "--cap" in sys.argv # scale selected between the two computed ones capmode = "--cap" in sys.argv # scale selected between the two computed ones
ordermode = "--order" in sys.argv # slot 1: the similarity read BY PLACE ordermode = "--order" in sys.argv # slot 1: the similarity read BY PLACE
decmode = "--decision" in sys.argv # slot 2: the reading in the DECISION place decmode = "--decision" in sys.argv # slot 2: the reading in the DECISION place
extmode = "--extrap" in sys.argv # A in the method: step extrapolation
combomode = "--combo" in sys.argv # modes A and B together combomode = "--combo" in sys.argv # modes A and B together
pointsmode = "--points" in sys.argv # mode A: triple by shape pointsmode = "--points" in sys.argv # mode A: triple by shape
dirmode = "--direction" in sys.argv # mode B: far end by dissimilarity dirmode = "--direction" in sys.argv # mode B: far end by dissimilarity
specs = (build_grid_decision(smoke) if decmode else specs = (build_grid_extrap(smoke) if extmode else
build_grid_decision(smoke) if decmode else
build_grid_order(smoke) if ordermode else build_grid_order(smoke) if ordermode else
build_grid_cap(smoke) if capmode else build_grid_cap(smoke) if capmode else
build_grid_combo(smoke) if combomode else build_grid_combo(smoke) if combomode else
@@ -362,8 +402,9 @@ def main():
# the published sweep_full.json is what verify_article.py checks the paper # the published sweep_full.json is what verify_article.py checks the paper
# against, so the log grid never writes into it # against, so the log grid never writes into it
if decmode or ordermode or capmode or combomode or pointsmode or dirmode: if extmode or decmode or ordermode or capmode or combomode or pointsmode or dirmode:
base = ("sweep_decision" if decmode else base = ("sweep_extrap" if extmode else
"sweep_decision" if decmode else
"sweep_order" if ordermode else "sweep_order" if ordermode else
"sweep_cap" if capmode else "sweep_cap" if capmode else
"sweep_combo" if combomode else "sweep_combo" if combomode else
+78
View File
@@ -0,0 +1,78 @@
"""Pins for PREREG_extrapolate_in_method_2026-09-21.md: step extrapolation (A)."""
from __future__ import annotations
import math
import pytest
from simroot.pure import pure_root
SMOOTH = (
(lambda t: t ** 3 - 2.0 * t - 5.0, 1.0, 4.0),
(lambda t: math.exp(t) - 2.0, 0.0, 3.0),
(lambda t: math.sin(t) - 0.5, 0.0, 1.5),
)
FLAT = (
(lambda t: 6.0 * (t - 2.0) ** 5, 0.0, 10.0),
(lambda t: t ** 9, -1.0, 2.0),
(lambda t: (t - 3.0) ** 3, 0.0, 10.0),
)
XTOL, MAXFEV = 1e-12, 200
@pytest.mark.parametrize("f,a,b", SMOOTH + FLAT)
@pytest.mark.parametrize("guard", ("project", "safe", "credit"))
def test_default_is_the_published_method_bit_for_bit(f, a, b, guard):
ref = pure_root(f, a, b, XTOL, MAXFEV, similarity="ratio", scale="derived3", guard=guard)
off = pure_root(f, a, b, XTOL, MAXFEV, similarity="ratio", scale="derived3", guard=guard,
extrapolate="none")
assert ref == off
@pytest.mark.parametrize("f,a,b", SMOOTH + FLAT)
@pytest.mark.parametrize("guard", ("project", "credit"))
def test_the_guarantee_survives_extrapolation(f, a, b, guard):
x, n, ok = pure_root(f, a, b, XTOL, MAXFEV, similarity="ratio", scale="derived3",
guard=guard, extrapolate="steps")
assert ok and min(a, b) <= x <= max(a, b)
half = math.ceil(math.log2(abs(b - a) / XTOL))
bound = 2 * half + 3 if guard == "project" else half + 4
assert n <= bound, (n, bound)
@pytest.mark.parametrize("f,a,b", FLAT[1:])
def test_extrapolation_breaks_the_flat_cycle(f, a, b):
"""The mechanism the pre-registration rests on: on a pure power the similar successive
undershoots are replaced by a longer step, and the run gets cheaper."""
_x, n_off, _ = pure_root(f, a, b, XTOL, MAXFEV, similarity="ratio", scale="derived3",
guard="project")
_x, n_on, _ = pure_root(f, a, b, XTOL, MAXFEV, similarity="ratio", scale="derived3",
guard="project", extrapolate="steps")
assert n_on < n_off, (n_on, n_off)
def test_on_a_budget_bound_run_extrapolation_changes_the_path_not_the_count():
"""MEASURED, pinned as measured: on 6(x-2)^5 over [0, 10] both runs cost 90 evaluations
against a projection guarantee of 2*44 + 3 = 91 - the budget schedule, not the step,
decides when the run ends. The paths differ from the fourth evaluation on (87
positions), so the mode is active; its gain is absorbed by the safeguard here. The
expectation written first (strictly fewer evaluations) was wrong for this problem."""
f, a, b = FLAT[0]
ev = {}
for mode in ("none", "steps"):
seen = []
def g(t, _s=seen):
_s.append(t)
return f(t)
_x, n, ok = pure_root(g, a, b, XTOL, MAXFEV, similarity="ratio", scale="derived3",
guard="project", extrapolate=mode)
assert ok
ev[mode] = (n, seen)
assert ev["steps"][0] <= ev["none"][0]
assert ev["steps"][1] != ev["none"][1]
def test_unknown_mode_is_refused():
with pytest.raises(ValueError):
pure_root(lambda t: t, -1.0, 1.0, XTOL, MAXFEV, extrapolate="aitken")