Laboratoire 5

Les nouvelles notions abordées dans ce laboratoire sont les suivantes:

  • Mathématique:
    • Les opérations sur les vecteurs.
  • Physique:
    • Le mouvement à vitesse uniforme selon une courbe.
    • La force de réaction.
    • Les frottements.
  • Programmation Python:
    • Le formatage de chaînes de caractères.
    • L’affichage de textes dans Pygame.

Programme 8: Les montagnes russes

Dans ce projet, nous allons simuler le mouvement d’un mobile se deplaçant le long d’un rail qui suit une courbe donnée, comme par exemple le chariot d’une montagne russe.

L’approche que nous allons suivre est similaire à celle du programme 7: A chaque instant, la position du mobile sera mise à jour en appliquant les lois de Newton, après avoir déterminé la résultante des forces qui agissent sur lui.

Programme de base

Le point de départ est semblable à celui du programme 7. Le fragment de code suivant crée une fenêtre et la peint dans une couleur de fond:

import math
import pygame
import sys

# Constantes

JAUNEPALE = (255, 255, 192)

# Paramètres

dimensions_fenetre = (800, 600)  # en pixels
images_par_seconde = 25

# Initialisation

pygame.init()

fenetre = pygame.display.set_mode(dimensions_fenetre)
pygame.display.set_caption("Programme 8")

horloge = pygame.time.Clock()
couleur_fond = JAUNEPALE

while True:
   for evenement in pygame.event.get():
      if evenement.type == pygame.QUIT:
          pygame.quit()
          sys.exit()

   fenetre.fill(couleur_fond)
   pygame.display.flip()
   horloge.tick(images_par_seconde)

Vous pouvez copier ce programme dans un nouveau fichier prog-8.py, et vous assurer qu’il fonctionne correctement.

Affichage de la piste

L’étape suivante consiste à dessiner la piste des montagnes russes. Le profil de celles-ci va être défini par une fonction appelée \(h\) (comme « hauteur ») qui associera à chaque position \(x\) sur l’axe horizontal la hauteur \(h(x)\) par rapport au sol de la piste à cet endroit.

Afin de pouvoir effectuer des calculs de vitesse et d’accélération à l’aide d’unités standard, nous allons associer le système de coordonnées suivant à la piste:

  • L’axe \(X\) est horizontal de gauche à droite.
  • L’axe \(Y\) est vertical de bas en haut (donc dans le sens contraire du repère utilisé par Pygame).
  • L’origine du repère est située au milieu du côté inférieur de la fenêtre.
  • Sur les deux axes, les distances sont exprimées en mètres.
  • La largeur totale de la fenêtre correspond à 40 mètres.
  • Si le rapport largeur:hauteur de la fenêtre est égal à 4:3, comme c’est le cas pour le programme de base ci-dessus, la hauteur totale de la fenêtre correspond donc à 30 mètres.

Sur base de ces informations, vous pouvez dès à présent créer deux fonctions chargées de convertir les coordonnées entre les repères de la fenêtre et de la piste:

  • Une fonction fenetre_vers_piste() prenant un argument (xf, yf) représentant les coordonnées d’un point dans le repère de la fenêtre (exprimées en pixels), et retournant la conversion (xp, yp) de ces coordonnées dans le repère de la piste (exprimées en mètres).
  • Une fonction piste_vers_fenetre() effectuant la conversion réciproque, en acceptant un argument (xp, yp) représentant les coordonnées d’un point dans le repère de la piste, et en en retournant la conversion (xf, yf) vers le repère de la fenêtre.

Note: Les mécanismes à mettre en oeuvre pour ces conversions sont identiques à ceux du laboratoire 1.

Vous allez ensuite spécifier le profil de la piste, en définissant une fonction hauteur_piste(x) correspondant à la fonction \(h(x)\) discutée au début de cette section. Pour implémenter cette fonction, vous pouvez utiliser le profil représenté par le polynôme

\(h(x) = a x^4 + b x^3 + c x^2 + d x + e\),

avec les paramètres suivants:

  • \(a = 0.000165\)
  • \(b = 0\)
  • \(c = -0.055\)
  • \(d = 0\)
  • \(e = 5\)

Tous les éléments sont maintenant en place pour dessiner la piste. Le plus simple est de définir une fonction dessiner_piste() chargée d’effectuer cette opération, et d’invoquer cette fonction dans la boucle principale du programme, juste après avoir repeint le fond de la fenêtre.

Pour implémenter dessiner_piste(), il faut énumérer toutes les valeurs de la coordonnée horizontale xf des pixels de la fenêtre, et pour chacune d’entre elles:

  1. Convertir cette valeur en une coordonnée horizontale xp exprimée dans le repère de la piste, grâce à la fonction fenetre_vers_piste().
  2. Calculer la hauteur yp de la piste à la position xp, grâce à la fonction hauteur_piste().
  3. Convertir les coordonnées (xp, yp) vers les coordonnées (xf, yf) du pixel correspondant dans la fenêtre, via piste_vers_fenetre().
  4. Peindre la ligne verticale reliant (xf, yf) au bas de la fenêtre, par exemple en utilisant pygame.draw.rect() avec un rectangle dont la largeur est égale à un pixel.

Si vous avez effectué cette étape correctement, vous devriez obtenir un résultat semblable à celui-ci:

Affichage de la piste.

Affichage de la piste.

Position du mobile

Pour gérer la position du mobile, vous allez simplement définir une variable globale position_mobile retenant les coordonnées courantes de celui-ci dans le repère de la piste.

Initialement, le mobile sera placé sur la piste à l’extrême gauche de la fenêtre, en d’autres termes aux coordonnées \((-\frac{\ell}{2}, h(-\frac{\ell}{2}))\), où \(\ell\) désigne la largeur de la fenêtre exprimée en mètres.

Note: Dans le programme, ces coordonnées doivent être représentées sous la forme d’une liste ([ ... ]) et non d’un tuple (( ... )), afin de pouvoir en modifier la valeur pendant la simulation.

Pour visualiser le mobile, vous allez à présent créer une fonction dessiner_mobile() qui affiche celui-ci dans la fenêtre à sa position courante, sous la forme d’un disque rouge de 10 pixels de rayon. (N’oubliez pas d’utiliser la fonction piste_vers_fenetre() pour convertir les coordonnées du mobile.) Cette fonction dessiner_mobile() sera invoquée à chaque itération de la boucle principale, juste après avoir dessiné la piste.

A ce stade, votre affichage devrait ressembler à celui-ci (le mobile étant à la limite de la fenêtre, seule une moitié de celui-ci est visible):

Affichage du mobile.

Affichage du mobile.

Mise à jour de la position

La simulation du mouvement du mobile va être effectuée en appelant périodiquement une fonction chargée d’en mettre à jour la position.

Pour que cette simulation soit suffisamment précise, nous n’allons cependant pas réaliser cette mise à jour à chaque rafraîchissement de la fenêtre, mais beaucoup plus fréquemment. L’idée est d’appeler la fonction de mise à jour pour chaque milliseconde de temps écoulé.

La marche à suivre est la suivante:

  1. Définir une fonction mettre_a_jour_position() acceptant les arguments suivants:

    • position représentant la position courante du mobile.
    • temps_maintenant donnant l’instant présent, exprimé en secondes.

    Cette fonction doit retourner la nouvelle valeur de position, après la mise à jour. (Dans un premier temps, on peut se contenter de laisser cette position inchangée.)

    Note: Nous ajouterons plus tard d’autres arguments à cette fonction, lorsque nous prendrons en compte l’effet de la gravité et des forces de frottement.

  2. A chaque itération de la boucle principale du programme, effectuer les opérations suivantes (avant de dessiner le contenu de la fenêtre):

    1. Invoquer pygame.time.get_ticks() afin de connaître l’instant courant. Cette fonction retourne une valeur exprimée en millisecondes.

      Définir une variable temps_maintenant destinée à accueillir cette valeur, et une autre variable temps_precedent retenant la valeur de temps_maintenant lors de l’itération précédente.

    2. Effectuer une boucle faisant varier la valeur d’une variable t depuis temps_precedent jusqu’à temps_maintenant - 1, par incréments d’une milliseconde. A chaque itération de cette boucle, invoquer la fonction mettre_a_jour_position().

      Attention: La fonction mettre_a_jour_position() attend un instant courant exprimé en secondes. Il ne faut donc pas oublier de convertir temps_maintenant en secondes avant de l’utiliser comme argument de cette fonction!

      De cette manière, pour un rafraîchissement de la fenêtre effectué 25 fois par secondes, la position du mobile sera mise à jour 40 fois entre deux affichages successifs. Cette stratégie conduira à une meilleure précision de la simulation des mouvements.

Mouvement à vitesse uniforme

A ce stade, le soi-disant « mobile » n’a toujours pas bougé! L’objectif est maintenant d’adapter la fonction mettre_a_jour_position() de façon à simuler un mouvement à vitesse constante le long de la piste.

Cette opération n’est pas triviale. En effet, il ne suffit pas de simplement simuler un mouvement rectiligne uniforme, car le mobile doit suivre la courbe formée par la piste: le chariot des montagnes russes ne peut pas quitter son rail!

Si le mouvement du mobile n’est pas rectiligne uniforme, alors cela signifie que ce dernier est soumis à une force. Celle-ci correspond à la réaction opposée par le rail à l’inertie du mobile. De façon équivalente, si l’on considère que la piste est composée d’une succession d’arcs de cercle infinitésimaux, il s’agit de la force centripète qui maintient le mobile en contact avec le rail.

Nous allons à présent calculer le mouvement du mobile lorsqu’il est uniquement soumis à cette force de réaction. A une échelle très petite, le mobile se déplace donc selon une succession de mouvements circulaires uniformes. Cela signifie que la norme de son vecteur vitesse reste constante. (Cette valeur correspond à la distance que le mobile parcourt sur sa courbe par unité de temps.)

La procédure suivante permet de simuler le mouvement du mobile:

  1. Définir une variable globale vitesse retenant le vecteur vitesse \(\vec{v}\) du mobile, représenté sous la forme d’une liste ([ ... ]).

  2. Lors de la première invocation de mettre_a_jour_position(), initialiser vitesse à [0, 0].

    (Pour détecter qu’il s’agit de la première invocation, le plus simple est d’utiliser une variable booléenne globale premiere_iteration mise à jour dans cette fonction. Il faut veiller à bien déclarer cette variable à l’aide du mot-clé global dans la fonction, tout comme vitesse.)

  3. Dans mettre_a_jour_position(), s’il ne s’agit pas de la première invocation de cette fonction, calculer la norme \(v_L\) du vecteur vitesse, ainsi que le temps \(\Delta_t\) qui s’est écoulé depuis l’invocation précédente. (Rappel: Dans cette fonction, les temps sont exprimés en secondes.)

  4. Calculer également la pente \(\alpha\) de la piste à l’endroit donné par la valeur courante de position. Si \(x\) désigne la composante horizontale de position, le plus simple consiste à évaluer

    \(\alpha \approx \frac{h(x + \delta) - h(x)}{\delta}\),

    avec \(\delta > 0\) très petit (par exemple, égal à \(10^{-6}\)). Une bonne idée est d’effectuer ce calcul dans une fonction séparée.

  5. Connaissant cette pente \(\alpha\), on peut maintenant construire un vecteur unitaire \(\vec{u}\) tangent à la piste à la position du mobile. On a ainsi:

    \(\vec{u} = (\frac{1}{\sigma}, \frac{\alpha}{\sigma})\),

    \(\sigma = \sqrt{1 + \alpha^2}\).

  6. Le vecteur \(\vec{u}\) donne la direction que devra avoir le vecteur \(\vec{v}\) après la mise à jour. Il est important de veiller à ce que ce vecteur \(\vec{u}\) soit correctement orienté: si la composante horizontale de \(\vec{v}\) est négative (ce qui indique un mouvement de droite à gauche), alors \(\vec{u}\) doit être retourné (ce qui signifie que ses deux composantes doivent changer de signe).

  7. Après la mise à jour, le vecteur vitesse aura pour nouvelle valeur

    \(\vec{v}' = v_L \vec{u}\),

    Cela signifie que la norme \(v_L\) du vecteur vitesse du mobile restera inchangée, mais que ce vecteur modifiera son orientation de façon à suivre \(\vec{u}\).

  8. Puisque le vecteur vitesse a changé de valeur (en passant de \(\vec{v}\) à \(\vec{v}'\)), alors le mobile a subi une accélération. Cette accélération \(\vec{a_T}\) due au profil de la trajectoire est donnée par:

    \(\vec{a_T} \approx \frac{\vec{v}' - \vec{v}}{\Delta_t}\).

  9. On peut donc finalement modifier \(\vec{v}\):

    \(\vec{v} \leftarrow \vec{v} + \Delta_t \vec{a_T}\)

    et la position \(\vec{p}\) du mobile:

    \(\vec{p} \leftarrow \vec{p} + \Delta_t \vec{v}\).

Après avoir implémenté cette procédure dans mettre_a_jour_position(), il est indispensable de vérifier qu’elle fonctionne comme attendu. Une stratégie de test consiste à modifier légèrement le code responsable de l’initialisation de la variable vitesse: plutôt que d’effectuer cette initialisation à l’aide du vecteur nul, on peut par exemple choisir la valeur initiale \(10 \vec{u}\), où \(\vec{u}\) est calculé de la même façon qu’au point 5 de la procédure. En exécutant le programme, vous devriez alors voir le mobile suivre la courbe de la piste à la vitesse constante de 10 mètres par seconde.

Mobile en mouvement.

Mobile en mouvement.

Note: Il est normal que le mobile disparaisse de l’écran après avoir franchi le bord droit de la fenêtre, puisqu’il continue indéfiniment son mouvement le long de la courbe.

Tableau de bord

Nous allons à présent ajouter au programme un tableau de bord permettant d’afficher en temps-réel quelques grandeurs, comme la vitesse et l’accélération instantanées du mobile.

Procédure à suivre:

  1. Dans la fonction mettre_a_jour_position(), vous avez déjà calculé la norme \(v_L\) du vecteur vitesse, correspondant à la vitesse instantanée du mobile. Vous pouvez maintenant rendre cette variable globale, afin de pouvoir en consulter la valeur dans le programme principal.

  2. Dans la boucle principale du programme, après l’appel à la fonction chargée de dessiner le mobile, insérer un appel à une nouvelle fonction afficher_tableau_de_bord().

  3. Dans cette fonction, afficher la valeur de \(v_L\) à l’écran. Les mécanismes suivants vous seront utiles:

    • Pour insérer dans une chaîne de caractères une valeur numérique formatée d’une façon particulière, vous pouvez invoquer la méthode format() de cette chaîne de caractères, comme dans l’exemple ci-dessous:

      texte = "Vitesse : {0:.2f} m/s".format(vitesse_lineaire)
      

      Dans cet exemple, le marqueur {0:.2f} figurant dans la chaîne sera remplacé par la valeur de vitesse_lineaire (l’argument d’indice “0” de format), formatée comme un nombre en virgule flottante (“f”), avec deux chiffres après la virgule (“.2”).

    • On peut afficher une chaîne de caractères à l’écran en générant d’abord une image correspondant à celle-ci, et en copiant ensuite cette image à une position donnée de la fenêtre, comme le montre cet exemple:

      image = police.render(texte, True, couleur)
      fenetre.blit(image, (x, y))
      

      Avant d’exécuter ces instructions, il faut charger dans police une police de caractères. (Cette opération peut être réalisée une seule fois lors de l’initialisation du programme.) Exemple:

      police  = pygame.font.SysFont("monospace", 16)
      

      (Le premier paramètre spécifie le nom de la police, et le second sa taille en pixels.)

  4. Vous pouvez également appeler après chaque invocation de mettre_a_jour_position() une nouvelle fonction mettre_a_jour_statistiques() chargée de calculer la vitesse maximum du mobile au cours de sa trajectoire, et afficher cette vitesse maximum sur le tableau de bord. Un test du programme devrait alors confirmer que le mobile se déplace bien à vitesse constante:

    Déplacement à vitesse constante.

    Déplacement à vitesse constante.

Effet de la gravité

Nous allons maintenant simuler l’effet de la gravité sur le mobile. Pour ce faire, il ne suffit pas d’ajouter l’accélération de la gravité à celle calculée dans mettre_a_jour_position(). En effet, si nous faisions cela, le mobile tomberait rapidement en chute libre en quittant la piste. (Vous pouvez essayer!)

Lorsque la gravité exerce une force sur le mobile, la piste exerce aussi sur ce dernier une force de réaction correspondante, visant à maintenir le mobile en contact avec le rail. Si nous souhaitons obtenir une simulation réaliste, il nous faudra tenir compte de cette force de réaction.

Procédure à suivre:

  1. Ajouter à la fonction mettre_a_jour_position() un paramètre supplémentaire donnant l’accélération \(g\) de la gravité. Etant donné que nous utilisons des unités standard (les distances sont exprimées en mètres et les temps en secondes), vous pouvez lors de l’invocation de cette fonction attribuer la valeur \(g = -9.81 m/s^2\) à ce paramètre.

  2. Dans mettre_a_jour_position(), calculer \(\vec{g} = (0, g)\) correspondant au vecteur accélération de la gravité.

  3. La force de réaction de la piste sur le mobile s’exerce perpendiculairement à cette piste. On peut calculer un vecteur unitaire \(\vec{n}\) normal à la piste:

    \(\vec{n} = (-\frac{\alpha}{\sigma}, \frac{1}{\sigma})\),

    \(\sigma = \sqrt{1 + \alpha^2}\), et \(\alpha\) représente la pente de la piste à la position courante du mobile. Remarquez que l’on a \(\vec{n}.\vec{u} = 0\), ce qui montre que ces vecteurs sont bien orthogonaux.

  4. Dans la direction donnée par \(\vec{n}\), l’accélération de la gravité a pour valeur \(\vec{g}.\vec{n}\).

    L’accélération \(\vec{a_R}\) résultant de la réaction de la piste au poids du mobile s’oppose à ce poids, et vaut donc

    \(\vec{a_R} = -(\vec{g}.\vec{n}) \vec{n}\).

  5. Pour tenir compte de l’ensemble des forces qui agissent sur le mobile, il suffit donc de:

    1. Calculer l’accélération \(\vec{a_T}\) due au profil de la piste, de la même façon que dans la section précédente.

    2. Calculer l’accélération \(\vec{g}\) de la gravité.

    3. Calculer l’accélération \(\vec{a_R}\) induite par la réaction de la piste à cette force de gravité.

    4. Calculer l’accélération résultante

      \(\vec{a} = \vec{a_T} + \vec{g} + \vec{a_R}\)

      du mobile, et utiliser cette accélération pour mettre à jour la vitesse de celui-ci:

      \(\vec{v} \leftarrow \vec{v} + \Delta_t \vec{a}\)

Après avoir implémenté ces opérations, vous pouvez tester le programme en rétablissant une vitesse initiale égale au vecteur nul. Vous devriez voir osciller le mobile d’une extrémité à l’autre de la fenêtre, en atteignant une vitesse maximum de l’ordre de 13.3 m/s. Est-ce bien le cas?

Note: Si vous laissez tourner longtemps le programme, certaines imprécisions de la simulation peuvent devenir apparentes, comme une déviation progressive de la position du mobile par rapport à la piste ou une légère augmentation de l’amplitude des mouvements. Ces imprécisions peuvent être atténuées en réduisant le pas de temps utilisé pour la simulation, au prix d’une augmentation du temps de calcul nécessaire.

Accéleration ressentie

Lorsque l’on conçoit des montagnes russes, il est important de vérifier que l’accélération subie par ses passagers reste dans des limites acceptables. Nous allons maintenant calculer cette accelération en vue de l’afficher sur le tableau de bord.

Les lois de la physique nous apprennent qu’il est impossible de ressentir la seule accélération de la gravité: Une expérience placée dans un ascenseur en chute libre (qui possèdera donc une accélération de \(9.81 m/s^2\) dirigée vers le bas) donnera le même résultat qu’une expérience identique qui n’est soumise à aucune force, gravitationnelle ou autre (dont l’accélération sera dès lors nulle).

Pour mesurer l’accélération propre ressentie par un objet, on doit donc tenir compte de toutes les forces s’exerçant sur cet objet, à l’exception des forces gravitationnelles.

Procédure à suivre:

  1. Dans la fonction mettre_a_jour_position(), calculer l’accélération propre

    \(\vec{a_P} = \vec{a_T} + \vec{a_R}\)

    du mobile. (Les valeurs de \(\vec{a_T}\) et \(\vec{a_R}\) ont déjà été calculées.)

  2. Placer la norme de cette accélération propre dans une nouvelle variable globale appelée acceleration_ressentie.

  3. Modifier le tableau de bord et la fonction mettre_a_jour_statistiques(), de façon à afficher l’accélération propre ainsi que les valeurs maximum et minimum de celle-ci.

    Note: Plutôt que d’afficher ces accélérations en \(m/s^2\), il est plus judicieux de les exprimer en \(g\), un \(g\) correspondant à l’accélération de la gravité.

Vous devriez obtenir un résultat semblable à celui-ci:

Affichage de l'accélération propre.

Affichage de l’accélération propre.

Pouvez-vous interpréter les valeurs de l’accélération propre affichées pendant cette simulation? Comment expliquez-vous la faible indication observée au sommet de la colline centrale?

Forces de frottement

La dernière modification à effectuer afin d’obtenir une simulation plus ou moins réaliste est de tenir compte des forces de frottement qui s’opposent au mouvement du mobile. Dans la réalité, ces forces combinent des effets aérodynamiques et des frottements au niveau des roues du chariot. Pour notre simulation, nous considérerons simplement que le mobile est ralenti par une force de frottement dynamique de coefficient connu, obéissant à la loi de friction de Coulomb.

Principes:

  • Dans la fonction mettre_a_jour_position(), vous avez déjà calculé l’accélération propre \(\vec{a_P}\) du mobile. La résultante des forces de réaction s’exerçant sur celui-ci est donc égale à

    \(\vec{F} = m\, \vec{a_P}\),

    \(m\) est la masse du mobile.

  • Pour calculer la force de frottement, il faut d’abord obtenir la valeur de la composante de \(\vec{F}\) qui est perpendiculaire à la piste. Comme nous disposons déjà du vecteur normal \(\vec{n}\), cette composante vaut:

    \(F_{N} = m\, \vec{a_P} . \vec{n}\).

  • La force de frottement s’opposant au mouvement du mobile vaut donc

    \(- \mu_c |F_N| \vec{u}\),

    \(\mu_c\) est le coefficient de frottement. En effet:

    • La norme de cette force est égale à la composante normale de la force de réaction exercée par la piste.
    • La force de frottement est identique que cette réaction s’exerce vers le haut ou vers le bas (le chariot ne peut pas quitter le rail!), donc seule la valeur absolue de \(F_{N}\) entre en ligne de compte.
    • La direction de la force de frottement est opposée à celle du mobile, donc cette force suit le vecteur unitaire \(-\vec{u}\).
  • L’accélération résultant de la force de frottement vaut donc:

    \(\vec{a_F} = - \mu_c |\vec{a_P} . \vec{n}| \vec{u}\).

    (Remarquez que la masse du mobile n’intervient plus dans cette expression.)

Vous pouvez implémenter ces calculs de la façon suivante:

  1. Ajouter à la fonction mettre_a_jour_position() un argument supplémentaire correspondant au coefficient de frottement cinétique. Lorsque vous invoquez cette fonction dans la boucle principale, vous pouvez utiliser la valeur

    \(\mu_c = 0.03\).

  2. Modifier mettre_a_jour_position() de façon à calculer l’accélération \(\vec{a_F}\) résultant de la force de frottement.

  3. Ajouter cette accélération à l’accélération résultante employée pour mettre à jour la vitesse du mobile. On a donc à présent:

    \(\vec{a} = \vec{a_T} + \vec{g} + \vec{a_R} + \vec{a_F}\).

  4. Vérifier que le programme fonctionne correctement.

  5. Déposer votre programme dans le répertoire centralisé des laboratoires, sous le suffixe prog-8.py.