3 Branches

So far we have concentrated mainly on sequential programs with a single pathway through them, where the flow of control proceeds through the program statements in linear sequence, except when it encounters a loop element. If a loop is encountered, then the control flow is redirected back ‘up’ the program to the start of a loop block.

However, in several of the notebooks, you have also seen how a conditional if... statement can be used to optionally pass control to a set of instructions in the sequential program if a particular condition is met.

The if... statement fits the sequential program model by redirecting control flow, albeit briefly, to a set of ‘extra’ commands if the conditional test evaluates to true.

A sequential program will always follow the same sequentially ordered path. But to be useful, a robot program will often need to make decisions and behave differently in different circumstances. To do this, the program has to have alternative branches in the program flow where it can follow different courses of action depending on some conditional test.

Although the while command does appear to offer some sort of branch-like behaviour, we still think of it as a sequential-style operator because the flow of control keeps trying to move in the same forwards direction.

In other programming languages, this construct may be referred to as an if...then statement. In Python, the colon following the test condition(s) can be read as ‘then’, or the ‘then’ can just be assumed.

We’ll be trying out some conditional statements using nbtutor to step through some simple programs as we execute them, so let’s load it in now:

%load_ext nbtutor

3.1 An if... without an else...

It is sometimes useful to have just a single branch to the if statement. Python provides a simple if... statement for this purpose, which you may recall from previous notebooks.

3.1.1 Activity – An if on its own

Run the following code cell as it stands, with the x variable taking the initial value 1 (x = 1). Can you predict what will happen?

#%%nbtutor --reset --force
x = 1

print("Are you ready?")

if x == 1:
    print("x equals 1")

print("All done...")

Try to predict what will happen if you change the initial value and run the cell again.

Double-click this cell to edit it and record your own prediction.

Run the code cell above again with a modified initial value. Was your prediction about what would happen correct? Make a note in the cell below about how successful your prediction was. If your prediction was incorrect, try to explain why you think your prediction was different to how the program actually behaved.

Double-click this cell to edit it to record notes on the success or otherwise of your own prediction.

Uncomment the %%nbtutor magic and run the code cell using different values of x, observing how the program flow progresses in each case.

Discussion

Click on the arrow in the sidebar or run this cell to reveal my observations.

With the initial value of the variable x set to 1 (x = 1) the program displayed the messages Are you ready?, x equals 1 and All done… because the if... statement evaluated the x == 1 test condition as True and passed control into the if... block.

When x was initialised to a different value, for example as x = 2, only the messages Are you ready? and All done… were displayed because the if... conditional test failed and redirected control flow to the first statement after the if... block.

3.2 A single branch – if...else...

On its own, an if statement will test a condition and, if the condition evaluates as True will then evaluate the code block within the if statement, otherwise it passes control to the next program statement.

If we wanted to choose alternative actions depending on the evaluation of a particular condition, we could create multiple if statements, one to handle each outcome:

if raining==True:
    print("Take your coat")
if raining==False:
    print("Looks like a nice day.")

Alternatively, we can use an if...else statement to take one path if the condition evaluates as True, otherwise (else) perform the alternative action:

if raining==True:
    print("Take your coat")
else:
    print("Looks like a nice day.")

In the branching if...else... operator, the program control flow takes one of two different ‘forward-flowing’ paths depending on whether the conditional statement evaluated as part of the if... statement evaluates to true or false. If it evaluates to True, then the statements in the first if block of code are evaluated; if the condition evaluates to False, then the statements in the else block are evaluated. In both cases, control then flows forwards to the next statement after the if...else... block.

3.2.1 Activity – Stepping through an if...else... statement

In this activity we will look at a simple branching program to explore how if...else... works in more detail.

If you were to run the following code in a code cell, what do you think will happen?

x = 1

if x == 1:
    print("x equals 1")
else:
    print("x does not equal 1")
    
print("All done...")

Double-click this cell to edit it and make your prediction here.

Once you have made your prediction, run the following cell. In the cell beneath it, record what happened and how it compared to your prediction.

You may find it informative to use nbtutor to step through each line of code in turn to see how the program flow progresses. To do this, uncomment the %%nbtutor magic in the first line of the code cell by deleting the # at the start of the line before running the code cell.

#%%nbtutor --reset --force
x = 1

if x == 1:
    print("x equals 1")
else:
    print("x does not equal 1")
    
print("All done...")

Double-click this cell to edit it to record what happened when you ran the code in the above cell. Did its behaviour match your prediction?

What do you think will happen when you run the following code cell?

Run the cell and use nbtutor to step through the program. How does the program flow differ from the case where x had the value 1?

%%nbtutor --reset --force
x = 2

if x == 1:
    print("x equals 1")
else:
    print("x does not equal 1")

print("All done...")

Discussion

Click the arrow in the sidebar or run this cell to reveal my observations.

In the cell where x = 1, I predicted that the program would print the message ‘x equals 1’ and then the message ‘All done…’.

Viewing the trace, I could see how the program started by initialising the x variable to the value 1, then checked whether x == 1 (that is, whether x was equal to 1); because it was, the program then moved onto the print("x equals 1") statement and printed the first message. Then the program flow continued to the first instruction after the if...else... block, which was the statement that printed the ‘All done…’ message.

When I ran the program with a value of x other then 1, the control passed from the if... statement, where the conditional test evaluated as False, to the first line in the else... block, which printed the message ‘x does not equal 1’, before moving onto the first line after the if...else... block as before.

3.3 Combining loops and branching statements

It is important to be clear that the condition in a branching statement (if... or if...else...) is checked only when execution reaches that part of the program.

In the examples above, you stepped through the programs and saw that execution passed through the if statement only once. When creating useful robot programs, we often want conditions to be checked repeatedly. For example the robot may need to repeatedly check that it has not bumped into an obstacle, or whether it has found a bright or dark area.

You have already seen how the while... loop tests a condition at the start of a loop and then passes control to the statements inside the loop before looping back to test the while... condition again.

3.3.1 Using conditional statements inside a loop

One commonly used robot programming design pattern is to embed conditional statements with an outer infinite control loop:

# Loop forever
while True:
    
    if condition:
        do_this()
    else:
        do_that()

    if another_condition:
        do_something_else()

You may also recall from an earlier notebook that we also used an if... statement to return the control flow back to the top of a loop before all the statements in the loop body had been executed, or break out of a loop early and pass control to the first statement after the loop block.

This ability to combine loop and branching statements is very powerful and even a very simple program can produce quite complex robot behaviour.

3.3.2 Nested if statements

Sometimes you may want to develop quite a complicated reasoning path.

In some cases, testing a compounded logical statement using Boolean logic operators may suit our purposes, but we are still limited by the fact that the conditional test must return a single True or False value:

weather = 'rain'
temperature = 'cold'

if (weather=='rain') or (temperature=='cold'):
    print("Wear a coat")

if (weather=='rain') and (temperature=='cold'):
    print("...and maybe a scarf too...")

In other cases, we may need to make use of a so-called nested if statement, where we build up a ladder of if statements, one inside the other.

For example, with the specified weather conditions, what does the program recommend you do?

weather = 'rain'
temperature = 'warm'
windy = False

if temperature=='warm':
    print("It's warm today...")
    
    if weather=="rain" and not windy:
        print("...but take a brolly")

    if windy:
        print("...and windy enough to fly a kite.")

What does it suggest if you change windy = False to windy = True?

Other more elaborate variants of compounded or nested branching statements are supported in other languages, for example in the form of case or switch statements that can select from multiple courses of action based on the element that is evaluated at the start of the statement.

3.4 Multiple conditions using if...elif...else...

The if...else... statement allows us to create a branching control flow statement that performs a conditional test and then chooses between two alternative outcomes depending on the result of the test.

Python also supports a yet more complex branch construction in the form of an if...elif...else... statement that allows us to make multiple conditional tests. Run the following code cell and use nbtutor to explore the flow through the program.

%%nbtutor --reset --force

days_of_week = ['Monday', 'Tuesday', 'Wednesday', 'Thursday',
                'Friday', 'Saturday', 'Sunday']

for day in days_of_week:
    print(f'Today is {day}...')
    
    if day == 'Wednesday':
        print('...half day closing')
    elif day in ['Saturday', 'Sunday']:
        print('...the weekend')
    else:
        print('...a weekday')
        
print("And that's all the days of the week.")

We can also have multiple elif statements between the opening if... and the closing else.

Read through the code in the following code cell. Try to work out what the program will do and how the control flow will pass through the program as it executes before you run the cell and step through the code using nbtutor.

%%nbtutor --reset --force

days_of_week = ['Monday', 'Tuesday', 'Wednesday', 'Thursday',
                'Friday', 'Saturday', 'Sunday']

for day in days_of_week:
    message = f'Today is {day}...'
    print(message)
    
    if day == 'Monday':
        feeling = "...I don't like Mondays..."
    elif day == 'Tuesday':
        feeling = '...Ruby Tuesday'
    elif day == 'Friday':
        feeling = "...Friday I'm In Love"
    else:
        feeling = "...I don't know a song title for that day"
    
    print(feeling)

Run the previous code cell and step through its execution using nbtutor; observe how the control flow steps increasingly through the stack of ...elif... tests as the for... loop iterates through the items in the days_of_week list.

Note that there is no requirement that you test the same variable in each step. The different steps could test a different variable or range of variables.

For example, in the following program we might decide what to take out with us on a walk based on a variety of conditions:

raining = False
temperature = 'warm'

if raining:
    print("Wear boots")
elif temperature == 'warm':
    print("Wear sandals")
else:
    print("Wear shoes")

Also note that there is an order in which we test the various conditions as the control passes through the if...elif... conditional tests. We can use this as an informal way of prioritising one behaviour over another:

if this_really_important_thing:
    ...
elif this_less_important_thing:
    ...
elif this minor_thing:
    ...
else:
    ...

3.5 Using control flow statements in robot programs

Branching operators, when combined with loop-based control flow operators, mean we can construct a wide range of control strategies and behaviours for a mobile robot. In the following activities, you will see several such approaches.

In order to get started, we need to load in the RoboLab simulator in the normal way:

from nbev3devsim.load_nbev3devwidget import roboSim, eds

%load_ext nbev3devsim

3.5.1 Activity – Detecting black and grey

Using the Grey_and_black background in the simulator, download the program to the simulator and then run it several times with the robot moved to different starting positions.

What does the program cause the robot to do?

%%sim_magic_preloaded -b Grey_and_black -x 400 -y 200

# Start the robot driving forwards
tank_drive.on(SpeedPercent(50), SpeedPercent(50))

# Sample the light sensor reading
sensor_value = colorLeft.reflected_light_intensity_pc

# Check the light sensor reading
while sensor_value == 100:
    # Whilst we are on the white background
    # update the reading
    sensor_value = colorLeft.reflected_light_intensity_pc
    # and optionally display it
    #print(sensor_value)
    # We also (implicitly) keep driving forwards

# When the reading is below 100
# we have started to see something.
# Drive a little way onto the band to get a good reading
tank_drive.on_for_rotations(SpeedPercent(50),
                            SpeedPercent(50), 0.2)

# Check the sensor reading
sensor_value = colorLeft.reflected_light_intensity_pc
# and optionally display it
#print(sensor_value)

# Now make a decision about what we see
if sensor_value < 50:
    say("I see black")
else:
    say("I see grey")

Double-click this cell to edit it and add your description here.

Example solution

Click the arrow in the sidebar or run this cell to reveal an example solution.

The robot moves forward over the white background until it reaches the grey or black area. If the background is black, then the robot says black; otherwise, it says grey.

The program works by driving the robot forwards and continues in that direction while it is over the white background (a reflected light sensor reading of 100). When the light sensor reading goes below the white background value of 100, control passes out of the while loop and on to the statement that drives the robot forwards a short distance further (0.2 wheel rotations) to ensure the sensor is fully over the band. The robot then checks its sensor reading again, and makes a decision about what to say based on the value of the sensor reading.

3.5.2 Activity – Combining loops and branching statements

Can you predict what the following program will cause the robot to do when it is downloaded and run in the simulator using the Loop background with the robot initially placed inside the loop ?

Double-click this cell to edit it and record your prediction.

%%sim_magic_preloaded --background Loop -x 500

tank_drive.on(SpeedPercent(30), SpeedPercent(30))

while True:
    if colorLeft.reflected_light_intensity_pc < 100:
        tank_drive.on_for_rotations(SpeedPercent(-30),
                                    SpeedPercent(-30), 2)

        tank_turn.on_for_rotations(-100, SpeedPercent(75), 2)
        tank_drive.on(SpeedPercent(30), SpeedPercent(30))

Download the program to the simulator and run it there to check your prediction. After a minute or two, stop the program from executing.

How does the behaviour of the program lead to the robot’s emergent behaviour in the simulator?

Example discussion

Click on the arrow in the sidebar or run this cell to reveal an example discussion.

When the program runs, the robot will explore the inside of the black oval, remaining inside it and reversing direction each time it encounters the black line.

The program is constructed from an if statement inside a forever loop. The if statement checks the light sensor reading; when this is low (which it will be when the black line is reached) the motor direction is reversed.

The while True: loop is a so-called infinite loop that will run indefinitely. In this case it is useful because we want the robot to continue to keep on behaving in the same way as the program runs.

In other circumstances, we might want the loop to continue only while some condition holds true. In such cases, using the while statement to test the truth of a conditional statement is more useful.

3.5.3 Challenge – Three shades of grey

An earlier activity provided an example of a program that used an if...else... statement to distinguish between black and grey areas. The background (loaded into the simulator as the Grey_and_black background) actually contains four different shades: black, dark grey, medium grey and light grey. Can you construct a program that will report which shade the robot encounters?

A copy of the original program is provided below as a starting point. You will need to extend the code so that it can decide between three grey alternatives as well as the black band and say which band it saw.

%%sim_magic_preloaded -b Grey_and_black

# Start the robot driving forwards
tank_drive.on(SpeedPercent(50), SpeedPercent(50))

# Sample the light sensor reading
sensor_value = colorLeft.reflected_light_intensity_pc

# Check the light sensor reading
while sensor_value == 100:
    # Whilst we are on the white background
    # update the reading
    sensor_value = colorLeft.reflected_light_intensity_pc
    # and display it
    print(sensor_value)

# When the reading is below 100
# we have started to see something.
# Drive a little way onto the band to get a good reading
tank_drive.on_for_rotations(SpeedPercent(50), SpeedPercent(50), 0.2)

# Check the sensor reading
sensor_value = colorLeft.reflected_light_intensity_pc
# and display it
print(sensor_value)

# Now make a decision about what we see
if sensor_value < 50:
    say("I see black")
else:
    say("I see grey")

When you have modified the code, run the cell to download it to the simulator, ensure the Grey_and_black background is loaded, and then run the program in the simulator for various starting positions of the robot. Does it behave as you intended?

Use this cell to record your own notes and predictions related to this challenge.

Hint: click the arrow in the sidebar or run this cell to reveal a hint.

The original program uses an if...else... condition to distinguish between black and grey reflected light readings. An ...elif... statement lets you test alternative values within the same if...else... block.

To identify the values to use in the condition statements, inspect the simulator output window messages to see what sensor values are reported when the robot goes over different bands.

Example solution

Click the arrow in the sidebar or run this cell to display an example solution.

The robot sees the following values over each of the grey bands:

  • light grey: ~86

  • medium grey: ~82

  • dark grey: ~50

  • black: 0

Generally, when we see lots of decimal places, we assume that the chances of ever seeing exactly the same sequence of numbers may be unlikely, so rather than testing for an exact match, we use one or more threshold tests to see if the number lies within a particular range of values, or is above a certain minimum value.

If we assume those sensor readings are reliable, and the same value is always reported for each of those bands, then we can make the make the following decisions:

if sensor_value > 86: 
    print('light grey')
elif sensor_value > 82: 
    print('medium grey')
elif sensor_value > 50: 
    print('dark grey')
else:
    print('black')

We can make the test even more reliable by setting the threshold test values to values that are halfway between the expected values for a particular band. For example, 84, rather than 82, for distinguishing between light and medium grey; 66 rather than 82 for distinguishing between dark and medium grey; and 25 rather than 50 for distinguishing between black and dark grey.

This means that if there is a slight error in the reading, our thresholded test is likely to make the right decision about which side of the threshold value the (noisy) reading actually falls on.

For example, if we have a value of sensor_value = 86 exactly, the conditional test sensor_value > 86 will evaluate as False because the variable value 86 is not strictly greater than threshold value 86. But if the sensor value is sensor_value = 86.00000000000000000001, the test will evaluate as True.

%%sim_magic_preloaded -b Grey_and_black

# Start the robot driving forwards
tank_drive.on(SpeedPercent(50), SpeedPercent(50))

# Sample the light sensor reading
sensor_value = colorLeft.reflected_light_intensity

# Check the light sensor reading
while sensor_value == 100:
    # Whilst we are on the white background
    # update the reading
    sensor_value = colorLeft.reflected_light_intensity
    # and display it
    print(sensor_value)

# When the reading is below 100
# we have started to see something.
# Drive onto the band to get a good reading
tank_drive.on_for_rotations(SpeedPercent(50), SpeedPercent(50), 0.2)

# Check the sensor reading
sensor_value = colorLeft.reflected_light_intensity
# and display it
print(sensor_value)

# Now make a decision about what we see
if sensor_value >  86: 
    say("I see light grey")
elif sensor_value > 82: 
    say("I see medium grey")
elif sensor_value > 50: 
    say("I see dark grey")
else:
    say("I see black")

Other solutions are possible.

3.6 Noise and variation in real and simulated robots

One thing you might notice is that sometimes the robot may appear to give the ‘wrong answer’ or an answer that diverges from the value you expect, when taking a particular measurement. For example, if the sensor is not completely over a band it will give a reading that does not exactly match a value you used in your conditional tests.

In a real robot, the sensors are also likely to be subject to various forms of environmental noise, such as electrical noise in the sensor, or perturbations in light readings caused by vibrations as the robot moves and slightly changes the height of the reflected sensor above the floor. Shadows and variations in illumination can also affect light sensor readings.

If you modify the position of the Light sensor noise slider in the simulator, you can see how the sensor readings are perturbed according to the amount of noise added.

In the idealised world of a robot simulator, it may at first seem as if we don’t have to cope with the messiness of physical world noise if we don’t want to. But even in a simulator, we find there are issues relating to precision in the way numbers are represented. For example, even if we think we have set a variable to a specific value, it may not actually be represented as that value at the machine level, as the following example shows:

point_one = 0.1

# The `format()` function lets us control
# the output display of a variable
# In the following case, we can display 
# the represented value 0.1 to 20 significant digits
format(point_one, '.20g')

You will learn a little bit more about noise, and how to deal with it, later in the module.

3.7 Summary

In this notebook, you have seen how if... statements can be used to make a variety of decisions and trigger a range of different actions based on one or more tested conditions. In particular:

  • a simple if... statement lets us perform one or more actions once and once only if a single conditional test evaluates to true

  • an if...else... statement allows us to branch between two possible futures based on the whether a single conditional test evaluates to true; if it is true, do one action, if not, do the other

  • an if...elif...else... construction lets us run multiple different conditional tests. If the first test is true, do one thing, otherwise test the next thing, and if that is true, do something, otherwise, do another test, and so on. If all the other elif... tests evaluate to false, then do the final else condition.

In the next notebook, you will have an opportunity to explore a few more ways of using control flow statements in the context of a robot control program.