Files
sim-numerics/stage0b_pure_similarity.py
T

159 lines
6.1 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
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 fx * fnew < 0.0:
# 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()