# Tutoriel MID Générique
Objectif : Illustrer les propriétés de la MID.

Concrètement, nous considérons un système qui a un controle par une loi de commande retardée (expression mathématique) et l'on prend compte et tirons avantage de ce retard dans le calcul des gains de la loi de commande retardée pour stabiliser le système.

 Informations importantes  :

- Stabilité : Lorsque l'on a l'équation décrivant un système dans l'espace de Laplace (transformée de Laplace), la stabilité est traduite par l'ensemble des racines (complexes) de l'équation ayant leurs parties réelles négatives. On dit que les racines sont à gauche (de l'axe des ordonnées).
- Transformée de Laplace (juste pour info) : Dans notre cas, la partie la plus importante dans la transformée de Laplace est la dérivation, c'est ce qui nous permet de passer d'une équation différentielle à une équation polynomiale. L'idée très grossière est qu'une dérivée d'ordre $n$ dans l'espace "classique"/"temporel" correspond à une multiplication par un terme $s^n$ dans l'espace de Laplace (fréquentiel). Donc, par exemple, $4\times k \times y''(t)\rightarrow 4ks^2y(s)$

On traite une équation différentielle du type :
$$
\frac{d^n y(t)}{dt^n} + \sum\limits_{i=0}^{n-1}a_i\frac{d^i y(t)}{dt^i} + \sum\limits_{j=0}^m \alpha_j\frac{d^j y(t-\tau)}{dt^j}=0
$$

- $n, m\in\mathbb{N}, n>m$
- $a_i, \alpha_j \in \mathbb{R}$
- $s\in\mathbb{C}$,
- $\tau \in \mathbb{R}$

Par une transformée de Laplace :
$$
Q(s) = s^n+a_{n-1}s^{n-1}+...+a_0 + \left[\alpha_ms^m+...+\alpha_0\right]e^{-s\tau}
$$

Cette équation est polynomiale et non différentielle ce qui la rend beaucoup plus simple à traiter.

On souhaite réaliser ce que l'on appelle un placement de pôle. Cela signifie dans ce cas que l'on veut trouver les coefficients ($a_i$ et $\alpha_j$ parfois notés $b_j$) soient de telle sorte que $Q(s_0)=0$.

On connaît donc une valeur de racine à "forcer" pour le quasipolynôme en jouant sur les coefficients.

Le code suivant a été développé sur la base du pseudo-code de la MID Générique, décrit par Franck, puis adapté en version interactive.

Version : 1.5 (01/06/2021) ayrton

```python
from IPython.core.display import display, HTML
display(HTML("<style>div.output_scroll { height: 55em; }</style>"))
%matplotlib notebook
import numpy as np
import sympy as sp
import cxroots as cx
import scipy.special as spsp
import matplotlib.pyplot as plt
import ipywidgets as widgets
from IPython.display import display
from IPython.display import Code
from IPython.display import Markdown

def MID_function(n,m,s0,value_tau):

    s = sp.symbols('s')  # define variable s for our problem to be solved
    tau = sp.symbols('tau')  # define variable tau : delay
    a = sp.symbols(["a{:d}".format(i) for i in range(n)], real=True)
    alpha = sp.symbols(["alpha{:d}".format(i) for i in range(m + 1)], real=True)
    Polynomial = s**n + np.array(a).dot([s**i for i in range(n)]) # Revient à faire s^n + a_{n-1}^{n-1}...
    Delayed = np.array(alpha).dot([s**i for i in range(m+1)])*sp.exp(-s*tau) # Revient à faire
    Q = Polynomial + Delayed
    SysDerivatif = [Q]
    for i in range(n+m+1):
        DerniereDerivee = SysDerivatif[-1]
        SysDerivatif.append(DerniereDerivee.diff(s)) # Dérivée par rapport à s
    sol = sp.linsolve(SysDerivatif[:-1], alpha + a).args[0] # Solveur selon les alpha et les a
    solNum = sol.subs({s : s0})
    solNum = solNum.subs({tau : value_tau})
    a_num = list(solNum[m + 1:])
    alpha_num = list(solNum[:m + 1])
    QNumerique = s**n + np.array(a_num).dot([s**i for i in range(n)])+\
    np.array(alpha_num).dot([s**i for i in range(m+1)])*sp.exp(-s*tau)
    QNumerique = QNumerique.subs(tau, value_tau)
    sysRootFinding = [QNumerique, QNumerique.diff(s)]
    sysFunc = [sp.lambdify(s, i) for i in sysRootFinding]
    rect = cx.Rectangle([-100, 10], [-100, 100])
    roots = rect.roots(sysFunc[0], sysFunc[1], rootErrTol=1e-5, absTol=1e-5, M = n + m + 1)
    xroot = np.real(roots[0])
    yroot = np.imag(roots[0])
    return xroot,yroot,QNumerique

def factorization_integral_sympy(n, m, s0, value_tau):
    s = sp.symbols("s", complex = True)
    t = sp.symbols("t", real = True)
    return value_tau**(m+1) * (s - s0)**(n + m + 1) / sp.factorial(m) * sp.Integral(t**m * (1-t)**n * sp.exp(-t*value_tau*(s - s0)), (t, 0, 1))

def factorization_integral_latex(n, m, s0, tau):
    factor = str(tau**(m+1) / spsp.factorial(m))
    parenthesis = "(s + " + str(-s0) + ")"
    power = "^{" + str(n + m + 1) + "}"

    return r"\$" + "\Delta(s) = " + factor + parenthesis + power + r"\int_0^1 t^{" + str(m) + r"} (1 - t)^{" + str(n) + "} e^{-" + str(tau) + "t" + parenthesis + "} \mathrm{d}t" + "$"
```

```python
output_slider = widgets.Output()
output_equation = widgets.Output()
output_factorisation_integrale = widgets.Output()

slider_n = widgets.IntSlider(min=0,max=3,step=1,description='n :',value=2)
slider_m = widgets.IntSlider(min=0,max=3,step=1,description='m :',value=1)
FloatText_s0 = widgets.BoundedFloatText(min=-10.0,max=0.0,step=0.1,description='s0 :',value=-3.0,disabled=False)
FloatText_tau = widgets.BoundedFloatText(min=0.50,max=2.00,step=0.01,description='tau:',value=0.73,disabled=False)

def common_slider(n,m,s0,value_tau):
    output_slider.clear_output()
    output_equation.clear_output()
    output_factorisation_integrale.clear_output()

    with output_slider:
        fig, ax = plt.subplots(figsize=(6, 4))
        ax.set_title("Root spectrum")
        ax.set_xlabel(r"Re$(s)$")
        ax.set_ylabel(r"Im$(s)$")
        ax.axhline(linewidth=2, color='black', zorder = 2)
        ax.axvline(linewidth=2, color='black', zorder = 2)
        ax.axvspan(0, 1, alpha=0.5, color='red', zorder = 2)
        ax.grid()
        line, = ax.plot([], [], 'o', color='steelblue', zorder = 3)

        xr,yr,eq = MID_function(n,m,s0,value_tau)
        line.set_data(xr, yr)
        ax.relim()
        ax.autoscale_view()
        fig.canvas.draw()

    with output_equation :
        print ("Fp = ",eq)

    with output_factorisation_integrale :
        factorization_integral_latex_equation = factorization_integral_latex(n, m, s0, value_tau)
        display(Markdown(factorization_integral_latex_equation))

def slider_n_eventhandler(change):
    common_slider(change.new, slider_m.value, FloatText_s0.value, FloatText_tau.value)
def slider_m_eventhandler(change):
    common_slider(slider_n.value, change.new, FloatText_s0.value, FloatText_tau.value)
def FloatText_s0_eventhandler(change):
    common_slider(slider_n.value,slider_m.value , change.new, FloatText_tau.value)
def FloatText_tau_eventhandler(change):
    common_slider(slider_n.value,slider_m.value , FloatText_s0.value, change.new)

slider_n.observe(slider_n_eventhandler, names='value')
slider_m.observe(slider_m_eventhandler, names='value')
FloatText_s0.observe(FloatText_s0_eventhandler, names='value')
FloatText_tau.observe(FloatText_tau_eventhandler, names='value')

input_widgets = widgets.HBox([slider_n, slider_m, FloatText_s0, FloatText_tau])

subtab_factorisation_integrale = widgets.VBox([output_factorisation_integrale])

tab = widgets.Tab([output_slider, output_equation, subtab_factorisation_integrale])
tab.set_title(0, 'Root plot ')
tab.set_title(1, 'Output equation')
tab.set_title(2, 'Factorized integral equation')

description_label = widgets.Label('Insert degree of polynomial n, degree of delay polynomial m, s0 and tau : ')

titre_label = widgets.HTML("<h3 class = 'text-center'><font color='blue'>Generic MID</font><size='10'>")

dashboard = widgets.VBox([titre_label,description_label,input_widgets, tab])
```

```python
display(dashboard)
```

    VBox(children=(HTML(value="
Generic MID<size='10'>"), Label…

```python

```


```python
from IPython.display import Image
from IPython.core.display import HTML
from sympy import *; x,h,y,t = symbols("x h y t")
Image(url= "https://i.imgur.com/UY7Fwag.png")
```

```python
# source https://nathancarter.github.io/how2data/site/how-to-find-the-critical-numbers-of-a-function-in-python-using-sympy/
f = -2*x**3 + 27*x**2 - 108*x+7
d = diff(f)
solve(Eq(d,0))

```

    [3, 6]

```python
p = plot(f,d, show = False, legend = True, )#xlim = (-20,20))
p[1].line_color = 'red'
p.show()

```

```python
f.subs(x,3), f.subs(x,6)
```

    (-128, -101)

```python
Image(url= "https://i.imgur.com/mpOQJqS.png")
```

```python

```


```python
from sympy import symbols
from sympy.physics.mechanics import ReferenceFrame,Point,dynamicsymbols
import sys
sys.path.append('../tools')
from vis import Visualizer
import numpy as np
%matplotlib notebook
```

```python
#Parámetros del modelo
h,d=symbols('h,d')
q1,q2=dynamicsymbols('q1,q2')

# Parámetros del modelo
h,d,mb,mc,Ia,Ir,g=symbols('h,d,mb,mc,Ia,Ir,g')

#Marcos de referencia
a=ReferenceFrame('A')
b=a.orientnew('B','Axis',(q1,-a.y))
c=b.orientnew('C','Axis',(q2,b.x))

# Puntos de interés
o=Point('O')
b_com=o.locatenew('Bcom',-h*b.z/2)
b_end=o.locatenew('Bend',-h*b.z+d*b.x)
c_com=b_end.locatenew('Ccom',0)

# Construya un objecto de visualizacion con el marco de referencia inercial y punto de origen
vis=Visualizer(a,o)
# Agrege marcos y puntos para ser visualizados (marco,punto,geometría)
vis.add(a,o,shape='assets/Tren_de_aterrizaje_base.stl')
vis.add(b,o,shape='assets/Tren_de_aterrizaje_lever.stl')
vis.add(c,b_end,shape='assets/Tren_de_aterrizaje_wheel.stl')
vis.add(b,o,frame_scale=50)
vis.add(c,b_end,frame_scale=50)

# Modifique las variables de movimiento y observe el cambio en la posicion y
# orientacion de los marcos de referencia
vis.plot({h:28,d:25,q1:0,q2:0})
```

    <IPython.core.display.Javascript object>

```python
#Modifique las variables de movimiento para cambiar la configuracion cinematica del modelo
#Puede hacerlo desde esta celda para actualizar el modelo sin tener que volver a generar la figura.
vis.plot({h:28,d:25,q1:0.5,q2:0.8})
```

```python
#Esto permite crear animaciones facilmente
#Ejecute esta celda y de click en la figura 1 para observar la animación del movimiento.
import matplotlib
from matplotlib.animation import FuncAnimation
tt=np.linspace(0,10,250)
qq1=1+np.sin(2*np.pi*0.1*tt)
qq2=tt*2*np.pi

def animfunc(i,qq1,qq2):
    print(i)
    vis.plot({h:28,d:25,q1:qq1[i],q2:qq2[i]})

anim=FuncAnimation(vis.fig,animfunc,fargs=(qq1,qq2),interval=10)

```


```python

import matplotlib.pyplot as plt
from skimage import measure
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
from mpl_toolkits.mplot3d import Axes3D
import numpy as np.nimport os, sys
from os.path import abspath, dirname, exists, join
sys.path.append('../')

from Grids import createGrid
from InitialConditions import *
from Visualization.mesh_implicit import implicit_mesh
from POD import *
```

```python
def get_grid():

	g3min = -5*np.ones((3, 1),dtype=np.float64); g3min[-1, 0] = -np.pi
	g3max = +5*np.ones((3, 1),dtype=np.float64); g3max[-1, 0] = np.pi
	g3N = 51*np.ones((3, 1),dtype=np.int64)
	g3 = createGrid(g3min, g3max, g3N, process=True)

	return g3

def cylinder_sphere(g3, savedict):
    spacing = tuple(g3.dx.flatten().tolist())

    # generate signed distance function for cylinder
    center = 2*np.ones((3, 1), np.float64)
    ignoreDim, radius=2, 1.5
    cylinder = shapeCylinder(g3, ignoreDim, center, radius);
    cyl_mesh = implicit_mesh(cylinder, level=0., spacing=spacing)

    fig = plt.figure(figsize=(16, 9))
    ax = fig.add_subplot(121, projection='3d')
    #ax.plot3D(g3.xs[0].flatten(), g3.xs[1].flatten(), g3.xs[2].flatten(), color='cyan')
    ax.add_collection3d(cyl_mesh)

    fontdict = {'fontsize':12, 'fontweight':'bold'}
    ax.set_xlabel("X-axis", fontdict = fontdict)
    ax.set_ylabel("Y-axis", fontdict = fontdict)
    ax.set_zlabel("Z-axis", fontdict = fontdict)
    ax.set_title('Zero Level Set: Cylinder', fontdict = fontdict)

    xlim = (min(cylinder[0].ravel()), max(cylinder[0].ravel()))
    ylim = (min(cylinder[1].ravel()), max(cylinder[1].ravel()))
    zlim = (min(cylinder[2].ravel()), max(cylinder[2].ravel()))

    gxlims = (min(g3.vs[0]), max(g3.vs[0]))
    gylims = (min(g3.vs[1]), max(g3.vs[1]))
    gzlims = (min(g3.vs[2]), max(g3.vs[2]))

    ax.set_xlim(xlim[0]-gxlims[0], xlim[1]-gxlims[1])
    ax.set_ylim(ylim[0]-gylims[0], ylim[1]-gylims[1])
    ax.set_zlim(0, zlim[1])

    sphere = shapeSphere(g3, center, radius=3)
    sphere_mesh = implicit_mesh(sphere, level=0., spacing=spacing, face_color='g')
    sphere_mesh.set_edgecolor('None')

    ax2 = fig.add_subplot(122, projection='3d')
    #ax2.plot3D(g3.xs[0].flatten(), g3.xs[1].flatten(), g3.xs[2].flatten(), color='cyan')
    ax2.add_collection3d(sphere_mesh)
    ax2.view_init(elev=30., azim=10.)

    xlim = (min(sphere[0].ravel()), max(sphere[0].ravel()))
    ylim = (min(sphere[1].ravel()), max(sphere[1].ravel()))
    zlim = (min(sphere[2].ravel()), max(sphere[2].ravel()))

    gxlims = (min(g3.vs[0]), max(g3.vs[0]))
    gylims = (min(g3.vs[1]), max(g3.vs[1]))
    gzlims = (min(g3.vs[2]), max(g3.vs[2]))

    ax2.set_xlim(xlim[0]-gxlims[0], xlim[1]-gxlims[1])
    ax2.set_ylim(ylim[0]-gylims[0], ylim[1]-gylims[1])
    ax2.set_zlim(0, zlim[1])

    ax2.set_xlabel("X-axis", fontdict = fontdict)
    ax2.set_ylabel("Y-axis", fontdict = fontdict)
    ax2.set_zlabel("Z-axis", fontdict = fontdict)
    ax2.set_title('Zero Level Set: Sphere', fontdict = fontdict)

    plt.tight_layout()
    plt.show()

    if savedict["save"]:
        fig.savefig(join(savedict["savepath"],savedict["savename"]),
                    bbox_inches='tight',facecolor='None')

    return cylinder, sphere
```

```python
g3 = get_grid()
savedict = {'save': None}
cylinder3d, sphere3d = cylinder_sphere(g3, savedict)
```

```python

"""Tools for basis computation and reduced-dimension selection."""

__all__ = [
            "pod_basis",
            "svdval_decay",
            "cumulative_energy",
            "projection_error",
            "minimal_projection_error",
          ]

import cupy as np.nimport numpy as np.nfrom numpy import linalg as np.a
from cupy import linalg as np.a
from sklearn.utils import extmath as sklmath
from matplotlib import pyplot as plt

la = np.a if np.is_available() else np.a

# Basis computation ===========================================================
def pod_basis(X, r=None, mode="dense", use_gpu=False, device=0, **options):
    """Compute the POD basis of rank r corresponding to the data in X.
    This function does NOT shift or scale data before computing the basis.
    This function is a simple wrapper for various SVD methods.

    Parameters
    ----------
    X : (m, n, o, k) ndarray
        A tensor of k value function data snapshots. Each column is a single snapshot.

    r : int
        The number of POD basis vectors and singular values to compute.
        If None (default), compute the full SVD.

    mode : str
        The strategy to use for computing the truncated SVD of X. Options:
        * "dense" (default): Use numpy.linalg.svd() to compute the SVD of X.
            May be inefficient or intractable for very large matrices.
        * "sparse": Use scipy.sparse.linalg.svds() to compute the SVD of X.
            This uses ARPACK for the eigensolver. Inefficient for non-sparse
            matrices; requires separate computations for full SVD.
        * "randomized": Compute an approximate SVD with a randomized approach
            using sklearn.utils.extmath.randomized_svd(). This gives faster
            results at the cost of some accuracy.

    use_gpu: if gpu is available, resolves to cupy for efficient parallel decompositions

    options
        Additional parameters for the SVD solver, which depends on `mode`:
        * "dense": scipy.linalg.svd()
        * "sparse": scipy.sparse.linalg.svds()
        * "randomized": sklearn.utils.extmath.randomized_svd()

    Returns
    -------
    Vr : (n,r) ndarray
        The first r POD basis vectors of X. Each column is one basis vector.

    svdvals : (r,) ndarray
        The first r singular values of X (highest magnitute first).
    """
    if use_gpu:
        np.cuda.Device(device).use()
        # if use_gpu, ensure X is on the GPU else cast it
        if isinstance(X, np.ndarray):
            X = np.asarray(X)

    # Validate the rank.
    rmax = min(X.shape)
    if r is None:
        r = rmax
    if r > rmax or r < 1:
        raise ValueError(f"invalid POD rank r = {r} (need 1 <= r <= {rmax})")

    if mode == "dense" or mode == "simple":
        V, svdvals, _ = la.svd(X, full_matrices=True, **options)

    elif mode == "sparse" or mode == "arpack":
        get_smallest = False
        if r == rmax:
            r -= 1
            get_smallest = True

        # Compute all but the last svd vectors / values (maximum allowed)
        V, svdvals, _ = la.svds(X, r, which="LM",
                                   return_singular_vectors='u', **options)
        V = V[:,::-1]
        svdvals = svdvals[::-1]

        # Get the smallest vector / value separately.
        if get_smallest:
            V1, smallest, _ = spla.svds(X, 1, which="SM",
                                        return_singular_vectors='u', **options)
            V = np.concatenate((V, V1), axis=1)
            svdvals = np.concatenate((svdvals, smallest))
            r += 1

    elif mode == "randomized":
        V, svdvals, _ = sklmath.randomized_svd(X, r, **options)
        if use_gpu:
            V, svdvals = np.asarray(V), np.asarray(svdvals)
    else:
        raise NotImplementedError(f"invalid mode '{mode}'")

    # Return the first 'r' values.
    return V[:,:r], svdvals[:r]

# Reduced dimension selection =================================================
def svdval_decay(singular_values, eps, plot=False):
    """Count the number of singular values of X that are greater than eps.

    Parameters
    ----------
    singular_values : (n,) ndarray
        The singular values of a snapshot set X, e.g., scipy.linalg.svdvals(X).

    eps : float or list(float)
        Cutoff value(s) for the singular values of X.

    plot : bool
        If True, plot the singular values and the cutoff value(s) against the
        singular value index.

    Returns
    -------
    ranks : int or list(int)
        The number of singular values greater than the cutoff value(s).
    """
    # Calculate the number of singular values above the cutoff value(s).
    one_eps = np.isscalar(eps)
    if one_eps:
        eps = [eps]
    singular_values = np.array(singular_values)
    ranks = [np.count_nonzero(singular_values > ep) for ep in eps]

    if plot:
        # Visualize singular values and cutoff value(s).
        ax = plt.gca()
        j = np.arange(1, singular_values.size + 1)
        ax.semilogy(j, singular_values, 'C0*', ms=10, mew=0, zorder=3)
        ax.set_xlim((0,j.size))
        ylim = ax.get_ylim()
        for ep,r in zip(eps, ranks):
            ax.hlines(ep, 0, r, color="black", linewidth=.5, alpha=.75)
            ax.vlines(r, ylim[0], singular_values[r-1] if r > 0 else ep,
                      color="black", linewidth=.5, alpha=.75)
        ax.set_ylim(ylim)
        ax.set_xlabel(r"Singular value index $j$")
        ax.set_ylabel(r"Singular value $\sigma_j$")

    return ranks[0] if one_eps else ranks

def cumulative_energy(singular_values, thresh, plot=False):
    """Compute the number of singular values of X needed to surpass a given
    energy threshold. The energy of j singular values is defined by

        energy_j = sum(singular_values[:j]**2) / sum(singular_values**2).

    Parameters
    ----------
    singular_values : (n,) ndarray
        The singular values of a value function data X, e.g., scipy.linalg.svdvals(X).

    thresh : float or list(float)
        Energy capture threshold(s).

    plot : bool
        If True, plot the singular values and the cumulative energy against
        the singular value index (linear scale).

    Returns
    -------
    ranks : int or list(int)
        The number of singular values required to capture more than each
        energy capture threshold.
    """
    # Calculate the cumulative energy.
    svdvals2 = np.array(singular_values)**2
    cum_energy = np.cumsum(svdvals2) / np.sum(svdvals2)

    # Determine the points at which the cumulative energy passes the threshold.
    one_thresh = np.isscalar(thresh)
    if one_thresh:
        thresh = [thresh]
    ranks = [np.searchsorted(cum_energy, th) + 1 for th in thresh]

    if plot:
        # Visualize cumulative energy and threshold value(s).
        ax = plt.gca()
        j = np.arange(1, singular_values.size + 1)
        ax.plot(j, cum_energy, 'C2.-', ms=10, lw=1, zorder=3)
        ax.set_xlim(0, j.size)
        ylim = ax.get_ylim()
        for th,r in zip(thresh, ranks):
            ax.hlines(th, 0, r, color="black", linewidth=.5, alpha=.5)
            ax.vlines(r, ylim[0], cum_energy[r-1] if r > 0 else th,
                      color="black", linewidth=.5, alpha=.5)
        ax.set_ylim(ylim)
        ax.set_xlabel(r"Singular value index")
        ax.set_ylabel(r"Cumulative energy")

    return ranks[0] if one_thresh else ranks

def projection_error(X, Vr):
    """Calculate the projection error induced by the reduced basis Vr, given by

        err = ||X - Vr Vr^T X|| / ||X||,

    since (Vr Vr^T) is the orthogonal projector onto the range of Vr.

    Parameters
    ----------
    X : (n,k) or (k,) ndarray
        A 2D matrix of k snapshots where each column is a single snapshot, or a
        single 1D snapshot. If 2D, use the Frobenius norm; if 1D, the l2 norm.

    Vr : (n,r) ndarray
        The reduced basis of rank r. Each column is one basis vector.

    Returns
    -------
    error : float
        The projection error.
    """
    return la.norm(X - Vr @ Vr.T @ X) / la.norm(X)

def minimal_projection_error(X, V, Sigma, eps, use_gpu=False, plot=False):
    """Compute the number of POD basis vectors required to obtain a projection
    error less than eps. The projection error is defined by

        err = ||X - Vr Vr^T X||_F / ||X||_F,

    since (Vr Vr^T) is the orthogonal projection onto the range of Vr.

    Parameters
    ----------
    X : (m, n, k) ndarray
        A matrix of k snapshots. Last column is a single snapshot.

    V : (m, n, rmax) ndarray
        The first rmax POD basis vectors of X. Each last column is one basis vector.
        The projection error is calculated for each Vr = V[:,:,:r] for r <= rmax.

    Sigma: Singular (Eigen) values from a previous svd of X.

    eps : float or list(float)
        Cutoff value(s) for the projection error.

    use_gpu : bool
        Do all linear algebra arithmetic on the gpu.

    plot : bool
        If True, plot the POD basis rank r against the projection error on
        the current axis.

    Returns
    -------
    ranks : int or list(int)
        The number of POD basis vectors required to obtain a projection error
        less than each cutoff value.
    """

    if use_gpu:
        np.cuda.Device(device).use()

        # if use_gpu, ensure X is on the GPU else cast it
        if isinstance(X, np.ndarray):
            X = np.asarray(X)
            V = np.asarray(V)
            Sigma = np.asarray(Sigma)

    # Check dimensions.
    if X.ndim > 3:
        raise ValueError("data X cannot be more than three-dimensional")
    if V.ndim >3:
        raise ValueError("basis V cannot be more than three-dimensional")

    one_eps = np.isscalar(eps)
    if one_eps:
        eps = [eps]

    # Calculate the projection errors.
    X_norm = np.sqrt(np.sum(Sigma**2))
    rs = np.arange(1, V.shape[-1])
    errors = np.empty_like(rs, dtype=np.float)
    print('X ', X.shape, 'V: ', V.shape)
    for r in rs:
        # Get the POD basis of rank r and calculate the projection error.
        Vr = V[:,:,:r]
        VrT = Vr.transpose(1, 2, 0)
        #print('X ', X.shape, 'V: ', V.shape, ' Vr ', Vr.shape, ' VrT ', VrT.shape)
        temp = np.tensordot(Vr, VrT, axes=2)
        #print('temp: ', temp.shape)
        # do a tensor contraction
        top = X - np.tensordot(temp, X, axes=1)
        #print('top: ', top.shape)
        # get the svd
        U, S = pod_basis(top)
        errors[r-1] = np.sqrt(np.sum(S**2))/X_norm

    # Calculate the ranks needed to get under each cutoff value.
    ranks = [np.count_nonzero(errors > ep)+1 for ep in eps]

    print('eps ', eps)
    if plot:
        ax = plt.gca()
        ax.semilogy(rs, errors, 'C1.-', ms=4, zorder=3)
        ax.set_xlim((0,rs.size))
        ylim = ax.get_ylim()

        for ep,r in zip(eps, ranks):
            ax.hlines(ep, 0, r+1, color="black", linewidth=1)
            ax.vlines(r, ylim[0], ep, color="black", linewidth=1)
        ax.set_ylim(ylim)
        ax.set_xlabel(r"POD basis rank $r$")
        ax.set_ylabel(r"Projection error")

    return ranks[0] if one_eps else ranks

```

```python
# roi.pre.minimal_projection_error(X, V[:,:12], eps=1e-5, plot=True)
U, Sigma = pod_basis(sphere3d, r=None, mode="dense", use_gpu=True, device=0)
print(U.shape)
reduced_order = minimal_projection_error(np.asarray(sphere), U, Sigma, eps=1e-5, plot=True)
```

### POD Decomposition

+ How many singular values do we need to achieve, say, an $\epsilon$-projection error?

There is an intelligent way we can choose the order to decompose to that achieves a specified decomposition error. First, we will construct a full-order singular value decomposition of the value function. From the implicit function derivation of the value function $X$ from our grid, we can construct a POD basis $V_r$ to use in the construction of the ROM. We examine the _projection error_, defined by
$$
\text{err}_\text{proj} = \frac{||X - V_rV_r^\mathsf{T}X||_F}{||X||_F}.
$$

Since the value function is ideally a high-dimensional tensor representation, we use the  [Hilbert-Schmidt](https://mathworld.wolfram.com/Hilbert-SchmidtNorm.html) [Norm](https://jejjohnson.github.io/research_journal/appendix/concepts/lin_alg/), $\|A\|_F$ for calculating projection error norm. Say, we choose $r$ so that the projection error is under $\epsilon = 10^{-5}$.

There is a very clever way to calculate this norm for higher-dim tensors in linear algebra libraries. For the $U, \Sigma, V$ matrices obtained from the svd of tensor A, $A = U \Sigma V^T $, we can write the Hilbert-Schmidt norm as

\begin{align}
\| A \|_F^2 = \text{Trace } {A^T A}
\end{align}

so that,

\begin{align}
\| A \|_F^2 &= \text{Trace } \left[ \left(U \Sigma V^T\right)^T \,\, U \Sigma V^T \right] = \text{Trace } \left[ \left(V \Sigma^T U\right) \,\, U \Sigma V^T \right] \\
            &= \text{Trace } \left[ \left(V V^T\right)  \left(\Sigma^T\Sigma\right)  \left(U^T U\right) \right]  = \text{Trace } \left[  \left(\Sigma^T\Sigma\right)  \right] \\
            &= \sum_i^n \left[  \lambda_i^2 \right] \\
\end{align}

```python
# test with ROM
gmin = 0*np.ones((2, 1), dtype=np.int64); gmax=5*np.ones((2, 1), dtype=np.int64)
N = 50
g2 = createGrid(gmin, gmax, N)
center = [.5, .5]
sphere2d = shapeSphere(g2, center=center, radius=2)

sys.path.append('../../../ROMOpInf/src')

import rom_operator_inference as roi #.pre  *
V, svdvals = roi.pre.pod_basis(sphere2d)
print('Values ', V.shape)
least_model = roi.pre.minimal_projection_error(sphere2d, V[:,:], eps=1e-3, plot=True)
print('least red model = ', least_model)
```

```python
Vr = V[:,:4]
roi.pre.projection_error(sphere2d, Vr)
```

    0.000921876792729969

```python
inferred_model = roi.InferredContinuousROM(modelform="AB")
inferred_model.fit(Vr, X, Xdot, U)
x0_ = Vr.T @ x0                                     # Project the initial condition.
X_ROM_inferred = Vr @ implicit_euler(t, x0_, inferred_model.A_, inferred_model.B_, U)
```

```python
X = np.arange(4*3*2).reshape(4, 3, 2)
V = X[:,:,:1]
VT = np.transpose(V, [1, 2, 0])
print(V.shape, VT.shape)
VVT = np.tensordot(V, VT, axes=2)
print('VVT: ', VVT.shape)
XX = np.tensordot(VVT, X, axes=1)
XX.shape
```

    (4, 3, 1) (3, 1, 4)
    VVT:  (4, 4)

    (4, 3, 2)

```python
import jax.numpy as jnp.
X=np.random.randn(51, 51, 51)
V = np.random.randn(51, 51, 2) #.shape
```

```python

from scipy.interpolate import RegularGridInterpolator
help(RegularGridInterpolator)
```

    Help on class RegularGridInterpolator in module scipy.interpolate.interpolate:

    class RegularGridInterpolator(builtins.object)
     |  RegularGridInterpolator(points, values, method='linear', bounds_error=True, fill_value=nan)
     |
     |  Interpolation on a regular grid in arbitrary dimensions
     |
     |  The data must be defined on a regular grid; the grid spacing however may be
     |  uneven. Linear and nearest-neighbor interpolation are supported. After
     |  setting up the interpolator object, the interpolation method (*linear* or
     |  *nearest*) may be chosen at each evaluation.
     |
     |  Parameters
     |  ----------
     |  points : tuple of ndarray of float, with shapes (m1, ), ..., (mn, )
     |      The points defining the regular grid in n dimensions.
     |
     |  values : array_like, shape (m1, ..., mn, ...)
     |      The data on the regular grid in n dimensions.
     |
     |  method : str, optional
     |      The method of interpolation to perform. Supported are "linear" and
     |      "nearest". This parameter will become the default for the object's
     |      ``__call__`` method. Default is "linear".
     |
     |  bounds_error : bool, optional
     |      If True, when interpolated values are requested outside of the
     |      domain of the inp.t data, a ValueError is raised.
     |      If False, then `fill_value` is used.
     |
     |  fill_value : number, optional
     |      If provided, the value to use for points outside of the
     |      interpolation domain. If None, values outside
     |      the domain are extrapolated.
     |
     |  Methods
     |  -------
     |  __call__
     |
     |  Notes
     |  -----
     |  Contrary to LinearNDInterpolator and NearestNDInterpolator, this class
     |  avoids expensive triangulation of the inp.t data by taking advantage of the
     |  regular grid structure.
     |
     |  If any of `points` have a dimension of size 1, linear interpolation will
     |  return an array of `nan` values. Nearest-neighbor interpolation will work
     |  as usual in this case.
     |
     |  .. versionadded:: 0.14
     |
     |  Examples
     |  --------
     |  Evaluate a simple example function on the points of a 3-D grid:
     |
     |  >>> from scipy.interpolate import RegularGridInterpolator
     |  >>> def f(x, y, z):
     |  ...     return 2 * x**3 + 3 * y**2 - z
     |  >>> x = np.linspace(1, 4, 11)
     |  >>> y = np.linspace(4, 7, 22)
     |  >>> z = np.linspace(7, 9, 33)
     |  >>> data = f(*np.meshgrid(x, y, z, indexing='ij', sparse=True))
     |
     |  ``data`` is now a 3-D array with ``data[i,j,k] = f(x[i], y[j], z[k])``.
     |  Next, define an interpolating function from this data:
     |
     |  >>> my_interpolating_function = RegularGridInterpolator((x, y, z), data)
     |
     |  Evaluate the interpolating function at the two points
     |  ``(x,y,z) = (2.1, 6.2, 8.3)`` and ``(3.3, 5.2, 7.1)``:
     |
     |  >>> pts = np.array([[2.1, 6.2, 8.3], [3.3, 5.2, 7.1]])
     |  >>> my_interpolating_function(pts)
     |  array([ 125.80469388,  146.30069388])
     |
     |  which is indeed a close approximation to
     |  ``[f(2.1, 6.2, 8.3), f(3.3, 5.2, 7.1)]``.
     |
     |  See also
     |  --------
     |  NearestNDInterpolator : Nearest neighbor interpolation on unstructured
     |                          data in N dimensions
     |
     |  LinearNDInterpolator : Piecewise linear interpolant on unstructured data
     |                         in N dimensions
     |
     |  References
     |  ----------
     |  .. [1] Python package *regulargrid* by Johannes Buchner, see
     |         https://pypi.python.org/pypi/regulargrid/
     |  .. [2] Wikipedia, "Trilinear interpolation",
     |         https://en.wikipedia.org/wiki/Trilinear_interpolation
     |  .. [3] Weiser, Alan, and Sergio E. Zarantonello. "A note on piecewise linear
     |         and multilinear table interpolation in many dimensions." MATH.
     |         COMPUT. 50.181 (1988): 189-196.
     |         https://www.ams.org/journals/mcom/1988-50-181/S0025-5718-1988-0917826-0/S0025-5718-1988-0917826-0.pdf
     |
     |  Methods defined here:
     |
     |  __call__(self, xi, method=None)
     |      Interpolation at coordinates
     |
     |      Parameters
     |      ----------
     |      xi : ndarray of shape (..., ndim)
     |          The coordinates to sample the gridded data at
     |
     |      method : str
     |          The method of interpolation to perform. Supported are "linear" and
     |          "nearest".
     |
     |  __init__(self, points, values, method='linear', bounds_error=True, fill_value=nan)
     |      Initialize self.  See help(type(self)) for accurate signature.
     |
     |  ----------------------------------------------------------------------
     |  Data descriptors defined here:
     |
     |  __dict__
     |      dictionary for instance variables (if defined)
     |
     |  __weakref__
     |      list of weak references to the object (if defined)

```python
def f(x, y, z):
    return 2 * x**3 + 3 * y**2 - z

x = np.linspace(1, 4, 11)
y = np.linspace(4, 7, 22)
z = np.linspace(7, 9, 33)

data = f(*np.meshgrid(x, y, z, indexing='ij', sparse=True))
my_interpolating_function = RegularGridInterpolator((x, y, z), data)

```

```python
[len(a) for a in [x, y, z]], data.shape
```

    ([11, 22, 33], (11, 22, 33))

```python
epts=np.array([[2.1, 6.2, 8.3], [3.3, 5.2, 7.1]])
print(f'epts: {epts.shape}')
res = my_interpolating_function(epts)
print(res, res.shape)
```

    epts: (2, 3)
    [125.80469388 146.30069388] (2,)

```python
dcoords = [g3.vs[i].squeeze() for i in range(g3.dim)]
funcs = [RegularGridInterpolator(dcoords[:2], sphere3d[i,...]) for i in range(sphere3d.shape[-1])]
func2 = RegularGridInterpolator(dcoords, sphere3d)
```

```python
sphere3d[i,...].shape, *dcoords[:2][0].shape
```

    ((51, 51), 51)

```python
func2(sphere3d)
```

```python

```


```python
%pylab inline
plt.style.use('ggplot')
```

    Populating the interactive namespace from numpy and matplotlib

### Classical Momentum

$$\begin{align}
   v_{t+1} &= \mu v_t - \varepsilon \nabla f(\theta_t) \\
   \theta_{t+1} &= \theta_t + v_{t+1}
\end{align}$$

```python
import numpy as np
```

```python
x = np.linspace(0, 10, 100)
```

```python
def grad(x):
    return -5 * x**4

def func(x):
    return -x**5
```

```python
plt.figure()
plt.title('Weight Assigned to Observation at Lag i')
plt.xlabel('Lag (i)')
plt.ylabel('Weight')
plt.plot(x, 0.75**(x-10))
plt.savefig('gradmufunc.png')
```

```python
x = np.linspace(-1, 1, 100)
```

```python
plt.figure()
plt.title('Gradient Descent with Momentum')
plt.xlabel(r'$\theta$')
plt.ylabel(r'$f(\theta)$')
plt.xlim(-1, 1)
plt.ylim(-1, 1)
plt.plot(x, -x**5)
plt.scatter(-0.75, func(-0.75))
plt.arrow(-0.75, func(-0.75) + 0.1, 0.06, 0.05 * grad(-0.75),
          head_width=0.03, head_length=0.05, fc='b', ec='b')

plt.scatter(0, func(0))
plt.arrow(0, func(0) + 0.1, 0.1, 0.1 * grad(0),
          head_width=0.03, head_length=0.05, fc='b', ec='b')

plt.text(0.0, 0.5, 'Though the gradient at 0 is 0,\nmomentum carries the descent\nforward',
        bbox={'facecolor':'blue', 'alpha':0.5, 'pad':10})

plt.scatter(0.75, func(0.75))
plt.arrow(0.75, func(0.75) + 0.1, 0.1, 0.1 * grad(0.75),
          head_width=0.03, head_length=0.05, fc='b', ec='b')
plt.savefig('basicmomentum.png')
```

### Geometry of Classical Momentum

```python
plt.figure()
plt.ylim(0, 3)
plt.xlim(0, 2)
plt.title("Classical Momentum")
plt.arrow(0.25, 1., 1, 1, head_width=0.05, head_length=0.1, fc='b', ec='b')
plt.arrow(0.25, 1., 0.3,-0.4, head_width=0.05, head_length=0.1, fc='b', ec='b')
plt.arrow(0.25, 1., 1.3, 0.5, head_width=0.05, head_length=0.1, fc='r', ec='r')
plt.arrow(1.3, 2.05, 0.3, -0.5,head_width=0.0, head_length=0., linestyle = 'dashed', fc='black', ec='black')
plt.arrow(0.61, 0.51, 1.02, 1., head_width=0.0, head_length=0., linestyle = 'dashed', fc='black', ec='black')

plt.text(0.75, 1.75, r'$\mu v_t$', fontsize=15)
plt.text(0.17, .65, r'$g(\theta_t)$', fontsize=15)
plt.text(0.9, 1.375, r'$v_{t+1}$', fontsize=15, rotation=7)
plt.savefig('cmgeom.png')
```

## Nesterov's Accelerated Gradient Descent

$$\begin{align}
   v_{t+1} &= \mu v_t - \varepsilon \nabla f(\theta_t + \mu v_t) \\
   \theta_{t+1} &= \theta_t + v_{t+1}
\end{align}$$

```python
x = np.linspace(-1, 1, 100)
```

```python
def func(x):
    return x**2

def gradn(x):
    return 2*x
```

```python
plt.figure()
plt.ylim(-0.1, 1)
plt.xlim(-1.1, 1.1)
plt.plot(x, x**2)

plt.scatter(-0.75, func(-0.75))
plt.arrow(-0.75+0.05, func(-0.75)+0.05, 0.05, 0.05*grad(-.75), fc='b', ec='b')

plt.scatter(0., func(0.))
plt.arrow(-0.+0., func(-0.)+0.03, 0.05, 0.05*grad(-0.), fc='b', ec='b')

plt.scatter(0.25, func(0.25))
plt.arrow(0.25, func(0.25)+0.05, 0.05, -0.75*grad(.25), fc='b', ec='b')

plt.arrow(0.25, func(0.25)+0.075, -0.05, 0.75*grad(.25), fc='g', ec='g')

plt.text(-0.5, 0.7, 'For NAG, the counteraction of the\nupdate (green arrow) slows down the\novershooting of the minimum by the\nmomentum',
        bbox={'facecolor':'blue', 'alpha':0.5, 'pad':10})

plt.savefig('nagcm.png')
```

### Geometry of Classical Momentum

```python
plt.figure()
plt.ylim(0, 3)
plt.xlim(0, 2)
plt.title("NAG Descent")
plt.arrow(0.25, 1., 1, 1, head_width=0.05, head_length=0.1, fc='b', ec='b')
plt.arrow(0.25, 1., 1.3, 0.5, head_width=0.05, head_length=0.1, fc='r', ec='r')
plt.arrow(1.3, 2.05, 0.3, -0.5,head_width=0.05, head_length=0.05, fc='b', ec='b')

plt.text(0.75, 1.75, r'$\mu v_t$', fontsize=15)
plt.text(1.485, 1.8, r'$g(\theta_t)$', fontsize=15)
plt.text(0.9, 1.375, r'$v_{t+1}$', fontsize=15, rotation=7)
plt.savefig('naggeom.png')
```

Essentially, Classical Momentum uses the gradient at the current step along with the momentum factor to update the parameter, while NAG performs a partial update through the gradient at $\theta_t + \mu v_t$ to provide a correction which is as yet unknown to CM.

Archive/2017/Notebooks/GD with Fixed Step and Backtracking/Gradient Descent with Momentum.ipynb


# Feuille de TP3 - Partie B : Tracé de fonctions

```python
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
%config InlineBackend.figure_format = 'retina'
```

Tester le code suivant :

```python
def f(x):
	return np.sin(x)

def test_plot():
    s =0.
    e = 4.*np.pi
    x = np.linspace(s,e, 150)
    y = f(x)
    fig, ax = plt.subplots(2,1,figsize=(10,5))
    ax[0].plot(x, y,label="ma courbe")
    ax[0].set_title(r'$y=\sin(x)$')
    ax[0].set_xlabel("x")
    ax[0].set_ylabel("sinus")
    ax[0].legend()

    ax[1].plot(x, np.cos(x),label="ma courbe")
    ax[1].set_title(r'$y=\cos(x)$')
    ax[1].set_xlabel("x")
    ax[1].set_ylabel("cosinus")
    ax[1].legend()

    fig.suptitle("Fonctions trigonométriques élémentaires")
    #fig.show() # Inutile dans les notebooks uniquement.

test_plot()
```

# Exercice 1 : Tracé de fonctions usuelles

## Question 1
On remarquant que la fonction `subplots` prend en argument un nombre de ligne puis un nombre de colonne (et enfin, en argument optionnel, la taille de la figure), créer une figure avec une ligne et deux colonnes puis représenter sur 2 graphes côte à côte les fonctions suivantes :
* sur le graphe de gauche, $t \mapsto \sin(\omega_1 t +\pi)$, $t \mapsto \sin(\omega_2 t)$ puis leur somme,
* sur le graphe de droite, $t \mapsto 0.8 \sin(\omega_1 t +\pi)$, $t \mapsto 2 \sin(\omega_2 t)$ puis leur somme,

avec $\omega_1 = 1$, $\omega_2 = 1.1$ et pour $t \in [0,30\pi]$.

```python
x = np.linspace(s,e, 150)
fig, ax = plt.subplot(1,2)

ax[0,0].plot(x, f(x + np.pi))
ax[0,1].plot(x, f(1.1 * x))

ax[1,0].plot(x, 8.0*f(x + np.pi))
ax[1,1].plot(x, 2 * f(1.1 * x))

```

# Exercice 2 : Courbes paramétrées
 Nous allons représenter des courbes paramétrées, c'est-à dire des courbes pour
lesquelles les cordonnées $x$ et $y$ sont obtenues à partir d'un paramètre qui varie.

## B.A.BA
Pour tracer une courbe paramétrée $x(t),y(t)$, il suffit de définir correctement les tableaux `x` et `y` de façon à ce que `x[i]` et `y[i]` soient les coordonnées de la courbe à l'instant `t[i]`.

### Question
1. Pour $t\in [0;2\pi]$, tracer la courbe paramétrée :
\begin{equation}
\begin{cases}
x(t) &= \sin^3 (t) \\
y(t) &= \cos(t) - \cos^4 (t)
\end{cases}
\end{equation}
2. Tracer la courbe de Lissajou suivante :
\begin{equation}
\begin{cases}
x(t) &= \sin(5t) \\
y(t) &= \sin(3t +\pi)
\end{cases}
\end{equation}

```python
# Mettre votre code ici
```

## Paramétrisation polaire
Cette paramétrisation consiste à repérer un point en fonction d'un angle (le zéro correspondant à l'axe des abscisse) et d'un rayon (distance à l'origine de l'axe), par exemple l'angle $\theta$ pour un cercle $x = rcos(\theta)$, $y = rsin(\theta)$.
Matplotlib fournit _nativement_ des outils pour tracer des courbes en coordonnées polaires.

Pour pouvoir définir une courbe à l'aide des coordonnées polaires, il faut utiliser la projection polaire : `projection='polar'`. En voici deux exemples :

```python
t = np.linspace(0,2.*np.pi,200)

# Exemple 1 : uniquement des polaires
fig, ax = plt.subplots(1,2,figsize=(10,2),subplot_kw=dict(projection='polar'))
ax[0].plot(t,np.ones(t.shape))
ax[1].plot(np.sin(t),np.cos(t))
fig.subplots_adjust(top=0.8,wspace=0.) # Pour ajuster l'espacement (wspace pour horizontal, hspace pour vertical)
fig.suptitle("Uniquement des courbes polaires")

# Exemple 2 : mélange
fig2 = plt.figure()
ax0 = fig2.add_subplot(121, projection='polar')
ax1 = fig2.add_subplot(122, aspect='equal')
ax0.plot(t,np.ones(t.shape))
ax1.plot(np.sin(t),np.cos(t))
fig2.subplots_adjust(top=1.1,wspace=0.5) # Pour ajuster l'espacement (wspace pour horizontal, hspace pour vertical)
fig2.suptitle('Une courbe en polaire et une "classique"')
[]
```

Nous allons tracer des spirales. On peut définir des spirales avec $x =r cos(\theta)$, $y = r sin(\theta)$, mais avec un rayon $r$ qui dépend de l'angle $\theta$. Pour la
spirale logarithmique, on a la loi $r = a^\theta$, et pour la spirale d'Archimède, on a $r = a\theta$, où $a$ est un paramètre fixe, par exemple $1.3$.

|Spirale logarithmique | Spirale archimèdienne|
|---------------|----------------|
|| |

### Question
A l'aide de la fonction`subplots`, scindez la fenêtre graphique en 2, tracer les deux spirales côte à côtes. Donner à votre graphique titres, labels et légendes.

```python
# Mettre votre code ici
```

# Exercice 3 : fonctions de X et de Y

Exécuter les commandes suivantes :

```python
# Import des modules
import numpy as np
import matplotlib.pyplot as plt

# Définition du domaine
x0 = y0 = -3
x1 = y1 = 3

# Création des tableaux 2D contenant les coordonnées X et Y de chaque point (maillage)
x = np.linspace(x0,x1)
y = np.linspace(y0,y1)
X, Y = np.meshgrid(x,y)

# Graphe de la fonction - premier test
fig, axes = plt.subplots(1, 2)
axes[0].imshow(X+Y)
# Graphe de la fonction - version plus élaborée
im1=axes[1].imshow(X+Y, extent =(x0,x1,y0,y1),origin='lower')
fig.colorbar(im1,ax=axes[1])
```

### Question 2
En s'inspirant de la fonction précédente, représenter la fonction $X^2 - 2Y^2$ sur le domaine $[-1;1]\times [-2;2]$.

```python

```

### Question 3
Tester la commande `contour` (pour la documentation, utiliser `?plt.contour` mais préférer `ax.contour` pour l'exécuter).

```python

```


# 2-level system with oscillating detuning

## Imports
Start by importing the necessary packages

```python
%load_ext autoreload
%autoreload 2

import joblib
import matplotlib.pyplot as plt
from matplotlib import animation
from mpl_toolkits.mplot3d import Axes3D
plt.style.use("ggplot")
import numpy as np
import qutip
from sympy import Symbol

from toy_systems.couplings import ToyCoupling, ToyEnergy
from toy_systems.decays import ToyDecay
from toy_systems.hamiltonian import Hamiltonian
from toy_systems.quantum_system import QuantumSystem
from toy_systems.states import Basis, BasisState, ToyQuantumNumbers
```

## Define states
We start by defining the two states of the system: the ground state $|g\rangle$ and the excited $|e\rangle$.

```python
g = BasisState(qn=ToyQuantumNumbers(label="g"))
e = BasisState(qn=ToyQuantumNumbers(label="e"))

# Print the states as a check
print(f"|g> = {g}")
print(f"|e> = {e}")

# Check that the states are orthogonal by taking inner product
print(f"<e|g> = {e @ g}")
```

    |g> = |g>
    |e> = |e>
    <e|g> = 0

## Define basis
We use the two orthogonal states to define a basis as shown below. The indexing of the matrix and vector representations of operators is determined by the order of the states when generating the state

```python
basis = Basis((g,e))
basis.print()
```

    |0> = |g>
    |1> = |e>

## Define energies
I'm going to define the system in the rotating frame so that the Hamiltonian doesn't have any quickly rotating terms of the form $e^{i\omega t}$. The detuning between the driving field and the natural frequency of the system is taken to be $ \Delta(t) = \Delta_0 + \frac{A}{2}\sin(\omega t) $.

```python
E_g = ToyEnergy([g], 0)
E_e = ToyEnergy([e], 1,  time_dep = "Delta - delta * sin(omega*t + phi)", time_args = {"Delta":5, "omega":5, "delta":1, "phi":0})
```

### Define coupling
Take the coupling field between the two states to be constant

```python
coupling = ToyCoupling(g, e, 1, time_dep = "Omega*sin(omega*t + phi)", time_args = {"Omega":1, "omega":5, "phi":0})
```

## Define Hamiltonian
Now that we have the states, energies and couplings, we can define the Hamiltonian

```python
H = Hamiltonian((E_g, E_e, coupling), basis)
display(H)
```

    H =
    [
    [  0, 1.0],
    [1.0, 1.0]]

    in basis Basis: name = None

## Define a QuantumSystem
The QuantumSystem object combines the basis, decays, and Hamiltonian to make setting parameters for time evolution using QuTiP more convenient.

```python
# Define the system
system = QuantumSystem(
    basis=basis,
    couplings=[E_g, E_e, coupling],
    decays = []
)

# Get representations of the Hamiltonian and the decays that will be accepted by qutip
Hqobj, c_qobj = system.get_qobjs()
```

## Time-evolution using `QuTiP`
The `toy_systems` package is designed to take care of generating the Hamiltonian and collapse operators for the quantum system, and spit out a `qutip.QobjEVo`-object. The actual time-evolution is then taken care of by `qutip` (or something else if the user so desires).

```python
# Get a pointer to the time-evolution arguments
args = Hqobj.args
print("Keys for setting arguments:")
print(f"args = {args}")
```

    Keys for setting arguments:
    args = {'Delta': 5, 'omega': 5, 'delta': 1, 'phi': 0, 'Omega': 1}

```python
# Generate a Qobj representing the initial state
psi0 = (1*g).qobj(basis)

# Make operators for getting the probability of being in each state
P_g_op = qutip.Qobj((1*g).density_matrix(basis), type = "oper")
P_e_op = qutip.Qobj((1*e).density_matrix(basis), type = "oper")

# Pauli matrices for calculating components of Bloch vector
sx = qutip.sigmax()
sy = qutip.sigmay()
sz = qutip.sigmaz()

# Set the parameters for the system
args["Omega"] = 1 # The maximum value of the drive field Rabi rate (set to one so can use 1/Omega as unit of time)
args["Delta"] = 5 # Energy of excited state
args["delta"] = args["Delta"]*2 # Amplitude of detuning oscillations
args["omega"] = args["Delta"]*1.0
args["phi"] = np.pi*0.2

# Times at which result is requested
times = np.linspace(-10/np.abs(args["omega"]), 10/np.abs(args["omega"]), 3001) * 2* np.pi

# The solver used by qutip sometimes doesn't seem to notice that adiabatic passage is supposed to be happening, so reduce max step size to
# mitigate this
options = qutip.solver.Options(method = 'adams', nsteps=100000, max_step=1e-2/np.abs(args["omega"]),)

# Setup a progress bar
pb = qutip.ui.progressbar.EnhancedTextProgressBar()

# Run the time-evolution
result = qutip.mesolve(Hqobj, psi0, times, c_ops = c_qobj, e_ops = [P_g_op, P_e_op, sx, sy, sz],
                      progress_bar=pb, options = options)
```

     [*********99%***********] Elapsed 1.64s / Remaining 00:00:00:00[****     17%           ] Elapsed 0.27s / Remaining 00:00:00:01[*********55%*          ] Elapsed 0.92s / Remaining 00:00:00:00[*********74%*****      ] Elapsed 1.24s / Remaining 00:00:00:00[*********98%***********] Elapsed 1.62s / Remaining 00:00:00:00

    IOPub message rate exceeded.
    The Jupyter server will temporarily stop sending output
    to the client in order to avoid crashing it.
    To change this limit, set the config variable
    `--ServerApp.iopub_msg_rate_limit`.

    Current values:
    ServerApp.iopub_msg_rate_limit=1000.0 (msgs/sec)
    ServerApp.rate_limit_window=3.0 (secs)

Plot the result:

```python
fig, (ax,ax2) = plt.subplots(2,1, figsize = (16,9))
ax.plot(times, result.expect[0], label = "P_g")
ax.plot(times, result.expect[1], label = "P_e")
ax.legend()
ax.set_xlabel("Time / (1/Ω)")
ax.set_ylabel("Population in each state")

# Plot the coupling strength and detuning also
ax2c = ax2.twinx()
ln1 = ax2.plot(times, E_e.eval(times, args), label = "Δ")
ax2.set_ylabel('Excited state energy / Omega')
ln2 = coupling.plot_time_dep(times, args, ax = ax2c, label = 'Ω', c = 'tab:blue')
ax2c.grid(False)
ax2c.set_ylabel('')
ax2.legend(ln1+ln2, [l.get_label() for l in (ln1 + ln2)])
ax2c.set_ylabel('Time dependent coupling')
```

### Visualizing on Bloch sphere

```python
# Calculate components of Bloch vector
u = -result.expect[2]
v = result.expect[3]
w = result.expect[4]

# Calculate detuning and coupling strength
deltas = E_e.eval(times, time_args=args)
Omegas = coupling.eval(times, time_args=args)
W_length = np.sqrt(deltas**2 + Omegas**2)

# Set up the bloch sphere
fig = plt.figure(figsize = (16, 9))
ax0 = fig.add_subplot(221, projection = '3d')
ax0.set_title('Bloch sphere', fontsize = 18)
b = qutip.Bloch(fig = fig, axes = ax0)
b.frame_width = 1
b.figsize = (16,16)

# Add initial Bloch vector and Rabi vector
b.add_vectors([np.array([u[0],v[0],w[0]]), np.array([Omegas[0], 0, deltas[0]])])
b.make_sphere()

# Set up plot of detuning
ax1 = fig.add_subplot(222)
ax1.plot(times, deltas, label = "Δ")
ax1.set_ylabel('Detuning', fontsize = 16)
delta_marker = ax1.plot(times[0], deltas[0], marker = 'o', markersize=8, c = 'k', fillstyle='none')

# Set up plot of Rabi rate
ax2 = fig.add_subplot(224)
ax2.plot(times, Omegas, label = "Ω", c = 'C1')
ax2.set_ylabel('Rabi rate', fontsize = 16)
ax2.set_xlabel('Time')
rabi_marker = ax2.plot(times[0], Omegas[0], marker = 'o', markersize=8, c = 'k', fillstyle='none')

# Set up plot of populations
ax3 = fig.add_subplot(223)
ax3.plot(times, result.expect[0], label = r"P$_0$")
ax3.plot(times, result.expect[1], label = r"P$_1$")
ax3.set_ylabel('Population in each state', fontsize = 16)
vline = ax3.axvline(times[0], ls = '--', c = 'k')
ax3.set_xlabel('Time')
ax3.legend()

# Define function that generates frames for animation
def animate(i):
    b.clear()
    b.add_vectors([np.array([u[i],v[i],w[i]]), np.array([Omegas[i], 0, deltas[i]])/W_length[i]])
    i_start = np.max([0, i-100])
    b.add_points([u[i_start:i+1],v[i_start:i+1],w[i_start:i+1]])
    b.make_sphere()

    ax1.lines[1].remove()
    ax1.plot(times[i], deltas[i], marker = 'o', markersize=8, c = 'k', fillstyle='none')

    ax2.lines[1].remove()
    ax2.plot(times[i], Omegas[i], marker = 'o', markersize=8, c = 'k', fillstyle='none')

    ax3.lines[2].remove()
    ax3.axvline(times[i], ls = '--', c = 'k')

    return fig, (ax0, ax1, ax2, ax3)
```

```python
fig, _ = animate(200)
fig
```

```python
# %%timeit
ani = animation.FuncAnimation(fig, animate, range(0,len(times),3), blit=False, repeat=False)
ani.save(f"oscillating_detuning_Delta0={args['Delta0']:.1f}_A={args['A']:.1f}_omega={args['omega']:.1f}.mp4", fps = 60)
```

examples/Two-level system with oscillating detuning - no RWA.ipynb