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:
vvs
2026-09-22 08:50:37 +01:00
co-authored by Claude Opus 5
parent 2fe6d729a8
commit 34b2a30d6c
5 changed files with 311 additions and 0 deletions
+113
View File
@@ -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