Files
sim-numerics/stage0b_pure_similarity.py
T
vvsandClaude Opus 5 68df1d25db Sign tests by product underflowed: FIXED everywhere (branch, not master - the paper is not yet updated). FA * fnew < 0.0 with FA ~ -1e-200, fnew ~ 1e-150 is -0.0 and '-0.0 < 0.0' is False, so on flat functions (x^19) the bracket update moved the wrong end, the root left the bracket and the run still reported convergence - the proposed rule returned 4.9e-9, bisection 3.5e-9, ITP 1.6e-8 with a claimed 1e-12 bracket; the 1e-6 harness scored them solved. Same defect in our bisection, false-position family, Ridders, Brent, ITP and twelve copies in stage scripts, simroot/sim.py and one test; all replaced by _opposite / _same_sign, pinned by test_sign_underflow.
Impact, every sweep regenerated: the paper's key configurations are unchanged to the evaluation (proposed 2806/3220, credit 3568/3020, Chandrupatla, TOMS 748, Brent, bisection); 46 stated numbers move (ITP CHA 2857 -> 2896, ITP oracle 2611 -> 2647, record CHA figures by 14-18, the grid means by ~1%) and one finding weakens: the measured-order exponent of section 7.1 was cheaper in 29 of 60 far cells and 7 of 7 safeguards; with the fix 12 of 61 and 6 of 7, and on the wide-bracket set it now loses in every safeguard and drops 5 problems.

PREREG G1-G3 outcome on the fixed code: G2 (repeating step pattern, period from the data) PASSES Y1-Y4 and is ACCEPTED by the stop rule - proposed rule APS 2806 -> 2782 (below Chandrupatla's 2787), CHA 3220 -> 3169, both 6026 -> 5951, per problem vs Chandrupatla on APS 95/20/38 p = 8e-7. G1 fails Y1/Y2 (its Y3 miss was the sign bug), G3 fails Y2, A fails Y2. Regression 887 pass; the two red tests are the paper-number check (awaiting the paper update) and test_blend (fixed since, copy in stage16).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 12:49:01 +01:00

161 lines
6.2 KiB
Python

"""Stage 0b - the PURE similarity idea, taken literally as a STEP-SIZE rule.
Valeri's formulation, word for word:
"in bisection the next point is chosen through half of the interval.
With similarity the next point is chosen proportionally to the value of
the similarity function (0 = not similar at all, 1 = identical) of the
result to the expectation (e.g. zero). So the step is LARGER when the
result is not similar to the expectation, and SMALLER when it is."
This is NOT the same object as Stage 0. Stage 0 asked "where between a and b
does the root sit" (an interpolation rule, which turned out to be regula
falsi identically). Here the rule is:
direction <- given by the sign (towards the side that brackets the root)
step <- (1 - S(f(x_n))) * (distance to the far end of the bracket)
with S a genuine [0,1] similarity function of a single value f(x) to the
target 0. Bisection is the special case S == 1/2 for every input, i.e.
"the similarity function carries no information".
The whole content of the method is therefore the similarity function itself.
The one thing S needs that a two-point rule did not is a SCALE: how large is
|f| = 3? Any constant here would be a hidden tuning parameter. Three
scale sources are compared, all taken from the run itself:
s0 the value at the starting point (fixed reference)
srun the largest |f| seen so far (growing reference)
slive the largest |f| at the current bracket ends (shrinking reference)
and the last one is the one that reproduces Stage 0 / regula falsi for the
linear S, which is exactly the collapse this stage is meant to expose.
"""
from __future__ import annotations
from simroot.baselines import _opposite, _same_sign # 2026-09-21 fix
import math
from simroot.baselines import Counter, _Budget, bisection, brent
XTOL = 1e-12
MAXFEV = 200
# --------------------------------------------------------------------------
# genuine [0,1] similarity functions of a normalised residual z = f / scale
# S(0) = 1 (identical to the expectation), S(large) -> 0 (not similar at all)
# --------------------------------------------------------------------------
SIMS_01 = {
# linear ramp, saturating at the reference scale
"linear": lambda z: max(0.0, 1.0 - abs(z)),
# rational, never saturates
"rational": lambda z: 1.0 / (1.0 + abs(z)),
# rational in the square
"rat_sq": lambda z: 1.0 / (1.0 + z * z),
# exponential decay of the distance
"exp_abs": lambda z: math.exp(-abs(z)),
# the article's negative squared Euclidean distance, exponentiated to [0,1]
"exp_sq": lambda z: math.exp(-z * z),
# cosine similarity of (1, f) against (1, 0) - article eq. (4) on the
# value axis lifted to 2-D
"cosine": lambda z: 1.0 / math.sqrt(1.0 + z * z),
}
SCALES = ("s0", "srun", "slive")
def pure_similarity_step(f, a, b, xtol, maxfev, sim="rational", scale="s0"):
"""The literal step-size rule. Bracket kept only for safety and for
knowing which way to walk; the step LENGTH comes from similarity alone."""
S = SIMS_01[sim]
g = Counter(f, maxfev)
x = 0.5 * (a + b)
try:
fa, fb = g(a), g(b)
if fa == 0.0:
return a, g.n, True
if fb == 0.0:
return b, g.n, True
# start from the endpoint whose result is already more similar to 0
if abs(fa) <= abs(fb):
x, fx, far, ffar = a, fa, b, fb
else:
x, fx, far, ffar = b, fb, a, fa
s0 = abs(fx)
smax = max(abs(fa), abs(fb))
while abs(far - x) > xtol:
if scale == "s0":
sc = s0
elif scale == "srun":
sc = smax
else:
sc = max(abs(fx), abs(ffar))
if sc == 0.0:
return x, g.n, True
s = S(abs(fx) / sc) # 1 = the result already IS zero
step = (1.0 - s) * (far - x) # dissimilar -> long step
x_new = x + step
lo, hi = (x, far) if x < far else (far, x)
if not math.isfinite(x_new) or not (lo < x_new < hi):
x_new = 0.5 * (x + far)
fnew = g(x_new)
if fnew == 0.0:
return x_new, g.n, True
smax = max(smax, abs(fnew))
if _opposite(fx, fnew):
# the root is between the old point and the new one
far, ffar = x, fx
x, fx = x_new, fnew
return x, g.n, True
except _Budget:
return x, g.n, False
PROBLEMS = [
("x^2 - 2", lambda x: x * x - 2.0, 0.0, 2.0, math.sqrt(2.0)),
("cos(x) - x", lambda x: math.cos(x) - x, 0.0, 1.0, 0.7390851332151607),
("exp(x) - 2", lambda x: math.exp(x) - 2.0, 0.0, 2.0, math.log(2.0)),
("x^3 - x - 2", lambda x: x ** 3 - x - 2.0, 1.0, 2.0, 1.5213797068045676),
("sin(x) - 0.5", lambda x: math.sin(x) - 0.5, 0.0, 1.5, math.asin(0.5)),
("x^10 - 1 STALL", lambda x: x ** 10 - 1.0, 0.0, 1.3, 1.0),
("exp(-x)(x-1) FLAT", lambda x: math.exp(-x) * (x - 1.0), -1.0, 40.0, 1.0),
("atan(x)", lambda x: math.atan(x), -1.0, 3.0, 0.0),
]
def run():
methods = [("bisection (S=1/2)", lambda f, a, b: bisection(f, a, b, XTOL, MAXFEV)),
("brent [reference]", lambda f, a, b: brent(f, a, b, XTOL, MAXFEV))]
for sc in SCALES:
for s in SIMS_01:
methods.append((
f"pure.{s}.{sc}",
(lambda ss, scl: lambda f, a, b: pure_similarity_step(
f, a, b, XTOL, MAXFEV, sim=ss, scale=scl))(s, sc)))
width = max(len(n) for n, _ in methods) + 1
header = (f"{'method':{width}s}" +
"".join(f"{p[0][:13]:>15s}" for p in PROBLEMS) +
f"{'TOTAL':>8s}{'FAIL':>6s}")
print(header)
print("-" * len(header))
for name, fn in methods:
cells, total, fails = [], 0, 0
for _, f, a, b, root in PROBLEMS:
x, n, conv = fn(f, a, b)
ok = conv and abs(x - root) <= 1e-8
cells.append(f"{n:>15d}" if ok else f"{'FAIL(' + str(n) + ')':>15s}")
total += n
fails += 0 if ok else 1
print(f"{name:{width}s}" + "".join(cells) + f"{total:>8d}{fails:>6d}")
if __name__ == "__main__":
run()