www-home/blog/20070502-robot-behaviors-python.html

235 lines
25 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" href="/favicon.ico">
<link rel="canonical" href="https://www.mcmillen.dev/blog/20070502-robot-behaviors-python.html">
<link rel="alternate" type="application/atom+xml" href="https://www.mcmillen.dev/feed.atom" title="Colin McMillen's Blog - Atom">
<title>Creating robot behaviors with Python generators | Colin McMillen</title>
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@500;700&display=block" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Fira+Mono:500&display=block" rel="stylesheet">
<link rel="stylesheet" href="/pygments.css">
<link rel="stylesheet" href="/style.css">
<meta name="twitter:card" content="summary">
<meta name="twitter:site" content="@mcmillen">
<meta name="twitter:title" content="Creating robot behaviors with Python generators | Colin McMillen">
<meta name="twitter:description" content="Generators are a powerful feature of the Python programming language. In a nutshell, generators let you write a function that behaves like an iterator. The standard approach to programming robot behaviors is based on state machines. However, robotics code is full of special cases, so a complex behavior will typically end up with a lot of bookkeeping cruft. Generators let us simplify the bookkeeping and express the desired behavior in a straightforward manner. (Idea originally due to Jim Bruce.)">
</head>
<script>
function fixEmails() {
const mailtoArray = [
'm', 'a', 'i', 'l', 't', 'o', ':',
'c', 'o', 'l', 'i', 'n', '@',
'm', 'c', 'm', 'i', 'l', 'l', 'e', 'n',
'.', 'd', 'e', 'v'];
const mailtoLink = mailtoArray.join('');
const anchors = document.getElementsByTagName('a');
for (let i = 0; i < anchors.length; i++) {
const anchor = anchors[i];
if (anchor.href == 'mailto:email@example.com') {
anchor.href = mailtoLink;
if (anchor.innerText == 'colin at mcmillen dot dev') {
anchor.innerText = mailtoLink.substring(7);
}
}
}
}
</script>
<body onload="fixEmails()">
<div id="page-container">
<div id="content-wrap">
<div id="header">
<div class="content">
<a href="/" class="undecorated">Colin McMillen</a>
<span style="float: right;"><a href="/feed.atom"><img src="/img/rss.svg" alt="Atom feed" style="width: 17px; height: 17px; margin-bottom: 1px;"></a></span>
<span style="float: right;"><a href="https://twitter.com/mcmillen"><img src="/img/twitter.svg" alt="@mcmillen"></a></span>
</div>
</div>
<div class="content">
<h1 id="creating-robot-behaviors-with-python-generators">Creating robot behaviors with Python generators</h1>
<p><em>Posted 2007-05-02.</em></p>
<p><a href="https://en.wikipedia.org/wiki/Generator_(computer_programming)">Generators</a> are a <a href="https://docs.python.org/2.7/reference/simple_stmts.html#the-yield-statement">powerful feature</a> of the Python programming language. In a nutshell, generators let you write a function that behaves like an iterator. The standard approach to programming robot behaviors is based on state machines. However, robotics code is <em>full</em> of special cases, so a complex behavior will typically end up with a lot of bookkeeping cruft. Generators let us simplify the bookkeeping and express the desired behavior in a straightforward manner.</p>
<p>(Idea originally due to <a href="http://www.cs.cmu.edu/~jbruce/">Jim Bruce</a>.)</p>
<p>I&rsquo;ve worked for several years on <a href="https://robocup.org">RoboCup</a>, the international robot soccer competition. <a href="http://www.cs.cmu.edu/~robosoccer/legged/">Our software</a> is written in a mixture of C++ (for low-level localization and vision algorithms) and Python (for high-level behaviors). Let&rsquo;s say we want to write a simple goalkeeper for a robot soccer team. Our keeper will be pretty simple; here&rsquo;s a list of the requirements:</p>
<ol>
<li>If the ball is far away, stand in place.</li>
<li>If the ball is near by, dive to block it. Dive to the left if the ball is to the left; dive to the right if the ball is to the right.</li>
<li>If we choose a &ldquo;dive&rdquo; action, then &ldquo;stand&rdquo; on the next frame, nothing will happen. (Well, maybe the robot will twitch briefly....) So when we choose to dive, we need to commit to sending the same dive command for some time (let&rsquo;s say one second).</li>
</ol>
<p>The usual approach to robot behavior design relies on hierarchical state machines. Specifically, we might be in a &ldquo;standing&rdquo; state while the ball is far away; when the ball becomes close, we enter a &ldquo;diving&rdquo; state that persists for one second. Because of requirement 3, this solution will have a few warts: we need to keep track of how much time we&rsquo;ve spent in the dive state. Every time we add a special case like this, we need to keep some extra state information around. Since robotics code is full of special cases, we tend to end up with a lot of bookkeeping cruft. In contrast, generators will let us clearly express the desired behavior.</p>
<p>On to the state-machine approach. First, we&rsquo;ll have a class called Features that abstracts the robot&rsquo;s raw sensor data. For this example, we only care whether the ball is near/far and left/right, so Features will just contain two boolean variables:</p>
<div class="codehilite"><pre><span></span><span class="k">class</span> <span class="nc">Features</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
<span class="n">ballFar</span> <span class="o">=</span> <span class="bp">True</span>
<span class="n">ballOnLeft</span> <span class="o">=</span> <span class="bp">True</span>
</pre></div>
<p>Next, we make the goalkeeper. The keeper&rsquo;s behavior is specified by the <code>next()</code> function, which is called thirty times per second by the robot&rsquo;s main event loop (every time the on-board camera produces a new image). The <code>next()</code> function returns one of three actions: <code>"stand"</code>, <code>"diveLeft"</code>, or <code>"diveRight"</code>, based on the current values of the Features object. For now, let&rsquo;s pretend that requirement 3 doesn&rsquo;t exist.</p>
<div class="codehilite"><pre><span></span><span class="k">class</span> <span class="nc">Goalkeeper</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">features</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">features</span> <span class="o">=</span> <span class="n">features</span>
<span class="k">def</span> <span class="nf">next</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">features</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">features</span>
<span class="k">if</span> <span class="n">features</span><span class="o">.</span><span class="n">ballFar</span><span class="p">:</span>
<span class="k">return</span> <span class="s1">&#39;stand&#39;</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">if</span> <span class="n">features</span><span class="o">.</span><span class="n">ballOnLeft</span><span class="p">:</span>
<span class="k">return</span> <span class="s1">&#39;diveLeft&#39;</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span> <span class="s1">&#39;diveRight&#39;</span>
</pre></div>
<p>That was simple enough. The constructor takes in the <code>Features</code> object; the <code>next()</code> method checks the current <code>Features</code> values and returns the correct action. Now, how about satisfying requirement 3? When we choose to dive, we need to keep track of two things: how long we need to stay in the <code>"dive"</code> state and which direction we dove. We&rsquo;ll do this by adding a couple of instance variables (<code>self.diveFramesRemaining</code> and <code>self.lastDiveCommand</code>) to the Goalkeeper class. These variables are set when we initiate the dive. At the top of the <code>next()</code> function, we check if <code>self.diveFramesRemaining</code> is positive; if so, we can immediately return <code>self.lastDiveCommand</code> without consulting the <code>Features</code>. Here&rsquo;s the code:</p>
<div class="codehilite"><pre><span></span><span class="k">class</span> <span class="nc">Goalkeeper</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">features</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">features</span> <span class="o">=</span> <span class="n">features</span>
<span class="bp">self</span><span class="o">.</span><span class="n">diveFramesRemaining</span> <span class="o">=</span> <span class="mi">0</span>
<span class="bp">self</span><span class="o">.</span><span class="n">lastDiveCommand</span> <span class="o">=</span> <span class="bp">None</span>
<span class="k">def</span> <span class="nf">next</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">features</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">features</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">diveFramesRemaining</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">diveFramesRemaining</span> <span class="o">-=</span> <span class="mi">1</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">lastDiveCommand</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">if</span> <span class="n">features</span><span class="o">.</span><span class="n">ballFar</span><span class="p">:</span>
<span class="k">return</span> <span class="s1">&#39;stand&#39;</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">if</span> <span class="n">features</span><span class="o">.</span><span class="n">ballOnLeft</span><span class="p">:</span>
<span class="n">command</span> <span class="o">=</span> <span class="s1">&#39;diveLeft&#39;</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">command</span> <span class="o">=</span> <span class="s1">&#39;diveRight&#39;</span>
<span class="bp">self</span><span class="o">.</span><span class="n">lastDiveCommand</span> <span class="o">=</span> <span class="n">command</span>
<span class="bp">self</span><span class="o">.</span><span class="n">diveFramesRemaining</span> <span class="o">=</span> <span class="mi">29</span>
<span class="k">return</span> <span class="n">command</span>
</pre></div>
<p>This satisfies all the requirements, but it&rsquo;s ugly. We&rsquo;ve added a couple of bookkeeping variables to the Goalkeeper class. Code to properly maintain these variables is sprinkled all over the <code>next()</code> function. Even worse, the structure of the code no longer accurately represents the programmer&rsquo;s intent: the top-level if-statement depends on the state of the robot rather than the state of the world. The intent of the original <code>next()</code> function is much easier to discern. (In real code, we could use a state-machine class to tidy things up a bit, but the end result would still be ugly when compared to our original <code>next()</code> function.)</p>
<p>With generators, we can preserve the form of the original <code>next()</code> function and keep the bookkeeping only where it&rsquo;s needed. If you&rsquo;re not familiar with generators, you can think of them as a special kind of function. The <code>yield</code> keyword is essentially equivalent to <code>return</code>, but the next time the generator is called, <em>execution continues from the point of the last <code>yield</code></em>, preserving the state of all local variables. With <code>yield</code>, we can use a <code>for</code> loop to &ldquo;return&rdquo; the same dive command the next 30 times the function is called! Lines 11-16 of the below code show the magic:</p>
<div class="codehilite"><pre><span></span><span class="k">class</span> <span class="nc">GoalkeeperWithGenerator</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">features</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">features</span> <span class="o">=</span> <span class="n">features</span>
<span class="k">def</span> <span class="nf">behavior</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
<span class="n">features</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">features</span>
<span class="k">if</span> <span class="n">features</span><span class="o">.</span><span class="n">ballFar</span><span class="p">:</span>
<span class="k">yield</span> <span class="s1">&#39;stand&#39;</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">if</span> <span class="n">features</span><span class="o">.</span><span class="n">ballOnLeft</span><span class="p">:</span>
<span class="n">command</span> <span class="o">=</span> <span class="s1">&#39;diveLeft&#39;</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">command</span> <span class="o">=</span> <span class="s1">&#39;diveRight&#39;</span>
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">xrange</span><span class="p">(</span><span class="mi">30</span><span class="p">):</span>
<span class="k">yield</span> <span class="n">command</span>
</pre></div>
<p>Here&rsquo;s a simple driver script that shows how to use our goalkeepers:</p>
<div class="codehilite"><pre><span></span><span class="kn">import</span> <span class="nn">random</span>
<span class="n">f</span> <span class="o">=</span> <span class="n">Features</span><span class="p">()</span>
<span class="n">g1</span> <span class="o">=</span> <span class="n">Goalkeeper</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>
<span class="n">g2</span> <span class="o">=</span> <span class="n">GoalkeeperWithGenerator</span><span class="p">(</span><span class="n">f</span><span class="p">)</span><span class="o">.</span><span class="n">behavior</span><span class="p">()</span>
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">xrange</span><span class="p">(</span><span class="mi">10000</span><span class="p">):</span>
<span class="n">f</span><span class="o">.</span><span class="n">ballFar</span> <span class="o">=</span> <span class="n">random</span><span class="o">.</span><span class="n">random</span><span class="p">()</span> <span class="o">&gt;</span> <span class="mf">0.1</span>
<span class="n">f</span><span class="o">.</span><span class="n">ballOnLeft</span> <span class="o">=</span> <span class="n">random</span><span class="o">.</span><span class="n">random</span><span class="p">()</span> <span class="o">&lt;</span> <span class="mf">0.5</span>
<span class="n">g1action</span> <span class="o">=</span> <span class="n">g1</span><span class="o">.</span><span class="n">next</span><span class="p">()</span>
<span class="n">g2action</span> <span class="o">=</span> <span class="n">g2</span><span class="o">.</span><span class="n">next</span><span class="p">()</span>
<span class="k">print</span> <span class="s2">&quot;</span><span class="si">%s</span><span class="se">\t</span><span class="si">%s</span><span class="se">\t</span><span class="si">%s</span><span class="se">\t</span><span class="si">%s</span><span class="s2">&quot;</span> <span class="o">%</span> <span class="p">(</span>
<span class="n">f</span><span class="o">.</span><span class="n">ballFar</span><span class="p">,</span> <span class="n">f</span><span class="o">.</span><span class="n">ballOnLeft</span><span class="p">,</span> <span class="n">g1action</span><span class="p">,</span> <span class="n">g2action</span><span class="p">)</span>
<span class="k">assert</span><span class="p">(</span><span class="n">g1action</span> <span class="o">==</span> <span class="n">g2action</span><span class="p">)</span>
</pre></div>
<p>&hellip; and we&rsquo;re done! I hope you&rsquo;ll agree that the generator-based keeper is much easier to understand and maintain than the state-machine-based keeper. You can grab the full source code below and take a look for yourself.</p>
<div class="codehilite"><pre><span></span><span class="ch">#!/usr/bin/env python</span>
<span class="k">class</span> <span class="nc">Features</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
<span class="n">ballFar</span> <span class="o">=</span> <span class="bp">True</span>
<span class="n">ballOnLeft</span> <span class="o">=</span> <span class="bp">True</span>
<span class="k">class</span> <span class="nc">Goalkeeper</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">features</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">features</span> <span class="o">=</span> <span class="n">features</span>
<span class="bp">self</span><span class="o">.</span><span class="n">diveFramesRemaining</span> <span class="o">=</span> <span class="mi">0</span>
<span class="bp">self</span><span class="o">.</span><span class="n">lastDiveCommand</span> <span class="o">=</span> <span class="bp">None</span>
<span class="k">def</span> <span class="nf">next</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">features</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">features</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">diveFramesRemaining</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">diveFramesRemaining</span> <span class="o">-=</span> <span class="mi">1</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">lastDiveCommand</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">if</span> <span class="n">features</span><span class="o">.</span><span class="n">ballFar</span><span class="p">:</span>
<span class="k">return</span> <span class="s1">&#39;stand&#39;</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">if</span> <span class="n">features</span><span class="o">.</span><span class="n">ballOnLeft</span><span class="p">:</span>
<span class="n">command</span> <span class="o">=</span> <span class="s1">&#39;diveLeft&#39;</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">command</span> <span class="o">=</span> <span class="s1">&#39;diveRight&#39;</span>
<span class="bp">self</span><span class="o">.</span><span class="n">lastDiveCommand</span> <span class="o">=</span> <span class="n">command</span>
<span class="bp">self</span><span class="o">.</span><span class="n">diveFramesRemaining</span> <span class="o">=</span> <span class="mi">29</span>
<span class="k">return</span> <span class="n">command</span>
<span class="k">class</span> <span class="nc">GoalkeeperWithGenerator</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">features</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">features</span> <span class="o">=</span> <span class="n">features</span>
<span class="k">def</span> <span class="nf">behavior</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
<span class="n">features</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">features</span>
<span class="k">if</span> <span class="n">features</span><span class="o">.</span><span class="n">ballFar</span><span class="p">:</span>
<span class="k">yield</span> <span class="s1">&#39;stand&#39;</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">if</span> <span class="n">features</span><span class="o">.</span><span class="n">ballOnLeft</span><span class="p">:</span>
<span class="n">command</span> <span class="o">=</span> <span class="s1">&#39;diveLeft&#39;</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">command</span> <span class="o">=</span> <span class="s1">&#39;diveRight&#39;</span>
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">xrange</span><span class="p">(</span><span class="mi">30</span><span class="p">):</span>
<span class="k">yield</span> <span class="n">command</span>
<span class="kn">import</span> <span class="nn">random</span>
<span class="n">f</span> <span class="o">=</span> <span class="n">Features</span><span class="p">()</span>
<span class="n">g1</span> <span class="o">=</span> <span class="n">Goalkeeper</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>
<span class="n">g2</span> <span class="o">=</span> <span class="n">GoalkeeperWithGenerator</span><span class="p">(</span><span class="n">f</span><span class="p">)</span><span class="o">.</span><span class="n">behavior</span><span class="p">()</span>
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">xrange</span><span class="p">(</span><span class="mi">10000</span><span class="p">):</span>
<span class="n">f</span><span class="o">.</span><span class="n">ballFar</span> <span class="o">=</span> <span class="n">random</span><span class="o">.</span><span class="n">random</span><span class="p">()</span> <span class="o">&gt;</span> <span class="mf">0.1</span>
<span class="n">f</span><span class="o">.</span><span class="n">ballOnLeft</span> <span class="o">=</span> <span class="n">random</span><span class="o">.</span><span class="n">random</span><span class="p">()</span> <span class="o">&lt;</span> <span class="mf">0.5</span>
<span class="n">g1action</span> <span class="o">=</span> <span class="n">g1</span><span class="o">.</span><span class="n">next</span><span class="p">()</span>
<span class="n">g2action</span> <span class="o">=</span> <span class="n">g2</span><span class="o">.</span><span class="n">next</span><span class="p">()</span>
<span class="k">print</span> <span class="s2">&quot;</span><span class="si">%s</span><span class="se">\t</span><span class="si">%s</span><span class="se">\t</span><span class="si">%s</span><span class="se">\t</span><span class="si">%s</span><span class="s2">&quot;</span> <span class="o">%</span> <span class="p">(</span>
<span class="n">f</span><span class="o">.</span><span class="n">ballFar</span><span class="p">,</span> <span class="n">f</span><span class="o">.</span><span class="n">ballOnLeft</span><span class="p">,</span> <span class="n">g1action</span><span class="p">,</span> <span class="n">g2action</span><span class="p">)</span>
<span class="k">assert</span><span class="p">(</span><span class="n">g1action</span> <span class="o">==</span> <span class="n">g2action</span><span class="p">)</span>
</pre></div>
</div>
</div>
<div id="footer">
<div class="content">
&copy; 2023 <a href="/" class="undecorated">Colin McMillen</a>. No cookies, no tracking.
</div>
</div>
</div>
</body>
</html>