Lab 5: Braitenberg Vehicles

Some of Valentino Braitenberg's most well-known work centers on thought experiments with what he calls "vehicles." These vehicles have simplistic controls, yet exhibit apparently complex behavior. Each experiment details a vehicle that has a small set of sensors, and how those sensors can be connected to the vehicle's motors in ways that mirror the neurological connections in living creatures. The resulting vehicles seem to be capable of complex behaviors like fear, aggression, love, free will, etc.(paraphrased from Learning Computing With Robots, edited by Deepak Kumar)

In his work, Braitenberg describes what he calls the Law of Uphill Analysis and Downhill Invention: "It is much more difficult to guess the internal structure of an entity just by observing its behavior than it is to actually create the structure that leads to that behavior." In other words, it is easier (a downhill task) to create one of Braitenberg's vehicles than it is to explain their behavior with no knowledge of their inner workings (uphill analysis).

In this lab, you will program your robot to emulate five of Braitenberg's vehicles, using the light sensors as the "small set of sensors" Braitenberg describes. You will begin with the most basic vehicle, Alive.

Topics:

The main topics you may need in this lab are:

Part I: Alive

Alive has one sensor connected to one motor, so for this section, you will use only the center light sensor, and will feed the same data to both of your robot's motors.

Do This: Copy the skeleton files from ~cs102/robot_labs/Lab_Skeletons/Lab5 to your Lab5 Directory. 

You may have noticed the light sensors are on the "back" of the robot. We want the robot to move towards the light, so you will need to set the "forwardness" of the robot to "scribbler-forward." [The command is robot.setForwardness("scribbler-forward").] Do not forget to reset the forwardness to "fluke-forward" when you are done in preparation for your next lab!

Algorithm Design: Alive

Ask yourself: what needs to happen? Think of the robot as a very stupid minion to which you must give simple, direct commands. The algorithm is the list of those commands. Your program is the way of telling the robot what to do. (This particular minion is unfortunately very stupid, and must be told things very simply.) To accomplish this particular task, the robot must:
  1. Read in sensor data
  2. Move in response to that data.
  3. Repeat steps 1 and 2.

Start at part 3. As you have learned in lectures, we need to use a looping structure of some kind to make our program "repeat" any behavior. For this first vehicle, we will make the vehicle run for 30 seconds.

Myro provides a function called "timeRemaining(int seconds)" that you can use within a while loop to run some behavior for a given ammount of time.

There are several ways to purposefully implement an endless loop. The key to doing so is to deliberately exclude the stopping condition. For example, you might use the code while (number != 3) as a part of your program (you may assume that number is a variable of appropriate type, and that the code contained in the loop has some way of updating the value of number). "number != 3" returns a boolean value based on whether or not number equals 3. If number's value is 25, then "number != 3" evaluates to "true". The loop sees that its condition for continuation is true, and it repeats its behavior. Similarly, if number's value is 3, "number != 3" will be false, and the loop will stop. So, if we write code like this: while (true), the loop will always see its condition as true, and will not stop unless we tell it to. (In this case, we'll use ctrl+c to interrupt the program and to tell the loop to stop - more on that later.)

Do This: Take a moment or so to analyze the algorithm above, and compare it to the code below. Make sure you understand both before you add the code for the vehicle Alive to braitenberg.cpp.

Reminder: You need to connect to your robot before you try to use any functions that begin robot.something - otherwise it will segfault

void alive(){ while (timeRemaining(30)) { int light = robot.getLight("center"); robot.motors(light, light); } }

You can use any method that will move the robot forward in place of motors(). We've used motors() solely because it will make adapting the code used in Alive to code that can be used in the other vehicles easier later. In Alive, one sensor feeds data to one motor. We mimic this by feeding the same data to each motor. Later, in the code for other vehicles, we'll feed different data to each motor.

Before you test anything, there are few more steps. The light sensors read in values from 0 to 5000, where 0 is the brightest and 5000 represents darkness; however, the motors() function can only take values from -1.0 to 1.0. To compensate for this, we will need to normalize the sensor input so that it will make sense to the motors. Take a look at the code below, but do not add it to your program yet:

double normalize(int v){ if (v >= 5000.0) return 0.0; else return 1.0 - v/5000.0; }

This code works in a situation where the robot is in complete darkness, except for the light source it is seeking. To adjust the function so that it will work in a room that is not completely dark, and so that it will respond to a flashlight as its "light source," we need to know the amount of light that is in the room when the robot is turned on. We'll call this our ambient lighting. We'll need a specially-positioned variable to collect this data. (Don't worry about why this works yet - you'll talk about scope, the principle that makes this bit of code possible, in a week or so.)

Do This: Add the code below to the main body of your program - there are comments to guide you.

A Note: Make sure you are not shining the flashlight into any of the sensors when you start your program. This will give you incorrect data, and will cause problems as your program runs.

ambient = robot.getLight("center"); . . . double normalize(int v){ if (v >= ambient) return 0.0; else return 1.0 - v/ambient; }

Do This:Adjust the code you wrote for Alive so that it looks like the code below.

void alive(){ while (timeRemaining(30)) { int light = robot.getLight("center"); robot.motors( normalize(light), normalize(light) ); } }

We divide v, our current light reading, by the ambient light value. We subtract the resulting number from 1.0, because while a lower light value means brighter light, a lower speed means, well, a slower robot. Subtracting v/ambient from 1.0 gives us a speed equivalent for the proportion. For example, if the ambient light reading was 500, and the current light reading was 100, the number 0.8 would be passed to motors(). The vehicle would move forward at .8 of its full speed, 1.0. In the event that the robot reads a darker value than the ambient reading, we would get a negative value. This is why we compare 1.0 - v/ambient and 0.0 and determine which value is greater: by doing this, the robot only goes forward, and follows only the described behavior. (If you are curious, by all means, remove the check and observe the robot's behavior. Be sure to put it back before you continue your work!)

As previously mentioned, in the normalization function, we compare 1.0 - v/ambient and 0.0, and return the greater of the two values. Another way to do this would be simply to return max( 1.0 - v/ambient, 0.0), which accomplishes the same thing.

At this point, you should add a call to Alive to the main() function of your code. Compile your code with the provided makefile, and run your program. Observe your robot's behavior. 

Part II: Coward and Aggressive

Before you begin the next phase of your lab you'll want to modify your main function in braitenburg.cpp to make it easier to control your robot.

Additionally, you'll want to stop your robot from moving when a particular command expires (otherwise "alive" or a similar mode might cause it to drive off). To do this simply call robot.motors(0,0) once you return to the loop control.

We'll now move on to vehicles that receive input from two sensors. Coward and Aggressive both follow light, but Coward moves away from a light source on one side, and Aggressive will move toward it.

For Coward, you will feed the data from the left light sensor to the left-side motor, and the data from the right light sensor to the right-side motor. For Aggressive, you feed the data from the left light sensor to the right-side motor, and the data from the right light sensor to the left-side motor. You can adjust the code from Alive for each of these vehicles.

Do This: Use the descriptions above to add the code for Coward and Aggressive to your program. Don't forget the appropriate function calls to your main method so they are invoked correctly.

At this point, because you are reading in data from more than one sensor, you should change the way you calculate the ambient lighting. You need to adapt your code so that it takes the readings of all three light sensors into account, by finding the average readings from all of the sensors.

Part III: Love and Explorer

Up until now, we've been using an excitatory normalizing function; that is, the darker the light values are, the closer the returned value is to 0. Thus, as the environment gets darker, smaller values are passed to the motors, and the vehicle moves more slowly. Think of it as if the vehicle gets excited as the light gets brighter, and moves forward. For Love and Explorer, the final vehicles, we'll need an inhibitory normalizing function. In this case, the greater the quantity of light reported by the sensors, the slower the motors will turn.

Let's take another look at our first normalization function:

double normalize(int v) { if (v >= ambient) { return 0.0; } else { return 1.0 - v/ambient; } }

The behavior of the inhibitory normalization function will be almost exactly the opposite of the behavior of our first, excitatory function. If the inhibitory function was passed a current light reading of 100, with an ambient reading of 500, it would return 0.1, and the robot would move forward at 0.1 of full speed. In the event that a high number is passed to the function, say, 1000, we have a problem. In that case, the function would return 2.0, unless we add a check similar to the one we added to our first normalize function. The largest number our function should ever return is 1.0.

Do This: Using the description above, as well as what you learned from the first normalization function, add the code for your second normalization function to your program. When you have finished your second normalization function to your satisfaction, add the code for Love and Explorer to braitenberg.cpp and invoke them in the appropriate part of your main function.

Part IV: Menu

Now that you have all these different behaviors in different functions, its time to add a menu to allow the user of your program to both select the behavior, and the amount of time for the behavior to run.  The menu presented should look as follows:
Please select a behavior: 1 - alive 2 - coward 3 - aggressive 4 - love 5 - explorer 0 - exit
Once the user has entered their selection, prompt them for how long they'd like the behavior to last for. How many seconds should this behavior run?
Likely, to do this you'll want to change the functions for each behaviors to take an int paremeter to tell the while loop in the behavior how long it should run. You should have an example program called braitenberg_example in your directory. Your program should run identically to this example.

Now that you are done, be sure to look over all of your code. Check to be sure you have added the appropriate comments and documentation, and that there are function calls for each vehicle's method in your main function. Once you are sure everything is in order, you may turn in your lab.