···2233Bien que la reproductibilité de compilation ne soit pas encore atteinte, l'utilisation du gestionnaire de paquets Nix semble possible pour obtenir des garanties de reproductibilité.
4455-Les performances du _bridge_ SDK2 $arrows.lr$ Gazebo sont encore à améliorer, mais son utilisation est envisageable dans un context asynchrone, où le développement ne demande pas d'attendre les résultats de la simulation, via la pratique du CI/CD, par exemple.
55+Les performances du _bridge_ SDK2 $arrows.lr$ Gazebo sont encore à améliorer, mais son utilisation est envisageable dans un contexte asynchrone, où le développement ne demande pas d'attendre les résultats de la simulation, via la pratique du CI/CD, par exemple.
+27-29
rapport/gz-unitree.typ
···634634 ` .ddq`,
635635 $RR quad ("rad" dot "s"^(-2))$,
636636 [Angle de rotation du moteur],
637637- empty,
637637+ [#empty#footnote[Tant que nos politiques n'ont pas besoin de ces champs, le SDK semble fonctionner avec des valeurs vides] <empty-why>],
638638639639 ` .tau_est`,
640640 $RR quad ("N" dot "m")$,
641641 [Estimation du couple exercé par le moteur],
642642- empty,
642642+ [#empty@empty-why],
643643)
644644645645···647647648648Avant de démarrer un nouveau pas de simulation, la méthode `::PreUpdate` vient mettre à jour l'état du robot simulé en modifiant le _State buffer_ interne au plugin (1A). Gazebo envoie également le nouveau tick de simulation (1C) et les valeurs du capteur IMU (1D) dans leurs topics respectifs.
649649650650-Le `LowStateWriter` vient lire le _State buffer_ (1B) pour publier l'état sur le canal DDS (2, 3) qui est ensuite lu par $Pi$ (4), qui (on le suppose) possède une subscription sur `rt/lowstate`.
650650+Le `LowStateWriter` vient lire le _State buffer_ (1B) pour publier l'état sur le canal DDS (2, 3) qui est ensuite lu par $Pi$ (4), qui possède une subscription sur `rt/lowstate`, via sa propre utilisation du SDK d'Unitree.
651651652652653653#let transparent = luma(0).opacify(0%)
···710710})
711711712712713713-Ici également, `LowStateWriter` s'exécute _en parallèle_ du code de `::PreUpdate`: En effet, la création du `ChannelPublisher` démarre une boucle qui vient exécuter `LowStateWriter` périodiquement, dans un autre _thread_: on a donc aucune garantie de synchronisation entre les deux.
713713+Ici également, `LowStateWriter` s'exécute _parallèlement_ à `::PreUpdate`: En effet, on démarre une boucle qui vient exécuter `LowStateWriter` périodiquement, dans un autre _thread_: on a donc aucune garantie de synchronisation entre les deux.
714714715715-Ici, il y a en plus non pas deux, mais _cinq_ boucles indépendantes qui sont en jeux:
715715+Ici, il y a en plus non pas deux, mais _cinq_ boucles indépendantes qui sont en jeu:
716716717717- La boucle de simulation de Gazebo (fréquence d'appel de `::PreUpdate`),
718718- La boucle du `ChannelPublisher` (fréquence d'appel de `::LowStateWriter`), et
···734734/ Si la boucle IMU est plus fréquente: Certaines valeurs du capteur ne seront pas prises en compte par la politique
735735/ Si la boucle IMU est moins fréquente: $Pi$ recevra plusieurs fois le même état, ce qui sera représentatif du fait que la simulation n'a pas encore avancé.
736736737737-Pour la boucle du tick, cela a peut d'importance. En effet, $Pi$ ne dépend probablement pas du tick de simulation, ou si il en dépend, ce serait de manièr peu précise (ce serait plutôt pour savoir "depuis quand est-ce qu'on a lancé la politique", ce qui ne demande pas une précision à la milliseconde) #refneeded. On met quand même à jour le tick pour que nos messages `rt/lowstate` synthétiques se rapprochent le plus possible des vrais messages tels qu'envoyés par le robot physique.
737737+Pour la boucle du tick, cela a peu d'importance. En effet, $Pi$ ne dépend probablement pas du tick de simulation, ou si elle en dépend, on suppose que c'est une dépendance à une valeur peu précise (ce serait plutôt pour savoir "depuis quand est-ce qu'on a lancé la politique", ce qui ne demande pas une précision à la milliseconde). On met quand même à jour le tick pour que nos messages `rt/lowstate` synthétiques se rapprochent le plus possible des vrais messages, tels qu'envoyés par le robot physique.
738738739739== Désynchronisations
740740···861861862862== Amélioration des performances <perf>
863863864864-Les premiers essais affichent un facteur temps-réel#footnote[Appelé RTF @rtf (Real-Time Factor). Un RTF de 100% signifie que la simulation s'exécute à vitesse réelle, un RTF inférieur à 1 signifie que la simulation est plus lente que la vitesse simulée] autour des 10 à 15%.
864864+Les premiers essais affichent un facteur temps-réel#footnote[Appelé RTF (Real-Time Factor) @rtf. Un RTF de 100% signifie que la simulation s'exécute à vitesse réelle, un RTF inférieur à 1 signifie que la simulation est plus lente que ce qu'elle simule] autour des 10 à 15%.
865865866866#grid(
867867 columns: 2,
···886886// ```
887887888888#figure(
889889- caption: [Profiling d'une simulation avec _gz-unitree_],
889889+ caption: [Profilage de _gz-unitree_ lors d'une simulation],
890890 image("./profiler-many-ticks.png"),
891891)
892892···897897#let durations = (0.267, 0.051, 0.039, 0.142, 0.028) // total, state, tick+crc, pub state, cmd
898898#let dur = idx => [#durations.at(idx) ms]
899899#figure(
900900- caption: [Profiling d'un cycle de simulation],
900900+ caption: [Profil d'un cycle de simulation],
901901 table(
902902 columns: durations.slice(1).map(x => x / durations.at(0) * 1fr),
903903 table.cell([`::PreUpdate` #dur(0)], colspan: 4),
···909909)
910910911911912912-Plus de la moitié du temps de calcul du plugin provient de l'envoi de l'état du robot sur le canal DDS `rt/lowstate`.
912912+Plus de la moitié du temps de calcul du plugin est dû à de l'envoi de l'état du robot sur le canal DDS `rt/lowstate`.
913913914914Notons également que, même si ce cyle-là a duré 0.267 ms, la durée d'un cycle est assez variable, certains atteignent 0.8 ms.
915915···917917918918Quelques mesures ont été tentées pour réduire le temps nécéssaire à l'envoi d'un message DDS:
919919920920-/ Restreindre DDS à `localhost`: Il est possible que DDS envoie les messages en mode "broadcast", c'est-à-dire à
920920+/ Restreindre DDS à `localhost`: Il est possible que DDS envoie les messages en mode "broadcast", c'est-à-dire à tout addresse IP accessible dans un certain intervalle. En restreignant à `localhost`, on s'assure que le message n'a pas à être copié plusieurs fois.
921921/ Déplacer dans un autre thread: C'est ce qui a motivé la désynchronisation du thread "LowStateWriter" (cf @send-lowstate)
922922/ Ajuster la fréquence d'envoi: Une fois `LowStateWriter` déplacé dans un thread indépendant, on peut ajuster la fréquence d'envoi, le thread étant récurrant#footnote[Créé avec `CreateRecurrentThreadEx`]
923923···933933934934Gazebo possède une fonctionnalité d'enregistrement vidéo, ce qui s'avère utile pour partager des résultats de simulation.
935935936936-Cependant, l'enregistrement vidéo n'est pas nativement contrôlable par du code. L'idée était en effet de faire automatiquement tourner une simulation à chaque changement de la politique RL, et d'obtenir la vidéo du résultat, pour en observer l'évolution.
936936+Cependant, l'enregistrement vidéo n'est pas nativement contrôlable programmatiquement. L'idée était en effet de faire automatiquement tourner une simulation à chaque changement de la politique RL, et d'obtenir la vidéo du résultat, pour en observer l'évolution.
937937938938-Il a donc fallu développer un autre plugin, héritant de `gz::gui::Plugin` cette fois-ci. Ce plugin écoute des messages sur des topics Gazebo, `/gui/record_video/...`, et permet de démarrer et arrêter l'enregistrement, tout en indiquant le chemin vers le fichier mp4 de sortie.
938938+Il a donc fallu développer un autre plugin, héritant de `gz::gui::Plugin` cette fois-ci. Ce plugin écoute des messages sur des topics Gazebo, `/gui/record_video/`${$`start`,`stop`$}$, et permet de démarrer et arrêter l'enregistrement, tout en indiquant le chemin vers le fichier MP4 de sortie.
939939940940Au final, un script complet permettant de démarrer une simulation et l'enregistrer en MP4 ressemble à ceci
941941942942```bash
943943-# Envoyer un message Gazebo avec un argument de type String et une valeur de retour de type Booléen
943943+# Fonction pour envoyer un message Gazebo avec un argument de type String et une valeur de retour de type Booléen
944944send_to_gz() {
945945 gz service -s $1 --reqtype gz.msgs.StringMsg --reptype gz.msgs.Boolean --req "data: \"$2\""
946946}
···955955956956# Lancement de la politique RL
957957uv run policy.py & policy_pid=$!
958958-# On décide de la durée maximale de la vidéo (si la politique ne s'arrête pas d'elle même)
958958+# On décide de la durée maximale de la vidéo (si la politique ne l'arrête pas d'elle même)
959959sleep 120
960960kill $policy_pid
961961···971971== Mise en CI/CD
972972973973974974-On appelle CI/CD (pour _Continuous Integration / Continuous Delivery_) la pratique consistant à intégrer fréquemment des petits changements à un dépôt de code source commun, en lançant des tests régulièrement (partie "CI") et éventuellement déployer la base de code fréquemment (partie "CD") @cicd.
974974+On appelle CI/CD (pour _Continuous Integration / Continuous Delivery_) la pratique consistant à intégrer fréquemment des petits changements à un dépôt de code source commun, en lançant des tests régulièrement (partie "CI") et éventuellement en déployant la base de code fréquemment (partie "CD") @cicd.
975975976976-Une fois l'enregistrement vidéo rendu automatisable, si l'on veut mettre en place le lancement automatique à chaque commit du dépôt git de la politique (i.e. chaque changement de la politique), il faut crééer une description de _workflow_ (dans notre cas, un workflow _Github Actions_).
976976+Une fois l'enregistrement vidéo rendu automatisable, si l'on veut mettre en place l'enregistrement vidéo automatique à chaque changement de la politique, il faut crééer une description de _workflow_ (dans notre cas, un workflow _Github Actions_).
977977978978-Un workflow est un ensemble de commandes à exécuter dans un environnement virtualisé (qu'il s'agisse d'une machine virtuelle ou d'un simple container), ainsi que des évènements et conditions décrivant quand lancer l'exécution (par exemple, "à chaque commit sur la branche `main`"). C'est un des outils permettant de mettre en place la CI/CD.
978978+Un workflow est un ensemble de commandes à exécuter dans un environnement virtualisé#footnote[Qu'il s'agisse d'une machine virtuelle ou d'un simple container] ainsi que des évènements et conditions décrivant quand lancer l'exécution (par exemple, "à chaque commit sur la branche `main`"). C'est un des outils permettant de mettre en place la CI/CD.
979979980980=== Une image de base avec Docker
981981982982L'environnement d'exécution des workflows ne comporte pas d'installation de Gazebo. Étant donné le temps de compilation élevé, on peut "factoriser" cette étape dans une _image de base_, de laquelle on démarre pour chaque exécution du workflow, dans laquelle tout les programmes nécéssaires sont déjà installés.
983983984984-Pour cela, on part d'une image Ubuntu, dans lequelle on installe le nécéssaire: Just (pour lancer des commandes, un sorte de Makefile mais plus moderne @just), FFMpeg (pour l'encodage H.264 servant à la création du fichier vidéo), XVFB (pour émuler un serveur X, cf @simulate-x), Python (pour lancer la politique RL), et Gazebo.
984984+Pour cela, on part d'une image Ubuntu, dans lequelle on installe le nécéssaire: Just (pour lancer des commandes, un sorte de Makefile mais plus moderne @just), FFMpeg (pour l'encodage H.264 servant à la création du fichier vidéo), XVFB (pour émuler un serveur X, cf @simulate-x), Python (pour lancer la politique RL), Gazebo et gz-unitree.
985985986986```dockerfile
987987FROM ubuntu:24.04
988988989989-RUN apt update
989989+RUN apt update
990990+# Just
990991RUN apt install -y curl just sudo
991992# Python (via le gestionnaire de versions et dépendances UV)
992993COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
···10061007RUN just install
10071008```
1008100910091009-Un autre workflow, celui-là vivant dans le dépôt de gz-unitree, crée une image Docker depuis ce Dockerfile, qui est ensuite utilisable via `ghcr.io/Gepetto/gz-unitree` @gzu-ghcr.
10101010+Un autre workflow, défini cette fois-ci dans le dépôt de gz-unitree et non celui de la politique RL, crée une image Docker depuis ce Dockerfile, qui est ensuite utilisable via `ghcr.io/Gepetto/gz-unitree` @gzu-ghcr. Les commandes installant Gazebo et les outils de compilation sont écrites dans le _Justfile_, que nous lançons ici avec `RUN just setup`.
1010101110111012=== Une pipeline Github Actions
1012101310131013-Une fois cette image disponible, on peut l'utiliser dans un workflow Github:
10141014+Une fois cette image disponible, on peut l'utiliser dans un workflow Github sur le dépôt Git de la politique RL:
1014101510151016#zebraw(
10161017 numbering: false,
10171018 highlight-lines: (6, 7),
10181019 ```yaml
10191019- ...
10201020-10211020 jobs:
10221021 test:
10231022 runs-on: ubuntu-latest
···10261025 steps:
10271026 - name: Checkout repository
10281027 uses: actions/checkout@v5
10291029- - ...
10301028 ```,
10311029)
10321030···10421040 path: /tmp/result.mp4
10431041```
1044104210451045-#v(2em)
10431043+#v(0.5em)
10461044#grid(
10471045 columns: (5fr, 3fr),
10481046 gutter: 2em,
10491047 [
1050104810511049 ==== Un environnement de développement contraignant
10521052- Développer et débugger une définition de workflow peut s'avérer complexe et particulièrement chronophage: n'ayant pas d'accès interactif au serveur exécutant celui-ci, il faut envoyer ses changements au dépôt git, attendre que le workflow s'exécute entièrement, et regardé si quelque chose s'est mal passé.
10501050+ Développer et débugger une définition de workflow peut s'avérer complexe et particulièrement chronophage: n'ayant pas d'accès interactif au serveur exécutant celui-ci, il faut envoyer ses changements au dépôt git, attendre que le workflow s'exécute entièrement, et regarde si quelque chose s'est mal passé.
1053105110541052 Par exemple, si jamais des fichiers sont manquants, ou ne sont pas au chemin attendu, il faut modifier le workflow pour y rajouter des instruction listant le contenu d'un répertoire (en utilisant `ls` ou `tree`, par exemple), lancer le workflow à nouveau et regarder les logs.
10551053···1059105710601058 Les environnements de CI/CD s'apparentent plus à des serveurs qu'à des ordinateurs complets: en particulier, il n'y a pas d'interface graphique et donc pas de serveur d'affichage (_display server_).
1061105910621062- Mais Gazebo nécéssite un display server pour enregistrer une vidéo.
10601060+ Mais Gazebo a besoin d'un display server pour enregistrer une vidéo.
1063106110641064- Il convient donc de simuler un serveur d'affichage. Dans notre cas, l'environnement de CI/CD étant sous Linux, on simule un serveur X11 avec _XVFB_ @xvfb.
10621062+ Il faut donc simuler un serveur d'affichage. Dans notre cas, l'environnement de CI/CD étant sous Linux, on simule un serveur X11 avec _XVFB_ @xvfb.
1065106310661064 ],
10671065 figure(