Files

117 lines
4.5 KiB
Python

"""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
# "lag2": POSTHOC_lag_branch_2026-09-22 - Lagouanelle on the branch m >= 2 only (post hoc)
ESTIMATORS = ("none", "lag", "analogy", "lag2")
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 in ("lag", "lag2"):
m = _lagouanelle_raw(xc, fc, xp, fp, xp2, fp2)
if estimator == "lag2" and m is not None and m < 2.0:
m = None # the fractional branch is not used (post hoc variant)
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