···55== 7-11 Juillet
6677- Capteur IMU rajouté
88-- Ajout du tick (temps) de simulation
88+- Ajout du tick (temps) de simulation
99- Essais d'utilisation de gz-unitree avec les politiques RL#footnote[Reinforcement Learning] de Gepetto
10101111== 14-18 Juillet
+3-3
log/may.typ
···11== 19-23 Mai
2233-- Mise en place de l'environnement de développement
33+- Mise en place de l'environnement de développement
44- Documentation sur Nix le langage @nix-language
55-- Découverte de la description d'une dérivation @nix-derivation et d'un flake
55+- Découverte de la description d'une dérivation @nix-derivation et d'un flake
66- Découverte de l'infrastructure autour de nixpkgs (github, la CI, Hydra @hydra...)
77- Packaging en flake et CI basique (`nix build`) de `open-dynamic-robot-initiative/{interface_controls,master-board}` @odri-controls, @odri-masterboard
88- Début du travail de packaging de `open-dynamic-robot-initiative/robot_properties_solo`
99- Migration de `robot_properties_solo` vers uv @uv
1010- Début du packaging de `xacro` sur NixOS/nixpkgs @nixpkgs-xacro
1111- Création d'un JSON Schema @json-schema pour des fichiers de configuration de `robot_properties_solo` et mise en place d'une CI pour les valider @odri-properties-solo
1212-- Recherche autour d'une potentielle validation au runtime en C++ des fichiers de config par le JSON Schema
1212+- Recherche autour d'une potentielle validation au runtime en C++ des fichiers de config par le JSON Schema
1313- Découverte des overlays Nix
14141515== 26-28 Mai
···11-#import "template.typ": arkheion, arkheion-appendices
22-#show: arkheion.with(
33- title: "Stage au LAAS",
44- authors: (
55- (name: "Gwenn Le Bihan", email: "gwenn.lebihan@etu.inp-n7.fr", affiliation: "ENSEEIHT"),
66- ),
77- date: datetime.today(),
88- logo: "enseeiht.jpeg",
99- abstract: [
1010- Ce stage porte sur l'intégration de Nix et NixOS dans les processus de développement et de déploiement logiciel dans le domaine robotique au sein du LAAS. Nix, le _package manager_, et NixOS, l'OS, sont des technologies permettant une reproductibilité, une qualité importante dans le monde de la recherche.
1111-1212- J'ai été aussi amenée à travailler sur la création d'un _plugin_ pour Gazebo, un logiciel de simulation robotique, pour l'utiliser avec le _SDK_ d'un robot de Unitree.
1313- ],
1414-)
1515-1616-#outline(
1717- title: [Table des matières],
1818-)
1919-2020-#pagebreak()
2121-2222-2323-#include "biblio.typ"
2424-2525-= Journal de bord
2626-2727-#for month in ("may", "june", "july", "august", "september", "november") {
2828- include("log/" + month + ".typ")
2929-}
3030-3131-#bibliography("bib.yaml")
11+#import "template.typ": arkheion, arkheion-appendices
22+#show: arkheion.with(
33+ title: "Stage au LAAS",
44+ authors: (
55+ (
66+ name: "Gwenn Le Bihan",
77+ email: "gwenn.lebihan@etu.inp-n7.fr",
88+ affiliation: "ENSEEIHT",
99+ ),
1010+ ),
1111+ date: datetime.today(),
1212+ logo: "enseeiht.jpeg",
1313+ abstract: [
1414+ Ce stage porte sur l'intégration de Nix et NixOS dans les processus de développement et de déploiement logiciel dans le domaine robotique au sein du LAAS. Nix, le _package manager_, et NixOS, l'OS, sont des technologies permettant une reproductibilité, une qualité importante dans le monde de la recherche.
1515+1616+ J'ai été aussi amenée à travailler sur la création d'un _plugin_ pour Gazebo, un logiciel de simulation robotique, pour l'utiliser avec le _SDK_ d'un robot de Unitree.
1717+ ],
1818+)
1919+2020+#outline(
2121+ title: [Table des matières],
2222+)
2323+2424+#pagebreak()
2525+2626+2727+#include "biblio.typ"
2828+2929+= Journal de bord
3030+3131+#for month in ("may", "june", "july", "august", "september", "november") {
3232+ include ("log/" + month + ".typ")
3333+}
3434+3535+#bibliography("bib.yaml")
+197-167
rapport/context.typ
···11-#import "utils.typ": todo, comment, refneeded
22-#import "@preview/fletcher:0.5.8": node, edge
11+#import "utils.typ": comment, refneeded, todo
22+#import "@preview/fletcher:0.5.8": edge, node
33#import "@preview/fletcher:0.5.8"
44#import "@preview/diagraph:0.3.6"
5566#show figure: set block(spacing: 4em)
77-#let diagram = (caption: none, ..args) => figure(caption: caption, fletcher.diagram(..args))
77+#let diagram = (caption: none, ..args) => figure(
88+ caption: caption,
99+ fletcher.diagram(..args),
1010+)
811#let dontbreak = content => block(breakable: false, content)
9121013#show math.equation.where(block: true): set block(spacing: 2em)
···1215//#let prod = $op(Pi, limits: #true)$
1316#let card = $op("card")$
1417#let indicatrix = contents => $thin op(bb(1), limits: #true)_(#contents) thin$
1515-#let argmax = $op("arg" #h(1em/12) "max", limits: #true)$
1818+#let argmax = $op("arg" #h(1em / 12) "max", limits: #true)$
1619#let exp = $op(bb(E), limits: #true)$
1717-#let function = (name, input_domain, output_domain, args, body) => $#name : thick thick cases(delim: #none, #input_domain &-> #output_domain, #args &|-> #body)$
2020+#let function = (name, input_domain, output_domain, args, body) => {
2121+ $#name : thick thick cases(delim: #none, #input_domain &-> #output_domain, #args &|-> #body)$
2222+}
182319242025== Bases théoriques du _Reinforcement Learning_
···56615762#let exhaustive_memory_table = (caption, filled: false) => {
5863 let maybe = content => if filled { content } else { [] }
5959- let costs = (plus_one, minus_one) => [ $L(x+1,) = #plus_one quad L(x-1,) = #minus_one$ ]
6464+ let costs = (
6565+ plus_one,
6666+ minus_one,
6767+ ) => [ $L(x+1,) = #plus_one quad L(x-1,) = #minus_one$ ]
6068 pad(x: 7%, y: 10%, figure(
6169 table(
6270 columns: (2fr, 1.9fr, 3fr),
6371 align: (left, center, left),
6472 inset: 8pt,
6565- [*État actuel* \ $(x, "retour")$], [*Meilleure action* \ +1 ou -1], [*Coûts associés* \ #maybe[avec $L = (x, "retour") |-> |x-2|$]],
7373+ [*État actuel* \ $(x, "retour")$],
7474+ [*Meilleure action* \ +1 ou -1],
7575+ [*Coûts associés* \ #maybe[avec $L = (x, "retour") |-> |x-2|$]],
7676+6677 [ $(0, "C'est plus")$ ], maybe[ +1 ], maybe(costs(2, 2)),
6778 [ $(1, "C'est plus")$ ], maybe[ +1 ], maybe(costs(1, 2)),
6879 [ $(3, "C'est moins")$ ], maybe[ -1 ], maybe(costs(2, 3)),
6980 [ $(4, "C'est moins")$ ], maybe[ -1 ], maybe(costs(3, 4)),
7070- [ $(5, "C'est moins")$ ], maybe[ -1 ], maybe(costs(4, 5))
7171- ),
7272- caption: caption
8181+ [ $(5, "C'est moins")$ ], maybe[ -1 ], maybe(costs(4, 5)),
8282+ ),
8383+ caption: caption,
7384 ))
7485}
75867676-#exhaustive_memory_table(filled: false)[ Exemple d'agent à mémoire exhaustive pour un "C'est plus ou c'est moins" dans ${ 0, 1, 2 }$, avec pour solution 2 ]
8787+#exhaustive_memory_table(
8888+ filled: false,
8989+)[ Exemple d'agent à mémoire exhaustive pour un "C'est plus ou c'est moins" dans ${ 0, 1, 2 }$, avec pour solution 2 ]
77907878-L'entraînement consiste donc ici en l'exploration de l'entièreté des états possibles de l'environnement, et, pour chaque état, le calcul du coût associé à chaque action possible.
9191+L'entraînement consiste donc ici en l'exploration de l'entièreté des états possibles de l'environnement, et, pour chaque état, le calcul du coût associé à chaque action possible.
79928093Il faut définir la fonction de coût, souvent appelée $L$ pour _loss_:
81948295$
8383-L: E -> S
9696+ L: E -> S
8497$
85988699avec $E$ l'ensemble des états possibles de l'environnement, et $S$ un ensemble muni d'un ordre total (on utilise souvent $[0, 1]$). Ces fonctions coût, qui ne dépendent que de l'état actuel de l'environnement, représente un domaine du RL#footnote[Reinforcement Learning] appelé _Q-Learning_ @qlearning
871008888-On remplit la colonne "Action à effectuer" avec l'action au coût le plus bas:
101101+On remplit la colonne "Action à effectuer" avec l'action au coût le plus bas:
891029090-#exhaustive_memory_table(filled: true)[ Entraînement terminé, avec pour fonction coût $L$ la distance à la solution ]
103103+#exhaustive_memory_table(
104104+ filled: true,
105105+)[ Entraînement terminé, avec pour fonction coût $L$ la distance à la solution ]
9110692107Ici, cette approche exhaustive suffit parce que l'ensemble des états possibles de l'environnement, $E$, posssède 6 éléments
93108···150165Le score associé à un état $s_t$ et une action $a_t$, appelée $Q(s_t, a_t)$ ici pour "quality" @qlearning-etymology, est mis à jour avec cette valeur @maxq:
151166152167$
153153-(1 - alpha) underbrace(Q(s_t, a_t), "valeur actuelle") + alpha ( underbrace(R_(t+1), "récompense\npour cette action") + gamma underbrace(max_a Q(S_(t+1), a), "récompense de la meilleure\naction pour l'état suivant") )
168168+ (1 - alpha) underbrace(Q(s_t, a_t), "valeur actuelle") + alpha ( underbrace(R_(t+1), "récompense\npour cette action") + gamma underbrace(max_a Q(S_(t+1), a), "récompense de la meilleure\naction pour l'état suivant") )
154169$
155170156171L'expression comporte deux hyperparamètres:
···185200Pour alléger les notations, on surchargera les fonctions récompenses pour qu'elle puissent prendre en entrée des éléments de $S times A$, en ignorant simplement l'action choisie:
186201187202$
188188-forall (s, a) in S times A, forall r in "récompenses", r(s, a) := r(s)
203203+ forall (s, a) in S times A, forall r in "récompenses", r(s, a) := r(s)
189204$
190205191206···197212198213#diagram(
199214 node((0, 0), $s_t$),
200200- edge(corner: right, label-pos: 2/8, label-side: left)[choix de l'action],
201201- edge("->", corner: right, label-pos: 3/8, label-side: left)[$cal(P)$],
215215+ edge(corner: right, label-pos: 2 / 8, label-side: left)[choix de l'action],
216216+ edge("->", corner: right, label-pos: 3 / 8, label-side: left)[$cal(P)$],
202217 node((1, -1))[$a_t$],
203203- edge("->", corner: right, label-pos: 5/8, label-side: left)[$M$],
204204- edge(corner: right, label-pos: 6/8, label-side: left)[simulation],
218218+ edge("->", corner: right, label-pos: 5 / 8, label-side: left)[$M$],
219219+ edge(corner: right, label-pos: 6 / 8, label-side: left)[simulation],
205220 node((2, 0))[$s_(t+1)$],
206206- edge((2, 0), (2, .75), (0, .75), (0, 0), "-->", label-side: left)[itération]
221221+ edge((2, 0), (2, .75), (0, .75), (0, 0), "-->", label-side: left)[itération],
207222)
208223209209-Quand on "déroule" $cal(P)$ en en partant d'un certain état initial $s_0$, on obtient une suite d'états et d'actions:
224224+Quand on "déroule" $cal(P)$ en en partant d'un certain état initial $s_0$, on obtient une suite d'états et d'actions:
210225211211-#diagram($
212212- s_0 edge(a_0, ->) & s_1 edge(a_1, ->) & s_2 edge(a_2, ->) & dots.c
213213-$)
226226+#diagram(
227227+ $
228228+ s_0 edge(a_0, ->) & s_1 edge(a_1, ->) & s_2 edge(a_2, ->) & dots.c
229229+ $,
230230+)
214231215232216233Pour tout pas de temps $t in NN$, on a:
217234218235$
219219-cases(
220220- a_t &= cal(P)(s_t),
221221- s_(t+1) &= M(s_t, a_t),
222222-)
236236+ cases(
237237+ a_t & = cal(P)(s_t),
238238+ s_(t+1) & = M(s_t, a_t),
239239+ )
223240$
224241225242Un chemin se modélise aisément par une suite d'éléments de $S times A$. Ainsi, on note
···229246230247231248$
232232-cal(C)_p := setbuilder(
233233- (s_t, a_t)_(t in NN) " avec "
234234- cases(
235235- & a_0 &= p(s_0),
236236- forall t in NN quad & a_(t+1) &= p(s_t),
237237- forall t in NN quad & s_(t+1) &= M(s_t, a_t)
238238- ),
239239- s_0 in S
240240-)
249249+ cal(C)_p := setbuilder(
250250+ (s_t, a_t)_(t in NN) " avec "
251251+ cases(
252252+ & a_0 & = p(s_0),
253253+ forall t in NN quad & a_(t+1) & = p(s_t),
254254+ forall t in NN quad & s_(t+1) & = M(s_t, a_t)
255255+ ),
256256+ s_0 in S
257257+ )
241258$
242259243260l'ensemble des chemins possibles avec la politique $p$. C'est tout simplement l'ensemble de tout les "déroulements" de la politique $p$ en partant des états possibles de l'environnement.
···246263On définit également l'ensemble de _tout_ les chemins d'états possibles, peut importe la politique, $cal(C)$ :
247264248265$
249249-cal(C) :=
250250-setbuilder(
251251- cases(
252252- & c_0 &= (s_0, a_0),
253253- forall t in NN quad & c_(t+1) &= M(c_t)
254254- ),
255255- (s_0, a) in S times A^NN
256256-)
266266+ cal(C) :=
267267+ setbuilder(
268268+ cases(
269269+ & c_0 & = (s_0, a_0),
270270+ forall t in NN quad & c_(t+1) & = M(c_t)
271271+ ),
272272+ (s_0, a) in S times A^NN
273273+ )
257274$
258275259276On notera que, selon $M$, on peut avoir $cal(C) subset.neq (S times A)^NN$: par exemple, certains états de l'environnement peuvent représenter des "impasses", où il est impossible d'évoluer vers un autre état, peut importe l'action choisie.
260277261278On note aussi que $cal(C)$ (et donc $cal(C)_p$ aussi) est dénombrable, étant construit à partir de $(S times A)^NN$ et $S$, $A$ et $NN$ étant aussi dénombrables#footnote[
262262- On a $card cal(C) <= card((S times A)^NN) = card(S times A) ^ (card NN) = (card S card A)^(card NN) <= (aleph_0)^(card NN) = attach(aleph_0, tl: 2) = aleph_0$
279279+ On a $card cal(C) <= card((S times A)^NN) = card(S times A)^(card NN) = (card S card A)^(card NN) <= (aleph_0)^(card NN) = attach(aleph_0, tl: 2) = aleph_0$
263280]
264281265282#align(center)[
266266-_Cette formalisation est utile par la suite, \ pour proprement définir certaines grandeurs._
283283+ _Cette formalisation est utile par la suite, \ pour proprement définir certaines grandeurs._
267284]
268285#comment[pas sûre de cette phrase]
269286270287==== Récompense attendue $eta$
271288272272-$eta$ représente la récompense moyenne à laquelle l'on peut s'attendre pour une politique $p$ avec fonction de récompense $r$.
289289+$eta$ représente la récompense moyenne à laquelle l'on peut s'attendre pour une politique $p$ avec fonction de récompense $r$.
273290274291Elle prend en compte le _discount factor_ $gamma$ : les récompenses des actions deviennent de moins en moins#footnote[En supposant $gamma < 1$, ce qui est souvent le cas #refneeded #todo[Mettre dans la def de $gamma$]] importantes avec le temps. $eta$ est définie ainsi @trpo
275292276293#let policyexp = policy => $exp_((c_t)_(t in NN) op(~) #policy op(in) cal(S))$
277294278295$
279279-eta(p, r)
280280- underbracket(
281281- sum_((c_t)_(t in NN) in cal(S))
296296+ eta(p, r)
282297 underbracket(
283283- rho_0(s_0)
284284- product_(t=0)^oo Q_p (c_t), "probabilité du chemin"
298298+ sum_((c_t)_(t in NN) in cal(S))
299299+ underbracket(
300300+ rho_0(s_0)
301301+ product_(t=0)^oo Q_p (c_t), "probabilité du chemin"
302302+ )
303303+ quad
304304+ underbracket(sum_(t=0)^oo gamma^t r(c_t), "récompense associée"),
305305+ "pour tout chemin possible"
285306 )
286286- quad
287287- underbracket(
288288- sum_(t=0)^oo gamma^t r(c_t), "récompense associée"
289289- ),
290290-"pour tout chemin possible"
291291-)
292307$
293308294309295310On peut également exprimer $eta(p, r)$ comme une espérance. Soit $C$ une variable aléatoire de $cal(S)$. On a (cf @proof-eta-esperance)
296311297312$
298298-eta(p, r) = exp( sum_(t=0)^oo gamma^t r(C_t) )
313313+ eta(p, r) = exp(sum_(t=0)^oo gamma^t r(C_t))
299314$
300315301316···356371Pour calculer $A_(p, r)(s, a)$, on regarde l'espérance des récompenses cumulées pour tout chemin commençant par $s$, et on la compare à celle pour tout chemin commençant par $M(s, a)$
357372358373$
359359-A_(p, r)(s, a) :=
360360-underbracket(
361361- exp_((s_t, a_t)_(t in NN) op(~) p op(in) cal(S) \ s_0 = s \ s_1 = M(s_0, a)) sum_(t=0)^oo gamma^t r(s_t),
362362- Q(s, a)
363363-) - underbracket(
364364- exp_((s_t, a_t)_(t in NN) op(~) p op(in) cal(S) \ s_0 = s) sum_(t=0)^oo gamma^t r(s_t),
365365- V(s)
366366-)
374374+ A_(p, r)(s, a) :=
375375+ underbracket(
376376+ exp_((s_t, a_t)_(t in NN) op(~) p op(in) cal(S) \ s_0 = s \ s_1 = M(s_0, a)) sum_(t=0)^oo gamma^t r(s_t),
377377+ Q(s, a)
378378+ ) - underbracket(
379379+ exp_((s_t, a_t)_(t in NN) op(~) p op(in) cal(S) \ s_0 = s) sum_(t=0)^oo gamma^t r(s_t),
380380+ V(s)
381381+ )
367382$
368383369384···384399385400386401$
387387-eta(p', r)
388388-&= eta(p, r) + policyexp(p') sum_(t=0)^oo gamma^t A_(p, r)(c_t) \
389389-&#[Qui se simplifie en @trpo] \
390390-&= eta(p, r) + sum
402402+ eta(p', r) & = eta(p, r) + policyexp(p') sum_(t=0)^oo gamma^t A_(p, r)(c_t) \
403403+ & #[Qui se simplifie en @trpo] \
404404+ & = eta(p, r) + sum
391405$
392406393407···395409396410Il est théoriquement possible d'utiliser $A$ pour optimiser une politique, en maximisant sa valeur à un état donné:
397411398398-#diagram(caption: [Boucle d'entraînement],
412412+#diagram(
413413+ caption: [Boucle d'entraînement],
399414 node((0, 0))[$s_t$],
400415 edge("-"),
401416 node(name: <policy>, (0, -1))[$cal(P)$],
···406421 edge(<final>, (0, 0), "-->", label-side: left)[itération],
407422 // edge("d,d,l,l,l,u,u,u", <policy>, "->", label-pos: 33%, label-side: left, align(center, [$Q_cal(P)(s_(t+1), argmax_(a in A) A_(cal(P), R)(s_(t+1), a)) <- A_(cal(P), R) (dots)$ \ Mise à jour]))
408423 // edge("d,d,l,l,l,u,u,u", <policy>, "->", label-pos: 37%, label-side: left, align(center)[$argmax_(a in A) A_(cal(P), R)(s_(t+1), a)$ \ mise à jour de $cal(P)$])
409409- edge("d,l,l,l,u,u", <policy>, "->", label-pos: 33%, label-side: left, align(center)[
410410- // mise à jour de $cal(P)$ \
424424+ edge("d,l,l,l,u,u", <policy>, "->", label-pos: 33%, label-side: left, align(
425425+ center,
426426+ )[
427427+ // mise à jour de $cal(P)$ \
411428 $Q_cal(P)(s_(t+1), a_(t+1)^*) <- A_(cal(P), R)(s_(t+1), a_(t+1)^*)$
412412- ])
429429+ ]),
413430) <policy-update-loop>
414431415415-Avec
432432+Avec
416433417434$
418418-a_(t+1)^* &:= argmax_(a in A) A_(cal(P), R)(s_(t+1), a) \
419419-435435+ a_(t+1)^* & := argmax_(a in A) A_(cal(P), R)(s_(t+1), a) \
420436$
421437422438Mais, en pratique, des erreurs d'approximations peuvent rendre $A_(cal(P), R)(s_(t+1), a_(t+1)^*)$ négatif, ce qui empêche de s'en servir pour définir une valeur de $Q_(cal(P))$ @trpo
···425441Le _surrogate advantage_ détermine la performance d'une politique par rapport à une autre
426442427443$
428428-cL_r (p', p) := exp_((s_t, a_t)_(t in NN) in cal(C)) sum_(t=0)^oo (Q_p (s_t, a_t)) / (Q_p' (s_t, a_t)) A_(p, r)(s_t, a_t)
444444+ cL_r (p', p) := exp_((s_t, a_t)_(t in NN) in cal(C)) sum_(t=0)^oo (Q_p (s_t, a_t)) / (Q_p' (s_t, a_t)) A_(p, r)(s_t, a_t)
429445$
430446431447···441457L'idée de la _TRPO_ est de maximiser le _surrogate advantage_ du nouveau $Q$ tout en limitant l'ampleur des modifications apportées à $Q$, ce qui procure une stabilité à l'algorithme, et évite qu'un seul "faux pas" dégrade violemment la performance de la politique.
442458443459$
444444-Q' = & cases(
445445- argmax_(q) cL_r (q, Q),
446446-"s.c. distance"(Q', Q) < delta
447447-)
460460+ Q' = & cases(
461461+ argmax_(q) cL_r (q, Q),
462462+ "s.c. distance"(Q', Q) < delta
463463+ )
448464$
449465450466Avec $delta$ une limite supérieure de distance entre $Q'$, la nouvelle politique, et $Q$, l'ancienne.
···454470Il existe plusieurs manières de mesurer l'écart entre deux distributions de probabilité, dont notamment la _divergence de Kullback-Leibler_, aussi appelée entropie relative @kullback-leibler @kullback-leibler2:
455471456472$
457457-D_"KL" (P || P') := sum_(x in cal(X)) P(x) log P(x) / (P'(x))
473473+ D_"KL" (P || P') := sum_(x in cal(X)) P(x) log P(x) / (P'(x))
458474$
459475460476Avec $cal(X)$ l'espace des échantillons et $P, P'$ deux distributions de probabilité sur celui-ci. Dans notre cas, $cal(X) = S times A$,
···464480Pour évaluer cette distance, on regarde la plus grande des distances entre des paires de distributions de probabilité de politiques $Q_cal(P)$ et $Q_cal(P)'$ pour $s in S$ fixé @trpo
465481466482$
467467-max_(s in S) D_"KL" (Q_cal(P)' (s, dot) || Q_cal(P) (s, dot)) < delta
483483+ max_(s in S) D_"KL" (Q_cal(P)' (s, dot) || Q_cal(P) (s, dot)) < delta
468484$
469485470486···483499484500485501$
486486-forall s in S, Q(s, 1) = Q(s, 2)
502502+ forall s in S, Q(s, 1) = Q(s, 2)
487503$
488504489505490506et
491507492508$
493493-Q' := (s, a) |-> cases(
494494- Q(s, a) dot 2 si a = 1 \
495495- Q(s, a) dot 1/2 si a = 2 \
496496- Q(s, a) sinon
497497-) \
498498-509509+ Q' := (s, a) |-> cases(
510510+ Q(s, a) dot 2 si a = 1 \
511511+ Q(s, a) dot 1/2 si a = 2 \
512512+ Q(s, a) sinon
513513+ ) \
499514$
500515501501-On a $D_"KL" (Q, Q') = 0$ (cf @dkl-zero), alors qu'il y a eu une modification très importante des probabilités de choix de l'action 1 et 2 dans tout les états possibles : si on imagine $Q(s, 1) = Q(s, 2) = 1 slash 4$, on a après modification $Q'(s, 1) = 1 slash 2$ et $Q'(s, 2) = 1 slash 8$.
516516+On a $D_"KL" (Q, Q') = 0$ (cf @dkl-zero), alors qu'il y a eu une modification très importante des probabilités de choix de l'action 1 et 2 dans tout les états possibles : si on imagine $Q(s, 1) = Q(s, 2) = 1 slash 4$, on a après modification $Q'(s, 1) = 1 slash 2$ et $Q'(s, 2) = 1 slash 8$.
502517503518==== Région de confiance
504519505520Cette contrainte définit un ensemble réduit de $cal(P)'$ acceptables comme nouvelle politique, aussi appelé une _trust region_ (région de confiance), d'où la méthode d'optimisation tire son nom @trpo.
506521507507-#let ddot = [ #sym.dot #h(-1em/16) #sym.dot ]
522522+#let ddot = [ #sym.dot #h(-1em / 16) #sym.dot ]
508523509509-En pratique, l'optimisation sous cette contrainte est trop demandeuse en puissance de calcul, on utilise plutôt l'espérance @trpo
524524+En pratique, l'optimisation sous cette contrainte est trop demandeuse en puissance de calcul, on utilise plutôt l'espérance @trpo
510525511526$
512512-overline(D_"KL") := bb(E)_(s in S) D_"KL" (Q(s, dot) || Q'(s, dot))
527527+ overline(D_"KL") := bb(E)_(s in S) D_"KL" (Q(s, dot) || Q'(s, dot))
513528$
514529515530···517532518533=== _Proximal Policy Optimization_
519534520520-La _PPO_ repose sur le même principe de stabilisation de l'entraînement par limitation de l'ampleur des changements de politique à chaque pas.
535535+La _PPO_ repose sur le même principe de stabilisation de l'entraînement par limitation de l'ampleur des changements de politique à chaque pas.
521536522537Cependant, les méthodes _PPO_ préfèrent changer la quantité à optimiser, pour limiter intrinsèquement l'ampleur des modifications, en résolvant un problème d'optimisation sans contraintes @ppo
523538524539525540$
526526-argmax_(cal(P)') & exp_((s, a) in cal(S)) L(s, a, cal(P), cal(P'), R) \
527527-"s.c." & top
541541+ argmax_(cal(P)') & exp_((s, a) in cal(S)) L(s, a, cal(P), cal(P'), R) \
542542+ "s.c." & top
528543$
529544530545==== Avec pénalité _(PPO-Penalty)_
···532547_PPO-Penalty_ soustrait une divergence K-L pondérée à l'avantage:
533548534549$
535535-L(s, a, cal(P), cal(P'), R) = (Q_cal(P) (s, a)) / (Q_cal(P') (s, a)) A_(cal(P), R) (s, a) - beta D_"KL" (cal(P) || cal(P'))
550550+ L(s, a, cal(P), cal(P'), R) = (Q_cal(P) (s, a)) / (Q_cal(P') (s, a)) A_(cal(P), R) (s, a) - beta D_"KL" (cal(P) || cal(P'))
536551$
537552538553Avec $beta$ ajusté automatiquement pour être dans la même échelle que l'autre terme de la soustraction.
···543558544559545560$
546546-L(s, a, cal(P), cal(P'), R) = min(
547547- &(Q_cal(P)' (s, a)) / (Q_cal(P) (s, a)) A_(cal(P)', R)(s, a), quad \
548548- &op("clip")(
549549- (Q_cal(P)' (s, a)) / (Q_cal(P) (s, a)),
550550- 1 - epsilon,
551551- 1 + epsilon
552552- ) A_(cal(P)', R)(s, a)
553553-)
561561+ L(s, a, cal(P), cal(P'), R) = min(
562562+ & (Q_cal(P)' (s, a)) / (Q_cal(P) (s, a)) A_(cal(P)', R)(s, a), quad \
563563+ &op("clip")(
564564+ (Q_cal(P)' (s, a)) / (Q_cal(P) (s, a)),
565565+ 1 - epsilon,
566566+ 1 + epsilon
567567+ ) A_(cal(P)', R)(s, a)
568568+ )
554569$
555570556571Avec $epsilon in RR_+^*$ un paramètre indiquant à quel point l'on peut s'écarter de la politique précédente, et
557572558573$
559559-op("clip") := (x, m, M) |-> cases(
560560- m si x < m,
561561- M si x > M,
562562- x sinon
563563-)
574574+ op("clip") := (x, m, M) |-> cases(
575575+ m si x < m,
576576+ M si x > M,
577577+ x sinon
578578+ )
564579$
565580566581La complexité de l'expression, et la présence d'un $min$ au lieu de simplement un $op("clip")$ est dûe au fait que l'avantage $A_(cal(P)', R) (s, a)$ peut être négatif. L'expression se simplifie en séparant les cas (cf @proof-ppo-clip-simplify)
567582568568-#let named_point = (x, y, shape: "@", color: black, side: right, content) => edge((x, y), shape + "-", (x+0.01, y), label-side: side, stroke: color, text(fill: color, content))
583583+#let named_point = (
584584+ x,
585585+ y,
586586+ shape: "@",
587587+ color: black,
588588+ side: right,
589589+ content,
590590+) => edge(
591591+ (x, y),
592592+ shape + "-",
593593+ (x + 0.01, y),
594594+ label-side: side,
595595+ stroke: color,
596596+ text(fill: color, content),
597597+)
569598570570-#let equation_and_diagram = (eqn, diagrm) => stack(dir: ltr,
599599+#let equation_and_diagram = (eqn, diagrm) => stack(
600600+ dir: ltr,
571601 block(width: 70%, math.equation(numbering: none, block: true, eqn)),
572572- diagrm
602602+ diagrm,
573603)
574604575605#dontbreak[
576606577577-/ Si l'avantage est positif: $a$ est un meilleur choix que $cal(P)(s)$.
607607+ / Si l'avantage est positif: $a$ est un meilleur choix que $cal(P)(s)$.
578608579579-#equation_and_diagram(
580580- $
581581- L(s, a, cal(P), cal(P)', R) = min(
582582- (Q_cal(P)' (s, a)) / (Q_cal(P) (s, a)),
583583- quad 1 + epsilon
584584- ) A_(cal(P)', R)(s, a)
585585- $,
586586- diagram(
587587- spacing: (2.7em, 2em),
588588- node((-1, 0))[$cal(P)'$],
589589- edge((-1, 0), "->", (3, 0), stroke: luma(150)),
590590- edge((-1, 0), "-|", (1, 0), extrude: (1, -1, 0) ),
591591- named_point(1, 0, shape: "|")[$1+epsilon$],
592592- named_point(0, 0)[$cal(P)$],
593593- named_point(1.5, 0, color: red, side: left)[$times$],
594594- named_point(0.5, 0, color: olive, side: left)[$checkmark$],
609609+ #equation_and_diagram(
610610+ $
611611+ L(s, a, cal(P), cal(P)', R) = min(
612612+ (Q_cal(P)' (s, a)) / (Q_cal(P) (s, a)),
613613+ quad 1 + epsilon
614614+ ) A_(cal(P)', R)(s, a)
615615+ $,
616616+ diagram(
617617+ spacing: (2.7em, 2em),
618618+ node((-1, 0))[$cal(P)'$],
619619+ edge((-1, 0), "->", (3, 0), stroke: luma(150)),
620620+ edge((-1, 0), "-|", (1, 0), extrude: (1, -1, 0)),
621621+ named_point(1, 0, shape: "|")[$1+epsilon$],
622622+ named_point(0, 0)[$cal(P)$],
623623+ named_point(1.5, 0, color: red, side: left)[$times$],
624624+ named_point(0.5, 0, color: olive, side: left)[$checkmark$],
625625+ ),
595626 )
596596-)
597627598598-/ Si l'avantage est négatif: choisir $a$ est pire que garder $cal(P)(s)$.
628628+ / Si l'avantage est négatif: choisir $a$ est pire que garder $cal(P)(s)$.
599629600600-#equation_and_diagram(
601601- $
602602- L(s, a, cal(P), cal(P)', R) = max(
603603- 1 - epsilon, quad
604604- (Q_cal(P)' (s, a)) / (Q_cal(P) (s, a))
605605- ) A_(cal(P)', R)(s, a)
606606- $,
607607- diagram(
608608- spacing: (2.7em, 2em),
609609- node((3, 0))[$cal(P)'$],
610610- edge((-1, 0), "<-", (3, 0), stroke: luma(150)),
611611- edge((1, 0), "|-", (3, 0), extrude: (1, -1, 0) ),
612612- named_point(1, 0, shape: "|")[$1-epsilon$],
613613- named_point(2, 0)[$cal(P)$],
614614- named_point(0, 0, color: red, side: left)[$times$],
615615- named_point(1.5, 0, color: olive, side: left)[$checkmark$],
616616- ),
617617-)
630630+ #equation_and_diagram(
631631+ $
632632+ L(s, a, cal(P), cal(P)', R) = max(
633633+ 1 - epsilon, quad
634634+ (Q_cal(P)' (s, a)) / (Q_cal(P) (s, a))
635635+ ) A_(cal(P)', R)(s, a)
636636+ $,
637637+ diagram(
638638+ spacing: (2.7em, 2em),
639639+ node((3, 0))[$cal(P)'$],
640640+ edge((-1, 0), "<-", (3, 0), stroke: luma(150)),
641641+ edge((1, 0), "|-", (3, 0), extrude: (1, -1, 0)),
642642+ named_point(1, 0, shape: "|")[$1-epsilon$],
643643+ named_point(2, 0)[$cal(P)$],
644644+ named_point(0, 0, color: red, side: left)[$times$],
645645+ named_point(1.5, 0, color: olive, side: left)[$checkmark$],
646646+ ),
647647+ )
618648619649]
620650621651// L'algorithme de mise à jour est le suivant @ppo-openai:
622622-//
652652+//
623653// 1. Mise à jour de la politique:
624624-//
654654+//
625655// $
626656// cal(P') = argmax_p 1/T sum_(t=1)^T L(s, a, cal(P), p, R)
627657// $
···634664635665Bien évidemment, ce sont des programmes complexes avec des résolutions souvent numériques d'équation physiques; il est presque inévitable que des bugs se glissent dans ces programmes.
636666637637-On est donc dans un cas où il est très utile de
667667+On est donc dans un cas où il est très utile de
638668639669Un environnement de RL#footnote[Reinforcement Learning] ne se résume pas à son moteur de physique: il faut également charger des modèles 3D, le modèle du robot (qui doit être contrôlable par les actions), et également, pendant les phases de développement, avoir un moteur de rendu graphique, une interface et des outils de développement.
640670···657687658688#todo[Déterminer si je parle de ça, en fonction de cmb de pages il reste après avoir fait le reste, ça fera ptet trop...]
659689660660-Il est possible d'éviter la définition manuelle de la fonction coût, ce qui requiert d'instrumentaliser l'environnement avec des capteurs supplémentaires, en fournissant à la place
690690+Il est possible d'éviter la définition manuelle de la fonction coût, ce qui requiert d'instrumentaliser l'environnement avec des capteurs supplémentaires, en fournissant à la place
661691662662-=== Inventaire des simulateurs en robotique
692692+=== Inventaire des simulateurs en robotique
663693664694==== Isaac
665695···671701672702Bien que MuJoCo est décrit comme un moteur de simulation physique et non un simulateur, il embarque une commande `simulate` qui le rend fonctionnellement équivalent à un simulateur @mujoco-simulate.
673703674674-==== Gazebo
704704+==== Gazebo
675705676706Les intérêts de Gazebo @gazebo sont multiples:
677707···686716687717688718689689-=== Inventaire des moteurs de simulation physique
719719+=== Inventaire des moteurs de simulation physique
690720691721==== DART
692722693693-DART, pour Dynamic Animation and Robotics Toolkit @dart,
723723+DART, pour Dynamic Animation and Robotics Toolkit @dart,
694724695725==== Bullet
696726···702732703733== Le _H1v2_ d'Unitree
704734705705-Le _H1v2_ est un modèle de robot humanoïde créé par la société Unitree.
735735+Le _H1v2_ est un modèle de robot humanoïde créé par la société Unitree.
706736707737Il possède plus de 26 degrés de liberté, dont
708738709709-- 6 dans chaque jambe (3 à la hanche, 2 au talon et un au genou),
739739+- 6 dans chaque jambe (3 à la hanche, 2 au talon et un au genou),
710740- 7 dans chaque bras (3 à l'épaule, 3 au poignet et un au coude) @h1v2
711741712742···718748719749#figure(
720750 caption: [Arbre des dépendances pour _Gepetto/h1v2-Isaac_],
721721- scale(10%, reflow: true, diagraph.render(read("./isaac-deptree.dot")))
751751+ scale(10%, reflow: true, diagraph.render(read("./isaac-deptree.dot"))),
722752)
723753724754Bien que toutes ces dépendances puissent être spécifiées à des versions strictes @lockfiles pour éviter des changements imprévus de comportement du code venant des bibliothèques, beaucoup celles-ci ont besoin de compiler du code C++ à l'installation pour des raisons de performance @cpp-python. Des problèmes de reproductibilité peuvent donc subsister à l'installation des dépendances, étant donné la dépendance du processus de compilation à la machine compilant le code.
+329-194
rapport/gz-unitree.typ
···11#import "@preview/zebraw:0.5.5"
22-#import "@preview/fletcher:0.5.8": diagram, node, edge
22+#import "@preview/fletcher:0.5.8": diagram, edge, node
33#import "@preview/cetz:0.4.2"
44-#import "./utils.typ": dontbreak, todo, refneeded
44+#import "./utils.typ": dontbreak, refneeded, todo
55#show figure: set block(spacing: 2em)
66-#let zebraw = (..args) => zebraw.zebraw(lang: false, background-color: luma(255).opacify(0%), ..args)
66+#let zebraw = (..args) => zebraw.zebraw(
77+ lang: false,
88+ background-color: luma(255).opacify(0%),
99+ ..args,
1010+)
711812// Utile: message marquant le début du dev de gz-unitree, 23 juin 2025
913// https://matrix.to/#/!MmlaUevGqfiZYSHREv:laas.fr/$omjzydhckQuIVkcNBw0LTYVT7Td1C9UeLqbIisJAnFg?via=laas.fr
···3034 gutter: 2em,
3135 [
32363333-Pour arriver à ces solutions, du débuggage du traffic RTPS (le protocole sur lequel est construit DDS @dds), _Wireshark_ @wireshark s'est avéré utile.
3737+ Pour arriver à ces solutions, du débuggage du traffic RTPS (le protocole sur lequel est construit DDS @dds), _Wireshark_ @wireshark s'est avéré utile.
343835393640 C'est notamment grâce à ce traçage des paquets que le problème d'ID de domaine a été découvert: notre _subscriber_ DDS était réglé sur le domaine anonyme (ID 0) alors que le SDK d'Unitree communique sur le domaine d'ID 1.
37413842 C'est aussi Wireshark qui nous a permis de voir quels étaient les types IDL utilisés pour les messages.
3943 ],
4040- figure(caption: [_Wireshark_ permet de visualiser des méta-données sur les paquets RTPS],
4141- stack(
4242- spacing: 1em,
4343- image("./wireshark-wrong-domain.png"),
4444- image("./wireshark-message-type.png"),
4545- ))
4444+ figure(
4545+ caption: [_Wireshark_ permet de visualiser des méta-données sur les paquets RTPS],
4646+ stack(
4747+ spacing: 1em,
4848+ image("./wireshark-wrong-domain.png"),
4949+ image("./wireshark-message-type.png"),
5050+ ),
5151+ ),
4652))
47534854Voici une trace wireshark d'un échange usuel entre commandes (`rt/lowcmd`) et états (`rt/lowstate`)
···5258#let overlayed-img = contents => layout(bounds => {
5359 let size = measure(img, ..bounds)
5460 img
5555- place(top+left, block(..size, contents))
6161+ place(top + left, block(..size, contents))
5662})
57635864#figure(
···6167 #diagram(spacing: (4.54pt, 2.58pt), {
6268 node((0, 0))[]
6369 let annotations-x = 80
6464- let annotate = (y-start, y-end, label) => edge((annotations-x, y-start), "|-|", (annotations-x, y-end), label-fill: white, label-side: left, label)
7070+ let annotate = (y-start, y-end, label) => edge(
7171+ (annotations-x, y-start),
7272+ "|-|",
7373+ (annotations-x, y-end),
7474+ label-fill: white,
7575+ label-side: left,
7676+ label,
7777+ )
65786679 annotate(3, 20)[Attente]
6780 annotate(20, 60)[Initialisation]
6881 annotate(60, 100)[Échange `rt/` \ `lowstate` $arrows.lr$ `lowcmd`]
6982 })
7070- ]
8383+ ],
7184)
72857386···7790Un _system plugin_ Gazebo consiste en la définition d'une classe héritant de `gz::sim::System`, ainsi que d'autres interfaces permettant notamment d'exécuter notre code avant ou après une mise à jour de l'état du simulateur (avec `gz::sim::ISystem`{`Pre`,`Post`}`Update`)
78917992#dontbreak(
8080-8181-```cpp
8282-#include <gz/sim/System.hh>
8383-namespace gz_unitree
8484-{
8585- class UnitreePlugin :
8686- public gz::sim::System,
8787- public gz::sim::ISystemPreUpdate
8888- {
8989- public:
9090- UnitreePlugin();
9191- public:
9292- ~UnitreePlugin() override;
9393- public:
9494- void PreUpdate(const gz::sim::UpdateInfo &_info,
9595- gz::sim::EntityComponentManager &ecm) override;
9696- };
9797-}
9898-```
9999-9393+ ```cpp
9494+ #include <gz/sim/System.hh>
9595+ namespace gz_unitree
9696+ {
9797+ class UnitreePlugin :
9898+ public gz::sim::System,
9999+ public gz::sim::ISystemPreUpdate
100100+ {
101101+ public:
102102+ UnitreePlugin();
103103+ public:
104104+ ~UnitreePlugin() override;
105105+ public:
106106+ void PreUpdate(const gz::sim::UpdateInfo &_info,
107107+ gz::sim::EntityComponentManager &ecm) override;
108108+ };
109109+ }
110110+ ```,
100111)
101112102113Il faut ensuite implémenter la classe puis appeler une macro ajoutant le plugin à Gazebo
···116127117128#zebraw(
118129 numbering: false,
119119- highlight-lines: (..range(3, 5)),
130130+ highlight-lines: (..range(3, 5),),
120131 ```xml
121132 <sdf version='1.11'>
122133 <world name="default">
···127138 <link name='pelvis'>
128139 <inertial>
129140 ...
130130- ```
141141+ ```,
131142)
132143133144Avec `filename` le chemin vers le plugin compilé, qui sera cherché dans les répertoires spécifiés par `GZ_SIM_SYSTEM_PLUGIN_PATH` @gz-system-plugin-path @sdf-plugin-filename.
···144155En plus de cela, il y a bien évidemment la politique de contrôle $cal(P)$, qui interagit via les canaux DDS avec le robot (qu'il soit réel, ou simulé)
145156146157#let legend = (
147147- ..descriptions
158158+ ..descriptions,
148159) => grid(
149149- columns: (1fr, 3fr),
160160+ columns: (1fr, 3fr),
150161 align: left,
151162 row-gutter: 0.5em,
152152- ..descriptions.pos().map(((arrow, desc)) => (
153153- diagram(edge((0, 0), arrow, (0.75, 0))),
154154- desc
155155- )).flatten()
163163+ ..descriptions
164164+ .pos()
165165+ .map(((arrow, desc)) => (
166166+ diagram(edge((0, 0), arrow, (0.75, 0))),
167167+ desc,
168168+ ))
169169+ .flatten()
156170)
157171158172#let architecture = (
159159- caption,
160160- group-inset: 12pt,
161161- group-color: luma(80),
173173+ caption,
174174+ group-inset: 12pt,
175175+ group-color: luma(80),
162176 show-legend: true,
163163- ..edges
164164-) => figure(caption: caption,
165165- pad(
166166- y: 10pt + group-inset,
177177+ ..edges,
178178+) => figure(caption: caption, pad(
179179+ y: 10pt + group-inset,
167180 diagram(
168168- debug: false,
169169- node-stroke: 0.5pt,
181181+ debug: false,
182182+ node-stroke: 0.5pt,
170183 edge-corner-radius: 6pt,
171171- {
172172-173173- if show-legend {
174174- node((2, 4.5), stroke: none, width: 15em, legend(("--", "Message DDS"), ("@->", "Désynchronisation")))
175175- }
184184+ {
185185+ if show-legend {
186186+ node((2, 4.5), stroke: none, width: 15em, legend(
187187+ ("--", "Message DDS"),
188188+ ("@->", "Désynchronisation"),
189189+ ))
190190+ }
176191177177- let group = (nodes, label, alignment: bottom + center, name: none) => node(
178178- name: name,
179179- enclose: nodes,
180180- snap: false,
181181- inset: group-inset,
182182- stroke: group-color.lighten(75%) + 2pt,
183183- align(alignment, move(dy: 2 * group-inset * if alignment.y == bottom { 1 } else { -1 }, text(fill: group-color, label)))
184184- )
192192+ let group = (
193193+ nodes,
194194+ label,
195195+ alignment: bottom + center,
196196+ name: none,
197197+ ) => node(
198198+ name: name,
199199+ enclose: nodes,
200200+ snap: false,
201201+ inset: group-inset,
202202+ stroke: group-color.lighten(75%) + 2pt,
203203+ align(alignment, move(
204204+ dy: 2 * group-inset * if alignment.y == bottom { 1 } else { -1 },
205205+ text(fill: group-color, label),
206206+ )),
207207+ )
185208186186- let subtitled = (title, subtitle) => [#title \ #text(size: 0.8em, subtitle)]
209209+ let subtitled = (title, subtitle) => [#title \ #text(
210210+ size: 0.8em,
211211+ subtitle,
212212+ )]
187213188188- node(name: <configure>, (0, 1), `::Configure`)
189189- node(name: <preupdate>, (0, 2), `::PreUpdate`)
190190- group((<configure>, <preupdate>), `gz::sim::System`, alignment: top + center)
214214+ node(name: <configure>, (0, 1), `::Configure`)
215215+ node(name: <preupdate>, (0, 2), `::PreUpdate`)
216216+ group(
217217+ (<configure>, <preupdate>),
218218+ `gz::sim::System`,
219219+ alignment: top + center,
220220+ )
191221192192- node(name: <channelfactory>, enclose: ((1, 0), (2, 0)), inset: 8pt, subtitled(`ChannelFactory`, [domaine 1, interface `lo`]))
193193- node(name: <publisher>, (1, 1), inset: 8pt, subtitled(`ChannelPublisher` , [canal `rt/lowstate`]))
194194- node(name: <subscriber>, (2, 1), inset: 8pt, subtitled(`ChannelSubscriber` , [canal `rt/lowcmd`]))
195195- group(name: <dds>, (<channelfactory>, <publisher>, <subscriber>), alignment: top+center)[SDK d'Unitree]
222222+ node(
223223+ name: <channelfactory>,
224224+ enclose: ((1, 0), (2, 0)),
225225+ inset: 8pt,
226226+ subtitled(`ChannelFactory`, [domaine 1, interface `lo`]),
227227+ )
228228+ node(name: <publisher>, (1, 1), inset: 8pt, subtitled(
229229+ `ChannelPublisher`,
230230+ [canal `rt/lowstate`],
231231+ ))
232232+ node(name: <subscriber>, (2, 1), inset: 8pt, subtitled(
233233+ `ChannelSubscriber`,
234234+ [canal `rt/lowcmd`],
235235+ ))
236236+ group(
237237+ name: <dds>,
238238+ (<channelfactory>, <publisher>, <subscriber>),
239239+ alignment: top + center,
240240+ )[SDK d'Unitree]
196241197242198198- node(name: <lowstate>, (1, 2), `::LowStateWriter`)
199199- node(name: <lowcmd>, (2, 2), `::CmdHandler`)
200200- node(name: <statebuf>, (1, 3), subtitled("State buffer", `statebuf`))
201201- node(name: <cmdbuf>, (2, 3), subtitled("Commands buffer", `cmdbuf`))
202202- group((<lowstate>, <lowcmd>, <statebuf>, <cmdbuf>))[Plugin internals]
243243+ node(name: <lowstate>, (1, 2), `::LowStateWriter`)
244244+ node(name: <lowcmd>, (2, 2), `::CmdHandler`)
245245+ node(name: <statebuf>, (1, 3), subtitled("State buffer", `statebuf`))
246246+ node(name: <cmdbuf>, (2, 3), subtitled("Commands buffer", `cmdbuf`))
247247+ group((<lowstate>, <lowcmd>, <statebuf>, <cmdbuf>))[Plugin internals]
203248204204- node(name: <policy>, (0, -1), $cal(P)$)
249249+ node(name: <policy>, (0, -1), $cal(P)$)
205250206206- for e in edges.pos() {
207207- e
208208- }
209209- }
210210-)))
251251+ for e in edges.pos() {
252252+ e
253253+ }
254254+ },
255255+ ),
256256+))
211257212258#architecture([Phase d'initialisation du plugin], show-legend: false, {
213213- edge(<configure>, "u", <channelfactory>, "->", label-side: left, label-pos: 50%)[appelle]
259259+ edge(
260260+ <configure>,
261261+ "u",
262262+ <channelfactory>,
263263+ "->",
264264+ label-side: left,
265265+ label-pos: 50%,
266266+ )[appelle]
214267 edge(<channelfactory>, "->", <publisher>)[initialise]
215268 edge(<channelfactory>, "->", <subscriber>)[initialise]
216269 edge(<publisher>, "<->", <lowstate>)[`std::bind`]
···226279227280Cette initialisation est faite à l'initialisation du plugin par Gazebo, en la faisant dans la méhode `::Configure` du plugin.
228281229229-En pratique, on utilise `std::bind` @cpp-bind pour fixer l'instance d'`UnitreePlugin` et ainsi passer des méthodes de la classe comme des simples fonctions
282282+En pratique, on utilise `std::bind` @cpp-bind pour fixer l'instance d'`UnitreePlugin` et ainsi passer des méthodes de la classe comme des simples fonctions
230283231284#grid(
232285 columns: 2,
233286 gutter: 1em,
234234-figure(
235235- caption: [Création d'un _subscriber_ à `rt/lowcmd` dans `UnitreePlugin::Configure`],
236236- text(size: 0.8em, ```cpp
237237-auto subscriber = ChannelSubscriberPtr<LowCmd_>(
238238- new ChannelSubscriber<LowCmd_>("rt/lowcmd")
239239-);
287287+ figure(
288288+ caption: [Création d'un _subscriber_ à `rt/lowcmd` dans `UnitreePlugin::Configure`],
289289+ text(size: 0.8em, ```cpp
290290+ auto subscriber = ChannelSubscriberPtr<LowCmd_>(
291291+ new ChannelSubscriber<LowCmd_>("rt/lowcmd")
292292+ );
240293241241-auto handler = std::bind(
242242- &UnitreePlugin::CmdHandler,
243243- this,
244244- std::placeholders::_1
245245-)
294294+ auto handler = std::bind(
295295+ &UnitreePlugin::CmdHandler,
296296+ this,
297297+ std::placeholders::_1
298298+ )
246299247247-subscriber->InitChannel(handler, 1);
300300+ subscriber->InitChannel(handler, 1);
248301249249-.
250250-```)
251251-),
302302+ .
303303+ ```),
304304+ ),
252305253253-figure(
254254- caption: [Création du _publisher_ pour `rt/lowstate` dans `UnitreePlugin::Configure`],
255255-text(size: 0.8em, ```cpp
256256-auto publisher = ChannelPublisherPtr<LowState_>(
257257- new ChannelPublisher<LowState_>("rt/lowstate")
258258-);
306306+ figure(
307307+ caption: [Création du _publisher_ pour `rt/lowstate` dans `UnitreePlugin::Configure`],
308308+ text(size: 0.8em, ```cpp
309309+ auto publisher = ChannelPublisherPtr<LowState_>(
310310+ new ChannelPublisher<LowState_>("rt/lowstate")
311311+ );
259312260260-publisher->InitChannel();
313313+ publisher->InitChannel();
261314262262-this->publisher_thread = CreateRecurrentThreadEx(
263263- "low_state_writer",
264264- UT_CPU_ID_NONE,
265265- 500,
266266- &UnitreePlugin::LowStateWriter,
267267- this
268268-);
269269-```)
270270-)
315315+ this->publisher_thread = CreateRecurrentThreadEx(
316316+ "low_state_writer",
317317+ UT_CPU_ID_NONE,
318318+ 500,
319319+ &UnitreePlugin::LowStateWriter,
320320+ this
321321+ );
322322+ ```),
323323+ ),
271324)
272325273326== `rt/lowcmd`
···277330Pour appliquer une commande à un moteur, on calcule la force effective que le moteur doit appliquer:
278331279332$
280280-tau =
281281- underbracket(K_p Delta q, "proportional") +
282282- underbracket(tau_"ff", "integrative") +
333333+ tau =
334334+ underbracket(K_p Delta q, "proportional") +
335335+ underbracket(tau_"ff", "integrative") +
283336 underbracket(K_d Delta dot(q), "derivative")
284337$
285338286286-Avec
339339+Avec
287340288341/ $tau$: pour _torque_, la force à donner au moteur
289342/ $tau_"ff"$: le $tau$ "feed-forward", #todo[I de PID ou pas?]
290343/ $Delta q$: écart d'angle de rotation du moteur entre la consigne et l'état actuel
291344/ $Delta dot(q)$: vitesse de changement de la consigne#footnote[
292345293293-#let ddt = derivee => $ ( op("d") #derivee ) / ( op("d") t ) $
346346+ #let ddt = derivee => $ ( op("d") #derivee ) / ( op("d") t ) $
294347295295-On a bien $ddt(Delta q) = Delta dot(q)$ par linéarité de la dérivation temporelle:
348348+ On a bien $ddt(Delta q) = Delta dot(q)$ par linéarité de la dérivation temporelle:
296349297297-$
298298-ddt(Delta q) = ddt(q_"new" - q_"old") = ddt(q_"new") - ddt(q_"old") = Delta ddt(q) = Delta dot(q)
299299-$
350350+ $ ddt(Delta q) = ddt(q_"new" - q_"old") = ddt(q_"new") - ddt(q_"old") = Delta ddt(q) = Delta dot(q) $
300351301301-]
352352+ ]
302353/ $K_p$: prépondérance de la partie proportionelle
303354/ $K_p$: prépondérance de la partie dérivée
304355···314365En pratique, les valeurs actuelles pour le calcul de $Delta q$ et $Delta dot(q)$ proviennent de l'état du moteur, accessible dans `rt/lowstate` avec les champs `q` et `dq` du moteur en question @h1-rt-lowstate
315366316367```cpp
317317- // Avec i l'indice du moteur
368368+ // Avec i l'indice du moteur
318369 auto force = cmdbuf->tau_ff.at(i) + // tau_ff
319370 cmdbuf->kp.at(i) * ( // K_p
320371 cmdbuf->q_target.at(i) - lowstate.motor_state().at(i).q() // Delta q
321321- ) +
372372+ ) +
322373 cmdbuf->kd.at(i) * ( // K_d
323374 cmdbuf->dq_target.at(i) - lowstate.motor_state().at(i).dq() // Delta q.
324375 );
···337388338389#architecture([Phase de réception des commandes], {
339390 edge(<policy>, (2, -1), (2, 0), "-->", label-pos: 10%)[(1A) publish]
340340- edge(<policy>, (2, -1), (2, 0), stroke: none, label-pos: 60%, label-side: left)[(1A) subscription]
391391+ edge(
392392+ <policy>,
393393+ (2, -1),
394394+ (2, 0),
395395+ stroke: none,
396396+ label-pos: 60%,
397397+ label-side: left,
398398+ )[(1A) subscription]
341399 edge((2, 0), <subscriber>, "->")[(2)]
342400 edge(<subscriber>, "->", <lowcmd>)[(3)]
343401 edge(<lowcmd>, "->", <cmdbuf>)[(4)]
···353411/ Si `::PreUpdate` est moins fréquente: Certaines commandes seront simplement ignorées par Gazebo, qui ne vera pas la valeur du buffer avant qu'il change de nouveau.
354412355413// L'initialisation du subscriber se fait pendant l'initialisation du plugin, c'est à dire dans `UnitreePlugin::Configure`. On relie la réception d'un message à une fonction, qui est ici une méthode, `UnitreePlugin::CmdHandler`.
356356-//
414414+//
357415// #dontbreak[
358358-//
416416+//
359417// ```cpp
360360-// ...
361361-//
362362-// void UnitreePlugin::Configure()
363363-// {
418418+// ...
419419+//
420420+// void UnitreePlugin::Configure()
421421+// {
364422// ```
365365-//
423423+//
366424// Instanciation d'un canal
367367-//
425425+//
368426// ```cpp
369369-// ChannelFactory::Instance()->Init(1, "lo" /* loopback interface */);
427427+// ChannelFactory::Instance()->Init(1, "lo" /* loopback interface */);
370428// ```
371371-//
429429+//
372430// Création de $x |-> mono("CmdHandler")(mono("this"), x)$. L'utilitaire `std::bind` permet de passer à `InitChannel` une fonction simple
373373-//
431431+//
374432// ```cpp
375433// auto handler = std::bind(
376434// &UnitreePlugin::CmdHandler,
···378436// std::placeholders::_1
379437// )
380438// ```
381381-//
439439+//
382440// Création du subscriber
383383-//
441441+//
384442// ```cpp
385443// auto subscriber = ChannelSubscriberPtr<LowCmd_>(
386444// new ChannelSubscriber<LowCmd_>("rt/lowcmd")
387445// );
388388-//
389389-//
446446+//
447447+//
390448// subscriber->InitChannel(handler, 1);
391449// }
392450// ```
393393-//
451451+//
394452// Définition du handler
395395-//
453453+//
396454// ```cpp
397397-// void UnitreePlugin::CmdHandler(const void *msg)
455455+// void UnitreePlugin::CmdHandler(const void *msg)
398456// {
399457// LowCmd_ _cmd = *(const LowCmd_ *)msg;
400400-//
458458+//
401459// // Remplissage du buffer interne à la classe
402460// MotorCommand motor_command_tmp;
403403-// for (size_t i = 0; i < H1_NUM_MOTOR; ++i)
461461+// for (size_t i = 0; i < H1_NUM_MOTOR; ++i)
404462// {
405463// motor_command_tau_ff[i] = _cmd.motor_cmd()[i].tau();
406464// ...
407465// }
408408-//
466466+//
409467// this->motor_command_buffer.SetData(motor_command_tmp);
410468// }
411469// ```
412412-//
470470+//
413471// ]
414472415473···423481424482#table(
425483 // columns: (1.5fr, 0.5fr, 3fr, 2fr),
426426- columns: 4,
484484+ columns: 4,
427485 stroke: none,
428486 inset: 8pt,
429487430488 "Champ", "Type", "Description", "Où récupérer la valeur",
431489 table.hline(),
432490433433- `version`, $NN^2$, [Tuple représentant la version d'Unitree], [Expérimentalement],
434434- `mode_pr`, ${0, 1}$, [Défini sur 0 par défaut], [0],
491491+ `version`,
492492+ $NN^2$,
493493+ [Tuple représentant la version d'Unitree],
494494+ [Expérimentalement],
495495+ `mode_pr`, ${0, 1}$, [Défini sur 0 par défaut], [0],
435496 `mode_machine`, ${4, 6}$, [Défini sur 6 par défaut], [6],
436436- `tick`, $NN quad ("ms")$, [Non documenté, proablement le temps écoulé depuis le début de la simulation], [Messages `gz::msgs::Clock` sur le topic Gazebo `/clock` ],
497497+ `tick`,
498498+ $NN quad ("ms")$,
499499+ [Non documenté, proablement le temps écoulé depuis le début de la simulation],
500500+ [Messages `gz::msgs::Clock` sur le topic Gazebo `/clock` ],
437501 `wireless_remote`, ${0, 1}^(40)$, [Non documenté], [_Laissé vide_],
438502 `reserve`, $NN^4$, [Non documenté], [_Laissé vide_],
439439- `crc`, $NN$, [Somme de contrôle du message, utilisant _CRC32_. ], [Implémentation de CRC32 par Unitree #footnote[
440440- Une implémentation ad-hoc existe dans le code source de `unitree_sdk2` et de `unitree_mujoco` #todo[Mettre en annexe ?] #refneeded
441441- ]],
442442- `imu_state…`, "struct.", [Valeurs des capteurs intertiels du robot], [Messages `gz::msgs::IMU` sur le topic Gazebo `/imu`],
443443- ` .quaternion`, $RR^4$, [Posture dans l'espace du robot, dans l'ordre $(w, x, y, z)$], [$w$, $x$, $y$ et $z$ sur `.orientation()`],
444444- ` .rpy`, $RR^3$, [Angle d'Euler du robot, dans l'ordre $(r, p, y)$], `.linear_acceleration()`,
445445- ` .gyroscope`, $RR^3$, todo[], $"atan"_2(2(w x + y z), 1 - 2 (x^2 + y^2) )) \ "asin"(2 (w y - z x)) \ "atan"_2(2(w z + x y), 1 - 2(y^2 + z^2))$,
503503+ `crc`,
504504+ $NN$,
505505+ [Somme de contrôle du message, utilisant _CRC32_. ],
506506+ [Implémentation de CRC32 par Unitree #footnote[
507507+ Une implémentation ad-hoc existe dans le code source de `unitree_sdk2` et de `unitree_mujoco` #todo[Mettre en annexe ?] #refneeded
508508+ ]],
509509+ `imu_state…`,
510510+ "struct.",
511511+ [Valeurs des capteurs intertiels du robot],
512512+ [Messages `gz::msgs::IMU` sur le topic Gazebo `/imu`],
513513+ ` .quaternion`,
514514+ $RR^4$,
515515+ [Posture dans l'espace du robot, dans l'ordre $(w, x, y, z)$],
516516+ [$w$, $x$, $y$ et $z$ sur `.orientation()`],
517517+ ` .rpy`,
518518+ $RR^3$,
519519+ [Angle d'Euler du robot, dans l'ordre $(r, p, y)$],
520520+ `.linear_acceleration()`,
521521+ ` .gyroscope`,
522522+ $RR^3$,
523523+ todo[],
524524+ $"atan"_2(2(w x + y z), 1 - 2 (x^2 + y^2) )) \ "asin"(2 (w y - z x)) \ "atan"_2(2(w z + x y), 1 - 2(y^2 + z^2))$,
446525 ` .accelerometer`, $RR^3$, todo[], `.angular_velocity()`,
447447- `motor_state…`, [$"struct."^(35)$], [Etat de chaque moteur], `gz::sim::Model(…)→joints`,
526526+ `motor_state…`,
527527+ [$"struct."^(35)$],
528528+ [Etat de chaque moteur],
529529+ `gz::sim::Model(…)→joints`,
448530 ` .mode`, ${0, 1}$, [$0$ pour "Brake" et $1$ pour "FOC" #todo[]], [0],
449449- ` .q`, $RR quad ("rad")$, [Angle en radians de rotation du moteur], `.Position()`,
450450- ` .dq`, $RR quad ("rad" dot "s"^(-1))$, [Angle de rotation du moteur], `.Velocity()`,
451451- ` .ddq`, $RR quad ("rad" dot "s"^(-2))$, [Angle de rotation du moteur], [_Laissé vide_],
452452- ` .tau_est`, $RR quad ("N" dot "m")$, [Estimation de la torque #todo[]], [_Laissé vide_],
531531+ ` .q`,
532532+ $RR quad ("rad")$,
533533+ [Angle en radians de rotation du moteur],
534534+ `.Position()`,
535535+ ` .dq`,
536536+ $RR quad ("rad" dot "s"^(-1))$,
537537+ [Angle de rotation du moteur],
538538+ `.Velocity()`,
539539+ ` .ddq`,
540540+ $RR quad ("rad" dot "s"^(-2))$,
541541+ [Angle de rotation du moteur],
542542+ [_Laissé vide_],
543543+ ` .tau_est`,
544544+ $RR quad ("N" dot "m")$,
545545+ [Estimation de la torque #todo[]],
546546+ [_Laissé vide_],
453547)
454548455549···468562 edge(<lowstate>, "->", <publisher>)[(2)]
469563 edge(<publisher>, "->", (1, 0))[(3)]
470564 edge(<policy>, (1, -1), (1, 0), "<--", label-pos: 20%)[(4) subscription]
471471- edge(<policy>, (1, -1), (1, 0), stroke: none, label-pos: 60%, label-side: left)[(4) publish]
565565+ edge(
566566+ <policy>,
567567+ (1, -1),
568568+ (1, 0),
569569+ stroke: none,
570570+ label-pos: 60%,
571571+ label-side: left,
572572+ )[(4) publish]
472573})
473574474575475475-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 éxécuter `LowStateWriter` périodiquement, dans un autre _thread_: on a donc aucune garantie de synchronisation entre les deux.
576576+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 éxécuter `LowStateWriter` périodiquement, dans un autre _thread_: on a donc aucune garantie de synchronisation entre les deux.
476577477578Ici, il y a en plus non pas deux, mais _trois_ boucles indépendantes qui sont en jeux:
478579···484585Similairement à la réception de commandes:
485586486587/ Si `::PreUpdate` est plus fréquente: On perdra des états intermédiaires, la résolution temporelle de l'évolution de l'état du robot disponible pour (ou acceptable par#footnote[
487487- En fonction de si `::LowStateWriter` est plus fréquente que $cal(P)$ (dans ce cas là, c'est ce qui est acceptable par $cal(P)$ qui est limitant) ou inversement (dans ce cas, c'est ce que la boucle du publisher met à disposition de $cal(P)$ qui est limitant)
488488-]) $cal(P)$ sera moins grande
588588+ En fonction de si `::LowStateWriter` est plus fréquente que $cal(P)$ (dans ce cas là, c'est ce qui est acceptable par $cal(P)$ qui est limitant) ou inversement (dans ce cas, c'est ce que la boucle du publisher met à disposition de $cal(P)$ qui est limitant)
589589+ ]) $cal(P)$ sera moins grande
489590/ Si `::PreUpdate` est moins fréquente: $cal(P)$ reçevra plusieurs fois le même état, ce qui sera représentatif du fait que la simulation n'a pas encore avancé.
490591491592492593== Désynchronisations
493594494494-Dans un même appel de `::PreUpdate`, on effectue d'abord la mise à jour du _State buffer_, puis on lit dans le _Commands buffer_.
595595+Dans un même appel de `::PreUpdate`, on effectue d'abord la mise à jour du _State buffer_, puis on lit dans le _Commands buffer_.
495596496496-Un cycle correspond donc à trois boucles indépendantes, représentées ci-après:
597597+Un cycle correspond donc à trois boucles indépendantes, représentées ci-après:
497598498599- Celle de la simulation (en bleu), qui doit englober l'entièreté d'un cycle
499600- Celle du `ChannelPublisher` (en rouge)
500601- Celle de $cal(P)$ (en vert)
501602502502-#architecture([Cycle complet. Un cycle commence avec la flèche "update" partant de `::PreUpdate`], {
503503- let colored-edge = (color, label, ..args) => edge(stroke: color, label: text(fill: color, label), ..args)
504504- let sim-edge = (label, ..args) => colored-edge(blue, label, ..args)
505505- let publisher-edge = (label, ..args) => colored-edge(red, label, ..args)
506506- let policy-edge = (label, ..args) => colored-edge(olive.darken(30%), label, ..args)
603603+#architecture(
604604+ [Cycle complet. Un cycle commence avec la flèche "update" partant de `::PreUpdate`],
605605+ {
606606+ let colored-edge = (color, label, ..args) => edge(
607607+ stroke: color,
608608+ label: text(fill: color, label),
609609+ ..args,
610610+ )
611611+ let sim-edge = (label, ..args) => colored-edge(blue, label, ..args)
612612+ let publisher-edge = (label, ..args) => colored-edge(red, label, ..args)
613613+ let policy-edge = (label, ..args) => colored-edge(
614614+ olive.darken(30%),
615615+ label,
616616+ ..args,
617617+ )
507618508508- // Simulation loop
509509- sim-edge("read", <preupdate>, "d,d,r,r", <cmdbuf>, "<-@")
510510- sim-edge("update", <preupdate.east>, "d", <statebuf>, "->", label-pos: 70%, label-side: right)
619619+ // Simulation loop
620620+ sim-edge("read", <preupdate>, "d,d,r,r", <cmdbuf>, "<-@")
621621+ sim-edge(
622622+ "update",
623623+ <preupdate.east>,
624624+ "d",
625625+ <statebuf>,
626626+ "->",
627627+ label-pos: 70%,
628628+ label-side: right,
629629+ )
511630512512- // lowstate publisher loop
513513- publisher-edge("read", <statebuf>, "@->", <lowstate>)
514514- publisher-edge("", <lowstate>, "-", <publisher>)
515515- publisher-edge("", <publisher>, (1, 0), <channelfactory.west>, "->")
631631+ // lowstate publisher loop
632632+ publisher-edge("read", <statebuf>, "@->", <lowstate>)
633633+ publisher-edge("", <lowstate>, "-", <publisher>)
634634+ publisher-edge("", <publisher>, (1, 0), <channelfactory.west>, "->")
516635517517- // policy loop
518518- // dds part
519519- policy-edge("commands", <policy>, (2.25, -1), (2.25, 0), <channelfactory.east>, "-->", label-pos: 10%)
520520- policy-edge("state", <policy>, (0, 0), <channelfactory.west>, "<--@", label-pos: 80%)
521521- // non-dds part
522522- policy-edge("", <channelfactory.east>, (2, 0), <subscriber>, "->")
523523- policy-edge("", <subscriber>, "-", <lowcmd>)
524524- policy-edge("update", <lowcmd>, "->", <cmdbuf>)
525525-})
636636+ // policy loop
637637+ // dds part
638638+ policy-edge(
639639+ "commands",
640640+ <policy>,
641641+ (2.25, -1),
642642+ (2.25, 0),
643643+ <channelfactory.east>,
644644+ "-->",
645645+ label-pos: 10%,
646646+ )
647647+ policy-edge(
648648+ "state",
649649+ <policy>,
650650+ (0, 0),
651651+ <channelfactory.west>,
652652+ "<--@",
653653+ label-pos: 80%,
654654+ )
655655+ // non-dds part
656656+ policy-edge("", <channelfactory.east>, (2, 0), <subscriber>, "->")
657657+ policy-edge("", <subscriber>, "-", <lowcmd>)
658658+ policy-edge("update", <lowcmd>, "->", <cmdbuf>)
659659+ },
660660+)
526661527662Ces désynchronisations pourraient expliquer les problèmes de performance recontrés (cf @perf)
528663···534669535670== Amélioration des performances <perf>
536671537537-Les premiers essais affichent un
672672+Les premiers essais affichent un
538673539674== Enregistrement de vidéos <video>
540675
+17-11
rapport/main.typ
···88)
991010#show terms: it => grid(
1111- columns: 2, row-gutter: 1em, column-gutter: (15pt, 0pt), align: (left, left),
1212- ..it.children.map(item =>
1313- (strong(item.term), item.description)
1414- ).flatten()
1515- )
1111+ columns: 2, row-gutter: 1em, column-gutter: (15pt, 0pt), align: (left, left),
1212+ ..it.children.map(item => (strong(item.term), item.description)).flatten()
1313+)
161417151816#let imagefigure(path, caption, size: 100%) = figure(
···6765#show ref: it => {
6866 let eq = math.equation
6967 let el = it.element
7070- let appendix_root = if el == none { 0 } else { counter("appendices").at(el.location()).at(0) }
6868+ let appendix_root = if el == none { 0 } else {
6969+ counter("appendices").at(el.location()).at(0)
7070+ }
7171 if el != none and el.func() == eq {
7272 // Override equation references.
7373 numbering(
7474 el.numbering,
7575- ..counter(eq).at(el.location())
7575+ ..counter(eq).at(el.location()),
7676 )
7777 } else if el != none and appendix_root != 0 {
7878- let letter = numbering(el.numbering, counter("appendices").at(el.location()).at(0))
7979- let heading_path = numbering("1.1", ..counter(heading).at(el.location()).slice(1))
7878+ let letter = numbering(
7979+ el.numbering,
8080+ counter("appendices").at(el.location()).at(0),
8181+ )
8282+ let heading_path = numbering(
8383+ "1.1",
8484+ ..counter(heading).at(el.location()).slice(1),
8585+ )
8086 let path = letter + "." + heading_path
8187 if appendix_root == 1 {
8288 [preuve en #path]
···117123118124#pagebreak()
119125120120-= Remerciements
126126+= Remerciements
121127122128#outline()
123129···125131126132#include "context.typ"
127133128128-= Packaging reproductible avec Nix
134134+= Packaging reproductible avec Nix
129135130136131137#include "nix.typ"
+4-3
rapport/nix.typ
···11-#import "utils.typ": todo, comment, refneeded
11+#import "utils.typ": comment, refneeded, todo
2233== Reproductibilité
44···8484 )
8585 ```,
86868787- [ *Python* (`if` et `else` sont des instructions) ], [ *OCaml* (`if` et `else` forment une expression) ],
8787+ [ *Python* (`if` et `else` sont des instructions) ],
8888+ [ *OCaml* (`if` et `else` forment une expression) ],
8889)
89909091Afin de décrire les dépendances d'un programme, l'environnement de compilation, et les étapes pour le compiler (en somme, afin de définir le $f in "bin"^"src"$), Nix comprend un langage d'expressions @nix-language. Un fichier `.nix` définit une fonction, que Nix sait exécuter pour compiler le code source.
···181182182183Ici encore, cela apporte un gain en terme de reproductibilité: l'état de configuration de l'OS sur lequel est déployé le programme du robot est, lui aussi, rendu reproductible.
183184184184-== Packaging Nix pour _gz-unitree_
185185+== Packaging Nix pour _gz-unitree_
185186186187#todo[Faire cette partie]