portions of this notebook were adapted from Mark Krumholz's public course notes
Each section 1-4 of this notebook contains an exercise that you must complete.
Jupyter notebooks have two kinds of cells. Markdown cells like this one have no labeling to the left of the cell, and, when executed, appear as ordinary text. Code cells like the one below have In [ ]: printed to the side of them and, when executed, a number appears inside of the brackets. To execute a cell of either type in Jupyter, hit shift + enter inside of that cell. Try it by double clicking on this cell and hitting shift + enter, and then do the same with the code cell below. Note that a markdown cell becomes pure text when you execute it, while a code cell spits out some output labelled Out [ ]:.
In [ ]:
1+2
The guts of Jupyter is the coding language Python, and any and all Python syntax will work inside of code cells. However, Jupyter code cells also have some extra capabilities, which we will talk about as needed in the future.
A key feature of Jupyter is that it integrates markdown (instructions/notes), code and output all in the same document. For this reason, many astronomers, including myself, use Jupyter as a sort of lab notebook rather than maintaining lab notes, code, and code output all in separate documents.
You should always keep in mind that cells within a Jupyter notebook can be executed and reexecuted in any order as this can sometimes get you into trouble. This feature is the reason why an executed code cell is numbered, so that you know what order the cells you see have been executed in. Note that this does not have to be linear from the top to the bottom of the document, but can jump around within it. Often, you will note a mistake or something that you want to try and will go back and change the code in an earlier cell. If you reexecute just this one cell, then any already executed cells that rely on variables defined in the reexecuted cell or its output will not be automatically updated.
For the purposes of this class, labs and homeworks should always be executable in a linear fashion, as you will be clearing all output before submitting them. For this reason, you should observe the following "best practices" for coding in notebooks.
Use descriptive variable labels (e.g. lum_stara = , mass_starb =) and create a new variable name each time you do a calculation. Do not reuse variable names or you risk getting a nonsense answer when you refer to that variable.
Before handing in any lab or homework, restart the kernel with the menu at the top of the notebook and execute the whole thing from start to finish once, making sure that all of your code output is how you want it. Note that restarting the kernel will clear the code output, not the code itself, so you will not be losing your work.
If you note a mistake in an earlier cell and go back to correct it, you should be careful to also reexecute any cells that depend on it. If you want to do a calculation similar to one that you executed in a previous cell, you should instead copy the contents of that cell into a new cell, rename the variables, and execute it again.
You have received an extensive Jupyter "cheat sheet", which I recommend you keep somewhere easily accessible so that you can refer to it often. If you ever need to reprint it, it can be found here. Use it now to execture the following tasks.
In [ ]:
a. Change this cell to a markdown cell
In [ ]:
1+2
1+3
This cell is junk
e. Make this text italic.
f. Make this text bold.
g. Make this text bold and italic.
h. Make this a heading.
i. Make this a hyperlink to your favorite webpage.
j. Insert the image logo.png here
k. Make the asterisks in the following sentence visible: I love astronomy
There are obviously lots of other things that you can do, but these are the basics that you'll use the most often.
In Jupyter notebooks, every cell is a command prompt into which we can enter python commands. One very basic way to use python is as a calculator.
Python knows the basic arithmetic operations plus (+), minus (-), times (*), divide (/), and raise to a power (**).
Sidenote Note that if you double click on this cell, you'll see that I've inserted a BACKSLASH () before the asterisks. That is because the asterisk has special meaning inside of a markdown cell. Single asterisks make an item italicized when you execute the cell, and double asterisks make it bold. This is NOT the case for code cells.
Execute all of the cells below and make sure that you understand what they're doing before moving on.
In [ ]:
2+3
In [ ]:
2*3
In [ ]:
2-3
In [ ]:
2/3
In [ ]:
2**3
It also understands parentheses, and follows the normal rules for order of operations:
In [ ]:
1+2*3
In [ ]:
1+2**3
In [ ]:
(1+2)*3
In [ ]:
In [ ]:
In [ ]:
In [ ]:
We can also define variables to store numbers, and we can perform arithmetic on those variables. Variables are just names for boxes that can store values, and on which you can perform various operations. For example, execute each of the cells below and make sure you understand the output:
In [ ]:
a=4
In [ ]:
a+1
In [ ]:
a/2
In [ ]:
a=a+1
In [ ]:
a
In [ ]:
a**2
Note that the line a=a+1 redefined the variable as one greater than it was before. This is a simple example of how not using a new variable name could get you into trouble (if you were to assume that a was still 4). Note too that lines in which you have deifned a variable do not have any output by default. If you want to check that the variable is properly assigned or see its value, you can type the variable name alone on a line below the variable assignment line.
In [ ]:
a=10**2
a
There's also a subtle but important point to notice here, which is the meaning of the equal sign. In mathematics, the statement that a = b is a statement that two things are equal, and it can be either true or false. In python, as in almost all other programming languages, a = b means something different. It means that the value of the variable a should be changed to whatever value b has. Thus the statement we made a = a + 1 is not an assertion (which is obviously false) that a is equal to itself plus one. It is an instruction to the computer to take the variable a, and 1 to it, and then store the result back into the variable a. In this example, it therefore changes the value of a from 4 to 5.
One more point regrading assignments: the fact that = means something different in programming than it does in mathematics implies that the statements a = b and b = a will have very different effects. The first one causes the computer to forget whatever is stored in a and replace it by whatever is stored in b. The second statement has the opposite effect: the computer forgets what is stored in b, and replaces it by whatever is stored in a.
The variable a that we have defined is an integer, or int for short. We can find this out by asking python using one of its many built-in functions, namely "type".
In [ ]:
b=5
In [ ]:
type(b)
Integers are exactly what they sound like: they hold whole numbers, and operations involving them and other whole numbers will always yield whole numbers. This is an important point in general programming, because variable types are not generally reassigned dynamically. So, for example, the operation 5/2 would yield 2 rather than 2.5 if 5 is an integer. In our case, however, we are using Python 3, which will automatically convert an integer to the next type of variable if the natural output is not a whole number, as below :
In [ ]:
b/2
If we assign this to a variable, we will have a new type of variable: a floating point number, or float for short. Note that after completing this operation, b is no longer an integer, as you can see using type.
unlike b, b/2 is not an integer, as you can see by creating a new variable, storing b/2 in it, and typing it.
In [ ]:
c = b/2
In [ ]:
c
In [ ]:
type(c)
A floating point variable is capable of holding real numbers.
Why have different types of variables for integers versus non-integer real numbers? In mathematics there is no need to make the distinction, of course: all integers are real numbers, so it would seem that there should be no reason to have a separate type of variable to hold integers. However, this ignores the way computers work. On a computer, operations involving integers are exact: 1 + 1 is exactly 2. However, operations on real numbers are necessarily inexact. I say necessarily because a real number is capable of having an arbitrary number of decimal places. The number pi contains infinitely many digits, and never repeats, but my computer only comes with a finite amount of memory and processor power.
Even rational numbers (like 2/3) run into this problem, because their decimal representation (or to be exact their representation in binary) may be an infinitely repeating sequence. Thus it is not possible to perform operations on arbitrary real numbers to exact precision. Instead, arithmetic operations on floating point numbers are approximate, with the level of accuracy determined by factors like how much memory one wants to devote to storing digits, and how much processor time one wants to spend manipulating them.
On most computers a python floating point number is accurate to about 1 in 10^15, but this depends on both the architecture and on the operations you perform. That's enough accuracy for many purposes, but there are plenty of situations (for example counting things) when we really want to do things precisely, and we want 1 + 1 to be exactly 2. That's what integers are there for.
A third type of very useful variable is strings, abbreviated str. A string is a sequence of characters, and one can declare that something is a string by putting characters in quotation marks (either " or ' is fine):
In [ ]:
d = "star"
In [ ]:
type(d)
The quotation marks are important here. To see why, try issuing the command without them:
In [ ]:
d = star
This is an error message, complaining that the computer doesn't know what star is. The problem is that, without the quotation marks, python thinks that star is the name of a variable, and complains when it can't find a variable by that name. Putting the quotation marks tells python that we mean a string, not a variable named star.
Obviously we can't add strings in the same sense that we add numbers, but we can still do operations on them. The plus operation concatenates two strings together:
In [ ]:
e = "planet"
In [ ]:
d+e
In addition to integers, floats, and strings, there are three other types of variables worth mentioning. The first is a Boolean variable (named after George Boole), which represents a logical value. Boolean variables can be either True or False:
In [ ]:
g = True
In [ ]:
type(g)
Boolean variables can have logic operations performed on them. In particular, there are three logical operators that we care about: not, and, and or. Play around with the cells below (defining new variables to test your theories on if you wish) and see if you can figure out what each one does, and then describe it below.
In [ ]:
not g
In [ ]:
h = False
In [ ]:
g and h
In [ ]:
g or h
The variables we have dealt with so far are fairly simple. They represent single values. However, for scientific or numeric purposes we often want to deal with large collections of numbers. The basic tool for doing this is an array. Arrays are not part of the core python language, but they are part of a very commonly used code library called numpy. In order to invoke or load numpy in this environment, you will need to "import" it.
In [ ]:
import numpy
Once loaded, you can take advantage of any numpy function (like the "type" function that is part of the python core language) by typing numpy.functionname. For example, the square root (sqrt) function:
In [ ]:
numpy.sqrt(16)
As we will use numpy a lot, it is convenient to import it under a shorter name, as below. This can be anything really, but it's easiest to use something that will be easy to remember. The standard short name for numpy is np
In [ ]:
import numpy as np
In [ ]:
np.pi
OK back to arrays, which besides built in mathematical functions like square roots, exponentials, etc., are one of the most common uses of the numpy library.
Formally, an array is a collection of objects that all have the same type: a collection of integers, or floats, or bools, or anything else. In the simplest case, these are simply arranged into a numbered list, one after another. Think of an array as a box with a bunch of numbered compartments, each of which can hold something. For example, here is a box with eight compartments.
In python, and in many (but not all) computer languages, we start counting at 0, so that the first compartment is compartment 0, the second is compartment 1, and so forth. We can create an array in python in a few different ways. One is just to explicitly list the elements we want it to contain. The syntax is straightforward:
In [ ]:
x = np.array([10,11,12,13,14,15,16,17])
This puts the values we specified into the appropriate compartments in the box. The array x can be thought of like this:
note that there is no output here, because again we are defining a variable. You can see it and check the stored values by typing the variable name.
In [ ]:
x
If you type the array you've defined, you'll also see that is is a special new kind of numpy variable.
In [ ]:
type(x)
For those of you who have used python before, note that an array is different from a list (defined simply with square brackets, e.g. a = [1,2,3,4,5]). There are some subtle differences between the two, but the principal practical difference is that you can do arithmetic operations on arrays, but not lists.
In this case the array we have created is an array of integers, since each of the entries we specified was an integer. If one of them had been a floating point number (for example if we changed the 10 to 10.0), then the array would have been an array of floats, meaning that EACH entry would have been made a float. Remember the rule: every element of an array must have the same type, so if you make one them into a float, then all of them will be floats.
Another way to create an array is to simply give the number of elements, and set them all to be zero initially. This can be done with the zeros command:
In [ ]:
y = np.zeros(8)
y
The zeros command creates an array with the specified number of elements, and sets them all to zero. Note that they are all floats (you can tell from the decimal point after each zero), which is what the zeros command creates by default. If you want some other data type, you can specify it using an option built into the zeros command called "dtype", which allows you to specify the data type you want:
In [ ]:
yint = np.zeros(8, dtype='int')
yint
In [ ]:
np.zeros?
The analogous numpy function "ones" creates an array where every element is 1. Here too, the default data type is float.
In [ ]:
z = np.ones(8)
z
A third and very useful way to create arrays is using the arange command. This creates an array with specified starting and ending values, and with a specified step size. For example:
In [ ]:
w = np.arange(0,16,2)
w
The first argument to arange is the starting value, the second is the ending value (notice that this is exclusive, not inclusive, so in the above example the array z does not contain the number 16), and the final entry is the step size.
Again, because all the entries were integers, the array consists of integers. If one of them had been a float, the array would have contained floats.
Note that one can omit the final entry in arange, in which case the step size defaults to a value of 1. One can also omit the second entry (for example just say arange(8)), in which case the array created starts with 0 as the first entry, has a step size of 1, and contains the number of entries specified in the argument. Thus arange(8) creates an array that contains the numbers 0 to 7.
In [ ]:
np.arange(8)
So how do we deal with arrays? What can we do with them?
One way to deal with an array is to interact with its elements individually. We can do that by specifying their index, which designates which compartment in the box we want to access. We can then print out values, alter them, or do any other operation on them we could do with a simple number. The way we index an array is by putting a number in square brackets after it. For example:
In [ ]:
x[2]
In [ ]:
type(x[2])
In [ ]:
x[2]*2
In [ ]:
x[2] = 32
x
In the examples above, we printed out the value stored in compartment number 2 of the array x, we checked the type of value stored in that compartment (in this case, a 64 bit integer), we multiplied it by 2 to get a result, and then we assigned a different value (32) to that compartment, erasing its original value.
So why are arrays more useful than just individual variables? There are a number of reasons, but one of the most important is called broadcasting, meaning that we can access, perform arithmetic with, and alter multiple elements of an array at once. We specify parts of an array by giving a range of indices in the square brackets, separated by a colon. For example,
In [ ]:
x[1:4]
Here the number before the colon specifies the starting index, and the number after the colon specifies the ending index; this is exclusive, not inclusive, so the ending index element is not included. If we omit the number before the colon, the region of the array we are referring to is assumed to start from element 0, and if we omit the number after the colon, it is assumed to end at the last element, as below:
In [ ]:
x[:4]
In [ ]:
x[4:]
We can also specify that we want not every element, but every 2nd element, or every 3rd element, etc., within our specified range. For example:
In [ ]:
x[::2]
Here the empty space before the first colon says to start at element 0, the empty space between the first and second colons says to end at the end of the array, and the number 2 after the colon says to access every 2nd element, rather than every element, which is the default.
Finally, array indices can be specified either as positive numbers, which count from the beginning, or as negative numbers, which count from the end. The last element is -1, the second to last is -2, and so forth, as shown below:
This is convenient, for example, if one wants to refer to all but the last 2 elements of an array:
In [ ]:
x[:-2]
The really useful thing is that one can use this capability to perform mathematical operations or assignments to multiple elements of an array at once. Several examples are given below. Execute the cell, examine the output, and make sure you understand what happened to the array before moving on.
In [ ]:
x-10
In [ ]:
x[1:4]-10
In [ ]:
x[1:4]=-10
x
In [ ]:
x[::2]=5
x
One can also do operations involving two arrays with the same number of elements. These operations are performed element by element. For example:
In [ ]:
arr1 = np.arange(10)
In [ ]:
arr2 = np.arange(10)*2
In [ ]:
arr1 + arr2
In this example, x is filled with 0, 1, 2, ... 9, and y is filled with 0, 2, 4, ... 18. When we add them, we get 0, 3, 6, ... 27. The arrays are added together one element at a time, producing an output array that has the same number of elements as the two inputs.
These capabilities for manipulating multiple elements at once are very convenient for manipulating large groups of numbers.
The arrays we've played with so far are one-dimensional, meaning that they look like a series of boxes laid out in a line. However, arrays can be multidimensional as well. For example:
In [ ]:
x2d = np.array([[10,11,12,13,14,15,16,17], [20,21,22,23,24,25,26,27], [30, 31, 32, 33, 34, 35, 36, 37]])
x2d
You can think of this as a box with multiple rows of compartments:
The zeros and ones commands also work in multiple dimensions. You just have to specify the size of each dimension:
In [ ]:
y2d = np.zeros((3,7))
y2d
Similarly, one can index multi-dimensional arrays just by indexing each dimension in turn. Execute each of the cells below and make sure you understand what it is doing.
In [ ]:
x2d[0,0]
In [ ]:
x2d[0,:]
In [ ]:
x2d[:,1]
In [ ]:
x2d[:,1:4]
Arrays can of course have more than two dimensions as well. In fact, there is no limit to how many dimensions they can have.
You can also do useful things like find the maximum and minimum values in arrays, and their means and totals:
In [ ]:
np.amin(x2d)
In [ ]:
np.amax(x2d)
In [ ]:
np.mean(x2d)
In [ ]:
np.sum(x2d)
Much of astronomy is image manipulation, and images are essentially very large arrays (large enough that it no longer becomes practical to print them to the window as we've been doing here). Upon importing an array (e.g. an astronomical image) or after manipulating it, you will often want to know some of its basic properties. There are several built-in python functions that are particularly useful for this, and you call them by typing the variable name + the operator name, as below.
In [ ]:
x2d.ndim
In [ ]:
x2d.shape
In [ ]:
x2d.size
In [ ]:
x2d.dtype
Follow the steps below.
1) Create a 10 x 10 numpy integer array and set it equal to a variable. Print it to output once you're done populating it as described below.
2) copy this array into a new variable, then in the new array, multiply each number in the third column by two
3) set the second to last element in each row of the new array equal to 0, and print the new array as output
4) multiply your original array by this new array and print the output
In [ ]:
#execute this cell before beginning so that the array will be more readable when you print it
np.set_printoptions(precision=3, suppress=True, linewidth=120)
In [ ]:
In addition to the capabilities we've just gone through, arrays can do far more. For a full list, see http://docs.scipy.org/doc/numpy/reference/arrays.ndarray.html.
This semester, you'll be doing most of your lab work in Jupyter notebooks, and submitting them for grading through the course Moodle site.
When you're done with a notebook, click "restart & clear all output" from the kernel menu and then "close and halt" from the file menu. Then to shut down the notebook server, go back to the original window where you started things up and type ctrl-C. You will be asked for confirmation, and if you type y, the server will then shut down.
In [1]:
#this cell sets up the formatting for exercise and sidebar cells
from IPython.core.display import HTML
def css_styling():
styles = open("../custom.css", "r").read()
return HTML(styles)
css_styling()
Out[1]: