Un tutoriel d'introduction à la programmation de robots
Publié: 2022-03-11Avouons-le, les robots sont cool. Ils vont aussi diriger le monde un jour, et j'espère qu'à ce moment-là, ils auront pitié de leurs pauvres créateurs charnus et doux (c'est-à-dire des développeurs de robotique) et nous aideront à construire une utopie spatiale remplie d'abondance. Je plaisante bien sûr, mais seulement en quelque sorte.
Dans mon ambition d'avoir une petite influence sur le sujet, j'ai suivi l'année dernière un cours sur la théorie du contrôle des robots autonomes, qui a abouti à la construction d'un simulateur robotique basé sur Python qui m'a permis de pratiquer la théorie du contrôle sur un robot simple, mobile et programmable. .
Dans cet article, je vais montrer comment utiliser un framework de robot Python pour développer un logiciel de contrôle, décrire le schéma de contrôle que j'ai développé pour mon robot simulé, illustrer comment il interagit avec son environnement et atteint ses objectifs, et discuter de certains des défis fondamentaux de la programmation robotique que j'ai rencontrés en cours de route.
Afin de suivre ce tutoriel sur la programmation robotique pour débutants, vous devez avoir une connaissance de base de deux choses :
- Mathématiques - nous utiliserons des fonctions et des vecteurs trigonométriques
- Python, puisque Python fait partie des langages de programmation de base les plus populaires pour les robots, nous utiliserons les bibliothèques et les fonctions de base de Python.
Les extraits de code présentés ici ne sont qu'une partie de l'ensemble du simulateur, qui repose sur des classes et des interfaces. Par conséquent, pour lire le code directement, vous aurez peut-être besoin d'une certaine expérience en Python et en programmation orientée objet.
Enfin, les sujets facultatifs qui vous aideront à mieux suivre ce tutoriel sont de savoir ce qu'est une machine d'état et comment fonctionnent les capteurs de distance et les encodeurs.
Le défi du robot programmable : perception contre réalité et la fragilité du contrôle
Le défi fondamental de toute robotique est le suivant : il est impossible de connaître le véritable état de l'environnement. Le logiciel de contrôle du robot ne peut que deviner l'état du monde réel sur la base des mesures renvoyées par ses capteurs. Il ne peut tenter de changer l'état du monde réel qu'en générant des signaux de contrôle.
Ainsi, l'une des premières étapes de la conception de la commande consiste à proposer une abstraction du monde réel, connue sous le nom de modèle , avec laquelle interpréter les lectures de nos capteurs et prendre des décisions. Tant que le monde réel se comporte selon les hypothèses du modèle, nous pouvons faire de bonnes suppositions et exercer un contrôle. Cependant, dès que le monde réel s'écartera de ces hypothèses, nous ne pourrons plus faire de bonnes suppositions et le contrôle sera perdu. Souvent, une fois que le contrôle est perdu, il ne peut jamais être retrouvé. (À moins qu'une force extérieure bienveillante ne le restaure.)
C'est l'une des principales raisons pour lesquelles la programmation robotique est si difficile. Nous voyons souvent des vidéos du dernier robot de recherche dans le laboratoire, réalisant des exploits fantastiques de dextérité, de navigation ou de travail d'équipe, et nous sommes tentés de demander : "Pourquoi n'est-ce pas utilisé dans le monde réel ?" Eh bien, la prochaine fois que vous verrez une telle vidéo, regardez à quel point l'environnement du laboratoire est hautement contrôlé. Dans la plupart des cas, ces robots ne sont capables d'accomplir ces tâches impressionnantes que tant que les conditions environnementales restent dans les limites étroites de leur modèle interne. Ainsi, l'une des clés de l'avancement de la robotique est le développement de modèles plus complexes, flexibles et robustes - et cet avancement est soumis aux limites des ressources de calcul disponibles.
[Note latérale : les philosophes et les psychologues remarqueraient que les êtres vivants souffrent également de la dépendance de leur propre perception interne de ce que leurs sens leur disent. De nombreuses avancées en robotique proviennent de l'observation de créatures vivantes et de la façon dont elles réagissent à des stimuli inattendus. Pensez-y. Quel est votre modèle interne du monde ? C'est différent de celui d'une fourmi, et de celui d'un poisson ? (Espérons-le.) Cependant, comme la fourmi et le poisson, il est susceptible de simplifier à l'extrême certaines réalités du monde. Lorsque vos hypothèses sur le monde ne sont pas correctes, cela peut vous exposer au risque de perdre le contrôle des choses. Parfois, nous appelons cela « danger ». De la même manière que notre petit robot lutte pour survivre contre l'univers inconnu, nous le faisons tous. C'est un aperçu puissant pour les roboticiens.]
Le simulateur de robot programmable
Le simulateur que j'ai construit est écrit en Python et très intelligemment baptisé Sobot Rimulator . Vous pouvez trouver la v1.0.0 sur GitHub. Il n'a pas beaucoup de cloches et de sifflets, mais il est conçu pour faire une chose très bien : fournir une simulation précise d'un robot mobile et donner à un aspirant roboticien un cadre simple pour pratiquer la programmation de logiciels de robot. Bien qu'il soit toujours préférable d'avoir un vrai robot avec lequel jouer, un bon simulateur de robot Python est beaucoup plus accessible et constitue un excellent point de départ.
Dans les robots du monde réel, le logiciel qui génère les signaux de commande (le « contrôleur ») doit fonctionner à une vitesse très élevée et effectuer des calculs complexes. Cela affecte le choix des langages de programmation de robots qu'il est préférable d'utiliser : habituellement, C++ est utilisé pour ce type de scénarios, mais dans les applications robotiques plus simples, Python est un très bon compromis entre la vitesse d'exécution et la facilité de développement et de test.
Le logiciel que j'ai écrit simule un robot de recherche réel appelé Khepera, mais il peut être adapté à une gamme de robots mobiles de dimensions et de capteurs différents. Depuis que j'ai essayé de programmer le simulateur de manière aussi similaire que possible aux capacités du robot réel, la logique de contrôle peut être chargée dans un vrai robot Khepera avec un refactoring minimal, et il fonctionnera de la même manière que le robot simulé. Les spécificités implémentées se réfèrent au Khepera III, mais elles peuvent être facilement adaptées au nouveau Khepera IV.
En d'autres termes, la programmation d'un robot simulé est analogue à la programmation d'un robot réel. Ceci est essentiel si le simulateur doit être utile pour développer et évaluer différentes approches logicielles de contrôle.
Dans ce didacticiel, je décrirai l'architecture logicielle de contrôle de robot fournie avec la v1.0.0 de Sobot Rimulator et fournirai des extraits de la source Python (avec de légères modifications pour plus de clarté). Cependant, je vous encourage à plonger dans la source et à déconner. Le simulateur a été bifurqué et utilisé pour contrôler différents robots mobiles, dont un Roomba2 d'iRobot. De même, n'hésitez pas à bifurquer le projet et à l'améliorer.
La logique de contrôle du robot est limitée à ces classes/fichiers Python :
-
models/supervisor.py
— cette classe est responsable de l'interaction entre le monde simulé autour du robot et le robot lui-même. Il fait évoluer notre machine d'état de robot et déclenche les contrôleurs pour calculer le comportement souhaité. -
models/supervisor_state_machine.py
— cette classe représente les différents états dans lesquels le robot peut se trouver, selon son interprétation des capteurs. - Les fichiers du répertoire
models/controllers
— ces classes implémentent différents comportements du robot en fonction d'un état connu de l'environnement. En particulier, un contrôleur spécifique est sélectionné en fonction de la machine d'état.
Le but
Les robots, comme les gens, ont besoin d'un but dans la vie. Le but de notre logiciel contrôlant ce robot sera très simple : il tentera de se frayer un chemin jusqu'à un point d'arrivée prédéterminé. C'est généralement la caractéristique de base que tout robot mobile devrait avoir, des voitures autonomes aux aspirateurs robotiques. Les coordonnées de l'objectif sont programmées dans le logiciel de contrôle avant l'activation du robot mais pourraient être générées à partir d'une application Python supplémentaire qui supervise les mouvements du robot. Par exemple, pensez à traverser plusieurs waypoints.
Cependant, pour compliquer les choses, l'environnement du robot peut être semé d'obstacles. Le robot NE PEUT PAS entrer en collision avec un obstacle sur son chemin vers le but. Ainsi, si le robot rencontre un obstacle, il devra trouver son chemin pour pouvoir poursuivre sa route vers l'objectif.
Le robot programmable
Chaque robot est livré avec des capacités et des problèmes de contrôle différents. Familiarisons-nous avec notre robot programmable simulé.
La première chose à noter est que, dans ce guide, notre robot sera un robot mobile autonome . Cela signifie qu'il se déplacera librement dans l'espace et qu'il le fera sous son propre contrôle. Cela contraste avec, par exemple, un robot télécommandé (qui n'est pas autonome) ou un bras de robot d'usine (qui n'est pas mobile). Notre robot doit trouver par lui-même comment atteindre ses objectifs et survivre dans son environnement. Cela s'avère être un défi étonnamment difficile pour les programmeurs robotiques novices.
Entrées de contrôle : capteurs
Il existe de nombreuses façons d'équiper un robot pour surveiller son environnement. Ceux-ci peuvent inclure n'importe quoi, des capteurs de proximité, des capteurs de lumière, des pare-chocs, des caméras, etc. De plus, les robots peuvent communiquer avec des capteurs externes qui leur donnent des informations qu'eux-mêmes ne peuvent pas observer directement.
Notre robot de référence est équipé de neuf capteurs infrarouges — le nouveau modèle a huit capteurs de proximité infrarouges et cinq à ultrasons — disposés en « jupe » dans toutes les directions. Il y a plus de capteurs face à l'avant du robot qu'à l'arrière car il est généralement plus important pour le robot de savoir ce qui se trouve devant lui que ce qui se trouve derrière lui.
En plus des capteurs de proximité, le robot dispose d'une paire de tickers de roue qui suivent le mouvement de la roue. Ceux-ci vous permettent de suivre le nombre de rotations effectuées par chaque roue, un tour complet vers l'avant d'une roue étant de 2 765 ticks. Les tours dans la direction opposée comptent à rebours, diminuant le nombre de ticks au lieu de l'augmenter. Vous n'avez pas à vous soucier des nombres spécifiques dans ce tutoriel car le logiciel que nous allons écrire utilise la distance parcourue exprimée en mètres. Plus tard, je vous montrerai comment le calculer à partir de ticks avec une fonction Python simple.
Sorties de contrôle : Mobilité
Certains robots se déplacent sur pattes. Certains roulent comme une balle. Certains glissent même comme un serpent.
Notre robot est un robot à entraînement différentiel, c'est-à-dire qu'il roule sur deux roues. Lorsque les deux roues tournent à la même vitesse, le robot se déplace en ligne droite. Lorsque les roues se déplacent à des vitesses différentes, le robot tourne. Ainsi, contrôler le mouvement de ce robot revient à bien contrôler les vitesses de rotation de chacune de ces deux roues.
API
Dans Sobot Rimulator, la séparation entre l'"ordinateur" du robot et le monde physique (simulé) est matérialisée par le fichier robot_supervisor_interface.py
, qui définit l'intégralité de l'API pour interagir avec les capteurs et les moteurs du "vrai robot" :
-
read_proximity_sensors()
renvoie un tableau de neuf valeurs au format natif des capteurs -
read_wheel_encoders()
renvoie un tableau de deux valeurs indiquant le nombre total de ticks depuis le début -
set_wheel_drive_rates( v_l, v_r )
prend deux valeurs (en radians par seconde) et définit la vitesse gauche et droite des roues à ces deux valeurs
Cette interface utilise en interne un objet robot qui fournit les données des capteurs et la possibilité de déplacer des moteurs ou des roues. Si vous souhaitez créer un robot différent, il vous suffit de fournir une classe de robot Python différente pouvant être utilisée par la même interface, et le reste du code (contrôleurs, superviseur et simulateur) fonctionnera immédiatement !
Le simulateur
Comme vous utiliseriez un vrai robot dans le monde réel sans prêter trop d'attention aux lois de la physique impliquées, vous pouvez ignorer la façon dont le robot est simulé et passer directement à la programmation du logiciel du contrôleur, car ce sera presque le même. entre le monde réel et une simulation. Mais si vous êtes curieux, je vais le présenter brièvement ici.
Le fichier world.py
est une classe Python qui représente le monde simulé, avec des robots et des obstacles à l'intérieur. La fonction step à l'intérieur de cette classe s'occupe de faire évoluer notre monde simple en :
- Appliquer les règles de la physique aux mouvements du robot
- Considérer les collisions avec des obstacles
- Fournir de nouvelles valeurs pour les capteurs du robot
En fin de compte, il appelle les superviseurs de robots chargés d'exécuter le logiciel du cerveau du robot.
La fonction step est exécutée dans une boucle de sorte que robot.step_motion()
déplace le robot en utilisant la vitesse de roue calculée par le superviseur lors de l'étape de simulation précédente.
# step the simulation through one time interval def step( self ): dt = self.dt # step all the robots for robot in self.robots: # step robot motion robot.step_motion( dt ) # apply physics interactions self.physics.apply_physics() # NOTE: The supervisors must run last to ensure they are observing the "current" world # step all of the supervisors for supervisor in self.supervisors: supervisor.step( dt ) # increment world time self.world_time += dt
La fonction apply_physics()
met à jour en interne les valeurs des capteurs de proximité du robot afin que le superviseur puisse estimer l'environnement à l'étape de simulation en cours. Les mêmes concepts s'appliquent aux encodeurs.
Un modèle simple
Tout d'abord, notre robot aura un modèle très simple. Il fera de nombreuses hypothèses sur le monde. Certains des plus importants incluent:
- Le terrain est toujours plat et uniforme
- Les obstacles ne sont jamais ronds
- Les roues ne glissent jamais
- Rien ne va jamais pousser le robot
- Les capteurs ne tombent jamais en panne ou ne donnent jamais de fausses lectures
- Les roues tournent toujours quand on leur dit de
Bien que la plupart de ces hypothèses soient raisonnables dans un environnement semblable à une maison, des obstacles ronds pourraient être présents. Notre logiciel d'évitement d'obstacles a une implémentation simple et suit la frontière des obstacles afin de les contourner. Nous indiquerons aux lecteurs comment améliorer le cadre de contrôle de notre robot avec une vérification supplémentaire pour éviter les obstacles circulaires.
La boucle de contrôle
Nous allons maintenant entrer dans le coeur de notre logiciel de contrôle et expliquer les comportements que nous voulons programmer à l'intérieur du robot. Des comportements supplémentaires peuvent être ajoutés à ce cadre, et vous devriez essayer vos propres idées après avoir fini de lire ! Un logiciel de robotique basée sur le comportement a été proposé il y a plus de 20 ans et c'est toujours un outil puissant pour la robotique mobile. À titre d'exemple, en 2007, un ensemble de comportements a été utilisé dans le DARPA Urban Challenge, la première compétition pour les voitures à conduite autonome !
Un robot est un système dynamique. L'état du robot, les lectures de ses capteurs et les effets de ses signaux de commande sont en constante évolution. Le contrôle du déroulement des événements implique les trois étapes suivantes :
- Appliquer les signaux de contrôle.
- Mesurez les résultats.
- Générer de nouveaux signaux de contrôle calculés pour nous rapprocher de notre objectif.
Ces étapes sont répétées encore et encore jusqu'à ce que nous ayons atteint notre objectif. Plus nous pouvons faire cela par seconde, plus nous aurons un contrôle précis sur le système. Le robot Sobot Rimulator répète ces étapes 20 fois par seconde (20 Hz), mais de nombreux robots doivent le faire des milliers ou des millions de fois par seconde afin d'avoir un contrôle adéquat. Rappelez-vous notre introduction précédente sur les différents langages de programmation de robots pour différents systèmes robotiques et exigences de vitesse.
En général, chaque fois que notre robot prend des mesures avec ses capteurs, il utilise ces mesures pour mettre à jour son estimation interne de l'état du monde, par exemple, la distance de son objectif. Il compare cet état à une valeur de référence de ce qu'il veut que l'état soit (pour la distance, il veut qu'il soit nul), et calcule l'erreur entre l'état souhaité et l'état réel. Une fois ces informations connues, la génération de nouveaux signaux de commande peut être réduite à un problème de minimisation de l'erreur qui finira par déplacer le robot vers le but.
Une astuce astucieuse : simplifier le modèle
Pour contrôler le robot que nous voulons programmer, nous devons envoyer un signal à la roue gauche lui indiquant à quelle vitesse tourner, et un signal séparé à la roue droite lui indiquant à quelle vitesse tourner. Appelons ces signaux v L et v R . Cependant, penser constamment en termes de v L et v R est très lourd. Au lieu de demander : « À quelle vitesse voulons-nous que la roue gauche tourne et à quelle vitesse voulons-nous que la roue droite tourne ? il est plus naturel de demander : « À quelle vitesse voulons-nous que le robot avance, et à quelle vitesse voulons-nous qu'il tourne ou change de cap ? Appelons ces paramètres vitesse v et vitesse angulaire (de rotation) ω (lire « oméga »). Il s'avère que nous pouvons baser tout notre modèle sur v et ω au lieu de v L et v R , et seulement une fois que nous avons déterminé comment nous voulons que notre robot programmé se déplace, transformer mathématiquement ces deux valeurs en v L et v R dont nous avons besoin pour contrôler réellement les roues du robot. C'est ce qu'on appelle un modèle de contrôle monocycle .
Voici le code Python qui implémente la transformation finale dans supervisor.py
. Notez que si ω vaut 0, les deux roues tourneront à la même vitesse :
# generate and send the correct commands to the robot def _send_robot_commands( self ): # ... v_l, v_r = self._uni_to_diff( v, omega ) self.robot.set_wheel_drive_rates( v_l, v_r ) def _uni_to_diff( self, v, omega ): # v = translational velocity (m/s) # omega = angular velocity (rad/s) R = self.robot_wheel_radius L = self.robot_wheel_base_length v_l = ( (2.0 * v) - (omega*L) ) / (2.0 * R) v_r = ( (2.0 * v) + (omega*L) ) / (2.0 * R) return v_l, v_r
Estimation de l'état : Robot, connais-toi toi-même
A l'aide de ses capteurs, le robot doit essayer d'estimer l'état de l'environnement ainsi que son propre état. Ces estimations ne seront jamais parfaites, mais elles doivent être assez bonnes car le robot basera toutes ses décisions sur ces estimations. En utilisant uniquement ses capteurs de proximité et ses tickers de roue, il doit essayer de deviner ce qui suit :
- La direction des obstacles
- La distance des obstacles
- La position du robot
- Le cap du robot
Les deux premières propriétés sont déterminées par les lectures du capteur de proximité et sont assez simples. La fonction API read_proximity_sensors()
renvoie un tableau de neuf valeurs, une pour chaque capteur. Nous savons à l'avance que la septième lecture, par exemple, correspond au capteur qui pointe à 75 degrés à droite du robot.
Ainsi, si cette valeur affiche une lecture correspondant à une distance de 0,1 mètre, nous savons qu'il y a un obstacle à 0,1 mètre, 75 degrés vers la gauche. S'il n'y a pas d'obstacle, le capteur renverra une lecture de sa portée maximale de 0,2 mètre. Ainsi, si nous lisons 0,2 mètre sur le capteur sept, nous supposerons qu'il n'y a en fait aucun obstacle dans cette direction.

En raison du fonctionnement des capteurs infrarouges (mesure de la réflexion infrarouge), les nombres qu'ils renvoient sont une transformation non linéaire de la distance réelle détectée. Ainsi, la fonction Python de détermination de la distance indiquée doit convertir ces relevés en mètres. Cela se fait dans supervisor.py
comme suit :
# update the distances indicated by the proximity sensors def _update_proximity_sensor_distances( self ): self.proximity_sensor_distances = [ 0.02-( log(readval/3960.0) )/30.0 for readval in self.robot.read_proximity_sensors() ]
Encore une fois, nous avons un modèle de capteur spécifique dans ce cadre de robot Python, tandis que dans le monde réel, les capteurs sont accompagnés d'un logiciel qui devrait fournir des fonctions de conversion similaires de valeurs non linéaires en mètres.
Déterminer la position et le cap du robot (ensemble connu sous le nom de pose dans la programmation robotique) est un peu plus difficile. Notre robot utilise l'odométrie pour estimer sa pose. C'est là que les tickers de roue entrent en jeu. En mesurant combien chaque roue a tourné depuis la dernière itération de la boucle de contrôle, il est possible d'obtenir une bonne estimation de la façon dont la pose du robot a changé, mais seulement si le changement est petit .
C'est l'une des raisons pour lesquelles il est important d'itérer très fréquemment la boucle de contrôle dans un robot du monde réel, où les moteurs déplaçant les roues peuvent ne pas être parfaits. Si nous avons attendu trop longtemps pour mesurer les tickers des roues, les deux roues auraient pu faire beaucoup, et il sera impossible d'estimer où nous nous sommes retrouvés.
Compte tenu de notre simulateur logiciel actuel, nous pouvons nous permettre d'exécuter le calcul d'odométrie à 20 Hz, la même fréquence que les contrôleurs. Mais ce pourrait être une bonne idée d'avoir un thread Python séparé fonctionnant plus rapidement pour attraper les petits mouvements des tickers.
Vous trouverez ci-dessous la fonction d'odométrie complète dans supervisor.py
qui met à jour l'estimation de la pose du robot. Notez que la pose du robot est composée des coordonnées x
et y
, et du cap theta
, qui est mesuré en radians à partir de l'axe X positif. Le x
positif est à l'est et le y
positif au nord. Ainsi, un cap de 0
indique que le robot fait directement face à l'est. Le robot suppose toujours que sa pose initiale est (0, 0), 0
.
# update the estimated position of the robot using it's wheel encoder readings def _update_odometry( self ): R = self.robot_wheel_radius N = float( self.wheel_encoder_ticks_per_revolution ) # read the wheel encoder values ticks_left, ticks_right = self.robot.read_wheel_encoders() # get the difference in ticks since the last iteration d_ticks_left = ticks_left - self.prev_ticks_left d_ticks_right = ticks_right - self.prev_ticks_right # estimate the wheel movements d_left_wheel = 2*pi*R*( d_ticks_left / N ) d_right_wheel = 2*pi*R*( d_ticks_right / N ) d_center = 0.5 * ( d_left_wheel + d_right_wheel ) # calculate the new pose prev_x, prev_y, prev_theta = self.estimated_pose.scalar_unpack() new_x = prev_x + ( d_center * cos( prev_theta ) ) new_y = prev_y + ( d_center * sin( prev_theta ) ) new_theta = prev_theta + ( ( d_right_wheel - d_left_wheel ) / self.robot_wheel_base_length ) # update the pose estimate with the new values self.estimated_pose.scalar_update( new_x, new_y, new_theta ) # save the current tick count for the next iteration self.prev_ticks_left = ticks_left self.prev_ticks_right = ticks_right
Maintenant que notre robot est capable de générer une bonne estimation du monde réel, utilisons ces informations pour atteindre nos objectifs.
Méthodes de programmation de robots Python – Comportement Go-to-Goal
Le but suprême de l'existence de notre petit robot dans ce didacticiel de programmation est d'atteindre le but. Alors, comment faire tourner les roues pour y arriver ? Commençons par simplifier un peu notre vision du monde et supposons qu'il n'y a pas d'obstacles sur le chemin.
Cela devient alors une tâche simple et peut être facilement programmé en Python. Si on avance en faisant face au but, on y arrivera. Grâce à notre odométrie, nous connaissons nos coordonnées et notre cap actuels. Nous savons également quelles sont les coordonnées de l'objectif car elles ont été préprogrammées. Par conséquent, en utilisant un peu d'algèbre linéaire, nous pouvons déterminer le vecteur de notre emplacement au but, comme dans go_to_goal_controller.py
:
# return a go-to-goal heading vector in the robot's reference frame def calculate_gtg_heading_vector( self ): # get the inverse of the robot's pose robot_inv_pos, robot_inv_theta = self.supervisor.estimated_pose().inverse().vector_unpack() # calculate the goal vector in the robot's reference frame goal = self.supervisor.goal() goal = linalg.rotate_and_translate_vector( goal, robot_inv_theta, robot_inv_pos ) return goal
Notez que nous obtenons le vecteur vers le but dans le cadre de référence du robot et NON dans les coordonnées mondiales. Si le but est sur l'axe X dans le cadre de référence du robot, cela signifie qu'il est directement devant le robot. Ainsi, l'angle de ce vecteur par rapport à l'axe X est la différence entre notre cap et le cap sur lequel nous voulons être. En d'autres termes, c'est l' erreur entre notre état actuel et ce que nous voulons que notre état actuel soit. Nous voulons donc ajuster notre taux de virage ω pour que l'angle entre notre cap et le but évolue vers 0. Nous voulons minimiser l'erreur :
# calculate the error terms theta_d = atan2( self.gtg_heading_vector[1], self.gtg_heading_vector[0] ) # calculate angular velocity omega = self.kP * theta_d
self.kP
dans l'extrait ci-dessus de l'implémentation Python du contrôleur est un gain de contrôle. C'est un coefficient qui détermine la vitesse à laquelle nous tournons proportionnellement à la distance qui nous sépare du but auquel nous sommes confrontés. Si l' erreur dans notre cap est de 0
, alors le taux de virage est également de 0
. Dans la vraie fonction Python à l'intérieur du fichier go_to_goal_controller.py
, vous verrez des gains plus similaires, puisque nous avons utilisé un contrôleur PID au lieu d'un simple coefficient proportionnel.
Maintenant que nous avons notre vitesse angulaire ω , comment déterminons-nous notre vitesse d'avancement v ? Une bonne règle générale est celle que vous connaissez probablement instinctivement : si nous ne tournons pas, nous pouvons avancer à pleine vitesse, puis plus nous tournons vite, plus nous devons ralentir. Cela nous aide généralement à maintenir la stabilité de notre système et à agir dans les limites de notre modèle. Ainsi, v est une fonction de ω . Dans go_to_goal_controller.py
l'équation est :
# calculate translational velocity # velocity is v_max when omega is 0, # drops rapidly to zero as |omega| rises v = self.supervisor.v_max() / ( abs( omega ) + 1 )**0.5
Une suggestion pour développer cette formule est de considérer que nous ralentissons généralement lorsque nous approchons du but afin de l'atteindre avec une vitesse nulle. Comment cette formule changerait-elle ? Il doit inclure en quelque sorte un remplacement de v_max()
par quelque chose de proportionnel à la distance. OK, nous avons presque terminé une seule boucle de contrôle. Il ne reste plus qu'à transformer ces deux paramètres du modèle de monocycle en vitesses différentielles des roues et à envoyer les signaux aux roues. Voici un exemple de la trajectoire du robot sous le contrôleur d'aller au but, sans obstacles :
Comme nous pouvons le voir, le vecteur vers l'objectif est une référence efficace sur laquelle baser nos calculs de contrôle. C'est une représentation interne de "où nous voulons aller". Comme nous le verrons, la seule différence majeure entre aller vers l'objectif et les autres comportements est que parfois aller vers l'objectif est une mauvaise idée, nous devons donc calculer un vecteur de référence différent.
Méthodes de programmation de robots Python – Comportement d'évitement d'obstacles
Aller vers le but quand il y a un obstacle dans cette direction est un bon exemple. Au lieu de foncer tête baissée dans les choses sur notre chemin, essayons de programmer une loi de commande qui oblige le robot à les éviter.
Pour simplifier le scénario, oublions maintenant complètement le point de but et faisons simplement de ce qui suit notre objectif : lorsqu'il n'y a pas d'obstacles devant nous, avançons. Lorsqu'un obstacle est rencontré, détournez-vous de lui jusqu'à ce qu'il ne soit plus devant nous.
Par conséquent, lorsqu'il n'y a pas d'obstacle devant nous, nous voulons que notre vecteur de référence pointe simplement vers l'avant. Alors ω sera nul et v sera la vitesse maximale. Cependant, dès que nous détectons un obstacle avec nos capteurs de proximité, nous voulons que le vecteur de référence pointe dans la direction qui s'éloigne de l'obstacle. Cela fera monter ω pour nous éloigner de l'obstacle, et fera chuter v pour s'assurer que nous ne rencontrons pas accidentellement l'obstacle dans le processus.
Une façon astucieuse de générer notre vecteur de référence souhaité consiste à transformer nos neuf lectures de proximité en vecteurs et à prendre une somme pondérée. Lorsqu'aucun obstacle n'est détecté, les vecteurs s'additionnent de manière symétrique, ce qui donne un vecteur de référence qui pointe droit devant comme souhaité. Mais si un capteur sur, disons, le côté droit capte un obstacle, il contribuera un vecteur plus petit à la somme, et le résultat sera un vecteur de référence qui est décalé vers la gauche.
Pour un robot général avec un placement différent des capteurs, la même idée peut être appliquée mais peut nécessiter des changements dans les poids et/ou des précautions supplémentaires lorsque les capteurs sont symétriques à l'avant et à l'arrière du robot, car la somme pondérée pourrait devenir nulle .
Voici le code qui fait cela dans avoid_obstacles_controller.py
:
# sensor gains (weights) self.sensor_gains = [ 1.0+( (0.4*abs(p.theta)) / pi ) for p in supervisor.proximity_sensor_placements() ] # ... # return an obstacle avoidance vector in the robot's reference frame # also returns vectors to detected obstacles in the robot's reference frame def calculate_ao_heading_vector( self ): # initialize vector obstacle_vectors = [ [ 0.0, 0.0 ] ] * len( self.proximity_sensor_placements ) ao_heading_vector = [ 0.0, 0.0 ] # get the distances indicated by the robot's sensor readings sensor_distances = self.supervisor.proximity_sensor_distances() # calculate the position of detected obstacles and find an avoidance vector robot_pos, robot_theta = self.supervisor.estimated_pose().vector_unpack() for i in range( len( sensor_distances ) ): # calculate the position of the obstacle sensor_pos, sensor_theta = self.proximity_sensor_placements[i].vector_unpack() vector = [ sensor_distances[i], 0.0 ] vector = linalg.rotate_and_translate_vector( vector, sensor_theta, sensor_pos ) obstacle_vectors[i] = vector # store the obstacle vectors in the robot's reference frame # accumulate the heading vector within the robot's reference frame ao_heading_vector = linalg.add( ao_heading_vector, linalg.scale( vector, self.sensor_gains[i] ) ) return ao_heading_vector, obstacle_vectors
En utilisant le ao_heading_vector
résultant comme référence pour le robot à essayer de faire correspondre, voici les résultats de l'exécution du logiciel du robot en simulation en utilisant uniquement le contrôleur d'évitement d'obstacles, en ignorant complètement le point d'objectif. Le robot rebondit sans but, mais il n'entre jamais en collision avec un obstacle et parvient même à naviguer dans des espaces très restreints :
Méthodes de programmation de robots Python : automates hybrides (machine d'état de comportement)
Jusqu'à présent, nous avons décrit deux comportements - atteindre l'objectif et éviter les obstacles - isolément. Les deux remplissent admirablement leur fonction, mais pour réussir à atteindre l'objectif dans un environnement plein d'obstacles, nous devons les combiner.
La solution que nous développerons réside dans une classe de machines qui porte la désignation extrêmement cool d' automates hybrides . Un automate hybride est programmé avec plusieurs comportements différents, ou modes, ainsi qu'une machine à états de supervision. La machine à états de supervision passe d'un mode à un autre à des moments discrets (lorsque les objectifs sont atteints ou que l'environnement a soudainement trop changé), tandis que chaque comportement utilise des capteurs et des roues pour réagir en permanence aux changements d'environnement. La solution a été qualifiée d'hybride car elle évolue à la fois de manière discrète et continue.
Notre framework de robot Python implémente la machine d'état dans le fichier supervisor_state_machine.py
.
Équipé de nos deux comportements pratiques, une logique simple s'impose : lorsqu'aucun obstacle n'est détecté, utilisez le comportement aller au but. Lorsqu'un obstacle est détecté, passez au comportement d'évitement des obstacles jusqu'à ce que l'obstacle ne soit plus détecté.
En fin de compte, cependant, cette logique produira beaucoup de problèmes. Ce que ce système aura tendance à faire lorsqu'il rencontre un obstacle, c'est de s'en détourner, puis dès qu'il s'en est éloigné, de rebrousser chemin et de le rentrer de nouveau. Le résultat est une boucle sans fin de commutation rapide qui rend le robot inutile. In the worst case, the robot may switch between behaviors with every iteration of the control loop—a state known as a Zeno condition .
There are multiple solutions to this problem, and readers that are looking for deeper knowledge should check, for example, the DAMN software architecture.
What we need for our simple simulated robot is an easier solution: One more behavior specialized with the task of getting around an obstacle and reaching the other side.
Python Robot Programming Methods: Follow-Wall Behavior
Here's the idea: When we encounter an obstacle, take the two sensor readings that are closest to the obstacle and use them to estimate the surface of the obstacle. Then, simply set our reference vector to be parallel to this surface. Keep following this wall until A) the obstacle is no longer between us and the goal, and B) we are closer to the goal than we were when we started. Then we can be certain we have navigated the obstacle properly.
With our limited information, we can't say for certain whether it will be faster to go around the obstacle to the left or to the right. To make up our minds, we select the direction that will move us closer to the goal immediately. To figure out which way that is, we need to know the reference vectors of the go-to-goal behavior and the avoid-obstacle behavior, as well as both of the possible follow-wall reference vectors. Here is an illustration of how the final decision is made (in this case, the robot will choose to go left):
Determining the follow-wall reference vectors turns out to be a bit more involved than either the avoid-obstacle or go-to-goal reference vectors. Take a look at the Python code in follow_wall_controller.py
to see how it's done.
Final Control Design
The final control design uses the follow-wall behavior for almost all encounters with obstacles. However, if the robot finds itself in a tight spot, dangerously close to a collision, it will switch to pure avoid-obstacles mode until it is a safer distance away, and then return to follow-wall. Once obstacles have been successfully negotiated, the robot switches to go-to-goal. Here is the final state diagram, which is programmed inside the supervisor_state_machine.py
:
Here is the robot successfully navigating a crowded environment using this control scheme:
An additional feature of the state machine that you can try to implement is a way to avoid circular obstacles by switching to go-to-goal as soon as possible instead of following the obstacle border until the end (which does not exist for circular objects!)
Tweak, Tweak, Tweak: Trial and Error
The control scheme that comes with Sobot Rimulator is very finely tuned. It took many hours of tweaking one little variable here, and another equation there, to get it to work in a way I was satisfied with. Robotics programming often involves a great deal of plain old trial-and-error. Robots are very complex and there are few shortcuts to getting them to behave optimally in a robot simulator environment…at least, not much short of outright machine learning, but that's a whole other can of worms.
I encourage you to play with the control variables in Sobot Rimulator and observe and attempt to interpret the results. Changes to the following all have profound effects on the simulated robot's behavior:
- The error gain
kP
in each controller - The sensor gains used by the avoid-obstacles controller
- The calculation of v as a function of ω in each controller
- The obstacle standoff distance used by the follow-wall controller
- The switching conditions used by
supervisor_state_machine.py
- Pretty much anything else
When Programmable Robots Fail
We've done a lot of work to get to this point, and this robot seems pretty clever. Yet, if you run Sobot Rimulator through several randomized maps, it won't be long before you find one that this robot can't deal with. Sometimes it drives itself directly into tight corners and collides. Sometimes it just oscillates back and forth endlessly on the wrong side of an obstacle. Occasionally it is legitimately imprisoned with no possible path to the goal. After all of our testing and tweaking, sometimes we must come to the conclusion that the model we are working with just isn't up to the job, and we have to change the design or add functionality.
In the mobile robot universe, our little robot's “brain” is on the simpler end of the spectrum. Many of the failure cases it encounters could be overcome by adding some more advanced software to the mix. More advanced robots make use of techniques such as mapping , to remember where it's been and avoid trying the same things over and over; heuristics , to generate acceptable decisions when there is no perfect decision to be found; and machine learning , to more perfectly tune the various control parameters governing the robot's behavior.
A Sample of What's to Come
Robots are already doing so much for us, and they are only going to be doing more in the future. While even basic robotics programming is a tough field of study requiring great patience, it is also a fascinating and immensely rewarding one.
In this tutorial, we learned how to develop reactive control software for a robot using the high-level programming language Python. But there are many more advanced concepts that can be learned and tested quickly with a Python robot framework similar to the one we prototyped here. I hope you will consider getting involved in the shaping of things to come!
Acknowledgement: I would like to thank Dr. Magnus Egerstedt and Jean-Pierre de la Croix of the Georgia Institute of Technology for teaching me all this stuff, and for their enthusiasm for my work on Sobot Rimulator.