Binder NbViewer

Équations Lotka-Volterra avec plotly¶

Ce carnet Jupyter a pour objectif de résoudre un système d'équations différentielles ordinaire (ODE).

Sa fonctionnalité majeure est :

  • Interactivité avec des graphiques par utilisation du module plotly

Modèle¶

Également connues sous le nom d'équations prédateur-proie, elles décrivent la variation des populations de deux espèces qui interagissent par le biais de la prédation. Il s'agit d'un modèle classique pour représenter la dynamique de deux populations où $x$ représente la quantité des proies et $y$ celles des prédateurs.

$$ \left \{ \begin{array}{rcl} \dot{x} &=& \alpha x - \beta x y \\ \dot{y} &=& -\delta y + \gamma x y \end{array} \right . $$

Codage¶

Importation des modules Python nécessaires.

In [1]:
import numpy
import scipy
import plotly.subplots
import plotly.graph_objects as go

Classe qui code le système d'équations différentielles dans la méthode derivative() et qui le résous avec la méthode solutions()

In [2]:
class lotka_volterra_model():
    """Model of Lotka-Volterra."""
    
    def __init__(self, alpha=1, beta=1, delta=0.8, gamma=1):
        """Initializes model parameters."""
        self.alpha = alpha
        self.beta = beta
        self.delta = delta
        self.gamma = gamma

    def derivative(self, t, X):
        """Returns derivative values."""
        x, y = X
        x_dot = x * (self.alpha - self.beta * y)
        y_dot = y * (-self.delta + self.gamma * x)
        return (x_dot, y_dot)

    def solution(self, t_init, t_final, x_init, y_init):
        """Returns temporal solution for t in [t_init, t_final]."""
        sol = scipy.integrate.solve_ivp(
            self.derivative, 
            [t_init, t_final], 
            [x_init, y_init],
            rtol = 1.0e-7,
            atol = 1.0e-7,
        )
        return sol.t, sol.y[0], sol.y[1]

Résolution des équations pour $t \in [t_{init}, t_{final}]$ avec les conditions initiales suivantes $x=2$ (proie) et $y=1$ (prédateur), et préparation de la figure pour monter $x(t)$ et $y(t)$ avec la bibliothèque Python plotly.

In [3]:
t_init, t_final = 0, 30
prey_init, pred_init = 2, 1

lvm = lotka_volterra_model(delta=0.4)
t, prey, pred = lvm.solution(t_init, t_final, prey_init, pred_init)

fig = go.Figure()
fig.update_layout(
    width=700,
    height=450,
    xaxis_title="temps",
    yaxis_title="population",
)

fig.add_trace(go.Scatter(x=t, y=prey, name="Proies"))
fig.add_trace(go.Scatter(x=t, y=pred, name="Prédateurs"))

print("les deux courbes sont tracées")
les deux courbes sont tracées

Affichage de l'évolution des proies $x$ et des prédateurs $y$. Les courbes sont interactives à l'aide de la bibliothèque JavaScript plotly.js sous-jacente. Sans code supplémentaire, l'utilisateur peut accéder aux données en promenant simplement le pointeur de la souris le long des courbes.

In [4]:
fig.show()

Construction d'une nouvelle figure composée de deux sous figures, l'évolution temporelle de $x(t)$ (proie) et $y(t)$ (prédateur), et le lieu du point $P(x, y)$ dans le repère $xOy$ (phase).

In [5]:
t_init, t_final = 0, 30
prey_init, pred_init = 2, 1

prey_max, pred_max = 0, 0

fig = plotly.subplots.make_subplots(
    rows=2,
    cols=1,
    subplot_titles=("Evolution temporelle", "Phase"),
    # vertical_spacing=0.15,
)

lvm = lotka_volterra_model(delta=1.0)
t, prey, pred = lvm.solution(t_init, t_final, prey_init, pred_init)
fig.add_trace(go.Scatter(x=t, y=prey, name="Proies"), row=1, col=1)
fig.add_trace(go.Scatter(x=t, y=pred, name="Prédateurs"), row=1, col=1)
fig.add_trace(go.Scatter(x=prey, y=pred, showlegend=False), row=2, col=1)
fig.update_layout(height=550, width=700)

type(fig)
Out[5]:
plotly.graph_objs._figure.Figure

Les courbes d'évolution temporelle de $x$ (proie), $y$ (prédateur) et le lieu du point $P(x,y)$ (phase) sont calculées pour plusieurs valeurs du paramètre $\delta \in \{0.2, 0.25, 0.3, 0.35 \dots 1.2\}$ et stockées dans la liste steps.

In [6]:
delta = numpy.arange(0.2, 1.25, 0.05)
steps = list()

for d in delta:
    lvm = lotka_volterra_model(delta=d)
    t, prey, pred = lvm.solution(t_init, t_final, prey_init, pred_init)
    prey_max, pred_max = max(prey_max, *prey), max(pred_max, *pred)
    step = dict(
        method="update",
        label = f"{d:.2f}", 
        args=[{"x": [t, t, prey], "y": [prey, pred, pred]}],
    )
    steps.append(step)

popul_max = max(prey_max, pred_max)

La taille des sous-figures est adaptée aux valeurs maximales des données précalculées.

In [7]:
fig.update_xaxes(title_text="temps", row=1, col=1)
fig.update_yaxes(title_text="Population", range=[0, popul_max], row=1, col=1)
fig.update_xaxes(title_text="Proie", range=[0, prey_max], row=2, col=1)
fig.update_yaxes(title_text="Prédateur", range=[0, pred_max], row=2, col=1)

print("Valeurs maximales")
print("  proie x =", prey_max)
print("  prédateur y=", pred_max)
Valeurs maximales
  proie x = 2.0
  prédateur y= 3.6281392945583772

Un curseur est ajouté à la figure pour sélectionner les courbes précalculées à afficher. L'interactivité de la figure avec l'utilisateur est accrue, il n'y a cependant pas d'interaction avec le noyau d'exécution Python, seules des données déjà calculées peuvent s'afficher.

In [8]:
sliders = [dict(currentvalue={'prefix': 'delta = '}, steps=steps)]

fig.update_layout(sliders=sliders)

print("curseur ajouté")
curseur ajouté

L'interactivité de la figure avec l'utilisateur est accrue, il n'y a cependant pas d'interaction avec le noyau d'exécution Python, seules des données déjà calculées peuvent s'afficher.

In [9]:
fig.show()