"""
Nonlinear ODEs. Wrapper for emulator dynamical models
    + Internal Emulators - in house ground truth equations
    + External Emulators - third party models
References:
    + https://en.wikipedia.org/wiki/List_of_nonlinear_ordinary_differential_equations
    + https://en.wikipedia.org/wiki/List_of_dynamical_systems_and_differential_equations_topics
"""
import numpy as np
import inspect, sys
from psl.emulator import EmulatorBase
from scipy.integrate import odeint
[docs]class ODE_Autonomous(EmulatorBase):
    """
    base class autonomous ODE
    """
[docs]    def simulate(self, ninit=None, nsim=None, ts=None, x0=None):
        """
        :param nsim: (int) Number of steps for open loop response
        :param ninit: (float) initial simulation time
        :param ts: (float) step size, sampling time
        :param x0: (float) state initial conditions
        :return: The response matrices, i.e. X
        """
        if ninit is None:
            ninit = self.ninit
        if nsim is None:
            nsim = self.nsim
        if ts is None:
            ts = self.ts
        if x0 is None:
            x = self.x0
        else:
            assert x0.shape[0] == self.nx, "Mismatch in x0 size"
            x = x0
        t = np.arange(0, nsim+1) * ts + ninit
        X = [x]
        for N in range(nsim-1):
            dT = [t[N], t[N + 1]]
            xdot = odeint(self.equations, x, dT)
            x = xdot[-1]
            X.append(x)
        Yout = np.asarray(X).reshape(nsim, -1)
        return {'Y': Yout, 'X': np.asarray(X)}  
[docs]class UniversalOscillator(ODE_Autonomous):
    """
    Harmonic oscillator
    + https://en.wikipedia.org/wiki/Harmonic_oscillator
    + https://sam-dolan.staff.shef.ac.uk/mas212/notebooks/ODE_Example.html
    """
    def __init__(self, nsim=1001, ninit=0, ts=0.1, seed=59):
        super().__init__(nsim=nsim, ninit=ninit, ts=ts, seed=seed)
        self.mu = 2
        self.omega = 1
        self.x0 = [1.0, 0.0]
        self.nx = 2
[docs]    def equations(self, x, t):
        dx1 = x[1]
        dx2 = -2*self.mu*x[1] - x[0] + np.cos(self.omega*t)
        dx = [dx1, dx2]
        return dx  
[docs]class Pendulum(ODE_Autonomous):
    """
    Simple pendulum.
    + https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.odeint.html
    """
    def __init__(self, nsim=1001, ninit=0, ts=0.1, seed=59):
        super().__init__(nsim=nsim, ninit=ninit, ts=ts, seed=seed)
        self.g = 9.81
        self.f = 3.
        self.nx = 2
        self.x0 = [0., 1.]
[docs]    def equations(self, x, t):
        theta = x[0]
        omega = x[1]
        return [omega, -self.f*omega - self.g*np.sin(theta)]  
[docs]class DoublePendulum(ODE_Autonomous):
    """
    Double Pendulum
    https://scipython.com/blog/the-double-pendulum/
    """
    def __init__(self, nsim=1001, ninit=0, ts=0.1, seed=59):
        super().__init__(nsim=nsim, ninit=ninit, ts=ts, seed=seed)
        self.L1 = 1
        self.L2 = 1
        self.m1 = 1
        self.m2 = 1
        self.g = 9.81
        self.x0 = np.array([3 * np.pi / 7, 0, 3 * np.pi / 4, 0])
        self.nx = 4
[docs]    def equations(self, x, t):
        theta1 = x[0]
        z1 = x[1]
        theta2 = x[2]
        z2 = x[3]
        c, s = np.cos(theta1 - theta2), np.sin(theta1 - theta2)
        dx1 = z1
        dx2 = (self.m2 * self.g * np.sin(theta2) * c - self.m2 * s * (self.L1 * z1 ** 2 * c + self.L2 * z2 ** 2) -
                 (self.m1 + self.m2) * self.g * np.sin(theta1)) / self.L1 / (self.m1 + self.m2 * s ** 2)
        dx3 = z2
        dx4 = ((self.m1 + self.m2) * (self.L1 * z1 ** 2 * s - self.g * np.sin(theta2) + self.g * np.sin(theta1) * c) +
                 self.m2 * self.L2 * z2 ** 2 * s * c) / self.L2 / (self.m1 + self.m2 * s ** 2)
        dx = [dx1, dx2, dx3, dx4]
        return dx  
[docs]class Lorenz96(ODE_Autonomous):
    """
    Lorenz 96 model
    + https://en.wikipedia.org/wiki/Lorenz_96_model
    """
    def __init__(self, nsim=1001, ninit=0, ts=0.1, seed=59):
        super().__init__(nsim=nsim, ninit=ninit, ts=ts, seed=seed)
        self.N = 36  # Number of variables
        self.F = 8  # Forcing
        self.x0 = self.F*np.ones(self.N)
        self.x0[19] += 0.01  # Add small perturbation to random variable
        self.nx = self.N
[docs]    def equations(self, x, t):
        dx = np.zeros(self.N)
        # First the 3 edge cases: i=1,2,N
        dx[0] = (x[1] - x[self.N - 2]) * x[self.N - 1] - x[0]
        dx[1] = (x[2] - x[self.N - 1]) * x[0] - x[1]
        dx[self.N - 1] = (x[0] - x[self.N - 3]) * x[self.N - 2] - x[self.N - 1]
        # Then the general case
        for i in range(2, self.N - 1):
            dx[i] = (x[i + 1] - x[i - 2]) * x[i - 1] - x[i]
        # Add the forcing term
        dx = dx + self.F
        return dx  
[docs]class LorenzSystem(ODE_Autonomous):
    """
    Lorenz System
    + https://en.wikipedia.org/wiki/Lorenz_system#Analysis
    + https://ipywidgets.readthedocs.io/en/stable/examples/Lorenz%20Differential%20Equations.html
    + https://scipython.com/blog/the-lorenz-attractor/
    + https://matplotlib.org/3.1.0/gallery/mplot3d/lorenz_attractor.html
    """
    def __init__(self, nsim=1001, ninit=0, ts=0.1, seed=59):
        super().__init__(nsim=nsim, ninit=ninit, ts=ts, seed=seed)
        self.rho = 28.0
        self.sigma = 10.0
        self.beta = 8.0 / 3.0
        self.x0 = [1.0, 1.0, 1.0]
        self.nx = 3
[docs]    def equations(self, x, t):
        dx1 = self.sigma*(x[1] - x[0])
        dx2 = x[0]*(self.rho - x[2]) - x[1]
        dx3 = x[0]*x[1] - self.beta*x[2]
        dx = [dx1, dx2, dx3]
        return dx  
[docs]class VanDerPol(ODE_Autonomous):
    """
    Van der Pol oscillator
    + https://en.wikipedia.org/wiki/Van_der_Pol_oscillator
    + http://kitchingroup.cheme.cmu.edu/blog/2013/02/02/Solving-a-second-order-ode/
    """
    def __init__(self, nsim=1001, ninit=0, ts=0.1, seed=59):
        super().__init__(nsim=nsim, ninit=ninit, ts=ts, seed=seed)
        self.mu = 1.0
        self.x0 = [1, 2]
        self.nx = 2
[docs]    def equations(self, x, t):
        dx1 = self.mu*(x[0] - 1./3.*x[0]**3 - x[1])
        dx2= x[0]/self.mu
        dx = [dx1, dx2]
        return dx  
[docs]class ThomasAttractor(ODE_Autonomous):
    """
    Thomas' cyclically symmetric attractor
    + https://en.wikipedia.org/wiki/Thomas%27_cyclically_symmetric_attractor
    """
    def __init__(self, nsim=1001, ninit=0, ts=0.1, seed=59):
        super().__init__(nsim=nsim, ninit=ninit, ts=ts, seed=seed)
        self.b = 0.208186
        self.x0 = [1, -1, 1]
        self.nx = 3
[docs]    def equations(self, x, t):
        dx1 = np.sin(x[1]) - self.b*x[0]
        dx2 = np.sin(x[2]) - self.b*x[1]
        dx3 = np.sin(x[0]) - self.b*x[2]
        dx = [dx1, dx2, dx3]
        return dx  
[docs]class RosslerAttractor(ODE_Autonomous):
    """
    Rössler attractor
    + https://en.wikipedia.org/wiki/R%C3%B6ssler_attractor
    """
    def __init__(self, nsim=1001, ninit=0, ts=0.1, seed=59):
        super().__init__(nsim=nsim, ninit=ninit, ts=ts, seed=seed)
        self.a = 0.2
        self.b = 0.2
        self.c = 5.7
        self.x0 = [0, 0, 0]
        self.nx = 3
[docs]    def equations(self, x, t):
        dx1 = - x[1] - x[2]
        dx2 = x[0] + self.a*x[1]
        dx3 = self.b + x[2]*(x[0]-self.c)
        dx = [dx1, dx2, dx3]
        return dx  
[docs]class LotkaVolterra(ODE_Autonomous):
    """
    Lotka–Volterra equations, also known as the predator–prey equations
    + https://en.wikipedia.org/wiki/Lotka%E2%80%93Volterra_equations
    """
    def __init__(self, nsim=1001, ninit=0, ts=0.1, seed=59):
        super().__init__(nsim=nsim, ninit=ninit, ts=ts, seed=seed)
        self.a = 1.
        self.b = 0.1
        self.c = 1.5
        self.d = 0.75
        self.x0 = [5, 100]
        self.nx = 2
[docs]    def equations(self, x, t):
        dx1 = self.a*x[0] - self.b*x[0]*x[1]
        dx2 = -self.c*x[1] + self.d*self.b*x[0]*x[1]
        dx = [dx1, dx2]
        return dx  
[docs]class Brusselator1D(ODE_Autonomous):
    """
    Brusselator
    + https://en.wikipedia.org/wiki/Brusselator
    """
    def __init__(self, nsim=1001, ninit=0, ts=0.1, seed=59):
        super().__init__(nsim=nsim, ninit=ninit, ts=ts, seed=seed)
        self.a = 1.0
        self.b = 3.0
        self.x0 = [1.0, 1.0]
        self.nx = 2
[docs]    def equations(self, x, t):
        dx1 = self.a + x[1]*x[0]**2 -self.b*x[0] - x[0]
        dx2 = self.b*x[0] - x[1]*x[0]**2
        dx = [dx1, dx2]
        return dx  
[docs]class ChuaCircuit(ODE_Autonomous):
    """
    Chua's circuit
    + https://en.wikipedia.org/wiki/Chua%27s_circuit
    + https://www.chuacircuits.com/matlabsim.php
    """
    def __init__(self, nsim=1001, ninit=0, ts=0.1, seed=59):
        super().__init__(nsim=nsim, ninit=ninit, ts=ts, seed=seed)
        self.a = 15.6
        self.b = 28.0
        self.m0 = -1.143
        self.m1 = -0.714
        self.x0 = [0.7, 0.0, 0.0]
        self.nx = 3
[docs]    def equations(self, x, t):
        fx = self.m1*x[0] + 0.5*(self.m0 - self.m1)*(np.abs(x[0] + 1) - np.abs(x[0] - 1))
        dx1 = self.a*(x[1] - x[0] - fx)
        dx2 = x[0] - x[1] + x[2]
        dx3 = -self.b*x[1]
        dx = [dx1, dx2, dx3]
        return dx  
[docs]class Duffing(ODE_Autonomous):
    """
    Duffing equation
    + https://en.wikipedia.org/wiki/Duffing_equation
    """
    def __init__(self, nsim=1001, ninit=0, ts=0.1, seed=59):
        super().__init__(nsim=nsim, ninit=ninit, ts=ts, seed=seed)
        self.delta = 0.02
        self.alpha = 1
        self.beta = 5
        self.gamma = 8
        self.omega = 0.5
        self.x0 = [1.0, 0.0]
        self.nx = 2
[docs]    def equations(self, x, t):
        dx1 = x[1]
        dx2 = - self.delta*x[1] - self.alpha*x[0] - self.beta*x[0]**3 + self.gamma*np.cos(self.omega*t)
        dx = [dx1, dx2]
        return dx  
[docs]class Autoignition(ODE_Autonomous):
    """
    ODE describing pulsating instability in open-ended combustor.
    + Koch, J., Kurosaka, M., Knowlen, C., Kutz, J.N.,
      "Multiscale physics of rotating detonation waves: Autosolitons and modulational instabilities,"
      Physical Review E, 2021
    """
    def __init__(self, nsim=1001, ninit=0, ts=0.1, seed=59):
        super().__init__(nsim=nsim, ninit=ninit, ts=ts, seed=seed)
        self.alpha = 0.3
        self.uc = 1.1
        self.s = 1.0
        self.k = 1.0
        self.r = 5.0
        self.q = 6.5
        self.up = 0.55
        self.e = 1.0
        self.x0 = [1.0, 0.7]
[docs]    def equations(self, x, t):
        reactionRate = self.k * (1.0 - x[1]) * np.exp((x[0] - self.uc) / self.alpha)
        regenRate = self.s * self.up * x[1] / (1.0 + np.exp(self.r * (x[0] - self.up)))
        dx1 = self.q * reactionRate - self.e * x[0] ** 2
        dx2 = reactionRate - regenRate
        dx = [dx1, dx2]
        return dx  
systems = dict(inspect.getmembers(sys.modules[__name__], lambda x: inspect.isclass(x)))
systems = {k: v for k, v in systems.items() if issubclass(v, ODE_Autonomous) and v is not ODE_Autonomous}