Files
sim-numerics/stage0d_derived_similarity.py
T

143 lines
6.1 KiB
Python

"""Stage 0d - faster similarity functions, and similarity DERIVED FROM DATA.
Two questions, in order.
(1) Ratio-type similarities. S(x,y) = |x|/|y| and relatives change much
faster than 1/(1+z) or exp(-z). Stage 0c derived the contraction rate of
the pure step rule as
rate = | 1 - c * |f'(r)| * D / scale | , c = slope of (1 - S) at 0
so a similarity that changes FASTER has a larger c and moves the rate away
from 1 - but it overshoots just as easily as it undershoots, because
nothing ties c to the problem. Measured below, not argued.
(2) Similarity derived from the data. The same formula says the rate is
exactly ZERO when
c = scale / ( |f'(r)| * D ).
Nothing in that expression has to be chosen: |f'| is estimated by the
secant slope of the two most recent evaluations, D by the current distance
to the far end, scale by the value in play. Substituting it back:
step = (1 - S)*(far - x) = c*|f|/scale * D = |f| / |f'|
- the calibrated similarity step IS the Newton/secant step. So "derive
the similarity parameter from the data" is not a tweak of the pure idea;
it is the statement that the value axis has a LOCAL scale, and reading
that scale off the data is what turns linear convergence into superlinear.
This is the same move the article makes when it normalises the input space
from the data instead of choosing membership widths by hand.
"""
from __future__ import annotations
import math
from simroot.baselines import bisection, brent
from simroot.pure import PURE_SIMS, pure_root
XTOL = 1e-12
MAXFEV = 200
# --------------------------------------------------------------------------
# (1) fast, ratio-type [0,1] similarities of a normalised residual z
# --------------------------------------------------------------------------
# The similarity functions and the step rule now live in simroot.pure, which
# is the single implementation behind every number in this project - the
# benchmark, the tables below and the paper all call the same code. This file
# keeps only the subset of similarities that stage 0d is about, plus two thin
# delegations so the stage-0d and stage-0e tables read unchanged.
FAST_SIMS = {k: PURE_SIMS[k] for k in
("ratio", "sqrt", "cbrt", "ratio_soft", "rational", "exp_abs")}
def pure_step(f, a, b, xtol, maxfev, sim):
"""The step rule with the scale CHOSEN: |f| at the starting point."""
return pure_root(f, a, b, xtol, maxfev, similarity=sim, scale="start",
guard="none")
def derived_step(f, a, b, xtol, maxfev, sim="ratio", guard=True):
"""The step rule with the scale DERIVED from the two most recent
evaluations. `guard=True` keeps the bracket safeguard that stage 0d
measured; stage 0e compares it against no safeguard at all."""
return pure_root(f, a, b, xtol, maxfev, similarity=sim, scale="derived",
guard="bracket" if guard else "none")
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 _table(methods, title):
print("\n" + title)
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:
xx, n, conv = fn(f, a, b)
ok = conv and abs(xx - 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}")
def rate_table():
"""The measured contraction rate of each fast similarity, against the
rate predicted by the stage-0c law."""
f = lambda x: x * x - 2.0
root = math.sqrt(2.0)
fp, D, scale = 2.0 * math.sqrt(2.0), 2.0 - math.sqrt(2.0), 2.0
print("\ncontraction rate on x^2 - 2, pure rule, scale = |f(x0)|")
print(f"{'similarity':10s} {'c (slope of 1-S at 0)':>24s} "
f"{'predicted':>11s} {'measured':>10s}")
print("-" * 60)
for sim in FAST_SIMS:
S = FAST_SIMS[sim]
h = 1e-7
c = (1.0 - S(h)) / h # numerical slope of (1 - S) at 0
pred = abs(1.0 - c * fp * D / scale) if math.isfinite(c) else float("inf")
x, errs = 0.0, []
for _ in range(14):
x = x + (1.0 - S(abs(f(x)) / scale)) * (2.0 - x)
errs.append(abs(x - root))
win = [errs[i + 1] / errs[i] for i in range(len(errs) - 1) if errs[i] > 1e-11]
meas = sum(win[-3:]) / len(win[-3:]) if win else float("nan")
cs = f"{c:.3e}" if math.isfinite(c) else "inf"
ps = f"{pred:.4f}" if math.isfinite(pred) else "inf (overshoot)"
print(f"{sim:10s} {cs:>24s} {ps:>11s} {meas:>10.4f}")
def main():
rate_table()
_table([("bisection", lambda f, a, b: bisection(f, a, b, XTOL, MAXFEV)),
("brent [reference]", lambda f, a, b: brent(f, a, b, XTOL, MAXFEV))] +
[(f"fast.{s}", (lambda ss: lambda f, a, b: pure_step(
f, a, b, XTOL, MAXFEV, ss))(s)) for s in FAST_SIMS],
"(1) FAST similarity functions, parameter CHOSEN")
_table([("bisection", lambda f, a, b: bisection(f, a, b, XTOL, MAXFEV)),
("brent [reference]", lambda f, a, b: brent(f, a, b, XTOL, MAXFEV))] +
[(f"derived.{s}", (lambda ss: lambda f, a, b: derived_step(
f, a, b, XTOL, MAXFEV, ss))(s)) for s in FAST_SIMS],
"(2) similarity SCALE DERIVED FROM DATA (secant slope), guarded")
if __name__ == "__main__":
main()