diff --git a/blog/20070502-robot-behaviors-python.html b/blog/20070502-robot-behaviors-python.html index 7dd7f0d..04c82fe 100644 --- a/blog/20070502-robot-behaviors-python.html +++ b/blog/20070502-robot-behaviors-python.html @@ -42,91 +42,91 @@
The usual approach to robot behavior design relies on hierarchical state machines. Specifically, we might be in a “standing” state while the ball is far away; when the ball becomes close, we enter a “diving” 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’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.
On to the state-machine approach. First, we’ll have a class called Features that abstracts the robot’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:
-class Features(object): - ballFar = True - ballOnLeft = True +class Features(object): + ballFar = True + ballOnLeft = TrueNext, we make the goalkeeper. The keeper’s behavior is specified by the
-next()
function, which is called thirty times per second by the robot’s main event loop (every time the on-board camera produces a new image). Thenext()
function returns one of three actions:"stand"
,"diveLeft"
, or"diveRight"
, based on the current values of the Features object. For now, let’s pretend that requirement 3 doesn’t exist.class Goalkeeper(object): - def __init__(self, features): - self.features = features +class Goalkeeper(object): + def __init__(self, features): + self.features = features - def next(self): - features = self.features - if features.ballFar: - return 'stand' + def next(self): + features = self.features + if features.ballFar: + return 'stand' + else: + if features.ballOnLeft: + return 'diveLeft' else: - if features.ballOnLeft: - return 'diveLeft' - else: - return 'diveRight' + return 'diveRight'That was simple enough. The constructor takes in the
-Features
object; thenext()
method checks the currentFeatures
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"dive"
state and which direction we dove. We’ll do this by adding a couple of instance variables (self.diveFramesRemaining
andself.lastDiveCommand
) to the Goalkeeper class. These variables are set when we initiate the dive. At the top of thenext()
function, we check ifself.diveFramesRemaining
is positive; if so, we can immediately returnself.lastDiveCommand
without consulting theFeatures
. Here’s the code:class Goalkeeper(object): - def __init__(self, features): - self.features = features - self.diveFramesRemaining = 0 - self.lastDiveCommand = None +class Goalkeeper(object): + def __init__(self, features): + self.features = features + self.diveFramesRemaining = 0 + self.lastDiveCommand = None - def next(self): - features = self.features - if self.diveFramesRemaining > 0: - self.diveFramesRemaining -= 1 - return self.lastDiveCommand + def next(self): + features = self.features + if self.diveFramesRemaining > 0: + self.diveFramesRemaining -= 1 + return self.lastDiveCommand + else: + if features.ballFar: + return 'stand' else: - if features.ballFar: - return 'stand' + if features.ballOnLeft: + command = 'diveLeft' else: - if features.ballOnLeft: - command = 'diveLeft' - else: - command = 'diveRight' - self.lastDiveCommand = command - self.diveFramesRemaining = 29 - return command + command = 'diveRight' + self.lastDiveCommand = command + self.diveFramesRemaining = 29 + return commandThis satisfies all the requirements, but it’s ugly. We’ve added a couple of bookkeeping variables to the Goalkeeper class. Code to properly maintain these variables is sprinkled all over the
next()
function. Even worse, the structure of the code no longer accurately represents the programmer’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 originalnext()
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 originalnext()
function.)With generators, we can preserve the form of the original
-next()
function and keep the bookkeeping only where it’s needed. If you’re not familiar with generators, you can think of them as a special kind of function. Theyield
keyword is essentially equivalent toreturn
, but the next time the generator is called, execution continues from the point of the lastyield
, preserving the state of all local variables. Withyield
, we can use afor
loop to “return” the same dive command the next 30 times the function is called! Lines 11-16 of the below code show the magic:class GoalkeeperWithGenerator(object): - def __init__(self, features): - self.features = features +class GoalkeeperWithGenerator(object): + def __init__(self, features): + self.features = features - def behavior(self): - while True: - features = self.features - if features.ballFar: - yield 'stand' + def behavior(self): + while True: + features = self.features + if features.ballFar: + yield 'stand' + else: + if features.ballOnLeft: + command = 'diveLeft' else: - if features.ballOnLeft: - command = 'diveLeft' - else: - command = 'diveRight' - for i in xrange(30): - yield command + command = 'diveRight' + for i in xrange(30): + yield commandHere’s a simple driver script that shows how to use our goalkeepers:
-import random +diff --git a/feed.atom b/feed.atom index 7d824bf..f8b32e8 100644 --- a/feed.atom +++ b/feed.atom @@ -4,7 +4,7 @@import random - f = Features() - g1 = Goalkeeper(f) - g2 = GoalkeeperWithGenerator(f).behavior() +f = Features() +g1 = Goalkeeper(f) +g2 = GoalkeeperWithGenerator(f).behavior() - for i in xrange(10000): - f.ballFar = random.random() > 0.1 - f.ballOnLeft = random.random() < 0.5 - g1action = g1.next() - g2action = g2.next() - print "%s\t%s\t%s\t%s" % ( - f.ballFar, f.ballOnLeft, g1action, g2action) - assert(g1action == g2action) +for i in xrange(10000): + f.ballFar = random.random() > 0.1 + f.ballOnLeft = random.random() < 0.5 + g1action = g1.next() + g2action = g2.next() + print "%s\t%s\t%s\t%s" % ( + f.ballFar, f.ballOnLeft, g1action, g2action) + assert(g1action == g2action)Colin McMillen's Blog -2021-07-01T20:10:09-04:00 +2021-07-01T20:13:55-04:00 @@ -29,91 +29,91 @@ Colin McMillen The usual approach to robot behavior design relies on hierarchical state machines. Specifically, we might be in a “standing” state while the ball is far away; when the ball becomes close, we enter a “diving” 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’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.
On to the state-machine approach. First, we’ll have a class called Features that abstracts the robot’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:
-class Features(object): - ballFar = True - ballOnLeft = True +class Features(object): + ballFar = True + ballOnLeft = TrueNext, we make the goalkeeper. The keeper’s behavior is specified by the
-next()
function, which is called thirty times per second by the robot’s main event loop (every time the on-board camera produces a new image). Thenext()
function returns one of three actions:"stand"
,"diveLeft"
, or"diveRight"
, based on the current values of the Features object. For now, let’s pretend that requirement 3 doesn’t exist.class Goalkeeper(object): - def __init__(self, features): - self.features = features +class Goalkeeper(object): + def __init__(self, features): + self.features = features - def next(self): - features = self.features - if features.ballFar: - return 'stand' + def next(self): + features = self.features + if features.ballFar: + return 'stand' + else: + if features.ballOnLeft: + return 'diveLeft' else: - if features.ballOnLeft: - return 'diveLeft' - else: - return 'diveRight' + return 'diveRight'That was simple enough. The constructor takes in the
-Features
object; thenext()
method checks the currentFeatures
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"dive"
state and which direction we dove. We’ll do this by adding a couple of instance variables (self.diveFramesRemaining
andself.lastDiveCommand
) to the Goalkeeper class. These variables are set when we initiate the dive. At the top of thenext()
function, we check ifself.diveFramesRemaining
is positive; if so, we can immediately returnself.lastDiveCommand
without consulting theFeatures
. Here’s the code:class Goalkeeper(object): - def __init__(self, features): - self.features = features - self.diveFramesRemaining = 0 - self.lastDiveCommand = None +class Goalkeeper(object): + def __init__(self, features): + self.features = features + self.diveFramesRemaining = 0 + self.lastDiveCommand = None - def next(self): - features = self.features - if self.diveFramesRemaining > 0: - self.diveFramesRemaining -= 1 - return self.lastDiveCommand + def next(self): + features = self.features + if self.diveFramesRemaining > 0: + self.diveFramesRemaining -= 1 + return self.lastDiveCommand + else: + if features.ballFar: + return 'stand' else: - if features.ballFar: - return 'stand' + if features.ballOnLeft: + command = 'diveLeft' else: - if features.ballOnLeft: - command = 'diveLeft' - else: - command = 'diveRight' - self.lastDiveCommand = command - self.diveFramesRemaining = 29 - return command + command = 'diveRight' + self.lastDiveCommand = command + self.diveFramesRemaining = 29 + return commandThis satisfies all the requirements, but it’s ugly. We’ve added a couple of bookkeeping variables to the Goalkeeper class. Code to properly maintain these variables is sprinkled all over the
next()
function. Even worse, the structure of the code no longer accurately represents the programmer’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 originalnext()
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 originalnext()
function.)With generators, we can preserve the form of the original
-next()
function and keep the bookkeeping only where it’s needed. If you’re not familiar with generators, you can think of them as a special kind of function. Theyield
keyword is essentially equivalent toreturn
, but the next time the generator is called, execution continues from the point of the lastyield
, preserving the state of all local variables. Withyield
, we can use afor
loop to “return” the same dive command the next 30 times the function is called! Lines 11-16 of the below code show the magic:class GoalkeeperWithGenerator(object): - def __init__(self, features): - self.features = features +class GoalkeeperWithGenerator(object): + def __init__(self, features): + self.features = features - def behavior(self): - while True: - features = self.features - if features.ballFar: - yield 'stand' + def behavior(self): + while True: + features = self.features + if features.ballFar: + yield 'stand' + else: + if features.ballOnLeft: + command = 'diveLeft' else: - if features.ballOnLeft: - command = 'diveLeft' - else: - command = 'diveRight' - for i in xrange(30): - yield command + command = 'diveRight' + for i in xrange(30): + yield commandHere’s a simple driver script that shows how to use our goalkeepers:
-import random +import random - f = Features() - g1 = Goalkeeper(f) - g2 = GoalkeeperWithGenerator(f).behavior() +f = Features() +g1 = Goalkeeper(f) +g2 = GoalkeeperWithGenerator(f).behavior() - for i in xrange(10000): - f.ballFar = random.random() > 0.1 - f.ballOnLeft = random.random() < 0.5 - g1action = g1.next() - g2action = g2.next() - print "%s\t%s\t%s\t%s" % ( - f.ballFar, f.ballOnLeft, g1action, g2action) - assert(g1action == g2action) +for i in xrange(10000): + f.ballFar = random.random() > 0.1 + f.ballOnLeft = random.random() < 0.5 + g1action = g1.next() + g2action = g2.next() + print "%s\t%s\t%s\t%s" % ( + f.ballFar, f.ballOnLeft, g1action, g2action) + assert(g1action == g2action)