# Implementation of TOMS748
# https://na.math.kit.edu/alefeld/download/1995_Algorithm_748_Enclosing_Zeros_of_Continuous_Functions.pdf

# from math import sin, fabs, cos
from math import *

# checks that none of the values are the same
# returns true if at least two values are identical
def checkTwoValuesIdentical(fa, fb, fc, fd):
    same = False
    same |= fa == fb
    same |= fa == fc
    same |= fa == fd

    same |= fb == fc
    same |= fb == fd

    same |= fc == fd
    return same

# finite differences 1st derivative given a, b, fa, fb
def fbracket1(a, b, fa, fb):
    return (fb-fa)/(b-a)

# finite differences 2nd derivative given a, b, d, fa, fb, fd
def fbracket2(a, b, d, fa, fb, fd):
    return (fbracket1(b, d, fb, fd) - fbracket1(a, b, fa, fb))/(d-a)

# standard bracketing routine
# returns ahat, bhat, d
# d is a point outside the new interval
def bracket(a, b, c, fa, fb, fc):
    if fa*fc < 0:
        return (a, c, b, fa, fc, fb)
    else:
        return (c, b, a, fc, fb, fa)

# standard bracketing routine
# returns ahat, bhat, d
# d is a point outside the new interval
def bracketAndCheckConvergence(a, b, c, fa, fb, fc, tol):
    ok = False
    if fabs(fc) < tol:
        ok = True
    if fa*fc < 0:
        return (a, c, b, fa, fc, fb, ok)
    else:
        return (c, b, a, fc, fb, fa, ok)

# finds an approximate solution to the quadratic P(x) = fa + f[a,b]*(x-a) + f[a,b,d]*(x-a)(x-b)
# with f[a,b] = fbracket1(a,b) and f[a,b,d] = fbracket2(a,b,d)
def NewtonQuadratic(a, b, d, fa, fb, fd, k):
    A = fbracket2(a,b,d,fa,fb,fd)
    B = fbracket1(a,b,fa,fb)
    if A == 0:
        return a - fa/B
    if A*fa > 0:
        r = a
    else:
        r = b

    for i in range(k):
        r = r - B*r/(B + A*(2*r - a - b))
    return r

# Inverse cubic interpolation evaluated at 0 (modified Aitken-Neville interpolation)
def ipzero(a, b, c, d, fa, fb, fc, fd):
    Q11 = (c-d)*fc/(fd-fc)
    Q21 = (b-c)*fb/(fc-fb)
    Q31 = (a-b)*fa/(fb-fa)
    D21 = (b-c)*fc/(fc-fb)
    D31 = (a-b)*fb/(fb-fa)
    Q22 = (D21-Q11)*fb/(fd-fb)
    Q32 = (D31-Q21)*fa/(fc-fa)
    D32 = (D31-Q21)*fc/(fc-fa)
    Q33 = (D32-Q22)*fa/(fd-fa)
    return a + Q31 + Q32 + Q33

def TOMS748_solve1(f, a, b, tol, Nmax = 1000):
    mu = 0.5
    fa, fb = f(a), f(b)
    c = a - fa/fbracket1(a, b, fa, fb)                              # 4.1.1 secant method
    fc = f(c)
    a, b, d, fa, fb, fd, ok = bracketAndCheckConvergence(a, b, c, fa, fb, fc, tol)              # 4.1.2
    if(ok): return (c, fc)
    e = d
    fe = fd
    # ---
    for n in range(2,Nmax):                                           # 4.1.3
        if n == 2 or checkTwoValuesIdentical(fa, fb, fd, fe):
            c = NewtonQuadratic(a, b, d, fa, fb, fd, 2)
        else:
            c = ipzero(a, b, d, e, fa, fb, fd, fe)
            if (c-a)*(c-b) >= 0:
                c = NewtonQuadratic(a, b, d, fa, fb, fd, 2)
        # ---
        fc = f(c)
        a, b, dbar, fa, fb, fdbar, ok = bracketAndCheckConvergence(a, b, c, fa, fb, fc, tol)    # 4.1.4
        if(ok): return (c, fc)
        # ---
        if fabs(fa) < fabs(fb):                                     # 4.1.5
            u = a
            fu = fa
        else:
            u = b
            fu = fb
        # ---
        c = u - 2*fu/fbracket1(a, b, fa, fb)                        # 4.1.6
        # ---
        if fabs(c - u) > 0.5*(b - a):                               # 4.1.7
            c = 0.5*(b + a)
        # ---
        fc = f(c)
        a, b, dhat, fa, fb, fdhat, ok = bracketAndCheckConvergence(a, b, c, fa, fb, fc, tol)    # 4.1.8
        if(ok): return (c, fc)
        # ---
        if b - a < mu*(b - a):                                      # 4.1.9
            d = dhat
            e = dbar
            fd = fdhat
            fe = fdbar
        else:
            e = dhat
            fe = fdhat
        c = 0.5*(a+b)
        fc = f(c)
        a, b, d, fa, fb, fd, ok = bracketAndCheckConvergence(a, b, c, fa, fb, fc, tol)
        if(ok): return (c, fc)
    return (c, fc)# no solution found, return last estimate

def TOMS748_solve2(f, a, b, tol, Nmax = 1000):
    mu = 0.5
    fa, fb = f(a), f(b)
    c = a - fa/fbracket1(a, b, fa, fb)                              # 4.2.1 secant method
    fc = f(c)
    a, b, d, fa, fb, fd, ok = bracketAndCheckConvergence(a, b, c, fa, fb, fc, tol)              # 4.2.2
    if(ok): return (c, fc)
    e = d
    fe = fd
    # ---
    for n in range(2,Nmax):                                           # 4.2.3
        if n == 2 or checkTwoValuesIdentical(fa, fb, fd, fe):
            c = NewtonQuadratic(a, b, d, fa, fb, fd, 2)
        else:
            c = ipzero(a, b, d, e, fa, fb, fd, fe)
            if (c-a)*(c-b) >= 0:
                c = NewtonQuadratic(a, b, d, fa, fb, fd, 2)
        # ---
        e = d
        fe = fd
        fc = f(c)
        a, b, d, fa, fb, fd, ok = bracketAndCheckConvergence(a, b, c, fa, fb, fc, tol)          # 4.2.4
        if(ok): return (c, fc)
        # ---
        if checkTwoValuesIdentical(fa, fb, fd, fe):                      # 4.2.5
            c = NewtonQuadratic(a, b, d, fa, fb, fd, 3)
        else:
            c = ipzero(a, b, d, e, fa, fb, fd, fe)
            if (c-a)*(c-b) >= 0:
                c = NewtonQuadratic(a, b, d, fa, fb, fd, 3)
        # ---
        fc = f(c)
        a, b, dbar, fa, fb, fdbar, ok = bracketAndCheckConvergence(a, b, c, fa, fb, fc, tol)    # 4.2.6
        if(ok): return (c, fc)
        # ---
        if fabs(fa) < fabs(fb):                                     # 4.2.7
            u = a
            fu = fa
        else:
            u = b
            fu = fb
        # ---
        c = u - 2*fu/fbracket1(a, b, fa, fb)                        # 4.2.8
        # ---
        if fabs(c - u) > 0.5*(b - a):                               # 4.2.9
            c = 0.5*(b + a)
        # ---
        fc = f(c)
        a, b, dhat, fa, fb, fdhat, ok = bracketAndCheckConvergence(a, b, c, fa, fb, fc, tol)    # 4.2.10
        if(ok): return (c, fc)
        # ---
        if b - a < mu*(b - a):                                      # 4.2.11
            d = dhat
            e = dbar
            fd = fdhat
            fe = fdbar
        else:
            e = dhat
            fe = fdhat
        c = 0.5*(a+b)
        fc = f(c)
        a, b, d, fa, fb, fd, ok = bracketAndCheckConvergence(a, b, c, fa, fb, fc, tol)
        if(ok): return (c, fc)
    return (c, fc)# no solution found, return last estimate

# TEST
f = lambda E : E - 0.5*sin(E) - 0.3
a, b, c, d = 0, 1, 0.3, 1.2
fa, fb, fc, fd = f(a), f(b), f(c), f(d)
tol = 1e-12

print(fbracket1(a, b, fa, fb))
print(fbracket2(a, b, d, fa, fb, fd))
print(bracket(a, b, c, fa, fb, fc))
print(NewtonQuadratic(a, b, d, fa, fb, fd, 2))
print(ipzero(a, b, c, d, fa, fb, fc, fd))

# TTTEST
print(checkTwoValuesIdentical(1,2,3,4))
print(checkTwoValuesIdentical(1,2,3,1))
print(checkTwoValuesIdentical(1,1,3,4))
print(checkTwoValuesIdentical(1,2,2,4))
print(checkTwoValuesIdentical(1,2,3,3))
print(checkTwoValuesIdentical(1,2,1,4))
print(checkTwoValuesIdentical(1,2,3,2))
# TTTEST

# solution using bracket only :
print('bracket only')
for i in range(50):
    c = (a+b)/2
    fc = f(c)
    a, b, d, fa, fb, fd = bracket(a, b, c, fa, fb, fc)
    if fabs(b-a) < tol:
        break
    print(i)

print((a,b), (a+b)/2, b-a)

print('Newton with derivative')
df = lambda E : 1 - 0.5*cos(E)
x = 0.5
for i in range(50):
    delta = f(x)/df(x)
    x = x - delta
    if fabs(delta) < tol:
        break
    print(i)
print(x)

print('TOMS748_solve1 only')
def fct1(E):
    print("x = %20.15f" % E)
    return E - 0.5*sin(E) - 0.3

c, fc = TOMS748_solve1(fct1, 0, 1, tol)
print("Root found :")
print((c, fc))

print('TOMS748_solve2 only')
c, fc = TOMS748_solve2(fct1, 0, 1, tol)
print("Root found :")
print((c, fc))

# ---

print('TOMS748_solve1 only')
def fct2(x):
    print("x = %20.15f" % x)
    n = 20
    return x**2 - (1-x)**n

c, fc = TOMS748_solve1(fct2, 0, 1, tol)
print("Root found :")
print((c, fc))

print('TOMS748_solve2 only')
c, fc = TOMS748_solve2(fct2, 1e-100, 1, tol)# if a is 0 the algorithm does not work (division by zero, because newton step a = 0 finds d = 0). It seems to be just this one case...
print("Root found :")
print((c, fc))

print('Newton with derivative')
dfct2 = lambda x : 2*x + 20*(-x + 1)**19
x = 0.5
for i in range(50):
    delta = fct2(x)/dfct2(x)
    x = x - delta
    if fabs(delta) < tol:
        break
    print(i)
print(x)

# ---

print('--------------------------')
print('fct3')
print('TOMS748_solve1 only')
def fct3(x):
    print("x = %20.15f" % x)
    return x*exp(-x**2)
# a, b = -.5, 1.5
a, b = -1/sqrt(2), 2/sqrt(2)

c, fc = TOMS748_solve1(fct3, a, b, tol)
print("Root found :")
print((c, fc))

print('TOMS748_solve2 only')
c, fc = TOMS748_solve2(fct3, a, b, tol)# if a is 0 the algorithm does not work (division by zero, because newton step a = 0 finds d = 0). It seems to be just this one case...
print("Root found :")
print((c, fc))

print('Newton with derivative')
dfct3 = lambda x : (-2*x**2 + 1)*exp(-x**2)
x = (a+b)/2
for i in range(50):
    delta = fct3(x)/dfct3(x)
    x = x - delta
    if fabs(delta) < tol:
        break
    print(i)
print(x)
