Un tutorial introductiv de programare a roboților
Publicat: 2022-03-11Să recunoaștem, roboții sunt cool. De asemenea, vor conduce lumea într-o zi și, sperăm, că la acel moment le vor fi milă de săracii lor creatori moale și cărnoase (alias dezvoltatori de robotică) și ne vor ajuta să construim o utopie spațială plină de o mulțime. Glumesc desigur, dar doar un fel.
În ambiția mea de a avea o mică influență asupra problemei, am urmat anul trecut un curs de teoria controlului roboților autonomi, care a culminat cu construirea unui simulator robotic bazat pe Python, care mi-a permis să exersez teoria controlului pe un robot simplu, mobil și programabil. .
În acest articol, voi arăta cum să folosesc un cadru de robot Python pentru a dezvolta software de control, voi descrie schema de control pe care am dezvoltat-o pentru robotul meu simulat, voi ilustra modul în care acesta interacționează cu mediul său și își atinge obiectivele și voi discuta câteva dintre provocările fundamentale ale programării robotice pe care le-am întâlnit pe parcurs.
Pentru a urma acest tutorial despre programarea robotică pentru începători, ar trebui să aveți cunoștințe de bază despre două lucruri:
- Matematică — vom folosi câteva funcții trigonometrice și vectori
- Python - deoarece Python este printre cele mai populare limbaje de programare de bază pentru roboți - vom folosi bibliotecile și funcțiile de bază Python
Fragmentele de cod prezentate aici sunt doar o parte a întregului simulator, care se bazează pe clase și interfețe, așa că pentru a citi codul direct, este posibil să aveți nevoie de ceva experiență în Python și programare orientată pe obiecte.
În cele din urmă, subiectele opționale care vă vor ajuta să urmați mai bine acest tutorial sunt cunoașterea ce este o mașină de stare și cum funcționează senzorii și codificatoarele de rază.
Provocarea robotului programabil: percepție vs. realitate și fragilitatea controlului
Provocarea fundamentală a tuturor roboticii este aceasta: este imposibil să cunoști vreodată adevărata stare a mediului. Software-ul de control al robotului poate doar ghici starea lumii reale pe baza măsurătorilor returnate de senzorii săi. Poate încerca doar să schimbe starea lumii reale prin generarea de semnale de control.
Astfel, unul dintre primii pași în proiectarea controlului este să venim cu o abstractizare a lumii reale, cunoscută sub numele de model , cu care să interpretăm citirile senzorilor și să luăm decizii. Atâta timp cât lumea reală se comportă conform ipotezelor modelului, putem face ghiciri bune și putem exercita controlul. De îndată ce lumea reală se abate de la aceste presupuneri, totuși, nu vom mai putea face presupuneri bune, iar controlul se va pierde. Adesea, odată ce controlul este pierdut, acesta nu poate fi recâștigat. (Cu excepția cazului în care o forță exterioară binevoitoare o restaurează.)
Acesta este unul dintre motivele cheie pentru care programarea robotică este atât de dificilă. Adesea vedem videoclipuri cu cel mai recent robot de cercetare în laborator, care efectuează fapte fantastice de dexteritate, navigare sau lucru în echipă și suntem tentați să ne întrebăm: „De ce nu este folosit acest lucru în lumea reală?” Ei bine, data viitoare când vezi un astfel de videoclip, aruncă o privire la cât de controlat este mediul de laborator. În cele mai multe cazuri, acești roboți sunt capabili să îndeplinească aceste sarcini impresionante doar atâta timp cât condițiile de mediu rămân în limitele înguste ale modelului său intern. Astfel, o cheie a progresului roboticii este dezvoltarea unor modele mai complexe, flexibile și mai robuste - iar avansarea menționată este supusă limitelor resurselor computaționale disponibile.
[Notă secundară: Filosofii și psihologii deopotrivă ar observa că creaturile vii suferă și de dependență de propria percepție internă a ceea ce le spun simțurile lor. Multe progrese în robotică provin din observarea creaturilor vii și de la modul în care acestea reacționează la stimuli neaștepți. Gandeste-te la asta. Care este modelul tău intern al lumii? Este diferit de cel al unei furnici și cel al unui pește? (Sperăm.) Cu toate acestea, la fel ca furnica și peștele, este probabil să simplifice prea mult unele realități ale lumii. Atunci când presupunerile tale despre lume nu sunt corecte, te poate expune riscului de a pierde controlul asupra lucrurilor. Uneori numim acest lucru „pericol”. Așa cum micul nostru robot se luptă să supraviețuiască împotriva universului necunoscut, la fel facem noi toți. Aceasta este o perspectivă puternică pentru robotiști.]
Simulatorul de robot programabil
Simulatorul pe care l-am construit este scris în Python și foarte inteligent numit Sobot Rimulator . Puteți găsi v1.0.0 pe GitHub. Nu are multe clopote și fluiere, dar este construit pentru a face un lucru foarte bine: să ofere o simulare precisă a unui robot mobil și să ofere unui robotian aspirant un cadru simplu pentru exersarea programării software-ului robot. Deși este întotdeauna mai bine să ai un robot adevărat cu care să te joci, un bun simulator de robot Python este mult mai accesibil și este un loc grozav de început.
În roboții din lumea reală, software-ul care generează semnalele de control („controlerul”) este necesar să ruleze la o viteză foarte mare și să facă calcule complexe. Acest lucru afectează alegerea limbajelor de programare pentru roboți care sunt cele mai bune de utilizat: de obicei, C++ este utilizat pentru acest tip de scenarii, dar în aplicațiile robotice mai simple, Python este un compromis foarte bun între viteza de execuție și ușurința de dezvoltare și testare.
Software-ul pe care l-am scris simulează un robot de cercetare din viața reală numit Khepera, dar poate fi adaptat unei game de roboți mobili cu diferite dimensiuni și senzori. Deoarece am încercat să programez simulatorul cât mai asemănător cu capabilitățile robotului real, logica de control poate fi încărcată într-un robot Khepera real cu refactorizare minimă și va funcționa la fel ca robotul simulat. Caracteristicile specifice implementate se referă la Khepera III, dar pot fi adaptate cu ușurință la noul Khepera IV.
Cu alte cuvinte, programarea unui robot simulat este analogă cu programarea unui robot real. Acest lucru este esențial dacă simulatorul este de folos pentru a dezvolta și evalua diferite abordări software de control.
În acest tutorial, voi descrie arhitectura software de control al robotului care vine cu v1.0.0 a Sobot Rimulator și voi oferi fragmente din sursa Python (cu mici modificări pentru claritate). Cu toate acestea, vă încurajez să vă scufundați în sursă și să vă încurcați. Simulatorul a fost bifurcat și folosit pentru a controla diferiți roboți mobili, inclusiv un Roomba2 de la iRobot. De asemenea, vă rugăm să nu ezitați să bifurcați proiectul și să-l îmbunătățiți.
Logica de control a robotului este restrânsă la aceste clase/fișiere Python:
-
models/supervisor.py
— această clasă este responsabilă pentru interacțiunea dintre lumea simulată din jurul robotului și robotul însuși. Evoluează mașina noastră de stare a robotului și declanșează controlerele pentru a calcula comportamentul dorit. -
models/supervisor_state_machine.py
— această clasă reprezintă diferitele stări în care se poate afla robotul, în funcție de interpretarea senzorilor. - Fișierele din directorul
models/controllers
— aceste clase implementează diferite comportamente ale robotului, având în vedere o stare cunoscută a mediului. În special, un controler specific este selectat în funcție de mașina de stare.
Obiectivul
Roboții, ca și oamenii, au nevoie de un scop în viață. Scopul software-ului nostru care controlează acest robot va fi foarte simplu: va încerca să-și croiască drum către un punct predeterminat. Aceasta este de obicei caracteristica de bază pe care ar trebui să o aibă orice robot mobil, de la mașini autonome până la aspiratoare robotizate. Coordonatele obiectivului sunt programate în software-ul de control înainte ca robotul să fie activat, dar ar putea fi generate dintr-o aplicație Python suplimentară care supraveghează mișcările robotului. De exemplu, gândiți-vă la conducerea prin mai multe puncte de referință.
Cu toate acestea, pentru a complica lucrurile, mediul robotului poate fi presărat cu obstacole. Robotul NU POATE să se ciocnească de un obstacol în drumul său către obiectiv. Prin urmare, dacă robotul întâlnește un obstacol, va trebui să-și găsească drumul pentru a-și putea continua drumul către obiectiv.
Robotul programabil
Fiecare robot vine cu capacități și preocupări de control diferite. Să ne familiarizăm cu robotul nostru programabil simulat.
Primul lucru de remarcat este că, în acest ghid, robotul nostru va fi un robot mobil autonom . Aceasta înseamnă că se va deplasa liber în spațiu și că o va face sub propriul control. Acest lucru este în contrast cu, de exemplu, un robot cu telecomandă (care nu este autonom) sau un braț de robot din fabrică (care nu este mobil). Robotul nostru trebuie să descopere singur cum să-și atingă obiectivele și să supraviețuiască în mediul său. Aceasta se dovedește a fi o provocare surprinzător de dificilă pentru programatorii începători în robotică.
Intrări de control: Senzori
Există multe moduri diferite în care un robot poate fi echipat pentru a-și monitoriza mediul. Acestea pot include orice, de la senzori de proximitate, senzori de lumină, bare de protecție, camere și așa mai departe. În plus, roboții pot comunica cu senzori externi care le oferă informații pe care ei înșiși nu le pot observa direct.
Robotul nostru de referință este echipat cu nouă senzori cu infraroșu — modelul mai nou are opt senzori de proximitate cu infraroșu și cinci cu ultrasunete — dispuși într-o „fustă” în fiecare direcție. Există mai mulți senzori în fața robotului decât în spate, deoarece este de obicei mai important pentru robot să știe ce este în fața lui decât ce este în spatele lui.
În plus față de senzorii de proximitate, robotul are o pereche de ticker -uri care urmăresc mișcarea roții. Acestea vă permit să urmăriți câte rotații face fiecare roată, o rotire completă înainte a unei roți fiind de 2.765 de ticuri. Întoarcerile în direcția opusă numără înapoi, scăzând numărul de căpușe în loc să-l mărească. Nu trebuie să vă faceți griji pentru anumite numere din acest tutorial, deoarece software-ul pe care îl vom scrie folosește distanța parcursă exprimată în metri. Mai târziu vă voi arăta cum să îl calculați din căpușe cu o funcție Python ușoară.
Ieșiri de control: Mobilitate
Unii roboți se mișcă pe picioare. Unii se rostogolesc ca o minge. Unii chiar alunecă ca un șarpe.
Robotul nostru este un robot cu acționare diferențială, ceea ce înseamnă că se rostogolește pe două roți. Când ambele roți se rotesc cu aceeași viteză, robotul se mișcă în linie dreaptă. Când roțile se mișcă cu viteze diferite, robotul se întoarce. Astfel, controlul mișcării acestui robot se reduce la controlul corespunzător a ratelor la care fiecare dintre aceste două roți se rotește.
API
În Sobot Rimulator, separarea dintre „calculatorul” robot și lumea fizică (simulată) este întruchipată de fișierul robot_supervisor_interface.py
, care definește întregul API pentru interacțiunea cu senzorii și motoarele „robot real”:
-
read_proximity_sensors()
returnează o matrice de nouă valori în formatul nativ al senzorilor -
read_wheel_encoders()
returnează o matrice de două valori care indică numărul total de bifături de la început -
set_wheel_drive_rates( v_l, v_r )
ia două valori (în radiani pe secundă) și setează viteza din stânga și dreapta a roților la acele două valori
Această interfață folosește intern un obiect robot care oferă datele de la senzori și posibilitatea de a deplasa motoare sau roți. Dacă doriți să creați un robot diferit, trebuie pur și simplu să furnizați o clasă de robot Python diferită, care poate fi utilizată de aceeași interfață, iar restul codului (controlere, supervizor și simulator) va funcționa imediat!
Simulatorul
Deoarece ați folosi un robot real în lumea reală fără să acordați prea multă atenție legilor fizicii implicate, puteți ignora modul în care robotul este simulat și să treceți direct la modul în care este programat software-ul controlerului, deoarece va fi aproape același între lumea reală și o simulare. Dar dacă ești curios, o voi prezenta pe scurt aici.
Fișierul world.py
este o clasă Python care reprezintă lumea simulată, cu roboți și obstacole în interior. Funcția pas din această clasă are grijă să evolueze lumea noastră simplă prin:
- Aplicarea regulilor fizicii la mișcările robotului
- Luând în considerare coliziunile cu obstacole
- Furnizarea de noi valori pentru senzorii robotului
În cele din urmă, îi cheamă pe supraveghetorii roboților responsabili de executarea software-ului creierului robot.
Funcția step este executată într-o buclă, astfel încât robot.step_motion()
mișcă robotul folosind viteza roții calculată de supervizor în pasul anterior de simulare.
# 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
Funcția apply_physics()
actualizează intern valorile senzorilor de proximitate robot, astfel încât supervizorul să poată estima mediul la pasul curent de simulare. Aceleași concepte se aplică codificatoarelor.
Un model simplu
În primul rând, robotul nostru va avea un model foarte simplu. Va face multe presupuneri despre lume. Unele dintre cele importante includ:
- Terenul este întotdeauna plat și uniform
- Obstacolele nu sunt niciodată rotunde
- Roțile nu alunecă niciodată
- Nimic nu va împinge vreodată robotul
- Senzorii nu eșuează niciodată și nu dau citiri false
- Roțile se întorc întotdeauna când li se spune
Deși majoritatea acestor ipoteze sunt rezonabile într-un mediu asemănător unei case, obstacole rotunde ar putea fi prezente. Software-ul nostru de evitare a obstacolelor are o implementare simplă și urmărește granița obstacolelor pentru a le ocoli. Vom sugera cititorilor cum să îmbunătățim cadrul de control al robotului nostru cu o verificare suplimentară pentru a evita obstacolele circulare.
Bucla de control
Vom intra acum în nucleul software-ului nostru de control și vom explica comportamentele pe care dorim să le programăm în interiorul robotului. La acest cadru pot fi adăugate comportamente suplimentare și ar trebui să încercați propriile idei după ce terminați de citit! Software-ul de robotică bazat pe comportament a fost propus cu mai bine de 20 de ani în urmă și este încă un instrument puternic pentru robotica mobilă. Ca exemplu, în 2007 a fost folosit un set de comportamente în cadrul DARPA Urban Challenge — prima competiție pentru mașini de conducere autonomă!
Un robot este un sistem dinamic. Starea robotului, citirile senzorilor săi și efectele semnalelor sale de control sunt în flux constant. Controlul modului în care se desfășoară evenimentele implică următorii trei pași:
- Aplicați semnale de control.
- Măsurați rezultatele.
- Generați noi semnale de control calculate pentru a ne aduce mai aproape de obiectivul nostru.
Acești pași se repetă mereu până când ne atingem scopul. Cu cât putem face acest lucru de mai multe ori pe secundă, cu atât vom avea un control mai fin asupra sistemului. Robotul Sobot Rimulator repetă acești pași de 20 de ori pe secundă (20 Hz), dar mulți roboți trebuie să facă acest lucru de mii sau milioane de ori pe secundă pentru a avea un control adecvat. Amintiți-vă de introducerea noastră anterioară despre diferitele limbaje de programare a roboților pentru diferite sisteme robotice și cerințe de viteză.
În general, de fiecare dată când robotul nostru face măsurători cu senzorii săi, folosește aceste măsurători pentru a-și actualiza estimarea internă a stării lumii, de exemplu, distanța de la obiectivul său. Compară această stare cu o valoare de referință a ceea ce vrea să fie starea (pentru distanță, vrea să fie zero) și calculează eroarea dintre starea dorită și starea reală. Odată ce aceste informații sunt cunoscute, generarea de noi semnale de control poate fi redusă la o problemă de minimizare a erorii care va muta în cele din urmă robotul spre obiectiv.
Un truc ingenios: simplificarea modelului
Pentru a controla robotul pe care vrem să-l programăm, trebuie să trimitem un semnal către roata din stânga care să îi spună cât de repede să se întoarcă și un semnal separat către roata din dreapta care să îi spună cât de repede să se întoarcă. Să numim aceste semnale v L și v R . Cu toate acestea, gândirea constantă în termeni de v L și v R este foarte greoaie. În loc să întrebăm: „Cât de repede vrem să se rotească roata din stânga și cât de repede vrem să se rotească roata din dreapta?” este mai firesc să întrebăm: „Cât de repede vrem să avanseze robotul și cât de repede vrem să se întoarcă sau să-și schimbe direcția?” Să numim acești parametri viteza v și viteza unghiulară (de rotație) ω (a se citi „omega”). Se pare că ne putem baza întregul model pe v și ω în loc de v L și v R , și numai odată ce am determinat cum vrem să se miște robotul nostru programat, transformăm matematic aceste două valori în v L și v R de care avem nevoie. pentru a controla efectiv roțile robotului. Acesta este cunoscut ca model de control monociclu .
Iată codul Python care implementează transformarea finală în supervisor.py
. Rețineți că dacă ω este 0, ambele roți se vor întoarce cu aceeași viteză:
# 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
Stare de estimare: robot, cunoaște-te pe tine însuți
Folosind senzorii săi, robotul trebuie să încerce să estimeze starea mediului, precum și propria stare. Aceste estimări nu vor fi niciodată perfecte, dar trebuie să fie destul de bune, deoarece robotul își va baza toate deciziile pe aceste estimări. Folosind doar senzorii de proximitate și tickerele roților, trebuie să încerce să ghicească următoarele:
- Direcția către obstacole
- Distanța față de obstacole
- Poziția robotului
- Capul robotului
Primele două proprietăți sunt determinate de citirile senzorului de proximitate și sunt destul de simple. Funcția API read_proximity_sensors()
returnează o matrice de nouă valori, câte una pentru fiecare senzor. Știm dinainte că a șaptea citire, de exemplu, corespunde senzorului care indică 75 de grade spre dreapta robotului.
Astfel, dacă această valoare arată o citire corespunzătoare unei distanțe de 0,1 metri, știm că există un obstacol la 0,1 metri distanță, la 75 de grade la stânga. Dacă nu există niciun obstacol, senzorul va returna o citire a intervalului maxim de 0,2 metri. Astfel, dacă citim 0,2 metri pe senzorul șapte, vom presupune că de fapt nu există niciun obstacol în acea direcție.
Datorită modului în care funcționează senzorii infraroșii (măsurând reflexia infraroșii), numerele pe care le returnează sunt o transformare neliniară a distanței reale detectate. Astfel, funcția Python pentru determinarea distanței indicate trebuie să transforme aceste citiri în metri. Acest lucru se face în supervisor.py
după cum urmează:

# 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() ]
Din nou, avem un model de senzor specific în acest cadru de robot Python, în timp ce în lumea reală, senzorii vin cu software însoțitor care ar trebui să ofere funcții de conversie similare de la valori neliniare la contoare.
Determinarea poziției și a direcției robotului (cunoscută împreună ca poziția în programarea robotică) este oarecum mai dificilă. Robotul nostru folosește odometria pentru a-și estima poziția. Aici intervin simbolurile roților. Măsurând cât de mult s-a întors fiecare roată de la ultima iterație a buclei de control, este posibil să obțineți o estimare bună a modului în care s-a schimbat poziția robotului, dar numai dacă modificarea este mică .
Acesta este unul dintre motivele pentru care este important să repetați bucla de control foarte frecvent într-un robot din lumea reală, unde motoarele care mișcă roțile pot să nu fie perfecte. Dacă am așteptat prea mult să măsuram ticker-urile roților, ambele roți ar fi putut face destul de multe și va fi imposibil de estimat unde am ajuns.
Având în vedere simulatorul nostru software actual, ne putem permite să rulăm calculul odometriei la 20 Hz - aceeași frecvență ca și controlerele. Dar ar putea fi o idee bună să aveți un fir Python separat care rulează mai repede pentru a prinde mișcări mai mici ale ticker-urilor.
Mai jos este funcția completă de odometrie din supervisor.py
care actualizează estimarea poziției robotului. Rețineți că poziția robotului este compusă din coordonatele x
și y
și direcția theta
, care este măsurată în radiani de pe axa X pozitivă. x
pozitiv este la est și y
pozitiv este la nord. Astfel, un titlu de 0
indică faptul că robotul este orientat direct spre est. Robotul presupune întotdeauna că poza sa inițială este (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
Acum că robotul nostru este capabil să genereze o estimare bună a lumii reale, să folosim aceste informații pentru a ne atinge obiectivele.
Metode de programare a roboților Python: Comportamentul Go-to-Goal
Scopul suprem în existența micului nostru robot în acest tutorial de programare este să ajungem la punctul obiectiv. Deci, cum facem roțile să se întoarcă pentru a ajunge acolo? Să începem prin a ne simplifica puțin viziunea asupra lumii și să presupunem că nu există obstacole în cale.
Aceasta devine apoi o sarcină simplă și poate fi programată cu ușurință în Python. Dacă mergem înainte în timp ce înfruntăm obiectivul, vom ajunge acolo. Datorită odometriei noastre, știm care sunt coordonatele și direcția noastră curentă. Știm și care sunt coordonatele obiectivului pentru că au fost preprogramate. Prin urmare, folosind puțină algebră liniară, putem determina vectorul de la locația noastră până la obiectiv, ca în 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
Rețineți că aducem vectorul la obiectiv în cadrul de referință al robotului și NU în coordonatele lumii. Dacă obiectivul este pe axa X în cadrul de referință al robotului, înseamnă că este direct în fața robotului. Astfel, unghiul acestui vector față de axa X este diferența dintre direcția noastră și direcția pe care vrem să ne aflăm. Cu alte cuvinte, este eroarea dintre starea noastră actuală și ceea ce vrem să fie starea noastră actuală. Prin urmare, dorim să ne ajustam viteza de viraj ω astfel încât unghiul dintre direcția noastră și obiectiv să se schimbe spre 0. Vrem să minimizăm eroarea:
# 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
din fragmentul de mai sus al implementării controlerului Python este un câștig de control. Este un coeficient care determină cât de repede ne întoarcem proporțional cu cât de departe de obiectivul cu care ne confruntăm. Dacă eroarea din direcția noastră este 0
, atunci rata de întoarcere este de asemenea 0
. În funcția Python reală din fișierul go_to_goal_controller.py
, veți vedea mai multe câștiguri similare, deoarece am folosit un controler PID în loc de un coeficient proporțional simplu.
Acum că avem viteza unghiulară ω , cum ne determinăm viteza înainte v ? O regulă generală bună este una pe care probabil o cunoașteți instinctiv: dacă nu facem un viraj, putem merge înainte cu viteză maximă, iar apoi, cu cât ne întoarcem mai repede, cu atât mai mult ar trebui să încetinim. În general, acest lucru ne ajută să ne menținem sistemul stabil și să acționăm în limitele modelului nostru. Astfel, v este o funcție a lui ω . În go_to_goal_controller.py
ecuația este:
# 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
O sugestie pentru a elabora această formulă este să luăm în considerare că de obicei încetinim când ne apropiem de obiectiv pentru a-l atinge cu viteză zero. Cum s-ar schimba această formulă? Trebuie să includă cumva o înlocuire a lui v_max()
cu ceva proporțional cu distanța. OK, aproape am finalizat o singură buclă de control. Singurul lucru care mai rămâne de făcut este să transforme acești doi parametri ai modelului monociclu în viteze diferențiale ale roților și să trimită semnalele către roți. Iată un exemplu de traiectorie a robotului sub controlerul de acces la obiectiv, fără obstacole:
După cum putem vedea, vectorul către obiectiv este o referință eficientă pe care ne putem baza calculele de control. Este o reprezentare internă a „unde vrem să ajungem”. După cum vom vedea, singura diferență majoră între comportamentul de a merge la obiectiv și alte comportamente este că, uneori, a merge spre obiectiv este o idee proastă, așa că trebuie să calculăm un vector de referință diferit.
Metode de programare a roboților Python: Comportamentul de evitare a obstacolelor
Mersul spre obiectiv atunci când există un obstacol în acea direcție este un exemplu. În loc să alergăm cu capul înainte în lucrurile din calea noastră, să încercăm să programăm o lege de control care să-l facă pe robot să le evite.
Pentru a simplifica scenariul, acum să uităm complet de punctul obiectiv și să facem următorul obiectiv: Când nu există obstacole în fața noastră, mergeți înainte. Când întâmpinați un obstacol, întoarceți-vă de la el până când acesta nu mai este în fața noastră.
În consecință, atunci când nu există niciun obstacol în fața noastră, dorim ca vectorul nostru de referință să îndrepte pur și simplu înainte. Atunci ω va fi zero și v va fi viteza maximă. Cu toate acestea, de îndată ce detectăm un obstacol cu senzorii noștri de proximitate, dorim ca vectorul de referință să îndrepte în orice direcție se află departe de obstacol. Acest lucru va face ca ω să se ridice pentru a ne îndepărta de obstacol și va face ca v să scadă pentru a ne asigura că nu întâlnim accidental obstacol în acest proces.
O modalitate bună de a genera vectorul nostru de referință dorit este să transformăm cele nouă citiri de proximitate în vectori și să luăm o sumă ponderată. Când nu sunt detectate obstacole, vectorii se vor însuma simetric, rezultând un vector de referință care arată drept înainte, după cum se dorește. Dar dacă un senzor de pe partea dreaptă, de exemplu, ridică un obstacol, acesta va contribui cu un vector mai mic la sumă, iar rezultatul va fi un vector de referință care este deplasat spre stânga.
Pentru un robot general cu o amplasare diferită a senzorilor, aceeași idee poate fi aplicată, dar poate necesita modificări ale greutăților și/sau îngrijire suplimentară atunci când senzorii sunt simetrici în față și în spatele robotului, deoarece suma ponderată ar putea deveni zero. .
Iată codul care face acest lucru în 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
Folosind ao_heading_vector
rezultat ca referință pentru robotul pentru a încerca să se potrivească, iată rezultatele rulării software-ului robotului în simulare folosind doar controlerul pentru evitarea obstacolelor, ignorând complet punctul obiectiv. Robotul sare fără țintă, dar nu se ciocnește niciodată de un obstacol și chiar reușește să navigheze în spații foarte înguste:
Metode de programare a roboților Python: automate hibride (mașină de stare a comportamentului)
Până acum am descris două comportamente – de a merge la obiectiv și de a evita obstacolele – în mod izolat. Ambele își îndeplinesc funcția în mod admirabil, dar pentru a atinge obiectivul cu succes într-un mediu plin de obstacole, trebuie să le combinăm.
Soluția pe care o vom dezvolta constă într-o clasă de mașini care are denumirea extrem de cool de automate hibride . Un automat hibrid este programat cu mai multe comportamente sau moduri diferite, precum și cu o mașină de supraveghere a stării. Mașina de supraveghere a stării trece de la un mod la altul în momente discrete (când obiectivele sunt atinse sau mediul se schimbă brusc prea mult), în timp ce fiecare comportament folosește senzori și roți pentru a reacționa continuu la schimbările mediului. Soluția a fost numită hibrid pentru că evoluează atât într-un mod discret, cât și continuu.
Cadrul nostru de robot Python implementează mașina de stări în fișierul supervisor_state_machine.py
.
Echipat cu cele două comportamente utile ale noastre, se sugerează o logică simplă: când nu este detectat niciun obstacol, utilizați comportamentul de a merge la obiectiv. Când este detectat un obstacol, treceți la comportamentul de evitare a obstacolelor până când obstacolul nu mai este detectat.
După cum se dovedește, totuși, această logică va produce o mulțime de probleme. Ceea ce va avea tendința de a face acest sistem atunci când întâlnește un obstacol este să se îndepărteze de el, apoi, de îndată ce s-a îndepărtat de el, să se întoarcă înapoi și să alerge din nou în el. Rezultatul este o buclă nesfârșită de comutare rapidă care face robotul inutil. 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.