$30
ECE 311 Lab 1: Introduction to Python
Hello and welcome to ECE 311: Digital Signal Processing Lab! The main goals of this course are to:
Reinforce the concepts you learn in ECE 310.
Give you experience with Python, which is a popular programming language for work in signal processing, machine learning, and many other fields.
Demonstrate the applications of signal processing within and beyond the context of ECE 310. And hopefully encourage you to take more courses in Signal Processing!
This first lab will serve as an introduction to Python and the scripting environment we will use throughout the class, Jupyter (or IPython) Notebooks. To be clear, ECE 311 is not a programming class! The students in ECE 311 have a wide range of programming experience, and we want the focus of the course to be on the fundamental tools and interesting applications of signal processing. With that said, we expect you to become more comfortable with Python as the semester goes on and hope you take the time to understand your code. In this lab, we will cover the basic syntax, data types, and a few more advanced techniques that are commonly used when programming in Python. Let's get started!
Importing Libraries
Python is an object-oriented scripting language that is popular due to its ease of development and tremendous assortment of libraries. The environment we are working in is called a Jupyter Notebook. Each notebook is composed of a collection of code and markdown cells. Markdown cells are those like this one that show text and headings, while code cells execute Python code. In order to run a cell, simply click on the cell and press Ctrl+Enter, Shift+Enter, or the "Play" button on the toolbar. To add or remove cells, click on the "Insert" and "Edit" tabs in the toolbar, respectively. In the below code cell, we will import three common libraries: numpy (Numerical Python), skimage (Image Processing), matplotlib (implementation of popular MATLAB plotting functions), and a couple other helpful utilities. Go ahead and run the next cell.
import numpy as np #rename the library to np for convenience
import matplotlib.pyplot as plt #we want the "pyplot" module
from IPython.display import Audio #listening to audio
from skimage import io #image i/o
from skimage import transform #image transformations
from scipy.io import wavfile #audio i/o
#allows plots to display in the notebook instead of another window
%matplotlib inline
print('Libraries successfully imported!')
Libraries successfully imported!
Data Types and Control Structures
Primitive Data Types
Now that we've imported some basic libraries, let's try creating variables to do simple math and printing. Python has "dynamically-typed" variables, which means the type of a variable is bound at runtime (i.e. when you run your code). In other words, unlike C or Java, we do not need to declare the types of our variables! This does not mean we should be reckless with creating our variables, but rather we have more flexbility.
a = 10 #integer
b = 5. #float
c = 'signal processing is super cool! ->' #string
d = True #boolean (True/False) data type
e = 3 + 4.j #complex valued number
add = a+b #addition
sub = a-b #subtraction
div = a/b #division
mult = a*b #multiplication
power = a**b #exponent
print('Hello World!') #printing
print('I must tell everyone that a+b is '+str(add)+'!') # '+' is used for string concatenation (connecting)
print('And also that',c,d) # or comma separation can be used to print multiple variables
print(type(a),type(b),type(c),type(d),type(e)) #check the type of some of our variables, this is great for debugging
Hello World!
I must tell everyone that a+b is 15.0!
And also that signal processing is super cool! -> True
<class 'int'> <class 'float'> <class 'str'> <class 'bool'> <class 'complex'>
Arrays, Lists, and Dictionaries
Three of the most important data types in Python are (numpy) arrays, lists, and dictionaries. In the below code cell, we explore several ways to create and manipulate these data types. But first, let's discuss their differences. Arrays in Python are like the arrays you may be familiar with from C or C++. They are fixed-sized, ordered collections of variables. Unlike other languages however, numpy arrays can mix data types. In order to access an element in an array, we supply an index in square brackets. If we have a multidimensional array, like an image, we can supply two indices, where the first index is the row and the second index is the column of the array we would like to access. This means that numpy arrays are "row major". It is best to separate these indices with a comma. It is important to note that Python zero-indexes its objects. Therefore, if you want the very first element in your array, you should use index "0". We will use numpy arrays the most in this class since they interface seemlessly with all the libraries we use.
Lists are similar to arrays except they do not have a fixed size upon instantiation. Below, you can see how the $\textrm{append()}$ method allows us to place a new element at the end of a list. For those of you familiar with C++ or Java, lists are similar to vectors in these languages. It is common practice to create a list first to store data we do not know the size of, then typecast the list to a numpy array for convenience in later work. This point is especially important! Take note of how we typecast from list to numpy array below.
Dictionaries are associative arrays that allow us to store key-value pairs together. Keys are like indices for lists and arrays, except they need not be integers! They can also be strings, floats, tuples, and more. Dictionaries are a great way to store information that is not necessarily ordered or should be referenced by more descriptive information than an integer index. We will not use dictionaries much in this class; however, an introduction to Python would be incomplete without discussing dictionaries.
Run the below code cell and please play around with the syntax. See what works, see what breaks, try printing different variables you create.
#Arrays
arr0 = np.zeros(3) #make an array of zeros, "np.zeros" means we want the zeros() function in the numpy module
arr1 = np.ones(3) #make an array of ones
arr2 = np.array([1,2,3]) #typecasting a list to an array
arr2d = np.array([[1,5],
[2,3]])
print(arr0)
print(arr2d.shape) #arrays have shape, lists do not
print(arr2d[1,1]) #can index numpy arrays with tuples as shown
[0. 0. 0.]
(2, 2)
3
#Lists
x = [1,2,3] #simple 3-element list
y = [[1,2,3],['banana','apple','orange']] #list of lists!
print(x[0],x[1])
print(x[-1]) #indices "wrap around", so we can also use negative indices
print(y[0][0],y[1][2]) #indexing a list of lists
print(len(x)) #len gives us the length of an array or list
#appending to a list
print(x)
x.append(4)
print(x)
1 2
3
1 orange
3
[1, 2, 3]
[1, 2, 3, 4]
#Dictionaries
d = {} #empty dictionary
d = {'Mechanical Engineering': '5/10', 'Chemical Engineering': '6/10',
'Electrical and Computer Engineering': '10/10', 'Computer Science': '-1/10'} #initialize some key-value pairs
d['Signal Processing'] = '11/10' #add another key-value pair
print(d['Electrical and Computer Engineering'])
10/10
Control Structures
Now let's look at some control structures. Most of you should be familiar with "if-else" statements (conditional structures) and iterative structures like "for" and "while" loops. Still, let's do a quick review.
If-else statements check if a (boolean) condition is true. If this condition is true, we execute the related code; else we execute different code. We can create a chain of if-else statements to try several conditions at a particular point in our code.
For loops execute a segment of code as we go through a predetermined set of values for a variable. In Python, all for loops are created as iterators. This means we do not count from 0 to 10, for example, like in C. Instead, our iterating variable goes through a collection of values we give it. This may be confusing at first, but it is incredibly convenient, so don't get frustrated if it doesn't make sense immediately. We strongly suggest you play around with the below code examples if you want some practice.
While loops, though we will use them infrequently, allow us to execute a piece of code until a condition is met. Be careful when using while loops to make sure the condition will be eventually met or overriden. Otherwise, your code will run forever!
One last thing we should note is that Python is a white-space drive language. This means we do not use curly braces {} to indicate the scope of a control structure, instead we use indentation (tabs). Each of the examples below demonstrate how this tab-controlled scoping works. Try modifying and uncommenting parts of the code to make sure you understand how to write each control structure. Helpful tip: you can comment and uncomment a section of code by highlighting multiple lines and typing Ctrl+/.
#if-else
a = 5
if(a > 2): #note the colon to terminate the if statement
print('Hooray!\n') #indentation for the if statement
b = 'Signal Processing'
print(b+'?')
if(b == 'Power'): #double equals is for equality comparison
print('Eh. Not interesting.')
elif(b == 'Circuits'): #note that "else if" is written as "elif"
print('Gross.')
elif(b == 'Signal Processing'):
print("Now we're talkin'!")
else:
print('Pass.')
Hooray!
Signal Processing?
Now we're talkin'!
#for
#range is a function that gives us a list of numbers based on different arguments
#try uncommenting each example to verify what it prints
for i in range(3): #with one integer "n", we get every number from 0 to n-1, inclusive
print(i)
#for i in range(1,4): #1,2,3
#print(i)
#for i in range(5,0,-1): #5,4,3,2,1
#print(i)
#can also iterate over lists, arrays and dictionaries
a = [1,5,9,20]
for i in a: #i will iterate through every value in a
print(i)
print('')
for key in d: #i will iterate through every key in the dictionary from the previous code cell
#print each key value pair
#string formatting, similar to C/C++, convenient tool for printing
#variables inside the .format() fill in the curly braces in order
print('{}: {}'.format(key, d[key]))
print('')
0
1
2
1
5
9
20
Mechanical Engineering: 5/10
Chemical Engineering: 6/10
Electrical and Computer Engineering: 10/10
Computer Science: -1/10
Signal Processing: 11/10
#while
j = 0
while(j < 4):
print(j)
j += 1 #compound addition, equivalent to j = j+1
0
1
2
3
Exercise 1:
a) Create a list of integers that represent your UIN. Use a for loop to iterate through and print every number in your UIN. Do not worry about making every number print on the same line: separate lines is fine.
b) Create a dictionary with three key-value pairs that has music artists as keys and one of your favorite songs from them as the value. Or you could do actors and TV shows/movies. Use a for loop to print each key-value pair.
#Code for 1.a) here.
uin = [6,5,4,9,1,8,6,0,1]
for i in range(len(uin)):
print(uin[i], end = "")
#Code for 1.b) here
d = {'Drake': 'Do Not Disturb', 'J. Cole': 'Snow on Tha Bluff', 'The Eagles': 'Hotel California'}
print('')
for key in d:
print('\n{}: {}'.format(key, d[key]))
654918601
Drake: Do Not Disturb
J. Cole: Snow on Tha Bluff
The Eagles: Hotel California
List Comprehension and Other Techniques
List comprehension is a popular and very efficient way to create lists. If you hear someone talk about "Pythonic" code practices, this is one of them. Quick tip: if you find yourself coding in Python for an interview, definitely build lists via list comprehension if possible. List comprehension is when we build a list "in-line". This means we initialize and populate the list all in one line. The below code presents toy examples, but when we start working with real signals like audio, images, etc. this can save lots of time and code. Furthermore, take note again how we typecast the list to a numpy array after constructing it.
Array/list slicing and truth array indices are powerful techniques to extract collections of items from a list based on some conditions. Array slicing allows us to specify a range of indices we would like to extract from an array. Slices can indicate we want a specific segment, every element along a dimension, something like "every other" element, and more. The general format for array slicing can be one of the following:
array[start:end]
array[start:end:step]
For now, we will start with the first option to extract a segment of an array (bonus: this stackoverflow post provides a nice reference for each option). We use a colon (:) to separate the arguments in our slices, and it is important to note that the second argument (end) is always non-inclusive. This means array[2:4] will return the elements at index 2 and 3 only. Furthermore, we may slice multiple dimensions at a time. Examples of array slicing and some of these more advanced cases are given below.
We can also build a Boolean array by "asking" a condition of the elements in the array. By placing that Boolean array as the index for our original array, we are able to automatically pull out the values we want! Don't worry if this is all too abstract right now. The following code should make it more concrete. And, as always, take some time to play around with the code. Taking the time now to familiarize yourself with these tools will save tons of time in the future.
#List Comprehension
a = np.array([i*2 for i in range(1,51)]) #create a list of the first 50 positive even numbers, then cast to array
b = np.array([i-1 for i in a]) #create a list/array of the first 50 odd numbers from the 'a' list
#Slicing
first_5_even = a[:5] #0,2,4,6,8 (the zero at the beginning is inferred)
print(first_5_even)
after_30_even = a[30:] #take every element in 'a' from the index-30 element onward
print(after_30_even)
print('')
#2D Slicing
c = np.array([[1,2,3,4,5],[6,7,8,9,10],[11,12,13,14,15],[16,17,18,19,20]])
# print(c) #original 2D array
# print(c[1:3]) #print rows 1 and 2
# print(c[:,0]) #print every element in the 0 column, commas separate dimensions (rows and columns)
# print(c[0,:]) #print every element in the 0 row
# print('')
#Truth array indices
greater_50 = a > 50 #elements larger than 50
perfect_squares = a % np.sqrt(a) == 0 #perfect squares
print(greater_50) #truth array
print(perfect_squares)
print(a[greater_50]) #show the actual entries in "a" that satisfy the above conditions
print(a[perfect_squares])
[ 2 4 6 8 10]
[ 62 64 66 68 70 72 74 76 78 80 82 84 86 88 90 92 94 96
98 100]
[False False False False False False False False False False False False
False False False False False False False False False False False False
False True True True True True True True True True True True
True True True True True True True True True True True True
True True]
[False True False False False False False True False False False False
False False False False False True False False False False False False
False False False False False False False True False False False False
False False False False False False False False False False False False
False True]
[ 52 54 56 58 60 62 64 66 68 70 72 74 76 78 80 82 84 86
88 90 92 94 96 98 100]
[ 4 16 36 64 100]
Exercise 2:
a) Using list comprehension, create an array of the first 100 multiples of 3 (including 0). Then, using array slicing, print the index 20 up to (but not including) index 30 multiples of three. To receive full points, your answer should be two lines (not including any print statements).
b) Using list comprehension, create an array of the first 100 perfect cubes, i.e. 0,1,8,27... (including 0). Then, using a truth array as your indices, print the perfect cubes that are also divisible by 32. To receive full points your answer should be three lines. Don't forget to typecast your list to an array!
#Code for 2.a) here.
multiples_3 = np.array([3*x for x in range(100)])
sliced_multiples_3 = multiples_3[20:30]
print(multiples_3,'\n')
print(sliced_multiples_3,'\n')
#Code for 2.b) here.
perfect_cubes = np.array([x**3 for x in range(100)])
print(perfect_cubes,'\n')
perfect_cubes_div32 = perfect_cubes % 32 == 0
print(perfect_cubes[perfect_cubes_div32])
[ 0 3 6 9 12 15 18 21 24 27 30 33 36 39 42 45 48 51
54 57 60 63 66 69 72 75 78 81 84 87 90 93 96 99 102 105
108 111 114 117 120 123 126 129 132 135 138 141 144 147 150 153 156 159
162 165 168 171 174 177 180 183 186 189 192 195 198 201 204 207 210 213
216 219 222 225 228 231 234 237 240 243 246 249 252 255 258 261 264 267
270 273 276 279 282 285 288 291 294 297]
[60 63 66 69 72 75 78 81 84 87]
[ 0 1 8 27 64 125 216 343 512 729
1000 1331 1728 2197 2744 3375 4096 4913 5832 6859
8000 9261 10648 12167 13824 15625 17576 19683 21952 24389
27000 29791 32768 35937 39304 42875 46656 50653 54872 59319
64000 68921 74088 79507 85184 91125 97336 103823 110592 117649
125000 132651 140608 148877 157464 166375 175616 185193 195112 205379
216000 226981 238328 250047 262144 274625 287496 300763 314432 328509
343000 357911 373248 389017 405224 421875 438976 456533 474552 493039
512000 531441 551368 571787 592704 614125 636056 658503 681472 704969
729000 753571 778688 804357 830584 857375 884736 912673 941192 970299]
[ 0 64 512 1728 4096 8000 13824 21952 32768 46656
64000 85184 110592 140608 175616 216000 262144 314432 373248 438976
512000 592704 681472 778688 884736]
Functions, Plotting, Loading Data
Functions are an important part of any language that supports them. They allow us to encapsulate and modularize our code for repeated and flexible reuse. Rather than rewriting or copying the same code to perform a common task for us, we can just call our function. Below, we define two functions, sine and cosine, that return the values for sine and cosine given an array of input values and a desired linear analog frequency (Hz).
def sine(t,f):
return np.array([np.sin(2*np.pi*f*time) for time in t])
def cosine(t,f):
return np.array([np.cos(2*np.pi*f*time) for time in t])
t = np.linspace(0,5,1000) #1000 evenly spaced points from 0 to 5
frequency = 2 #2 Hz frequency
s = sine(t,frequency)
c = cosine(t,frequency)
Now that we have some signals, let's try plotting them. There are many plotting interfaces we will use in the matplotlib module. The below code cell will show a few of them. Take note of how we label the axes, title, and provide a legend for our plots. You must always provide adequate labeling in this lab to make sure the graders know what your plots correspond to! Otherwise, you may lose points.
plt.figure()
plt.plot(t,s,label='sine') #first argument is x-axis, second is y-axis, label is useful if we want a legend
plt.plot(t,c,label='cosine') #labels are not always necessary
plt.title('Sine and Cosine with f = 2Hz') #title
plt.xlabel('Time (s)') #time
plt.ylabel('Signal Value') #ylabel
plt.legend() #legend that uses the "label" identifiers from above
<matplotlib.legend.Legend at 0x7fe056f142e8>
Having one plot with both signals is fine and good, but what if we want separate plots for each signal? Or what if we want them side-by-side as subplots?
plt.figure() #create separate figure
plt.plot(t,s)
plt.title('Sine')
plt.xlabel('Time (s)')
plt.ylabel('Sine Value')
plt.figure() #create separate figure
plt.plot(t,c,'r') #change color to red
plt.title('Cosine')
plt.xlabel('Time (s)')
plt.ylabel('Cosine Value')
#Subplots. Syntax looks different with multiple plots. Similar
#syntax can be used for single plots also.
plt.figure(figsize=(12,6)) #increase figure size for visibility, first argument is width, second is height
plt.subplot(121) # 1 row, 2 columns, first plot
plt.plot(t,s,'black')
plt.title('Sine')
plt.xlabel('Time (s)')
plt.ylabel('Sine Value')
plt.subplot(122) # 1 row, 2 columns, second plot
plt.plot(t,c,'g')
plt.title('Cosine')
plt.xlabel('Time (s)')
plt.ylabel('Cosine Value')
Text(0, 0.5, 'Cosine Value')