I am working on the file "PwNT Notebook 2.ipynb".
The first task is:
Notice that this function has no return line.  The function terminates automatically at the end of its scope.
The function also uses a slick Python string manipulation called string substitution.  This has changed between Python 2.x and 3.x, and as usual we are following Python 2.x syntax.
String substitution allows you to place variable numbers in a string by putting placeholders like %d within the string, and then placing the list of variables after the string (as a tuple).  Here are some examples to get used to the syntax, since we will use this often in this lesson.
Can you write Python code for it?

print "My favorite number is %d"%(17)  # The % symbol substitutes 17 for %d.

print "%d + %d = %d"%(13,12,13+12)


I am working on the file "PwNT Notebook 2.ipynb".
The first task is:
Now we can use multiple assignment to turn three lines of code into one line of code.  For the remainder variable is only used temporarily before its value is given to the divisor variable.  Using multiple assignment, the three lines
python
    remainder = dividend % divisor
    dividend = divisor
    divisor = remainder
can be written in one line,
python
    dividend, divisor = divisor, dividend % divisor # Evaluations on the right occur before any assignments!
Our newly shortened GCD function looks like this.
python
def GCD(a,b):
    dividend = a
    divisor = b
    while divisor != 0:   # Recall that != means "not equal to".
        dividend, divisor = divisor, dividend % divisor
    return abs(dividend)
The next trick involves the while loop.  The usual syntax has the form while <boolean>:.  But if while is followed by a numerical type, e.g. while <int>:, then the scope of the while loop will execute as long as the number is nonzero!  Therefore, the line
python
while divisor != 0:
can be replaced by the shorter line
python
while divisor:
This is truly a trick.  It probably won't speed anything up, and it does not make your program easier to read for beginners.  So use it if you prefer communicating with experienced Python programmers!  Here is the whole function again.
python
def GCD(a,b):
    dividend = a
    divisor = b
    while divisor:   # Executes the scope if divisor is nonzero.
        dividend, divisor = divisor, dividend % divisor
    return abs(dividend)
The next optimization is a bit more dangerous for beginners, but it works here.  In general, it can be dangerous to operate directly on the arguments to a function.  But in this setting, it is safe, and makes no real difference to the Python interpreter.  Instead of creating new variables called dividend and divisor, one can manipulate a and b directly within the function.  If you do this, the GCD function can be shortened to the following.
Can you write Python code for it?

def GCD(a,b):
    while b:   # Recall that != means "not equal to".
        a, b = b, a % b
    return abs(a)

# Try it out.  Try it on some big numbers and see how quick it runs!


I am working on the file "PwNT Notebook 2.ipynb".
The first task is:
The broad syntax of the function should be familiar.  We have created a function called is_even with one argument called n.  The body of the function uses the control statement if n%2 == 0:.  Recall that n%2 gives the remainder after dividing n by 2.  Thus n%2 is 0 or 1, depending on whether n is even or odd.  Therefore the boolean n%2 == 0 is True if n is even, and False if n is odd.
The next two lines (the first print and return statements) are within the scope of the if <boolean>: statement, as indicated by the colon and the indentation.  The if <boolean>: statement tells the Python interpreter to perform the statements within the scope if the boolean is True, and to ignore the statements within the scope if the boolean is False.
Putting it together, we can analyze the code.
python
if n%2 == 0:
        print "%d is even."%(n)
        return True
If n is even, then the Python interpreter will print a sentence of the form n is even.  Then the interpreter will return (output) the value True and the function will terminate.  If n is odd, the Python interpreter will ignore the two lines of scope.
Often we don't just want Python to do nothing when a condition is not satisfied.  In the case above, we would rather Python tell us that the number is odd.  The else: control statement tells Python what to do in case the if <boolean>: control statement receives a False boolean.  We analyze the code
python
    else:
        print "%d is odd."%(n)
        return False
The print and return commands are within the scope of the else: control statement.  So when the if statement receives a false signal (the number n is odd), the program prints a sentence of the form n is odd. and then returns the value False and terminates the function.
The function is_even is a verbose, or "talkative" sort of function.  Such a function is sometimes useful in an interactive setting, where the programmer wants to understand everything that's going on.  But if the function had to be called a million times, the screen would...
Can you write Python code for it?

def is_even(n):
    return (n%2 == 0)

is_even(17)


I am working on the file "PwNT Notebook 2.ipynb".
The first task is:
The placeholder %d is meant to hold the place of an integer.  For floating point numbers, use %f instead.  This will impose a bit of rounding before printing.  One can modify the placeholder to change the amount of digits displayed.  Here is the official reference for placeholder syntax and string substitutions.  We will only use the most basic features.
Can you write Python code for it?

print "pi is approximately %f"%(3.14159265) # This uses a default precision for display.

print "pi is approximately %.3f"%(3.14159265) # This sets 3 digits after the decimal point.


I am working on the file "PwNT Notebook 2.ipynb".
The first task is:
The while loop syntax begins with while <boolean>: and the following indented lines comprise the scope of the loop.  If the boolean is True, then the scope of the loop is executed.  If the boolean is True again afterwards, then the scope of the loop is executed again.  And again and again and so on.
This can be a dangerous process!  For example, what would happen if you made a little typo and the last line of the while loop read current_value = current_value + 1?  The numbers would increase and increase... and the boolean current_value > 0 would always be True.  Therefore the loop would never end.  Bigger and bigger numbers would scroll down your computer screen.
You might panic under such a circumstance, and maybe turn your computer off to stop the loop.  Here is some advice for when your computer gets stuck in such a neverending loop:

Back up your work often.  When you're programming, make sure everything else is saved just in case.
Save your programming work (use "Save and checkpoint" under the "File" menu) often, especially before running a cell with a loop for the first time.
If you do get stuck in a neverending loop, click on "Kernel... Interrupt".  This will often unstick the loop and allow you to pick up where you left off.
On a Mac, you might try a "Force Quit" of the Python process, using the Activity Manager.

Now, if you're feeling brave, save your work, change the while loop so that it never ends, and try to recover where you left off.  But be aware that this could cause your computer to freeze or behave erratically, crashing your browser, etc.  Don't panic... it won't break your computer permanently.
The neverending loop causes two problems here.  One is with your computer processor, which will be essentially spinning its wheels.  This is called busy waiting, and your computer will essentially be busy waiting forever.  The other problem is that your loop is printing more and more lines of text into the notebook.  This could easily crash your web browser, which is trying to store...
Can you write Python code for it?

def Euclidean_algorithm(a,b):
    dividend = a
    divisor = b
    while divisor != 0:   # Recall that != means "is not equal to".
        quotient = dividend // divisor
        remainder = dividend % divisor
        print "%d = %d (%d) + %d"%(dividend, quotient, divisor, remainder)
        dividend = divisor
        divisor = remainder

Euclidean_algorithm(133, 58)

Euclidean_algorithm(1312331323, 58123123)


I am working on the file "PwNT Notebook 2.ipynb".
The first task is:
This code is essentially optimal, if one wishes to execute the Euclidean algorithm to find the GCD of two integers.  It almost matches the GCD code in a standard Python library.  It might be slightly faster than our original code -- but there is a tradeoff here between execution speed and readability of code.  In this and the following lessons, we often optimize enough for everyday purposes, but not so much that readability is lost.
Exercises

Modify the is_prime function by using a while loop instead of for j in range(2,n):.  Hint:  the function should contain the lines j = 2 and while j < n: and j = j + 1 in various places.  Why might this be an improvement from the for loop?

Modify the Euclidean_algorithm function to create a function which returns the number of steps that the Euclidean algorithm requires, i.e., the number of divisions-with-remainder.

Create a function which carries out division with minimal remainder.  In other words, given integers a,b, the function expresses a = q(b) + r, where r is a positive or negative integer of magnitude bounded by b/2.  Use such a function to create a new Euclidean algorithm function which uses minimal remainder.

What GCD(a,b) function do you think strikes the best balance between efficiency and readability?
Can you write Python code for it?

# Use this space to work on the exercises.


I am working on the file "PwNT Notebook 2.ipynb".
The first task is:
This is particular useful for self-referential assignments, because as for ordinary assignment, the right side is evaluated first and then bound to the variables on the left side.  For example, after the line above, try the line below.  Use print statements to see what the values of the variables are afterwards!
Can you write Python code for it?

x,y = y,x #  Guess what this does!

print "x =",x
print "y =",y


I am working on the file "PwNT Notebook 2.ipynb".
The first task is:
To conclude this lesson, we put everything together to create a long(ish) function to solve linear Diophantine equations.  We want this function to be smart enough to respond when an equation has no solutions, and to describe all solutions when they exist.
The first part of the function solve_LDE is the same as the hop and skip function above.  But rather than expressing $GCD(a,b)$ as $ax + by$, the function uses the GCD to determine the existence and the general form of a solution to $ax + by = c$.  The formula for the general form comes from An Illustrated Theory of Numbers, Chapter 1, Corollary 1.25.
Can you write Python code for it?

def solve_LDE(a,b,c):
    '''
    Describes all of the solutions to the linear Diophantine equation
    ax + by = c.  There are either no solutions or infinitely many solutions.
    Prints a description of the solution set, and returns None if there are no solutions
    or returns a single solution if one exists.
    '''
    u = a # We use u instead of dividend.
    v = b # We use v instead of divisor.
    u_hops, u_skips = 1,0 # u is built from one hop (a) and no skips.
    v_hops, v_skips = 0,1 # v is built from no hops and one skip (b).
    while v != 0:   # We could just write while v:
        q = u // v  # q stands for quotient.
        r = u % v  # r stands for remainder.  So u = q(v) + r.

        r_hops = u_hops - q * v_hops  # Tally hops
        r_skips = u_skips - q * v_skips  # Tally skips

        u,v = v,r  # The new dividend,divisor is the old divisor,remainder.
        u_hops, v_hops = v_hops, r_hops # The new u_hops, v_hops is the old v_hops, r_hops
        u_skips, v_skips = v_skips, r_skips # The new u_skips, v_skips is the old v_skips, r_skips

    g = u # The variable g now describes the GCD of a and b.

    if c%g == 0:  # When GCD(a,b) divides c...
        d = c/g
        x = d * u_hops
        y = d * u_skips  # Now ax + by = c is a specific solution!
        print "%d x + %d y = %d if and only if "%(a, b, c)
        print "x = %d + %d n and y = %d - %d n, for some integer n."%(x,b/g,y,-a/g)
        return x,y
    else:  # When GCD(a,b) does not divide c...
        print "There are no solutions to %d x + %d y = %d,"%(a,b,c)
        print "because GCD(%d, %d) = %d, which does not divide %d."%(a,b,g,c)
        return None

solve_LDE(102,45,3)

solve_LDE(72,100,17)


I am working on the file "PwNT Notebook 2.ipynb".
The first task is:
Try out the hop_and_skip code on some integers of your choice.  Does it behave correctly?  Check the results, using Python as a calculator.  Does it run quickly for large integers?
Can you write Python code for it?


I am working on the file "PwNT Notebook 2.ipynb".
The first task is:
Try the is_prime function on bigger numbers -- try numbers with 4 digits, 5 digits, 6 digits.  Where does it start to slow down?  Do you get any errors when the numbers are large?  Make sure to save your work first, just in case this crashes your computer!
Can you write Python code for it?


I am working on the file "PwNT Notebook 2.ipynb".
The first task is:
We can refine our code in a few ways.  First, note that the quotient variable is never used!  It was nice in the verbose version of the Euclidean algorithm, but plays no role in finding the GCD.  Our refined code reads
python
def GCD(a,b):
    dividend = a
    divisor = b
    while divisor != 0:   # Recall that != means "not equal to".
        remainder = dividend % divisor
        dividend = divisor
        divisor = remainder
    return abs(dividend)
Now there are two slick Python tricks we can use to shorten the code.  The first is called multiple assignment.  It is possible to set the values of two variables in a single line of code, with a syntax like below.
Can you write Python code for it?


I am working on the file "PwNT Notebook 2.ipynb".
The first task is:
When you run the code block, you probably didn't see anything happen.  But you have effectively taught your computer a new trick, increasing the vocabulary of commands it understands through the Python interpreter.  Now you can use the square command as you wish.
Can you write Python code for it?


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
Another modest improvement is the following.  In the code above, the program counts the terms in sequences like 9,15,21,27,..., in order to set them to False.  This is accomplished with the length command len(flags[p*p::2*p]).  But that length computation is a bit too intensive.  A bit of algebraic work shows that the length is given formulaically in terms of p and n by the formula:
$$len = \lfloor \frac{n - p^2 - 1}{2p} \rfloor + 1$$
(Here $\lfloor x \rfloor$ denotes the floor function, i.e., the result of rounding down.)  Putting this into the code yields the following.
Can you write Python code for it?

def isprime_list(n):
    '''
    Return a list of length n+1
    with Trues at prime indices and Falses at composite indices.
    '''
    flags = [True] * (n+1)  # A list [True, True, True,...] to start.
    flags[0] = False  # Zero is not prime.  So its flag is set to False.
    flags[1] = False  # One is not prime.  So its flag is set to False.
    flags[4::2] = [False] * ((n-2)/2)
    p = 3
    while p <= sqrt(n):  # We only need to sieve by p is p <= sqrt(n).
        if flags[p]:  # We sieve the multiples of p if flags[p]=True.
            flags[p*p::2*p] = [False] * ((n-p*p-1)/(2*p)+1) # Sieves out multiples of p, starting at p*p.
        p = p + 2 # Try the next value of p.

    return flags

%timeit sum(isprime_list(10**6))  # How much did this speed it up?


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
As a last example of data visualization, we use matplotlib to produce a histogram of the prime gaps.
Can you write Python code for it?

max(primegaps)  # The largest prime gap that appears!

%matplotlib inline
plt.figure(figsize=(12, 5))  #  Makes the resulting figure 12in by 5in.
plt.hist(primegaps, bins=range(1,115)) #  Makes a histogram with one bin for each possible gap from 1 to 114.
plt.ylabel('Frequency')
plt.xlabel('Gap size')
plt.grid(True)
plt.title('The frequency of prime gaps, for primes up to 1 million')
plt.show()


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
Changing list slices
Not only can we extract and study terms or slices of a list, we can change them by assignment.  The simplest case would be changing a single term of a list.
Can you write Python code for it?

print L

L[5] = 'Bacon!'

print L  # What do you think L is now?

print L[2::3] # What do you think this will do?


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
Combined with the isprime_list function, we can produce long lists of primes.
Can you write Python code for it?


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
Exercises

Create a list L with L = [1,2,3,...,100] (all the numbers from 1 to 100).  What is L[50]?

Take the same list L, and extract a slice of the form [5,10,15,...,95] with a command of the form L[a:b:c].

Take the same list L, and change all the even numbers to zeros, so that L looks like [1,0,3,0,5,0,...,99,0].  Hint:  You might wish to use the list [0]*50.

Try the command L[-1::-1] on a list.  What does it do?  Can you guess before executing it?  Can you understand why?  In fact, strings are lists too.  Try setting L = 'Hello' and the previous command.

Sieve of Eratosthenes
The sieve of Eratosthenes (hereafter called "the Sieve") is a very fast way of producing long lists of primes, without doing repeated primality checking.  It is described in more detail in Chapter 2 of An Illustrated Theory of Numbers.  The basic idea is to start with all of the natural numbers, and successively filter out, or sieve, the multiples of 2, then the multiples of 3, then the multiples of 5, etc., until only primes are left.
Using list slicing, we can carry out this sieving process efficiently.  And with a few more tricks we encounter here, we can carry out the Sieve very efficiently.
The basic sieve
The first approach we introduce is a bit naive, but is a good starting place.  We will begin with a list of numbers up to 100, and sieve out the appropriate multiples of 2,3,5,7.
Can you write Python code for it?

primes = range(100) # Let's start with the numbers 0...99.


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
Finally, the multiples of 7, starting at 49 (the first multiple of 7 greater than 7 that's left!)
Can you write Python code for it?

primes[49::7] = [None] * len(primes[49::7])  # The right side is a list of Nones, of the necessary length.
print primes # What have we done?


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
I've chosen function names with "fast" and "slow" in them.  But what makes them faster or slower?  Are they faster than the original?  And how can we tell?
Python comes with a great set of tools for these questions.  The simplest (for the user) are the time utilities.  By placing the magic %timeit before a command, Python does something like the following:

Python makes a little container in your computer devoted to the computations, to avoid interference from other running programs if possible.
Python executes the command lots and lots of times.
Python averages the amoung of time taken for each execution.

Give it a try below, to compare the speed of the functions is_prime (the original) with the new is_prime_fast and is_prime_slow.  Note that the %timeit commands might take a little while.
Can you write Python code for it?

%timeit is_prime_fast(1000003)

%timeit is_prime_slow(1000003)

%timeit is_prime(1000003)


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
If $n$ is a prime number, then the is_prime(n) function will iterate through all the numbers between $2$ and $n-1$.  But this is overkill!  Indeed, if $n$ is not prime, it will have a factor between $2$ and the square root of $n$.  This is because factors come in pairs:  if $ab = n$, then one of the factors, $a$ or $b$, must be less than or equal to the square root of $n$.  So it suffices to search for factors up to (and including) the square root of $n$.
We haven't worked with square roots in Python yet.  But Python comes with a standard math package which enables square roots, trig functions, logs, and more.  Click the previous link for documentation.  This package doesn't load automatically when you start Python, so you have to load it with a little Python code.
Can you write Python code for it?


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
If you look carefully at the list of booleans, you will notice a True value at the 2nd index, the 3rd index, the 5th index, the 7th index, etc..  The indices where the values are True are precisely the prime indices.  Since booleans take the smallest amount of memory of any data type (one bit of memory per boolean), your computer can carry out the isprime_list(n) function even when n is very large.
To be more precise, there are 8 bits in a byte.  There are 1024 bytes (about 1000) in a kilobyte.  There are 1024 kilobytes in a megabyte.  There are 1024 megabytes in a gigabyte.  Therefore, a gigabyte of memory is enough to store about 8 billion bits.  That's enough to store the result of isprime_list(n) when n is about 8 billion.  Not bad!  And your computer probably has 4 or 8 or 12 or 16 gigabytes of memory to use.
To transform the list of booleans into a list of prime numbers, we create a function called where.  This function uses another Python technique called list comprehension.  We discuss this technique later in this lesson, so just use the where function as a tool for now, or read about list comprehension if you're curious.
Can you write Python code for it?

def where(L):
    '''
    Take a list of booleans as input and
    outputs the list of indices where True occurs.
    '''
    return [n for n in range(len(L)) if L[n]]


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
If, in this three-argument syntax, the first or second argument is absent, then the slice starts at the beginning of the list or ends at the end of the list accordingly.
Can you write Python code for it?

L  # Just a reminder.  We haven't modified the original list!

L[:9:3]  # Start at zero, go up to (but not including) 9, by steps of 3.

L[2: :3] # Start at two, go up through the end of the list, by steps of 3.

L[::3]  # Start at zero, go up through the end of the list, by steps of 3.


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
It still looks almost linear, but there's a visible downward bend here.  How can we see this bend more clearly?  If the graph were linear, its equation would have the form $\pi(x) = mx$ for some fixed slope $m$ (since the graph does pass through the origin).  Therefore, the quantity $\pi(x)/x$ would be constant if the graph were linear.
Hence, if we graph $\pi(x) / x$ on the y-axis and $x$ on the x-axis, and the result is nonconstant, then the function $\pi(x)$ is nonlinear.
Can you write Python code for it?

m_values = pix_values[1:] / x_values[1:]  # We start at 1, to avoid a division by zero error.

%matplotlib inline
plt.plot(x_values[1:], m_values)
plt.title('The ratio $\pi(x) / x$ as $x$ varies.')
plt.xlabel('x')
plt.ylabel('$\pi(x) / x$')
plt.grid(True)
plt.show()


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
Just like the range command, list slicing can take an optional third argument to give a step size.  To understand this, try the command below.
Can you write Python code for it?


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
Let's analyze the graphing code a bit more.  See the official pyplot tutorial for more details.
python
%matplotlib inline
plt.plot(x_values, y_values)
plt.title('The graph of $y = x^2$')  # The dollar signs surround the formula, in LaTeX format.
plt.ylabel('y')
plt.xlabel('x')
plt.grid(True)
plt.show()
The first line contains the magic %matplotlib inline.  We have seen a magic word before, in %timeit.  Magic words can call another program to assist.  So here, the magic %matplotlib inline calls matplotlib for help, and places the resulting figure within the notebook.
The next line plt.plot(x_values, y_values) creates a plot object based on the data of the x-values and y-values.  It is an abstract sort of object, behind the scenes, in a format that matplotlib understands.  The following lines set the title of the plot, the axis labels, and turns a grid on.  The last line plt.show renders the plot as an image in your notebook.  There's an infinite variety of graphs that matplotlib can produce -- see the gallery for more!  Other graphics packages include bokeh and seaborn, which extends matplotlib.
Analysis of the prime counting function
Now, to analyze the prime counting function, let's graph it.  To make a graph, we will first need a list of many values of x and many corresponding values of $\pi(x)$.  We do this with two commands.  The first might take a minute to compute.
Can you write Python code for it?

x_values = numpy.linspace(0,1000000,1001) # The numpy array [0,1000,2000,3000,...,1000000]
pix_values = numpy.array([primes_upto(x) for x in x_values])  # [FUNCTION(x) for x in LIST] syntax


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
Let's push it a bit further.  How many primes are there between 1 and 1 million?  We can figure this out in three steps:

Create the isprime_list.
Use where to get the list of primes.
Find the length of the list of primes.

But it's better to do it in two steps.

Create the isprime_list.
Sum the list!  (Note that True is 1, for the purpose of summation!)
Can you write Python code for it?

sum(isprime_list(1000000))  # The number of primes up to a million!

%timeit isprime_list(10**6)

%timeit sum(isprime_list(10**6))


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
List terms and indices
Notice that the entries in a list can be of any type.  The above list L has some integer entries and some string entries.  The list B has boolean entries.  Lists are ordered in Python, starting at zero.  One can access the $n^{th}$ entry in a list with a command like L[n].
Can you write Python code for it?


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
Next the multiples of 5, starting at 25 (the first multiple of 5 greater than 5 that's left!)
Can you write Python code for it?

primes[25::5] = [None] * len(primes[25::5])  # The right side is a list of Nones, of the necessary length.
print primes # What have we done?


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
Now let's filter out the multiples of 2, starting at 4.  This is the slice primes[4::2]
Can you write Python code for it?

primes[4::2] = [None] * len(primes[4::2])  # The right side is a list of Nones, of the necessary length.
print primes # What have we done?


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
Now let's graph the function $y = x^2$ over the domain $-2 \leq x \leq 2$ for practice.  As a first step, we use numpy's linspace function to create an evenly spaced set of 11 x-values between -2 and 2.
Can you write Python code for it?

x_values = numpy.linspace(-2,2,11)  # The argument 11 is the *number* of terms, not the step size!
print x_values
type(x_values)


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
Now let's return to our primegaps data set.  It contains all the gap-sizes for primes up to 1 million.
Can you write Python code for it?

print len(primes)
print len(primegaps)  # This should be one less than the number of primes.


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
Now we have a function is_prime_fast(n) that is speedy for numbers n in the trillions!  You'll probably start to hit a delay around $10^{15}$ or so, and the delays will become intolerable if you add too many more digits.  In a future lesson, we will see a different primality test that will be essentially instant even for numbers around $10^{1000}$!
Exercises

To check whether a number n is prime, you can first check whether n is even, and then check whether n has any odd factors.  Change the is_prime_fast function by implementing this improvement.  How much of a speedup did you get?

Use the %timeit tool to study the speed of is_prime_fast for various sizes of n.  Using 10-20 data points, make a graph relating the size of n to the time taken by the is_prime_fast function.

Write a function is_square(n) to test whether a given integer n is a perfect square (like 0, 1, 4, 9, 16, etc.).  How fast can you make it run?  Describe the different approaches you try and which are fastest.

List manipulation
We have already (briefly) encountered the list type in Python.  Recall that the range command produced a list.  For example, range(10) produces the list [0,1,2,3,4,5,6,7,8,9].  You can also create your own list by a writing out its terms, e.g. L = [4,7,10].
Here we work with lists, and a very Pythonic approach to list manipulation.  With practice, this can be a powerful tool to write fast algorithms, exploiting the hard-wired capability of your computer to shift and slice large chunks of data.  Our application will be to implement the Sieve of Eratosthenes, producing a long list of prime numbers (without using any is_prime test along the way).
We begin by creating two lists to play with.
Can you write Python code for it?


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
Now we have two lists of the same length.  The gaps in the original list L are the differences between terms of the same index in the two new lists.  One might be tempted to just subtract, e.g., with the command L[1:] - L[:-1], but subtraction is not defined for lists.
Fortunately, by converting the lists to numpy arrays, we can use numpy's term-by-term subtraction operation.
Can you write Python code for it?

L[1:] - L[:-1]  # This will give a TypeError.  You can't subtract lists!

numpy.array(L[1:]) - numpy.array(L[:-1])  # That's better.  See the gaps in the list [1,3,7,20] in the output.


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
Now, to "filter", i.e., to say that a number is not prime, let's just change the number to the value None.
Can you write Python code for it?

primes[0] = None # Zero is not prime.
primes[1] = None # One is not prime.
print primes # What have we done?


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
That is certainly not constant!  The decay of $\pi(x) / x$ is not so different from $1 / \log(x)$, in fact.  To see this, let's overlay the graphs.  We use the numpy.log function, which computes the natural logarithm of its input (and allows an entire array as input).
Can you write Python code for it?

%matplotlib inline
plt.plot(x_values[1:], m_values, label='$\pi(x)/x$')  # The same as the plot above.
plt.plot(x_values[1:], 1 / numpy.log(x_values[1:]), label='$1 / \log(x)$')  # Overlay the graph of 1 / log(x)
plt.title('The ratio of $\pi(x) / x$ as $x$ varies.')
plt.xlabel('x')
plt.ylabel('$\pi(x) / x$')
plt.grid(True)
plt.legend()  # Turn on the legend.
plt.show()


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
That should be pretty fast!  It should be under 100 ms (one tenth of one second!) to determine the primes up to a million, and on a newer computer it should be under 50ms.  We have gotten pretty close to the fastest algorithms that you can find in Python, without using external packages (like SAGE or sympy).  See the related discussion on StackOverflow... the code in this lesson was influenced by the code presented there.
Exercises

Prove that the length of range(p*p, n, 2*p) equals $\lfloor \frac{n - p^2 - 1}{2p} \rfloor + 1$.

A natural number $n$ is called squarefree if it has no perfect square divides $n$ except for 1.  Write a function squarefree_list(n) which outputs a list of booleans:  True if the index is squarefree and False if the index is not squarefree.  For example, if you execute squarefree_list(12), the output should be [False, True, True, True, False, True, True, True, False, False, True, True, False].  Note that the False entries are located the indices 0, 4, 8, 9, 12.  These natural numbers have perfect square divisors besides 1.

Your DNA contains about 3 billion base pairs.  Each "base pair" can be thought of as a letter, A, T, G, or C.  How many bits would be required to store a single base pair?  In other words, how might you convert a sequence of booleans into a letter A,T,G, or C?  Given this, how many megabytes or gigabytes are required to store your DNA?  How many people's DNA would fit on a thumb-drive?

Data analysis
Now that we can produce a list of prime numbers quickly, we can do some data analysis:  some experimental number theory to look for trends or patterns in the sequence of prime numbers.  Since Euclid (about 300 BCE), we have known that there are infinitely many prime numbers.  But how are they distributed?  What proportion of numbers are prime, and how does this proportion change over different ranges?  As theoretical questions, these belong the the field of analytic number theory.  But it is hard to know what to prove without doing a bit of...
Can you write Python code for it?

primes = where(isprime_list(1000000))

len(primes) # Our population size.  A statistician might call it N.

primes[-1]  # The last prime in our list, just before one million.

type(primes) # What type is this data?

print primes[:100] # The first hundred prime numbers.


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
The location of an entry is called its index.  So at the index 3, the list L stores the entry three.  Note that the same entry can occur in many places in a list.  E.g. [7,7,7] is a list with 7 at the zeroth, first, and second index.
Can you write Python code for it?


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
This command imports the square root command (sqrt) from the package called math.  Now you can find square roots.
Can you write Python code for it?


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
This continues the strange (for beginners) Python convention of starting at the first number and ending just before the last number.  Compare to range(3,7), for example.
The command L[0:5] can be replaced by L[:5] to abbreviate.  The empty opening index tells Python to start at the beginning.  Similarly, the command L[5:11] can be replaced by L[5:].  The empty closing index tells Python to end the slice and the end.  This is helpful if one doesn't know where the list ends.
Can you write Python code for it?


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
This is pretty close!  It seems like prime numbers are about evenly distributed between red and blue.  Their remainder after division by 4 is about as likely to be 1 as it is to be 3.  In fact, it is proven that asymptotically the ratio between the number of red primes and the number of blue primes approaches 1.  However, Chebyshev noticed a persistent slight bias towards blue primes along the way.
Some of the deepest conjectures in mathematics relate to the prime counting function $\pi(x)$.  Here $\pi(x)$ is the number of primes between 1 and $x$ (inclusive).  So $\pi(2) = 1$ and $\pi(3) = 2$ and $\pi(4) = 2$ and $\pi(5) = 3$.  One can compute a value of $\pi(x)$ pretty easily using a list comprehension.
Can you write Python code for it?

def primes_upto(x):
    return len([p for p in primes if p <= x]) # List comprehension recovers the primes up to x.

primes_upto(1000)  # There are 168 primes between 1 and 1000.


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
This isn't too bad!  It takes a fraction of a second to identify the primes up to a million, and a smaller fraction of a second to count them!  But we can do a little better.
The first improvement is to take care of the even numbers first.  If we count carefully, then the sequence 4,6,8,...,n (ending at n-1 if n is odd) has the floor of (n-2)/2 terms.  Thus the line flags[4::2] = [False] * ((n-2)/2) will set all the flags to False in the sequence 4,6,8,10,...  From there, we can begin sieving by odd primes starting with 3.
The next improvement is that, since we've already sieved out all the even numbers (except 2), we don't have to sieve out again by even multiples.  So when sieving by multiples of 3, we don't have to sieve out 9,12,15,18,21,etc..  We can just sieve out 9,15,21,etc..  When p is an odd prime, this can be taken care of with the code flags[p*p::2*p] = [False] * len(flags[p*p::2*p]).
Can you write Python code for it?

def isprime_list(n):
    '''
    Return a list of length n+1
    with Trues at prime indices and Falses at composite indices.
    '''
    flags = [True] * (n+1)  # A list [True, True, True,...] to start.
    flags[0] = False  # Zero is not prime.  So its flag is set to False.
    flags[1] = False  # One is not prime.  So its flag is set to False.
    flags[4::2] = [False] * ((n-2)/2)
    p = 3
    while p <= sqrt(n):  # We only need to sieve by p is p <= sqrt(n).
        if flags[p]:  # We sieve the multiples of p if flags[p]=True.
            flags[p*p::2*p] = [False] * len(flags[p*p::2*p]) # Sieves out multiples of p, starting at p*p.
        p = p + 2 # Try the next value of p.  Note that we can proceed only through odd p!

    return flags

%timeit sum(isprime_list(10**6))  # How much did this speed it up?


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
Time is measured in seconds, milliseconds (1 ms = 1/1000 second), microseconds (1 µs = 1/1,000,000 second), and nanoseconds (1 ns = 1/1,000,000,000 second).  So it might appear at first that is_prime is the fastest, or about the same speed.  But check the units!  the other two approaches are about a thousand times faster!  How much faster were they on your computer?
Can you write Python code for it?

is_prime_fast(10000000000037)  # Don't try this with `is_prime` unless you want to wait for a long time!


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
To carry out serious analysis, we will use the method of list comprehension to place our population into "bins" for statistical analysis.  Our first type of list comprehension has the form [x for x in LIST if CONDITION].  This produces the list of all elements of LIST satisfying CONDITION.  It is similar to list slicing, except we pull out terms from the list according to whether a condition is true or false.
For example, let's divide the (odd) primes into two classes.  Red primes will be those of the form 4n+1.  Blue primes will be those of the form 4n+3.  In other words, a prime p is red if p%4 == 1 and blue if p%4 == 3.  And the prime 2 is neither red nor blue.
Can you write Python code for it?

redprimes = [p for p in primes if p%4 == 1] # Note the [x for x in LIST if CONDITION] syntax.
blueprimes = [p for p in primes if p%4 == 3]

print redprimes[:20] # The first 20 red primes.
print blueprimes[:20] # The first 20 blue primes.

print "There are %d red primes and %d blue primes, up to 1 million."%(len(redprimes), len(blueprimes))


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
We can also implement this test with a while loop instead of a for loop.  This will save some memory (space), since it avoids the storage of the list range(2,n) of numbers.
Can you write Python code for it?

def is_prime(n):
    '''
    Checks whether the argument n is a prime number.
    Uses a brute force search for factors between 1 and n.
    '''
    j = 2
    while j < n:  # j will proceed through the list of numbers 2,3,...,n-1.
        if n%j == 0:  # is n divisible by j?
            print "%d is a factor of %d."%(j,n)
            return False
        j = j + 1  # There's a Python abbreviation for this:  j += 1.
    return True

is_prime(10001)

is_prime(101)


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
We can change a slice of a list with a single assignment, even when that slice does not consist of consecutive terms.  Try to predict what the following commands will do.
Can you write Python code for it?

print L  # Let's see what the list looks like before.

L[::2] = ['A','B','C','D','E','F']  # What was L[::2] before this assignment?

print L  # What do you predict?


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
We can change an entire slice of a list with a single assignment.  Let's change the first two terms of L in one line.
Can you write Python code for it?

L[:2] = ['Pancakes', 'Ham']  # What was L[:2] before?

print L # Oh... what have we done!

L[0]

L[1]

L[2]


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
We created an array of x-values as before.  But the creation of an array of y-values (here, called pix_values to stand for $\pi(x)$) probably looks strange.  We have done two new things!

We have used a list comprehension [primes_upto(x) for x in x_values] to create a list of y-values.
We have used numpy.array(LIST) syntax to convert a Python list into a numpy array.

First, we explain the list comprehension.  Instead of pulling out values of a list according to a condition, with [x for x in LIST if CONDITION], we have created a new list based on performing a function each element of a list.  The syntax, used above, is [FUNCTION(x) for x in LIST].  These two methods of list comprehension can be combined, in fact.  The most general syntax for list comprehension is [FUNCTION(x) for x in LIST if CONDITION].
Second, a list comprehension can be carried out on a numpy array, but the result is a plain Python list.  It will be better to have a numpy array instead for what follows, so we use the numpy.array() function to convert the list into a numpy array.
Can you write Python code for it?

type(numpy.array([1,2,3]))  # For example.


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
What have we done?  It is useful to try out this method on a short list.
Can you write Python code for it?

L = [1,3,7,20]  # A nice short list.

print L[:-1]
print L[1:]


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
What's left?  A lot of Nones and the prime numbers up to 100.  We have successfully sieved out all the nonprime numbers in the list, using just four sieving steps (and setting 0 and 1 to None manually).
But there's a lot of room for improvement, from beginning to end!

The format of the end result is not so nice.
We had to sieve each step manually.  It would be much better to have a function prime_list(n) which would output a list of primes up to n without so much supervision.
The memory usage will be large, if we need to store all the numbers up to a large n at the beginning.

We solve these problems in the following way.

We will use a list of booleans rather than a list of numbers.  The ending list will have a True value at prime indices and a False value at composite indices.  This reduces the memory usage and increases the speed.
A which function (explained soon) will make the desired list of primes after everything else is done.
We will proceed through the sieving steps algorithmically rather than entering each step manually.

Here is a somewhat efficient implementation of the Sieve in Python.
Can you write Python code for it?

def isprime_list(n):
    '''
    Return a list of length n+1
    with Trues at prime indices and Falses at composite indices.
    '''
    flags = [True] * (n+1)  # A list [True, True, True,...] to start.
    flags[0] = False  # Zero is not prime.  So its flag is set to False.
    flags[1] = False  # One is not prime.  So its flag is set to False.
    p = 2  # The first prime is 2.  And we start sieving by multiples of 2.

    while p <= sqrt(n):  # We only need to sieve by p is p <= sqrt(n).
        if flags[p]:  # We sieve the multiples of p if flags[p]=True.
            flags[p*p::p] = [False] * len(flags[p*p::p]) # Sieves out multiples of p, starting at p*p.
        p = p + 1 # Try the next value of p.

    return flags

print isprime_list(100)


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
You can also use Python to find the sum of a list of numbers.
Can you write Python code for it?

sum([1,2,3,4,5])

sum(range(100))  # Be careful.  This is the sum of which numbers?


I am working on the file "PwNT Notebook 3.ipynb".
The first task is:
You might notice that the format looks a bit different from a list.  Indeed, if you check type(x_values), it's not a list but something else called a numpy array.  Numpy is a package that excels with computations on large arrays of data.  On the surface, it's not so different from a list.  The numpy.linspace command is a convenient way of producing an evenly spaced list of inputs.
The big difference is that operations on numpy arrays are interpreted differently than operations on ordinary Python lists.  Try the two commands for comparison.
Can you write Python code for it?

[1,2,3] + [1,2,3]

x_values + x_values

y_values = x_values * x_values  # How is multiplication interpreted on numpy arrays?
print y_values


I am working on the file "PwNT Notebook 4.ipynb".
The first task is:
By having each list entry point to the same location in memory, Python avoids having to fill three blocks of memory with the same number 19.
Python objects can have methods attached to them.  Methods are functions which can utilize and change the data within an object.  The basic syntax for using methods is <object>.<method>().  Here are two examples to get started:  The keys and values of a dictionary can be recovered using the keys() and values() methods.
Can you write Python code for it?

nemo.keys()  # What are the keys of nemo?

nemo.values()  # What are the values of nemo?


I am working on the file "PwNT Notebook 4.ipynb".
The first task is:
Dictionaries can have values of any type, and their keys can be numbers or strings.  In this case, the keys are all strings, while the values include strings and integers.  In this way, dictionaries are useful for storing properties of different kinds -- they can be used to store [records](https://en.wikipedia.org/wiki/Record_(computer_science%29 ), as they are called in other programming languages.
An interlude on Python objects
We have discussed how Python stores data of various types:  int, bool, str, list, dict, among others.  But now seems like a good time to discuss the fundamental "units" which are stored:  these are called Python objects.  If you have executed the cells above, Python is currently storing a lot of objects in your computer's memory.  These objects include nemo and L.  Also L[0] is an object and nemo['age'] is an object.  Each of these objects are occupying a little space in memory.
We reference these objects by the names we created, like nemo and L.  But for internal purposes, Python assigns every object a unique ID number.  You can see an object's ID number with the id function.
Can you write Python code for it?


I am working on the file "PwNT Notebook 4.ipynb".
The first task is:
Exercises

A positive integer $n$ is called deficient/perfect/abundant according to whether the sum of its proper divisors is less than/equal to/greater than $n$ itself.  Among the numbers up to 10000, how many are deficient, perfect, and abundant?

If $f(n)$ is a function with natural number input and real output, define $F(n)$ to be the function given by the formula $F(n) = \sum_{i=0}^n f(i)$.  Create a function sumfun(f) which takes as input a function f and outputs the function F as described above.

Consider the function $f(n)$ which counts the number of positive divisors of $n$ which are not divisible by 4.  Verify that this is a multiplicative function, and implement it using mult_function.

Write a function foursquare(n) which counts the number of ways that a positive integer n can be expressed as a*a + b*b + c*c + d*d for integers a, b, c, d.  Hint:  loop the variables through integers between $-\sqrt{n}$ and $\sqrt{n}$.  Compare the values of foursquare(n) to the multiplicative function in the previous problem.

A positive integer is "square-free" if it has no square factors besides 1.  The Mobius function $\mu(n)$ is defined by $\mu(n) = 0$ if $n$ is not square-free, and otherwise $\mu(n) = 1$ or $\mu(n) = -1$ according to whether $n$ has an even or odd number of prime factors.  Verify that the Mobius function is multiplicative and implement it.  Try to reproduce the graph of the Mertens function $M(n)$ as described at Wikipedia's article on the Mertens conjecture.  (See the previous Python notebook for an introduction to matplotlib for creating graphs.)
Can you write Python code for it?

#  Use this space to work on the exercises.


I am working on the file "PwNT Notebook 4.ipynb".
The first task is:
Now let's re-implement the divisor sum function, using prime decomposition and the divisor_sum_pp function for prime powers.
Can you write Python code for it?

def divisor_sum(n):
    '''
    Computes the sum of the positive divisors of a
    positive integer n.
    '''
    D = decompose(n)  # We require the decompose function from before!
    result = 1
    for p in D.keys():
        result = result * divisor_sum_pp(p,D[p])
    return result

divisor_sum(15)

% timeit(divisor_sum(730)) # this probably runs faster than the previous version.


I am working on the file "PwNT Notebook 4.ipynb".
The first task is:
Now that we have a function to compute the prime decomposition of a positive integer, we write a function to recover a positive integer from such a prime decomposition.  The function is deceptively simple, since Python makes it easy to iterate through the keys of a dictionary.  Make sure that you understand every line.
Can you write Python code for it?

def recompose(D):
    '''
    If D is a dictionary with prime keys and natural values,
    this function outputs the product of terms of the form
    key^value.  In this way, it recovers a single number from a
    prime decomposition.
    '''
    N = 1
    for p in D.keys():  # iterate p through all the keys of D.
        N = N * (p ** D[p])  # Note that D[p] refers to the value (exponent) for the key p.
    return N

D = decompose(1000)
print D

recompose(D)  # This should recover 1000.

recompose({2:1, 3:1, 5:1, 7:1})  # What will this give?

# Use this space to experiment with decompose and recompose.


I am working on the file "PwNT Notebook 4.ipynb".
The first task is:
Now we exploit this function-as-object approach to create a Python function called mult_function.  Given a function f_pp(p,e), the function mult_function outputs the multiplicative function which coincides with f_pp on prime powers.  In other words, if f = mult_function(f_pp), then f(p**e) will equal f_pp(p,e).
Can you write Python code for it?

def mult_function(f_pp):
    '''
    When a function f_pp(p,e) of two arguments is input,
    this outputs a multiplicative function obtained from f_pp
    via prime decomposition.
    '''
    def f(n):
        D = decompose(n)
        result = 1
        for p in D:
            result = result * f_pp(p, D[p])
        return result

    return f


I am working on the file "PwNT Notebook 4.ipynb".
The first task is:
Observe that Python changed the order of the items in the list.  But it didn't move them around in memory!  The object 2 maintains the same ID number, and stays in the same place in memory.  But the list item L[0] points at 2 before reversing while L[3] points at 2 after reversing.  This kind of thing is confusing at first, but the general framework is <variable> points at <memory location>.  You choose the name of the variable and work with the variable directly.  Python labels each memory location with an ID number, and puts stuff in memory and retrieves values from memory according to your wishes.
Can you write Python code for it?

L.append(11) # Let's add another term to the list with the append(*) method.
print L

L.sort()  # Let's get this list back in order.
print L


I am working on the file "PwNT Notebook 4.ipynb".
The first task is:
Python dictionaries are structures built for data that have a key-value structure.  The keys are like indices.  But instead of numerical indices (0,1,2,etc.), the keys can be any numbers or strings (technically, any immutable type)!  Each key in the dictionary references a value, in the same way that each index of a list references an item.  The syntax for defining a dictionary is {key1:value1, key2:value2, key3:value3, ...}.  A first example is below.  You can also read the official tutorial for more on dictionaries.
Can you write Python code for it?

nemo = {'species':'clownfish', 'color':'orange', 'age':6}

nemo['color']  # The key 'color' references the value 'orange'.

nemo['age']  # Predict the result.

nemo[1]  # This yields a KeyError, since 1 is not a key in the dictionary.


I am working on the file "PwNT Notebook 4.ipynb".
The first task is:
Python won't be confused by this.  When we assigned x = 5, Python opened up a new memory slot for the number 5, and assigned x to refer to the number in this new slot.  Note that y still "points" at the old slot.  Python tries to be smart about memory, remembering where numbers are stored, and putting numbers into slots "under the hood" as it sees fit.
Can you write Python code for it?

id(3)  # Does Python remember where it put 3?

id(5)  # Does Python remember where it put 5?

id(4)  #  4 is probably not in memory before.  But now it is!

y = 5

id(y)  #  Did Python change the number in a slot?  Or did it point `y` at another slot?

id(L[2]) #  Python doesn't like to waste space.


I am working on the file "PwNT Notebook 4.ipynb".
The first task is:
Since functions are Python objects, it is possible to define a function which takes a function as input and outputs a function too!  You can pass a function as an input parameter to another function, just as if it were any other variable.  You can output a function with the return keyword, just as if it were another variable.  And you can define a new function within the scope of a function!
Here's a basic example as a warmup.
Can you write Python code for it?

def addone(x):  # Let's make a simple function.
    return x + 1  # It's not a very interesting function, is it.

addone(10) # Predict the result.

def do_twice(f):
    '''
    If a function f is input, then the output is the function
    "f composed with f."
    '''
    def ff(x):  # Defines a new function ff!
        return f(f(x)) # This is what ff does.

    return ff

addtwo = do_twice(addone)  # addtwo is a function!

addtwo(10)  # What is the result?


I am working on the file "PwNT Notebook 4.ipynb".
The first task is:
Since the divisor-counting function is multiplicative, we can implement it by applying mult_function to sigma0_pp.
Can you write Python code for it?

sigma0 = mult_function(sigma0_pp)

sigma0(100)  # How many divisors does 100 have?


I am working on the file "PwNT Notebook 4.ipynb".
The first task is:
Some more useful list methods can be found at the official Python tutorial.
Prime decomposition dictionaries
If $N$ is a positive integer, then $N$ can be uniquely decomposed into a product of primes.  Here "uniquely" means that $N$ has a unique expression of the form
$$N = 2^{e_2} 3^{e_3} 5^{e_5} \cdots$$
in which theexponents $e_2, e_3, e_5$, etc., are natural numbers (and only finitely many are nonzero).
A Python dictionary is well-suited to store the resulting prime decomposition.  For example, we might store the prime decomposition $2^3 3^2 7$ with the dictionary {2:3, 3:2, 7:1}.  The prime numbers which occur in the decomposition become the keys of the dictionary, and the natural number exponents becomes the values of the dictionary.
The functions below decompose a positive integer N into primes, storing the result in a dictionary.  The strategy is to repeatedly strip off (divide by) the smallest prime factor of a number, adjusting the dictionary along the way, until the number is reduced to 1.  The first function below finds the smallest prime factor of a number.
Can you write Python code for it?

from math import sqrt  # We'll want to use the square root.

def smallest_factor(n):
    '''
    Gives the smallest prime factor of n.
    '''
    if n < 2:
        return None # No prime factors!

    test_factor = 2 # The smallest possible prime factor.
    max_factor = sqrt(n) # we don't have to search past sqrt(n).

    while test_factor <= max_factor:
        if n%test_factor == 0:
            return test_factor
        test_factor = test_factor + 1  # This could be sped up.

    return n  # If we didn't find a factor up to sqrt(n), n itself is prime!

smallest_factor(105)

smallest_factor(1999**2)  # 1999 might be called the Prince of primes.

smallest_factor(11**3 * 13**9) # The result should be 11.

def decompose(N):
    '''
    Gives the unique prime decomposition of a positive integer N,
    as a dictionary with primes as keys and exponents as values.
    '''
    current_number = N  # We'll divide out factors from current_number until we get 1.
    decomp = {} # An empty dictionary to start.
    while current_number > 1:
        p = smallest_factor(current_number) # The smallest prime factor of the current number.
        if p in decomp.keys():  # Is p already in the list of keys?
            decomp[p] = decomp[p] + 1 # Increase the exponent (value with key p) by 1.
        else:  # "else" here means "if p is not in decomp.keys()".
            decomp[p] = 1  # Creates a new entry in the dictionary, with key p and value 1.
        current_number = current_number / p # Factor out p.
    return decomp

decompose(100) # What is the prime decomposition of 100?

decompose(56401910421778813463) # This should be quick.

#  Use this space to experiment a bit with the decompose function.


I am working on the file "PwNT Notebook 4.ipynb".
The first task is:
Sometimes we'll want to change a dictionary.  Perhaps we learn that nemo has gotten lost.
Can you write Python code for it?

nemo['status'] = 'lost'

id(nemo)

id('status')

id(nemo.keys()[1])  # Can you understand the result of this?

print nemo


I am working on the file "PwNT Notebook 4.ipynb".
The first task is:
The command nemo['status'] = 'lost' creates a new key in the dictionary called 'status' and assigns the value 'lost' to the key.  If we find nemo, then we can change the value.
Can you write Python code for it?


I am working on the file "PwNT Notebook 4.ipynb".
The first task is:
The output of the keys() and values() methods are lists.  As such, they are convenient for iteration and membership-testing.
Can you write Python code for it?

'color' in nemo.keys()

'taste' in nemo.keys()

'orange' in nemo.keys()  # Is 'orange' a key in the dictionary?

for k in nemo.keys():  # Iterates through the keys.
    print 'Nemo\'s',k,'is',nemo[k]  # \' is used to get a single-quote in a string.


I am working on the file "PwNT Notebook 4.ipynb".
The first task is:
There are a lot of interesting multiplicative functions.  We could implement each one by a two-step process as above:  implementing the function for prime powers, then defining a version for positive integers by using the decompose function.  But there's a shortcut for the second step, which brings in a very cool aspect of Python.
In Python, functions are Python objects.
Can you write Python code for it?

type(divisor_sum_pp) # Every object has a type.

id(divisor_sum_pp) # Yes, every object gets an ID number.


I am working on the file "PwNT Notebook 4.ipynb".
The first task is:
This sort of memory management can be helpful to avoid repetetion.  For example, consider a list with repetition.
Can you write Python code for it?

R = [19,19,19]

id(R)  # The list itself is an object.

id(R[0])  # The 0th item in the list is an object.

id(R[1])  # The 1st item in the list is an object.

id(R[2])  # The 2nd item in the list is an object.


I am working on the file "PwNT Notebook 4.ipynb".
The first task is:
We mentioned earlier that keys() and values() are methods attached to the object nemo, and methods are functions which are attached to Python objects.
Python objects often (and often by default!) have methods attached to them.  Every dictionary and every list in Python comes with attached methods.  Methods can be used to extract properties of objects or change them.  Here are examples of some list methods.
Can you write Python code for it?

L = [2,3,5,7]
print L # Let's remember what the list L is.

print L[0] # What is this?

id(L[0]) # What is the ID number of the 0th item in the list?

L.reverse() # The reverse() method changes L!
print L

L[0]  # We have definitely changed L.

L[3]  # The last item in the list L.

id(L[3]) # The ID number of the last item in the list L.


I am working on the file "PwNT Notebook 5.ipynb".
The first task is:
For Carmichael numbers, it turns out that finding a good witness is just as difficult as finding a factor.  Although Carmichael numbers are rare, they demonstrate that Fermat's Little Theorem by itself is not a great way to be certain of primality.  Effectively, Fermat's Little Theorem can often be used to quickly prove that a number is not prime... but it is not so good if we want to be sure that a number is prime.
The Miller-Rabin primality test will refine the Fermat's Little Theorem test, by cleverly taking advantage of another property of prime numbers.  We call this the ROO (Roots Of One) property:  if $p$ is a prime number, and $x^2 \equiv 1$ mod $p$, then $x \equiv 1$ or $x \equiv -1$ mod $p$.
Can you write Python code for it?

for x in range(41):
    if x*x % 41 == 1:
        print "%d squared is congruent to 1, mod 41."%(x)  # What numbers do you think will be printed?


I am working on the file "PwNT Notebook 5.ipynb".
The first task is:
How fast is our new is_prime function?  Let's give it a try.
Can you write Python code for it?


I am working on the file "PwNT Notebook 5.ipynb".
The first task is:
In fact, one can prove that at least 3/4 of the witnesses will detect the non-primality of any non-prime.  Thus, if you keep on asking witnesses at random, your chances of detecting non-primality increase exponentially!  In fact, the witness 2 suffices to check whether any number is prime or not up to 2047.  In other words, if $p < 2047$, then $p$ is prime if and only if Miller_Rabin(p,2) is True.  Just using the witnesses 2 and 3 suffice to check primality for numbers up to a million (1373653, to be precise, according to Wikipedia.)
The general strategy behind the Miller-Rabin test then is to use just a few witnesses for smallish potential primes (say, up to $2^{64}$).  For larger numbers, try some number $x$ (like 20 or 50) random bases.  If the tested number is composite, then the probability of all witnesses reporting True is is less than $1 / 4^x$.  With 50 random witnesses, the chance that a composite number tests as prime is less than $10^{-30}$.
Note that these are statements about conditional probability.  In more formal language,
$$\text{Prob} \left( \text{tests prime} \ \vert \ \text{ is composite} \right) < \frac{1}{4^{# \text{witnesses} } }.$$
As those who study medical testing know, this probability differs from the probability that most people care about:  the probability that a number is prime, given that it tests prime.  The relationship between the two probabilities is given by Bayes Theorem, and depends on the prevalence of primes among the sample.  If our sample consists of numbers of absolute value about $N$, then the prevalence of primes will be about $1 / \log(N)$, and the probability of primality given a positive test result can be approximated.
$$\text{Prob} \left( \text{ is prime } \ \vert \ \text{ tests prime } \right) > 1 - \frac{\log(N) - 1}{4^{# \text{witnesses}}}.$$
As one chooses more witnesses, this probability becomes extremely close to $1$.
Can you write Python code for it?

from mpmath import *
# The mpmath package allows us to compute with arbitrary precision!
# It has specialized functions for log, sin, exp, etc.., with arbitrary precision.
# It is probably installed with your version of Python.

def prob_prime(N, witnesses):
    '''
    Conservatively estimates the probability of primality, given a positive test result.
    N is an approximation of the size of the tested number.
    witnesses is the number of witnesses.
    '''
    mp.dps = witnesses # mp.dps is the number of digits of precision.  We adapt this as needed for input.
    prob_prime = 1 - (log(N) - 1) / (4**witnesses)
    print str(100*prob_prime)+"% chance of primality" # Use str to convert mpmath float to string for printing.

prob_prime(10**100, 50) # Chance of primality with 50 witnesses, if a 100-digit number is tested.


I am working on the file "PwNT Notebook 5.ipynb".
The first task is:
It is straightforward to modify Pingala's algorithm to compute exponents in modular arithmetic.  Just reduce along the way.
Can you write Python code for it?

def powmod_Pingala(base,exponent,modulus):
    result = 1
    bitstring = bin(exponent)[2:] # Chop off the '0b' part of the binary expansion of exponent
    for bit in bitstring: # Iterates through the "letters" of the string.  Here the letters are '0' or '1'.
        if bit == '0':
            result = (result*result) % modulus
        if bit == '1':
            result = (result*result * base) % modulus
    return result

powmod_Pingala(5,90,91)


I am working on the file "PwNT Notebook 5.ipynb".
The first task is:
It might seem tedious to carry out this "reduction mod m" at every step along the way.  But the advantage is that you never have to work with numbers much bigger than the modulus (m) if you can reduce modulo m at each step.
For example, consider the following computation.
Can you write Python code for it?

(3**999) % 1000  # What are the last 3 digits of 3 raised to the 999 power?


I am working on the file "PwNT Notebook 5.ipynb".
The first task is:
Let's compare the performance of our new modular exponentiation algorithm.
Can you write Python code for it?

%timeit powmod_Pingala(3,999,1000)  # Pingala's algorithm, modding along the way.

%timeit powermod_1(3,999,1000)  # Raise to the power, then mod, using Python built-in exponents.

%timeit powermod_2(3,999,1000)  #  Multiply 999 times, modding along the way.

%timeit pow(3,999,1000)  #  Use the Python built-in modular exponent.


I am working on the file "PwNT Notebook 5.ipynb".
The first task is:
Note that along the way, we carried out 11 multiplications (count the * symbols), and didn't have to remember too many numbers along the way.  So this process was efficient in both time and memory.  We just followed the BIT code.  The number of multiplications is bounded by twice the number of BITs, since each BIT requires at most two multiplications (squaring then multiplication by 5) to execute.
Why did we call the code a BIT code?  It's because the code consists precisely of the bits (binary digits) of the exponent 90!  Since computers store numbers in binary, the computer "knows" the BIT code as soon as it knows the exponent.  In Python, the bin command recovers the binary expansion of a number.
Can you write Python code for it?

bin(90)  # Compare this to the sequence of bits, from bottom up.


I am working on the file "PwNT Notebook 5.ipynb".
The first task is:
Now let's compare the performance of these two functions.  It's also good to double check the code in powermod_2 and run it to check the results.  The reason is that loops like the while loop above are classic sources of Off by One Errors.  Should e start at zero or one?  Should the while loop have the condition e < exponent or e <= exponent?  One has to unravel the loop carefully to be completely certain, and testing is a necessity to avoid bugs!
We can compare the performance of the two functions with identical input parameters below.
Can you write Python code for it?


I am working on the file "PwNT Notebook 5.ipynb".
The first task is:
Now we can be pretty sure that the powermod_1 function is faster (perhaps by a factor of 8-10) than the powermod_2 function we designed.  At least, this is the case for inputs in the 2-3 digit range that we sampled.  But why?  We reduced the complexity of the calculation by using the mod operation % throughout.  Here are a few issues one might suspect.

The mod operation itself takes a bit of time.  Maybe that time added up in powermod_2?
The Python power operation ** is highly optimized already, and outperforms our while loop.
We used more multiplications than necessary.

It turns out that the mod operation is extremely fast... as in nanoseconds (billionths of a second) fast.
Can you write Python code for it?


I am working on the file "PwNT Notebook 5.ipynb".
The first task is:
Python outputs binary expansions as strings, beginning with '0b'.  To summarize, we can compute an exponent like $b^e$ by the following process:
Pingala's Exponentiation Algorithm

Set the number to 1.

Read the bits of $e$, from left to right.
    a. When the bit is zero, square the number.
    b. When the bit is one, square the number, then multiply by $b$.

Output the number.
Can you write Python code for it?

def pow_Pingala(base,exponent):
    result = 1
    bitstring = bin(exponent)[2:] # Chop off the '0b' part of the binary expansion of exponent
    for bit in bitstring: # Iterates through the "letters" of the string.  Here the letters are '0' or '1'.
        if bit == '0':
            result = result*result
        if bit == '1':
            result = result*result * base
    return result

pow_Pingala(5,90)


I am working on the file "PwNT Notebook 5.ipynb".
The first task is:
The fully built-in modular exponentiation pow(b,e,m) command is probably the fastest.  But our implementation of Pingala's algorithm isn't bad -- it probably beats the simple (b**e) % m command (in the powermod_1 function), and it's certainly faster than our naive loop in powermod_2.
One can quantify the efficiency of these algorithms by analyzing how the time depends on the size of the input parameters.  For the sake of exposition, let us keep the base and modulus constant, and consider how the time varies with the size of the exponent.
As a function of the exponent $e$, our powmod_Pingala algorithm required some number of multiplications, bounded by twice the number of bits of $e$.  The number of bits of $e$ is approximately $\log_2(e)$.  The size of the numbers multiplied is bounded by the size of the (constant) modulus.  In this way, the time taken by the powmod_Pingala algorithm should be $O(\log(e))$, meaning bounded by a constant times the logarithm of the exponent.
Contrast this with the slow powermod_2 algorithm, which performs $e$ multiplications, and has thus has runtime $O(e)$.
The Miller-Rabin test
Pingala's algorithm is effective for computing exponents, in ordinary arithmetic or in modular arithmetic.  In this way, we can look for violations of Fermat's Little Theorem as before, to find witnesses to non-primality.  But if we look more closely at the algorithm... we can sometimes find violations of the ROO property of primes.  This strengthens the primality test.
To see this, we create out a "verbose" version of Pingala's algorithm for modular exponentiation.
Can you write Python code for it?

def powmod_verbose(base, exponent, modulus):
    result = 1
    print "Computing %d raised to %d, modulo %d."%(base, exponent, modulus)
    print "The current number is %d"%(result)
    bitstring = bin(exponent)[2:] # Chop off the '0b' part of the binary expansion of exponent
    for bit in bitstring: # Iterates through the "letters" of the string.  Here the letters are '0' or '1'.
        sq_result = result*result % modulus  # We need to compute this in any case.
        if bit == '0':
            print "BIT 0:  %d squared is congruent to %d, mod %d"%(result, sq_result, modulus)
            result = sq_result
        if bit == '1':
            newresult = (sq_result * base) % modulus
            print "BIT 1:  %d squared times %d is congruent to %d, mod %d"%(result, base, newresult, modulus)
            result = newresult
    return result

powmod_verbose(2,560,561)  # 561 is a Carmichael number.


I am working on the file "PwNT Notebook 5.ipynb".
The first task is:
The function has displayed every step in Pingala's algorithm.  The final result is that $2^{560} \equiv 1$ mod $561$.  So in this sense, $2$ is a bad witness.  For $561$ is not prime (3 is a factor), but it does not violate Fermat's Little Theorem when $2$ is the base.
But within the verbose output above, there is a violation of the ROO property.  The penultimate line states that "67 squared is congruent to 1, mod 561".  But if 561 were prime, only 1 and 560 are square roots of 1.  Hence this penultimate line implies that 561 is not prime (again, without finding a factor!).
This underlies the Miller-Rabin test.  We carry out Pingala's exponentiation algorithm to compute $b^{p-1}$ modulo $p$.  If we find a violation of ROO along the way, then the test number $p$ is not prime.  And if, at the end, the computation does not yield $1$, we have found a Fermat's Little Theorem (FLT) violation, and the test number $p$ is not prime.
The function below implements the Miller-Rabin test on a number $p$, using a given base.
Can you write Python code for it?

def Miller_Rabin(p, base):
    '''
    Tests whether p is prime, using the given base.
    The result False implies that p is definitely not prime.
    The result True implies that p **might** be prime.
    It is not a perfect test!
    '''
    result = 1
    exponent = p-1
    modulus = p
    bitstring = bin(exponent)[2:] # Chop off the '0b' part of the binary expansion of exponent
    for bit in bitstring: # Iterates through the "letters" of the string.  Here the letters are '0' or '1'.
        sq_result = result*result % modulus  # We need to compute this in any case.
        if sq_result == 1:
            if (result != 1) and (result != exponent):  # Note that exponent is congruent to -1, mod p.
                return False  # a ROO violation occurred, so p is not prime
        if bit == '0':
            result = sq_result
        if bit == '1':
            result = (sq_result * base) % modulus
    if result != 1:
        return False  # a FLT violation occurred, so p is not prime.

    return True  # If we made it this far, no violation occurred and p might be prime.

 Miller_Rabin(101,6)


I am working on the file "PwNT Notebook 5.ipynb".
The first task is:
The miracle that makes modular arithmetic work is that the end-result of a computation "mod m" is not changed if one works "mod m" along the way.  At least this is true if the computation only involves addition, subtraction, and multiplication.
Can you write Python code for it?


I am working on the file "PwNT Notebook 5.ipynb".
The first task is:
The randint(a,b) command chooses a random integer between a and b, inclusive!  Unlike the range(a,b) command which iterates from a to b-1, the randint command includes both a and b as possibilities.  The following lines iterate the randint(1,10) and keep track of how often each output occurs.  The resulting frequency distribution should be nearly flat, i.e., each number between 1 and 10 should occur about 10% of the time.
Can you write Python code for it?

Freq = {1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0, 9:0, 10:0}  # We prefer a dictionary here.
for t in range(10000):
    n = randint(1,10) # Choose a random number between 1 and 10.
    Freq[n] = Freq[n] + 1

print Freq


I am working on the file "PwNT Notebook 5.ipynb".
The first task is:
The result of this computation should not have the letter "L" at the end, because Python never had to work with long integers.  Computations with long integers are time-consuming, and unnecessary if you only care about the result of a computation modulo a small number m.
Performance analysis
The above loop works quickly, but it is far from optimal.  Let's carry out some performance analysis by writing two powermod functions.
Can you write Python code for it?

def powermod_1(base, exponent, modulus):  # The naive approach.
    return (base**exponent) % modulus

def powermod_2(base, exponent, modulus):
    P = 1 # Start the running product at 1.
    e = 0
    while e < exponent:  # The while loop saves memory, relative to a for loop, by avoiding the storage of a list.
        P = (P * base) % modulus
        e = e + 1
    return P


I am working on the file "PwNT Notebook 5.ipynb".
The first task is:
The results will probably on the order of a millisecond, perhaps even a tenth of a millisecond ($10^{-4}$ seconds) for non-primes!  That's much faster than looking for factors, for numbers of this size.  In this way, we can test primality of numbers of hundreds of digits!
For an application, let's find some Mersenne primes.  Recall that a Mersenne prime is a prime of the form $2^p - 1$.  Note that when $2^p - 1$ is prime, it must be the case that $p$ is a prime too.  We will quickly find the Mersenne primes with $p$ up to 1000 below!
Can you write Python code for it?

for p in range(1,1000):
    if is_prime(p):  # We only need to check these p.
        M = 2**p - 1 # A candidate for a Mersenne prime.
        if is_prime(M):
            print "2^%d - 1 = %d is a Mersenne prime."%(p,M)


I am working on the file "PwNT Notebook 5.ipynb".
The first task is:
The syntax of the timeit function is a bit challenging.  The first parameter is the Python code which we are timing (as a string), in this case powermod_*(3,999,1000).  The second parameter probably looks strange.  It exists because the timeit function sets up a little isolation chamber to run the code -- within this isolation chamber, it only knows standard Python commands and not the new functions you've created.  So you have to import your functions (powermod_1 or powermod_2) into its isolation box.  Where are these imported from?  They are contained in __main__ which is the location of all of your other code.  Finally, the third parameter number is the number of times the timeit function will repeat the code (by default, this might be a large number like 1000000).
The output of the timeit function is a float, which represents the number of seconds taken for all of the repetitions.  Contrast this with the timeit magic, which found the average.  So you need to divide by the number parameter (10000 in the above examples) to find the average time taken.
Using the timeit function, we can compare the performance of the two powermod functions on multiple inputs, wrapping the whole comparison process in a bigger function.  We choose our inputs randomly in order to estimate the expected performance of our functions.  To choose random inputs, Python has a package aptly called random.
Can you write Python code for it?

from random import randint # randint chooses random integers.

print "My number is ",randint(1,10) # Run this line many times over to see what happens!


I am working on the file "PwNT Notebook 5.ipynb".
The first task is:
We can learn something from the previous two examples.  Namely, 91 and 12319 are not prime numbers.  We say that 7 witnesses the non-primality of 12319.  Moreover, we learned this fact without actually finding a factor of 12319!  Indeed, the factors of 12319 are 97 and 127, which have no relationship to the "witness" 7.
In this way, Fermat's Little Theorem -- a statement about prime numbers -- can be turned into a way of discovering that numbers are not prime.  After all, if $p$ is not prime, then what are the chances that $a^{p-1} \equiv 1$ mod $p$ by coincidence?
Can you write Python code for it?


I am working on the file "PwNT Notebook 5.ipynb".
The first task is:
We have described two properties of prime numbers, and therefore two possible indicators that a number is not prime.

If $p$ is a number which violates Fermat's Little Theorem, then $p$ is not prime.

If $p$ is a number which violates the ROO property, then $p$ is not prime.

The Miller Rabin test will combine these indicators.  But first we have to introduce an ancient algorithm for exponentiation.
Pingala's exponentiation algorithm
If we wish to compute $5^{90}$ mod $91$, without the pow command, we don't have to carry out 90 multiplications.  Instead, we carry out Pingala's algorithm.  To understand this algorithm, we begin with the desired exponent (e.g. $e=90$), and carry out a series of steps:  replace $e$ by $e/2$ if $e$ is even, and replace $e$ by $(e-1) / 2$ if $e$ is odd.  Repeat this until the exponent is decreased to zero.  The following function carries out this process on any input $e$.
Can you write Python code for it?

def Pingala(e):
    current_number = e
    while current_number > 0:
        if current_number%2 == 0:
            current_number = current_number / 2
            print "Exponent %d BIT 0"%(current_number)
        if current_number%2 == 1:
            current_number = (current_number - 1) / 2
            print "Exponent %d BIT 1"%(current_number)

Pingala(90)


I am working on the file "PwNT Notebook 5.ipynb".
The first task is:
We implement the Miller-Rabin test for primality in the is_prime function below.
Can you write Python code for it?

def is_prime(p, witnesses=50):  # witnesses is a parameter with a default value.
    '''
    Tests whether a positive integer p is prime.
    For p < 2^64, the test is deterministic, using known good witnesses.
    Good witnesses come from a table at Wikipedia's article on the Miller-Rabin test,
    based on research by Pomerance, Selfridge and Wagstaff, Jaeschke, Jiang and Deng.
    For larger p, a number (by default, 50) of witnesses are chosen at random.
    '''
    if (p%2 == 0): # Might as well take care of even numbers at the outset!
        if p == 2:
            return True
        else:
            return False

    if p > 2**64:  # We use the probabilistic test for large p.
        trial = 0
        while trial < witnesses:
            trial = trial + 1
            witness = randint(2,p-2) # A good range for possible witnesses
            if Miller_Rabin(p,witness) == False:
                return False
        return True

    else:  # We use a determinisic test for p <= 2**64.
        verdict = Miller_Rabin(p,2)
        if p < 2047:
            return verdict # The witness 2 suffices.
        verdict = verdict and Miller_Rabin(p,3)
        if p < 1373653:
            return verdict # The witnesses 2 and 3 suffice.
        verdict = verdict and Miller_Rabin(p,5)
        if p < 25326001:
            return verdict # The witnesses 2,3,5 suffice.
        verdict = verdict and Miller_Rabin(p,7)
        if p < 3215031751:
            return verdict # The witnesses 2,3,5,7 suffice.
        verdict = verdict and Miller_Rabin(p,11)
        if p < 2152302898747:
            return verdict # The witnesses 2,3,5,7,11 suffice.
        verdict = verdict and Miller_Rabin(p,13)
        if p < 3474749660383:
            return verdict # The witnesses 2,3,5,7,11,13 suffice.
        verdict = verdict and Miller_Rabin(p,17)
        if p < 341550071728321:
            return verdict # The witnesses 2,3,5,7,11,17 suffice.
        verdict = verdict and Miller_Rabin(p,19) and Miller_Rabin(p,23)
        if p <...


I am working on the file "PwNT Notebook 5.ipynb".
The first task is:
Well, ok.  Sometimes coincidences happen.  We say that 3 is a bad witness for 91, since 91 is not prime, but $3^{90} \equiv 1$ mod $91$.  But we could try multiple bases (witnesses).  We can expect that someone (some base) will witness the nonprimality.  Indeed, for the non-prime 91 there are many good witnesses (ones that detect the nonprimality).
Can you write Python code for it?

for witness in range(1,20):
    flt = pow(witness, 90, 91)
    if flt == 1:
        print "%d is a bad witness."%(witness)
    else:
        print "%d raised to the 90th power equals %d, mod 91"%(witness, flt)


I am working on the file "PwNT Notebook 6.ipynb".
The first task is:
A theorem of Gauss states that, if $p$ is prime, there exists an integer $b$ whose order is precisely $p-1$ (as big as possible!).  Such an integer is called a primitive root modulo $p$.  For example, the previous computation found 12 primitive roots modulo $37$:  they are 2,5,13,15,17,18,19,20,22,24,32,35.  To see these illustrated (mod 37), check out this poster (yes, that is blatant self-promotion!)
For everything that follows, suppose that $p$ is a prime number.  Not only do primitive roots exist mod $p$, but they are pretty common.  In fact, the number of primitive roots mod $p$ equals $\phi(p-1)$, where $\phi$ denotes Euler's totient.  On average, $\phi(n)$ is about $6 / \pi^2$ times $n$ (for positive integers $n$).  While numbers of the form $p-1$ are not "average", one still expects that $\phi(p-1)$ is a not-very-small fraction of $p-1$.  You should not have to look very far if you want to find a primitive root.
The more difficult part, in practice, is determining whether a number $b$ is or is not a primitive root modulo $p$.  When $p$ is very large (like hundreds or thousands of digits), $p-1$ is also very large.  It is certainly not practical to cycle all the powers (from $1$ to $p-1$) of $b$ to determine whether $b$ is a primitive root!
The better approach, sometimes, is to use the fact that the multiplicative order of $b$ must be a divisor of $p-1$.  If one can find all the divisors of $p-1$, then one can just check whether $b^d \equiv 1$ mod $p$ for each divisor $d$.  This makes the problem of determining whether $b$ is a primitive root just about as hard as the problem of factoring $p-1$.  This is a hard problem, in general!
But, for the application we're interested in, we will want to have a large prime number $p$ and a primitive root mod $p$.  The easiest way to do this is to use a Sophie Germain prime $q$.  A Sophie Germain prime is a prime number $q$ such that $2q + 1$ is also prime.  When $q$ is a Sophie Germain prime, the resulting prime $p = 2q + 1$ is called a safe prime.
Observe that...
Can you write Python code for it?

def is_primroot_safe(b,p):
    '''
    Checks whether b is a primitive root modulo p,
    when p is a safe prime.  If p is not safe,
    the results will not be good!
    '''
    q = (p-1) / 2   # q is the Sophie Germain prime
    if b%p == 1:  # Is the multiplicative order 1?
        return False
    if (b*b)%p == 1:  # Is the multiplicative order 2?
        return False
    if pow(b,q,p) == 1:  # Is the multiplicative order q?
        return False
    return True  # If not, then b is a primitive root mod p.


I am working on the file "PwNT Notebook 6.ipynb".
The first task is:
As designed, the Caesar cipher turns plaintext into ciphertext by using a shift of the ASCII table.  To decipher the ciphertext, one can just use the Caesar cipher again, with the negative shift.
Can you write Python code for it?

print Caesar_cipher('Mjqqt&%%Hfs%~tz%wjfi%ymnxD', -5)  # Shift back 5 units in ASCII.


I am working on the file "PwNT Notebook 6.ipynb".
The first task is:
Let's test this out by finding the Sophie Germain primes up to 100, and their associated safe primes.
Can you write Python code for it?

for j in range(1,100):
    if is_SGprime(j):
        print j, 2*j+1


I am working on the file "PwNT Notebook 6.ipynb".
The first task is:
Next we find a primitive root, modulo the safe prime p.
Can you write Python code for it?

def findprimroot_safe(p):
    '''
    Finds a primitive root,
    modulo a safe prime p.
    '''
    b = 2  # Start trying with 2.
    while True:  # We just keep on looking.
        if is_primroot_safe(b,p):
            return b
        b = b + 1 # Try the next base.  Shouldn't take too long to find one!

g = findprimroot_safe(p)
print g


I am working on the file "PwNT Notebook 6.ipynb".
The first task is:
Next, we find the first 100-digit Sophie Germain prime!  This might take a minute!
Can you write Python code for it?

test_number = 10**99 # Start looking at the first 100-digit number, which is 10^99.
while not is_SGprime(test_number):
    test_number = test_number + 1
print test_number


I am working on the file "PwNT Notebook 6.ipynb".
The first task is:
Now Alice and Bob do something that seems very strange at first.  Alice sends Bob her new number $A$ and Bob sends Alice his new number $B$.  Since they are far apart, and the channel is insecure, we can assume everyone in the world now knows $A$ and $B$.
Can you write Python code for it?

print "Everyone knows A = %d and B = %d."%(A,B)


I am working on the file "PwNT Notebook 6.ipynb".
The first task is:
Now Alice and Bob use their secrets to generate new numbers.  Alice computes the number
$$A = g^a \text{ mod } p,$$
and Bob computes the number
$$B = g^b \text{ mod } p.$$
Can you write Python code for it?

A = pow(g,a,p)  # This would be computed on Alice's computer.
B = pow(g,b,p)  # This would be computed on Bob's computer.


I am working on the file "PwNT Notebook 6.ipynb".
The first task is:
Now Alice, on her private computer, computes $B^a$ mod $p$.  She can do that because everyone knows $B$ and $p$, and she knows $a$ too.
Similarly, Bob, on his private computer, computes $A^b$ mod $p$.  He can do that because everyone knows $A$ and $p$, and he knows $b$ too.
Alice and Bob do not share the results of their computations!
Can you write Python code for it?

print pow(B,a,p)  # This is what Alice computes.

print pow(A,b,p)  # This is what Bob computes.


I am working on the file "PwNT Notebook 6.ipynb".
The first task is:
Now we can implement a substitution cipher by converting characters to their ASCII codes, shuffling the codes, and converting back.  One of the simplest substitution ciphers is called a Caesar cipher, in which each character is shifted -- by a fixed amount -- down the list.  For example, a Caesar cipher of shift 3 would send 'A' to 'D' and 'B' to 'E', etc..  Near the end of the list, characters are shifted back to the beginning -- the list is considered cyclicly, using our inrange function.
Here is an implementation of the Caesar cipher, using the ASCII range between 32 and 126.  We begin with a function to shift a single character.
Can you write Python code for it?

def Caesar_shift(c, shift):
    '''
    Shifts the character c by shift units
    within the ASCII table between 32 and 126.
    The shift parameter can be any integer!
    '''
    ascii = ord(c)
    a = ascii + shift # Now we have a number between 32+shift and 126+shift.
    a = inrange(a,32,126) # Put the number back in range.
    return chr(a)


I am working on the file "PwNT Notebook 6.ipynb".
The first task is:
Now we can use the Caesar cipher to encrypt strings.
Can you write Python code for it?

def Caesar_cipher(plaintext, shift):
    ciphertext = ''
    for c in plaintext:  # Iterate through the characters of a string.
        ciphertext = ciphertext + Caesar_shift(c,shift)
    return ciphertext

print Caesar_cipher('Hello!  Can you read this?', 5)  # Shift forward 5 units in ASCII.


I am working on the file "PwNT Notebook 6.ipynb".
The first task is:
Since computers store data in binary, the designers of early computers (1960s) created a code called ASCII (American Standard Code for Information Interchange) to associate to each character a number between 0 and 127.  Every number between 0 and 127 is represented in binary by 7 bits (between 0000000 and 1111111), and so each character is stored with 7 bits of memory.  Later, ASCII was extended with another 128 characters, so that codes between 0 and 255 were used, requiring 8 bits.  8 bits of memory is called a byte.  One byte of memory suffices to store one (extended ASCII) character.
You might notice that there are 256 ASCII codes available, but there are fewer than 256 characters available on your keyboard, even once you include symbols like # and ;.  Some of these "extra" codes are for accented letters, and others are relics of old computers.  For example, ASCII code 7 (0000111) stands for the "bell", and older readers might remember making the Apple II computer beep by pressing Control-G on the keyboard ("G" is the 7th letter).  You can look up a full ASCII table if you're curious.
Nowadays, the global community of computer users requires far more than 256 "letters" -- there are many alphabets around the world!  So instead of ASCII, we can access over 100 thousand unicode characters.  Scroll through a unicode table to see what is possible.  In Python version 3.x, all strings are considered in Unicode, but in Python 2.7 (which we use), it's a bit trickier to work with Unicode.
Here we stay within ASCII codes, since they will suffice for basic English messages.  Python has built-in commands chr and ord for converting from code-number (0--255) to character and back again.
Can you write Python code for it?


I am working on the file "PwNT Notebook 6.ipynb".
The first task is:
The Vigenère cipher becomes an effective way for two parties to communicate securely, as long as they share a secret key.  In the 19th century, this often meant that the parties would require an initial in-person meeting to agree upon a key, or a well-guarded messenger would carry the key from one party to the other.
Today, as we wish to communicate securely over long distances on a regular basis, the process of agreeing on a key is more difficult.  It seems like a chicken-and-egg problem, where we need a shared secret to communicate securely, but we can't share a secret without communicating securely in the first place!
Remarkably, this secret-sharing problem can be solved with some modular arithmetic tricks.  This is the subject of the next section.
Exercises

A Caesar cipher was used to encode a message, with the resulting ciphertext:  'j!\'1r$v1"$v&&+1t}v(v$2'.  Use a loop (brute force attack) to figure out the original message.

Imagine that you encrypt a long message (e.g., 1000 words of standard English) with a Vigenère cipher.  How might you detect the length of the key, if it is short (e.g. 3 or 4 characters)?

Consider running a plaintext message through a Vigenère cipher with a 3-character key, and then running the ciphertext through a Vigenère cipher with a 4-character key.  Explain how this is equivalent to running the original message through a single cipher with a 12-character key.

Key exchange
Now we study Diffie-Hellman key exchange, a remarkable way for two parties to share a secret without ever needing to directly communicate the secret with each other.  Their method is based on properties of modular exponentiation and the existence of a primitive root modulo prime numbers.
Primitive roots and Sophie Germain primes
If $p$ is a prime number, and $GCD(a,p) = 1$, then recall Fermat's Little Theorem:  $$a^{p-1} \equiv 1 \text{ mod } p.$$
It may be the case that $a^\ell \equiv 1$ mod $p$ for some smaller (positive) value of $\ell$ however.  The...
Can you write Python code for it?

def mult_order(a,p):
    '''
    Determines the (multiplicative) order of an integer
    a, modulo p.  Here p is prime, and GCD(a,p) = 1.
    If bad inputs are used, this might lead to a
    never-ending loop!
    '''
    current_number = a % p
    current_exponent = 1
    while current_number != 1:
        current_number = (current_number * a)%p
        current_exponent = current_exponent + 1
    return current_exponent

for j in range(1,37):
    print "The multiplicative order of %d modulo 37 is %d"%(j,mult_order(j,37))
    # These orders should all be divisors of 36.


I am working on the file "PwNT Notebook 6.ipynb".
The first task is:
The Vigenère cipher is called a symmetric cryptosystem, because the same key that is used to encrypt the plaintext can be used to decrypt the ciphertext.  All we do is subtract the shift at each stage.
Can you write Python code for it?

def Vigenere_decipher(ciphertext, key):
    plaintext = '' # Start with an empty string
    for j in range(len(ciphertext)):
        c = ciphertext[j] # the jth letter of the ciphertext
        key_index = j % len(key) # Cycle through letters of the key.
        shift = - ord(key[key_index]) # Note the negative sign to decipher!
        plaintext = plaintext + Caesar_shift(c,shift) # Add new letter to plaintext
    return plaintext

Vigenere_decipher('@n$_&$_&1Qx4ky Ox `', 'Key')

# Try a few cipher/deciphers yourself to get used to the Vigenere system.


I am working on the file "PwNT Notebook 6.ipynb".
The first task is:
The following code will produce a table of the ASCII characters with codes between 32 and 126.  This is a good range which includes all the most common English characters and symbols on a U.S. keyboard.  Note that ASCII code 32 corresponds to an empty space (an important character for long messages!)
Can you write Python code for it?

for a in range(32,127):
    c = chr(a)
    print "ASCII %d is %s"%(a, c)


I am working on the file "PwNT Notebook 6.ipynb".
The first task is:
The function above searches and searches among random numbers until it finds a Sophie Germain prime.  The (possibly endless!) search is performed with a while True: loop that may look strange.  The idea is to stay in the loop until such a prime is found.  Then the return p command returns the found prime as output and halts the loop.  One must be careful with while True loops, since they are structured to run forever -- if there's not a loop-breaking command like return or break inside the loop, your computer will be spinning for a long time.
Can you write Python code for it?

q = getrandSGprime(256) # A random ~256 bit Sophie Germain prime
p = 2*q + 1 # And its associated safe prime

print "p is ",p  # Just to see what we're working with.
print "q is ",q


I am working on the file "PwNT Notebook 6.ipynb".
The first task is:
The pair of numbers $(g, p)$, the primitive root and the safe prime, chosen by either Alice or Bob, is now made public.  They can post their $g$ and $p$ on a public website or shout it in the streets.  It doesn't matter.  They are just tools for their secret-creation algorithm below.
Alice and Bob's private secrets
Next, Alice and Bob invent private secret numbers $a$ and $b$.  They do not tell anyone these numbers.  Not each other.  Not their family.  Nobody.  They don't write them on a chalkboard, or leave them on a thumbdrive that they lose.  These are really secret.
But they don't use their phone numbers, or social security numbers.  It's best for Alice and Bob to use a secure random number generator on their separate private computers to create $a$ and $b$.  They are often 256 bit numbers in practice, so that's what we use below.
Can you write Python code for it?

a = SystemRandom().getrandbits(256)  # Alice's secret number
b = SystemRandom().getrandbits(256)  # Bob's secret number

print "Only Alice should know that a = %d"%(a)
print "Only Bob should know that b = %d"%(b)

print "But everyone can know p = %d and g = %d"%(p,g)