IMPORTANT, Jupyter/iPython notebooks are comprised of many cell blocks. Clicking on text selects the block it is contained in. Double clicking allows you to edit the text. You can always run the currently selected cell, regenerating the formatted text or running the code within it, by hitting "shift-return". Please click the cell with code right below this one and hit "shift-return" to run it. This will load a CSS file that formats the rest of this notebook.

In [72]:
from IPython.core.display import HTML
def css_styling():
    styles = open("custom.css", "r").read()
    return HTML(styles)
css_styling()
Out[72]:
/* From Lorena Barba's AeroPython course: https://github.com/barbagroup/AeroPython */

For our purposes there are two types of cells that will be useful: Markdown and code. The type of a cell can be selected from the menubar above. This cell is an example of a Markdown cell, which is used for formatted text. The cell above this with the Python code to load the css style sheet is an example of a code cell. Clicking "shift-enter" on it causes the code within that cell to be run.


Lab 0 - Python and Jupyter/iPython Notebook Tutorial

(adapted from Professor Mark Kramer's MATLAB tutorial)

This tutorial will get you up to speed on the basic commands / usage of Python. We will use Python throughout this course, and you'll probably find Python useful in other courses... On the course website I will post links to a number of other Python tutorials / references I strongly suggest looking over.

In [ ]:
##  Preliminaries.
#   In Python text preceded by a '#' indicates a 'comment'.  This text should appear
#   gray in an Jupyter notebook.  I will use comments to explain what we're doing 
#   and to ask you questions.  Also, comments are useful in your own code
#   to note what you've done (so it makes sense when you return to the code
#   in the future).  It's a good habit to *always* comment your code.  I'll
#   try to set a good example, but won't always . . . 

##  Example 1.  Python is a calculator.

4 + 3 

#Q:  Select this code cell by clicking and hit "shift-return" to run it
#    What do you get?  Does it make sense?
In [ ]:
#   Example 1a. Python treats numbers like "4" as integer data types while "4." 
#   indicates that we want to treat the number of as a decimal type. Luckily
#   this doesn't really matter in Python 3. For example, the following should
#   all be equivalent in Python 3 (but are not equivalent in Python 2)
#   Note - "print" just prints the output of the operation (it is
#   needed here to get more then one result to printout from a cell)
print( 4 / 3 )
print( 4.0 / 3 )
print( 4 / 3.0 )
print( 4. / 3 )
print( 4. / 3. )
print( float(4) / 3 )

This shows an important property in Python: if we want to ensure a variable is a floating point value, we need to include a decimal point like 4. or 4.0. When performing arithmetic with variables that are a mix of floating point and integer types it is a good idea to explicitly convert all integer variables to floats.

In [ ]:
# Example 1b. Integer and floating point variables
# To save typing we can store the result of an operation in a variable
# that can be re-used and updated:
a = 4    # a is an integer type variable
b = 11   # b is an integer type variable
c = 11.  # c is a floating point type variable (has a decimal component)
d = 11.0 # d is also a floating point type variable
print( d - c )   # this does what we expect
print( b / a )   # this does floating point division, treating "a" as a floating point number
print( c / a )   # this does what we expect
print( c / float(a) ) # this makes clear that we want to treat "a" as a floating point number
In [ ]:
##  Example 2.  Python can compute complicated quantities. 
#               a**b mean a to the b power.

4./10.**2  # since only one output in this code block we don't need print

#Q:  Can you use parentheses to alter the result? (try (4./10.)**2)
#    You should be sure to learn the general precedence of different
#    operations in Python.
In [ ]:
##  Example 3.  Python has useful built in functions.
#   A function is a program that operates on arguments. 
#   standard math functions can be accessed from the math module
#   versions of these functions that can work on vectors/arrays are available
#   in the numyp module. Let's load these modules now.
import math          # we can now call functions from math using math.function_name
import numpy as np   # we can now call functions from numpy using np.function_name

#   consider the following function 
print( math.sin(2.*math.pi) )

#   Above, 'sin' is a function.  It operates on the argument '2*pi'.  Below
#   are three more examples of functions that operate on arguments 

print( math.cos(2.*math.pi + 1./10.) )
print( np.cos(2.*np.pi + 1./10.) )     # here we use the numpy version
print( math.exp(-2.) )
print( math.atan(2.*math.pi) )
In [ ]:
#Q:  What is 'atan'?  To answer this, try using Python Help.  To start the
# Python Help, simply put a ? at the end of atan and then run this code block
# you should see a description of the function pop up at the bottom of the window

math.atan?

#NOTE:  The Python Help is *extremely* useful.  Always look there when you
#have questions about a function.
In [ ]:
##  Example 4.  We can use Python to define vectors.
#   A vector is a list of numbers.  Let's define one 

np.array([1, 2, 3, 4])
In [ ]:
## Example 4a, using print just gives us the values
print( np.array([1, 2, 3, 4]) )
In [ ]:
##  Example 5.  We can manipulate vectors by scalars.
#   A scalar is a single number.  Consider 

a = np.array( [1, 2, 3, 4] )
print( a * 3 )
print( 4 * a )
print( a + 1 )

#Q:  What do you find?
In [ ]:
##  Example 6.  We can manupulate vectors with vectors 

a * a

#Q:  What does this return?

#We see that the operator "*" performs element-by-element multiplication.
In [ ]:
## Example 7. Some examples of working with vectors and variables
a = 2.
b = np.array( [0., 4., 7., 6.] )
c = np.array( [1., 5., 6., 8.] )
d = np.array( [2., 4.])

print( a*b * c )
print( b / c + a)
print( np.dot( b, c ))   # dot product

Example 8. We can probe the variables we've defined in Python. To see a list of the variables you've defined, type who or whos in a code block by themselves. Notice whos provides more information.

In [ ]:
who
In [ ]:
whos
In [ ]:
#  To determine the length of a vector/array
np.size(c)

Sometimes we need to reset the workspace and get rid of all the variables, type %reset and enter y

In [ ]:
%reset

Enter a command below to check there are no variables anymore in the workspace?

In [ ]:
 
In [ ]:
##  Example 10.  We can define matrices in Python.
#   A matrix is a group of vectors.  Consider the following 

import numpy as np  # have to reimport as we cleared the workspace above!

p = np.array( [[1,2,3],[4,5,6]] ) 

#   This creates a matrix with two rows and three columns.  We can
#   manipulate matrices like we manipulate vectors.  Consider 
print( p )
print( p + 2 )
print( 2 * p )
print( p * p )
In [ ]:
##  Example 11.  Indexing matrices and vectors.
#   Matrices and vectors are lists of numbers, and sometimes we want to
#   access individual elements or small subsets of these lists.  That's
#   easy to do in Python.  Consider 

a = np.array( [1, 2, 3, 4, 5] )
b = np.array( [6, 7, 8, 9, 10] )

#  Python indexes from 0 (like C/C++/Java, and unlike MATLAB/Fortran which start at 1) 
#  To access the 2nd element of 'a' or 'b', type a[1] / b[1].
#  We'll be a bit fancier with our printing now to distinguish variables. 
#  Calling str(a) converts the variable "a" to a string that can be printed easily.
#  Adding two strings just concatenates them: "hi" + " bye" = "hi bye".
print( "a[1] = " + str(a[1]) )
print( "b[1] = " + str(b[1]) )

#Q:  Do the results make sense?  How would you access the 4th element of
#each vector?

#   We can combine 'a' and 'b' to form a matrix with a as the first row and b as the second.
#   Note we pass the function the "tuple" (a,b) which it converts to a matrix
c = np.row_stack((a,b))
print("c = \n" + str(c))    # \n is a newline, i.e. return, which makes the printed matrix lineup better 

#   To learn the size/shape of 'c' we use np.shape:

print( "shape of c = " + str( np.shape(c) ) )

#   The shape of 'c' is [2 5].  It has two rows and five columns.  To access
#   the individual element in the 1st row and 4th column of 'c', type c[0,3]

print( "c[0,3] = " + str( c[0,3] ) )

#NOTE:  We access matrices using 'row,column' notation.  
#print the element in row 0, column 3 of c.


#   To access all columns in the entire first row of 'c', type c[0,:]
print( "c[0,:] = " + str( c[0,:] ) )
#   The notation ':' means 'all indices'


#   To access the 2nd thru 4th columns of the first row of 'c', type  c[0,1:4]
print( "2nd through 4th columns of the first row are c[0,1:4] = " + str(c[0,1:4]) )
#   The notation '1:4' means 'all integers from 1 up to, but not including 4', 
#   which in this case gives columns 1, 2, and 3.
In [ ]:
#Q:  Print all rows in the 2nd column of 'c'?
In [ ]:
##  Example 12:  We can find subsets of elements in matrices and vectors.
#   Sometimes we're interested in locating particular values within a
#   matrix or vector.  For example, let's first define a vector 

a = np.arange(1,10)    # this creates a vector of increasing values from 1 to 9
a = 2 * np.arange(1,10) 

print( "a = " + str(a) )
In [ ]:
#Q:  Calculate the shape of 'a'?  What is the maximum value of 'a'? (hint use the np.max function)
In [ ]:
#   Now let's find all values in 'a' that exceed 10.
#   Doing this is simple in Python 

a[a > 10]
In [ ]:
# this is called logical indexing, let's look at what "a>10" returns:
lgIdx = a > 10
print( lgIdx )

# when we now index "a" using this array we get back only the entries 
# in "a" corresponding to "True", as above
print( a[lgIdx] )
In [ ]:
# sometimes we want to know the actual indices in a where "a > 10"
# we can get them using the "nonzero()" function, which returns the
# index of all entries that were true
lgIdx.nonzero()
In [ ]:
# this gives another way to then pull them out of "a"
print( a[ (a > 10).nonzero() ] )
In [ ]:
# we can use these two types of indexing to change subsets of the values of a
print("a = " + str(a))
a[a > 10] = 100
print("a = " + str(a))
In [ ]:
# what about for a matrix?
b = np.array([[1,2,3],[4,5,6],[7,8,9]])
print( "b = " + str(b) )
print( " b > 5 is \n" + str(b > 5) )
print(" b[b>5] is an array: " + str(b[b>5]) )
# notice that the last line collapses the True entries to an array, 
# ordered by row and then by column. (If you've used MATLAB, this is 
# the opposite of what it does!)
In [ ]:
##  Example 13:  Plotting data in Python.
#   It's not easy to look at lists of numbers and gain any intuitive
#   feeling for their behavior, especially when the lists are long.  In
#   these cases, it's much better to visualize the lists of numbers by
#   plotting then.  Consider 

x = np.linspace(0,10,11)   
print( "x = " + str(x) )

#   The above line constructs a vector that starts at 0, ends at 10, and
#   takes steps of size 1 from 0 to 10 (so has 11 entries). Let

y = np.sin(x)
print( "y = " + str(y) )

#   Looking at the values in 'y' can you tell what's happending?

Let's visualize y vs x instead. First we must turn on inline plotting in the notebook:

In [ ]:
%matplotlib inline
In [ ]:
#   To visualize 'y' versus 'x' let's plot it
import matplotlib as mpl
from matplotlib import pyplot as plt  # import basic plotting routines
plt.plot(x,y) 
plt.show()          # this causes the plot to actually be displayed
In [ ]:
#  The plot of x versus y should look a bit jagged. and not 
#  smooth like a sinusoid.  To make the curve more smooth,
#  let's redefine 'x' as 

x = np.linspace(0,10, 101)
print(x)

#Q:  Compare this definition of 'x' to the definition above?  How do these
#two definitions differ?
#Q:  What is the size of 'x'?  Does this make sense?
In [ ]:
# replot the sine function
y = np.sin(x)
plt.plot(x,y,'k')   # the 'k' we've added makes the curve black instead of blue
plt.show()
In [ ]:
# Example 14: what if we want to compare several functions?
z = np.cos(x)
plt.plot(x,y,'k')  # y vs x is black
plt.plot(x,z,'b')  # z vs x is blue
plt.xlabel('x')    # x-axis label
plt.ylabel('y or z')  # y-axis label
plt.title('y vs x and z vs x')
plt.legend(('y','z'))  # make a legend labeling each line
plt.show()
In [ ]:
# I think the font size for the labels is too small, 
# we can change the default with:
mpl.rcParams.update({'font.size': 12})  
mpl.rcParams['axes.labelsize']=14      # make the xlabel/ylabel sizes a bit bigger to match up better

# we can change the default linewidth with
mpl.rcParams['lines.linewidth']=2

# let's make a new plot to check 
plt.plot(x,y)
plt.plot(x,z)     # notice without a color matplotlib will assign one
plt.xlabel('x')
plt.ylabel('y')
plt.title('y vs x')
plt.legend(('y','z'))
plt.show()
In [ ]:
# We can also use Latex for titles
# Latex fonts are smaller, so make the size bigger to start:
mpl.rcParams.update({'font.size': 14})  
mpl.rcParams['axes.labelsize']=16

# turn on Latex 
mpl.rc('text', usetex=True)

# let's make a new plot to check 
plt.plot(x,y)
plt.plot(x,z)     
plt.xlabel('$x$') # now we use Latex notation in our labels
plt.ylabel('$y$')
plt.title('$y$ vs $x$')
plt.legend(('$y$','$z$'))
plt.show()
In [ ]:
##  Example 15:  We can make random numbers in Python.
#   To generate a single Gaussian random number in Python, type 

print("a Gaussian random number (mean=0, variance=1): " + str( np.random.randn() ))

# a uniform random number on [0,1)
print("a uniform random number from [0,1): " + str(np.random.rand()))
In [ ]:
# lets generate a vector of 1000 Gaussian random numbers
r = np.random.randn(1000)
print(len(r))
In [ ]:
# first we turn off Latex text and update font sizes
mpl.rcParams.update({'font.size': 12})  
mpl.rcParams['axes.labelsize']=14     
mpl.rc('text', usetex=False)

# to look at a histogram (hopefully a Gaussian!)
plt.hist(r)
plt.show()

#   See Python Help to learn about the function 'hist'.  We'll talk more
#   about histograms this semester.
In [ ]:
##  Example 16:  Repeating commands over and over and over . . . 
#   Sometimes we'll want to repeat the same command over and over again.
#   What if we want to plot sin(x + k*pi/4) where k varies from 1 to 5 in
#   steps of 1;  how do we do it?  Consider the following 

x = np.linspace(0,10,101)  #Define a vector x that ranges from 0 to 10 with step 0.1.
k = 1
y = np.sin(x + k*np.pi/4)

plt.figure()
plt.plot(x,y)

k = 2
y = np.sin(x + k*np.pi/4)
plt.plot(x,y)

k = 3
y = np.sin(x + k*np.pi/4)
plt.plot(x,y)

k = 4
y = np.sin(x + k*np.pi/4)
plt.plot(x,y)

k = 5
y = np.sin(x + k*np.pi/4)
plt.plot(x,y)

plt.show()
In [ ]:
#   That's horrible code!  All I did was cut and paste the same thing
#   four times.  As a general rule, if you're cutting and pasting in code,
#   you're doing something wrong.  There's a much more elegant way to do
#   this, and it involves making a 'for' loop.  Consider 

x = np.linspace(0,10,101)        #First, define the vector x.

# here we declare a "for" loop where k successively takes the values
# 1, then 2, then 3, ..., up to 5. Note, any code we want to execute as 
# part of the loop must be indented one level. The first line of code
# that is not indented, in this case "plt.show()" below, executes after
# the for loop completes
for k in range(1,6):               
    y = np.sin(x + k*np.pi/4)      #Define y (note the variable 'k' in sin), also note we have indenteded here!
    plt.plot(x,y)                  #Plot x versus y
    
# no indentation now, so this code follows the loop
plt.show()

#   The small section of code above replaces all the cutting-and-pasting.
#   Instead of cutting and pasting, we update the definition of 'y' and
#   plot it within this for-loop.

#Q:  Spend some time studying this for-loop.  Does it make sense?
In [ ]:
##  Example 17:  Defining a new function.
#   We've spent some time in this lab writing and executing code.  
#   Sometimes we'll need to write our own Python functions.  Let's do that now.
#
#   Our function will do something very simple:  it will take as input a
#   vector and return as output the vector elements squared plus an additive
#   constant.  Ideally,  we'll call this function in Python as,

v = np.linspace(0.,10.,11)
b = 2.5

#   we would like to call:
#   vsq = my_square_function(v, b);
#   This won't work!  We first need to define 'my_square_function':
#   notice, just like the for loop earlier, the code the function 
#   executes should be indented one level. The first line that is
#   not indented runs outside the function definition.
#   Finally, notice the text inside the triple quotes. This is a doc
#   string that describes our function. If we type my_square_function?
#   it will be shown.

def my_square_function(x, c):
    """Square a vector and add a constant.

    Keyword arguments:
    x -- vector to square
    c -- constant to add to the square of x
    
    Returns:
    x*x + c
    """
    
    return x * x + c    

# let's run the code
v2 = my_square_function(v, b)
print("v = " + str(v))
print("v*v+2.5 = " + str(v2))
In [ ]:
# finally, let's check that our docstring works
my_square_function?
In [ ]:
# Q: Try to make a function, my_power, so that 
# y = power(x,n) evaluates y = x^n, 
# (in Python you can use x**n to take the power)

For our last example let's make a movie in iPython. It doesn't seem we can make these plots appear inline within the notebook, so we'll have them appear in a separate window. To do this we switch to the non-inline matplotlib mode:

In [ ]:
%matplotlib 
In [ ]:
## Example 18: Finally, let's make a movie in Python
# this requires a bit more work as we have to save the line 
# object that is plotted and then update it to change the curve
# NOTE, IF YOUR FIGURE APPEARS BEHIND OTHER WINDOWS YOU MIGHT NEED
# TO MOVE IT OUT OF THE WAY AND THEN RE-RUN THIS CODE TO SEE THE MOVIE

import matplotlib

x      = np.linspace(0.,2.,1001)
plt.plot(x, 0. * np.sin(x*np.pi))  # make the first plot, save the curve in "lines"
plt.axis([0, 2, -1, 1])            # set the x and y limits in the plot
plt.title("plot number = 0")
plt.ion()                          # this turns on interactive controls at the bottom of the figure
plt.draw()

for i in range(1,101):
    plt.close()
    plt.plot(x, float(i)/100. * np.sin(x*np.pi))  # here we change the y values at each x location
    plt.axis([0, 2, -1, 1])                     # set the x and y limits in the plot
    plt.title('plot number = ' + str(i))                  # update the title with the new plot number
    plt.draw()                                            # redraw the plot
    plt.pause(.001)                                       # the pause forces each plot to stay up for a bit of time
In [ ]:
# finally, let's make an XKCD style plot!
plt.xkcd()
plt.plot(x, np.sin(np.pi*x))
plt.title('An XKCD plot!!! (Do not use these on assignments!)')
In [ ]: