This commit is contained in:
Adrien Guatto
2025-11-10 18:12:19 +01:00
parent 88595d288f
commit 70fc7019dc
20 changed files with 916 additions and 1 deletions

View File

@@ -0,0 +1,223 @@
(******************************************************************************)
(* PENDULE INVERSÉ EN HEPTAGON *)
(******************************************************************************)
(* Le but de ce fichier est d'illustrer l'automatique par un exemple classique,
le pendule inversé. Il s'agit d'un mobile sur lequel est fixé une tringle
rigide à l'extrémité de laquelle se trouve une sphère d'une certaine
masse. Le but est de déplacer le mobile de sorte à maintenir la sphère à la
verticale.
https://en.wikipedia.org/wiki/Inverted_pendulum
Le présent fichier est conçu pour s'interfacer avec l'interface graphique en
Python contenue dans `main.py`. *)
(* On commence par quelques fonctions et noeuds utilitaires. *)
fun max(x, y : float) returns (z : float)
let
z = if x <. y then y else x;
tel
fun min(x, y : float) returns (z : float)
let
z = if x <. y then x else y;
tel
fun clamp(x, x_min, x_max : float) returns (y : float)
let
y = min(x_max, max(x_min, x));
tel
fun abs(x : float) returns (y : float)
let
y = if x >. 0.0 then x else -. x;
tel
fun ignore_below(x, eps : float) returns (y : float)
let
y = if abs(x) <. eps then 0.0 else x;
tel
node maintain(ini : float; c : bool; x : float on c) returns (y : float)
let
y = merge c x ((ini fby y) whenot c);
tel
node edge(b : bool) returns (c : bool)
let
c = b -> (b <> pre b);
tel
(* Le pendule inversé, en tant que dispositif physique, voit sa dynamique
gouvernée par des équations différentielles. Pour le simuler, nous avons
besoin de résoudre celles-ci, ou du moins d'approcher leur solution par des
moyens numériques. *)
(* La résolution d'équations différentielles par des moyens numériques constitue
un vaste pan de l'analyse numérique. Nous allons nous contenter de solutions
très simples, dites à pas fixe, où chaque instant synchrone sera supposé
correspondre à une certaine durée de temps physique `dt`. *)
const dt : float = 0.01
(* L'intégration s'effectue par la méthode des rectangles, à la manière de la
définition de l'intégrale de Riemann. Plus `dt` est petit, plus les valeurs
calculées vont être précises. *)
node integ(x_ini, dx : float) returns (x : float)
let
x = x_ini -> (dt *. dx +. pre x);
tel
(* La dérivée s'effectue en suivant la formule classique
x'(t) = lim_{dt → 0} (x(t + dt) - x(t)) / dt
sauf qu'ici on s'arrête à un `dt` fixé, qu'on espère assez petit. *)
node deriv(x : float) returns (dx : float)
let
dx = 0.0 -> ((x -. pre x) /. dt);
tel
(* Pour définir la physique du pendule, on a besoin de certaines constantes : la
constante gravitationnelle sur Terre, la longueur de la tige, et la masse. *)
const g : float = 9.81
const l : float = 100.0
const m : float = 10.0
(* Une partie de la simulation dépend du référentiel choisi, un rectangle de
`max_w` unités de côté et `max_h` unités de haut. *)
const max_w : int = 1200
const max_h : int = 300
(* L'équation différentielle ci-dessous gouverne la physique du pendule inversé.
Ici est la longueur du pendule, m sa masse, g la gravité à la surface de la
Terre, et θ l'angle par rapport à la verticale.
· d²θ/d²t - m · g · sin(θ) = - d²x₀/d²t · cos(θ)
On peut exprimer d²θ/d²t en fonction de x₀.
d²θ/dt² = (m · g · sin(θ) + d²x₀/dt² · cos(θ)) /
Ensuite, on intègre.
θ = ∫² (m · g · sin(θ) + d²x₀/dt² · cos(θ)) / · dt²
On peut maintenant approcher la solution de l'intégrale et les dérivées
numériquement, à l'aide des noeuds integ() et deriv() définis précédemment,
pour obtenir la valeur de θ en fonction de celle de x₀.
Attention : comme θ apparaît à gauche et à droite du signe égal, on doit
introduire un délai (`fby`) pour éviter une erreur de causalité. *)
node pendulum(d2x0 : float) returns (theta : float)
var thetap, d2theta : float;
let
thetap = 0.0 fby theta;
d2theta = (m *. g *. Mathext.sin(thetap) -. d2x0 *. Mathext.cos(thetap)) /. l;
theta = integ(0.0, integ(0.0, d2theta));
tel
(* Notre but est maintenant d'écrire des contrôleurs, c'est à dire des noeuds
qui vont chercher à maintenir l'angle θ à 0 en déplaçant le mobile vers la
gauche quand il est positif, et vers la droite quand il est négatif. *)
(* En pratique, on va écrire nos contrôleurs de manière générique, comme des
noeuds cherchant à calculer une _consigne_ `y` de sorte à ramener leur entrée
d'erreur `e` à 0. *)
(* Il faut commencer par calculer l'erreur à fournir aux contrôleurs, en
fonction de θ. Pour ce faire, on ramène les angles dans l'intervalle [π, 2π]
dans l'intervalle [-π, 0]. *)
fun error(theta : float) returns (err : float)
var tm : float;
let
tm = Mathext.fmod(theta, 2.0 *. Mathext.pi);
err = if tm <. Mathext.pi then tm else tm -. 2.0 *. Mathext.pi;
tel
(* On peut maintenant écrire nos contrôleurs. *)
(* Le premier contrôleur, le contrôleur bang-bang, est le plus simple. Si
l'erreur est positive, on envoie `act`, si elle est négative on envoie
`-. act`. *)
node bangbang(e, act : float) returns (y : float)
var eps : float;
let
eps = 0.001;
y = if e >. eps then act
else if e <. -. eps then -. act
else 0.0;
tel
(* Le contrôleur PID agit de façon proportionnelle à l'erreur, son intégrale et
sa dérivée. Il est paramétré par trois _gains_, c'est à dire les facteurs
qu'on applique respectivement à l'erreur, l'intégrale et la dérivée. *)
node pid(e : float; kp, ki, kd : float) returns (y : float)
let
y = kp *. e +. ki *. integ(0.0, e) +. kd *. deriv(e);
tel
(* Le noeud ci-dessous réunit tous les composants du fichier, le pendule et son
mobile, ainsi que la détection d'erreur, et envoie les données nécessaires à
l'interface graphique. Ces données sont représentées par le type `pend`. *)
type pend = { x0 : float; y0 : float; x : float; y : float;
mode_name : string; theta : float; error : float }
node cart(mouse_x0 : float; mouse_diff, mode_diff : bool) returns (p : pend)
var x_ini, y_ini, x0user, x0, x, y, theta, error : float;
last d2x0 : float = 0.0;
mouse_click, mode_change : bool;
mode_name : string;
let
mouse_click = edge(mouse_diff);
mode_change = edge(mode_diff);
x_ini = Mathext.float(max_w) /. 2.0;
y_ini = Mathext.float(max_h) /. 2.0;
x0user = maintain(x_ini, mouse_click, mouse_x0 when mouse_click);
x0 = integ(x_ini, integ(0.0, d2x0));
theta = pendulum(d2x0);
error = 0.0 fby error(theta);
x = x0 +. l *. Mathext.sin(theta);
y = y_ini +. l *. Mathext.cos(theta);
p = { x0 = x0; y0 = y_ini; x = x; y = y;
mode_name = mode_name;
theta = theta; error = error };
automaton
state User
do mode_name = "user";
d2x0 = x0user -. (x_ini fby x0);
unless mode_change then PID
state PID
var kP, kI, kD : float;
do mode_name = "PID control";
d2x0 = 10.0 *. pid(error, kP, kI, kD);
kP = 40.0;
kI = 41.0;
kD = 70.0;
unless mode_change then BangBang
state BangBang
do mode_name = "BangBang control";
d2x0 = bangbang(error, 50.0)
unless mode_change then User
end;
tel