1 Introduction to functions and robot control strategies
Contents
1 Introduction to functions and robot control strategies¶
Sensors are at the heart of robotics. A machine without sensors cannot be a robot in our terms. The human body is replete with sensors. Our five external senses – sight, hearing, touch, smell and taste – and internal sensing such as balance and proprioception (body awareness) are all marvellously sophisticated.
For this week’s practical activities, we will be concerned with various techniques that can be used to allow a robot to use sensory information to control its actuators. We will investigate a progression of control strategies:
dead reckoning – no sensor input
reflex behaviour – sensors linked directly to motors according to the sense–act model
deliberative behaviour – actuation depends on reasoning about sensor information and other knowledge, according to the sense–think–act model.
The first control strategy, dead reckoning, is an ‘open-loop’ control approach, since it does not use sensor input.
The second is an example of a ‘sense–act’ control strategy that you encountered earlier in the block; we will illustrate this control strategy using simulated implementations of simple Braitenberg vehicles.
Finally, there is the most complex control strategy, in which the robot deliberates on the sensor inputs in the context of other knowledge using an approach we refer to as ‘sense–think–act’. This involves reasoning and corresponds more closely to the way humans solve complex problems and plan actions in the long and short term.
But before we do that, we’ll have a look in a bit more detail at another powerful idea in computer programming: functions. You’ve already met some of these, but without much explanation. So now let’s introduce you to them for real.
1.1 Defining simple Python functions¶
Many of the programs we have used so far have been quite short with little, if any, reused code.
As programs get larger, it is often convenient to encapsulate several lines of code within a function. Multiple lines of code within a function can then be called conveniently from a single statement whenever they are needed.
Functions are very powerful, and if you have studied other programming courses then you may well be familiar with them.
For our purposes, the following provides a very quick overview of some of the key behaviours of Python functions. Remember that this isn’t a Python programming module per se; rather, it’s a module where we explore how to use Python to get things done. What follows should be enough to get you started writing your own functions, without creating too many bad habits along the way.
To see how we can create our own functions, let’s consider a really simple example: a function that just prints out the word Hello.
The function definition has a very specific syntax:
def FUNCTION_NAME():
# ONE_OR_MORE_LINES_OF_CODE
pass
A Python function requires at least one line of valid code (which does not include comments) in the function body. If we don’t know what lines of code we want just yet, the pass
command is enough to create a valid program line that doesn’t actually have to do anything.
Here are some of the rules relating to the syntactic definition of a Python function:
the
FUNCTION_NAME
MUST NOT contain any spaces or punctuation other than underscore (_
) charactersthe function name MUST be followed by a pair of brackets (
()
), that may contain something (we’ll see what later), followed by a colon (:
)the body of the function MUST be indented using space or tab characters; the level of indentation of the first line sets the effective ‘left-hand margin’ for the remaining lines of code in the function
the body of the function must include AT LEAST one valid statement or line of code EXCLUDING comments; if you don’t want the function to do anything, but need it as a placeholder, use
pass
as the single line of required code in the function body.
It is good practice to annotate your function with a so-called ‘docstring’ (documentation string) providing a concise, imperative description of what the function does.
def FUNCTION_NAME():
""""Docstring containing a concise summary of the function behaviour."""
# ONE_OR_MORE_LINES_OF_CODE
pass
Run the following code cell to define a simple function that prints the message Hello:
def sayHello():
"""Print a hello message."""
print('Hello')
When we call the function, the code contained within the function body is executed.
Run the following cell to call the function:
sayHello()
Functions can contain multiple lines of code, which means they can provide a convenient way of calling multiple lines of code from a single line of code.
1.2 Passing arguments into functions¶
Functions can also be used to perform actions over one or more arguments passed into the function. For example, if you want to say hello to a specific person by name, we can pass their name into the function as an argument, and then use that argument within the body of the function.
We’ll use a Python f-string as a convenient way of passing the variable value, by reference, into a string:
def sayHelloName(name):
"""Print a welcome message."""
print(f"Hello, {name}")
Let’s call that function to see how it behaves:
sayHelloName("Sam")
What happens if we forget to provide a name?
sayHelloName()
Oops… We have defined the argument as a positional argument that is REQUIRED if the function is to be called without raising an error.
If we want to make the argument optional then we need to provide a default value:
def sayHelloName(name='there'):
"""Print a message to welcome someone by name."""
print(f"Hello, {name}")
sayHelloName()
If we want to have different behaviours depending on whether a value is passed for the name, then we can set a default such as None
and then use a conditional statement to determine what to do based on the value that is presented:
def sayHelloName(name=None):
"""Print a message to welcome someone optionally by name."""
if name:
print(f"Hello, {name}")
else:
print("Hi there!")
sayHelloName()
Sometimes, we may want to get one or more values returned back from a function. We can do that using the return
statement. The return
statement essentially does two things when it is called: firstly, it terminates the function’s execution at that point; secondly, it optionally returns a value to the part of the program that called the function.
1.2.1 Activity – Defining a simple function¶
Run the following code cell to define a function that constructs a welcome message, displays the message and returns it:
def sayAndReturnHelloName(name):
"""Print a welcome message and return it."""
message = f"Hello, {name}"
print("Printing:", message)
return message
What do you think will happen when we call the function?
Write your prediction here about what you think will happen when the function is run here before you run the code cell to call it.
sayAndReturnHelloName('Sam')
Run the above cell to call the function. Did you get the response you expected?
Discussion¶
Click on the arrow in the sidebar or run this cell to reveal my observations.
In the first case, a message was printed out in the cell’s print area. In the second case, the message was returned as the value returned by the function. As the function appeared on the last line of the code cell, its value was displayed as the cell output.
1.3 Setting variables to values returned from a function¶
As you might expect, we can set a variable to the value returned from a function:
message = sayAndReturnHelloName('Sam')
If we view the value of that variable by running the following cell, what do you think you will see? Will the message be printed as well as displayed?
Write your prediction about what you think will happen when the function is run here before you run the code cell to call it.
message
Only the value returned from the function is displayed. The function is not called again, and so there is no instruction to print the message.
To return multiple values, we still use a single return
statement:
def sayAndReturnHelloName(name):
"""Print a welcome message and return it."""
message = f"Hello, {name}"
print("Printing:", message)
return (name, message)
sayAndReturnHelloName('Sam')
Finally, we can have multiple return statements in a function, but only one of them can be called from a single invocation of the function:
def sayHelloName(name=None):
"""Print a message to welcome someone optionally by name."""
if name:
print(f"Hello, {name}")
return (name, message)
else:
print("Hi there!")
return
print(sayHelloName(), 'and', sayHelloName("Sam"))
Generally, it is not good practice to return different sorts of object from different parts of the same function. If you try to assign the values returned from the function to a particular variable, that variable could end up being defined in different ways depending on which part of the function returned the value to it.
There is quite a lot more to know about functions, particularly in respect of how variables inside the function relate to variables defined outside the function, a topic referred to as variable scope.
Variables defined within a Python are scoped according to where they are defined. Variables are in scope at a particular part of a program if they can be seen and referred to at that part of the program.
In Python, variables defined outside a function can typically be seen and referred to from within the function. Variables can also be passed into a function via the function’s arguments. But variables defined inside the function cannot be seen outside the function.
We will not consider issues of scope any further here, but it is a very powerful concept and one that any comprehensive introduction to programming should cover.
1.4 Using functions in robot control programs¶
Let’s now consider how we might use our functions in a robot control program.
We’ll start by considering the simple program we explored previously to make the robot trace out a square.
If you recall, our first version of this program explicitly coded each turn and edge movement, and then we used a loop to repeat the same action several times.
To get things set up correctly, load the simulator into the notebook in the usual way:
from nbev3devsim.load_nbev3devwidget import roboSim, eds
%load_ext nbev3devsim
%load_ext nbtutor
Tweak the constant value settings in the program below until the robot approximately traces out the shape of a square.
%%sim_magic_preloaded -x 200 -y 500 -a 0 -p -C --pencolor red
SIDES = 4
# Try to draw a square, ish...
STEERING = -100
TURN_ROTATIONS = 1.6
TURN_SPEED = 10
STRAIGHT_SPEED_PC = SpeedPercent(40)
STRAIGHT_ROTATIONS = 4
for side in range(SIDES):
# Go straight
# Set the left and right motors in a forward direction
# and run for the specified number of forward rotations
tank_drive.on_for_rotations(STRAIGHT_SPEED_PC,
STRAIGHT_SPEED_PC,
STRAIGHT_ROTATIONS)
# Turn
# Set the robot to turn on the spot
# and run for a certain number of rotations *of the wheels*
tank_turn.on_for_rotations(STEERING,
SpeedPercent(TURN_SPEED),
TURN_ROTATIONS)
We can extract this code into a function that allows us to draw a square whenever we want. By adding an optional side_length
parameter we can change the side length as required.
Download the following program to the simulator and run it there.
Can you modify the program to draw a third square with a size somewhere between the size of the first two squares?
%%sim_magic_preloaded -p --pencolor green -a 0
SIDES = 4
# Try to draw a square
STEERING = -100
TURN_ROTATIONS = 1.6
TURN_SPEED = 10
STRAIGHT_SPEED_PC = SpeedPercent(40)
STRAIGHT_ROTATIONS = 6
def draw_square(side=STRAIGHT_ROTATIONS):
"""Draw square of specified side length."""
for side_number in range(SIDES):
# Go straight
# Set the left and right motors in a forward direction
# and run for the number of rotations specified by: side
tank_drive.on_for_rotations(STRAIGHT_SPEED_PC,
STRAIGHT_SPEED_PC,
# Use provided side length
side)
#Turn
# Set the robot to turn on the spot
# and run for a certain number of rotations *of the wheels*
tank_turn.on_for_rotations(STEERING,
SpeedPercent(TURN_SPEED),
TURN_ROTATIONS)
# Call the function to draw a small size square
draw_square(4)
# And an even smaller square
draw_square(2)
1.4.1 Optional activity¶
Copy the code used to define the draw_square()
function, and modify it so that it takes a second turn
parameter that replaces the TURN_ROTATIONS
value.
Use the turn
parameter to tune how far the robot turns at each corner.
Then see if you can use a for...in range(N)
loop to call the square-drawing function several times.
Can you further modify the program so that the side length is increased each time the function is called by the loop?
Share your programs in your Cluster group forum.
1.5 Previewing the simulated robot state from a notebook code cell¶
As well as viewing the sensor state via the simulator user interface, we can also review it in the notebook itself.
We can create a reference to an object that uses some magic to grab a snapshot of the state of the robot in the default roboSim
simulator.
robotState = %sim_robot_state
The %sim_robot_state
needs to be run in a cell, before we can check that captured state in another cell.
But once it has run, we can use the robotState
variable to preview the snapshot of the state of the robot.
The state data itself is returned as a Python dictionary which we can reference into to view specific data values. For example, the x
-coordinate:
robotState.state["x"]
Or how about the penDown
state?
robotState.state['penDown']
For a full list of possible values, we can review all the keys associated with the robotState.state
dictionary:
robotState.state.keys()
Let’s use some magic to change the pen down state and then take another snapshot of the robot’s state:
%sim_magic --pendown
robotState = %sim_robot_state
robotState.state['penDown']
1.6 Reporting on robot state in the notebook¶
Having grabbed a snapshot of the robot’s state into the notebook, we can create a function to write reports in the notebook’s own code environment describing the state of the robot.
For example, the robotState.state
dictionary includes the following keys:
left_light_raw / right_light_raw
for the raw RGB valuesleft_light / right_light
for thereflected_light_intensity
valuesleft_light_pc / right_light_pc
for thereflected_light_intensity_pc
valuesleft_light_full / right_light_full
for thefull_reflected_light_intensity
values.
We can create a simple function to display this values to make it easier for us to probe the state of the robot:
def report_robot_left_sensor(state):
"""Print a report of the left light sensor values."""
print(f"""
RGB: {state['left_light_raw']}
Reflected light intensity: {state['left_light']}
Reflected light intensity per cent: {state['left_light_pc']}
Full reflected light intensity (%): {state['left_light_full']}
""")
Let’s see how it works:
report_robot_left_sensor(robotState.state)
By defining our own robot state sensor value reporting function, we have created a tool that we can use to quickly preview the state of the robot (which exists in the simulator) in the notebook environment.
This demonstrates one of the ways in which we can use code: not just as a tool for writing programs, such as the code that defines the robot simulator, or as code for controlling our simulated robot within the simulator – nor even as an automation script for setting up the simulator using cell magics – but as a tool for building tools we can use in our computing environments.
To simplify access to a variant of this function, report_light_sensor(state, side)
in future notebooks, we can import it from the pre-installed custom nn_tools.sensor_data
package and make use of it as follows:
from nn_tools.sensor_data import report_light_sensor
report_light_sensor(robotState.state, 'left')
1.7 Summary¶
You have seen that a Python function is a named sequence of commands that can be ‘called’ or invoked from anywhere in the main program or from another function. Functions offer four advantages:
they allow a piece of program functionality to be ‘encapsulated’ in a clear and detached way
they allow the functionality to be used many times from anywhere in the program
they simplify the program, making it easier to understand
they make programs less prone to error.
Other programming languages have similar features to functions that offer additional benefits. Depending on the language, or the context in which they are defined, these may be known as macros, methods, procedures or subroutines.