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.
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:
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().
Calculer la hauteur yp de la piste à la position xp,
grâce à la fonction hauteur_piste().
Convertir les coordonnées (xp,yp) vers les coordonnées
(xf,yf) du pixel correspondant dans la fenêtre, via
piste_vers_fenetre().
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:
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):
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:
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.
A chaque itération de la boucle principale du programme,
effectuer les opérations suivantes (avant de dessiner le contenu
de la fenêtre):
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.
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.
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:
Définir une variable globale vitesse retenant le vecteur
vitesse \(\vec{v}\) du mobile, représenté sous la forme d’une
liste ([...]).
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.)
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.)
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
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.
Connaissant cette pente \(\alpha\), on peut maintenant construire
un vecteur unitaire \(\vec{u}\) tangent à la
piste à la position du mobile. On a ainsi:
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).
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}\).
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:
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.
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.
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:
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.
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().
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:
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:
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.)
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:
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:
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.
Dans mettre_a_jour_position(), calculer
\(\vec{g} = (0, g)\) correspondant au vecteur accélération
de la gravité.
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:
où \(\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.
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}\).
Pour tenir compte de l’ensemble des forces qui agissent sur le mobile,
il suffit donc de:
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.
Calculer l’accélération \(\vec{g}\) de la gravité.
Calculer l’accélération \(\vec{a_R}\) induite par la réaction
de la piste à cette force de gravité.
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.
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:
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.)
Placer la norme de cette accélération propre dans une nouvelle
variable globale appelée acceleration_ressentie.
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:
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?
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}\),
où \(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}\),
où \(\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:
(Remarquez que la masse du mobile n’intervient plus dans
cette expression.)
Vous pouvez implémenter ces calculs de la façon suivante:
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\).
Modifier mettre_a_jour_position() de façon à calculer
l’accélération \(\vec{a_F}\) résultant de la force de
frottement.
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: