Résolution d'équations différentielles
Cours · Python (école d'ingénieur), chapitre 13 · école d'ingénieur, informatique · le début du chapitre, la suite sur Adloun
L'évolution temporelle de nombreux systèmes physiques, biologiques, ou chimiques est régie par des équations différentielles. S'il est parfois possible de trouver des solutions analytiques (comme la décharge simple d'un condensateur dans un circuit ), la grande majorité des modèles réels (tels que le mouvement gravitationnel à trois corps ou le modèle épidémiologique SIR) ne possède aucune solution exacte sous forme de fonctions usuelles. Nous devons alors recourir aux schémas de résolution numérique. Ce chapitre présente les méthodes d'intégration à un pas pour les problèmes de Cauchy (méthodes d'Euler explicite et implicite, méthodes de Runge-Kutta d'ordres 2 et 4) et montre comment modéliser et simuler des systèmes d'équations différentielles d'ordres supérieurs en Python.
13.1 Problème de Cauchy et Champ de Vecteurs
On s'intéresse à la résolution du problème de Cauchy suivant sur un intervalle :
où est une fonction continue de deux variables et est la condition initiale.
13.1.1 Visualisation : Champ de Vecteurs (Direction Field)
Graphiquement, l'équation attribue à chaque point un vecteur de pente . Une solution est une courbe tangente en tout point à ce champ de vecteurs.
import numpy as np
import matplotlib.pyplot as plt
def champ_vecteurs(f, t_range, x_range, norm=True):
"""Affiche le champ de vecteurs de l'équation x' = f(x, t)."""
t = np.linspace(t_range[0], t_range[1], 20)
x = np.linspace(x_range[0], x_range[1], 20)
T, X = np.meshgrid(t, x)
# Composantes du champ (dt=1, dx=f(x,t))
U = np.ones_like(T)
V = f(X, T)
if norm:
N = np.sqrt(U**2 + V**2)
U, V = U/N, V/N
plt.quiver(T, X, U, V, color="gray", alpha=0.7)
plt.xlabel("t")
plt.ylabel("x")
plt.title("Champ de vecteurs")
plt.grid(True, alpha=0.3)
# Exemple : x' = -x/tau (décroissance exponentielle)
tau = 2.0
f = lambda x, t: -x / tau
champ_vecteurs(f, (0, 10), (-2, 2))
plt.show()
13.2 Méthodes d'Euler
Pour approcher les valeurs de sur une subdivision régulière de pas définie par , nous pouvons intégrer l'équation entre et :
13.2.1 Méthode d'Euler Explicite
En approchant l'intégrale par le rectangle à gauche (valeur en ), on obtient le schéma explicite :
Lorsque la fonction est de classe , le schéma d'Euler explicite est consistant d'ordre 1.
def euler_explicite(f, x0, t0, tf, n):
"""Méthode d'Euler explicite."""
h = (tf - t0) / n
t = np.linspace(t0, tf, n+1)
x = np.zeros(n+1)
x[0] = x0
for k in range(n):
x[k+1] = x[k] + h * f(x[k], t[k])
return t, x
13.2.2 Méthode d'Euler Implicite
En utilisant le rectangle à droite (valeur en ), on obtient le schéma implicite :
Pour une équation non linéaire, on doit résoudre une équation à chaque pas. Pour l'équation linéaire , la solution implicite s'écrit :
def euler_implicite_linear(f, x0, t0, tf, n, lambd):
"""Euler implicite pour x' = -lambda x."""
h = (tf - t0) / n
t = np.linspace(t0, tf, n+1)
x = np.zeros(n+1)
x[0] = x0
for k in range(n):
x[k+1] = x[k] / (1 + lambd * h)
return t, x
13.2.3 Stabilité des méthodes d'Euler
Pour l'équation test (), la solution exacte décroît exponentiellement vers 0.
- Euler explicite : . Stabilité si , soit .
- Euler implicite : . Toujours stable ( pour tout ).
def comparer_euler(lambd=5.0, t_max=2.0):
"""Compare Euler explicite et implicite pour x' = -lambda x."""
x0 = 1.0
t_exact = np.linspace(0, t_max, 200)
x_exact = x0 * np.exp(-lambd * t_exact)
plt.figure(figsize=(10, 6))
plt.plot(t_exact, x_exact, 'k-', linewidth=2, label='Solution exacte')
for h in [0.3, 0.5, 0.7]:
n = int(t_max / h)
# Euler explicite
t_exp, x_exp = euler_explicite(lambda x, t: -lambd*x, x0, 0, t_max, n)
plt.plot(t_exp, x_exp, '--', label=f'Euler expo (h={h})')
# Euler implicite
t_imp, x_imp = euler_implicite_linear(lambda x, t: -lambd*x, x0, 0, t_max, n, lambd)
plt.plot(t_imp, x_imp, ':', label=f'Euler impli (h={h})')
plt.xlabel('t')
plt.ylabel('x(t)')
plt.title('Comparaison Euler explicite vs implicite')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
13.3 Méthodes de Runge-Kutta
Les méthodes de Runge-Kutta (RK) évaluent la fonction en plusieurs points intermédiaires pour approcher la pente moyenne avec un ordre de précision supérieur.
13.3.1 Méthode du Point Milieu (RK2 - ordre 2)
13.3.2 Méthode de Heun (RK2 alternative)
13.3.3 Méthode RK4 classique (ordre 4)
def rk4(f, x0, t0, tf, n):
"""Méthode de Runge-Kutta d'ordre 4."""
h = (tf - t0) / n
t = np.linspace(t0, tf, n+1)
x = np.zeros(n+1)
x[0] = x0
for k in range(n):
k1 = f(x[k], t[k])
k2 = f(x[k] + 0.5 * h * k1, t[k] + 0.5 * h)
k3 = f(x[k] + 0.5 * h * k2, t[k] + 0.5 * h)
k4 = f(x[k] + h * k3, t[k] + h)
x[k+1] = x[k] + (h / 6.0) * (k1 + 2.0*k2 + 2.0*k3 + k4)
return t, x
13.4 Systèmes Différentiels et Équations d'Ordre Supérieur
Pour simuler des systèmes dynamiques complexes ou des équations différentielles d'ordre , on les récrit sous la forme d'un système vectoriel de premier ordre.
13.4.1 Principe de Réduction
Soit l'équation du second ordre . En posant le vecteur d'état , sa dérivée s'écrit :
L'équation se récrit en système :
def oscillateur(X, t, gamma=0.5, omega0=2.0):
"""Système pour oscillateur harmonique amorti."""
y, v = X
return np.array([v, -gamma * v - omega0**2 * y])
def rk4_systeme(F, X0, t0, tf, n, args=()):
"""RK4 pour système vectoriel."""
h = (tf - t0) / n
t = np.linspace(t0, tf, n+1)
X = np.zeros((n+1, len(X0)))
X[0] = X0
for k in range(n):
k1 = F(X[k], t[k], *args)
k2 = F(X[k] + 0.5 * h * k1, t[k] + 0.5 * h, *args)
k3 = F(X[k] + 0.5 * h * k2, t[k] + 0.5 * h, *args)
k4 = F(X[k] + h * k3, t[k] + h, *args)
X[k+1] = X[k] + (h / 6.0) * (k1 + 2.0*k2 + 2.0*k3 + k4)
return t, X
# Simulation
t, X = rk4_systeme(oscillateur, [1.0, 0.0], 0, 20, 1000, args=(0.5, 2.0))
plt.figure(figsize=(10, 6))
plt.plot(t, X[:, 0], 'b-', label='Position y(t)')
plt.plot(t, X[:, 1], 'r--', label='Vitesse v(t)')
plt.xlabel('t')
plt.ylabel('y, v')
plt.title('Oscillateur harmonique amorti (RK4)')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
13.5 Modèle SIR (Épidémiologie)
Le modèle SIR (Susceptible-Infectious-Removed) est un classique de l'épidémiologie :
où est la proportion de susceptibles, d'infectieux, de guéris.
def sir_model(X, t, beta, gamma):
"""Modèle SIR."""
S, I, R = X
dS = -beta * S * I
dI = beta * S * I - gamma * I
dR = gamma * I
return np.array([dS, dI, dR])
# Paramètres
beta, gamma = 0.3, 0.1
S0, I0, R0 = 0.99, 0.01, 0.0
t, X = rk4_systeme(sir_model, [S0, I0, R0], 0, 100, 1000, args=(beta, gamma))
plt.figure(figsize=(10, 6))
plt.plot(t, X[:, 0], 'b-', label='Susceptibles S(t)')
plt.plot(t, X[:, 1], 'r-', label='Infectieux I(t)')
plt.plot(t, X[:, 2], 'g-', label='Guéris R(t)')
plt.xlabel('t')
plt.ylabel('Population')
plt.title('Modèle SIR')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
13.6 Utilisation de Solveurs Professionnels
La bibliothèque scipy.integrate fournit la fonction solve_ivp (Initial Value Problem) avec plusieurs méthodes intégrées.
from scipy.integrate import solve_ivp
def oscillateur_scipy(t, X, gamma, omega0):
y, v = X
return [v, -gamma * v - omega0**2 * y]
# Résolution avec solve_ivp
solution = solve_ivp(oscillateur_scipy, [0, 20], [1.0, 0.0],
args=(0.5, 2.0), method='RK45', dense_output=True)
t_scipy = np.linspace(0, 20, 500)
X_scipy = solution.sol(t_scipy)
plt.figure(figsize=(10, 6))
plt.plot(t_scipy, X_scipy[0], 'b-', label='solve_ivp (RK45)')
plt.xlabel('t')
plt.ylabel('y(t)')
plt.title('Oscillateur avec solve_ivp')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
13.7 Exercices résolus
Niveau 1 : Méthode d'Euler explicite
Soit l'équation différentielle avec .
- Donner la solution analytique.
- Implémenter la méthode d'Euler explicite avec sur .
- Comparer avec la solution exacte.
Démonstration (Solution)
- La solution analytique est .
- Implémentation et comparaison en Python :
import numpy as np
import matplotlib.pyplot as plt
# Solution analytique
def x_exact(t):
return np.exp(-2 * t)
# Euler explicite
def euler_explicite(f, x0, t0, tf, n):
h = (tf - t0) / n
t = np.linspace(t0, tf, n+1)
x = np.zeros(n+1)
x[0] = x0
for k in range(n):
x[k+1] = x[k] + h * f(x[k], t[k])
return t, x
# Paramètres
f = lambda x, t: -2 * x
x0 = 1.0
t0, tf = 0.0, 3.0
n = 30 # h = 0.1
t_euler, x_euler = euler_explicite(f, x0, t0, tf, n)
t_exact = np.linspace(t0, tf, 300)
x_exact_vals = x_exact(t_exact)
# Erreur
erreur = np.abs(x_euler[-1] - x_exact(t_euler[-1]))
print(f"Valeur exacte à t=3 : {x_exact(3):.6f}")
print(f"Euler explicite à t=3 : {x_euler[-1]:.6f}")
print(f"Erreur absolue : {erreur:.6f}")
# Visualisation
plt.figure(figsize=(10, 6))
plt.plot(t_exact, x_exact_vals, 'k-', linewidth=2, label='Solution exacte')
plt.plot(t_euler, x_euler, 'ro-', markersize=4, label='Euler explicite (h=0.1)')
plt.xlabel('t')
plt.ylabel('x(t)')
plt.title('Décroissance exponentielle')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

Résultat dans la console :
Valeur exacte à t=3 : 0.002479
Euler explicite à t=3 : 0.002478
Erreur absolue : 0.000001
L'erreur diminue avec le pas (théoriquement en ).
Pour l'équation , étudier l'évolution de l'erreur en fonction du pas .
La suite du cours est sur Adloun
Le chapitre en entier, formules et figures comprises, avec le tuteur, les fiches, les QCM et les exercices.
Lire la suite du coursEn accès libre, sans inscription.