Python - Scéance 4
Simulation et commande d’un système dynamique
Objectifs
- définir un système dynamique sous forme d’état ;
- créer une fonction
model(X, U); - définir une fonction de mesure
h(X); - intégrer un système dynamique avec Euler ;
- construire une boucle de simulation ;
- définir une fonction de guidage ;
- définir une fonction de commande ;
- stocker les résultats d’une simulation ;
- représenter les résultats ;
- comparer plusieurs simulations.
L’architecture générale d’une simulation est :
Consigne -> Guidance -> Contrôleur -> U -> Modèle -> dX -> Euler -> X -> h -> Y -> Sauvegarde
Petit mot…
Convention de notation :
les variables représentant des vecteurs ou matrices du modèle (X, U, Y) utilisent des majuscules afin de rester cohérentes avec les notations mathématiques du cours. Les noms des variables scalaires sont écrits en snake_case.
1. Le système dynamique
On considère une masse qui se déplace sur une ligne.
x
─────────────●──────────────────►
masse
L’état du système est :
\[X = \begin{bmatrix} x \\ v \end{bmatrix}\]avec :
- $x$ : position [m]
- $v$ : vitesse [m/s]
La commande est :
\[U = \begin{bmatrix} u \end{bmatrix}\]où $u$ est l’accélération [m/s²].
Le système dynamique est :
\[\dot{x} = v\] \[\dot{v} = u\]On peut donc écrire :
\[\dot{X} = \begin{bmatrix} \dot{x} \\ \dot{v} \end{bmatrix} = \begin{bmatrix} v \\ u \end{bmatrix}\]Dimensions des variables
Dans notre simulation :
\[X \in \mathbb{R}^{2 \times 1}\] \[U \in \mathbb{R}^{1 \times 1}\] \[\dot{X} \in \mathbb{R}^{2 \times 1}\]On aura donc :
X = [[x],
[v]]
U = [[u]]
dX = [[dx],
[dv]]
Il est important de conserver ces dimensions pendant toute la simulation.
2. Le modèle dynamique
Le rôle de la fonction model est de calculer la dérivée de l’état.
Elle prend en entrée :
-
X: état actuel ; -
U: commande actuelle.
Elle retourne :
-
dX: dérivée de l’état.
On veut donc construire :
U -> Modèle -> dX
Pour notre système :
\[\dot{x} = v\] \[\dot{v} = u\]def model(X, U):
x, v = X.flatten()
dx = v
dv = U[0, 0]
return np.array([[dx], [dv]])
Vérification (N’oubliez pas de tester !)
On peut tester le modèle avec :
\[x = 2, v = 3, u = 1\]On doit obtenir :
\[\dot{x} = 3, \dot{v} = 1\]Cellule Code
X = np.array([[2], [3]])
U = np.array([[1]])
dX = model(X, U)
print("X =")
print(X)
print("U =")
print(U)
print("dX =")
print(dX)
Exercice 1
Modifier les valeurs de X et U.
Vérifier que :
\[\dot{x} = v\]et :
\[\dot{v} = u\]Essayez par exemple :
X = np.array([[5], [-2]])
U = np.array([[4]])
3. La fonction de mesure
Le système possède deux états :
\[X = \begin{bmatrix} x \\ v \end{bmatrix}\]mais supposons que notre capteur mesure uniquement la position.
La sortie est donc :
\[Y = x\]On définit une fonction :
X -> h() -> Y
La fonction h permet de séparer :
- l’état interne du système ;
- les grandeurs que l’on observe ou mesure.
Cellule Code
def h(X):
return np.array([[X[0, 0]]])
X = np.array([[2], [3]])
Y = h(X)
print("X =")
print(X)
print("Y =")
print(Y)
Ici :
X = [[x],
[v]]
Y = [[x]]
Donc X est de taille (2, 1) alors que Y est de taille (1, 1).
4. Intégration d’Euler explicite
Le modèle donne une dérivée :
\[\dot{X} = f(X, U)\]Mais nous voulons connaître l’état au temps suivant.
La méthode d’Euler explicite donne :
\[X_{k+1} = X_k + \Delta t \dot{X}_k\]En Python :
X = X + dX * dt
où :
-
Xest l’état actuel ; -
dXest sa dérivée ; -
dtest le pas de temps.
Exemple
Si :
\[X = \begin{bmatrix} 2 \\ 3 \end{bmatrix} \quad \text{et} \quad \dot X = \begin{bmatrix} 3 \\ 1 \end{bmatrix}\]avec $dt = 0.1$
alors :
\[X_{k+1} = \begin{bmatrix} 2 \\ 3 \end{bmatrix} + 0.1 \begin{bmatrix} 3 \\ 1 \end{bmatrix}\]donc :
\[X_{k+1} = \begin{bmatrix} 2.3 \\ 3.1 \end{bmatrix}\]Cellule Code
X = np.array([[2], [3]])
dX = np.array([[3], [1]])
dt = 0.1
X = X + dX * dt
print(X)
5. Première boucle de simulation
Nous allons maintenant simuler le système.
On impose une accélération constante :
\[u = 1\]Le système part de :
\[x(0) = 0\] \[v(0) = 0\]On définit :
- la durée de simulation ;
- le pas de temps ;
- l’état initial ;
- la commande.
tf = 5
dt = 0.01
times = np.arange(0, tf, dt)
X = np.array([[0], [0]])
U = np.array([[1]])
Boucle de simulation
À chaque instant :
- on calcule
dXavec le modèle ; - on met à jour
Xavec la méthode d’Euler explicite.
Cellule Code
for t in times:
dX = model(X, U)
X = X + dX * dt
print(X)
6. Stockage des résultats
À la fin de la simulation, nous ne possédons que l’état final.
Pour tracer l’évolution du système, il faut conserver les états à chaque instant.
On crée :
X_vec = np.zeros((2, 0))
On ajoute ensuite chaque nouvel état comme une colonne :
X_vec = np.hstack((X_vec, X))
On obtient :
t0 t1 t2 t3 ...
x x0 x1 x2 x3
v v0 v1 v2 v3
Cellule Code
tf = 5
dt = 0.01
times = np.arange(0, tf, dt)
X = np.array([[0], [0]])
U = np.array([[1]])
X_vec = np.zeros((2, 0))
for t in times:
dX = model(X, U)
X = X + dX * dt
X_vec = np.hstack((X_vec, X))
plt.figure()
plt.plot(times, X_vec[0, :], label='Position')
plt.plot(times, X_vec[1, :], label='Vitesse')
plt.xlabel('Temps (s)')
plt.grid()
plt.legend()
plt.show()
7. Ajouter la mesure
Nous allons maintenant utiliser h(X) dans la boucle.
La chaîne devient :
X -> Modèle -> dX -> Euler -> X -> h -> Y
Nous allons également stocker Y.
Cellule Code
Y_vec = np.zeros((1, 0))
X = np.array([[0], [0]])
for t in times:
dX = model(X, U)
X = X + dX * dt
Y = h(X)
X_vec = np.hstack((X_vec, X))
Y_vec = np.hstack((Y_vec, Y))
8. Ajouter une commande
Jusqu’ici, l’accélération était imposée :
\[u = 1\]Nous voulons maintenant que le système atteigne une position souhaitée :
\[x_d = 5\]Nous avons besoin d’un contrôleur.
On choisit un contrôleur de type Proportionnel-Dérivé :
\[u = K_p(x_d-x)-K_dv\]où :
- $K_p$ contrôle l’erreur de position ;
- $K_d$ agit sur la vitesse ;
- $x_d-x$ est l’erreur de position.
L’objectif est :
\[x \rightarrow x_d\]9. Fonction de guidance
Dans une architecture de commande, on peut séparer :
Consigne
↓
Guidance
↓
Référence
↓
Contrôleur
↓
Commande
La fonction guidance calcule ici l’accélération de référence.
Elle reçoit :
- l’état
X; - la consigne
Xd.
Elle retourne :
-
ud.
Cellule Code
kp = 2
kd = 1
def guidance(X, Xd, kp, kd):
x, v = X.flatten()
ud = kp * (Xd - x) - kd * v
return ud
Dans cette fonction :
\[e = x_d-x\]puis :
\[u_d = K_p e-K_dv\]Le terme $K_p e$ pousse le système vers la consigne.
Le terme $-K_dv$ permet de tenir compte de la vitesse du système.
10. Fonction de contrôle
La fonction ctrl transforme la référence de commande ud en commande du système U.
Ici, le contrôleur est volontairement très simple :
\[U = \begin{bmatrix} u_d \end{bmatrix}\]Cellule Code
def ctrl(X, ud):
return np.array([[ud]])
11. Boucle de simulation avec commande
Nous avons maintenant toutes les fonctions nécessaires.
À chaque instant :
- définir la consigne ;
- calculer la référence avec
guidance; - calculer la commande avec
ctrl; - calculer la dynamique avec
model; - intégrer avec Euler ;
- mesurer avec
h; - stocker les résultats.
La boucle est donc :
Xd -> Guidance -> ud -> Contrôleur -> U -> Modèle -> dX -> Euler -> X -> h -> Y
Cellule Code
tf = 10
dt = 0.05
times = np.arange(0, tf, dt)
X = np.array([[0], [0]])
dX = np.array([[0], [0]])
Y = np.array([[0]])
U = np.array([[0]])
X_vec = np.zeros((2, 0))
Y_vec = np.zeros((1, 0))
U_vec = np.zeros((1, 0))
Y_d_vec = np.zeros((1, 0))
for t in times:
Xd = 5
Y_d = np.array([[Xd]])
ud = guidance(X, Xd, kp, kd)
U = ctrl(X, ud)
dX = model(X, U)
X = X + dX * dt
Y = h(X)
X_vec = np.hstack((X_vec, X))
Y_vec = np.hstack((Y_vec, Y))
U_vec = np.hstack((U_vec, U))
Y_d_vec = np.hstack((Y_d_vec, Y_d))
12. Visualisation des résultats
Nous pouvons maintenant afficher :
- la position ;
- la consigne ;
- la vitesse ;
- la commande.
Cellule Code
plt.figure(figsize=(8, 4))
plt.plot(times, Y_vec[0, :], label='Position')
plt.plot(times, Y_d_vec[0, :], '--', label='Consigne')
plt.xlabel('Temps (s)')
plt.ylabel('Position (m)')
plt.grid()
plt.legend()
plt.show()
plt.figure(figsize=(8, 4))
plt.plot(times, X_vec[1, :], label='Vitesse')
plt.xlabel('Temps (s)')
plt.ylabel('Vitesse (m/s)')
plt.grid()
plt.legend()
plt.show()
plt.figure(figsize=(8, 4))
plt.plot(times, U_vec[0, :], label='Commande', color='green')
plt.xlabel('Temps (s)')
plt.ylabel('Accélération (m/s²)')
plt.grid()
plt.legend()
plt.show()
13. Comparer plusieurs contrôleurs
Nous allons maintenant faire plusieurs simulations.
On souhaite comparer différentes valeurs de $K_p$ :
\[K_p \in \{0.5,\ 1,\ 2,\ 5\}\]Chaque simulation doit :
- repartir du même état initial ;
- utiliser une valeur différente de
kp; - être simulée sur toute la durée ;
- stocker son résultat ;
- être affichée avec les autres.
Cellule Code
kp_values = [0.5, 1, 2, 5]
results = np.zeros((len(kp_values), len(times)))
for j, kp in enumerate(kp_values):
X = np.array([[0], [0]])
for i, t in enumerate(times):
Xd = 5
ud = guidance(X, Xd, kp, kd)
U = ctrl(X, ud)
dX = model(X, U)
X = X + dX * dt
results[j, i] = X[0, 0]
plt.figure(figsize=(8, 5))
for j, kp in enumerate(kp_values):
plt.plot(times, results[j, :], label=f'kp = {kp}')
plt.axhline(5, color='black', linestyle='--', label='Consigne')
plt.xlabel('Temps (s)')
plt.ylabel('Position (m)')
plt.grid()
plt.legend()
plt.show()