import time

import numpy as np
from numpy import sin, cos, pi, exp, sqrt, abs
from scipy.optimize import rosen


class SimpleQuadratic:

    def fun(self, x):
        return np.dot(x, x)

    def der(self, x):
        return 2. * x

    def hess(self, x):
        return 2. * np.eye(x.size)


class AsymmetricQuadratic:

    def fun(self, x):
        return np.dot(x, x) + x[0]

    def der(self, x):
        d = 2. * x
        d[0] += 1
        return d

    def hess(self, x):
        return 2. * np.eye(x.size)


class SlowRosen:

    def fun(self, x):
        time.sleep(40e-6)
        return rosen(x)


class LJ:
    """
    The Lennard Jones potential

    a mathematically simple model that approximates the interaction between a
    pair of neutral atoms or molecules.
    https://en.wikipedia.org/wiki/Lennard-Jones_potential

    E = sum_ij V(r_ij)

    where r_ij is the cartesian distance between atom i and atom j, and the
    pair potential has the form

    V(r) = 4 * eps * ( (sigma / r)**12 - (sigma / r)**6

    Notes
    -----
    the double loop over many atoms makes this *very* slow in Python.  If it
    were in a compiled language it would be much faster.
    """

    def __init__(self, eps=1.0, sig=1.0):
        self.sig = sig
        self.eps = eps

    def vij(self, r):
        return 4. * self.eps * ((self.sig / r)**12 - (self.sig / r)**6)

    def dvij(self, r):
        p7 = 6. / self.sig * (self.sig / r)**7
        p13 = -12. / self.sig * (self.sig / r)**13
        return 4. * self.eps * (p7 + p13)

    def fun(self, coords):
        natoms = coords.size // 3
        coords = np.reshape(coords, [natoms, 3])
        energy = 0.
        for i in range(natoms):
            for j in range(i + 1, natoms):
                dr = coords[j, :] - coords[i, :]
                r = np.linalg.norm(dr)
                energy += self.vij(r)
        return energy

    def der(self, coords):
        natoms = coords.size // 3
        coords = np.reshape(coords, [natoms, 3])
        energy = 0.
        grad = np.zeros([natoms, 3])
        for i in range(natoms):
            for j in range(i + 1, natoms):
                dr = coords[j, :] - coords[i, :]
                r = np.linalg.norm(dr)
                energy += self.vij(r)
                g = self.dvij(r)
                grad[i, :] += -g * dr/r
                grad[j, :] += g * dr/r
        grad = grad.reshape([natoms * 3])
        return grad

    def get_random_configuration(self):
        rnd = np.random.uniform(-1, 1, [3 * self.natoms])
        return rnd * float(self.natoms)**(1. / 3)


class LJ38(LJ):
    natoms = 38
    target_E = -173.928427


class LJ30(LJ):
    natoms = 30
    target_E = -128.286571


class LJ20(LJ):
    natoms = 20
    target_E = -77.177043


class LJ13(LJ):
    natoms = 13
    target_E = -44.326801


class Booth:
    target_E = 0.
    solution = np.array([1., 3.])
    xmin = np.array([-10., -10.])
    xmax = np.array([10., 10.])

    def fun(self, coords):
        x, y = coords
        return (x + 2. * y - 7.)**2 + (2. * x + y - 5.)**2

    def der(self, coords):
        x, y = coords
        dfdx = 2. * (x + 2. * y - 7.) + 4. * (2. * x + y - 5.)
        dfdy = 4. * (x + 2. * y - 7.) + 2. * (2. * x + y - 5.)
        return np.array([dfdx, dfdy])


class Beale:
    target_E = 0.
    solution = np.array([3., 0.5])
    xmin = np.array([-4.5, -4.5])
    xmax = np.array([4.5, 4.5])

    def fun(self, coords):
        x, y = coords
        p1 = (1.5 - x + x * y)**2
        p2 = (2.25 - x + x * y**2)**2
        p3 = (2.625 - x + x * y**3)**2
        return p1 + p2 + p3

    def der(self, coords):
        x, y = coords
        dfdx = (2. * (1.5 - x + x * y) * (-1. + y) +
                2. * (2.25 - x + x * y**2) * (-1. + y**2) +
                2. * (2.625 - x + x * y**3) * (-1. + y**3))
        dfdy = (2. * (1.5 - x + x * y) * (x) +
                2. * (2.25 - x + x * y**2) * (2. * y * x) +
                2. * (2.625 - x + x * y**3) * (3. * x * y**2))
        return np.array([dfdx, dfdy])


"""
Global Test functions for minimizers.

HolderTable, Ackey and Levi have many competing local minima and are suited
for global minimizers such as basinhopping or differential_evolution.
(https://en.wikipedia.org/wiki/Test_functions_for_optimization)

See also https://mpra.ub.uni-muenchen.de/2718/1/MPRA_paper_2718.pdf

"""


class HolderTable:
    target_E = -19.2085
    solution = [8.05502, 9.66459]
    xmin = np.array([-10, -10])
    xmax = np.array([10, 10])
    stepsize = 2.
    temperature = 2.

    def fun(self, x):
        return - abs(sin(x[0]) * cos(x[1]) * exp(abs(1. - sqrt(x[0]**2 +
                     x[1]**2) / pi)))

    def dabs(self, x):
        """derivative of absolute value"""
        if x < 0:
            return -1.
        elif x > 0:
            return 1.
        else:
            return 0.

#commented out at the because it causes FloatingPointError in
#basinhopping
#     def der(self, x):
#         R = sqrt(x[0]**2 + x[1]**2)
#         g = 1. - R / pi
#         f = sin(x[0]) * cos(x[1]) * exp(abs(g))
#         E = -abs(f)
#
#         dRdx = x[0] / R
#         dgdx = - dRdx / pi
#         dfdx = cos(x[0]) * cos(x[1]) * exp(abs(g)) + f * self.dabs(g) * dgdx
#         dEdx = - self.dabs(f) * dfdx
#
#         dRdy = x[1] / R
#         dgdy = - dRdy / pi
#         dfdy = -sin(x[0]) * sin(x[1]) * exp(abs(g)) + f * self.dabs(g) * dgdy
#         dEdy = - self.dabs(f) * dfdy
#         return np.array([dEdx, dEdy])


class Ackley:
    # note: this function is not smooth at the origin.  the gradient will never
    # converge in the minimizer
    target_E = 0.
    solution = [0., 0.]
    xmin = np.array([-5, -5])
    xmax = np.array([5, 5])

    def fun(self, x):
        E = (-20. * exp(-0.2 * sqrt(0.5 * (x[0]**2 + x[1]**2))) + 20. + np.e -
             exp(0.5 * (cos(2. * pi * x[0]) + cos(2. * pi * x[1]))))
        return E

    def der(self, x):
        R = sqrt(x[0]**2 + x[1]**2)
        term1 = -20. * exp(-0.2 * R)
        term2 = -exp(0.5 * (cos(2. * pi * x[0]) + cos(2. * pi * x[1])))

        deriv1 = term1 * (-0.2 * 0.5 / R)

        dfdx = 2. * deriv1 * x[0] - term2 * pi * sin(2. * pi * x[0])
        dfdy = 2. * deriv1 * x[1] - term2 * pi * sin(2. * pi * x[1])

        return np.array([dfdx, dfdy])


class Levi:
    target_E = 0.
    solution = [1., 1.]
    xmin = np.array([-10, -10])
    xmax = np.array([10, 10])

    def fun(self, x):
        E = (sin(3. * pi * x[0])**2 + (x[0] - 1.)**2 *
             (1. + sin(3 * pi * x[1])**2) +
             (x[1] - 1.)**2 * (1. + sin(2 * pi * x[1])**2))
        return E

    def der(self, x):

        dfdx = (2. * 3. * pi *
                cos(3. * pi * x[0]) * sin(3. * pi * x[0]) +
                2. * (x[0] - 1.) * (1. + sin(3 * pi * x[1])**2))

        dfdy = ((x[0] - 1.)**2 * 2. * 3. * pi * cos(3. * pi * x[1]) * sin(3. *
                pi * x[1]) + 2. * (x[1] - 1.) *
                (1. + sin(2 * pi * x[1])**2) + (x[1] - 1.)**2 *
                2. * 2. * pi * cos(2. * pi * x[1]) * sin(2. * pi * x[1]))

        return np.array([dfdx, dfdy])


class EggHolder:
    target_E = -959.6407
    solution = [512, 404.2319]
    xmin = np.array([-512., -512])
    xmax = np.array([512., 512])

    def fun(self, x):
        a = -(x[1] + 47) * np.sin(np.sqrt(abs(x[1] + x[0]/2. + 47)))
        b = -x[0] * np.sin(np.sqrt(abs(x[0] - (x[1] + 47))))
        return a + b


class CrossInTray:
    target_E = -2.06261
    solution = [1.34941, -1.34941]
    xmin = np.array([-10., -10])
    xmax = np.array([10., 10])

    def fun(self, x):
        arg = abs(100 - sqrt(x[0]**2 + x[1]**2)/pi)
        val = np.power(abs(sin(x[0]) * sin(x[1]) * exp(arg)) + 1., 0.1)
        return -0.0001 * val


class Schaffer2:
    target_E = 0
    solution = [0., 0.]
    xmin = np.array([-100., -100])
    xmax = np.array([100., 100])

    def fun(self, x):
        num = np.power(np.sin(x[0]**2 - x[1]**2), 2) - 0.5
        den = np.power(1 + 0.001 * (x[0]**2 + x[1]**2), 2)
        return 0.5 + num / den


class Schaffer4:
    target_E = 0.292579
    solution = [0, 1.253131828927371]
    xmin = np.array([-100., -100])
    xmax = np.array([100., 100])

    def fun(self, x):
        num = cos(sin(abs(x[0]**2 - x[1]**2)))**2 - 0.5
        den = (1+0.001*(x[0]**2 + x[1]**2))**2
        return 0.5 + num / den
