# dependances
numpy
matplotlib
sympy
Évaluation de la CFL pour différents couple de schémas espace-temps
===

Table des matières :

* [Présentation du schéma WENO](#1.-Présentation-du-schéma-WENO)
	+ [WENO linéarisé](#1.1-WENO-linéarisé)
	+ [WENO non linéarisé](#1.2-WENO-non-linéarisé)
* [Présentation des schémas compacts](#2.-Présentation-des-schémas-compacts)
* [Schéma BWENO](#3.-Schéma-BWENO)
	+ [Étude du coefficient d'amplification](#3.1-Étude-du-coefficient-d'amplification)
	+ [Tests numériques](#3.2-Tests-numériques)
* [Schémas RK](#4.-Schémas-RK)
	+ [Petit test en 3D](#4.1-Petit-test-en-3D)
	+ [Exploration pour les ordres supérieurs](#4.2-Exploration-pour-les-ordres-supérieurs)
	+ [Polynôme caractéristique pour d'autres méthodes](#4.3-Polynôme-caractéristique-pour-d'autres-méthodes)
	+ [Recherche du meilleur domaine de stabilité](#4.4-Recherche-du-meilleur-domaine-de-stabilité)
	+ [Domaine de stabilité intermédiaire](#4.5-Domaine-de-stabilité-intermédiaire)
	+ [*Order Star*](#4.6-Order-Star)
* [Étude de couples RK$N$-WENO5](#5.-Étude-de-couples-RK$N$-WENO5)
	+ [Automatisation du calcul de CFL](#5.1-Automatisation-du-calcul-de-CFL)
	+ [Validation par simulation numérique](#5.2-Validation-par-simulation-numérique)
	+ [Estimation du coût numérique](#5.3-Estimation-du-coût-numérique)
* [Schémas Lawson et exponentiels](#6.-Schémas-Lawson-et-exponentiels)
	+ [Schémas exponentielles](#6.1-Schémas-exponentielles)
	+ [Schémas de Lawson](#6.2-Schémas-de-Lawson)
* [Schéma *diagonal implicit Runge-Kutta*](#7.-Schéma-diagonal-implicit-Runge-Kutta)

Nous nous intéressons ici à résoudre au problème :

$$
  \partial_t u + \partial_x f(u) = 0
$$

Dans le cadre de l'étude de la stabilité du schéma, nous nous intéressons uniquement au cas $f'>0$ (dans la pratique on prendra même l'identité, *i.e.* un transport de vitesse 1).

La discrétisation spatiale du problème s'effectue avec le schéma WENO à l'ordre 5. Il s'agit d'un schéma non linéaire, donc la stabilité a déjà été étudié dans le cas linéarisé dans [Wang R. and Spiteri R. J. (2007)](https://doi.org/10.1137/050637868).

Nous effectuons une discrétisation en temps à l'aide d'un schéma RK3 (ERK SSP(3,3) ou ERK SSP(4,3)), mais nous allons dans un premier temps vérifier quelques résultats sur différents schémas temporels.

```python
"""
global import, and define symbols
"""

import numpy as np
import matplotlib.pyplot as plt

import sympy as sp
from IPython.display import display
sp.init_printing()

from sympy import I
#sp.init_printing(use_latex='mathjax')

phi      = sp.symbols("\\phi",real=True) #,positive=True)
dx,dv,dt = sp.symbols("\\Delta\\ x \\Delta\\ v \\Delta\\ t",real=True,positive=True)
sig      = sp.symbols("\\sigma",real=True,positive=True)
x = sp.symbols("x",real=True)

def reim(b):
  """
    function to split into 2 arrays real part and imaginary part of complex array
  """
  return ([z.real for z in b],[z.imag for z in b])

class Data(object):
  """
    dummy class to store data
  """
  pass
data = Data()

# phi array to store angle where evaluate ztilde(phi)
data.phi = np.linspace(0,2.0*np.pi,1000)
```

# 1. Présentation du schéma WENO

Le schéma WENO nécessite l'évaluation d'*indicator of smoothness* $IS_i$, pour évaluer les poids des 3 estimateurs ENO. N'est présenté ici que le calcul si la fonction $f$ est de dérivée positive (i.e. transport à vitesse positive).

$$
  \begin{aligned}
    IS_0 &= \frac{13}{12}(f_{j-2} - 2f_{j-1} + f_{j}  )^2 + \frac{1}{4}( f_{j-2} - 4f_{j-1} + 3f_{j}  )^2 \\
    IS_1 &= \frac{13}{12}(f_{j-1} - 2f_{j}   + f_{j+1})^2 + \frac{1}{4}( f_{j-1} -  f_{j+1})^2 \\
    IS_2 &= \frac{13}{12}(f_{j}   - 2f_{j+1} + f_{j+2})^2 + \frac{1}{4}(3f_{j}   - 4f_{j+1} +  f_{j+2})^2 \\
  \end{aligned}
$$

On calcule ensuite les poids non normalisés :

$$
    \alpha_i = \frac{\gamma_i}{(\epsilon + IS_i)^2},\quad i=0,1,2
$$

avec $\gamma_0 = \frac{1}{10}$, $\gamma_0 = \frac{6}{10}$ et $\gamma_0 = \frac{3}{10}$.

La normalisation des poids s'effectue comme suit :

$$
    w_i = \frac{\alpha_i}{\sum_m \alpha_m},\quad i=0,1,2
$$

Nous pouvons ensuite calculer les flux numériques pour WENO5 donnés par :

$$
  \begin{aligned}
    \hat{f}_{j+\frac{1}{2}}=\ &  w_0\left( \frac{2}{6}f_{j-2} - \frac{7}{6}f_{j-1} +\frac{11}{6}f_{j}  \right)
                             +   w_1\left(-\frac{1}{6}f_{j-1} + \frac{5}{6}f_{j}   + \frac{2}{6}f_{j+1}\right) \\
                             + & w_2\left( \frac{2}{6}f_{j}   + \frac{5}{6}f_{j+1} - \frac{1}{6}f_{j+2}\right)
  \end{aligned}
$$

La méthode WENO5, pour un transport à vitesse positive, prend la forme finale :

$$
  \frac{\mathrm{d}u_j}{\mathrm{d}t} = -\frac{1}{\Delta x}\left( \hat{f}_{j+\frac{1}{2}} - \hat{f}_{j-\frac{1}{2}} \right)
$$

C'est ceci dont nous allons nous intéresser à la stabilité avec différentes discrétisation en temps.

```python
"""
  fonctions pour l'aide à l'écriture du schéma WENO
"""

def f(k):
  """
    return $f_{i+k}$
  """
  if k==0:
    return sp.symbols("f_j")
  return sp.symbols("f_j%+d"%k)

def stencil(i_min,i_max) :
  """
    stencil
    return dictionary of (i,f_i) value between `i_min` and `i_max`
  """
  return dict(enumerate([ f(i) for i in range(i_min,i_max+1) ],start=i_min))
f_stencil = stencil(-3,3)

def symbols_to( stencil , func ):
  """
    symbols_to
    convert dictionary `stencil` of `int => sp.Symbols` to dictionary of`sp.Symbols => sp.Symbols`
    useful to replace all $f_i$ symbols by an other value in expression
  """
  r = {}
  for j,f in stencil.items():
    r[f] = func(j,f)
  return r

def symbols_to_vonNeumann( stencil ):
  """
    symbols_to_vonNeumann
    convert all $f_i$ symbols to equivalent in von Neumann analysis with exponential
    $\phi$ symbol need to be initialized
  """
  return symbols_to( stencil , lambda j,_:sp.exp(I*j*phi) )
```

Nous allons utiliser l'analyse de von Neumann pour estimer le coefficient d'amplification $\lambda^W(\phi)$ du schéma WENO. L'analyse de von Neumann se basant sur un concept similaire à celui d'une transformée de Fourier, il n'est pas possible de se placer localement en un point de la courbe, nous excitons, selon notre condition initiale, plusieurs modes.

> Il est possible de n'exciter qu'un seul mode si la condition initiale est une simple sinusoïde correspondant à un mode d'excitation particulier. Dans ce cas nous pouvons estimer une CFL propre à cette condition initiale.

Nous allons nous intéresser à 2 courbes de représentation de stabilité du schéma WENO, une première correspondant à la version linéarisée de la méthode (calcul effectué par [Wang R. and Spiteri R. J. (2007)](https://doi.org/10.1137/050637868) ainsi que [Motamed M. and Macdonald C. B. (2010)](https://doi.org/10.1007/s10915-010-9423-9)). Cette linéarisation consiste à approximer les poids (la non linéarité du schéma) par :

$$
  w_i = \gamma_i + \epsilon_i
$$

avec $\epsilon_i = \mathcal{O}(\Delta x^2)$ qui sera par conséquent majoré et négligé par la suite.

Par la suite nous utiliserons la version complète de la méthode WENO, donnant ainsi une seconde courbe.

## 1.1 WENO linéarisé

Le schéma WENO peut se décomposer en une partie linéaire et une partie non linéaire (entièrement guidé par les poids). La fonction $\lambda^W(\phi)$ se décompose comme :

$$
  \lambda^W(\phi) = \tilde{z}(\phi) + M(\{\epsilon_i\}_i,\phi)
$$

Wang et Spiteri montrent qu'il est possible de majorer tranquillement $M$ (partie non linéaire du WENO). Ceci est intéressant pour démontrer la stabilité ou non d'un couple de schéma espace-temps. L'utilisation pour l'évaluation d'une CFL est peut-être un peu plus discutable, cela donne au moins une approximation de celle-ci, facilement calculable.

```python
"""
  linearized WENO method
"""
wenol = Data() # objet qui contiendra les données pour l'analyse numérique du schéma WENO linéarisé

e0,e1,e2 = sp.symbols("\\epsilon_0 \\epsilon_1 \\epsilon_2",real=True)
e3,e4,e5 = sp.symbols("\\epsilon_3 \\epsilon_4 \\epsilon_5",real=True)

w0_l = sp.Rational(1,10) + e0
w1_l = sp.Rational(6,10) + e1
w2_l = sp.Rational(3,10) + e2

w3_l = sp.Rational(1,10) + e3
w4_l = sp.Rational(6,10) + e4
w5_l = sp.Rational(3,10) + e5

fjp12_l = ( w0_l*( sp.Rational(2,6)*f(-2) - sp.Rational(7,6)*f(-1) + sp.Rational(11,6)*f(+0) )
          + w1_l*(-sp.Rational(1,6)*f(-1) + sp.Rational(5,6)*f(+0) +  sp.Rational(2,6)*f(+1) )
          + w2_l*( sp.Rational(2,6)*f(+0) + sp.Rational(5,6)*f(+1) -  sp.Rational(1,6)*f(+2) ) )
fjm12_l = fjp12_l.subs(symbols_to(f_stencil, lambda j,_:f(j-1)),simultaneous=True).subs({e0:e3,e1:e4,e2:e5})

flux = fjp12_l - fjm12_l

flux
```

Nous distinguons 2 parties dans ce flux :

* $\tilde{z}(\phi)$ qui est la partie linéaire du schéma WENO
* $M(\{\epsilon_i\}_{i=0,\dots,5},\phi)$ la partie non linéaire du schéma WENO

L'analyse de von Neumann nous permettra d'étudier les différentes contributions de ces termes

```python
ztilde = flux.expand().subs({e0:0,e1:0,e2:0,e3:0,e4:0,e5:0})
M = (flux-ztilde).expand().collect([e0,e1,e2,e3,e4,e5])

ztilde_vN = ztilde.subs(symbols_to_vonNeumann(f_stencil))
M_vN = M.subs(symbols_to_vonNeumann(f_stencil))
```

```python
(ztilde,ztilde_vN)
```

```python
(M,M_vN)
```

On va vouloir étudier les données $\tilde{z}$ et $M$. Ces variables sont issues de la décomposition de $\lambda^Z(\phi) = \tilde{z}(\phi) + M(\{\epsilon_i\}_{i=0,\dots,5},\phi)$ avec $M$ comportant tous les $\{\epsilon_i\}_{i=0,\dots,5}$, et $\tilde{z}$ le reste. La seule étude de $\tilde{z}$ suffit à l'étude de la stabilité linéaire du schéma avec différentes méthodes en temps, c'est-à-dire l'existence d'un nombre $\tilde{\sigma}$ tel que pour tout couple $(\Delta t,\Delta x)$ vérifiant :

$$
  \Delta t \leq \tilde{\sigma}\Delta x
$$

la solution calculée ne diverge pas (on peut sans doute écrire un truc du genre $||u^{n+1}|| \leq ||u^n||$ mais dans le doute je ne l'écris pas).

> L'étude du schéma WENO linéarisé permet d'affirmer l'existence d'une condition CFL et donc affirmer la stabilité d'un schéma, elle ne permet par contre qu'une évaluation de la CFL (que l'on peut conjecturer comme étant systématiquement plus petite que la CFL du schéma complet).

### 1.1.1 Les coefficients de $M$

$M$ représente la partie avec les $\{\epsilon_j\}_{j\in[0,\cdots,5]} = \mathcal{O}(\Delta x^2)$.

```python
wenol.func_M = []
wenol.data_M = []
for arg in M_vN.args:
  fun = sp.lambdify(phi,arg.subs({e0:1,e1:1,e2:1,e3:1,e4:1,e5:1}),'numpy')
  wenol.func_M.append(fun)
  wenol.data_M.append(fun(data.phi))
```

```python
"""
  Plot of each composant in M (one by epsilon_i value)
"""
fig, (ax1, ax2) = plt.subplots(2,1)

for fun in wenol.func_M:
  ax1.plot(data.phi,fun(data.phi).real); ax1.set_xlabel("$\\varphi$"); ax1.set_ylabel("$\\Re(M_{\\epsilon_i})$")
  ax2.plot(data.phi,fun(data.phi).imag); ax2.set_xlabel("$\\varphi$"); ax2.set_ylabel("$\\Im(M_{\\epsilon_i})$")

plt.show()
```

En réalité la valeur des différentes composantes de $M$ nous importent peu. Ce qu'il faut remarquer est la forme général de chaque composante, prenons comme exemple la composante $\epsilon_0$ :

$$
  \begin{aligned}
    \Re \left| \epsilon_0\left(\frac{11}{6} -\frac{7}{6}e^{-i\phi} + \frac{1}{3}e^{-2i\phi}\right)\right| &\leq c_0^{\Re}|\epsilon_0| \\
    \Im \left| \epsilon_0\left(\frac{11}{6} -\frac{7}{6}e^{-i\phi} + \frac{1}{3}e^{-2i\phi}\right)\right| &\leq c_0^{\Im}|\epsilon_0|
  \end{aligned}
$$

avec $c_0^{\Re|\Im}$ des constantes positives. Dans [Wang R. and Spiteri R. J. (2007)](https://doi.org/10.1137/050637868), une estimation de $c_0^{\Re}=\frac{10}{3}$ est prise comme exemple. On majore ainsi toutes les composantes en $\epsilon_i$ de $M$, on se retrouve alors avec :

$$
  \begin{aligned}
    \left|\Re\left(M(\{\epsilon_i\}_{i=0,\dots,5},\phi)\right)\right| &\leq \Gamma^{\Re}\max_{0\leq m \leq 5}|\epsilon_m| \\
    \left|\Im\left(M(\{\epsilon_i\}_{i=0,\dots,5},\phi)\right)\right| &\leq \Gamma^{\Im}\max_{0\leq m \leq 5}|\epsilon_m|
  \end{aligned}
$$

avec $\Gamma^{\Re|\Im}$ des constantes postives ne dépendant que de la taille du *stencil* considéré. Cela permet de justifier $M = \mathcal{O}(\Delta x^2)$.

```python
"""
  Plot of the sum of all composant of M (epsilon_i=1 for all i)
"""

fig, (ax1, ax2) = plt.subplots(2,1)

M_sum = reim(sum(wenol.data_M))
ax1.plot(data.phi,M_sum[0]); ax1.set_xlabel("$\\varphi$"); ax1.set_ylabel("$\\sum_i\\Re(M_{\\epsilon_i})$")
ax2.plot(data.phi,M_sum[1]); ax2.set_xlabel("$\\varphi$"); ax2.set_ylabel("$\\sum_i\\Im(M_{\\epsilon_i})$")
plt.show()
```

La somme de toutes les composantes en $\epsilon_i$ est facilement majorable (il ne s'agit que d'une somme de cosinus et sinus). Wang et Spiteri majore ceci par $\Gamma\max_i|\epsilon_i|$, ce qu'ils retiennent surtout est la majoration par un truc qui est proportionnel à $\epsilon_i = \mathcal{O}(\Delta x^2)$.

```python
""" just because it's pretty: all composant of M in complex plan """
for dat in wenol.data_M:
  plt.plot(dat.real,dat.imag)
plt.show()
```

### 1.1.2 La partie linéaire $\tilde{z}$

$\tilde{z}$ est la partie linéaire de $\lambda^W(\phi)$ (c'est-à-dire ne dépendant pas des $\{\epsilon_j\}_{j\in[0,\cdots,5]}$. C'est la partie principale, la valeur exacte de $\lambda^W(\phi)$ peut être interprété comme la valeur de $\tilde{z} + \mathcal{O}(\Delta x^2)$.

Donc en prenant $\Delta x$ suffisamment petit, $\lambda^W(\phi) \xrightarrow{\Delta x \to 0} \tilde{z}(\phi)$.

```python
wenol.func_z = sp.lambdify(phi,ztilde_vN,'numpy')
wenol.data_z = wenol.func_z(data.phi)

plt.plot(*reim(wenol.data_z))
plt.axis('equal')
plt.show()
```

Le résultat est similaire à ce qui est présenté systématiquement comme la courbe de stabilité de la linéarisation du schéma WENO.

### 1.1.3 Étude du schéma WENO linéarisé

Le schéma WENO linéarisé, qui correspond à l'approximation des poids à :

$$
    w_i = \gamma_i
$$

c'est-à-dire en considérant que les *indicators of smootheness* valent tous 1, ce qui se justifie avec une fonction suffisamment régulière.

On perd *a priori* l'intérêt du schéma WENO, il est donc intéressant d'étudier un peu plus ce schéma.

#### 1.1.3.1 Mesure de l'ordre

On mesure numériquement l'ordre à partir du problème suivant :

$$
  \begin{cases}
    \partial_t u + \partial_x u = 0 \\
    u(t=0,x) = \cos(x)
  \end{cases}
$$

On retrouve bien l'ordre 5 de la méthode WENO classique.

#### 1.1.3.2 Comportement face à une discontinuité

Observons le comportement du schéma WENO linéarisé face à une discontinuité. Nous conservons ici une simple équation de transport linéaire à vitesse 1. La condition initiale est donnée par :

$$
  u(t=0,x) = \begin{cases}
    x \quad &\text{si $x \in [0,1]$}\\
    1 \quad &\text{si $x \in [1,4]$}\\
    0 \quad &\text{sinon}
  \end{cases}
$$

La simulation s'effectuant jusqu'au temps $T_f = 2\pi$ sur le domaine spatial périodique $[0,2\pi]$ avec $N_x = 100$ le nombre de points, et une condition CFL $\Delta t = 0.1\Delta x$. La solution exacte coïncide avec la condition initiale. La discrétisation en temps est un RK SSP(3,3).

On remarque que le schéma WENO linéarisé oscille à proximité d'une discontinuité, le schéma WENO classique quant à lui devient plus visqueux à l'aide des poids qui permettent d'effectuer une meilleure interpolation qu'une interpolation polynomiale de degré élevé induisant localement des oscillations.

#### 1.1.3.3 Viscosité en temps long

Il n'est pas beaucoup plus ou moins visqueux que le schéma WENO classique, j'ai effectué des simulations en temps long ($T_f = 300\pi$) de différentes conditions initiales et j'ai surtout observé un décalage de phase entre les schémas WENO et WENO linéarisé et la solution exacte.

Ici la simulation d'un cosinus de mode 2 : $u(t=0,x)=\cos(2x)$, toujours avec $N_x = 100$, et une condition CFL $\Delta t = \Delta x$.

Il semble en premier abord, moins diffusif que le schéma WENO classique (en effet l'amplitude en partie réelle est plus faible que la version non linéarisé, voir ci-dessous).

> La diffusion moindre du WENO linéarisé n'est pas flagrante sur ce cas test.

## 1.2 WENO non linéarisé

Puisque j'effectue mes calculs à l'aide de `sympy` et qu'il est facilement d'écrire automatiquement une fonction de $\phi$ du schéma WENO complet, j'étudie l'analyse de von Neumann du schéma WENO complet non linéarisé. Il n'est pas possible de justifier ce calcul par le calcul d'une transformée de Fourrier du schéma, on peut cependant indiquer que l'on choisit en entrée une fonction se décomposant en série de Fourrier.

Le calcul fonctionne malgré la non linéarité des poids du schéma parce qu'il est toujours possible de l'exprimer comme :

$$
    \hat{u}^{n+1} = W(\phi)\hat{u}^n
$$

en effet la non linéarité des coefficients $IS_i$ permet toujours d’exhiber en facteur $(\hat{u}^n)^2$, puis un $(\hat{u}^n)^4$ dans les poids non normalisés $\alpha_i$, module que l'on simplifie lors de la normalisation des poids $w_i$ où il ne reste plus que la phase. C'est ce même phénomène qui permet de mettre en facteur la vitesse dans cette méthode lors d'une équation de transport à vitesse constante.

```python
"""
  complet WENO method
"""

epsilon = sp.symbols("\\epsilon",real=True,positive=True)

IS0 = sp.Rational(13,12)*( f(-2) - 2*f(-1) + f(+0) )**2 + sp.Rational(1,4)*( f(-2) - 4*f(-1) + 3*f(+0) )**2
IS1 = sp.Rational(13,12)*( f(-1) - 2*f(+0) + f(+1) )**2 + sp.Rational(1,4)*( f(-1) - f(+1) )**2
IS2 = sp.Rational(13,12)*( f(+0) - 2*f(+1) + f(+2) )**2 + sp.Rational(1,4)*( f(+2) - 4*f(+1) + 3*f(+0) )**2

a0 = sp.Rational(1,10)*(1/(epsilon+IS0))**2
a1 = sp.Rational(6,10)*(1/(epsilon+IS1))**2
a2 = sp.Rational(3,10)*(1/(epsilon+IS2))**2

s = a0 + a1 + a2
w0 = a0 / s
w1 = a1 / s
w2 = a2 / s

fjp12 = ( w0*( sp.Rational(2,6)*f(-2) - sp.Rational(7,6)*f(-1) + sp.Rational(11,6)*f(+0) )
        + w1*(-sp.Rational(1,6)*f(-1) + sp.Rational(5,6)*f(+0) +  sp.Rational(2,6)*f(+1) )
        + w2*( sp.Rational(2,6)*f(+0) + sp.Rational(5,6)*f(+1) -  sp.Rational(1,6)*f(+2) ) )
# on définit fjm12 comme étant fjp12 où on substitut f(j) par f(j-1)
fjm12 = fjp12.subs(symbols_to(f_stencil, lambda j,_:f(j-1)),simultaneous=True)
```

```python
weno = Data()
weno.func = sp.lambdify(phi,(fjp12 - fjm12).subs(symbols_to_vonNeumann(f_stencil)).subs(epsilon,10**(-6)),'numpy')
weno.data = weno.func(data.phi)

plt.plot(*reim(weno.data),label="WENO")
plt.plot(*reim(wenol.data_z),label="linearized WENO")
plt.axis('equal')
plt.legend(loc=(0.,1.02),ncol=2, mode="expand",borderaxespad=0.)
plt.show()
```

On remarque une divergence dans les résultats. Une précédente étude avait montré que la CFL du couple RK3-WENO5 était atteinte à la base de l'*œuf* que forme la linéarisation du schéma WENO. La version non linéarisée étant moins large à la base, la CFL semble pouvoir être plus importante.

> Il est intéressant à noter qu'un transport pur s'exprime avec l'axe imaginaire, la partie réelle traduit un phénomène de diffusion. On remarque que le schéma WENO linéarisé est donc moins diffusif que sa version standard. Ceci s'explique par la construction du schéma WENO qui par le biais de ses poids, diffuse à l'approche d'une discontinuité au lieu d'osciller. Une confirmation de se résultat a été apporté numériquement par des tests sur le schémas WENO linéarisé, celui-ci oscille légèrement à l'approche d'une discontinuité au lieu de diffuser. Diffusion observée lors de la comparaison des deux schémas en temps long.

# 2. Présentation des schémas compacts

Une autre approche avait été lancé au début du stage, dans la recherche de schémas d'ordre élevé, celle des schémas compacts. Ceux-ci sont oscillants, et je n'ai jamais réussi à les faire fonctionner dans le cadre d'une vitesse négative (il s'agit sans doute d'une symétrie similaire à celle effectuée dans le schéma WENO, symétrie pour laquelle j'ai toujours bêtement appliqué les formules).

On ne va pas présenté de manière exhaustive la famille des schémas compacts, de plus on ne s'intéressera ici qu'au schéma d'ordre 5 (ordre identique à celui du schéma WENO5 précédemment présenté). Il s'agit d'une interpolation polynomiale lagrangienne de degré 5. Comme toute interpolation polynomiale de degré élevé, celle-ci présente le risque d'osciller (c'est pour prévenir ce caractère que le schéma WENO fait intervenir des poids non linéaires).

Pour plus d'information j'invite le lecteur à lire [Després B. (2009)](https://doi.org/10.1137/080734571).

On souhaite résoudre un problème du type :

$$
  \partial_t u + v\partial_x u = 0,\quad v>0
$$

nous prendrons $v=1$. Le nombre de Courant est défini par $\nu = v\frac{\Delta t}{\Delta x}$.

Le schéma d'ordre 5 qui nous intéresse possède un flux qui s'écrit comme :

$$
  \begin{aligned}
    u_{j+\frac{1}{2}} = u^n_{j+2} + \frac{\nu+3}{2}(u^n_{j+1}-u^n_{j+2}) + \frac{(2+\nu)(1+\nu)}{6}(u^n_j - 2u^n_{j+1} + u^n_{j+2}) + \frac{(2+\nu)(1+\nu)(\nu-1)}{24}(u^n_{j-1} - 3u^n_{j} + 3u^n_{j+1} - u^n_{j+2}) + \frac{(2+\nu)(1+\nu)(\nu-1)(\nu-2)}{120}(u^n_{j-2} - 4u^n_{j-1} + 6u^n_{j} - 4u^n_{j+1} + u^n_{j+2})
  \end{aligned}
$$

On obtient, après analyse de von Neumann, un flux qui s'écrit comme un polynôme de $\nu = \frac{\Delta t}{\Delta x}$ et de $e^{i\phi}$. Il devient par conséquent difficile de tracer la courbe du coefficient d'amplification du schéma compact, à la manière du schéma WENO. Par conséquent l'étude du schéma compact s'arrête là.

```python
nu = sp.symbols("\\nu") #dt/dx
ujp12 = ( f(-2) + (nu+3)/2*(f(+1)-f(+2)) + (2+nu)*(1+nu)/6*(f(+0) - 2*f(+1) + f(+2))
        + (2+nu)*(1+nu)*(nu-1)/24*(f(-1) - 3*f(+0) + 3*f(+1) - f(+2))
        + (2+nu)*(1+nu)*(nu-1)*(nu-2)/120*(f(-2) - 4*f(-1) + 6*f(+0) - 4*f(+1) + f(+2)) )
ujm12 = ujp12.subs(symbols_to(f_stencil, lambda j,_:f(j-1)),simultaneous=True)

(ujp12-ujm12).subs(symbols_to_vonNeumann(f_stencil)).expand().collect(nu)
```

Le problème est similaire à celui de l'étude du schéma IFRK, c'est-à-dire que l'on obtient un polynôme à 2 variables. L'idée est alors de tester de tracer une fonction de $\phi$ pour différentes valeurs de $\nu$.

```python
compact = Data()
compact.func = []
compact.data = []

save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [10,8]
for i in range(0,201,10):
  nu_num = i/100.
  print(i,end="\r")
  fun = sp.lambdify(phi,(ujp12-ujm12).subs(symbols_to_vonNeumann(f_stencil)).expand().subs(nu,nu_num),'numpy')
  compact.func.append(fun)
  dat = fun(data.phi)
  compact.data.append(dat)
  plt.plot(*reim(-dat),label="ν ={}".format(nu_num))
print()
plt.legend(loc=(0.,1.02),ncol=8, mode="expand",borderaxespad=0.)
plt.show()
plt.rcParams['figure.figsize'] = save_figsize
```

# 3. Schéma BWENO

Dans [Banks J. W. and Hittinger J. A. F. (2010)](https://doi.org/10.1109/TPS.2010.2056937) et [Banks J. W. et al (2019)](https://github.com/Kivvix/miMaS/blob/master/bibliography/pdf/2019-Banks.pdf) est proposé une modification du schéma WENO plus intéressante pour l'étude de l'équation de Vlasov. Nous étudierons ici la présentation effectuée dans la seconde publication. Dans la continuation des schémas d'ordre élevé en espace, nous nous intéresserons au schéma BWENO d'ordre 6 présenté.

L'idée de ce schéma reste similaire à celle du WENO dans l'esprit du calcul de poids non linéaire s'adaptant aux irrégularités de la fonction. Le nombre de poids est en revanche limité à 2, et ceux-ci ne sont plus des polynôme en $\left(f_{i+k}^4\right)_{k\in [\![ -3,3 ]\!]}$ mais seulement de degré 2 : $\left(f_{i+k}^2\right)_{k\in [\![ -3,3 ]\!]}$. Cette diminution du nombre de multiplication peut rendre ce schéma compétitif au niveau du temps de calcul. Les solutions attendues dans la simulation de plasmas avec l'équation de Vlasov-Poisson sont régulières, l'ordre élevé des schémas sert à capturer les forts gradients de la solution qui peuvent apparaître lors de la filamentation.

Le flux $\hat{u}^{(B)}_{j-\frac{1}{2}}$ du schéma BWENO d'ordre $p$ s'obtient par l'addition pondéré de 2 interpolations $L$ et $R$, d'ordre $p-1$. Nous ne nous intéresserons ici qu'à la méthode d'ordre 6, nous nous abstiendrons donc d'indiquer l'ordre.

$$
  \hat{u}^{(B)}_{j-\frac{1}{2}} = w^{(L)}_{j-\frac{1}{2}}u^{(L)}_{j-\frac{1}{2}} + w^{(R)}_{j-\frac{1}{2}}u^{(R)}_{j-\frac{1}{2}}
$$

L'écriture du schéma commence par l'estimation des indicateurs de continuité :

$$
  \begin{aligned}
      \beta(u_i) = (\Delta^1_4u_i)\left( (\Delta^1_4u_i) + (\Delta^2_4u_i) + \frac{1}{3}(\Delta^3_2u_i) + \frac{1}{12}(\Delta^4_2u_i) \right) \\
      + (\Delta^2_4u_i)\left( \frac{4}{3}(\Delta^2_4u_i) + \frac{5}{4}(\Delta^3_2u_i) + \frac{2}{5}(\Delta^4_2u_i) \right) \\
      + (\Delta^3_2u_i)\left( \frac{83}{60}(\Delta^3_2u_i) + \frac{23}{18}(\Delta^4_2u_i) \right) \\
      + \frac{437}{315}(\Delta^4_2u_i)^2
  \end{aligned}
$$

où les $(\Delta^d_pu_i)$ sont des approximations d'ordre $p$ de la dérivée d'ordre $d$ :

$$
  \begin{aligned}
    (\Delta^1_4u_i) &= \frac{1}{12}\left( -u_{j+2} +  8u_{j+1}           -  8u_{j-1} + u_{j-2} \right) \\
    (\Delta^2_4u_i) &= \frac{1}{12}\left( -u_{j+2} + 16u_{j+1} - 30u_{j} + 16u_{j-1} - u_{j-2} \right) \\
    (\Delta^3_2u_i) &=  \frac{1}{2}\left(  u_{j+2} -  2u_{j+1}           +  2u_{j-1} - u_{j-2} \right) \\
    (\Delta^4_2u_i) &=             \left(  u_{j+2} -  4u_{j+1} +  6u_{j} -  4u_{j-1} + u_{j-2} \right) \\
  \end{aligned}
$$

Nous prendrons $\beta^{(L)}_{j-\frac{1}{2}} = \beta(u_{j-1})$ et $\beta^{(R)}_{j-\frac{1}{2}} = \beta(u_j)$ comme indicateurs de continuité respectivement à gauche et à droite.

Les 2 poids non normalisés sont :

$$
  a^{(\Xi)}_{j-\frac{1}{2}} = \frac{d}{\epsilon + \beta^{(\Xi)}_{j-\frac{1}{2}}}
$$

où $\Xi = L,R$, $d=\frac{1}{2}$ et $\epsilon$ un paramètre pour éviter la nullité du dénominateur. Ce qui nous donne les poids :

$$
  \tilde{w}^{(\Xi)}_{j-\frac{1}{2}} = \frac{a^{(\Xi)}_{j-\frac{1}{2}}}{a^{(L)}_{j-\frac{1}{2}}+a^{(R)}_{j-\frac{1}{2}}}
$$

Les poids sont ensuites classé selon la direction, ici pour une vitesse positive (dans l'étude du schéma nous prendrons $v=1$) :

$$
  \begin{cases}
    w^{(L)} = \max\left(\tilde{w}^{(L)}_{j-\frac{1}{2}},\tilde{w}^{(R)}_{j-\frac{1}{2}}\right) \\
    w^{(R)} = \min\left(\tilde{w}^{(L)}_{j-\frac{1}{2}},\tilde{w}^{(R)}_{j-\frac{1}{2}}\right)
  \end{cases}
$$

Les flux à gauche et à droite sont maintenant calculés comme suit :

$$
  \begin{cases}
    \hat{u}^{(L)}_{j-\frac{1}{2}} = \frac{1}{60}\left( -3u_{j+1}+27u_{j}+47u_{j-1} -13u_{j-2} + 2u_{j-3} \right) \\
    \hat{u}^{(R)}_{j-\frac{1}{2}} = \frac{1}{60}\left( 2u_{j+2}-13u_{j+1}+47u_{j}+27u_{j-1}-3u_{j-2}  \right) \\
  \end{cases}
$$

On peut enfin calculer une approximation de $\partial_x u$ à l'aide du flux de BWENO par :

$$
  \partial_x u \approx \frac{1}{\Delta x}\left( \hat{u}^{(B)}_{j+\frac{1}{2}} - \hat{u}^{(B)}_{j-\frac{1}{2}} \right)
$$

## 3.1 Étude du coefficient d'amplification

> Pour l'étude du schéma BWENO, l'article [Banks J. W. et al (2019)](https://github.com/Kivvix/miMaS/blob/master/bibliography/pdf/2019-Banks.pdf) propose une linéarisation du schéma en prenant les poids $w^L$, $w^R$ égaux à différents couples de nombres positifs vérifiant que $w^L+w^R=1$, ainsi est tracé le coefficient d'amplification pour $w^L=1,\frac{3}{4},\frac{1}{2}$. On remarquera par la suite que l'étude du schéma pour les poids donnés par la méthode est compliqué, ce qui justifie cette approche.

```python
def bweno_expr ( wl=None , wr=None ) :
  """
    return expression of all BWENO scheme
    it could be linearized with optional special weight `wl` and `wr`. By default it is not linearized,
    if only `wl` is done then `wr = 1-wl`.
  """

  def beta ( k ) :
    """
      Indicator of smootheness of BWENO method
    """
    D14uj = sp.Rational(1,12)*( -f(k+2) +  8*f(k+1)             -  8*f(k-1) + f(k-2) )
    D24uj = sp.Rational(1,12)*( -f(k+2) + 16*f(k+1) - 30*f(k+0) + 16*f(k-1) - f(k-2) )
    D32uj =  sp.Rational(1,2)*(  f(k+2) -  2*f(k+1)             +  2*f(k-1) - f(k-2) )
    D42uj =                   (  f(k+2) -  4*f(k+1) +  6*f(k+0) -  4*f(k-1) + f(k-2) )

    return ( D14uj*( D14uj + D24uj + sp.Rational(1,3)*D32uj + sp.Rational(1,12)*D42uj )
           + D24uj*( sp.Rational(4,3)*D24uj + sp.Rational(5,4)*D32uj + sp.Rational(2,5)*D42uj )
           + D32uj*( sp.Rational(83,60)*D32uj + sp.Rational(23,18)*D42uj )
           + sp.Rational(437,315)*D42uj*D42uj )

  if wl==None :
    b5Rjm12 = beta(0)
    b5Ljm12 = beta(-1)

    a5Ljm12 = sp.Rational(1,2)/( epsilon + b5Ljm12 )
    a5Rjm12 = sp.Rational(1,2)/( epsilon + b5Rjm12 )

    wt5Ljm12 = a5Ljm12 / ( a5Ljm12 + a5Rjm12 )
    wt5Rjm12 = a5Rjm12 / ( a5Ljm12 + a5Rjm12 )

    w5Ljm12 = sp.Max(wt5Ljm12,wt5Rjm12)
    w5Rjm12 = sp.Min(wt5Ljm12,wt5Rjm12)
  else :
    w5Ljm12 = wl
    if wr == None:
      w5Rjm12 = 1-wl
    else:
      w5Rjm12 = wr

  u5Ljm12 = sp.Rational(1,60)*( -3*f(+1) + 27*f(+0) + 47*f(-1) - 13*f(-2) + 2*f(-3) )
  u5Rjm12 = sp.Rational(1,60)*(  2*f(+2) - 13*f(+1) + 47*f(+0) + 27*f(-1) - 3*f(-2) )

  u6Bjm12 = w5Ljm12*u5Ljm12 + w5Rjm12*u5Rjm12
  u6Bjp12 = u6Bjm12.subs(symbols_to(f_stencil, lambda j,_:f(j+1)),simultaneous=True)
  return u6Bjp12-u6Bjm12

bweno = Data()
bweno.func = sp.lambdify(phi,bweno_expr(wl=None).subs(symbols_to_vonNeumann(f_stencil)).subs(epsilon,10**(-40)),'numpy')
bweno.data = bweno.func(data.phi)

plt.plot(*reim(bweno.data),',',label="BWENO")
plt.plot(*reim(weno.data),":",label="WENO")
plt.plot(*reim(wenol.data_z),":",label="linearized WENO")
plt.legend(loc=(0.,1.02),ncol=3, mode="expand",borderaxespad=0.)
plt.show()
```

On remarque que la partie réel du coefficient d'amplification explose en comparaison aux méthodes WENO et WENO linéarisé.

```python
fig, (ax1, ax2) = plt.subplots(2,1)

bweno.reim = reim(bweno.data)
weno.reim = reim(weno.data)
wenol.reim = reim(wenol.data_z)

ax1.plot(data.phi,bweno.reim[0],label="BWENO");
ax2.plot(data.phi,bweno.reim[1],label="BWENO");
ax1.plot(data.phi,weno.reim[0],":",label="WENO");
ax2.plot(data.phi,weno.reim[1],":",label="WENO");
ax1.plot(data.phi,wenol.reim[0],":",label="linarized WENO");
ax2.plot(data.phi,wenol.reim[1],":",label="linarized WENO");

ax1.set_xlabel("$\\varphi$"); ax1.set_ylabel("$\\Re(W(\phi))$")
ax2.set_xlabel("$\\varphi$"); ax2.set_ylabel("$\\Im(W(\phi))$")
plt.legend(loc=(0.,2.25),ncol=3, mode="expand",borderaxespad=0.)
plt.show()
```

On peut supposer que le comportement étrange de la courbe de coefficient d'amplification du BWENO est dû à l'impossibilité d'écrire simplement le schéma comme $u^{n+1} = B(\phi)u^n$ comme pour le WENO (ce qui est relativement étrange, car de ce que j'ai pu en voir le schéma a finalement la même forme que le WENO classique , après ai-je besoin d'aller plus loin dans l'étude ? ce n'est pas la première fois que Nicolas me dit que ça ne sert à rien que je m'attarde sur ce schéma). Nous allons donc étudier le cas linéarisé avec les différents cas $w^L=1,\frac{3}{4},\frac{1}{2}$.

```python
wls = (1,sp.Rational(3,4),sp.Rational(1,2))

bweno.funcwl = [ sp.lambdify(phi,bweno_expr(wl).subs(symbols_to_vonNeumann(f_stencil)).subs(epsilon,10**(-8)),'numpy') for wl in wls ]
bweno.datawl = [ f(data.phi) for f in bweno.funcwl ]
bweno.reimwl = [ reim(dat) for dat in bweno.datawl ]

fig, ax = plt.subplots(nrows=len(wls),ncols=3,figsize=(14,14))

for i,wl in enumerate(wls) :
  ax[i][0].plot(*bweno.reimwl[i],label="BWENO")
  ax[i][0].plot(*reim(weno.data),':',label="WENO")
  ax[i][0].plot(*reim(wenol.data_z),':',label="linearized WENO")
  ax[i][0].set_ylabel("$w^L = {}$".format(wl))

  ax[i][1].plot(data.phi,bweno.reimwl[i][0],label="BWENO")
  ax[i][1].plot(data.phi,reim(weno.data)[0],':',label="WENO")
  ax[i][1].plot(data.phi,reim(wenol.data_z)[0],':',label="linearized WENO")

  ax[i][2].plot(data.phi,bweno.reimwl[i][1],label="BWENO")
  ax[i][2].plot(data.phi,reim(weno.data)[1],':',label="WENO")
  ax[i][2].plot(data.phi,reim(wenol.data_z)[1],':',label="linearized WENO")

ax[0][1].title.set_text("$\\Re(W(\phi))$")
ax[0][2].title.set_text("$\\Im(W(\phi))$")
handles, labels = fig.gca().get_legend_handles_labels()
fig.legend(handles,labels,loc=(0.,0.04),ncol=len(wls),mode="expand",borderaxespad=0.)
plt.show()
```

En linéarisant les poids ($w^L=1,\frac{3}{4},\frac{1}{2}$), on remarque un comportement similaire à celui du schéma WENO linéarisé (qui coïncide parfaitement pour $w^L=1$, $w^R=0$), avec un comportement dégénéré dans le cas centré $w^L = w^R = \frac{1}{2}$ (transport pur sans diffusion). La partie imaginaire, même dans le cas non linéarisé, la méthode BWENO reproduit le même comportement que la méthode WENO linéarisée.

## 3.2 Tests numériques

### 3.2.1 Obtention de l'ordre

On peut vérifier l'ordre du schéma. Pour cela on calcule l'erreur sur une advection à vitesse 1 :

$$
  \begin{cases}
    u_t + u_x = 0 \\
    u^0(x) = u(t=0,x) = \cos(2x)
  \end{cases}
$$

jusqu'au temps final $T_f = \pi$ (le mode 2 de la condition initiale permet d'avoir $u(t=T_f,x)=u^0(x)$, avec $m=10,20,\dots,200$ points, $\Delta x = \frac{2\pi}{m}$, on prendra $\Delta t = 0.1\frac{\Delta x}{200}$. La discrétisation en temps s'effectue avec RK SPP(3,3).

Il serait envisageable de prendre un $\Delta t$ plus grand, mais cela permet de minimiser l'erreur en temps pour ne considérer que l'erreur en espace du schéma.

[Banks J. W. et al (2019)](https://github.com/Kivvix/miMaS/blob/master/bibliography/pdf/2019-Banks.pdf) annonce un ordre 6, celui-ci est sans doute plus visible avec la norme $L^{\infty}$ qu'avec la norme $L^1$, la norme $L^2$ n'a pas été calculé dans ce cas.

### 3.2.2 Comportement face à une discontinuité

Nous effectuons ici les mêmes cas tests qu'avec le schéma WENO linéarisé. Il s'agit essentiellement d'étudier son comportement face à une discontinuité. Comme précédemment, nous conservons qu'une simple équation de transport linéaire à vitesse 1. La condition initiale est donnée par :

$$
  u(t=0,x) = \begin{cases}
    x \quad &\text{si $x \in [0,1]$}\\
    1 \quad &\text{si $x \in [1,4]$}\\
    0 \quad &\text{sinon}
  \end{cases}
$$

La simulation s'effectuant jusqu'au temps $T_f = 2\pi$ sur le domaine spatial périodique $[0,2\pi]$ avec $N_x = 100$ le nombre de points, et une condition CFL $\Delta t = 0.1\Delta x$. La solution exacte coïncide avec la condition initiale. La discrétisation en temps est un RK SSP(3,3).

On remarque que le schéma BWENO oscille à proximité d'une discontinuité, avec une norme $L^{\infty}$ légèrement supérieure à la méthode WENO linéarisée.

> Joackim m'a fait remarquer que la méthode WENO semblait de prime abord respecter le principe du *max*. Effectivement sur tous les différents tests d'advection, de rotation de gaussienne, et finalement même les cas de Vlasov-Poisson semble confirmer empiriquement que le schéma WENO respecte le principe du *max* (à la différence du WENO linéarisé et de BWENO). Si une telle chose devait être démontrée, il semble devoir majorer des fonctions rationnelles, c'est qui n'est pas une chose triviale.

### 3.2.3 Test sur une fonction chapeau

En étudiant le comportement en temps long, étudions le comportement du schéma sur une fonction avec de fortes variations de gradient : une fonction chapeau :

$$
  u(t=0,x) = \begin{cases}
    0    \quad & \text{si $x \in [0,1]$} \\
    x-1  \quad & \text{si $x \in [1,2]$} \\
    -x+3 \quad & \text{si $x \in [2,3]$} \\
    0    \quad &\text{sinon}
  \end{cases}
$$

La simulation s’effectue jusqu'au temps $T_f = 300\pi$ sur le domaine spatial périodique $[0,2\pi]$ avec $N_x = 100$, et une condition CFL $\Delta t = \Delta x$. La discrétisation en temps est un RK SSP(3,3). Vu la viscosité des schémas, on ne s'attend pas à une erreur faible, surtout avec un pas de temps aussi grossier ; l'idée ici est plus d'illustrer certains comportement, en particulier le principe du *max*.

On voit une certaine violation du principe du *max* avec WENO dans le sens où le *min* de la fonction n'est pas préservé, ou sa positivité. Le schéma BWENO réagit de manière similaire au schéma WENO linéarisé.

La même simulation mais en temps *court* $T_f = 2\pi$, on constate un *shift* important de WENO par rapport à ses équivalent linéarisé. et une mauvaise capture de la forte variation de gradient.

# 4. Schémas RK

Les schémas Runge-Kutta sont souvent utilisés dans le domaine des EDP pour résoudre la discrétisation en temps, conforme à cette tradition c'est ce que nous allons effectuer. Nous cherchons quel est l'ordre minimale $N$ pour stabiliser le couple RK$N$-WENO5, ou celui qui permettra d'obtenir la plus grande CFL avec le minimum de coût numérique.

On s'intéresse ici au polynôme caractéristique d'un schéma RK. On sait que le polynôme caractéristique d'un schéma RK$N$ est une troncature de la série entière de l'exponentielle (auquel peut s'ajouter des termes de degré plus élevé si le nombre d'étage $s$ est supérieur à l'ordre de la méthode $N$), donc de la forme :

$$
  p_N(z) = \sum_{n=0}^N \frac{z^n}{n!}
$$

Il est ensuite nécessaire de tracer la courbe d'équation $|p_N(z)| = 1$, pour cela Miguel m'a proposé de résoudre dans un premier temps l'équation :

$$
  p_N(z) = e^{i\theta}
$$

à $\theta$ fixé, on a ainsi $N$ solutions $(b_0(\theta),\dots,b_N(\theta))$. Puis ensuite faire varier $\theta \in [0,2\pi[$. On obtient ainsi $N$ courbes paramétriques que l'on peut tracer sans trop de problèmes.

```python
z = sp.symbols("z",complex=True)
theta = sp.symbols("\\theta",real=True)

import math

def poly_carac(N):
  """
    return trunc developpement of exponential function to the order `N`
  """
  return sum([ sp.Rational(1,(math.factorial(n)))*z**n for n in range(N+1) ])

def b(N):
  """
    return a list of points where modulus of caracteristic polynomial is equal to 1
    this function compute this set of points with a polynomial solver in C, so it
    doesn't work for `N > 4`.
  """
  p = poly_carac(N)
  bset = sp.solveset(sp.Eq(p,sp.exp(I*theta)),z)

  bcurve = []
  for b in [ sp.lambdify(theta,b,'numpy') for b in bset ] :
    tmp = b(data.phi)
    bcurve.extend(tmp.tolist())
  return np.array(sorted(bcurve,key=lambda z:np.angle(z+1)))

b1 = b(1)
b2 = b(2)
b3 = b(3)
b4 = b(4)

save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [10,8]
plt.plot(*reim(b1),label="RK1")
plt.plot(*reim(b2),label="RK2")
plt.plot(*reim(b3),label="RK3")
plt.plot(*reim(b4),label="RK4")
plt.axis('equal')
plt.legend(loc=(0.,1.02),ncol=4,mode="expand",borderaxespad=0.)
plt.show()
plt.rcParams['figure.figsize'] = save_figsize
```

On retrouve bien les mêmes profiles que dans la littératures. L'obtention de la courbe nécessite l'obtention d'un polynôme de degré $N$, cette méthode n'est donc plus envisageable sans évaluation numérique aux ordres supérieurs.

## 4.1 Petit test en 3D

Il est relativement simple d'évaluer pour une série de point d'une grille la valeur de $|p_N(z_{j,k})|$ avec $z$ définit par :

$$
  z_{j,k} = j\Delta x + ik\Delta y
$$

Vu la facilité de la tâche il est relativement simple de répéter l'opération pour différents polynômes caractéristiques $p_N$. Le travail a été effectué pour $N=2,\dots,7$ (sans la moindre analyse des résultats, et avec une grille sans doute très mauvaise ainsi qu'un domaine qui ne permet pas de voir tout le domaine de RK7). On trace ainsi facilement :

$$
  \{z\in\mathbb{C},|p_N(z)| \leq 1\}
$$

Pour $N=3$, le cas qui nous intéresse, on obtient ainsi :

Nous évaluons ici le polynôme en tous les points de la grille, la méthode est la seule pour déterminer la frontière $|p_N(z)| = 1$, par conséquent l'étude de CFL à des degrés supérieurs s'effectue nécessairement numériquement.

## 4.2 Exploration pour les ordres supérieurs

Je n'arrive pas à trouver avec `sympy` par la même méthode RK5 (problème d'absence de formule analytique pour les racines pour les polynôme de degré supérieur ou égal à 5). Pour obtenir l'ensemble :

$$
  b_5 = \{ z\in\mathbb{C},|p_5(z)| = 1 \}
$$

je dois explorer une autre méthode.

```python
p5 = poly_carac(5)
b5set = sp.solveset(p5-sp.exp(I*theta),z)
b5set
```

La seule méthode généralisable est l'évaluation numérique du polynôme sur une grille. Une étude un peu plus approfondie permettrait de se donner une idée des points de la grille à évaluer (le domaine de stabilité d'une méthode RK$N+1$ contient approximativement celui de RK$N$ par exemple, le domaine s'étend beaucoup moins du côté de la partie réelle positive).

Je ne sais pas exactement comment fonctionne la fonction `plt.contour` qui permet effectivement de faire cela, mais elle réussit avec une grille relativement grossière à obtenir un contour assez précis, en un temps relativement court. Une tentative d'écriture d'une fonction similaire en évaluant la fonction en tous les points d'un maillage fut un échec aussi bien au niveau de la précision que du temps de calcul.

```python
import matplotlib.colors as mcolors # for mcolors.TABLEAU_COLORS
palette = list(mcolors.TABLEAU_COLORS)

def contour ( expr , z , Ir , Ii , **kwargs):
  """
    draw a contour of a C expression `expr` on `z` parameter on `Ir`x`Ii` domain
  """
  x,y = sp.symbols("x y",real=True)
  f = sp.lambdify((x, y), expr.subs(z,x+I*y),'numpy')

  a = np.linspace(*Ir)
  b = np.linspace(*Ii)
  X, Y = np.meshgrid(a, b)
  try:
    lab = kwargs.pop("label")
  except:
    lab = ""
  try:
    c = kwargs["colors"]
  except:
    c = palette[0]
  if len(lab) > 0 :
    plt.plot([0],[0],label=lab,color=c)
  return plt.contour(X,Y, f(X,Y).real,**kwargs)
```

Ce qui fut découvert relativement tard est la possibilité à partir de cette fonction de récupérer les points tracés à l'aide de l'attribut `allsegs` de la classe `matplotlib.contour.QuadContourSet`

```python
R = sp.Abs(poly_carac(6))

save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [7,7]

dat0 = contour(R,z,(-6.,3.,500),(-6.,6.,500), levels=[1.]).allsegs

plt.suptitle("$"+sp.latex(R)+"$",fontsize=18)
plt.plot(dat0[0][1][:,0],dat0[0][1][:,1])
plt.plot(dat0[0][0][:,0],dat0[0][0][:,1])
plt.plot(dat0[0][2][:,0],dat0[0][2][:,1])
plt.show()

plt.rcParams['figure.figsize'] = save_figsize

# comparaison du nombre de point dans la frontière de chaque sous domaine
print(len(dat0[0][0][:]),len(dat0[0][1][:]),len(dat0[0][2][:]))
```

Pour un domaine non connexe (comme c'est le cas pour RK6) on obtient une liste de tableaux de points (en coordonnées cartésiennes). Il est nécessaire d'identifier le tableau le plus long pour obtenir *a priori* le domaine central.

On peut ainsi investiguer les ordres supérieurs.

```python
save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [10,10]

for n in range(1,12):
  print(n,end="\r")
  R = sp.Abs(poly_carac(n))
  contour(R,z,(-7,6,1000),(-6.5,6.5,1000), levels=[1.],colors=palette[n%len(palette)],label="RK{}".format(n))
print(" "*len(str(n)))
plt.legend(loc=(0.,1.02),ncol=6,mode="expand",borderaxespad=0.)
plt.show()

plt.rcParams['figure.figsize'] = save_figsize
```

> Joackim m'a fait remarquer que les *bras* contiennent une infinité de *bulles* de plus en plus petites (on voit ceci pour la petite *bulle* orange à l'extrémité du *bras* qui apparait seulement lorsque on raffine le domaine). On peut même conjecturer que la forme des bras sont des paraboles. On voit la naissance de la deuxième paire de *bras* avec les grosses *bulles* oranges.
>
> L'étude de ces non-connexités des domaines de stabilités ne semblent pas présenter d'intérêt pour ce que l'on fait. De plus ces domaines ne correspondent pas à la stabilité d'une méthode connue. En effet il n'y a pas de méthode Runge-Kutta d'ordre $n$ à $n$ étages connues pour $n\geq 5$.

L'étude des domaines de stabilité pour des méthodes de Runge-Kutta d'ordre 1 à 4 reste plus simple (et suffit pour ce que l'on souhaite faire), par conséquent l'étude des ordres plus élevés ne sera pas au cœur de la suite et servira plus de test pour valider l'automatisation d'un calcul.

## 4.3 Polynôme caractéristique pour d'autres méthodes

### 4.3.1 Obtention du polynôme caractéristique depuis un schéma numérique

Le calcul effectué ne s'applique que pour un RK$N$ à $N$ étages. Considérons maintenant un RK$N$ à $s$ étages ($s>N$). Il est nécessaire de calculer son polynôme caractéristique.

> Il s'agit d'un travail préliminaire pour l'obtention de polynôme caractéristique d'un IFRK (*integrating factor Runge-Kutta method*) qui est une famille de schémas introduisant une exponentielle pour résoudre une équation du type : $$
        \frac{\mathrm{d}u}{\mathrm{d}t} = Lu + N(u)
    $$

Pour obtenir un polynôme caractéristique il est nécessaire de linéariser le problème en passant en complexe. Ainsi pour un problème du type :

$$
  \frac{\mathrm{d}u}{\mathrm{d}t} = L(u)
$$

on effectue la substitution :

* $L(u) \rightarrow \lambda u$ (on linéarise $L$ sur le plan complexe à l'aide d'une variable $\lambda$, dans notre cas $\lambda$ représente le coefficient d'amplification de notre méthode WENO)
* $\lambda \Delta t \rightarrow z$ (on veut étudier le coefficient d'amplification donné par le produit $\lambda \Delta t$)

Nous étudions ainsi un schéma RK3 à 4 étages :

$$
  \begin{aligned}
    u^{(1)} &= u^n + \frac{1}{2}\Delta t L(u^n,t^n) \\
    u^{(2)} &= u^{(1)} + \frac{1}{2}\Delta t L(u^{(1)},t^n+\frac{1}{2}\Delta t) \\
    u^{(3)} &= \frac{2}{3}u^n + \frac{1}{3}u^{(2)} + \frac{1}{6}\Delta t L(u^{(2)},t^n+\Delta t) \\
    u^{n+1} &= u^{(3)} + \frac{1}{2}\Delta t L(u^{(3)},t^n+\frac{1}{2}\Delta t)
  \end{aligned}
$$

> Il est à noter qu'une méthode à $s$ étages donne un polynôme de degré $s$, par conséquent il n'est pas envisageable dans l'état actuel d'étudier des schémas en temps de degré supérieur à 4 pour les raisons expliqués sur la résolution de polynôme de degré 5.

```python
un = sp.symbols("u_n")
dt = sp.symbols("\\Delta\\ t",real=True)
lamb = sp.symbols("\\lambda",complex=True)
z = sp.symbols("z",complex=True)
L = sp.Function("L")

def name_stages ( v_s , n ):
  vs = [ sp.symbols(v_s+"^n") ]
  vs.extend([ sp.symbols(v_s+"^{(%i)}"%j) for j in range(1,n) ])
  vs.append(sp.symbols(v_s+"^{n+1}"))
  return vs

class RK(object):
  ui = name_stages("u",42)
  def __init__(self,s,name=""):
    self.s = s
    self.stages = [0 for x in range(s)]
    self.ui = RK.ui[:s]
    self.ui.append(RK.ui[-1])
    self.name = name

  def __getitem__(self,s):
    return self.stages[s]
  def __setitem__(self,key,value):
    self.stages[key]=value

  def scheme(self):
    [ display(sp.Eq(self.ui[s+1],stage)) for s,stage in enumerate(self.stages) ]

  def poly(self):
    expr = self.stages[-1]
    for i,us in enumerate(reversed(self.stages[:-1])) :
      expr = expr.subs(self.ui[self.s-1-i],us)
    return expr.replace(L,lambda x:lamb*x).subs(lamb*dt,z).expand().subs(self.ui[0],1).collect(z)

  def sub_poly(self,s):
    expr = self.stages[s]
    for i,us in enumerate(reversed(self.stages[:-1])) :
      expr = expr.subs(self.ui[self.s-1-i],us)
    return expr.replace(L,lambda x:lamb*x).subs(lamb*dt,z).expand().subs(self.ui[0],1).collect(z)

  def sub_polys(self):
    return [ self.sub_poly(i) for i in range(self.s) ]

un,u1,u2,u3,u4,u5,u6,u7 = RK.ui[0:8]
```

```python
"""
Test de la procédure sur différents RK
RK3 Shu-Osher
"""
rk3so = RK(3,name="RK3 Shu-Osher")
rk3so[0] = un + dt*L(un)
rk3so[1] = sp.Rational(3,4)*un + sp.Rational(1,4)*u1 + sp.Rational(1,4)*dt*L(u1)
rk3so[2] = sp.Rational(1,3)*un + sp.Rational(2,3)*u2 + sp.Rational(2,3)*dt*L(u2)

rk3so.poly()
```

On retrouve bien le polynôme caractéristique de SPPRK(3,3).

```python
""" RK3 à 4 étages """
rk43 = RK(4,name="RK(4,3)")
rk43[0] = un + sp.Rational(1,2)*dt*L(un)
rk43[1] = u1 + sp.Rational(1,2)*dt*L(u1)
rk43[2] = sp.Rational(2,3)*un + sp.Rational(1,3)*u2 + sp.Rational(1,6)*dt*L(u2)
rk43[3] = u3 + sp.Rational(1,2)*dt*L(u3)

rk43.poly()
```

On peut aussi (juste pour l'illustration) effectuer le même travail sur un schéma RK6 à 7 étages, obtenu dans [Luther H. A. (1968)](https://doi.org/10.2307/2004675).

```python
""" RK6 à 7 étages [Luther H. A. (1968)] """
nu = sp.symbols("\\nu")
s21 = sp.sqrt(21)

rk76 = RK(8,name="RK(7,6)")
rk76.ui = name_stages("k",8)
rk76.ui[0] = RK.ui[0]
rk76.ui[-1] = RK.ui[-1]
k1,k2,k3,k4,k5,k6,k7 = rk76.ui[1:-1]

rk76[0] = dt*L(un)
rk76[1] = dt*L(un+nu*k1)
rk76[2] = dt*L(un+ ((4*nu-1)*k1+k2)/(8*nu) )
rk76[3] = dt*L(un+ ((10*nu-2)*k1 + 2*k2 + 8*nu*k3)/(27*nu) )
rk76[4] = dt*L(un+ (-((77*nu-56)+(17*nu-8)*s21)*k1
                -8*(7+s21)*k2 + 48*(7+s21)*nu*k3
                -3*(21+s21)*nu*k4)/(392*nu) )
rk76[5] = dt*L(un+ (-5*((287*nu-56)-(59*nu-8)*s21)*k1
                - 40*(7-s21)*k2 + 320*s21*nu*k3 + 3*(21-121*s21)*nu*k4
                + 392*(6-s21)*nu*k5)/(1960*nu) )
rk76[6] = dt*L(un+ ( 15*((30*nu-8)-(7*nu*s21))*k1 + 120*k2
                - 40*(5+7*s21)*nu*k3 + 63*(2+3*s21)*nu*k4
                - 14*(49-9*s21)*nu*k5 + 70*(7+s21)*nu*k6)/(180*nu) )
rk76[7] = un + (9*k1 + 64*k3 + 49*k5 + 49*k6 + 9*k7)/180

prk6_7 = rk76.poly()

# domaine de stabilité de cette méthode
save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [6,6]

contour(sp.Abs(poly_carac(3)),z,(-6.,2.,500),(-4.,4.,500), levels=[1.],colors=palette[0],label="RK SSP(3,3)")
contour(sp.Abs(prk6_7),z,(-6.,2.,500),(-4.,4.,500), levels=[1.],colors=palette[1],label="RK (7,6)")
contour(sp.Abs(poly_carac(6)),z,(-6.,2.,500),(-4.,4.,500), levels=[1.],colors=palette[2],label="RK 6")

plt.legend(loc=(0.,1.02),ncol=3,mode="expand",borderaxespad=0.)
plt.show()

plt.rcParams['figure.figsize'] = save_figsize

(prk6_7,poly_carac(7))
```

Il est maintenant intéressant d'étudier le domaine de stabilité de ce schéma RK3 à 4 étages.

```python
p = rk43.poly()

def b_poly(p):
  """
    same as `b` function but from a caracteristic polynom which is not a truncature of exponential
  """
  bset = sp.solveset(sp.Eq(p,sp.exp(I*theta)),z)
  bcurve = []
  for (i,b) in enumerate([ sp.lambdify(theta,b,'numpy') for b in bset ]) :
    bcurve.extend(b(data.phi))
  return np.array(sorted(bcurve,key=lambda z:np.angle(z+1)))

b34 = b_poly(p)
b3  = b_poly(poly_carac(3)) # juste pour exemple pour montrer que l'équivalence des fonctions

save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [10,8]
plt.plot(*reim(b34),label="ERK SSP(4,3)")
plt.plot(*reim(b3),label="ERK SSP(3,3)")
plt.plot(*reim(b4),label="ERK SSP(4,4)")
plt.legend(loc=(0.,1.02),ncol=3,mode="expand",borderaxespad=0.)
plt.plot(*reim(-1.923*weno.data),label="WENO") # WENO maximisé par la CFL que l'on calculera après
plt.axis('equal')
plt.show()
plt.rcParams['figure.figsize'] = save_figsize
```

### 4.3.2 Obtention du polynôme caractéristique depuis le tableau de Butcher

Parce qu'il est moins facile d'écrire un tableau de Butcher qu'un schéma numérique, je m'étais contenté d'écrire une méthode d'obtention du polynôme caractéristique à partir du schéma et non du tableau de Butcher.

Pour le moment la fonction que je propose se limite aux cas d'ERK (*Explicit Runge-Kuta method*). Je n'ai pas encore réfléchi à l'obtention du polynôme caractéristique dans le cas par exemple d'une méthode DIRK (*Diagonal Implicit Runger-Kutta method*).

La fonction `poly_butcher` fonctionne pour un tableau de Butcher écrit comme suit :

$$
  \begin{array}{c|c}
    \begin{matrix}
      a_{11} & \cdots & a_{1s} \\
      \vdots & \ddots & \vdots \\
      a_{s1} & \cdots & a_{ss}
    \end{matrix} & \begin{matrix} b_1 \\ \vdots \\ b_s \end{matrix} \\ \hline
    \begin{matrix}c_1 & \cdots & c_s \end{matrix} & \\
  \end{array}
$$

Le schéma qui en résulte est de la forme :

$$
  \begin{aligned}
    u^{(i)} &= u^n + \Delta t \sum_j a_{ij} L(t^n+b_j\Delta t , u^{(j)}) \\
    u^{n+1} &= u^n + \Delta t \sum_i c_i L(t^n+b_i\Delta t , u^{(i)})
  \end{aligned}
$$

Ce schéma est explicite si et seulement si la matrice $A$ est triangulaire strictement inférieure. On ne précisera pas ici les propriétés liant $A$, $b$ et $c$ pour déterminer l'ordre de la méthode, son caractère SSP ou autre.

Dans l'obtention du polynôme caractéristique du schéma, le vecteur $b$ n'est pas nécessaire puisqu'on linéarise l'opération $L(t^n+b_j\Delta t , u^{(j)})$ par $\lambda u^{(j)}$.

```python
def poly_butcher( A , c , b=None ):
  """
    /!\ fonction ne fonctionnant que pour des schémas explicites
    pour des méthodes DIRK, revoir cette fonction en créeant un tableau
    de symbols u_s et en cherchant comment expliciter la diagonale implicite
  """
  r = range(len(c))
  u_s = [ 0 for i in r ]
  for i in r:
    u_s[i] = 1 + dt*sum([ A[i][j]*lamb*u_s[j] for j in r ])
  un1 = 1 + dt*sum([ c[j]*lamb*u_s[j] for j in r ])
  return un1.expand().collect(z).subs(dt*lamb,z).collect(z)

class Butcher(object):
  def __init__ (self,A,c,b=None,name=None):
    self.A = A
    self.b = b
    self.c = c
    self.name = name
    if b == None:
      self.b = [ sum(A[i][:]) for i in range(len(c)) ]

  def stages(self):
    return len(self.c)

  def poly(self):
    return poly_butcher(A=self.A,b=self.b,c=self.c)
```

```python
""" vérification sur RK3 """
A = [[0,0,0],
     [1,0,0],
     [sp.Rational(1,4),sp.Rational(1,4),0]]
b = [0,1,sp.Rational(1,2)]
c = [sp.Rational(1,6),sp.Rational(1,6),sp.Rational(2,3)]

rk3 = Butcher(A=A,b=b,c=c,name="RK SSP (3,3)")
rk3.poly()
```

```python
""" vérification sur RK4 """
A = [[ 0               , 0               , 0 , 0 ] ,
     [ sp.Rational(1,2), 0               , 0 , 0 ] ,
     [ 0               , sp.Rational(1,2), 0 , 0 ] ,
     [ 0               , 0               , 1 , 0 ]]
b = [0,sp.Rational(1,2),sp.Rational(1,2),1]
c = [sp.Rational(1,6),sp.Rational(1,3),sp.Rational(1,3),sp.Rational(1,6)]

rk4 = Butcher(A=A,b=b,c=c,name="RK (4,4)")
rk4.poly()
```

```python
""" test sur RK4 3/8-rule """
A = [[ 0               , 0 , 0 , 0 ],
     [ sp.Rational(1,3), 0 , 0 , 0 ],
     [-sp.Rational(1,3), 1 , 0 , 0 ],
     [ 1               ,-1 , 1 , 0 ]]
b = [0,sp.Rational(1,3),sp.Rational(2,3),1]
c = [sp.Rational(1,8),sp.Rational(3,8),sp.Rational(3,8),sp.Rational(1,8)]

rk4_38 = Butcher(A=A,b=b,c=c,name="RK (4,4) 3/8-rule")
rk4_38.poly()
```

Le schéma RK4 $\frac{3}{8}$-rule est un schéma d'ordre 4 à 4 étages, son polynôme caractéristique est donc le même. Le domaine de stabilité ne dépendant que de ce dernier, la stabilité absolue de la méthode reste inchangée. La méthode trouve son intérêt dans son schéma : pas de temps de chaque étage, coefficients plus équilibré entre les approximations successives, etc.

```python
""" test sur RK NSSP(3,3) [Wang R. and Spiteri R. J. (2007)] """
A = [[0,0,0],
     [-sp.Rational(4,9),0,0],
     [sp.Rational(7,6),-sp.Rational(1,2),0]]
b = [0,-sp.Rational(4,9),sp.Rational(2,3)]
c = [sp.Rational(1,4),0,sp.Rational(3,4)]

rknssp3 = Butcher(A=A,b=b,c=c,name="RK NSSP(3,3)")
rknssp3.poly()
```

On peut effectuer une remarque similaire au RK4 $\frac{3}{8}$-rule, le schéma RK NSSP(3,3) reste avant tout un schéma d'ordre 3 à 3 étapes, il est donc normal que son polynôme caractérsitique soit le même que le RK3 classique à 3 étages. Ce jeu de coefficients permet d'obtenir de nouvelles propriétés (ici plutôt l'absence du caractère SSP).

Les schémas suivants à l'inverse, ajoute des étages pour, entre autre, améliorer le domaine de stabilité.

```python
""" test sur RK NSSP(5,3) [Wang R. and Spiteri R. J. (2007)] """
A = [[ 0 , 0 , 0 , 0 , 0 ]                 ,
     [ sp.Rational(1,7)  , 0 , 0 , 0 , 0 ] ,
     [ 0 , sp.Rational(3,16) , 0 , 0 , 0 ] ,
     [ 0 , 0 , sp.Rational(1,3)  , 0 , 0 ] ,
     [ 0 , 0 , 0 , sp.Rational(2,3)  , 0 ]]
b = [0,sp.Rational(1,7),sp.Rational(3,16),sp.Rational(1,3),sp.Rational(2,3)]
c = [sp.Rational(1,4),0,0,0,sp.Rational(3,4)]

rk53 = Butcher(A=A,b=b,c=c,name="RK NSSP(5,3)")
rk53.poly()
```

```python
""" test sur RK SSP(3,2) [Wang R. and Spiteri R. J. (2007)] """
A = [[ 0                , 0                , 0 ],
     [ sp.Rational(1,2) , 0                , 0 ],
     [ sp.Rational(1,2) , sp.Rational(1,2) , 0 ]]
b =  [ 0 , sp.Rational(1,2) , 1 ]
c =  [ sp.Rational(1,3) , sp.Rational(1,3) , sp.Rational(1,3) ]
rk32 = Butcher(A=A,b=b,c=c,name="RK SSP(3,2)")
rk32.poly()
```

```python
""" test sur DP 5 (Dormand-Prince) [Motamed M. and Macdonald C. and Ruuth S. (2010)] """
A = [[ 0                       , 0                       , 0                       , 0                    , 0                       , 0                  , 0 ],
     [ sp.Rational(1,5)        , 0                       , 0                       , 0                    , 0                       , 0                  , 0 ],
     [ sp.Rational(3,40)       , sp.Rational(9,40)       , 0                       , 0                    , 0                       , 0                  , 0 ],
     [ sp.Rational(44,45)      ,-sp.Rational(56,15)      , sp.Rational(32,9)       , 0                    , 0                       , 0                  , 0 ],
     [ sp.Rational(19372,6561) ,-sp.Rational(25360,2187) , sp.Rational(64448,6561) ,-sp.Rational(212,729) , 0                       , 0                  , 0 ],
     [ sp.Rational(9017,3168)  ,-sp.Rational(355,33)     , sp.Rational(46732,5247) , sp.Rational(49,176)  ,-sp.Rational(5103,18656) , 0                  , 0 ],
     [ sp.Rational(35,384)     , 0                       , sp.Rational(500,1113)   , sp.Rational(125,192) ,-sp.Rational(2187,6784)  , sp.Rational(11,84) , 0 ]]
c1 = [ sp.Rational(35,384)     , 0                       , sp.Rational(500,1113)   , sp.Rational(125,192) ,-sp.Rational(2187,6784)  , sp.Rational(11,84) , 0 ]
c2 = [ sp.Rational(5179,57600) , 0                       , sp.Rational(7571,16695) , sp.Rational(393,640) ,-sp.Rational(92097,339200) , sp.Rational(187,2100) , sp.Rational(1,40) ]
b = [0,sp.Rational(1,5),sp.Rational(3,10),sp.Rational(4,5),sp.Rational(8,9),1,1]
dp5 = Butcher(A=A,b=b,c=c1,name="DP 5")
dp5.poly()
```

```python
""" test sur RK (8,6) [Banks J. W. et all (2019)] """
A = [[  0 ,  0 , 0 , 0 , 0 , 0 , 0 , 0 ] ,
     [  sp.Rational(1,9)             ,  0                         ,  0                          ,  0                           ,  0                      , 0                     , 0                  , 0 ] ,
     [  sp.Rational(1,24)            ,  sp.Rational(1,8)          ,  0                          ,  0                           ,  0                      , 0                     , 0                  , 0 ] ,
     [  sp.Rational(1,6)             , -sp.Rational(1,2)          ,  sp.Rational(2,3)           ,  0                           ,  0                      , 0                     , 0                  , 0 ] ,
     [  sp.Rational(935,2536)        , -sp.Rational(2781,2536)    ,  sp.Rational(309,317)       ,  sp.Rational(321,1268)       ,  0                      , 0                     , 0                  , 0 ] ,
     [ -sp.Rational(12710,951)       ,  sp.Rational(8287,317)     , -sp.Rational(40,317)        , -sp.Rational(6335,317)       ,  8                      , 0                     , 0                  , 0 ] ,
     [  sp.Rational(5840285,3104064) , -sp.Rational(7019,2536)    , -sp.Rational(52213,86224)   ,  sp.Rational(1278709,517344) , -sp.Rational(433,2448)  , sp.Rational(33,1088)  , 0                  , 0 ] ,
     [ -sp.Rational(5101675,1767592) ,  sp.Rational(112077,25994) ,  sp.Rational(334875,441898) , -sp.Rational(973617,883796)  , -sp.Rational(1421,1394) , sp.Rational(333,5576) , sp.Rational(36,41) , 0 ]]

c =  [  sp.Rational(41,840)          ,  0                         ,  sp.Rational(9,35)          ,  sp.Rational(9,280)          ,  sp.Rational(34,105)    , sp.Rational(9,280)    , sp.Rational(9,35)  , sp.Rational(41,840) ]
b =  [ 0 , sp.Rational(1,9) , sp.Rational(1,6) , sp.Rational(1,3) , sp.Rational(1,2) , sp.Rational(2,3) , sp.Rational(5,6) , 1 ]

rk86 = Butcher(A=A,b=b,c=c,name="RK (8,6)")
( rk86.poly() , poly_carac(8) )
```

```python
save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [6,6]

for i,rk in enumerate([rk3,rk53,rk86,dp5,rk32]) :
  contour(sp.Abs(rk.poly()),z,(-6.,3.,500),(-6.,6.,500),levels=[1.],colors=palette[i],label=rk.name)
plt.legend(loc=(0.,1.02),ncol=3,mode="expand",borderaxespad=0.)
plt.show()

plt.rcParams['figure.figsize'] = save_figsize
```

On peut aussi souhaiter effectuer l'inverse, à partir d'un polynôme caractéristique obtenu en maximisant un certain coefficient sur un RK3 à 4 étages, chercher à obtenir le schéma ou le tableau de Butcher associé. Pour cela on calcule le polynôme caractéristique d'une matrice $A=(a_{ij})_{ij}$ et un vecteur $c = (c_j)_j$, et on cherche à obtenir les relations d'ordre.

```python
s=3
rk_coef = Butcher(A=np.array(sp.symbols('a_:%i:%i'%(s,s))).reshape((s,s)),
                  b=sp.symbols('b:%i'%s),
                  c=sp.symbols('c:%i'%s))

pb = sp.Poly(rk_coef.poly(),z)
pc = sp.Poly(poly_carac(s),z)

# égalités entre le polynôme caractéristique et les coefficients de Butcher
display((pb.as_expr(),pc.as_expr()))
[sp.Eq(cb,cc) for (cb,cc) in zip( pb.coeffs() , pc.coeffs() )]
```

Maintenant il faut savoir quoi faire de ces relations d'ordre. On peut par exemple contraindre la forme de la matrice $A$ (par exemple diagonale inférieure, comme dans RK NSSP(5,3)), la positivité de certains coefficients, d'autres relations pour assurer un caractère SSP ou non, etc.

> Il est normal que le résultat donné ici ne donne qu'une relation faisant intervenir $a_{10}$, $a_{20}$ et $a_{21}$ puisque pour le moment la fonction `poly_butcher` ne fonctionne qu'avec une méthode RK explicite. L'étude de DIRK2 par exemple nécessiterait une amélioration de la fonction.

On peut vouloir aussi écrire le schéma issu d'un tableau de Butcher. Par défaut le schéma obtenu directement à l'aide d'un tableau de Butcher n'est pas optimal d'un point de vue numérique dans le sens où il sous entend plus que $s$ (le nombre d'étages de la méthode) évaluations de la fonction $L$. Une amélioration du schéma est possible à l'aide d'un jeu de substituion des étapes précédentes.

```python
#L = sp.Function("L")
tn = sp.symbols("t^n",real=True)

def butcher_scheme ( A , b , c , shu_osher=False ):
  us = name_stages("u",len(c))
  scheme_stages = []
  r = range(len(c))
  u_s = [ 0 for i in r ]
  for i in r:
    u_si = us[0] + dt*sum([ A[i][j]*L(tn+b[j]*dt,us[j]) for j in r ])
    scheme_stages.append(sp.Eq(us[i],u_si))
  un1 = us[0] + dt*sum([ c[i]*L(tn+b[i]*dt,us[i]) for i in r ])
  scheme_stages.append(sp.Eq(us[-1],un1))

  scheme_stages = scheme_stages[1:]
  if shu_osher :
    L_subs = []
    for i,eq in enumerate(scheme_stages[:-1]):
      scheme_stages[i] = eq.subs(L_subs).simplify().expand()
      L_subs.append(( L(tn+b[i]*dt,us[i]) , sp.solve(scheme_stages[i],L(tn+b[i]*dt,us[i]))[0] ))
    scheme_stages[-1] = scheme_stages[-1].subs(L_subs).expand()

  return scheme_stages

Butcher.scheme = lambda self,shu_osher=False:butcher_scheme(self.A,self.b,self.c,shu_osher)
```

```python
[ display(eq) for eq in rk3.scheme(shu_osher=True) ];
```

Pour RK3 on retrouve bien le schéma simplifié de Shu-Osher.

### 4.3.3 Récapitulatif des domaines des différentes méthodes en temps

On peut résumer tous les schémas en temps présentés par la figure ci-dessous. Figure à laquelle on a ajouté la méthode du point milieu, servant pour la résolution par méthode spectrale.

```python
save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [12,12]

R = sp.Abs(poly_carac(6))
contour(R,z,(-6.,3.,500),(-6.,6.,500), levels=[1.],colors=palette[0],label="RK (6,6)")
R = sp.Abs(prk6_7)
contour(R,z,(-6.,3.,500),(-6.,6.,500), levels=[1.],colors=palette[1],label="RK (7,6)")
R = sp.Abs(rk86.poly())
contour(R,z,(-6.,3.,500),(-6.,6.,500), levels=[1.],colors=palette[2],label=rk86.name)
plt.plot(*reim(b34),label="ERK SSP(4,3)",color=palette[3])
plt.plot(*reim(b3),label="ERK SSP(3,3)",color=palette[4])
plt.plot(*reim(b4),label="ERK SSP(4,4)",color=palette[5])
R = sp.Abs((z+1)/(z-1))
contour(R,z,(-6.,3.,500),(-6.,6.,500), levels=[1.],colors=palette[6],label="point milieu")
R = sp.Abs(rk53.poly())
contour(R,z,(-6.,3.,500),(-6.,6.,500), levels=[1.],colors=palette[7],label=rk53.name)

plt.legend(loc=(0.,1.02),ncol=8,mode="expand",borderaxespad=0.)
plt.show()
plt.rcParams['figure.figsize'] = save_figsize
```

## 4.4 Recherche du meilleur domaine de stabilité

Nicolas est intéressé par effectuer une étude un peu inverse, c'est-à-dire trouver le meilleur coefficient $\alpha$ tel que le polynôme caractéristique :

$$
  \alpha z^4 + \frac{z^3}{6} + \frac{z^2}{2} + z + 1
$$

ait le plus grand domaine de stabilité (en particulier sur l'axe imaginaire, pour exprimer la stabilité d'un schéma non diffusif). C'est à dire ajouter un étage sur une méthode RK3 existante, telle que celle-ci obtienne une meilleure stabilité.

```python
a = sp.symbols("\\alpha")
p = a*z**4 + poly_carac(3)
```

```python
save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [10,8]
for n in range(1,57,2):
  print(n,end="\r")
  bn = b_poly(p.subs(a,sp.Rational(1,n)))
  plt.plot(*reim(bn),',',label="$\\alpha = {}$".format(sp.latex(sp.Rational(1,n))))
print(" "*len(str(n)))
plt.plot(*reim(b_poly(poly_carac(4))),linestyle='-.',color=palette[3],label="ERK SSP(4,4) $\\alpha = {}$".format(sp.latex(sp.Rational(1,24))))
plt.plot(*reim(b3),linestyle=':',linewidth=2,color=palette[9],label="ERK SSP(3,3)")
plt.legend(loc=(0.,1.02),ncol=8,mode="expand",borderaxespad=0.,markerscale=10)
plt.show()
plt.rcParams['figure.figsize'] = save_figsize
```

Il s'agit là que d'une étude prospective, pour estimer la faisabilité à Nicolas d'une telle étude de manière quasi-automatisée. On semble remarquer que le schéma le plus *large* sur l'axe imaginaire correspond à RK4 (coefficient $\frac{1}{24}$ correspondant à la courbe en rouge en pointillé).

```python
"""
  même étude pour RK2 à 3 étages
"""

a = sp.symbols("\\alpha",real=True,positive=True)
p = a*z**3 + poly_carac(2)

save_figsize = plt.rcParams['figure.figsize']
msize = 1
plt.rcParams['figure.figsize'] = [10,8]
for n in range(1,17,1):
  print(n,end="\r")
  if n == 6 :
    msize = 5
  else:
    msize = 1
  bn = b_poly(p.subs(a,sp.Rational(1,n)))
  plt.plot(*reim(bn),'.',markersize=msize,label="$\\alpha = {}$".format(sp.latex(sp.Rational(1,n))))
print(" "*len(str(n)))

plt.plot(*reim(b3),label="ERK SSP(3,3)")
plt.plot(*reim(b2),label="ERK SSP(2,2)")

plt.legend(loc=(0.,1.02),ncol=8,mode="expand",borderaxespad=0.,markerscale=10)
plt.show()
plt.rcParams['figure.figsize'] = save_figsize
```

On remarque qu'il est possible d'avoir une partie réelle positive avec un RK2 à 3 étages, cela permet de stabiliser un schéma WENO (démarche déjà effectué dans [Wang R. and Spiteri R. J. (2007)](https://doi.org/10.1137/050637868) sur des schémas RK2 à $s$ étages.

Nous nous intéressons maintenant à maximiser la portion sur l'axe imaginaire.

```python
p = a*z**3 + poly_carac(2)

save_figsize = plt.rcParams['figure.figsize']
msize = 1
plt.rcParams['figure.figsize'] = [10,8]
for n in (3,4,5,7,8):
  print(n,end="\r")
  bn = b_poly(p.subs(a,sp.Rational(1,n)))
  plt.plot(*reim(bn),'.',markersize=msize,label="$\\alpha = {}$".format(sp.latex(sp.Rational(1,n))))
print(" "*len(str(n)))
plt.plot(*reim(b3),label="ERK SSP(3,3)")
plt.legend(loc=(0.,1.02),ncol=6,mode="expand",borderaxespad=0.,markerscale=10)
#plt.xlim([-0.1,0.1])
plt.plot(*reim(-1.4*weno.data),label="WENO")
plt.show()
plt.rcParams['figure.figsize'] = save_figsize
```

La maximisation du domaine sur l'axe imaginaire permet de stabiliser avec la plus grande CFL possible un transport pur. Le coefficient $\alpha$ maximisant le domaine sur l'axe imaginaire avec un RK2 à 3 étages est $\alpha = \frac{1}{4}$.

> **Preuve :** considérons le polynôme caractéristique $R_{\alpha}$ d'un schéma RK2 à 3 étages avec un coefficient $\alpha$ :
>
> $$
  R_{\alpha}(z) = 1 + z + \frac{z^2}{2} + \alpha z^3
$$
>
> On cherche à évaluer le polynôme en $z=i\beta$, correspondant à l'intersection de l'ensemble $\{ z\ /\  |R_\alpha(z)|=1 \}$ avec l'axe imaginaire.
>
> $$
  R_\alpha(i\beta) = 1 + i\beta - \frac{\beta^2}{2} - i\beta^3\alpha
$$
>
> on souhaite un module égale à 1 :
>
> $$
  1 = |R_\alpha(i\beta)|^2 = \left(1-\frac{\beta^2}{2}\right)^2 + \left(\beta - \beta^3\alpha \right)^2
$$
>
> ce qui peut se simplifier par :
>
> $$
  \alpha^2\beta^6 + (\frac{1}{4}-2\alpha)\beta^4 = 0
$$
>
>En supposant $\beta \neq 0$ :
>
>$$
  \alpha^2\beta^2 -2\alpha + \frac{1}{4} = 0
$$
>
>soit :
>
>$$
  \beta^2 = \frac{2\alpha - \frac{1}{4}}{\alpha^2}
$$
>
>on obtient ainsi une fonction $\beta^2:\alpha\mapsto \frac{2\alpha - \frac{1}{4}}{\alpha^2}$ dont on souhaite trouver le maximum (ou plus exactement le coefficient $\alpha$ où est atteint le maximum, ce qui explique que l'on ne s'intéresse qu'au coefficient $\beta^2$ et non explicitement $\beta$).
>
>$$
  \left(\beta^2\right)'(\alpha) = \frac{-2\alpha^2 + \frac{\alpha}{2}}{\alpha^4}
$$
>
>la dérivée s'annule en $\alpha = \frac{1}{4}$ □

La maximisation du domaine sur l'axe imaginaire ne permet pas rigoureusement de maximiser la CFL du couple de schéma RK$^\alpha N$-WENO mais permet de se donner une idée ; prenons par exemple le cas de RK2 à 3 étages, comme dit précédemment le coefficient $\alpha$ maximisant le domaine de stabilité sur l'axe imaginaire est $\alpha = \frac{1}{4}$, représenté par le courbe orange sur la figure, or il est possible d'obtenir une CFL plus importante avec le coefficient $\alpha=\frac{1}{5}$, pour lequel on représente le coefficient d'amplification en rose de WENO$\times$1.4 (sans aller chercher $\alpha=\frac{1}{6}$ qui correspond à RK3).

```python
"""
  calcul systématique du meilleur coefficient alpha pour maximiser le domaine de stabilité sur l'axe imaginaire
"""

alpha = sp.symbols("\\alpha",real=True,positive=True)

def poly_RKsn(n):
  return alpha*z**(n+1) + poly_carac(n)

n=3 # avec n=2 on retrouve bien 1/4
beta = sp.symbols("\\beta",real=True,positive=True)
p = poly_RKsn(n)

alphas = []
# on cherche à résoudre $|p(ib)|^2 -1 = 0$, solutions auxquels on soustrait 0
# puis on dérive les solutions (qui dépendent de alpha) pour savoir où la dérivée s'annule et donc où beta
# est maximisé
for x in sp.solveset((sp.Abs(p.subs(z,beta*I))**2-1).collect(beta),beta**2)-sp.sets.FiniteSet(0):
  betaset = sp.solveset(x.diff(alpha),alpha,domain=sp.S.Reals)
  if betaset != sp.EmptySet() :
    alphas.extend(betaset)
(alphas[0],alphas[0].evalf())
```

```python
"""
  vérification graphique de la solution trouvée
"""
fig, (ax1, ax2) = plt.subplots(1,2,figsize=(14,7))

p = poly_RKsn(3)

for n in (20,22,24,26,28):
  print(n,end="\r")
  bn = b_poly(p.subs(alpha,sp.Rational(1,n)))
  ax1.plot(*reim(bn),markersize=msize,label="$\\alpha = {}$".format(sp.latex(sp.Rational(1,n))))
  ax2.plot(*reim(bn),markersize=msize)
print(alphas[0],end="\r")
bn = b_poly(p.subs(alpha,alphas[0]))
ax1.plot(*reim(bn),markersize=msize,label="$\\alpha = {}$".format(sp.latex(alphas[0])))
ax2.plot(*reim(bn),markersize=msize)
print(" "*len(str(alphas[0])))

fig.legend(loc=(0.,0.),ncol=6,mode="expand",borderaxespad=0.,markerscale=10)

ax2.set_xlim([-0.025,0.01]) ; ax2.set_ylim([2.82,2.86])

plt.show()
```

## 4.5 Domaine de stabilité intermédiaire

Juste pour tester, on peut essayer de tracer le domaine de stabilité de chaque étage. Je ne sais pas si cela a réellement un sens. La majorité des schémas RK que j'ai pu voir commence par une étape d'Euler explicite, or celle-ci est instable avec le schéma WENO, donc la stabilité est apporté par les étages supplémentaires.

L'idée de tracer ces domaines provient du survol que j'ai pu effectué des travaux de Ketcheson (ce qu'il appelle *internal stability*).

```python
"""
RK3 Shu-Osher stages stability
"""
save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [7,7]

rk = rk43
for i,p in enumerate(rk.sub_polys()):
  #plt.plot(*reim(b_poly(p)),label="ERK SSP(3,3) stage n°%i"%i)
  contour(sp.Abs(p.subs(nu,0.5)),z,(-6,2,100),(-4,4,100),levels=[1.],colors=palette[i],label=rk.name+" stage n°%i"%(i+1))

plt.legend(loc=(0.,1.02),ncol=3,mode="expand",borderaxespad=0.,markerscale=10)
plt.axis('equal')
plt.show()

plt.rcParams['figure.figsize'] = save_figsize
rk.sub_polys()
```

## 4.6 *Order Star*

Il est possible assez facilement, pour étudier certaines propriétés de nos schémas en temps, de tracer l'*order star* (n'ayant vu aucun article francophone en parler j'utlise le terme en anglais). L'*order star* est définie comme : $\{\mathcal{A}_+,\mathcal{A}_0,\mathcal{A}_-\}$ avec :

* $\mathcal{A}_+ = \left\{ z\in\mathbb{C}\,:\, |e^{-z}R(z)|>1 \right\}$
* $\mathcal{A}_0 = \left\{ z\in\mathbb{C}\,:\, |e^{-z}R(z)|=1 \right\}$
* $\mathcal{A}_- = \left\{ z\in\mathbb{C}\,:\, |e^{-z}R(z)|<1 \right\}$

où $R$ est le polynôme caractéristique, ou approximation de Padé de la fonction à étudier.

> Pour des raisons techniques, il est plus simple de colorier l'ensemble des points du plan dont la valeur d'une fonction est comprise entre 2 bornes finies, par conséquent, en bleu, nous alons afficher $ \mathcal{A}_-$.

```python
def contourf ( expr , z , Ir , Ii , **kwargs):
  x,y = sp.symbols("x y",real=True)
  f = sp.lambdify((x, y), expr.subs(z,x+I*y),'numpy')

  a = np.linspace(*Ir)
  b = np.linspace(*Ii)
  X, Y = np.meshgrid(a, b)
  try:
    lab = kwargs.pop("label")
  except:
    lab = ""
  try:
    c = kwargs["colors"]
  except:
    c = palette[0]
  if len(lab) > 0 :
    plt.plot([0],[0],'s',markersize=1,label=lab,color=c)
  return plt.contourf(X,Y, f(X,Y).real,**kwargs)
```

```python
save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [6,6]
contourf(sp.Abs(sp.exp(-z)*rk3.poly()),z,(-5,5,100),(-5,5,100),levels=[1.,1.e299],colors=palette[0],label="$\mathcal{A}_+$")
plt.legend(loc=(0.,1.02),ncol=3,mode="expand",borderaxespad=0.,markerscale=8)
plt.show()
plt.rcParams['figure.figsize'] = save_figsize
```

```python
save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [10,10]

for i,rk in enumerate([rk3,rk4,rk53,dp5,rk86,rk43,rk76]):
  contour(sp.Abs(sp.exp(-z)*rk.poly()) ,z,(-5,5,200),(-5,5,200),levels=[1.],colors=palette[i],label=rk.name )

plt.legend(loc=(0.,1.02),ncol=7,mode="expand",borderaxespad=0.)
plt.show()

plt.rcParams['figure.figsize'] = save_figsize
```

On retrouve ainsi bien l'*order star* de différentes méthodes.

# 5. Étude de couples RK$N$-WENO5

On regarde maintenant la stabilité de WENO avec les différents schémas RK$N$. Pour cela on trace le domaine de stabilité du schéma RK choisi, et on compare ce domaine avec celui du coefficient d'amplification du schéma WENO5. Le schéma est linéairement stable s'il existe un coefficient $\sigma$, appelé CFL, permettant de faire *rentrer* la courbe du schéma WENO dans le domaine de stabilité de RK$N$.

```python
fig, ax = plt.subplots(2,2,figsize=(10,10))
coords = [(x,y) for x in range(2) for y in range(2)]

for (n,b) in enumerate((b1,b2,b3,b4),start=1):
  ax[coords[n-1]].plot(*reim(b),label="RK%i"%n)
  ax[coords[n-1]].plot(*reim(-wenol.data_z),label="linearized WENO")
  ax[coords[n-1]].plot(*reim(-weno.data),label="WENO")
  ax[coords[n-1]].axis('equal')
  ax[coords[n-1]].legend(loc=(0.,1.02),ncol=3,mode="expand",borderaxespad=0.)
plt.show()
```

## 5.1 Automatisation du calcul de CFL

Il est maintenant intéressant d'étudier quel est le rapport d'homothétie liant le mieux RK$N$ à WENO, ce rapport d'homothétie est $\sigma$ le nombre de CFL. Dans l'article [Wang R. and Spiteri R. J. (2007)](https://doi.org/10.1137/050637868) une approximation grossière est faite en supposant que RK$N$, pour $N\geq 3$ contient un rectangle contenant la courbe de WENO linéarisé. Une estimation est ainsi faite d'un $\sigma_0 \leq \sigma$. Il est possible dans un premier temps d'estimer numériquement un coefficient $\sigma_{\texttt{num}}$ tel que :

$$
  \sigma_0 \leq \sigma_{\texttt{num}} \leq \sigma
$$

travail effectué dans [Motamed M. and Macdonald C. B. (2010)](https://doi.org/10.1007/s10915-010-9423-9). Il est aussi possible de travailler sur la version complète du schéma WENO, en prenant en compte dans l'analyse de von Neumann de la partie non linéaire.

### 5.1.1 Estimation sur WENO linéarisé

Il est plus facile de travailler sur des tableaux des données du WENO et de RK$N$ de même dimension, et avec des données triées par argument (pour estimer le rapport de l'homothétie).

```python
def cfl( z_weno , b_rk , display=True ):
  zb = [ min(z_weno,key=lambda z:np.abs(np.angle(z)-np.angle(b_))) for b_ in b_rk ]
  sigs = np.array([ np.array([np.angle(z),np.abs((b_)/(z)) if np.abs(z)>1E-2 else float("nan")]) for (z,b_) in zip(zb,b_rk) ])
  if display :
    fig,ax = plt.subplots(1,2,figsize=(12,4))
    ax[0].plot(sigs[:,0],sigs[:,1])
    ax[0].set_xlabel("φ");ax[0].set_ylabel("σ")
    ax[1].plot(*reim(b_rk),label="time method")
    cfl = min(sigs[:,1])
    ax[1].plot(*reim(cfl*z_weno),label="space method $\\times\\,\\sigma = {0:.3f}$".format(cfl))
    ax[1].plot(*reim(z_weno),label="space method")
    plt.legend(loc=(0.,1.02),ncol=2,mode="expand",borderaxespad=0.)
    plt.show()
  return min(sigs[:,1])
```

```python
sigma_l = cfl(-wenol.data_z,b3[::3])
sigma_l
```

```python
cfl(-wenol.data_z,b4[::4])
```

### 5.1.2 Estimation sur WENO

On utilise maintenant la même fonction pour effectuer le même travail sur WENO complet.

```python
sigma_w = cfl(-weno.data,b3[::3])
sigma_w
```

```python
cfl(-weno.data,b4[::4])
```

On observe (ce qui est relativement étrange) une CFL plus faible dans le cas complet que linéarisé pour le couple RK4-WENO5. Ceci s'explique par la forme du domaine, on peut voir le cas extrême avec le RK d'ordre 3 à 4 étages maximisant le domaine de stabilité sur l'axe imaginaire mais ayant une CFL peut avantageuse face à d'autres méthodes.

```python
sigma_w34 = cfl(-weno.data,b34[::4])
sigma_w34
```

### 5.1.3 Validation des CFL

On s'intéresse ici à regarder où s'effectue la contrainte de la CFL. Il peut être intéressant numériquement d'exciter le mode particulier avec le bon $\Delta x$ qui stabilise ou non le schéma.

```python
save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [8,7]

b_rk = b3[::3]; n=3
plt.plot(*reim(b_rk[:]),label="RK SPP(3,3)")
plt.plot(*reim(-sigma_l*wenol.data_z[:]),label="linearized WENO times %.3f"%sigma_l)
plt.plot(*reim(-sigma_l*weno.data[:]),label="WENO times %.3f"%sigma_l)
#plt.axis('equal')
plt.legend(loc=(0.,1.02),ncol=3,mode="expand",borderaxespad=0.)
plt.show()

plt.plot(*reim(b_rk[:]),label="RK SPP(3,3)")
plt.plot(*reim(-sigma_w*wenol.data_z[:]),label="linearized WENO times %.3f"%sigma_w)
plt.plot(*reim(-sigma_w*weno.data[:]),label="WENO times %.3f"%sigma_w)
#plt.axis('equal')
plt.legend(loc=(0.,1.02),ncol=3,mode="expand",borderaxespad=0.)
plt.show()

plt.plot(*reim(b_rk[:]),label="RK SPP(3,3)")
plt.plot(*reim(-wenol.data_z[:]),label="linearized WENO")
plt.plot(*reim(-weno.data[:]),label="WENO times")
#plt.axis('equal')
plt.legend(loc=(0.,1.02),ncol=3,mode="expand",borderaxespad=0.)
plt.show()

(data.phi[300],np.angle(wenol.data_z[300]),wenol.data_z)

plt.rcParams['figure.figsize'] = save_figsize
```

On effectue maintenant un zoom dans le cas de la CFL `sigma_w`.

```python
save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [10,7] # default:[6,4]

plt.plot(*reim(b3),label="RK%i"%n)
plt.plot(*reim(-sigma_w*wenol.data_z),label="linearized WENO times %f"%sigma_w)
plt.plot(*reim(-sigma_w*weno.data),label="WENO times %f"%sigma_w)
plt.xlim([-1.75,-0.6])
plt.ylim([1.5,2.4])
plt.legend(loc=(0.,1.02),ncol=3,mode="expand",borderaxespad=0.)
plt.show()

plt.rcParams['figure.figsize'] = save_figsize
```

```python
"""
  Stability of ERK SSP(4,3)-WENO5 with a CFL of 1.923 with the computation time than RK4 (and a "bad" CFL)
"""

save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [10,8]

b_rk = b34[::4]
plt.plot(*reim(b_rk[:]),label="RK SPP(4,3)")
plt.plot(*reim(-sigma_w34*wenol.data_z[:]),label="linearized WENO times %f"%sigma_w34)
plt.plot(*reim(-sigma_w34*weno.data[:]),label="WENO times %f"%sigma_w34)
#plt.axis('equal')
plt.legend(loc=(0.,1.02),ncol=3,mode="expand",borderaxespad=0.)
plt.show()

plt.rcParams['figure.figsize'] = save_figsize
```

On s'intéresse maintenant aux méthodes de type Runge-Kutta d'ordre 3 à 4 étages avec la recherche du meilleur coefficient $\alpha$. On trace donc différents couples de schémas en représentant le domaine de stabilité du schéma en temps, et le coefficient d'amplification du schéma WENO multiplié par sa CFL.

```python
save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [10,7]

plt.plot(*reim(b_poly(p.subs(alpha,alphas[0]))),label="RK(4,3) $\\alpha_o = {}$".format(sp.latex(alphas[0])))
plt.plot(*reim(-1.579*weno.data),label="WENO RK(4,3) $\\alpha_o = {}$".format(sp.latex(alphas[0])))
plt.plot(*reim(b34),label="RK(4,3) $\\alpha = {}$".format(sp.latex(sp.Rational(1,48))))
plt.plot(*reim(-1.923*weno.data),label="WENO RK(4,3) $\\alpha = {}$".format(sp.latex(sp.Rational(1,48))))
plt.plot(*reim(b3),label="RK(3,3)")
plt.plot(*reim(-1.606*weno.data),label="WENO RK(3,3)")
plt.legend(loc=(0.,1.02),ncol=3,mode="expand",borderaxespad=0.)
plt.show()

plt.rcParams['figure.figsize'] = save_figsize
```

Les couples de schémas représentés (en *CFLisant* WENO) :

* RK(4,3) $\alpha_{\text{opt}} = - \frac{\left(-2 + \sqrt[3]{2}\right) \left(1 + \sqrt[3]{2}\right)}{36}$ (bleu) WENO (orange) : schéma temporel de type Runge-Kutta d'ordre 3 à 4 étages qui maximise le domaine de stabilité sur l'axe imaginaire.
* RK(4,3) $\alpha = \frac{1}{48}$ (vert) WENO (rouge) : schéma temporel aussi appelé RK SSP(4,3).
* RK(3,3) (violet) WENO (marron) : schéma temporel RK3 classique.

Le schéma temporel RK(4,3) $\alpha_{\text{opt}}$ (en bleu sur la figure) est celui qui maximise le domaine de stabilité sur l'axe imaginaire, malgré cela la condition CFL de ce schéma (1.579) lié à WENO est similaire à celle du couple RK SSP(3,3)-WENO (1.579 contre 1.606 pour le RK SSP(3,3)).

Récapitulatif des conditions CFL entre différents schémas temporels et WENO5 :

| RK(4,3) $\alpha_{\text{opt}}$ | RK SSP(4,3) | RK SSP(3,3) |
|-------------------------------|-------------|-------------|
| 1.579                         | 1.923       | 1.606       |

On remarque qu'il n'est pas nécessaire dans le cadre de WENO de maximiser le domaine sur l'axe imaginaire (le schéma étant diffusif). Étudions maintenant l'évolution de la CFL en fonction de $\alpha$.

```python
def cfl( z_weno , b_rk ):
  zb = [ min(z_weno,key=lambda z:np.abs(np.angle(z)-np.angle(b_))) for b_ in b_rk[:300] ]
  sigs = [ np.abs((b_)/(z)) if np.abs(z)>1E-2 else float('nan') for (z,b_) in zip(zb,b_rk[:300]) ]
  return min(sigs)

cfls = []
ns = range(20,50,1)

p = poly_RKsn(3)
for n in ns:
  print(n,end="\r")
  bn = b_poly(p.subs(a,sp.Rational(1,n)))
  cfls.append(cfl(-weno.data,bn[::4]))
print()

plt.plot([1./n for n in ns],cfls)
plt.xlabel("α");plt.ylabel("CFL")
plt.show()

(sp.Rational(1,ns[cfls.index(max(cfls))]),max(cfls)) # value where the is the maximum CFL and the CFL
```

```python
## %config InlineBackend.figure_format = 'retina'
save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [10,7]

p = poly_RKsn(3)
plt.plot(*reim(b_poly(p.subs(a,sp.Rational(1,38)))),label="RK(4,3) $\\alpha = {}$".format(sp.latex(sp.Rational(1,38))))
plt.plot(*reim(-1.969*weno.data),label="WENO RK(4,3) $\\alpha = {}$".format(sp.latex(sp.Rational(1,38))))
plt.plot(*reim(b34),label="RK(4,3) $\\alpha = {}$".format(sp.latex(sp.Rational(1,48))))
plt.plot(*reim(-1.923*weno.data),label="WENO RK(4,3) $\\alpha = {}$".format(sp.latex(sp.Rational(1,48))))
plt.plot(*reim(b3),label="RK(3,3)")
plt.plot(*reim(-1.606*weno.data),label="WENO RK(3,3)")
plt.legend(loc=(0.,1.02),ncol=3,mode="expand",borderaxespad=0.)
plt.show()

plt.rcParams['figure.figsize'] = save_figsize
```

Le récapitulatif des conditions CFL devient :

| RK(4,3) $\alpha_{\text{opt}}$ | RK SSP(4,3) | RK(4,3) $\alpha_{\text{max}}$ | RK SSP(3,3) |
|-------------------------------|-------------|-------------------------------|-------------|
| 1.579                         | 1.923       | 1.969                         | 1.606       |

Il est sans doute possible de trouver une CFL légèrement supérieur au cas $\alpha_{\text{max}}=\frac{1}{38}$ mais cela donne un ordre d'idée assez précis de la CFL qu'il est possible d'atteindre au maximum. Le schéma étant déjà écrit pour le cas RK SSP(4,3), correspondant au cas $\alpha_{\text{RK SSP(4,3)}} = \frac{1}{48}$ avec une CFL relativement proche, il ne semble pas nécessaire de s'impliquer dans la recherche du schéma optimal en résolvant les relations d'ordres.

### 5.1.4 Calcul de CFL pour des schémas RK d'ordre élevé

On peut aussi s'intéresser aux ordres plus élevé. Il s'agit là plus d'une démonstration technique qu'une étude guidée par un intérêt scientifique.

```python
def cfl( z_weno , b_rk , display=True ):
  sigs = np.array([ np.array([np.angle(z),np.abs((b_)/(z))]) for (z,b_) in zip(z_weno,b_rk) if np.abs(z)>1e-1 and np.abs((b_)/(z)) > 1. ])
  if display :
    plt.plot(sigs[50:-50,0],sigs[50:-50,1])
    plt.xlabel("φ");plt.ylabel("σ")
    plt.show()
  return min(sigs[50:-50,1])

R_rk6   = sp.Abs(poly_carac(6))

save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [2,2]
# Pour le moment l'obtention des segments demande aussi le tracé de la courbe
# j'ai commencé à regarder comment faire pour avoir un `plt.hidden` ou similaire
dat0 = contour(R_rk6,z,(-6.,3.,500),(-6.,6.,500), levels=[1.]).allsegs
plt.show()
plt.rcParams['figure.figsize'] = save_figsize

# on regarde, pour l'étude de la stabilité, que le domaine central
dat1 = max(dat0[0][:],key=lambda l:len(l))
dat1_c = [ re+im*1j for (re,im) in zip(dat1[:,0],dat1[:,1]) ] # on convertit les données en complexes
# on trie les données selon l'argument
b6 = np.array([ min(dat1_c,key=lambda b:np.abs(np.angle(b)-np.angle(-z_)) ) for z_ in weno.data ])

# calcul de la CFL
sig_rk6 = cfl(weno.data[:],b6[:])

save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [6,6]
[ plt.plot(c[:,0],c[:,1],color=palette[i],label="RK6 n°{}".format(i)) for i,c in enumerate(dat0[0]) ]
plt.plot(*reim(-sig_rk6*weno.data),color=palette[3],label="WENO $\\sigma =${0:.3f}".format(sig_rk6))
plt.legend(loc=(0.,1.02),ncol=4,mode="expand",borderaxespad=0.)
plt.show()

plt.rcParams['figure.figsize'] = save_figsize

print(sig_rk6)
```

```python
R_rk6_8 = sp.Abs(rk86.poly())
R_rk6_7 = sp.Abs(prk6_7)
R_rk3_5 = sp.Abs(rk53.poly())
R_dp5   = sp.Abs(dp5.poly())

def cfl_expr ( expr , z , wenodat , Ir , Ii ) :
  dat0 = contour(expr,z,Ir,Ii, levels=[1.]).allsegs[0]
  tmp = max(dat0,key=lambda l:len(l))
  tmp_c = [ re+im*1j for (re,im) in zip(tmp[:,0],tmp[:,1]) ]
  b = np.array([ min(tmp_c,key=lambda zb:np.abs(np.angle(zb)-np.angle(-zw)) ) for zw in wenodat ])
  return cfl(wenodat,b,display=False)

save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [2,2]
sig_rk6_8 = cfl_expr(R_rk6_8,z,weno.data,(-6.,3.,500),(-6.,6.,500))
sig_rk6_7 = cfl_expr(R_rk6_7,z,weno.data,(-6.,3.,500),(-6.,6.,500))
sig_rk3_5 = cfl_expr(R_rk3_5,z,weno.data,(-6.,3.,500),(-6.,6.,500))
sig_dp5   = cfl_expr(R_dp5,z,weno.data,(-6.,3.,500),(-6.,6.,500))
plt.show()
plt.rcParams['figure.figsize'] = save_figsize

save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [12,12]

# RK6
contour(R_rk6,z,(-6.,4.,500),(-5.,5.,500), levels=[1.],
        colors=palette[0],label="RK 6 $\\sigma = {0:.3f}$".format(sig_rk6))
plt.plot(*reim(-sig_rk6*weno.data),color=palette[1],label="WENO RK6")

# RK(8,6)
contour(R_rk6_8,z,(-6.,4.,500),(-5.,5.,500), levels=[1.],
        colors=palette[2],label="RK(8,6) $\\sigma = {0:.3f}$".format(sig_rk6_8))
plt.plot(*reim(-sig_rk6_8*weno.data),color=palette[3],label="WENO RK(8,6)")

# RK(7,6)
contour(R_rk6_7,z,(-6.,4.,500),(-5.,5.,500), levels=[1.],
        colors=palette[4],label="RK(7,6) $\\sigma = {0:.3f}$".format(sig_rk6_7))
plt.plot(*reim(-sig_rk6_7*weno.data),color=palette[5],label="WENO RK(7,6)")

# RK(5,3)
contour(R_rk3_5,z,(-6.,4.,500),(-5.,5.,500), levels=[1.],
        colors=palette[6],label="RK(5,3) $\\sigma = {0:.3f}$".format(sig_rk3_5))
plt.plot(*reim(-sig_rk3_5*weno.data),color=palette[7],label="WENO RK(5,3)")

# DP5
contour(R_dp5,z,(-6.,4.,500),(-5.,5.,500), levels=[1.],
        colors=palette[8],label="DP5 $\\sigma = {0:.3f}$".format(sig_dp5))
plt.plot(*reim(-sig_dp5*weno.data),color=palette[9],label="WENO DP5")

plt.legend(loc=(0.,1.02),ncol=5,mode="expand",borderaxespad=0.)
plt.show()

plt.rcParams['figure.figsize'] = save_figsize
```

Récapitulatif des CFL d'ordre supérieur :

| RK (6,6) | RK (8,6) | RK (7,6) | RK (5,3) |
|----------|----------|----------|----------|
| 2.124    | 2.564    | 1.756    | 2.538    |

Le RK(6,6) est purement hypothétique puisque actuellement aucun schéma de type Runge-Kutta d'ordre 6 à 6 étages n'a été trouvé.

## 5.2 Validation par simulation numérique

Nous nous intéressons ici qu'à la vérification du travail sur le schéma WENO non linéarisé, en comparant ceci au cas linéarisé.

Il est maintenant nécessaire de tester en différents points du domaine et de tester la validité des dires sur la CFL. Pour s'assurer que l'on excite qu'un seul mode il est nécessaire d'effectuer le transport d'un cosinus avec un mode $\xi$ et un $\Delta x$ vérifiant :

$$
  \phi = 2\pi\xi\Delta x
$$

> **Petite note pour Paul:** puisque je fais du numérique je prends $\xi=2$ c'est très bien, et je ne travaille pas sur des schémas capables de gérer des modes élevé comme Joackim avec son mode 41 ^^

On prendra le mode d'excitation $\xi=2$ par conséquent pour un $\phi$ donné on calcul un $\Delta x$ :

$$
  \Delta x = \frac{\phi}{4\pi}
$$

Nous fixons la CFL $\sigma^{W_{NL}} \approx 1.606$ tel que donné par l'étude de CFL du couple WENO non linéarisé avec RK3. L'angle $\phi$ est choisi en prenant l'argument d'un point sur la courbe $\tilde{z}$ (donc un coefficient d'amplification du WENO linéarisé) tel qu'il soit en dehors du domaine de stabilité de RK3. Numériquement nous prendrons : $\phi = 1.38337934$.

Nous effectuons une simulation de :

$$
  \begin{cases}
    \partial_t u + \partial_x u = 0 \\
    u^0 = \cos(2 x)
  \end{cases}
$$

sur le domaine spatiale $[0,2\pi]$, jusqu'au temps final $T_f = 15$. Nous prendrons comme nombre de CFL $\sigma^{W_{NL}} \approx 1.606$.

|         step          |         value         |
|-----------------------|-----------------------|
| $\Delta x$            | `0.11008583006610187` |
| $\Delta t$            | `0.15742273699452566` |

La simulation s'effectue avec $N=57$ points.

Nous constatons une stabilité du schéma WENO5-RK3, malgré quelques oscillations, celles-ci ne divergent pas même en temps longs, le schéma est malheureusement très diffusif sous ces conditions. Ces oscillations s'estompent dès que le contraint moins la CFL, en prenant par exemple pour CFL $\sigma^{W_L} \approx 1.433$ on obtient :

ce qui laisse sous entendre que le coefficient d'amplification du schéma WENO est bien donné par la courbe verte. De plus en prenant une CFL légèrement supérieur ($\approx 1.7$) le schéma est instable (la solution diverge).

Un test avec un schéma WENO linéarisé, c'est-à-dire avec des poids $w_i = \gamma_i$ est aussi effectué. On remarque qu'il est alors nécessaire de conserver la CFL à $\sigma^{W_L} \approx 1.433$ pour stabiliser le schéma.

Cela semble confirmer le calcul de CFL pour le WENO complet.

> Le domaine de stabilité de RK6 n'étant pas connexe, il est possible d'illustrer qu'en excitant un mode $\xi$ particulier, on peut stabiliser le schéma en prenant une CFL $\frac{\Delta t}{\Delta x}$ plus grande, et le rentre instable avec une CFL plus petite.

## 5.3 Estimation du coût numérique

### 5.3.1 Coût numérique par CFL

Le principal coût numérique en temps de calcul est l'évaluation du schéma WENO à chaque étage d'un schéma Runge-Kutta. On peut estimer le coût de chaque étage nécessitant l'approximation d'une dérivée par la méthode WENO à 1, on se retrouve alors avec le tableaux de coûts suivant :

| couple           | $\sigma_i$ | nombre d'itérations         | coût par itération | coût par unité de temps       |
|------------------|------------|-----------------------------|--------------------|-------------------------------|
| RK SSP(3,3)-WENO | $1,606$    | $n \propto 0,62 \times T_f$ | 3                  | $\frac{3}{1,606}\approx 1,86$ |
| RK SSP(4,3)-WENO | $1,923$    | $n \propto 0,52 \times T_f$ | 4                  | $\frac{4}{1,923}\approx 2,08$ |
| RK SSP(4,4)-WENO | $1,680$    | $n \propto 0,59 \times T_f$ | 4                  | $\frac{4}{1,680}\approx 2,38$ |

La comparaison s'effectue avec des paramètres de simulations ($\Delta x$, $T_f$, ...) fixés, par conséquent le pas en espace peut être pris égal à 1 sans perdre en généralité ; nous prenons en revanche le plus grand pas de temps $\Delta t = \sigma\Delta x$ permis par la CFL $\sigma$. Le nombre d'itérations est obtenu par $n=\frac{T_f}{\Delta t}$ et est proportionnel à $n \propto \frac{1}{\sigma}T_f$ pour un $\Delta x$ fixé. Le coût par itération correspond au nombre d'évaluation de la méthode WENO par itération, et correspond donc au nombre d'étages de la méthode d'intégration en temps. Le coût par unité de temps est obtenu en calculant le coût de la simulation jusqu'au temps final 1.

Le coût par unité de temps du couple RK SSP(4,3)-WENO reste supérieur à RK SSP(3,3)-WENO malgré la CFL plus grande. La plus grande CFL de la méthode RK(4,3) $\alpha_{\text{max}}$ ne permet pas de réduire drastiquement le coût par unité de temps ($\approx 2,03$), il serait nécessaire d'avoir une CFL $\sigma \geq 2,14$ avec un RK à 4 étages pour avoir un coût par unité de temps inférieur au couple RK SSP(3,3)-WENO. Il semblerait, en extrapolant les résultats de [Wang R. and Spiteri R. J. (2007)](https://doi.org/10.1137/050637868) à propos de la stabilité du couple RK2 (optimal) à $s$ étages - WENO 5 (corollaire 3.10), qu'il soit difficile de stabiliser WENO 5 avec un RK2 à 4 étages (l'étude faite sur le RK(3,2) n'est pas forcément très concluante sur la généralisation de ce résultat), il semble donc difficile de trouver une CFL permettant de diminuer le coût par unité de temps.

On peut de la même manière estimer la CFL minimale d'une méthode de type Runge-Kutta à 5 étages pour diminuer le coût par unité de temps ($\sigma \geq 2,67$).

> Ce coût numérique n'est qu'un estimateur du temps de calcul, fonctionnant aussi pour l'utilisation de la mémoire (sauf cas particulier d'un schéma explicite Runge-Kutta dont la table de Butcher est sous diagonale, qui permet des optimisations importantes en mémoire). On ne prend pas en compte le gain sur l'erreur numérique par exemple.

La diminution de l'erreur peut s'obtenir aussi en prenant un pas de temps plus petit, ainsi il est intéressant d'étudier le coût par unité de temps de RK SSP(3,3)-WENO avec une CFL $\alpha\sigma$. Tant que $\alpha \geq 0.785$ il est plus intéressant de réduire le pas de temps pour diminuer l'erreur, en deçà le coût numérique devient moindre avec une méthode d'ordre plus élevé (RK SSP(4,4) pour l'exemple).

En estimant toujours le coût au nombre d'étages de la méthode, on peut s'intéresser aux couples d'ordre supérieurs :

| couple        | $\sigma_i$ | nombre d'itérations          | coût par itération | coût par unité de temps        |
|---------------|------------|------------------------------|--------------------|--------------------------------|
| RK (8,6)-WENO | $2.564$    | $n \propto 0.390 \times T_f$ | 8                  | $\frac{8}{2.564}\approx 3.120$ |
| RK (7,6)-WENO | $1.756$    | $n \propto 0.569 \times T_f$ | 7                  | $\frac{7}{1.756}\approx 3.986$ |
| RK (5,3)-WENO | $2.538$    | $n \propto 0.394 \times T_f$ | 5                  | $\frac{5}{2.538}\approx 1.970$ |

On remarque que la méthode RK (5,3) a un coût numérique similaire à cela de RK SSP(3,3) bien qu'ayant 5 étages. L'ordre de la méthode étant le même, je ne suis pas convaincu par l'intérêt de la méthode.

### 5.3.2 Coût numérique et erreur par ordre

L'estimation du coût par unité de temps ne met pas en lumière la diminution de l'erreur avec la monté en ordre ; de plus nous ne consdiérons que le cas où nous prenons le plus grand pas de temps $\Delta t$ permis par la CFL. Nous allons donc tracer ici l'erreur en fonction de $\frac{\Delta t}{s}$ où $s$ est le nombre d'étage de la méthode considérée, et ce pour les différents schémas en temps présentés.

Pour ceci nous appliquons le même protocole que pour l'obtention de l'ordre, c'est-à-dire la simulation d'un problème d'advection :

$$
  \begin{cases}
    u_t + u_x = 0 \\
    u^0(x) = u(t=0,x) = \cos(7x)
  \end{cases}
$$

sur le domaine périodique $x\in[0,2\pi]$, simulation effectué avec $N_x = 10000$ points, $\Delta x = \frac{2\pi}{N}$, jusqu'au temps final $T_f=\frac{10}{N_x}$ où la solution exacte est connue : $u^f(x) = u(t=T_f,x)=u^0(x-T_f)$. Nous prendrons $\Delta t = \frac{10}{mN_x}$ avec $m=1,2,\dots,5$ (pour $m=1$ on est toujours sous les conditions CFL calculées précédemment, il s'agit de prendre le plus grand $\Delta t$ possible, tout en étant rationnellement indépendant de $\Delta x$ car selon Joackim cela est plus *fair play*). On obtient ainsi une approximation numérique de l'ordre, en plus de pouvoir comparer les erreurs entre les différentes méthodes. Nous traçons ensuite $\log(e_{\infty}) = f(\log(\frac{\Delta t}{s})$ où $e_{\infty}$ est l'erreur en norme $\infty$ :

$$
  e_{\infty} = \max_i |u_i^n - u_i^f|
$$

et où $s$ est le nombre d'étages (*stages*) de la méthode. Cela permet de considérer l'erreur d'une méthode en fonction de son coût.

On constate une saturation de l'erreur malgré un passage à des `long double` pour palier à ce problème.

# 6. Schémas Lawson et exponentiels

Les schémas de type Runge-Kutta permettent la résolution d'une EDP du type :

$$
  \dot{u} = \lambda u
$$

avec $\lambda\in\mathbb{C}$. Le problème qui nous intéresse ici est du type :

$$
  \dot{u} = \lambda u + \mu u
$$

avec $\lambda,\mu\in\mathbb{C}$. Les coefficients $\lambda$ et $\mu$ représentent différents opérateurs. Dans le contexte de la résolution de l'équation de Vlasov, $\lambda$ peut représenter la discrétisation en espace (via une transformée de Fourier), et $\mu$ celle en temps (via la méthode WENO). Pour ces raisons on peut vouloir *faire rentrer* une opération sous la dérivée en temps via une exponentielle :

$$
  \partial_t \left(e^{-\lambda t}u\right) = e^{-\lambda t}\mu u
$$

Pour construire un schéma en temps, il faut intégrer sur $[t^n,t^{n+1})$ :

$$
  \int_{t^n}^{t^{n+1}} \partial_t \left(e^{-\lambda t}u\right)\,\mathrm{d}t = \int_{t^n}^{t^{n+1}}e^{-\lambda t}\mu u\,\mathrm{d}t
$$

On évalue de manière exacte le terme de gauche :

$$
  e^{-\lambda t^n}\left(e^{-\lambda\Delta t}u(t^{n+1}) - u(t^n)\right) = \int_{t^n}^{t^{n+1}}e^{-\lambda t}\mu u\,\mathrm{d}t
$$

soit :

$$
  e^{-\lambda\Delta t}u(t^{n+1}) - u(t^n) = \int_{t^n}^{t^{n+1}}e^{-\lambda (t-t^n)}\mu u\,\mathrm{d}t
$$

En effectuant le changement de variable $\tau = t-t^n$ on trouve une intégrale seulement sur le pas de temps $\Delta t$, ce qui est rassurant quant à la stabilité numérique en temps long.

## 6.1 Schémas exponentielles

Deux options existent pour traiter le terme de droite, la première consiste à effectuer une quadrature de l'intégrale, ce qui revient à reconstruire une méthode de type Runge-Kutta, le schéma ainsi obtenue s'appelle un schéma IFRK (pour *integrating factor Runge-Kutta*) ou Lawson ; la seconde consiste à résoudre de manière exacte l'exponentielle, en approximant $u(t) \approx u(t^n)$. On obtient alors :

$$
  e^{-\lambda\Delta t}u^{n+1} - u^n = \frac{\mu}{\lambda} u^n (1-e^{-\lambda\Delta t})
$$

soit :

$$
  u^{n+1} = e^{\lambda\Delta t}u^n + \frac{\mu}{\lambda} u^n (e^{\lambda\Delta t}-1)
$$

Pour étudier la stabilité du schéma on se propose de poser :

* $z_1 = \lambda\Delta t$
* $z_2 = \mu\Delta t$

On obtient alors l'expression suivante :

$$
  u^{n+1} = \left(e^{z_1} + \frac{z_2}{z_1}(e^{z_1}-1)\right)u^n
$$

On se propose maintenant d'étudier le module du coefficient d'amplification. En supposant que l'opérateur représenté par $z_1$ est peu visqueux, voir pas du tout dans le cas d'une méthode spectrale, c'est-à-dire que $\Re(z_1) \approx 0$, on écrit alors $z_1 = ib_1$.

$$
  R(z_1,z_2) = \left| e^{ib_1} - i\frac{z_2}{b_1}(e^{ib_1}-1) \right|
$$

En factorisant par $e^{ib_1}$, qui est de module 1, on obtient :

$$
  R(z_1,z_2) = \left| 1 + i\frac{z_2}{b_1}(e^{-ib_1} - 1) \right|
$$

On écrit maintenant $z_2 = a_2 + ib_2$ avec $a_2,b_2\in\mathbb{R}$, le module peut alors s'exprimer explicitement :

$$
  R(z_1,z_2)^2 = \left| 1 + i\frac{a_2+ib_2}{b_1}(\cos(b_1) - i\sin(b_1) - 1) \right|^2
$$

```python
b_1,a_2,b_2 = sp.symbols("b_1 a_2 b_2",real=True)
z = sp.symbols("z",complex=True)
R = sp.Abs(sp.exp(I*b_1) - I*z/b_1*(sp.exp(I*b_1)-1))

save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [7,7]
for i,n in enumerate([1,5,10,15,20,25,30,35,40,50,55]):
  contour( R.subs(b_1,n/10.) , z , (-5,15,100) , (-13,7,100) , levels=[1.] , colors=palette[i%len(palette)] )

plt.show()
plt.rcParams['figure.figsize'] = save_figsize
```

On remarque que le domaine de stabilité évolue en fonction de la valeur de $\lambda = i\texttt{b1}$, ce qui peut paraître normal puisque l'on fait *rentrer* $e^{-\lambda t}$ dans la dérivée temporelle.

Nicolas a poussé l'étude des schémas Runge-Kuta exponentiel aux ordres plus élevés, et on obtient des domaines de stabilités très étranges et surtout asymétriques. C'est l’asymétrie qui nous pose le plus de problème pour l'obtention d'une CFL optimal avec un schéma spatial d'ordre élevé (style WENO).

## 6.2 Schémas de Lawson

Plaçons nous dans un cas plus général que le cas d'opérateurs linéarisés, considérons l'équation suivante :

$$
  \partial_t u = Lu + N(u)
$$

avec $L$ un opérateur linéaire et $N$ un opérateur non-linéaire. On peut écrire une formulation exponentielle de la forme :

$$
  \partial_t \left(e^{-L t}u\right) = e^{-L t}N(u)
$$

Notons $v=e^{-\lambda t}u$, et considérons alors l'opérateur non linéaire $(v,t)\mapsto\tilde{N}(v,t) = e^{-L t}N(e^{L t}v)$ ; l'équation devient, avec ces nouvelles notations :

$$
  \dot{v} = \tilde{N}(v,t)
$$

Équation que l'on peut résoudre à l'aide d'un schéma RK classique, ici le schéma RK SSP(3,3) aussi dit de Shu-Osher :

$$
  \begin{aligned}
    v^{(1)} &= v^n + \Delta t \tilde{N}(v^n,t^n) \\
    v^{(2)} &= \frac{3}{4}v^n + \frac{1}{4}v^{(1)} + \frac{1}{4}\Delta t \tilde{N}(v^{(1)},t^n+\Delta t) \\
    v^{n+1} &= \frac{1}{3}v^n + \frac{2}{3}v^{(2)} + \frac{2}{3}\Delta t \tilde{N}(v^{(2)},t^n+\frac{1}{2}\Delta t) \\
  \end{aligned}
$$

Ce qui nous donne en fonction de $u$ :

$$
  \begin{aligned}
    v^{(1)} &= e^{-L t^n}u^n + \Delta t e^{-L t^n}N(u^n) \\
    v^{(2)} &= \frac{3}{4}e^{-L t^n}u^n + \frac{1}{2}v^{(1)} + \frac{1}{4}\Delta t e^{-Lt^n}e^{-L\Delta t}N(e^{Lt^n}e^{L\Delta t}v^{(1)}) \\
    e^{-L t^n}e^{-L\Delta t}u^{n+1} &= \frac{1}{3}e^{-L t^n}u^n + \frac{2}{3}v^{(2)} + \frac{2}{3}\Delta t e^{-Lt^n}e^{-\frac{1}{2}L\Delta t}N(e^{Lt^n}e^{\frac{1}{2}L\Delta t}v^{(2)})
  \end{aligned}
$$

Pour des raisons purement numériques liées à la précision machine, simplifions dès que possible par $e^{-Lt^n}$ pour s'assurer que le schéma ne dégénère pas en temps long :

$$
  \begin{aligned}
    \tilde{u}^{(1)} &= u^n + \Delta t N(u^n) \\
    \tilde{u}^{(2)} &= \frac{3}{4}u^n + \frac{1}{2}\tilde{u}^{(1)} + \frac{1}{4}\Delta t e^{-L\Delta t}N(e^{L\Delta t}\tilde{u}^{(1)}) \\
    u^{n+1} &= \frac{1}{3}e^{L\Delta t}u^n + \frac{2}{3}e^{L\Delta t}\tilde{u}^{(2)} + \frac{2}{3}\Delta t e^{\frac{1}{2}L\Delta t}N(e^{\frac{1}{2}L\Delta t}\tilde{u}^{(2)})
  \end{aligned}
$$

Les coefficients exponentiels à l'intérieur de l'opérateur non-linéaire nous incitent à poser :

* $u^{(1)} = e^{L\Delta t}\tilde{u}^{(1)}$
* $u^{(2)} = e^{\frac{1}{2}L\Delta t}\tilde{u}^{(2)}$

D'où le schéma suivant :

$$
  \begin{aligned}
    u^{(1)} &= e^{L\Delta t}u^n + \Delta t e^{L\Delta t} N(u^n) \\
    u^{(2)} &= \frac{3}{4}e^{\frac{1}{2}L\Delta t}u^n + \frac{1}{4}e^{-\frac{1}{2}L\Delta t}u^{(1)} + \frac{1}{4}\Delta t e^{-\frac{1}{2}L\Delta t}N(u^{(1)}) \\
    u^{n+1} &= \frac{1}{3}e^{L\Delta t}u^n + \frac{2}{3}e^{\frac{1}{2}L\Delta t}u^{(2)} + \frac{2}{3}\Delta t e^{\frac{1}{2}L\Delta t}N(u^{(2)})
  \end{aligned}
$$

On retrouve ainsi le schéma proposé dans [Isherwood L. et al (2018)](https://github.com/Kivvix/miMaS/blob/master/bibliography/pdf/2018-Isherwood(1).pdf).

Intéressons nous maintenant au domaine de stabilité d'une telle méthode, toujours dans l'approximation que $L=\lambda=ib_1$. On posera $z=\mu \Delta t$.

```python
mu = sp.symbols("\\mu")
def N (u) :
  return mu*u

u1 = sp.exp(lamb*dt)*un + dt*sp.exp(lamb*dt)*N(un)
u2 = sp.Rational(3,4)*sp.exp(sp.Rational(1,2)*lamb*dt)*un + sp.Rational(1,4)*sp.exp(-sp.Rational(1,2)*lamb*dt)*u1 + sp.Rational(1,4)*dt*sp.exp(-sp.Rational(1,2)*lamb*dt)*N(u1)
un1 = sp.Rational(1,3)*sp.exp(lamb*dt)*un + sp.Rational(2,3)*sp.exp(sp.Rational(1,2)*lamb*dt)*u2 + sp.Rational(2,3)*dt*sp.exp(sp.Rational(1,2)*lamb*dt)*N(u2)

un1.subs(mu*dt,z).expand().collect(un).collect(z).collect(sp.exp(dt*lamb))
```

On retrouve le polynôme caractéristique d'un schéma RK SSP(3,3) classique multiplié par $e^{\lambda\Delta t}$. Par conséquent pour un $\lambda$ imaginaire pur, le module de son exponentiel est égal à 1, donc le domaine de stabilité reste inchangé. On remarque que le cas général $\lambda\in\mathbb{C}$ ou le cas particulier $\lambda\in\mathbb{R}$ (intéressant pour la prise en compte d'un terme de collision BGK par exemple) est plus délicat car le module du polynôme est alors multiplié par $e^{\Delta t \Re(\lambda)}$, qui dépend de $\Delta t$, il ne s'agit donc pas d'une simple homothétie du domaine de stabilité.

Le cas $\lambda\in i\mathbb{R}$ est celui qui nous intéressera le plus par la suite, à cause de l'utilisation de méthode spectrale dans une direction et d'une méthode WENO dans l'autre. Ce cas ne changeant pas le domaine de stabilité, il n'est pas nécessaire d'effectuer de nouvelle étude sur la stabilité d'une telle méthode.

Il est possible d'automatiser la méthode d'obtention du schéma de Lawson (ou IFRK) pour la résolution de l'équation :

$$
  \partial_t u = Lu + N(u)
$$

à partir d'un tableau de Butcher de la méthode RK associée. La méthode algorithmique mise en place consiste à :

1. Écrire le schéma RK de base en $v=e^{-Lt}u$ et $\tilde{N}(t,v)=e^{-Lt}N(e^{Lt}v)$, on obtient ainsi une méthode de résolution du problème :

    $$
      \partial_t v = \tilde{N}(t,v)
    $$

2. Substituer :
    - $v^n \mapsto e^{-Lt^n}u^n$
    - $v^{n+1} \mapsto e^{-L(t^n+\Delta t)}u^{n+1}$
    - $v^{(i)} \mapsto e^{-L(t^n+b_i\Delta t)}u^{(i)}$

Il est possible d'effectuer ce même travail à partir de l'écriture sous forme de schéma, mais il est plus compliqué d'un point de vue informatique de gérer des expressions plutôt que des tableaux. Il s'agit donc uniquement d'une contrainte technique.

> On remarquera que le schéma ainsi obtenu n'est pas optimal dans le sens où il y a plus que $s$ appels à la fonction non linéaire $N$, chose évitée souvent dans l'écriture sous forme de schéma, comme c'est le cas pour la méthode RK3 de Shu-Osher. Il est donc possible de substituer récurisvement, grâce aux équations précédentes, les valeurs des différentes évaluations de la fonction $N$.

```python
L = sp.symbols("L",real=True)
N = sp.Function("N",nargs=1)
tn = sp.symbols("t^n",real=True)

#Nt = sp.Function("\\tilde{N}",nargs=2)
def Nt(t,v):
  """ `Nt` for N tilde, just for `ifrk` scheme """
  return sp.exp(-L*t)*N(sp.exp(L*t)*v)

def ifrk ( A , b , c , shu_osher=False ):
  """
    return Lawson, or IFRK scheme of table of Butcher `A`, `b`, `c`. `shu_osher` boolean parameter is
    to optimize callee of non-linear function $N$
  """
  vs = name_stages("v",len(c))
  us = name_stages("u",len(c))

  # write scheme with $\tilde{N}(t,v) = e^{-Lt}N(e^{Lt}v)$ and $v=e^{-Lt}u$
  scheme_stages = []
  r = range(len(c))
  u_s = [ 0 for i in r ]
  for i in r:
    u_si = vs[0] + dt*sum([ A[i][j]*Nt(tn+b[j]*dt,vs[j]) for j in r ])
    eq = sp.Eq(vs[i],u_si)
    # on ne prend en considération les étapes différentes de u^0 = u^n.
    if eq != True :
      scheme_stages.append(eq)
  un1 = vs[0] + sum([ dt*c[i]*Nt(tn+b[i]*dt,vs[i]) for i in r ])
  scheme_stages.append(sp.Eq(sp.symbols("v^{n+1}"),un1))

  # substitut all occurences of $v$ by $u$
  vs_us = dict([ (v,u) for (v,u) in zip(vs,us) ])
  vs_usexp = dict([ (v,u*sp.exp(-L*(tn+bs*dt))) for (v,u,bs) in zip(vs,us,b) ])
  vs_usexp[vs[-1]] = us[-1]*sp.exp(-L*(tn+dt))

  tmp = [ sp.Eq(us,(sp.solve(eq.subs(vs_usexp),us)[0]).simplify().expand()) for (eq,bs,us) in zip(scheme_stages,b,us[1:]) ]

  # simplification by substitution to evaluate only s times N
  if shu_osher:
    N_subs = []
    for i,eq in enumerate(tmp[:-1]):
      tmp[i] = eq.subs(N_subs).simplify().expand()
      N_subs.append((N(us[i]),sp.solve(tmp[i],N(us[i]))[0]))
    tmp[-1] = tmp[-1].subs(N_subs).expand()

  return tmp

Butcher.ifrk = lambda self,shu_osher=False:ifrk(self.A,self.b,self.c,shu_osher)
```

```python
[ display(eq) for eq in rk4.ifrk() ];
```

```python
[ display(eq) for eq in dp5.ifrk(shu_osher=True) ];
```

```python
[ display(eq) for eq in rk86.ifrk(shu_osher=True) ];
```

```python
fig = plt.figure()
contourf(sp.Abs(sp.exp(-z)*dp5.poly()),z,(-3,3,500),(-3,3,500),levels=[0,1.],colors=palette[0],label="$\mathcal{A}_-$")
plt.close(fig)
```

De manière plus générale, pour une méthode ERK à 4 étages, voici le schéma de Lawson associé.

```python
n=3
us = name_stages("u",n)
A = [ [ sp.symbols("a_{%i%i}"%(i,j)) if j<i else 0 for j in range(n) ] for i in range(n) ]
b = sp.symbols('b:{}'.format(n))
c = sp.symbols('c:{}'.format(n))
[ display(sp.Eq(eq.lhs,eq.rhs.collect(us))) for eq in ifrk(A=A,b=b,c=c,shu_osher=True) ];
```

# 7. Schéma *diagonal implicit Runge-Kutta*

Nous n'avons étudier jusqu'à présent que des schémas de type *Runge-Kutta* explicite (ou ERK), et cela se traduit par un tableau de Butcher triangulaire strictement inférieur. Le cas général d'un tableau de Butcher plein est envisageable, le schéma est alors implicite, ce qui permet souvent d'améliorer sa stabilité au détriment du coût de calcul. Le compromis de DIRK est de ne rendre que la diagonale implicite. Nous allons étudier ici ce genre de schéma.

```python
def poly_dirk( A , c , b=0 ):
  r = range(len(c))
  u_s = [ 0 for i in r ]
  for i in r:
    u_si = un + dt*sum([ A[i][j]*lamb*sp.symbols("u^{}".format(j)) for j in r ])
    for j in range(i) :
      u_si = u_si.subs(sp.symbols("u^{}".format(j)),u_s[j])
    ui = sp.symbols("u^{}".format(i))
    eq = sp.Eq(ui,u_si)
    u_s[i] = sp.solve(eq,ui)[0]
  un1 = un + dt*sum([ c[j]*lamb*u_s[j] for j in r ])
  return un1.subs(sp.symbols("u^0"),u_s[0]).expand().subs(lamb*dt,z).subs(un,1)

def to_gnuplot ( R , z , dic=None ):
  x,y = sp.symbols("x y",real=True)
  r = R.subs(z,x+I*y)
  if dic :
    r = r.subs(dic)
  return str(r)
```

Commençons par le premier schéma DIRK présenté dans [Alexander R. (1976)](https://doi.org/10.1137/0714068), dont voici le tableau de Butcher :

$$
  \begin{array}{c c | c}
    \frac{1}{2}+\frac{1}{2\sqrt{3}} & 0                               & \frac{1}{2} + \frac{1}{2\sqrt{3}} \\
    -\frac{1}{\sqrt{3}}             & \frac{1}{2}+\frac{1}{2\sqrt{3}} & \frac{1}{2} - \frac{1}{2\sqrt{3}} \\
    \hline
    \frac{1}{2}                     & \frac{1}{2} &
  \end{array}
$$

est une méthode DIRK(2,3). Le polynôme caractéristique des méthodes DIRK est une fonction rationnelle.

```python
A = [[sp.Rational(1,2)+1/(2*sp.sqrt(3)),0                                ],
     [-1/sp.sqrt(3)                    ,sp.Rational(1,2)+1/(2*sp.sqrt(3))]]
c = [ sp.Rational(1,2)                 ,sp.Rational(1,2)                  ]
R = poly_dirk(A=A,c=c).simplify().collect(z)
display(R)
print(to_gnuplot(R,z))
```

On représenter en 3D la fonction rationnelle ainsi obtenue.

Par la suite, on ne présentera que les domaines de stabilité.

```python
sp.roots(sp.Poly(sp.fraction(R)[1],z))
```

```python
save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [7,7]
contourf(sp.Abs(R),z,(-3,17,500),(-10,10,500),levels=[0,1.],colors=palette[0],label="Stability domaine of DIRK(2,3)")
plt.plot(*reim(-weno.data),color=palette[1],label="WENO RK6")
plt.legend(loc=(0.,1.02),ncol=3,mode="expand",borderaxespad=0.,markerscale=8)
plt.show()
plt.rcParams['figure.figsize'] = save_figsize
```

```python
save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [5,5]
contourf(sp.Abs(sp.exp(-z)*R),z,(-3,3,500),(-3,3,500),levels=[0,1.],colors=palette[0],label="$\mathcal{A}_-$")
plt.legend(loc=(0.,1.02),ncol=3,mode="expand",borderaxespad=0.,markerscale=8)
plt.show()
plt.rcParams['figure.figsize'] = save_figsize
```

Posons $\alpha = \frac{2\cos\left(\frac{\pi}{18}\right)}{\sqrt{3}}$ :

$$
  \begin{array}{c c c | c}
  \frac{1+\alpha}{2}  & 0                     & 0                   & \frac{1+\alpha}{2} \\
  -\frac{\alpha}{2}   & \frac{1+\alpha}{2}    & 0                   & \frac{1}{2}        \\
  1+\alpha            & -(1+2\alpha)          & \frac{1+\alpha}{2}  & \frac{1-\alpha}{2} \\
  \hline
  \frac{1}{6\alpha^2} & 1-\frac{1}{3\alpha^2} & \frac{1}{6\alpha^2} &
  \end{array}
$$

est une méthode DIRK(3,4).

```python
alpha = 2*sp.cos(sp.pi/18)/sp.sqrt(3)
A = [[(1+alpha)/2   ,0               ,0            ],
     [-alpha/2      ,(1+alpha)/2     ,0            ],
     [1+alpha       ,-(1+2*alpha)    ,(1+alpha)/2  ]]
c = [ 1/(6*alpha**2),1-1/(3*alpha**2),1/(6*alpha**2)]
R = poly_dirk(A=A,c=c)

R = R.subs(alpha,alpha.evalf()).collect(z)
save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [7,7]
contourf(sp.Abs(R),z,(-3,17,200),(-10,10,200),levels=[0,1.],colors=palette[0],label="Stability domaine of DIRK(3,4)")
plt.plot(*reim(-weno.data),color=palette[1],label="WENO RK6")
plt.legend(loc=(0.,1.02),ncol=3,mode="expand",borderaxespad=0.,markerscale=8)
plt.show()
plt.rcParams['figure.figsize'] = save_figsize
```

```python
save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [7,7]
contourf(sp.Abs(R),z,(-1.3,1.7,100),(-1.5,1.5,100),levels=[0,1.],colors=palette[0],label="Stability domaine of DIRK(3,4)")
plt.plot(*reim(-weno.data),color=palette[1],label="WENO RK6")
plt.legend(loc=(0.,1.02),ncol=3,mode="expand",borderaxespad=0.,markerscale=8)
plt.show()
plt.rcParams['figure.figsize'] = save_figsize
```

```python
save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [5,5]
contourf(sp.Abs(sp.exp(-z)*R),z,(-3,3,100),(-3,3,100),levels=[0,1.],colors=palette[0],label="$\mathcal{A}_-$")
plt.legend(loc=(0.,1.02),ncol=3,mode="expand",borderaxespad=0.,markerscale=8)
plt.show()
plt.rcParams['figure.figsize'] = save_figsize
```

Maintenant deux méthodes *strong S-stable* d'ordre 2 à 2 étages.

$$
  \begin{array}{c c  | c }
    \alpha   & 0      & \alpha \\
    1-\alpha & \alpha & 1 \\
    \hline
    1-\alpha & \alpha &
  \end{array}
$$

avec $\alpha = 1\pm\frac{1}{2}\sqrt{2}$. Pour la démonstration nous ne tracerons que le cas $\alpha = 1+\frac{1}{2}\sqrt{2}$, pour l'autre cas on indiquera uniquement que le cercle d'instabilité est plus large.

```python
alpha = 1+sp.Rational(1,2)*sp.sqrt(2)
A = [[alpha  ,0    ],
     [1-alpha,alpha]]
c = [ 1-alpha,alpha ]
R = poly_dirk(A=A,c=c).collect(z)

save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [7,7]
contourf(sp.Abs(R),z,(-1.3,1.7,100),(-1.5,1.5,100),levels=[0,1.],colors=palette[0],label="Stability domaine of DIRK(2,2) S-stable")
plt.plot(*reim(-weno.data),color=palette[1],label="WENO RK6")
plt.legend(loc=(0.,1.02),ncol=3,mode="expand",borderaxespad=0.,markerscale=8)
plt.show()
plt.rcParams['figure.figsize'] = save_figsize
```

```python
save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [5,5]
contourf(sp.Abs(R*sp.exp(-z)),z,(-1.5,1.5,100),(-1.5,1.5,100),levels=[0,1.],colors=palette[0],label="$\mathcal{A}_-$")
plt.legend(loc=(0.,1.02),ncol=3,mode="expand",borderaxespad=0.,markerscale=8)
plt.show()
plt.rcParams['figure.figsize'] = save_figsize
```

Maintenant une méthode *strong S-stable* d'ordre 3 à 3 étages :

$$
  \begin{array}{c c c | c}
    \alpha        & 0      & 0      & \alpha \\
    \tau_2-\alpha & \alpha & 0      & \tau_2 \\
    b_1           & b_2    & \alpha & 1      \\
    \hline
    b_1           & b_2    & \alpha &
  \end{array}
$$

avec $\alpha$ la racine de $x^3-3x+\frac{3}{2}x-\frac{1}{6}=0$ vivant dans $[\frac{1}{6},\frac{1}{2}]$, $\alpha \approx 0.43586652$

* $\tau = \frac{1+\alpha}{2}$
* $b_1 = -\frac{6\alpha^2-16\alpha+1}{4}$
* $b_2 = \frac{6\alpha^2-20\alpha+5}{4}$

```python
x = sp.symbols("x",real=True)
p = x**3 - 3*x**2 + sp.Rational(3,2)*x - sp.Rational(1,6)
subset = sp.solve(sp.Poly(x**3-3*x**2+sp.Rational(3,2)*x-sp.Rational(1,6)),x)
alpha = sp.re(subset[0].evalf())
tau2 = (1+alpha)/2
b1 = -(6*alpha**2-16*alpha+1)/4
b2 = (6*alpha**2-20*alpha+5)/4

A = [[alpha     ,0    ,0    ],
     [tau2-alpha,alpha,0    ],
     [b1        ,b2   ,alpha]]
c = [ b1        ,b2   ,alpha ]
R = poly_dirk(A=A,c=c)

save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [7,7]
contourf(sp.Abs(R),z,(-3,10,400),(-6.5,6.5,400),levels=[0,1.],colors=palette[0],label="Stability domaine of DIRK(3,3) S-stable")
plt.plot(*reim(-weno.data),color=palette[1],label="WENO RK6")
plt.legend(loc=(0.,1.02),ncol=3,mode="expand",borderaxespad=0.,markerscale=8)
plt.show()
plt.rcParams['figure.figsize'] = save_figsize
```

```python
save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [5,5]
contourf(sp.Abs(R*sp.exp(-z)),z,(-5,9,100),(-7,7,100),levels=[0,1.],colors=palette[0],label="$\mathcal{A}_-$")
plt.legend(loc=(0.,1.02),ncol=3,mode="expand",borderaxespad=0.,markerscale=8)
plt.show()
plt.rcParams['figure.figsize'] = save_figsize
```

Un autre test issu de [numipedia](http://ketch.github.io/numipedia/methods/SSPIRK33.html), où Ketcheson présente la méthode SSPIRK(3,3). Il est à noté que je ne me suis jamais intéressé au caractère SSP d'un schéma, donc je n'ai pas lu ou vérifié cet aspect sur les précédentes méthodes DIRK présentées.

Le tableau de Butcher de la méthode SSPIRK(3,3) est :

$$
  \begin{array}{c c c | c}
    -\frac{\sqrt{2}}{4}+\frac{1}{2} & 0                               & 0                               & -\frac{\sqrt{2}}{4}+\frac{1}{2} \\
    \frac{\sqrt{2}}{4}              & -\frac{\sqrt{2}}{4}+\frac{1}{2} & 0                               & \frac{1}{2} \\
    \frac{\sqrt{2}}{4}              & \frac{\sqrt{2}}{4}             & -\frac{\sqrt{2}}{4}+\frac{1}{2} & \frac{\sqrt{2}}{4}+\frac{1}{2} \\
    \hline
    \frac{1}{3} & \frac{1}{3} & \frac{1}{3}  &
  \end{array}
$$

```python
""" test sur SSPIRK33 (http://ketch.github.io/numipedia/methods/SSPIRK33.html) """
A = [[-sp.sqrt(2)/4+sp.Rational(1,2) , 0                             , 0                             ],
     [ sp.sqrt(2)/4                  ,-sp.sqrt(2)/4+sp.Rational(1,2) , 0                             ],
     [ sp.sqrt(2)/4                  , sp.sqrt(2)/4                  ,-sp.sqrt(2)/4+sp.Rational(1,2) ]]
b = [-sp.sqrt(2)/4+sp.Rational(1,2),sp.Rational(1,2),sp.sqrt(2)/4+sp.Rational(1,2)]
c = [sp.Rational(1,3),sp.Rational(1,3),sp.Rational(1,3)]
R = poly_dirk(A=A,c=c)

save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [7,7]
contourf(sp.Abs(R),z,(-39,1,50),(-20,20,50),levels=[0,1.],colors=palette[0],label="Stability domaine of SSPIRK(3,3)")
plt.plot(*reim(-weno.data),color=palette[1],label="WENO RK6")
plt.legend(loc=(0.,1.02),ncol=3,mode="expand",borderaxespad=0.,markerscale=8)
plt.show()
plt.rcParams['figure.figsize'] = save_figsize
```

On remarque sur ce dernier exemple que les méthodes DIRK ne sont pas toutes inconditionnellement stables. Des schémas DIRK inconditionnellement stables sont aussi présents dans la base de données de `nodepy`.

```python
save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [5,5]
contourf(sp.Abs(R*sp.exp(-z)),z,(-5,9,100),(-7,7,100),levels=[0,1.],colors=palette[0],label="$\mathcal{A}_-$")
plt.legend(loc=(0.,1.02),ncol=3,mode="expand",borderaxespad=0.,markerscale=8)
plt.show()
plt.rcParams['figure.figsize'] = save_figsize
```

```python
plt.rcParams['figure.figsize'] = [6,4] # la valeur par défaut du matplotlib
```

```python
"""
  tentative d'expression artistique
"""

save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [7,7]

R = sp.Abs(sp.exp(I*b_1) - I*z/b_1*(sp.exp(I*b_1)-1))
for i,n in enumerate(range(1,56,5)):
  contour( R.subs(b_1,n/10.) , z , (-5,15,100) , (-13,7,100) , colors=palette[i%len(palette)] )
R = sp.Abs(sp.exp(I*b_1) - I*z/b_1*(sp.exp(-I*b_1)-1))
for i,n in enumerate(range(1,56,8)):
  contour( R.subs(b_1,n/10.) , z , (-5,15,100) , (-13,7,100) , colors=palette[i%len(palette)] )
R = sp.Abs(sp.exp(I*b_1) - I*z/b_1*(sp.exp(I*b_1)+1))
for i,n in enumerate(range(1,56,7)):
  contour( R.subs(b_1,n/10.) , z , (-5,15,100) , (-13,7,100) , colors=palette[i%len(palette)] )
plt.plot(reim(2*weno.data)[1],reim(4*wenol.data_z)[0])
plt.plot(reim(3*weno.data)[1],-np.array(reim(6*wenol.data_z)[0]),linewidth=2,color=palette[6])

plt.show()
plt.rcParams['figure.figsize'] = save_figsize
```

```python
from sympy.plotting.plot import plot_contour
plot_contour(sp.sin(a_2)*sp.sin(b_2), (a_2, -5, 5), (b_2, -5, 5))
```
save_figsize = plt.rcParams['figure.figsize']
plt.rcParams['figure.figsize'] = [10,10]

for n in range(1,31):
  print(n,end="\r")
  R = sp.Abs(poly_carac(n))
  contour(R,z,(-5,10,2000),(-12.5,12.5,2000), levels=[1.],colors=palette[n%len(palette)],label="RK{}".format(n))
plt.legend(loc=(0.,1.02),ncol=6,mode="expand",borderaxespad=0.)
plt.show()

plt.rcParams['figure.figsize'] = save_figsize

```python
np_ = 0.9; nb_ = 0.2 ; ui_ = 4.5
x,v = sp.symbols("x v")
f_init = ( np_/sp.sqrt(2*sp.pi)*sp.exp(-sp.Rational(1,2)*v**2) +
           nb_/sp.sqrt(2*sp.pi)*sp.exp(-sp.Rational(1,2)*(v-ui_)**2/sp.Rational(1,4))
         )*(1+0.01*sp.cos(0.3*x))
sp.integrate(sp.integrate(f_init,(v,-sp.oo,sp.oo))-1,x)
```

```python

```