In [95]:
%%html
<style>
.annotate {color: tomato}
</style>

A Crash Course in Python

Here is a short Python tutorial meant to set you quickly on the path of doing Computational Physics right away, and eventually learning some more Python as you go along. This is not supposed to be comprehensive, but instead is intended to introduce you to the main concepts that are most important for our tasks. If you have never used Python before, you would want to supplement this with some sort of beginner tutorial. If you have already got acquainted with Python, this will refresh your knowledge and help you understand how exactly is Python is used for solving physics problems.

There is no computer language specific to Computational Physics. Researcher have used a variety of programming tools as a mean toward achieving a goal. The computer science aspect was never the focus, and many times a language has been adopted just because was new, full of promise, and maybe because physicists are driven by the desire to explore and experiment. Programming in Computational Physics has been done in Pascal, Basic, C, C++ and Java, each coming with a set of advantages and disadvantages. It might be no coincidence that the first programming language was invented exactly for helping us to tell computers how to solve physics and math problem. The name of this language is FORTRAN, it means literally FORmula TRANslation, and has been in use since 1960's because is quite fast and direct. We don't know yet if Python's popularity will fade away some day, but the skills and concepts on which it is build are quite universal across programming languages, and your time and effort invested in learning it will not be wasted, even if a new programming language will pop up some day.

Guido van Rossum named his invention Python at the beginning of 1990 as an allusion to the irreverent British TV comedy "Monty Python's Flying Circus". The ideals he was striving for in the desing of the new programming language are expressed in a Zen form, and embedded directly in the language. For example:

Beautiful is better than ugly. Explicit is better than implicit. Simple is better than complex.

Run the following code to read all aphorisms (this is one of the eastern eggs hidden in Python):

In [80]:
import this
print("".join([this.d.get(c, c) for c in this.s]))
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

What Python is?

Python, like the shell, is an interpreted high level REPL that can be used to automatize, facilitate and make easy everything that a computer can do from art or engineering, to surf the web (without a browser), calculate your taxes, write poetry, make a movie or do math. Python has gained popularity because it has been widely adopted by Data Scientists and researchers in Machine Learning. As a result, Python and all associated tools are very well documented, with tutorials and complete manuals.

Python is easy to learn, simple to use, and enormously powerful. Python runs on an amazing range of hardware, from weeny underpowered Arduino's and stamp size microcontrollers, to world fastest supercomputers.

Currently, two main version of Python are in circulation: Python 2.X and Python 3.X, with X the subversion number. These version have significant difference, and an old Python 2 code will most likely not run as you would expect on more recent version of Python 3, but the differences are minor and can be fixed with some effort. Python 2 is not supported, anymore and no serious programs are written using it. It is recommended that all new pieces of Python to be written using Python 3.

Python is a procedural language, meaning that most results can be obtained by applying functions, eventually chained one after another, to the input. Object oriented programming style is also possible. In fact, almost everything in Python is an object that encapsulate attributes and methods, and supports inheritance and polymorphism.

What Python is not?

The Python language has a small minimal set of keywords and does not place many strict restrictions on the data types in a program and it has very relaxed rules with respect to memory allocation and management. As a result, Python programs are clean and easy to understand. They almost read like English. This flexibility comes with a price, however. Python itself is not particularly fast and it can not run concurrently on multicore CPU's. The solution for this problem is to used Python as a glue language. Python has powerful and easy to use facilities to integrate binary code written in other language for efficiency. For example, instead of using native Python sluggish mathematical facilities, one can use the numpy library that has vector operations written explicitly as pieces of Fortran and C code, and offered to the user as Python functions. The code below multiplies two one million dimensional vectors using native Python instruction and numpy specialized code. Don't worry if you don't understand all the commands here, although everything is pretty intuitive. The important thing is that numpy is more than 100 times faster.

In [82]:
import random
N = 1000000
A = B = []
for k in range(N):
    A.append(random.random())
    B.append(random.random())

AdotB = 0
%time for k in range(N):  AdotB += A[k]*B[k]
print(AdotB)

import numpy as np
An = np.array(A)
Bn = np.array(B)
%time fast_AdotB = np.dot(An, Bn)
print(fast_AdotB)
CPU times: user 335 ms, sys: 0 ns, total: 335 ms
Wall time: 335 ms
333217.7275894949
CPU times: user 21 ms, sys: 845 µs, total: 21.8 ms
Wall time: 2.88 ms
666058.3377440467

This "gluing" capacity is really what sets Python apart from other interpreted languages (as opposed to compiled ones), explaining its wild popularity.

So Python is not the solution for everything, by itself, but it makes it easy to be extended in many possible ways to satisfy a large sets of needs. So it is not surprising that even Quantum Computers can be programmed in Python.

Python is an interpreted language, meaning that a program is not compiled as a whole into binary machine instructions. Instead, commands are compiled on the fly, one after the other in the order in which they appear in the source code. This makes it quick to start, but eventually slow in the long run if some commands need to be compiled repeatedly.

First things first

Python is organized as sequence of statements that are executed in order, and the next statement is not started before the one before has not return an answer.

Many languages use curly braces as a way to delimit and indicate monolithic blocks of code. Python uses indentation:

In [ ]:
# The pound sign marks the start of a comment. Python itself
# ignores the comments, but they are helpful for anyone reading the code.
# ";" does not have to end a command unless there are two or more in a line
#
for i in [1, 2, 3, 4, 5]:         #white space can make code looking better
    print(i)                      #first line in "for i" block
    for j in [1, 2, 3, 4]: 
        print(j)                  #first line in "for j" block
        print(i+j)                #last  line in "for j" block
    print(i*i)                    #last  line in "for i" block
print("done looping")

These rules make Python code very readable, but you need to be careful with formatting.

Warning\ For many languages there is no difference is one uses spaces or tabs for indentation. Python considers tabs and spaces as different indentation, and the code will not run if you mix the two. As a simplifying rule, you should always use spaces, never tabs. Editors can be configured to insert spaces when Tab is pressed.

Whitespace is ignored inside parentheses and brackets, which can be helpful for long-winded computations:

In [ ]:
long_winded_computations = (1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 12 + 13 
                           + 14 + 15 + 16)

or for making code easier to read:

In [ ]:
list_of_lists = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
easier_to_read_list_of_lists = [[1, 2, 3],
                                [4, 5, 6],
                                [7, 8, 9]]

Long line can also be continued with a backslash:

In [89]:
two_plus_three = 2 + \
                 3

The first thing to look when Python gives an error is to check if indentation is correct and ":" is placed correctly after the for loops or if statements. You should be specially careful when copying and pasting code because the white space could mean different things for Python.

When stuck, help is available at many sites, and also from within Python with the command help(), where you put between parentheses the command for which we need to find details. For example, with help(print) we can find out how to use the print command. Similarly, help(input) shows us how to get input from the keyboard.

Variables and Assignments

Quantities of interest in physics have dimensions, units, prefixes and uncertainty. For Python, quantities are just numbers or sets of numbers like vectors and matrices. Physics equations need to be first transformed into a dimensionless form before introduced into the computer to be stored somewhere in the memory. A variable is a label for that piece of memory that holds our quantity. The statement:

In [ ]:
x = 1

introduces the quantity 1 and gives it's location in memory the name x with the assignment statement indicated by the symbol "=". The value at the address indicated by x can be retrieved unless x is re-assign or x disappears from Python scope. Note that the symbol "=" does NOT represent an equality. A better representation for assignment statement would be "<-", but that is illegal in Python, although the language "R" understands it.

The name for a variable can be as long as you want and can contain letters, numbers and even the underscore symbol "_", but they can not start with a number, contain other symbols, or spaces. Python distinguishes between upper and lower case letters, meaning that x and X are two different variables pointing to two different values. Greek letters can be used in Jupyter notebooks with a LaTeX-like notation. For example typing "\alpha" and pressing Tab will generate the greek letter $\alpha$, that can be use as legal name for a variable. However, this facility, called Unicode symbols, may be harder to use from the Terminal shell, or when you write code in a different text editor

The programs we write to solve physics problems contain a large number of variables representing the values of many different things, keeping them straight in your head can be a challenge. In the long run, you should spend a little bit of time to plan and think about naming your variables in a meaningful and way that can describe what they represent. If you want to represent a quantity of energy in your program, that variable should be called energy. Naming it just e might be confusing: would that be the number 'e', the base of natural logarithm? For more complex quantities you can use the underscore symbol "_" to create variables with names that have more than one word, like angular_velocity, or planks_constant. You can use comments right before introducing a new variable to explain the meaning of the name.

Type of a variable

The regions of memory to where variables point hold information representing different kinda of quantities, that require different amounts of storage (in terms of bytes) and that are operated by the processor with different rules. The most elementary types, of the following kinds:

  • Integers
  • Float
  • Complex
  • String: represents some text that can be printed on the screen, transferred to a file or over the network. All the information over the internet is transmitted as text.
  • Boolean: represent one of the two values: True or False and is useful when taking decisions. As a boolean, any value is evaluated to True is it has some sort of content, except when a number is 0.

The type of a variable can change as a Python program runs. For example, x=1 followed by x=1.0 changes the type of x from an integer to a float. Later on it can even change to x=1.0 + 0j. Mathematically, they all represent the number 1, however, for the computer they are different. Run the following code to see the difference:

In [90]:
x = 1; y = 1.0; z = 1.0 + 0j
print(type(x), type(y), type(z), sep=", ")
<class 'int'>, <class 'float'>, <class 'complex'>

Variables can be converted to and from different type by "casting". For example the function int() can be used to transform other types into integer types: int("1") and int(1.2) will both result in producing the number one, represented explicitly as an integer. Note that this function can be used to get the integer part of a real number because it removes all decimals. A more precise way to obtain this is to use functions floor and ceiling.

Python knows a number of compound data types, used to group together other values. The most versatile is the list, which can be written as a list of comma-separated values (items) between square brackets. Lists might contain items of different types, but usually the items all have the same type.

Warning\ Elements are identified by an index that start from 0. Therefore the first element in a list is squares[0]. A negative index starts counting backwards from the end of the list.

In [74]:
squares = [1, 4, 9, 16, 25]
squares
Out[74]:
[1, 4, 9, 16, 25]
In [75]:
print(squares[0]," ... ", squares[-1])
1  ...  25

A string variable stores text in the form of strings of characters that can be letter, digit, punctuation, symbols and so on. To indicate a string value one uses different kinds of quotation marks. The simplest form is, for example: x = "This is a string". This statement assigns, or re-assigns if x was used to point to something else, to x the value "This is a string", which can be though as the sequence of characters "T","h","i","s"," ","i","s"." ","a"," ","s","t","r","i","n","g". In this way, x[3] would represent the 4th character "s" in the string x. The variable y = "1.234" is not that same with as a floating-point variable with the value 1.234, since one can not do mathematical operations in the normal sense. Here is an example:

In [91]:
A = "1.234"
B = 1.234
print(3*A)
print(3*B)
print(A," = ", B)
1.2341.2341.234
3.702
1.234  =  1.234

Operators

As you have seen in the previous example, variables and literal constants, such as the number 3, can be combined to form expressions that produce results, which can be in turn assigned to variables. This resembles the way we do math on paper, and most things should work as you expected. For example, the order of operations, first multiplications, then additions, or the way parentheses are used to prioritize operations, are conventional. However there are some subtleties that you need to be aware of, because probably operations with variables are the most useful feature of Python when solving physics problems.

The result of an operation between two variables depends on their types. The general rule is that if the operands have the same type, the result will of the same type, except when diving two integers, the result may be float, as you expect mathematically. Other cases may not be so clear. For example, the result of 4.0/2.0 is a float, because Python gives preference to float representation over integer.

In the example below, variable x and y hold values with different types:

In [92]:
x, y = "1", "2"; print(x+y)
x, y = 1, 2; print(x+y)
x, y = 1, "2"; print(x+y)
12
3
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-92-b3887e49b5e6> in <module>
      1 x, y = "1", "2"; print(x+y)
      2 x, y = 1, 2; print(x+y)
----> 3 x, y = 1, "2"; print(x+y)

TypeError: unsupported operand type(s) for +: 'int' and 'str'

The results of the first two additions make sense. But the third one will fail and Python will indicate an error telling you that it does not know how to an integer to a string, and will refuse to execute any command after the offending one. Note the compressed way of assigning variables, and also the use of ; to place multiple statements on the same line. Just be sure, you can always execute some tests over an interactive Python session, and check if the result matches your expectations.

Some operators between numbers may surprise you:

  • Raising a number to a power is NOT written with ^, but one uses the double star ** symbol, or the function pow().
  • the operator ^ is the logical bitwise operator XOR setting each bit in the result if only one of the two bits is 1
  • The function sqrt(), trigonometric functions and constants, are not defined in the core of Python, but are part of the standard 'math' module, which has to be imported before using them. The example below calculates $\sin(\pi^2)$. Note that trigonometric functions require arguments in radians.
In [94]:
import math
x = math.sin(math.sqrt(math.pi**2))
print(x)
1.2246467991473532e-16
  • x//y gives the integer part of x divided by y, meaning that x is divided by y and the result is rounded to the nearest integer.
  • x%y does x modulo y, which means the remainder of the division x/y. For example 14%3 gives 2, because 14 divided by 3 gives 4-remainder-2. The modulo operation is particularly useful for telling if a number is even or odd, or when a number is divisible by another, the value of n%m is zero if n is divisible by m.
  • x & y does not mean x AND y, when x and y have boolean values. Instead you need to use x && y, because simple & operator is the bitwise AND operator which sets each bit of the result to 1 if one pf the two bits is 1.

Expressions can be as long as you wish combining sub-expressions the same way you may write it on paper. Remember to insert explicity the multiplication symbol *, since xy or x y do not result in the multiplication x*y. An expressions such as x = a + 2*b - 0.5*(2.618**c + 2/7) will do exactly what you expect algebraically. On the other hand the following will NOT work

In [ ]:
2*x = y

This expression will not result in x being assign half of the value of y, and will generate an error message because the right hand side of the assignment operator "=" has to be an expression that results in a variable that can be assigned a value.

Mathematically, the Python statement x = x + 1 does not make sense. Python firstly evaluates the left hand side expression and then assigns the result to the variable on the right hand side, which could be any variable, including ones that were eventually used on the left hand side. So this statement results in incrementing the value held by x. Python does not know how to solve equations by default. This code below

x = 0 print(x) x = x**2 - 2 print(x)

does not solve the equation $x = x^2 -2$ and sets x to solutions $x=2$ or $x=-1$. Instead it calculates $x^2-2 = 0^2 - 2= -2$, sets x to this new value, and prints it to the screen.

Python, as some other languages offers some shortcuts for most used operations. This makes Python more efficient and the code easier to understand. For example, x += 1 uses the increment operator +=, with the same effect as the more longer command x = x + 1. Similarly, one can use -=, *=, /= or even %=.

Example solved problem

Here is a Python solution for a very simple physics problem that illustrates the principles of CP and Python rules.

The problem is as follows. A ball is dropped from a tower of heigh h. It has zero initial velocity and accelerates downward under gravity. The challenge is to write a program that asks the user to enter the height in meters of the tower and a time interval t in seconds, then prints on the screen the elevation of the ball above the ground at time t after it is dropped. Ignore the air resistance this time.

The steps involved are the following. First, we will use input statements to get the values of h and t from the user. Second, we will calculate how far the ball falls during the given time, using the standard kinematics formula $s=\frac 12 gt^2$, with $g = 9.81$ms$^{-2}$, the standard value for acceleration due to gravity. Third, we print the height above the ground after time t, which is equal to the total height of the tower minus the traveled distance, or h-s

In [ ]:
h = float(input("Enter the height of the tower in meters:"))
t = float(input("Enter the time interval in seconds:"))
s = 9.81*t**2/2
print(f"the height of the ball is {h-s:4.2f} meters")

Here we printed a formatted string, which starts with f" ... " that can interpolate values enclosed within curly brackets {} and use formatting hints, here is to print in a floating point form 4 characters long with 2 digits after the decimal point. Of course, there are several other ways to represent numbers. Python gives you total power in the way numbers are written to the output. The value itself is not modified in any way, retaining its original number of significant digits, only the printed representation can be customized.

Notice that the result can be negative, which means that the ball would have fallen to below ground level if that were possible, although in practice the ball would hit the ground first. Thus a negative value indicates that the ball hits the ground before time t. This program could be improved to check the sign of h-s and inform the user appropriately about what happens.

The constant 9.81 is written explicitly in the program. It would be better if we define a variable holding this value at the beginning of the program g = 9.81 and write the calculation in a symbolic form as s = 0.5*g*t**2. In this way our program is neater, easier to read and understand than a row of digits, and prevents typos if the constant need to be used in multiple places in the program. It is good practice to collect and define all the constants, and other known quantities, in an initialization section of your program, right after importing the needed modules.

Flow Control and Logic

Python uses the usual flow control statements known from other languages, with some twists.

The if statement is used to execute different blocks of code depending on the truth value of a given condition expression. Here is an example

In [ ]:
x = int(input("Enter a whole number no greater than ten:"))
if x > 10:
    print("You entered a number greater than ten.")
    print("Let me fix that for you.")
    x = 10
elif x == 10:
    print("You enter exaclty ten. Nothing to be done here")
else:
    print("Well done!")
print("The number x is: ",x)

Pay attention to the indentation of the blocks of code for each branch of decision, and also the use of ":" to indicate the end of the condition expression after the keywords if, elif or else. This expression is evaluated to result in a boolean True/False value. The boolean value can be combined with logical operator and, or and not. For example you can use:

In [ ]:
if x > 10 or x <1:
    print("Your number is either too big or too small")

The while statement is a variation of the if statement. For example:

In [ ]:
x = int(input("Enter a whole number no greater than ten:"))
while x > 10:
    print("This is greater than ten. Try again")
    x = int(input("Enter a whole number no greater than ten:"))
print("The number x is: ",x)

Like for the if statement, the condition is evaluated for its truth value, and the following indented block of code is executed repeatedly unless the condition is False. If the condition never fails, the code will loop forever. You can still break the loop, though, with Ctr-C. The while statement can be used to force the program to advance only if given conditions are met.

The statements break and continue will explicitly break or make the loop go on, if needed. This only applies to the current state of the loop. After a break Python interrupts the current going through the block of code and jumps back to the while statement.

Example code

The Fibonacci numbers are the sequence of integers in which each is the sum of the previous two, with the first two number being both 1. Therefore the first few members are of the sequence are 1,1,2,3,5,8,13,21. We write Python code to calculate the Fibonacci numbers up to 1000.

In [ ]:
f1 = 1
f2 = 1
while f1 <= 1000:
    print(f1)
    fnext = f1 + f2
    f1 = f2
    f2 = fnext

Observe how this program works. At all times the variables f1 and f2 store the two most recent elements of the sequence. At each step we calculate the next element in the sequence by summing f1 and f2 and store the result in the intermediary place fnext. Then we update the values of f1 and f2 because now fnext becomes the newest member of the sequence and f2 becomes the trailing number f1.

The for statement in Python differs a bit from what you may be used to in C or Java. Rather than always iterating over an arithmetic progression of numbers, or giving the user the ability to define both the iteration step and halting condition , Python’s for statement iterates over the items of any sequence (a list or a string), in the order that they appear in the sequence. For example:

In [76]:
words = ['cat', 'window','defenestrate']
for w in words:
    print(w, len(w))
cat 3
window 6
defenestrate 12

When you need to iterate over a sequence of numbers, the build-in function range() is very useful because it represents just that. Note that the range(n) starts with 0 and ends with n-1

In [77]:
for i in range(5):
    print(i)
0
1
2
3
4

Defining your own function

Python does not have a function to calculate Fibonacci numbers. Let's create a function that writes the Fibonacci series to an arbitrary boundary:

In [79]:
def fib(n):    # write Fibonacci series up to n
     """Print a Fibonacci series up to n."""
     a, b = 0, 1
     while a < n:
         print(a, end=' ')
         a, b = b, a+b
     print()
# Now call the function we just defined:
fib(2000)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 

The keyword def introduces a function definition. It must be followed by the function name and the parenthesized list of formal parameters. The statements that form the body of the function start at the next line, and must be indented.

break and continue statements work in for similarly with the way they work in while statement.

Programming Assignment\ Write code in a new notebook where you calculate the sum of the first 1000 prime numbers. Use markup cell to document and explain in detail the operation of your code. Your program should print this result at the end:

sum = 3 682 913


More resources for learning Python:

In [ ]: