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,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
|
||||
Reference in New Issue
Block a user