Homework_00 - Jupyter and Python

We will be making heavy use of the Python library called NumPy. It is not included by default, so we first need to import it. Go ahead and run the following cell:

import numpy as np

Now, we have access to all NumPy functions via the variable np (this is the convention in the Scientific Python community for referring to NumPy). We can take a look at what this variable actually is, and see that it is in fact the numpy module (remember that you will need to have run the cell above before np will be defined!):

np
<module 'numpy' from '/home/ryan/.conda/envs/newwork/lib/python3.9/site-packages/numpy/__init__.py'>

NumPy is incredibly powerful and has many features, but this can be a bit intimidating when you’re first starting to use it. If you are familiar with other scientific computing languages, the following guides may be of use:

  • NumPy for Matlab Users: https://numpy.org/devdocs/user/numpy-for-matlab-users.html

  • NumPy for R (and S-Plus) Users: http://mathesaurus.sourceforge.net/r-numpy.html

If not, don’t worry! Here we’ll go over the most common NumPy features.

Arrays and lists

The core component of NumPy is the ndarray, which is pronounced like “N-D array” (i.e., 1-D, 2-D, …, N-D). We’ll use both the terms ndarray and “array” interchangeably. For now, we’re going to stick to just 1-D arrays – we’ll get to multidimensional arrays later.

Arrays are very similar to lists. Let’s first review how lists work. Remember that we can create them using square brackets:

mylist = [3, 6, 1, 0, 10, 3]
mylist
[3, 6, 1, 0, 10, 3]

And we can access an element via its index. To get the first element, we use an index of 0:

print("The first element of 'mylist' is: " + str(mylist[0]))
The first element of 'mylist' is: 3

To get the second element, we use an index of 1:

print("The second element of 'mylist' is: " + str(mylist[1]))
The second element of 'mylist' is: 6

And so on.

Arrays work very similarly. The first way to create an array is from an already existing list:

myarray = np.array(mylist) # equivalent to np.array([3, 6, 1, 0, 10, 3])
myarray
array([ 3,  6,  1,  0, 10,  3])
Notice that myarray looks different than mylist -- it actually tells you that it's an array. If we take a look at the types of mylist and myarray, we will also see that one is a list and one is an array. Using type can be a very useful way to verify that your variables contain what you want them to contain:
# look at what type mylist is
type(mylist)
list
# look at what type myarray is
type(myarray)
numpy.ndarray

We can get elements from a NumPy array in exactly the same way as we get elements from a list:

print("The first element of 'myarray' is: " + str(myarray[0]))
print("The second element of 'myarray' is: " + str(myarray[1]))
The first element of 'myarray' is: 3
The second element of 'myarray' is: 6

Array slicing

Also like lists, we can use "slicing" to get different parts of the array. Slices look like myarray[a:b:c], where a, b, and c are all optional (though you have to specify at least one). a is the index of the beginning of the slice, b is the index of the end of the slice (exclusive), and c is the step size.

Note that the exclusive slice indexing described above is different than some other languages you may be familiar with, like Matlab and R. myarray[1:2] returns only the second elment in myarray in Python, instead of the first and second element.

First, let’s quickly look at what is in our array and list (defined above), for reference:

print("mylist:", mylist)
print("myarray:", myarray)
mylist: [3, 6, 1, 0, 10, 3]
myarray: [ 3  6  1  0 10  3]

Now, to get all elements except the first:

myarray[1:]
array([ 6,  1,  0, 10,  3])

To get all elements except the last:

myarray[:-1]
array([ 3,  6,  1,  0, 10])

To get all elements except the first and the last:

myarray[1:-1]
array([ 6,  1,  0, 10])

To get every other element of the array (beginning from the first element):

myarray[::2]
array([ 3,  1, 10])

To get every element of the array (beginning from the second element):

myarray[1::2]
array([6, 0, 3])

And to reverse the array:

myarray[::-1]
array([ 3, 10,  0,  1,  6,  3])

Array computations

So far, NumPy arrays seem basically the same as regular lists. What’s the big deal about them?

Working with single arrays

One advantage of using NumPy arrays over lists is the ability to do a computation over the entire array. For example, if you were using lists and wanted to add one to every element of the list, here’s how you would do it:

mylist = [3, 6, 1, 0, 10, 22]
mylist_plus1 = []
for x in mylist:
    mylist_plus1.append(x + 1)
mylist_plus1
[4, 7, 2, 1, 11, 23]

Or, you could use a list comprehension:

mylist = [3, 6, 1, 0, 10, 22]
mylist_plus1 = [x + 1 for x in mylist]
mylist_plus1
[4, 7, 2, 1, 11, 23]
If you haven't seen list comprehensions before, we strongly recommend that you go through the "Advanced Topics" section on Codecademy before proceeding!:

In contrast, adding one to every element of a NumPy array is far simpler:

myarray = np.array([3, 6, 1, 0, 10, 22])
myarray_plus1 = myarray + 1
myarray_plus1
array([ 4,  7,  2,  1, 11, 23])

This won’t work with normal lists. For example, if you ran mylist + 1, you’d get an error like this:

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-19-5b3951a16990> in <module>()
----> 1 mylist + 1

TypeError: can only concatenate list (not "int") to list

We can do the same thing for subtraction, multiplication, etc.:

print("Subtraction: \t" + str(myarray - 2))
print("Multiplication:\t" + str(myarray * 10))
print("Squared: \t" + str(myarray ** 2))
print("Square root: \t" + str(np.sqrt(myarray)))
print("Exponential: \t" + str(np.exp(myarray)))
Subtraction: 	[ 1  4 -1 -2  8 20]
Multiplication:	[ 30  60  10   0 100 220]
Squared: 	[  9  36   1   0 100 484]
Square root: 	[1.73205081 2.44948974 1.         0.         3.16227766 4.69041576]
Exponential: 	[2.00855369e+01 4.03428793e+02 2.71828183e+00 1.00000000e+00
 2.20264658e+04 3.58491285e+09]

Working with multiple arrays

We can also easily do these operations for multiple arrays. For example, let’s say we want to add the corresponding elements of two lists together. Here’s how we’d do it with regular lists:

list_a = [1, 2, 3, 4, 5]
list_b = [6, 7, 8, 9, 10]
list_c = [list_a[i] + list_b[i] for i in range(len(list_a))]
list_c
[7, 9, 11, 13, 15]

With NumPy arrays, we just have to add the arrays together:

array_a = np.array(list_a) # equivalent to np.array([1, 2, 3, 4, 5])
array_b = np.array(list_b) # equivalent to np.array([6, 7, 8, 9, 10])
array_c = array_a + array_b
array_c
array([ 7,  9, 11, 13, 15])
Note: make sure when adding arrays that you are actually working with arrays, because if you try to add two lists, you will *not* get an error. Instead, the lists will be concatenated:
list_a + list_b
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Just as when we are working with a single array, we can add, subtract, divide, multiply, etc. several arrays together:

print("Subtraction: \t" + str(array_a - array_b))
print("Multiplication:\t" + str(array_a * array_b))
print("Exponent: \t" + str(array_a ** array_b))
print("Division: \t" + str(array_a / array_b))
Subtraction: 	[-5 -5 -5 -5 -5]
Multiplication:	[ 6 14 24 36 50]
Exponent: 	[      1     128    6561  262144 9765625]
Division: 	[0.16666667 0.28571429 0.375      0.44444444 0.5       ]

Creating and modifying arrays

One thing that you can do with lists that you cannot do with NumPy arrays is adding and removing elements. For example, I can create a list and then add elements to it with append:

mylist = []
mylist.append(7)
mylist.append(2)
mylist
[7, 2]

However, you cannot do this with NumPy arrays. If you tried to run the following code, for example:

myarray = np.array([])
myarray.append(7)

You’d get an error like this:

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-25-0017a7f2667c> in <module>()
      1 myarray = np.array([])
----> 2 myarray.append(7)

AttributeError: 'numpy.ndarray' object has no attribute 'append'
To create a NumPy array, you must create an array with the correct shape from the beginning. However, the array doesn't have to have all the correct values from the very beginning: these you can fill in later.

There are a few ways to create a new array with a particular size:

  • np.empty(size) – creates an empty array of size size

  • np.zeros(size) – creates an array of size size and sets all the elements to zero

  • np.ones(size) – creates an array of size size and sets all the elements to one

So the way that we would create an array like the list above is:

myarray = np.empty(2) # create an array of size 2
myarray[0] = 7
myarray[1] = 2
myarray
array([7., 2.])
Another very useful function for creating arrays is np.arange, which will create an array containing a sequence of numbers (it is very similar to the built-in range or xrange functions in Python).

Here are a few examples of using np.arange. Try playing around with them and make sure you understand how it works:

# create an array of numbers from 0 to 3
np.arange(3)
array([0, 1, 2])
# create an array of numbers from 1 to 5
np.arange(1, 5)
array([1, 2, 3, 4])
# create an array of every third number between 2 and 10
np.arange(2, 10, 3)
array([2, 5, 8])
# create an array of numbers between 0.1 and 1.1 spaced by 0.1
np.arange(0.1, 1.1, 0.1)
array([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1. ])

“Vectorized” computations

Another very useful thing about NumPy is that it comes with many so-called “vectorized” operations. A vectorized operation (or computation) works across the entire array. For example, let’s say we want to add together all the numbers in a list. In regular Python, we might do it like this:

mylist = [3, 6, 1, 10, 22]
total = 0
for number in mylist:
    total += number
total
42

Using NumPy arrays, we can just use the np.sum function:

# you can also just do np.sum(mylist) -- it converts it to an
# array for you!
myarray = np.array(mylist)
np.sum(myarray)
42
There are many other vectorized computations that you can do on NumPy arrays, including multiplication (np.prod), mean (np.mean), and variance (np.var). They all act essentially the same way as np.sum -- give the function an array, and it computes the relevant function across all the elements in the array.

Exercise: Euclidean distance (2 points)

Recall that the Euclidean distance \(d\) is given by the following equation:

\[ d(a, b) = \sqrt{\sum_{i=1}^N (a_i - b_i) ^ 2} \]

In NumPy, this is a fairly simple computation because we can rely on array computations and the np.sum function to do all the heavy lifting for us.

Complete the function euclidean_distance below to compute $d(a,b)$, as given by the equation above. Note that you can compute the square root using np.sqrt.
def euclidean_distance(a, b):
    """Computes the Euclidean distance between a and b.
    
    Hint: your solution can be done in a single line of code!
    
    Parameters
    ----------
    a, b : numpy arrays or scalars with the same size
    
    Returns
    -------
    the Euclidean distance between a and b
    
    """
    # YOUR CODE HERE
    raise NotImplementedError()
Remember that you need to execute the cell above (with your definition of euclidean_distance), and then run the cell below to check your answer. If you make changes to the cell with your answer, you will need to first re-run that cell, and then re-run the test cell to check your answer again.

Problem 1

Show that for a=[0 3 0] and b=[4 0 0], the euclidean distance is 5.

# Show work

Creating multidimensional arrays

Previously, we saw that functions like np.zeros or np.ones could be used to create a 1-D array. We can also use them to create N-D arrays. Rather than passing an integer as the first argument, we pass a list or tuple with the shape of the array that we want. For example, to create a \(3\times 4\) array of zeros:

arr = np.zeros((3, 4))
arr
array([[0., 0., 0., 0.],
       [0., 0., 0., 0.],
       [0., 0., 0., 0.]])
The *shape* of the array is a very important concept. You can always get the shape of an array by accessing its shape attribute:
arr.shape
(3, 4)

Note that for 1-D arrays, the shape returned by the shape attribute is still a tuple, even though it only has a length of one:

np.zeros(3).shape
(3,)

This also means that we can create 1-D arrays by passing a length one tuple. Thus, the following two arrays are identical:

np.zeros((3,))
array([0., 0., 0.])
np.zeros(3)
array([0., 0., 0.])
There is a warning that goes with this, however: be careful to always use tuples to specify the shape when you are creating multidimensional arrays. For example, to create an array of zeros with shape (3, 4), we must use np.zeros((3, 4)). The following will not work:
np.zeros(3, 4)

It will give an error like this:

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-39-06beb765944a> in <module>()
----> 1 np.zeros(3, 4)

TypeError: data type not understood

This is because the second argument to np.zeros is the data type, so numpy thinks you are trying to create an array of zeros with shape (3,) and datatype 4. It (understandably) doesn’t know what you mean by a datatype of 4, and so throws an error.

Another important concept is the size of the array -- in other words, how many elements are in it. This is equivalent to the length of the array, for 1-D arrays, but not for multidimensional arrays. You can also see the total size of the array with the size attribute:
arr = np.zeros((3, 4))
arr.size
12

We can also create arrays and then reshape them into any shape, provided the new array has the same size as the old array:

arr = np.arange(32).reshape((8, 4))
arr
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11],
       [12, 13, 14, 15],
       [16, 17, 18, 19],
       [20, 21, 22, 23],
       [24, 25, 26, 27],
       [28, 29, 30, 31]])

Accessing and modifying multidimensional array elements

To access or set individual elements of the array, we can index with a sequence of numbers:

# set the 3rd element in the 1st row to 0
arr[0, 2] = 0
arr
array([[ 0,  1,  0,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11],
       [12, 13, 14, 15],
       [16, 17, 18, 19],
       [20, 21, 22, 23],
       [24, 25, 26, 27],
       [28, 29, 30, 31]])

We can also access the element on it’s own, without having the equals sign and the stuff to the right of it:

arr[0, 2]
0

We frequently will want to access ranges of elements. In NumPy, the first dimension (or axis) corresponds to the rows of the array, and the second axis corresponds to the columns. For example, to look at the first row of the array:

# the first row
arr[0]
array([0, 1, 0, 3])

To look at columns, we use the following syntax:

# the second column
arr[:, 1]
array([ 1,  5,  9, 13, 17, 21, 25, 29])

The colon in the first position essentially means “select from every row”. So, we can interpret arr[:, 1] as meaning “take the second element of every row”, or simply “take the second column”.

Using this syntax, we can select whole regions of an array. For example:

# select a rectangular region from the array
arr[2:5, 1:3]
array([[ 9, 10],
       [13, 14],
       [17, 18]])
Note: be careful about setting modifying an array if what you really want is a copy of an array. Remember that in Python, variables are really just pointers to objects.

For example, if I want to create a second array that mutliples every other value in arr by two, the following code will work but will have unexpected consequences:

arr = np.arange(10)
arr2 = arr
arr2[::2] = arr2[::2] * 2
print("arr:  " + str(arr))
print("arr2: " + str(arr2))
arr:  [ 0  1  4  3  8  5 12  7 16  9]
arr2: [ 0  1  4  3  8  5 12  7 16  9]

Note that arr and arr2 both have the same values! This is because the line arr2 = arr doesn’t actually copy the array: it just makes another pointer to the same object. To truly copy the array, we need to use the .copy() method:

arr = np.arange(10)
arr2 = arr.copy()
arr2[::2] = arr2[::2] * 2
print("arr:  " + str(arr))
print("arr2: " + str(arr2))
arr:  [0 1 2 3 4 5 6 7 8 9]
arr2: [ 0  1  4  3  8  5 12  7 16  9]

Problem 2: Border

Write a function to create a 2D array of arbitrary shape. This array should have all zero values, except for the elements around the border (i.e., the first and last rows, and the first and last columns), which should have a value of one.

def border(n, m):
    """Creates an array with shape (n, m) that is all zeros
    except for the border (i.e., the first and last rows and
    columns), which should be filled with ones.

    Hint: you should be able to do this in three lines
    (including the return statement)

    Parameters
    ----------
    n, m: int
        Number of rows and number of columns

    Returns
    -------
    numpy array with shape (n, m)

    """
    # YOUR CODE HERE
# Show work

Problem 3

Below a 2D array, A88 is created that reshapes the integers 1 through 64 into an \(8\times8\) array. Create a new 1-D array that only contains the last column of a8 e.g. [8, 16, 24, 32, 40, 48, 56, 64].

A88 = np.arange(1,65).reshape(8,8)
# a8 = your work

Vector algebra with Arrays

You can represent physics vectors with 1D arrays. If you have a chosen basis set, you can save the components of the vector. Consider the following example, a force, \(\mathbf{F}\), is applied to a an object and it moves one meter along the x-axis. The force has three components, \(\mathbf{F} = 2\hat{i}+3\hat{j}+0\hat{k}~N\).

You can determine the work done by the force using np.dot as such

F = np.array([2, 3, 0])
dx = np.array([1, 0, 0])

W = np.dot(F,dx)

print('work done by F is {} N-m'.format(W))
work done by F is 2 N-m

You can also animate the motion of the object with the applied force. Here, the motion is assumed to travel at a constant speed from \(x = 0 - 1~m\).

import matplotlib.pyplot as plt

from matplotlib import animation, rc
from IPython.display import HTML

fig, ax = plt.subplots()

ax.set_xlim(( 0, 3))
ax.set_ylim((-0.1, 5))

line1, = ax.plot([], [], 's')
line2, = ax.plot([], [], 'r-v')

def init():
    line.set_data([], [])
    line2.set_data([],[])
    return (line1, line2)
    
def animate(i):
    x = np.linspace(0, 1, 100)
    line1.set_data(x[i], 0)
    line2.set_data([x[i], x[i] + F[0]], [0, 0 + F[1]]) 
    #plt.quiver(x[i],0,F[0],F[1])
    return (line1, line2)
    
anim = animation.FuncAnimation(fig, animate, init_func=init,
                               frames=100, interval=20, blit=True)

HTML(anim.to_html5_video())
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Input In [53], in <cell line: 26>()
     23     #plt.quiver(x[i],0,F[0],F[1])
     24     return (line1, line2)
---> 26 anim = animation.FuncAnimation(fig, animate, init_func=init,
     27                                frames=100, interval=20, blit=True)
     29 HTML(anim.to_html5_video())

File ~/.conda/envs/newwork/lib/python3.9/site-packages/matplotlib/animation.py:1634, in FuncAnimation.__init__(self, fig, func, frames, init_func, fargs, save_count, cache_frame_data, **kwargs)
   1631 # Needs to be initialized so the draw functions work without checking
   1632 self._save_seq = []
-> 1634 super().__init__(fig, **kwargs)
   1636 # Need to reset the saved seq, since right now it will contain data
   1637 # for a single frame from init, which is not what we want.
   1638 self._save_seq = []

File ~/.conda/envs/newwork/lib/python3.9/site-packages/matplotlib/animation.py:1396, in TimedAnimation.__init__(self, fig, interval, repeat_delay, repeat, event_source, *args, **kwargs)
   1394 if event_source is None:
   1395     event_source = fig.canvas.new_timer(interval=self._interval)
-> 1396 super().__init__(fig, event_source=event_source, *args, **kwargs)

File ~/.conda/envs/newwork/lib/python3.9/site-packages/matplotlib/animation.py:883, in Animation.__init__(self, fig, event_source, blit)
    880 self._close_id = self._fig.canvas.mpl_connect('close_event',
    881                                               self._stop)
    882 if self._blit:
--> 883     self._setup_blit()

File ~/.conda/envs/newwork/lib/python3.9/site-packages/matplotlib/animation.py:1197, in Animation._setup_blit(self)
   1194 self._drawn_artists = []
   1195 self._resize_id = self._fig.canvas.mpl_connect('resize_event',
   1196                                                self._on_resize)
-> 1197 self._post_draw(None, self._blit)

File ~/.conda/envs/newwork/lib/python3.9/site-packages/matplotlib/animation.py:1150, in Animation._post_draw(self, framedata, blit)
   1148     self._blit_draw(self._drawn_artists)
   1149 else:
-> 1150     self._fig.canvas.draw_idle()

File ~/.conda/envs/newwork/lib/python3.9/site-packages/matplotlib/backend_bases.py:2060, in FigureCanvasBase.draw_idle(self, *args, **kwargs)
   2058 if not self._is_idle_drawing:
   2059     with self._idle_draw_cntx():
-> 2060         self.draw(*args, **kwargs)

File ~/.conda/envs/newwork/lib/python3.9/site-packages/matplotlib/backends/backend_agg.py:436, in FigureCanvasAgg.draw(self)
    432 # Acquire a lock on the shared font cache.
    433 with RendererAgg.lock, \
    434      (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
    435       else nullcontext()):
--> 436     self.figure.draw(self.renderer)
    437     # A GUI class may be need to update a window using this draw, so
    438     # don't forget to call the superclass.
    439     super().draw()

File ~/.conda/envs/newwork/lib/python3.9/site-packages/matplotlib/artist.py:73, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
     71 @wraps(draw)
     72 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 73     result = draw(artist, renderer, *args, **kwargs)
     74     if renderer._rasterizing:
     75         renderer.stop_rasterizing()

File ~/.conda/envs/newwork/lib/python3.9/site-packages/matplotlib/artist.py:50, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
     47     if artist.get_agg_filter() is not None:
     48         renderer.start_filter()
---> 50     return draw(artist, renderer)
     51 finally:
     52     if artist.get_agg_filter() is not None:

File ~/.conda/envs/newwork/lib/python3.9/site-packages/matplotlib/figure.py:2847, in Figure.draw(self, renderer)
   2844 finally:
   2845     self.stale = False
-> 2847 self.canvas.draw_event(renderer)

File ~/.conda/envs/newwork/lib/python3.9/site-packages/matplotlib/backend_bases.py:1779, in FigureCanvasBase.draw_event(self, renderer)
   1777 s = 'draw_event'
   1778 event = DrawEvent(s, self, renderer)
-> 1779 self.callbacks.process(s, event)

File ~/.conda/envs/newwork/lib/python3.9/site-packages/matplotlib/cbook/__init__.py:292, in CallbackRegistry.process(self, s, *args, **kwargs)
    290 except Exception as exc:
    291     if self.exception_handler is not None:
--> 292         self.exception_handler(exc)
    293     else:
    294         raise

File ~/.conda/envs/newwork/lib/python3.9/site-packages/matplotlib/cbook/__init__.py:96, in _exception_printer(exc)
     94 def _exception_printer(exc):
     95     if _get_running_interactive_framework() in ["headless", None]:
---> 96         raise exc
     97     else:
     98         traceback.print_exc()

File ~/.conda/envs/newwork/lib/python3.9/site-packages/matplotlib/cbook/__init__.py:287, in CallbackRegistry.process(self, s, *args, **kwargs)
    285 if func is not None:
    286     try:
--> 287         func(*args, **kwargs)
    288     # this does not capture KeyboardInterrupt, SystemExit,
    289     # and GeneratorExit
    290     except Exception as exc:

File ~/.conda/envs/newwork/lib/python3.9/site-packages/matplotlib/animation.py:907, in Animation._start(self, *args)
    904 self._fig.canvas.mpl_disconnect(self._first_draw_id)
    906 # Now do any initial draw
--> 907 self._init_draw()
    909 # Add our callback for stepping the animation and
    910 # actually start the event_source.
    911 self.event_source.add_callback(self._step)

File ~/.conda/envs/newwork/lib/python3.9/site-packages/matplotlib/animation.py:1698, in FuncAnimation._init_draw(self)
   1696     self._draw_frame(frame_data)
   1697 else:
-> 1698     self._drawn_artists = self._init_func()
   1699     if self._blit:
   1700         if self._drawn_artists is None:

Input In [53], in init()
     14 def init():
---> 15     line.set_data([], [])
     16     line2.set_data([],[])
     17     return (line1, line2)

NameError: name 'line' is not defined
../_images/HW_00_123_1.png