2 Program control flow using a while... loop

In the previous notebook you learned how a for...in... loop could be used to iterate through a list of items, with a conditional test in the body of the loop providing an opportunity to either foreshorten code execution within the loop’s code block using a continue command, or break out of the loop using a break command.

In this notebook, you will learn how a while command combines loop control with its own conditional test.

2.1 The while loop

A while... loop tests the truth of a statement on each iteration of the loop. In ‘pseudo-code’, the behaviour can be described as:


do the following sequence of instructions while a condition holds 

 { 

 instruction 1 

 instruction 2 

 instruction 3 

 instruction 4 

 instruction 5 

 etc. 

 } 

In some respects the while resembles an if statement in that if (while) the tested condition is true, then control passes into the body of the while statement block, and if it evaluates as false, then control passes immediately to the next statement after the while block. However, unlike the if statement, when all the statements inside the while block have been executed, in sequential order, control does not then flow to the next statement after the while block: it passes back to the top of the while loop and the condition is re-evaluated.

In this way, the program can keep repeating the lines of code inside the while block until some condition is met, or some condition fails.

Note that if the conditionally tested value changes to a value that would cause the condition to evaluate as false whilst the program flow is inside the while block, the statements inside the while block will continue to execute in sequential order. Control only passes from the while statement to the statement after the while block at the point when control passes to the while statement and its condition is tested and found to evaluate as false.

Let’s see an example of how to use a while loop to help us keep track of whether we have counted up to a particular number yet. We’ll use nbtutor to illustrate what’s going on as the loop executes, so let’s load that magic in:

%reload_ext nbtutor

The following lines of code are rendered using language-sensitive code styling within this Markdown cell:

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

while counter < 5:
    print(counter)
    counter = counter + 1
    
# Display the final value of counter
counter

Although it looks like code, we can’t execute it because it isn’t in a code cell.

Copy the code and paste it into the empty code cell below (or create your own new code cell) and run the cell and observe what happens.

Then uncomment the %%nbtutorcell magic by deleting the # symbol at the start of the first line and run it again to step through the code as it executes a line at a time.

Observe how the program flow repeatedly moves from the last line of the code inside the while block back up to the while statement, before going from the while statement to the final counter statement when the conditional test eventually evaluates as false.

# Paste a copy of the code here

Typically, a package only needs to be imported once into a notebook even if we call it from multiple calls. However, if we run a cell run using the %%nbtutor --reset --force magic, it creates a fresh Python environment with no previously set variables or imported packages for its code execution. In such a case, we would need to import the package again into that specific cell.

2.1.1 Activity – Generating several random numbers using a while loop

The Python random function from the random package is capable of generating a random number between 0 and 1, as you will see if you run the following cell repeatedly.

import random

random.random()

We can think of this as a coin toss, where we toss ‘heads’ for values greater than or equal to 0.5, or ‘tails’ for values less than 0.5.

Write a simple while loop that tests a simulated coin toss for as long as it tosses the equivalent of ‘heads’, printing ‘heads’ for each successful toss.

Run the code cell several times to see what happens.

# Add your code here

Example solution

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

To test the coin toss, we are looking for a random value of greater than or equal to 0.5, that is, a value of random.random() >= 0.5.

We can use this as a conditional test in a while loop. In the body of the loop, we print ‘heads’ to show we are in the loop.

while random.random() >= 0.5:
    print("heads")

If we run the cell multiple times, then sometimes nothing is printed (if the ‘coin’ flips as ‘tails’, that is, the first random value is less than 0.5); at other times, we may get one or more ‘heads’ displayed. (The most I saw in several attempts was eight heads in a row!)

2.2 Infinite loops

We can create a special sort of loop known as an infinite loop using the while True: construction, where the statement True always evaluates as True and so the loop repeats until the program is forced to stop or the flow is forced out of the loop and onto the next instruction using a break statement.

The following code cell demonstrates how to escape an otherwise infinite loop by using a break statement. Run the cell to see how it works. Uncomment the nbtutor magic to step through it a line at a time as it executes.

#%%nbtutor --reset --force
counter = 0

while True:
    counter = counter + 1
    print(counter)
    if counter == 5:
        break
        
print(f"We escaped at counter=={counter}")

In the expression counter = counter + 1 we explicitly set the new value of counter on the left-hand side of the expression to be equal to the current value of counter incremented by 1.

We could also use the equivalent expression counter += 1, which reads as ‘add the value on the right-hand side of the expression to the numerical variable on the left-hand side’.

2.3 Using a while loop in the simulator

Being able to loop whilst a particular condition holds allows us to perform actions until that condition no longer holds.

This may be particularly useful in a robot programming context, as the following simple example demonstrates.

To start with, load the simulator into the notebook:

from nbev3devsim.load_nbev3devwidget import roboSim, eds
%load_ext nbev3devsim

In the simulator reset the trace and select the Grey_bands background. (You can also disable the Pen Down control: we don’t need to keep track of where the robot has travelled for this activity.)

Now run the following code cell to download the program to the simulator and then run it in the simulator, observing the behaviour of the robot. (If you have not already selected the Grey_bands background, it should be automatically loaded by the magic invocation.)

%%sim_magic_preloaded -b Grey_bands

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

# For the sensor value, use the
# reflected light intensity as a percentage
sensor_value = colorLeft.reflected_light_intensity_pc

# Test the sensor value; loop while the
# sensor value exceeds 85
while sensor_value > 85:
    # Display the sensor value in the simulator output window
    print(sensor_value)
    # Update the sensor_value variable
    # to the latest sensor reading
    sensor_value = colorLeft.reflected_light_intensity_pc

# The sensor value is now not greater than 85 
# so print a final message...
print("I now see {}".format( sensor_value))

# And explicitly turn the motors off 
# to stop the simulated robot
tank_drive.off()

Record your observations here about what the robot does when you run the program.

When you run the program in the simulator, the robot should drive forwards until it encounters the second grey line, and then it should stop.

By default, the robot will stop when the program ends, because the simulator run stops at the end of the program. However, it is good practice to explicitly turn the motors off yourself. By doing this, you know for sure what state the motors are in at the end of the program. In the above example, what would happen if, for some reason, the motor off command was omitted and the simulator carried on running even as the program execution had completed?

The program works by checking the value from one of the robot’s sensors: a downward-facing light sensor, which you will meet in more detail in a later notebook. The sensor returns a ‘reflected light’ reading: a percentage value which relates to the colour of the background over which the robot is travelling. The simulator output display window shows the sensor value, starting at 100 when the robot is on the plain white background. This value is above the conditionally tested threshold value of 85 used in the original program’s while statement, and so the program continues looping round the while loop. When the robot encounters the first grey line, the sensor returns a lower value of just over 94 when I ran the program.

Rather than testing and reporting the colorLeft.reflected_light_intensity_pc value directly, the program is constructed as it is because the sensor value may change in going from the while program step to the print() step. Even though computers may step between lines of code very quickly, they still take a finite time to do so.

Try modifying the numerical value used in the while conditional test and downloading and running the modified program. Can you get the robot to stop as soon as it encounters the second medium grey band? How about on the third, dark grey line, or on the final, black line?

2.4 Blocking program control flow

One of the features of a sequential program is that control is passed in turn from one line of code to the next. Control passes when a particular line has finished executing. Typically, a line of code is evaluated in a just a few milliseconds (that is, thousandths of second), or even quicker. But some commands may take some considerable time before they complete their execution, and hold up the program’s execution until they have completed. Such statements are said to be blocking: they block the continued control flow for a non-trivial amount of time.

You have already seen this in the form of the time.sleep() command. This delays the passage of control flow until the sleep period (in seconds) has elapsed. However, any things that are ‘free-running’, such as switched-on motors driving forwards or backwards, continue to run even as the blocking statements hold up progress of the program control flow.

Let’s explore this in a slightly different context: what do we do when the robot is speaking?

2.4.1 Activity – Counting up to 10

As well as programming the simulated robot to respond to a sensor value, we can also get it to count aloud.

The following program, for example, when downloaded to the simulator, will cause the simulated robot to count aloud as well as printing the count value in the simulator Output display.

Can you get the robot to count to 10, rather than 5?

Also observe how the spoken count and the printed count values are displayed. Are they in synchronisation with each other?

%%sim_magic_preloaded --autorun -WO

say("Listen to me count")

count = 1

while count < 5:
    say( count)
    print( count )
    count = count + 1

Record your observations here about when the spoken and printed counts occur relative to each other. How might you explain this behaviour?

2.4.2 Making the program wait

When you ran the previous program, you should have noticed that the printed count values appeared almost at the same time as each other, whereas the spoken count values were clearly spoken one after the other.

By default, the preloaded say() command is non-blocking: phrases to be spoken are added to a ‘first in, first out’(FIFO) queue managed using a parallel process running outside the simulator as part of your browser’s own control flow. Essentially, the say() command asks the browser to handle the spoken statement, and once it has made that request of the browser, it considers itself to have completed. At that point, it can pass control onto the next statement in the downloaded simulator program, even if the browser has not completed the actual speaking task.

However, we can also get the say() function to operate in a blocking mode by passing the wait=True parameter to it. In this case, the say() command does not consider itself to have successfully completed until the provided phrase has been spoken. This means that it will not pass control to the next line of the downloaded program until the browser informs it that the phrase has been spoken.

Run the following code cell to see this behaviour in action:

%%sim_magic_preloaded --autorun -WO

say("Listen to me count")

count = 1

while count < 5:
    # The say() command by default will also print
    # the message to the simulator Output display
    say( count, wait=True)
    count = count + 1

Record your observations here about when the spoken and printed count values appear relative to each other this time. How does this behaviour compare to previously? How do you explain the behaviour in each case?

2.4.3 Blocking behaviour in robot control programs

If you turn the simulated robot’s motors on, they continue running until either they are switched off by another statement in the program, or by the program completing.

For example, as before, the following program will drive the robot forward until it sees the second grey line, at which point it will stop:

%%sim_magic_preloaded -b Grey_bands --autorun

tank_drive.on(SpeedPercent(50), SpeedPercent(50))

sensor_value = colorLeft.reflected_light_intensity_pc

while sensor_value > 85:
    sensor_value = colorLeft.reflected_light_intensity_pc

But what happens if we start the robot driving forwards and then using a blocking say(wait=True) command at the start of the program?

%%sim_magic_preloaded -b Grey_bands --autorun

tank_drive.on(SpeedPercent(50), SpeedPercent(50))

say("""My motors are now running,
       so let's get this party started.
       Here we go...""",
     wait=True)

sensor_value = colorLeft.reflected_light_intensity_pc

while sensor_value > 85:
    sensor_value = colorLeft.reflected_light_intensity_pc

Record your observations about what the robot does this time. Can you explain its behaviour?

Some of the robot’s own motor operations are also blocking.

For example:

  • the .on_for_rotations() command starts the motor and is then blocking until the motor has turned for the specified number of rotations, at which point the motor is switched off and control passes to the next statement of the program

  • the .on_for_seconds() command starts the motor and is then blocking until a specified amount of time has elapsed, at which point the motor is switched off and control passes to the next statement of the program.

Using a blocking motor command can therefore provide unwanted behaviour.

For example, let’s run the motors for a bit before we pass control to the loop that should stop the robot at the third grey line:

%%sim_magic_preloaded -b Grey_bands --autorun

tank_drive.on_for_rotations(SpeedPercent(50),
                            SpeedPercent(50),
                            10)

# The show=True parameter will also print the spoken message
say("""Start looping...""")

sensor_value = colorLeft.reflected_light_intensity_pc

while sensor_value > 85:
    sensor_value = colorLeft.reflected_light_intensity_pc

Record your observations here describing what the robot does when the program is downloaded. How do you explain its behaviour?

2.5 Travelling a specific distance

The blocking nature of some of the motor commands presents us with a challenge that might at first feel insurmountable: if we want the robot to drive forwards for a specified distance using the .on_for_X() command to turn the motors on for a specific time or number of rotations, then we can’t do anything else during that time, such as collect sensor data, because the motor command will be blocking the flow of program control.

However, the motor drive commands do provide us with .left_motor.position and .right_motor.position tacho (tachometer) values that describe how far each wheel has turned.

Using a while loop, we could set up a program to drive the robot forwards in a straight line, perhaps collecting some sensor data as it does so, until a particular distance, as tested by a condition in the while loop, has been traversed.

For example, the following program will drive the robot forwards until the left motor tacho count reaches a value of 1000:

%%sim_magic_preloaded -b Empty_Map --autorun -ORH

from time import sleep

# Turn the motors on
tank_drive.on(SpeedPercent(50), SpeedPercent(50))

while int(tank_drive.left_motor.position) < 1000:
    print("Left motor value:", tank_drive.left_motor.position)
    # We need some delay in the loop
    # or the program will hang
    sleep(0.1)
    
say("All done")

2.6 Summary

In this notebook, you have seen how we can control the way in which program statements are executed in a program using a while loop, which checks a condition at the start of each loop and then either passes control to the first line inside the block if the condition evaluates as True, with control returning to the top of the loop, and another conditional test, once the code inside the loop has been evaluated. When the loop’s conditional test fails (that is, it evaluates as False) control is passed to the next statement in the program outside the while block.

As with the for...in.. loop, control flow within a while loop can also be interrupted using continue and break statements.

You also learned how certain statements are ‘blocking’ of control flow, in that rather than being evaluated in a near-instantaneous fashion, they may take some time to complete before passing control to the next program statement. Blocking commands include time.sleep(s), say(message, wait=True) and the motor commands .on_for_rotations() and .on_for_seconds().

Using blocking statements within a robot control program can lead to behaviours that are not desired, not least because they may prevent the timely inspection of sensor values that are used to determine robot behaviour.

In the next notebook, we’ll look in a bit more detail at one of the control flow statements you have already met in passing: the conditional if branch control flow statement.