K3 outcome (PREREG_estimated_transform, Linux run): the 6.6 claim stands. Corpus 4: A' 3656 vs Chandrupatla on sign(f)|f|^(1/m_hat) with the analogy order 3806 (A' 126/38/76, p=5e-4) and with Lagouanelle 7526; six corpora 13032 vs 15733 and 30991. On multiple/fractional roots the analogy order inside Chandrupatla closes 59%, 58%, 31% of the A'->oracle gap (CHA 322 vs 654, corpus 1 721 vs 1229, corpus 4 1766 vs 2159) and pays on simple roots (APS 3453 vs 2595); Lagouanelle on Chandrupatla's side-alternating triples reads a fractional order on 115 of 154 simple APS roots.
simroot/transformed.py: Chandrupatla on G(f) with m_hat estimated in the run; bit-identical to chandrupatla with no estimate (test). Bug found and pinned by a test: |f|^(1/m_hat) overflowed with an OverflowError on the fractional branch; now inf and the step halves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
E1 - corpus 4 (decisive): K3 total vs A' total; A' w/t/l against K3, p
|
||||
k3_lag: 7526 vs A' 3656 A' 199/24/17 p=1.5e-40 -> stands
|
||||
k3_analogy: 3806 vs A' 3656 A' 126/38/76 p=0.00053 -> stands
|
||||
outcome for the claim of 6.6: stands
|
||||
|
||||
E2 - six corpora totals (violations of the 2x bound in brackets)
|
||||
K3L APS 7215(0) CHA 2188(0) NEW 6618(0) RJL 3167(0) APP 4277(0) D 7526(0) | all 30991
|
||||
K3A APS 3453(0) CHA 952(0) NEW 4433(0) RJL 1416(0) APP 1673(0) D 3806(0) | all 15733
|
||||
A' APS 2259 CHA 1074 NEW 3290 RJL 1371 APP 1382 D 3656 | all 13032
|
||||
Chandrupatla(f) APS 2595 CHA 1505 NEW 3716 RJL 1566 APP 1382 D 5717 | all 16481
|
||||
|
||||
E3 - multiple/fractional roots (group of Table 11): K3L / K3A / A' / oracle (Chandrupatla on g)
|
||||
CHA: n=20 k3_lag 1062 (closes -73% of A'->oracle) k3_analogy 322 (closes +59% of A'->oracle) A' 654 oracle 96
|
||||
NEW: n=40 k3_lag 1699 (closes -54% of A'->oracle) k3_analogy 721 (closes +58% of A'->oracle) A' 1229 oracle 358
|
||||
D: n=88 k3_lag 3280 (closes -89% of A'->oracle) k3_analogy 1766 (closes +31% of A'->oracle) A' 2159 oracle 901
|
||||
|
||||
E4 - the other problems: K3 against Chandrupatla on f (cost of false readings)
|
||||
CHA: k3_lag 1126 k3_analogy 630 Chandrupatla(f) 437 A' 420
|
||||
NEW: k3_lag 4919 k3_analogy 3712 Chandrupatla(f) 1924 A' 2061
|
||||
D: k3_lag 4246 k3_analogy 2040 Chandrupatla(f) 1566 A' 1497
|
||||
k3_lag vs Chandrupatla(f), APS: 7215 vs 2595
|
||||
k3_analogy vs Chandrupatla(f), APS: 3453 vs 2595
|
||||
estimated done
|
||||
@@ -0,0 +1,113 @@
|
||||
"""PREREG_estimated_transform_2026-09-22 (control K3): Chandrupatla's method on
|
||||
g = sign(f)|f|^(1/m_hat), with m_hat estimated during the run.
|
||||
|
||||
The solver is simroot.baselines.chandrupatla line for line (it matches scipy's
|
||||
elementwise.find_root on all 199 problems); it keeps the TRUE values of f at its three points, so
|
||||
signs, the best point and the stopping rule are those of f, and only the acceptance band's
|
||||
(xi, phi) and the inverse quadratic interpolation read G(v) = sign(v)|v|^(1/m_hat), recomputed
|
||||
from the stored f whenever m_hat changes - no extra evaluation. m_hat starts at 1 and is
|
||||
replaced by an order read from the last three evaluations only when it lies on a branch of
|
||||
section 6.1 (m >= 2 or 0 < m <= 2/3); there is no reset. With estimator="none" the method is
|
||||
the original, bit for bit (tested).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
from .baselines import Counter, _Budget
|
||||
from .pure import _analogy_order, _config3, _lagouanelle_raw
|
||||
|
||||
ESTIMATORS = ("none", "lag", "analogy")
|
||||
|
||||
|
||||
def _G(v, inv):
|
||||
if inv == 1.0:
|
||||
return v
|
||||
if v == 0.0:
|
||||
return 0.0
|
||||
try:
|
||||
return math.copysign(abs(v) ** inv, v)
|
||||
except OverflowError:
|
||||
# 1/m_hat > 1 on the fractional branch: a huge |f| overflows; the interpolation that
|
||||
# reads it is then not finite and the step halves, as the method does for any such t
|
||||
return math.copysign(math.inf, v)
|
||||
|
||||
|
||||
def chandrupatla_transformed(f, a, b, xtol, maxfev, estimator="none", stats=None):
|
||||
if estimator not in ESTIMATORS:
|
||||
raise ValueError(f"unknown estimator {estimator!r}")
|
||||
g = Counter(f, maxfev)
|
||||
x1, x2 = b, a
|
||||
f1 = f2 = 0.0
|
||||
m_hat = 1.0
|
||||
hist = [] # evaluations in order: (x, f)
|
||||
configs = [] # analogy memory (estimator "analogy")
|
||||
|
||||
def observe(x, fx):
|
||||
nonlocal m_hat
|
||||
hist.append((x, fx))
|
||||
if estimator == "none" or len(hist) < 3:
|
||||
return
|
||||
(xp2, fp2), (xp, fp), (xc, fc) = hist[-3], hist[-2], hist[-1]
|
||||
if estimator == "lag":
|
||||
m = _lagouanelle_raw(xc, fc, xp, fp, xp2, fp2)
|
||||
else:
|
||||
cur = _config3(xc, fc, xp, fp, xp2, fp2)
|
||||
m = _analogy_order(cur, configs, translation=True)
|
||||
if cur is not None:
|
||||
configs.append(cur)
|
||||
if m is not None and (m >= 2.0 or 0.0 < m <= 2.0 / 3.0):
|
||||
if stats is not None and m != m_hat:
|
||||
stats["updates"] = stats.get("updates", 0) + 1
|
||||
m_hat = m
|
||||
if stats is not None:
|
||||
stats["m_hat"] = m_hat # diagnostic only: the order last held
|
||||
|
||||
try:
|
||||
f1 = g(x1)
|
||||
observe(x1, f1)
|
||||
f2 = g(x2)
|
||||
observe(x2, f2)
|
||||
if f1 == 0.0:
|
||||
return x1, g.n, True
|
||||
if f2 == 0.0:
|
||||
return x2, g.n, True
|
||||
x3, f3 = x2, f2
|
||||
t = 0.5
|
||||
while True:
|
||||
x = x1 + t * (x2 - x1)
|
||||
fx = g(x)
|
||||
observe(x, fx)
|
||||
if (fx > 0.0) == (f1 > 0.0):
|
||||
x3, f3 = x1, f1
|
||||
x1, f1 = x, fx
|
||||
else:
|
||||
x3, f3 = x2, f2
|
||||
x2, f2 = x1, f1
|
||||
x1, f1 = x, fx
|
||||
xm, fm = (x1, f1) if abs(f1) < abs(f2) else (x2, f2)
|
||||
if fm == 0.0:
|
||||
return xm, g.n, True
|
||||
tol = 2.0 * 2.22e-16 * abs(xm) + 0.5 * xtol
|
||||
span = abs(x2 - x1)
|
||||
if span == 0.0:
|
||||
return xm, g.n, True
|
||||
tl = tol / span
|
||||
if tl > 0.5:
|
||||
return xm, g.n, True
|
||||
inv = 0.0 if math.isinf(m_hat) else 1.0 / m_hat
|
||||
G1, G2, G3 = _G(f1, inv), _G(f2, inv), _G(f3, inv)
|
||||
t = 0.5
|
||||
if x3 != x2 and G3 != G2:
|
||||
xi = (x1 - x2) / (x3 - x2)
|
||||
ph = (G1 - G2) / (G3 - G2)
|
||||
if 0.0 < xi < 1.0 and 1.0 - math.sqrt(1.0 - xi) < ph < math.sqrt(xi):
|
||||
d1, d2, d3 = G1 - G2, G1 - G3, G2 - G3
|
||||
if d1 != 0.0 and d2 != 0.0 and d3 != 0.0:
|
||||
t = (G1 / d1) * (G3 / (-d3)) + \
|
||||
((x3 - x1) / (x2 - x1)) * (G1 / d2) * (G2 / d3)
|
||||
if not math.isfinite(t):
|
||||
t = 0.5
|
||||
t = min(max(t, tl), 1.0 - tl)
|
||||
except _Budget:
|
||||
return (x1 if abs(f1) < abs(f2) else x2), g.n, False
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,114 @@
|
||||
"""PREREG_estimated_transform_2026-09-22 (control K3): Chandrupatla on sign(f)|f|^(1/m_hat), m_hat
|
||||
estimated during the run by Lagouanelle (K3L) or by A's analogy order (K3A), on the six corpora with
|
||||
the harness of sweep_fourth.py (same problems, seeding, charging, uniform criterion). Report E1-E4.
|
||||
|
||||
python -u sweep_estimated.py run -> sweep_estimated.json, then the report
|
||||
python -u sweep_estimated.py --report
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import json
|
||||
import sys
|
||||
from multiprocessing import Pool
|
||||
|
||||
import sweep_fourth
|
||||
import sweep_full
|
||||
from simroot.transformed import chandrupatla_transformed
|
||||
from sweep_controls import nonsimple
|
||||
from sweep_oos import sign_p
|
||||
|
||||
for _name, _est in (("k3_lag", "lag"), ("k3_analogy", "analogy")):
|
||||
sweep_full.EXTRA_BASELINES[_name] = functools.partial(chandrupatla_transformed, estimator=_est)
|
||||
sweep_fourth.CONFIGS[_name] = ("base", _name)
|
||||
NAMES = ("k3_lag", "k3_analogy")
|
||||
SETS = ("APS", "CHA", "NEW", "RJL", "APP", "D")
|
||||
|
||||
|
||||
def job(j):
|
||||
name, which = j
|
||||
return name, sweep_fourth.evaluate((name, which))[1]
|
||||
|
||||
|
||||
def cost(v):
|
||||
return v[0] if v[1] else 200
|
||||
|
||||
|
||||
def tot(r, keep=None):
|
||||
return sum(cost(v) for p, v in r.items() if keep is None or keep(p))
|
||||
|
||||
|
||||
def h2h(a, b, keep=None):
|
||||
ps = [p for p in a if keep is None or keep(p)]
|
||||
w = sum(cost(a[p]) < cost(b[p]) for p in ps)
|
||||
l = sum(cost(a[p]) > cost(b[p]) for p in ps)
|
||||
return w, len(ps) - w - l, l
|
||||
|
||||
|
||||
def report(R):
|
||||
seen = json.load(open("sweep_fourth_seen.json", encoding="utf8"))
|
||||
four = json.load(open("sweep_fourth_fourth.json", encoding="utf8"))
|
||||
ref = {}
|
||||
for k in ("Aprime", "chandrupatla"):
|
||||
ref[k] = dict(seen[k])
|
||||
ref[k].update(four[k])
|
||||
ap, ch = ref["Aprime"], ref["chandrupatla"]
|
||||
K1 = json.load(open("sweep_controls.json", encoding="utf8"))["K1"]
|
||||
print("E1 - corpus 4 (decisive): K3 total vs A' total; A' w/t/l against K3, p")
|
||||
verdict = []
|
||||
for k in NAMES:
|
||||
w, t, l = h2h(ap["D"], R[k]["D"])
|
||||
k3_not_worse = tot(R[k]["D"]) <= tot(ap["D"])
|
||||
k3_wins = l > w and sign_p(w, l) < 0.05
|
||||
a_wins = tot(ap["D"]) < tot(R[k]["D"]) and w > l and sign_p(w, l) < 0.05
|
||||
verdict.append("withdraw" if (k3_not_worse or k3_wins) else ("stands" if a_wins else "intermediate"))
|
||||
print(f" {k}: {tot(R[k]['D'])} vs A' {tot(ap['D'])} A' {w}/{t}/{l} p={sign_p(w, l):.2g} -> {verdict[-1]}")
|
||||
overall = "withdraw" if "withdraw" in verdict else ("stands" if all(v == "stands" for v in verdict)
|
||||
else "intermediate")
|
||||
print(f" outcome for the claim of 6.6: {overall}")
|
||||
print("\nE2 - six corpora totals (violations of the 2x bound in brackets)")
|
||||
for k, r in (("K3L", R["k3_lag"]), ("K3A", R["k3_analogy"]), ("A'", ap), ("Chandrupatla(f)", ch)):
|
||||
cells = " ".join(f"{s} {tot(r[s])}" + (f"({sum(v[2] for v in r[s].values())})" if k.startswith("K3") else "")
|
||||
for s in SETS)
|
||||
print(f" {k:<16} {cells} | all {sum(tot(r[s]) for s in SETS)}")
|
||||
print("\nE3 - multiple/fractional roots (group of Table 11): K3L / K3A / A' / oracle (Chandrupatla on g)")
|
||||
for s in ("CHA", "NEW", "D"):
|
||||
m = K1[s]["mstar"]
|
||||
keep = lambda p, _m=m: nonsimple(_m[p]) # noqa: E731
|
||||
orc = tot(K1[s]["chandrupatla_g"], keep)
|
||||
a_ = tot(ap[s], keep)
|
||||
row = []
|
||||
for k in NAMES:
|
||||
v = tot(R[k][s], keep)
|
||||
share = (a_ - v) / (a_ - orc) if a_ != orc else float("nan")
|
||||
row.append(f"{k} {v} (closes {100 * share:+.0f}% of A'->oracle)")
|
||||
print(f" {s}: n={sum(1 for p in m if keep(p))} " + " ".join(row) + f" A' {a_} oracle {orc}")
|
||||
print("\nE4 - the other problems: K3 against Chandrupatla on f (cost of false readings)")
|
||||
for s in ("CHA", "NEW", "D"):
|
||||
m = K1[s]["mstar"]
|
||||
keep = lambda p, _m=m: not nonsimple(_m[p]) # noqa: E731
|
||||
print(f" {s}: " + " ".join(f"{k} {tot(R[k][s], keep)}" for k in NAMES)
|
||||
+ f" Chandrupatla(f) {tot(ch[s], keep)} A' {tot(ap[s], keep)}")
|
||||
for k in NAMES:
|
||||
keep = lambda p: True # noqa: E731
|
||||
print(f" {k} vs Chandrupatla(f), APS: {tot(R[k]['APS'])} vs {tot(ch['APS'])}")
|
||||
|
||||
|
||||
def main():
|
||||
if "--report" not in sys.argv:
|
||||
jobs = [(n, w) for n in NAMES for w in ("seen", "fourth")]
|
||||
with Pool(len(jobs)) as pool:
|
||||
out = pool.map(job, jobs)
|
||||
R = {}
|
||||
for name, res in out:
|
||||
R.setdefault(name, {}).update(res)
|
||||
with open("sweep_estimated.json", "w", encoding="utf8") as fh:
|
||||
json.dump(R, fh)
|
||||
with open("sweep_estimated.json", encoding="utf8") as fh:
|
||||
R = json.load(fh)
|
||||
report(R)
|
||||
print("estimated done")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,60 @@
|
||||
"""PREREG_estimated_transform_2026-09-22: the control K3 must be Chandrupatla's method itself when
|
||||
no order is estimated, and must stay a bracketing method when one is."""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import pytest
|
||||
|
||||
from simroot.baselines import chandrupatla
|
||||
from simroot.transformed import chandrupatla_transformed
|
||||
from sweep_full import load_problems
|
||||
|
||||
PROBS = load_problems()
|
||||
|
||||
|
||||
def test_without_an_estimate_it_is_chandrupatla_bit_for_bit():
|
||||
bad = []
|
||||
for s, pid, f, a, b, _r in PROBS:
|
||||
r1 = chandrupatla(f, a, b, 1e-12, 200)
|
||||
r2 = chandrupatla_transformed(f, a, b, 1e-12, 200, estimator="none")
|
||||
if (float(r1[0]).hex(), r1[1], r1[2]) != (float(r2[0]).hex(), r2[1], r2[2]):
|
||||
bad.append(pid)
|
||||
assert not bad, bad[:5]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("estimator", ("lag", "analogy"))
|
||||
def test_with_an_estimate_it_still_brackets(estimator):
|
||||
for s, pid, f, a, b, _r in PROBS:
|
||||
x, n, ok = chandrupatla_transformed(f, a, b, 1e-12, 200, estimator=estimator)
|
||||
assert min(a, b) <= x <= max(a, b), pid
|
||||
assert n <= 200
|
||||
|
||||
|
||||
def test_the_estimate_is_read_and_used_on_a_pure_power():
|
||||
"""The analogy estimator must reach m_hat on (t - 1)^9 and pay. Lagouanelle's is NOT asserted:
|
||||
on Chandrupatla's iterates, which alternate sides of the root, its three-point parabola reads
|
||||
m ~ 1 at every step - an outcome of the experiment, reported by the run, not a property
|
||||
to pin here."""
|
||||
f = lambda t: (t - 1.0) ** 9 # noqa: E731
|
||||
plain = chandrupatla_transformed(f, -2.0, 5.0, 1e-12, 200, estimator="none")[1]
|
||||
st = {}
|
||||
x, n, ok = chandrupatla_transformed(f, -2.0, 5.0, 1e-12, 200, estimator="analogy", stats=st)
|
||||
assert ok and abs(x - 1.0) < 1e-6
|
||||
assert st.get("updates", 0) >= 1
|
||||
assert n < plain, (n, plain)
|
||||
x, n, ok = chandrupatla_transformed(f, -2.0, 5.0, 1e-12, 200, estimator="lag")
|
||||
assert ok and abs(x - 1.0) < 1e-6
|
||||
|
||||
|
||||
def test_an_overflowing_transform_halves_instead_of_raising():
|
||||
"""Found 2026-09-22: on the fractional branch 1/m_hat > 1 and |f|^(1/m_hat) overflowed with
|
||||
an OverflowError that escaped the solver. Now it is inf, the interpolation is refused, and
|
||||
the step halves."""
|
||||
from simroot.transformed import _G
|
||||
assert _G(1e300, 3.0) == math.inf and _G(-1e300, 3.0) == -math.inf
|
||||
|
||||
|
||||
def test_unknown_estimator_is_refused():
|
||||
with pytest.raises(ValueError):
|
||||
chandrupatla_transformed(math.sin, 3.0, 4.0, 1e-12, 200, estimator="guess")
|
||||
Reference in New Issue
Block a user