Robots, mazes, and subsumption architecture

Programming virtual robots in the Java language

Robotics is a field that long ago left the realm of science fiction to drive advances in industrial automation, health care, space exploration, and other applications. Software robot simulators not only simplify development work for robotics engineers, but they also give researchers a tool for studying artificial intelligence (AI) algorithms and machine learning. One such research-focused simulator is the open source Simbad project, built on Java 3D technology (see Related topics). This article shows how to program virtual robots using the Simbad toolkit to realize the robotics-design philosophy of subsumption architecture.

This article starts with a brief overview of robotics and explains the subsumption architecture concept. Then it introduces the Simbad toolkit and shows how to implement a subsumption architecture within Simbad. Then, you'll program a simple robot using this architecture. Finally, you'll look at the fascinating world of mazes and program a second robot that, unlike Homer Simpson (see Related topics), can find its way out of a maze. Your robots won't be physical but will "live" in the Simbad virtual world.

Robotics programming

The word robot has no universally accepted definition. For this article's purposes, consider a robot to have three parts:

A collection of sensors

A program that defines the robot's behavior

A collection of actuators and effectors

Traditional robotics

In traditional robotics (that is, robotics before 1986), a robot has a central "brain" that builds and maintains a "map" of the world and makes plans based on that map. First, the robot's sensors — for example, touch sensors, light sensors, and ultrasonic sensors — take in information from its environment. The robot's brain fuses all the information gathered by its sensors and updates the map of its world. The robot then decides on a course of action. It acts through its actuators and effectors. Actuators are basically motors and are attached to effectors, which interact with the robot's environment. Examples of effectors are wheels and arms. (The term actuator is often used loosely to mean either an actuator or an effector.)

In short, a traditional robot takes input from possibly multiple sensors, fuses this sensor information, updates its map of the world, makes a plan based on its current view of the world, and then acts. This method is problematic, though. For one thing, it's highly computation-intensive. Also, keeping a world map up to date is hard because the external world is always changing. Further, many organisms, such as insects, thrive without a map of their external world or even memory; might it be better to try to emulate them? These problems led to a new style of robotics, called behavior-based robotics (BBR). BBR is perhaps the dominant philosophy in robotics labs today.

Subsumption architecture

BBR can be implemented using a subsumption architecture. Subsumption architecture's inventor — Rodney A. Brooks, now head of the MIT AI Lab — introduced it in his seminal 1986 paper, "Elephants Don't Play Chess" (see Related topics). Behavior-based robots are built up out of a set of independent, simple behaviors. Behaviors are defined by what triggers them (usually a sensor reading) and the action taken (which usually involves an effector). Behaviors are layered on top of one another. When two behaviors conflict, a central arbitrator decides which should take precedence. The robot's overall behavior is emergent, and, according to BBR proponents, can be greater than the sum of its parts. The emergent higher-level behaviors subsume the lower-level behaviors. Rather than try to engineer a robot, you just add behaviors and see what emerges.

Simbad: A robotic simulation environment

LEGO Mindstorms This article focuses on building softbots, but if you want to try building physical robots, LEGO Mindstorms is an excellent robotics kit. A sign at LEGO Mindstorms headquarters reads: "We will do for robotics what iPod did for music." LEGO introduced the first version of its Mindstorms robotics kit in 1998. The kit was immediately a big seller, bigger than LEGO expected. Although it might seem a little pricey at $250, keep in mind that that's the price of an iPod Classic, and everyone has an iPod. However, the iPod isn't nearly as hackable as Mindstorms. Shortly after Mindstorms was first released, hardware hackers started pulling apart the Mindstorms RCX brick — the "brains" of Mindstorms robots — and reverse-engineering it. LEGO hadn't anticipated this and wasn't sure whether to allow it or to send out cease-and-desist letters. To the credit of LEGO management, they decided to allow Mindstorms hackers to do what they liked. The result is a flourishing Mindstorms community (see Related topics). Although Mindstorms ships with only a drag-and-drop graphical programming language called NXT-G, software hackers were soon porting other languages, such as C and the Java language, to Mindstorms. As a result, it's estimated that 50 percent of Mindstorms kits are being used by adults.

Simbad allows you to simulate robots in software. According to the project Web site, it "enables programmers to write their own robot controller, modify the environment, and use the available sensors. It is mainly dedicated to researchers/programmers who want a simple basis for studying Situated Artificial Intelligence, Machine Learning, and more generally AI algorithms, in the context of Autonomous Robotics and Autonomous Agents."

Simbad is written in the Java language by Louis Hugues and Nicolas Bredeche. The project, hosted at SourceForge.net, is free for you to use and modify under the conditions of the GNU General Public License.

Technical details

A Simbad world can contain Agents (robots) and inanimate objects (boxes, walls, lights, and so on). Time in the Simbad world is divided into discrete ticks. Simbad schedules time-sharing among Agents. Like physical robots, Simbad Agents have both sensors (distance, touch, light, and so on) and actuators (usually wheels). At each tick, a robot can act.

Agents override the performBehavior() method to determine their behavior. In performBehavior() , a robot can take note of sensor readings and set its translational and rotational velocities. performBehavior() takes place in a moment, so it can't issue a command such as "go forward one meter." To get around this limitation, you usually have to keep track of your robot's state. You might also use a timer variable to keep track of how many ticks of the clock you've been in the current state.

The Simbad API

For this article's exercises, you'll mainly be concerned with two Simbad API packages:

simbad.sim : This package's classes represent both your robot and the world it lives in. They include (among others): Agent : Agent s are robots. Arch : An arch that your robot can drive around or under. Box : Can be used as obstacles in the robot's world. CameraSensor : Lets you view the robot's world from its point of view. EnvironmentDescription : Represents the "world" to which you add robots and objects such as walls or boxes. LampActuator : A lamp that you can add to your robot. LightSensor : Senses the intensity of light. RangeSensorBelt : Contains a set of range sensors around the robot. RobotFactory : Use this to add sensors to your robot. Wall : Another type of obstacle for your robot.

: This package's classes represent both your robot and the world it lives in. They include (among others): simbad.gui : This package's classes display your robot world and let you control it. They include (among others): Simbad : The frame showing the robot world, sensor input, and controls.

: This package's classes display your robot world and let you control it. They include (among others):

Implementing a subsumption architecture in Simbad

To start implementing a subsumption architecture in Simbad, you define a subclass of Agent , called BehaviorBasedAgent . BehaviorBasedAgent contains an array of Behavior s and a boolean matrix specifying which Behavior s suppress which other Behavior s:

private Behavior[] behaviors; private boolean suppresses[][];

BehaviorBasedAgent acts as a scheduler of Behavior s. Listing 1 shows the code that cycles over behaviors (using the currentBehaviorIndex class variable to keep track of which behavior should get its turn next) and arbitrates among them:

Listing 1. Code for cycling over and arbitrating behaviors

protected void performBehavior() { boolean isActive[] = new boolean[behaviors.length]; for (int i = 0; i < isActive.length; i++) { isActive[i] = behaviors[i].isActive(); } boolean ranABehavior = false; while (!ranABehavior) { boolean runCurrentBehavior = isActive[currentBehaviorIndex]; if (runCurrentBehavior) { for (int i = 0; i < suppresses.length; i++) { if (isActive[i] && suppresses[i][currentBehaviorIndex]) { runCurrentBehavior = false; break; } } } if (runCurrentBehavior) { if (currentBehaviorIndex < behaviors.length) { Velocities newVelocities = behaviors[currentBehaviorIndex].act(); this.setTranslationalVelocity(newVelocities .getTranslationalVelocity()); this .setRotationalVelocity(newVelocities .getRotationalVelocity()); } ranABehavior = true; } if (behaviors.length > 0) { currentBehaviorIndex = (currentBehaviorIndex + 1) % behaviors.length; } } }

Note that performBehavior() overrides simbad.sim.Agent.performBehavior() .

Behavior has two abstract methods:.

isActive() returns a boolean value depending on whether it should be active given current state of the sensors. (All Behaviors share a set of sensors.)

returns a boolean value depending on whether it should be active given current state of the sensors. (All share a set of sensors.) act() returns a pair of velocities — translational and rotational (in that order) — that show the action it wants the motor to take.

Example: A light-seeking, wandering bot

Now you'll create an example bot that has four Behavior s (in order of decreasing precedence), shown in Listings 2 through 5. (Download the source code this article uses.)

Avoidance : Change direction after collision or to avoid collision.

: Change direction after collision or to avoid collision. LightSeeking : Move toward light.

: Move toward light. Wandering : Occasionally change direction randomly.

: Occasionally change direction randomly. StraightLine : Move in a straight line.

Listing 2. The Avoidance class (based on the Simbad SingleAvoiderDemo.java demo code)

public boolean isActive() { return getSensors().getBumpers().oneHasHit() || getSensors().getSonars().oneHasHit(); } public Velocities act() { double translationalVelocity = 0.8; double rotationalVelocity = 0; RangeSensorBelt sonars = getSensors().getSonars(); double rotationalVelocityFactor = Math.PI / 32; if (getSensors().getBumpers().oneHasHit()) { // if ran into something translationalVelocity = -0.1; rotationalVelocity = Math.PI / 8 - (rotationalVelocityFactor * Math.random()); } else if (sonars.oneHasHit()) { // reads the three front quadrants double left = sonars.getFrontLeftQuadrantMeasurement(); double right = sonars.getFrontRightQuadrantMeasurement(); double front = sonars.getFrontQuadrantMeasurement(); // if obstacle near if ((front < 0.7) || (left < 0.7) || (right < 0.7)) { double maxRotationalVelocity = Math.PI / 4; if (left < right) rotationalVelocity = -maxRotationalVelocity - (rotationalVelocityFactor * Math.random()); else rotationalVelocity = maxRotationalVelocity - (rotationalVelocityFactor * Math.random()); translationalVelocity = 0; } else { rotationalVelocity = 0; translationalVelocity = 0.6; } } return new Velocities(translationalVelocity, rotationalVelocity); }

Listing 3. The LightSeeking class (based on the Simbad LightSearchDemo.java demo code)

public boolean isActive() { float llum = getSensors().getLightSensorLeft().getAverageLuminance(); float rlum = getSensors().getLightSensorRight().getAverageLuminance(); double luminance = (llum + rlum) / 2.0; // Seek light if it's near. return luminance > LUMINANCE_SEEKING_MIN; } public Velocities act() { // turn towards light float llum = getSensors().getLightSensorLeft().getAverageLuminance(); float rlum = getSensors().getLightSensorRight().getAverageLuminance(); double translationalVelocity = 0.5 / (llum + rlum); double rotationalVelocity = (llum - rlum) * Math.PI / 4; return new Velocities(translationalVelocity, rotationalVelocity); }

Listing 4. The Wandering class

public boolean isActive() { return random.nextDouble() < WANDERING_PROBABILITY; } public Velocities act() { return new Velocities(0.8, random.nextDouble() * 2 * Math.PI); }

Listing 5. The StraightLine class

public boolean isActive() { return true; } public Velocities act() { return new Velocities(0.8, 0.0); }

Listing 6 specifies which behaviors suppress which other behaviors:

Listing 6. Specifying overall behavior suppression

private void initBehaviorBasedAgent(BehaviorBasedAgent behaviorBasedAgent) { Sensors sensors = behaviorBasedAgent.getSensors(); Behavior[] behaviors = { new Avoidance(sensors), new LightSeeking(sensors), new Wandering(sensors), new StraightLine(sensors), }; boolean subsumes[][] = { { false, true, true, true }, { false, false, true, true }, { false, false, false, true }, { false, false, false, false } }; behaviorBasedAgent.initBehaviors(behaviors, subsumes); }

Note that although the Behavior s in this example have a total order of precedence (of suppression), it needn't be that way.

As an exercise, you might like to try the following:

Add a socialization behavior: move toward friends and away from enemies.

Add a light-avoidance behavior.

Add lights to some of the robots, so they're attracted to each other.

Mazes

"Finally! I knew we could solve that maze using Tremaux's algorithm!" —Lisa Simpson

Of the several existing maze-solving algorithms, two commonly used ones consume a constant amount of memory regardless of the maze's size. They are wall-following and the Pledge algorithm (invented by Jon Pledge of Exeter, England at age 12). Tremaux's algorithm (Lisa Simpson's algorithm of choice) is an excellent algorithm, but for simplicity this article concentrates on the wall-following and Pledge algorithms.

Maze-generation algorithms Not only do maze-solving algorithms abound, but so do algorithms for generating mazes. The mazes this article is concerned with are called perfect mazes. Perfect mazes have exactly one path from any point in the maze to any other point (this rules out mazes with loops, "islands," or closed-off sections). Most perfect-maze-generation algorithms work by starting with a maze that has only its exterior walls and "growing" walls inward segment by segment. To ensure that the maze is perfect, every time you add a new segment, you make sure you're not creating a loop or a closed-off section.

Wall-following

Wall-following is a simple maze algorithm you might have learned as a child. All you do to solve a maze using this algorithm is keep your left hand on the left wall (or your right hand on the right wall) and just follow it along until you exit the maze. It's easy to see that this algorithm always works if the maze you're in has an entrance and an exit on its border. However, if the goal is within an island — a part of the maze that's disconnected from the rest of the maze — this algorithm can't find a solution because it can't "hop" over to the island.

The Pledge algorithm

The Pledge algorithm is more sophisticated than wall-following and solves a larger class of mazes because it can jump between islands. The basic idea of the Pledge algorithm is that you pick an absolute direction (such as North, South, East, or West) and always try to go in that direction. I'll call this direction your favored direction. When you run into a wall, you turn right and do left-hand wall-following until you're facing your favored direction and the sum total of your turns is zero (where clockwise turns are considered negative and counter-clockwise turns are considered positive). At that point, you continue going straight in your favorite direction again, and so on. The requirement that your turns sum to zero is necessary to keep you from getting caught in certain loops, such as one shaped like a capital G (try it out on paper to see what I mean).

Algernon: A maze-solving robot

Now it's time to amaze (forgive the pun) your friends and build a maze-solving robot named Algernon.

Designing the robot

To implement either wall-following or the Pledge algorithm, you need to know when the robot reaches an intersection and — when it does reach an intersection — which directions passages go off in.

There's probably more than one way to accomplish this, but you'll do it by mounting a sonar sensor on the robot's left side. This sensor will alert you when the maze has passages going off to the left. To know when the passage the robot is traveling down ends (that is, when it "runs into" a wall), you'll add a touch sensor to the front of the robot.

Programming wall-following

You program Algernon using the algernon.subsumption package (download the source code). Algernon is pretty simple as far as robots go, and you could program him in a straightforward "procedural" manner. However, using subsumption programming makes the code a lot cleaner, more modular, and easier to understand even in a robot as simple as this.

To simplify the algorithm implementation, I'll assume that walls are laid out at right angles. Thus, all the robot's turns will be 90-degree left or 90-degree right turns.

If you think about the (left-hand) wall-following algorithm, you'll see that you can decompose it into four behaviors:

Go straight.

When you run into a wall, turn right.

If you see a passageway to your left, turn left.

Stop when you reach the goal.

You need to decide the priority of the four behaviors. The previous list is correct in order of decreasing priority. You'll end up with four classes, each of which extends Behavior :

GoStraight

TurnRight

TurnLeft

ReachGoal

Listing 7 is the GoStraight code. TRANSLATIONAL_VELOCITY is a constant set to 0.4 :

Listing 7. Behavior code for going straight

public boolean isActive() { return true; } public Velocities act() { double rotationalVelocity = 0.0; return new Velocities(TRANSLATIONAL_VELOCITY, rotationalVelocity); }

Listing 8 is the TurnRight code. getRotationCount() returns the number of ticks of the clock it will take to rotate 90 degrees at the rotational velocity you're using.

Listing 8. Behavior code for turning right

public boolean isActive() { if (turningRightCount > 0) { return true; } RangeSensorBelt bumpers = getSensors().getBumpers(); // Check the front bumper. if (bumpers.hasHit(0)) { backingUpCount = 10; turningRightCount = getRotationCount(); return true; } else { return false; } } public Velocities act() { if (backingUpCount > 0) { // We back up a bit (we just ran into a wall) before turning right. backingUpCount--; return new Velocities(-TRANSLATIONAL_VELOCITY, 0.0); } else { turningRightCount--; return new Velocities(0.0, -Math.PI / 2); } }

When Algernon turns left, he first goes forward a bit so that his back is clear of the wall ending to his left. Then he rotates left. Finally, he needs to go forward a bit more so a wall will be on his left again. Listing 9 is the TurnLeft code:

Listing 9. Behavior code for turning left

public boolean isActive() { if (postGoingForwardCount > 0) { return true; } RangeSensorBelt sonars = getSensors().getSonars(); // Check the sonar on the left. if (sonars.getMeasurement(1) > 1.0) { // There is a passageway to the left. preGoingForwardCount = 20; postGoingForwardCount = 40; turnLeftCount = getRotationCount(); return true; } else { return false; } } public Velocities act() { if (preGoingForwardCount > 0) { preGoingForwardCount--; return new Velocities(TRANSLATIONAL_VELOCITY, 0.0); } else if (turnLeftCount > 0) { turnLeftCount--; return new Velocities(0.0, Math.PI / 2); } else { postGoingForwardCount--; return new Velocities(TRANSLATIONAL_VELOCITY, 0.0); } }

Listing 10 is the ReachGoal code:

Listing 10. Behavior code for reaching the goal

public boolean isActive() { RangeSensorBelt sonars = getSensors().getSonars(); // Is there open space all around us? That is, are we out of the maze? double clearDistance = 1.2; return sonars.getMeasurement(0) > clearDistance && sonars.getMeasurement(1) > clearDistance && sonars.getMeasurement(3) > clearDistance && sonars.getMeasurement(2) > clearDistance; } public Velocities act() { // Stop return new Velocities(0.0, 0.0); }

Listing 11 is the main behavior code for Algernon:

Listing 11. Algernon's behavior control code

private void initBehaviorBasedAgent( algernon.subsumption.BehaviorBasedAgent behaviorBasedAgent) { algernon.subsumption.Sensors sensors = behaviorBasedAgent.getSensors(); algernon.subsumption.Behavior[] behaviors = { new ReachGoal(sensors), new TurnLeft(sensors), new TurnRight(sensors), new GoStraightAlways(sensors) }; boolean subsumes[][] = { { false, true, true, true }, { false, false, true, true }, { false, false, false, true }, { false, false, false, false } }; behaviorBasedAgent.initBehaviors(behaviors, subsumes); }

Figure 1 shows Algernon navigating a maze:

Figure 1. Algernon running a maze

Note that your robot can solve a maze even though its component parts know nothing about mazes (or even walls). Algernon has no central brain that calculates a way out of the maze. This is what you get out of a subsumption architecture: complex, seemingly purposeful behavior out of a set of simple, layered behaviors.

Conclusion

The Roomba As I type this, a Roomba is vacuuming the rug under my feet (while unknowingly being stalked by a kitten). The Roomba is developed by iRobot, a company founded by three MIT alumni: Rodney Brooks, Colin Angle, and Helen Greiner. The Roomba is built using a subsumption architecture and has an open interface through which you can create all sorts of interesting hacks. Tod E. Kurt's book Hacking Roomba shares many interesting hacks (see Related topics).

In this article, you programmed simulated robots. Programming real physical robots is a lot harder. The physical world intrudes in all sorts of messy ways. For example, it was quite easy to make your wall-following robot move parallel to the wall on its left. In the real world with its imperfect surfaces, getting your robot not to veer into the wall on its left or veer too far away from that wall is quite a challenge in itself. If you enjoy programming, that doesn't mean you'll necessarily enjoy making robots. Making interesting robots probably requires more mechanical skills than it does programming skills.

If you are interested in building and programming your own robots, LEGO Mindstorms is an excellent robotics kit. A low-cost alternative to working with Mindstorms is building Biological Electronic Aesthetics Mechanics (BEAM) robots. BEAM takes the idea of behavior-based robotics one step further and eliminates programming altogether. The overall behavior comes from hardwired, analog reflex-response behaviors. For $30 or less, you could build your first robot with a BEAM kit or through plans you can find in books, such as Gareth Branwyn's Absolute Beginner's Guide to Building Robots (see Related topics). Or you could buy a Roomba and hack it.

One of the striking things I noticed after programming robots and looking at others' robot code for only a short time is that you can get a robot to do quite a lot with only a small amount of code. (However, it might take a lot of tinkering and experimenting with constants to get that small amount of code exactly right.) With the LEGO Mindstorms kit, you can build your first simple robot from instructions in an afternoon.

A whole thriving amateur-robotics subculture is out there to explore: robot books, robot contests, robot videos, and probably a robotics club in your area.

Downloadable resources

Related topics