# IMPORT DATA FILES USED BY THIS NOTEBOOK
import os, requests
file_links = [("data/fifth_republic.txt", "https://ndcbe.github.io/cbe-xx258/data/fifth_republic.txt"),
("data/hats.txt", "https://ndcbe.github.io/cbe-xx258/data/hats.txt")]
# This cell has been added by nbpages. Run this cell to download data files required for this notebook.
for filepath, fileurl in file_links:
stem, filename = os.path.split(filepath)
if stem:
if not os.path.exists(stem):
os.mkdir(stem)
if not os.path.isfile(filepath):
with open(filepath, 'wb') as f:
response = requests.get(fileurl)
f.write(response.content)
Reference: Chapter 3 of Computational Nuclear Engineering and Radiological Science Using Python, R. McClarren (2018)
After studying this notebook, completing the activities, and asking questions in class, you should be able to:
Why use functions? We want to write, debug, and test code once and then reuse as much as possible.
In a few class sessions, we'll formulate mass balances as linear systems and solve them using Python. But for now, let's just consider a problem you would expect to see in math class:
We want to solve the linear system,
$$\mathrm{Eqn.~1}:\quad 4.5 x + 3 y = 10.5\\\mathrm{Eqn.~2}:\quad 1.5 x + 3 y = 7.5.$$One way to do this is using Python as calculator. In the comments below, we walk through the steps.
"""python code to solve
4.5 x + 3 y = 10.5
1.5 x + 3 y = 7.5
by solving the second equation for y first,
and then solving for x"""
#step 1 solve for y, multiply equation 2 by -3,
## and add to first equation
LHS_coefficient = -3*3 + 3 #the coefficient for y
RHS = -3*7.5 + 10.5 #the right-hand side
print('LHS_coefficient:',LHS_coefficient)
print('RHS:',RHS)
Mathematical, we started by multiplying equation 2,
$$1.5 x + 3 y = 7.5$$by -3,
$$(-3) \times 1.5 x + (-3) \times 3 y = (-3) \times 7.5$$and then added this scaled equation 2 to equation 1, giving:
$$(4.5 - 3 \times 1.5) x + (3 - 3 \times 3) y = 10.5 - 3 \times 7.5$$Notice that our choice of scaling equation 2 by -3 means that the coefficient for x becomes zero after addition addition. The coefficient for $y$ is LHS_coefficient
is our code. RHS
is the right hand side of the new equation.
#now divide right-hand side by left-hand side coefficient
y = RHS / LHS_coefficient
#plug y into first equation
x = (10.5 - 3*y)/4.5
#print the solution, note \n produces a linebreak
print("The solution to:\n4.5 x + 3 y = 10.5\n1.5 x + 3 y = 7.5\n is x =",
x,"y=",y)
How to extend this code to another linear system?
Let's define a function that will solve the system for (almost) any coefficients and right-hand side.
I'll define such a function to solve $$a_1 x + b_1 y = c_1\\ a_2 x + b_2 y = c_2.$$
Below is a function that solves (most) 2x2 linear systems. Take a few minutes to study the code below. Specifically:
LOUD
, is followed by =False
. This sets the default value of LOUD
to false.LOUD
toggles on/off a print statement.def two_by_two_solver(a1,b1,c1,a2,b2,c2, LOUD=False):
"""Calculate the solution of the system
a1 x + b1 y = c1,
a2 x + b2 y = c2
Args:
a1: x coefficient in first equation (cannot be zero)
b1: y coefficient in first equation
c1: right-hand side in first equation
a2: x coefficient in second equation
b2: y coefficient in second equation
c2: right-hand side in second equation
LOUD: boolean that decides whether to print out the answer
Returns:
list containing the solution in the format [x,y]
"""
#step one, eliminate x from the second equation by
#multiplying first equation by -a2/a1
#and then adding it to second equation
new_b2 = b2 - a2/a1*b1
new_c2 = c2 - a2/a1*c1
#solve the new equation 2
y = new_c2/new_b2
#plug y into original equation 1
x = (c1-b1*y)/a1
if (LOUD):
print("The solution to:\n",a1,"x +",b1,"y =",c1,
"\n",a2,"x +",b2,"y =",c2,"\n is x =",x,"y=",y)
return [x,y]
We can call this function for the problem above by typing
two_by_two_solver(4.5,3,10.5,1.5,3,7.5,True)
We can also solve other systems, including simple ones
two_by_two_solver(1,0,3,0,1,2,True)
We can't solve systems where $a_1$ is zero because our function divides by $a_1$:
two_by_two_solver(0,1,2,1,0,3,True)
Approach above: give inputs in order
We called the function two_by_two_solver by listing out the arguments in the order that it expects them a1, b1, c1, a2, b2, c2, LOUD.
Another option: use keywords
Python allows you to call them in any order, as long as you are explicit in what goes where.
two_by_two_solver(a1 = 4.5, b1 = 3,
a2 = 1.5, b2 = 3,
c1 = 10.5, c2 = 7.5, LOUD = True)
It is often a good idea to call a function explicitly (with keywords). That way if you mess up the order of the arguments, it does not matter.
Notice that in the function definition, the argument LOUD has =False after it. This indicates that if the function is called without a value for LOUD, it assumes the caller does not what the function to "be loud".
In other words, False is the default for argument LOUD.
two_by_two_solver(a1 = 4.5, b1 = 3, a2 = 1.5,
b2 = 3, c1 = 10.5, c2 = 7.5)
Notice that it did not print out it's spiel about the system.
two_by_two_solver(1,1,2,a2 = 1, c2 = 0, b2 = 3)
At the end of the function we have a return statement. This tells python what the function is returning to the caller. In this case we return a list that has the solution for $x$ and $y$. We can store this in a new variable, or do whatever we like with it.
answer = two_by_two_solver(a1 = 4.5, b1 = 3, a2 = 1.5,
b2 = 3, c1 = 10.5, c2 = 7.5)
x = answer[0] #store in the variable x the first value in the list answer
y = answer[1] #store in the variable y the first value in the list answer
print("The list",answer,"contains",x,"and",y)
# YOUR SOLUTION HERE
We can do even fancier things, if we are so bold
#just get x
x = two_by_two_solver(a1 = 4.5, b1 = 3, a2 = 1.5,
b2 = 3, c1 = 10.5, c2 = 7.5)[0]
print("x =",x)
#assign variables to the output on the fly
x,y = two_by_two_solver(a1 = 4.5, b1 = 3, a2 = 1.5,
b2 = 3, c1 = 10.5, c2 = 7.5)
print("x =",x,"y =",y)
These examples are more advanced and they are designed to show you some of the neat tricks you can do in python.
Our 2x2 solver code had a long, and pretty detailed comment at the beginning of it. This is called a docstring and it is printed by a user by typing
help(two_by_two_solver)
The point of this long comment is to communicate to other people:
In this example we can see that we need to provide at least 6 numbers, and possibly an optional boolean.
It is good programming practice to include good docstrings. It will be mandatory for any code you turn in this class.
Let's look at the docstring for some members of the math module and the random module.
import math
help(math.fabs)
import random
help(random.uniform)
We don't have the source code for these functions in front of us, but if we want to know how to call them (and we didn't want to Google® them), the docstrings tell us what to do.
You may wonder why those docstrings are a bit different that the one I used. The format for mine is derived from the <a, href="http://google-styleguide.googlecode.com/svn/trunk/pyguide.html#Comments"> Google coding standards for python docstrings</a>.
def nothing():
#this function does nothing
#docstring?
return 0
help(nothing)
When we call a function, it carves out in the computer's memory its own space. Variables that live in the special space, known as local scope are completely different than those in other parts of the program. Here's a simple, but illustrative example:
Home Activity - Your Predictions
x =
new_x =
y =
new_y =
def scope_demonstration(input_variable):
''' A simple demonstration of scoping rules
Args:
input_variable: a string or number
Returns:
x: --REDACTED TO NOT GIVE AWAY THE ACTIVITY ANSWER --
'''
x = input_variable*3
return x
#now call the function after defining some variables
x = "oui "
y = "no "
new_x = scope_demonstration(x)
new_y = scope_demonstration(y)
print("x =",x,"\nnew_x =",new_x)
print("y =",y,"\nnew_y =",new_y)
When I call scope_demonstration it creates its own memory space and any variable I create in there is different than in the rest of the program, even if the variables have the same name.
There are many subleties in scoping rules, but this example outlines just about everything that you'll need to know as a neophyte programmer.
A variable declared at the top level Python file, i.e., outside a function, is in the global scope for that file. This is easiest to see in an example:
my_var = 1
def print_my_var():
''' Another scope demonstration
Args:
Nothing
Returns:
Nothing
Other:
prints `my_var`
'''
print(my_var)
return None
print_my_var()
We were able to access my_var
inside the function even though it was not an input to a function.
The next natural guess is can we modify my_var
with a function? Let's see.
def change_my_var():
''' A third scope demonstration
Args:
Nothing
Returns:
Nothing
Other:
Attempts to change `my_var, prints to screen
'''
my_var = 3*my_var + 2
print("my_var = ",my_var," inside the function")
return None
change_my_var()
print("my_var =",my_var," outside the function")
We got an error message! In Python, we can only access my_var
inside functions. We cannot change it inside functions. If we wanted to change my_var, we would need to do the following:
def change_my_var2():
''' A third scope demonstration
Args:
Nothing
Returns:
New value of my_var
'''
return 3*my_var + 2
my_var = change_my_var2()
print("Now my_var =",my_var)
def my_func1(x,y):
''' A simple function to demonstrate the nuances of scope
Arguments:
x: scalar real number
y: scalar real number
Returns:
z: scalar real number
'''
z = x + y
x = 3
y = z - x + 1
return z
# Run the function
x = 1
y = 1
z = my_func1(x,y)
# Print values of x, y, and z to screen
print("x =",x," y =",y," z =",z)
Class Activity multiple choice answers:
### Create your answer here.
The idea behind recursion is that a function can call itself. That is really it. That capability can allow some neat tricks, but in practice there is often a faster way of doing things than using recursion.
Below are two functions that calculate factorial with and without recurision.
def factorial(n, prev=1):
if not((n==1) or (n==0)):
prev = factorial(n-1,prev)*n
return prev
def factorial_no_recursion(n):
output = 1;
#can skip 1 because x*1 = 1
for i in range(2,n+1):
output = output*i
return output
x = int(input("Enter an integer: "))
print(x,"! =",factorial(x))
print(x,"! =",factorial_no_recursion(x))
Let's see which is faster. This will use some ipython magic commands for timing execution. We'll compute the factorials of 0 through 20, 100,000 times.
%%time
for times in range(10**5):
for n in range(21):
factorial(n)
%%time
for times in range(10**5):
for n in range(21):
factorial_no_recursion(n)
Side-tangent: wall versus CPU user versus CPU clock time: https://stackoverflow.com/questions/7335920/what-specifically-are-wall-clock-time-user-cpu-time-and-system-cpu-time-in-uni (Side-tangents will not appear in tests or assignments, but are given to satisfy your curiosity.)
The no recursion version, while not as neat, is nearly 50% faster. It is also possible to have too many levels of recursion.
import sys
sys.setrecursionlimit(1000) # change this to answer the home activity question
x = 1000
#this won't work and prints ~1000 errors
#the errors are not repeated here
print(x,"! =",factorial(x))
x = 1000
#this works
answer = factorial_no_recursion(x)
print(x,"! =",answer)
By the way, I'm surprised it is giving the correct answer.
Sometimes we have to define many functions and don't want to have one giant source file (in fact this is good programming practice). I've created a file called sphere.py in the same directory as this notebook. This file defines two functions volume and surface_area that compute the volume and surface area of a sphere. Because the file is called sphere.py we can import those functions using import sphere. The text of sphere.py is
def volume(radius):
"""compute volume of a sphere
Args:
radius: float giving the radius of the sphere
Returns:
volume of the sphere as a float
"""
return 4.0/3.0*math.pi*radius**3
def surface_area(radius): """compute surface area of a sphere
Args:
radius: float giving the radius of the sphere
Returns:
surface area of the sphere as a float
"""
return 4.0*math.pi*radius**2
</code>
I can use the help function to tell me about the module:
import sphere
Now that I have imported the module, we can can the help
to see the docstring.
help(sphere)
r = 1.0
print("The volume of a sphere of radius",r,"cm is",
sphere.volume(r),"cm**3")
print("The surface area of a sphere of radius",r,"cm is",
sphere.surface_area(r),"cm**2")
It is very easy to read in text files in python. The file fifth_republic.txt, lists the presidents of France's fifth republic. It is in the same folder as this notebook.
In Python, we can read it in very simply:
file = open('./data/fifth_republic.txt', 'r')
#open fifth_republic.txt for reading ('r')
for line in file:
# Repeat the first 5 characters 3 times
print(line[0:5]*3)
file.close()
Notice how the for loop can iterate through each line of the file. You can also read a line at a time.
file = open('./data/fifth_republic.txt', 'r')
#open fifth_republic.txt for reading ('r')
# Read the first line
first_line = file.readline()
# Read the second line
second_line = file.readline()
print(first_line)
print(second_line)
file.close()
help(file.readline)
You can also easily write to a file.
writeFile = open("./data/hats.txt","w")
#open hats.txt to write (clobber if it exists)
hats = ["fedora","trilby","porkpie",
"tam o'shanter","Phrygian cap","Beefeaters' hat","sombrero"]
for hat in hats:
writeFile.write(hat + "\n") #add the endline
writeFile.close()
#now open file and print
readFile = open("./data/hats.txt","r")
for line in readFile:
print(line)
readFile.close()
We can also use enumerate
with a text file:
import csv
file = open('./data/fifth_republic.txt', 'r')
for i,row in enumerate(file):
print(row)
Write pseudocode and Python function program to interactively play the following game:
First write the pseudocode and then implement as a function in Python. Test your program at least 2 times.
# import libraries
import numpy as np
# function to accept guess from user and determine if guess matches secret
# number between 0 and 100
def guessing_game(rand_num=None):
''' Play the guess my number interactively with keyboard input
Argument:
rand_num: the random (secret) number. If None (default), the program generates
a random integer between 0 and 100.
Returns:
Nothing
Other:
Asks user for interactive keyboard input that are integers
'''
# YOUR SOLUTION HERE
Test 1: No input. The function will automatically generate a secret number.
# YOUR SOLUTION HERE
Test 2: What happens if we supply an out-of-bounds (invalid) secret number?
# YOUR SOLUTION HERE