(******************************************************************************)
(*                       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
