This is an introduction to more advanced controllers that have memory, or "state". This module provides an overview of Finite State Machine controllers, and the general notion of sequencing control. When completed, the reader should be ready to apply this knowledge, and explore specific ideas in AI and robotics.
Pyro Sequencing Control
In order to create more complex robot controllers, it is useful to be able to group low-level robot commands into logical units, often called behaviors. Each behavior is triggered by a particular condition in the environment, and responds appropriately. Once the initiating condition has been addressed, the current behavior can pass control off to another behavior.
Example: Traversing a Square
Consider the task of getting a robot to travel in a square. This task can be broken up into two simpler behaviors which we'll call edge and turn. In the edge behavior, we'll keep track of how far we've traveled aonlg a side of the square. In the turn behavior, we'll keep track of the angle of a turn and stop once we've gone 90 degrees. We can simply switch back and forth between these two behaviors to traverse a square.
One straight-forward method of implementing this style of behavior-based control is through finite state machines (FSMs). Each state in the FSM represents a robot behavior. Using a FSM you can build up a graph of states to go in and out of, sequencing the order of states into a "plan".
In the program below, we will use the robot's dead reckoning to monitor its position and heading. Recall that dead reckoning can quickly accumulate errors, so the square-like motion will gradually degrade over time. You can try this program using the Stage simulator with the room.cfg world.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
from pyrobot.geometry import * # import distance function from pyrobot.brain.behaviors import State, FSMBrain class edge (State): def onActivate(self): # method called when activated or gotoed self.startX = self.robot.x self.startY = self.robot.y def step(self): x = self.robot.x y = self.robot.y dist = distance( self.startX, self.startY, x, y) print "EDGE: actual = (%f, %f) start = (%f, %f); dist = %f" \ % (x, y, self.startX, self.startY, dist) if dist > 1.0: self.goto('turn') else: self.robot.move(.3, 0) class turn (State): def onActivate(self): self.th = self.robot.th def step(self): th = self.robot.th print "TURN: actual = %f start = %f" % (th, self.th) if angleAdd(th, - self.th) > 90: self.goto('edge') else: self.robot.move(0, .2) def INIT(engine): brain = FSMBrain(engine=engine) # add a few states: brain.add(edge(1)) # 1 makes it active brain.add(turn()) return brain
Notice that each State in a FSMBrain is defined as its own class with its own methods. The next section describes these State methods. Typically every State will include the step method to describe what computation to do on each time step that the state is active.
For a much more complex example that uses blob vision and the gripper see ExamplePickingUpPucks.
Commonly used state methods:
|setup()||called when state is created (like a constructor)|
|goto(statename)||called to switch to another state|
|onActivate()||called when this state becomes active|
|onDeactivate()||called when this state becomes inactive|
|step()||called each step|
|pop()||used to return to the last pushed state|
|push(<STATE>)||used to set the state to return after a pop. <STATE> is optional, and defaults to the current state name|
Also some less used state methods:
|onGoto(argv)||called when a state is initiated with goto()|
|getState(statename)||returns a pointer to a named state|
|activate(statename)||like goto(), but doesn't deactivate self (causes a fork)|
|deactivate(statename)||sets named state to inactive|
Consider the problem of adding a state to get the robot unstuck if it finds itself against a wall. You might need to go to the "stuck" state from every other state. However, you might want to return to the same state from which you came. Currently, the only way to return back to the same state would be to have a unique "getting unstuck state" for each and every state. This is due to the fact that the only way to have memory in an FSM is in the state. However, there is a simple mechanism in the Pyro FSM which allows you to keep track of a "calling" state, so that you can return to it, and thereby bypass this problem. To use this mechanism, you call the push and pop methods.
Consider the following step method from a state in an FSM:
def step(self): x = self.robot.x y = self.robot.y dist = distance( self.startX, self.startY, x, y) print "EDGE: actual = (%f, %f) start = (%f, %f); dist = %f" \ % (x, y, self.startX, self.startY, dist) if self.robot.stall: self.push() self.goto('stuck') elif dist > 1.0: self.goto('turn') else: self.move(.3, 0)
Here we have added a call to push() before we head off to the stuck state. In the stuck state, we only need call pop() when we are finished getting unstuck, like so:
class stuck(State): def step(self): if done: self.pop()
The push method defaults to pushing the name of the current state on a stack of states. You can also pass in a different state for which you wish control to pass when the state being called is finished. To indicate that the state is done, simply call the pop method. This will retrieve the last state added to the stack, and then goto it.
If you attempt to pop when no push has been issued, an error will be raised. The stack is a part of the brain, and can be accessed via brain.stack. You may recursively push states onto the stack, or may have a series of goto's before issuing a pop().
Note that when you pop back, you will enter the return state at the begining of the step method: you will not continue from the line after a goto(), but will start that state at the top again.
Advanced Python users might be interested in implementing a FSM with a true "gosub". A discussion of these ideas are discussed in the section PyroModuleFSM:UsingGenerators.