Elementary Number Theory in Sage

Karl-Dieter Crisman, Gordon College

Sage Education Days 3

 

This Sage worksheet is partly based on materials developed for the MAA PREP Workshop "Sage: Using Open-Source Mathematics Software with Undergraduates" (funding provided by NSF DUE 0817071) and was developed for the Sage Education Days 3 as part of the UTMOST Grant (several NSF DUE grants).

Why Sage and elementary number theory?

Elementary number theory shows up in various places in the curriculum.

So why use Sage (or any other computer system) in these contexts?

Since Sage began life as a project in algebraic and analytic number theory (and this continues to be a big emphasis), it is no surprise that functionality in this area is extremely comprehensive.  

The rest of this talk will show various basic topics that I cover in my own number theory course, highlighting how I use Sage to discuss them, especially to visualize them.  Along the way, there will be plenty of '@interacts' or 'Sagelets' to (hopefully) inspire.

 

One of the most important things in Sage is that the ring of integers modulo $n$ is available without further effort, so one can do examples in modular arithmetic very easily.  

For instance, we can create a number in $\mathbb{Z}_n$.  The 'type' command tells us that $a$ is not a regular integer.

{{{id=109| a = mod(2,11); a; type(a); a^10; a^1000000 /// 2 1 1 }}}

In fact, "mod(a,n)" will create an integer modulo $n$, represented as the least nonnegative residue of that integer $a$.

Note two things about the previous cell.

This highlights two points about how to use Sage.

Here's another example of this.  Recalling the basic programming construct called a loop, we can verify this for all $a$ in the integers modulo $p$.  

{{{id=110| for a in Integers(11): print a, a^10 /// 0 0 1 1 2 1 3 1 4 1 5 1 6 1 7 1 8 1 9 1 10 1 }}}

Notice that 'Integers(11)' gave us an algebraic object which is the ring of integers modulo the prime ideal generated by the element 11.  This is handy later.

This works for much bigger numbers too, of course.  We won't want to print out all the integers here, but we can check things, at least!

{{{id=145| p=random_prime(10^200,proof=True) Zp=Integers(p) # Here we give ourselves shorthand for the modular integers /// }}}

If I'm going to do things with my integers modulo $p$, I should name them.  I can name individual elements, too, as we saw above.

{{{id=117| a=Zp(2) # Here we ask for 2 as an element of that ring a p; a^(p-1) /// 12175749457485261323484342548813090831853594104614993416363962307255475872160716311272728940078211326045601051983589414266918386755653479471852846103058479931430533099115465341959637292476842332898283 1 }}}

Here the student sees that it's the last line that counts when it comes to printing.

{{{id=9| a^(10^400) /// 667341860419940833796129083121003719719303707438654397250300236758579316812430945254713755146961572305624053487037532854247248289759595685250040511759445620464084950052304753065232782552186716875365 }}}

A slight interlude:

Whenever you encounter a new object, you should try tab-completion to see what you can do to it.   Let's try it here, and pick one (hopefully one that won't be too long!).

{{{id=121| Zp. /// }}}

Here's one that sounds interesting.

{{{id=124| Zp.zeta? ///

File: /home/sageserver/sage/devel/sage/sage/rings/ring.pyx

Type: <type ‘builtin_function_or_method’>

Definition: Zp.zeta(n=2, all=False)

Docstring:

Return an n-th root of unity in self if there is one, or raise an ArithmeticError otherwise.

INPUT:

  • n – positive integer
  • all – bool, default: False. If True, return a list of all n-th roots of 1.

OUTPUT:

element of self of finite order

EXAMPLES:

sage: QQ.zeta()
-1
sage: QQ.zeta(1)
1
sage: CyclotomicField(6).zeta()
zeta6
sage: CyclotomicField(3).zeta()
zeta3
sage: CyclotomicField(3).zeta().multiplicative_order()
3
sage: a = GF(7).zeta(); a
3
sage: a.multiplicative_order()
6
sage: a = GF(49,'z').zeta(); a
z
sage: a.multiplicative_order()
48
sage: a = GF(49,'z').zeta(2); a
6
sage: a.multiplicative_order()
2
sage: QQ.zeta(3)
Traceback (most recent call last):
...
ValueError: no n-th root of unity in rational field
sage: Zp(7, prec=8).zeta()
3 + 4*7 + 6*7^2 + 3*7^3 + 2*7^5 + 6*7^6 + 2*7^7 + O(7^8)
}}}

And we use it to find the fifth roots of unity in this field.  

{{{id=136| root_list = Zp.zeta(5,all=True); root_list /// [80199770388563324100334548626345240081294273289109866436996652525328268652922508892946068538641538316054373187019168781211876036849531337824832226216684677717580165592175377569174402189281574130719978, 69839783895572286297568834485025073557885364348071061715465477061873400359794989367423407683971299361817245213947182344090739843367076197016322541936552333837227080274674865687645877633828974738751695, 57407444219199061498801298672323590163238592201574572482836619025676869537007609315386800852204337587805249250896651467970585450518157701115893749407382500580682168292929753154872678880962261809848942, 41936641877539676652531567723249367917108638987131879521606243741814402095343724540844607212999297064816230828621064026263296602805535970091696570138772210094329631491849238240260893562065879302446837] }}}

Are these really fifth roots of unity?  We use the list comprehension (set-builder) notation this time - another opportunity.

{{{id=125| [root^5 for root in root_list] /// [1, 1, 1, 1] }}}

Luckily, it checked out.  

I can also try to make this interactive, to remove the 'programmingese'.

{{{id=16| @interact def _(update=''): p=random_prime(10^20,proof=True) Zp=Integers(p) root_list = Zp.zeta(5,all=True) html("The prime is $%s$"%p) html("The roots are:") print root_list html("The fifth powers are:") print [root^5 for root in root_list] /// }}}

Incidentally, it seems like there is either a bug here or a documentation error.  Shouldn't ONE be a fifth root of unity?

This also exemplifies that it's important to know the level you are aiming at.  

Similarly, what background do they have mathematically?

 

So the previous example may seem like it wasn't elementary, but let's think about the connection.

And that is something we constantly should emphasize - that things are different modulo $p$!

There is a lot more we can access at the elementary level, such as

A good way to use Sage in this context is to allow students to experiment with pencil and paper first, then use Sage to see whether patterns they discover hold true before one attempts to prove them.

{{{id=62| p = 13 primitive_root(p); two_squares(p); is_prime(p); legendre_symbol(p,next_prime(p)) /// 2 (2, 3) True 1 }}}

By the way, there are lots of pedagogical opportunities to improve Sage in number theory.  For instance, someone just added the Jacobi symbol to fill in the gap between Legendre and Kronecker symbols.

Another place where someone is working is in the 'naive' modular solver.

{{{id=19| var('x,y') solve_mod([x^2 + 2 == x, x^2 + y == y^2], 14) /// [(4, 2), (4, 6), (4, 9), (4, 13)] }}}

All of these capabilities makes it fairly easy to construct elementary cryptographic examples as well.  

Here is a standard example of a Diffie-Hellman key exchange, for instance.  

{{{id=128| p=random_prime(10^20,10^30) # a random prime between these numbers q=mod(primitive_root(p),p) # makes the primitive root a number modulo p, not an integer n=randint(1,p) # Alice's random number m=randint(1,p) # Bob's random number x=q^n; y=q^m p;q;x; y; x^m; y^n /// 17686401212520143419 2 11357069553300816796 6155949584214785623 7166503060918115829 7166503060918115829 }}}

So Alice picks $n$ and sends $q^n$, Bob picks $m$ and sends $q^m$, and then they jointly have the key $$q^{mn}=q^{nm}$$ which an "Eve"sdropper can't discover (until they send lots of messages, at least).

The last thing verifies that the private keys they get are the same.

 

Most number-theoretic functions are pretty easy to find with tab-completion.  Let's try this now - what's something we should have available?

{{{id=22| /// }}}

For those who are interested, more advanced number-theoretic objects are easy to come by.

In the first example, K is the field extension $\QQ(\sqrt{-14})$, where the symbol 'a' plays the role of $\sqrt{-14}$; we discover several basic facts about $K$ in the next several cells.

{{{id=111| K. = NumberField(x^2+14); K /// Number Field in a with defining polynomial x^2 + 14 }}} {{{id=113| K.discriminant(); K.class_group().order(); K.class_group().is_cyclic() /// -56 4 True }}}

Various zeta functions are also available; here is a complex plot of the Riemann zeta.

{{{id=132| complex_plot(zeta, (-30,30), (-30,30)) /// }}}

Sage supports various more advanced cryptographic procedures as well as some basic pedagogical ones natively.  This example is adapted from the documentation.  The pycrypto module has serious versions.

{{{id=116| from sage.crypto.block_cipher.sdes import SimplifiedDES sdes = SimplifiedDES(); sdes /// Simplified DES block cipher with 10-bit keys }}} {{{id=133| bin = BinaryStrings() P = [0,1,0,0,1,1,0,1] # our message K = sdes.random_key() # generate a random key C = sdes.encrypt(P, K) # encrypt our message plaintxt = sdes.decrypt(C, K) # decrypt it plaintxt # print it /// [0, 1, 0, 0, 1, 1, 0, 1] }}} {{{id=26| /// }}}

 

 

Now it's time to start visualizing mathematics. First, let's see the traditional way. Look at powers of $a\text{ mod }(p)$; students are to look for patterns.

{{{id=0| @interact(layout=[['p','a']]) def _(p=(7,prime_range(50)),a=(3,[0..50])): b=mod(a,p) top=ceil(2*p/10)*10 html("If we look at some of the powers of $%s$"%(a,)) html("modulo the prime $%s$, we get:"%(p,)) html("") /// }}}

Did you get a chance to come up with something?  What do you think?  Did you come up with some potential theorems? Notice that we use a little string formatting - totally optional, of course.

Now, how many theorems do you see here?

{{{id=135| p=11 P=matrix_plot(matrix(p-1,[mod(a,p)^b for a in range(1,p) for b in srange(p)]),cmap='jet') show(P) /// }}}

This is a graphic giving the various powers of integers modulo $p$ as colors, not numbers.  The columns are the powers, so the first column is the zeroth power (always 1) and the second column gives the colors for the numbers modulo the given prime (first power). The $a-1$ row and $b$ column gives the color corresponding to $(a-1)^b$ mod ($p$).  That means the first ($0$th) column is the color for $1$ and the second ($1$th) column gives the colors of each element of $\mathbb{Z}_p$.  For instance, $(3,4)$ corresponds to $2^4$ mod ($7$) in the initial example below.

Theorems:

It's certainly possible to have students or you now try changing $p$ to see what else happens. After all, they won't necessarily see any patterns immediately.  Let's try this.

 

But with just a little work, we can make this interactive.  Now the programming is hidden, and it's far more interesting to the students.  This is my pedagogy.

{{{id=146| @interact def power_table_plot(p=(7,prime_range(50))): P=matrix_plot(matrix(p-1,[mod(a,p)^b for a in range(1,p) for b in srange(p)]),cmap='jet') show(P) /// }}}

My contention is that combining interaction with visualization is a great way to teach this discipline.

 

Note that if you don't like the colors, you can change the word in the quotes after the word 'cmap'; if you get rid of that, it will be a grayscale plot.  Some others you could try are 'Oranges' or  'hsv' or $\ldots$ 

 

Another nice exercise is to start trying to find patterns in the primes $p$ such that  $$x^2\equiv -1\text{ mod }(p)$$ has a solution.  Students would start by hand, but then we create a table and see if we can find something.   Here, the visualization is more traditional - via a table - but still interactive, with immediate feedback.

{{{id=31| @interact def _(n=20): yeslist=[] nolist=[] for p in prime_range(3,n): res = 0 for res in [0..p]: if mod(res,p)^2+1 == 0: yeslist.append(p) break else: nolist.append(p) t = [['exist', 'do not exist']] + [[a,b] for (a,b) in map(None,yeslist,nolist)] for item in t: for i in range(len(item)): if item[i] is None: item[i]='' html("Solutions to $x^2\equiv -1$ mod $(p)$ for $2< p <%s$:"%n) html.table(t, header = True) /// }}}

Another very useful object is the prime counting function $\pi(x)$.  This comes with its own custom plotting.

{{{id=119| prime_pi(100); plot(prime_pi,1,100) /// 25 }}}

A very nice aspect of Sage is combining several aspects of mathematics together.  It can be very eye-opening to students to see analytic aspects of number theory early on.  

{{{id=127| var('x') plot(prime_pi,2,10^6,thickness=2)+plot(Li,2,10^6,color='red')+plot(x/ln(x),2,10^6,color='green') /// }}}

We'll return to this later.

{{{id=138| /// }}}

 

I think that one of the most powerful ways to use Sage is to illuminate visual proofs that are often hard to understand with a single static picture.  The following is part of the proof of quadratic reciprocity.

 

Recall the statement of quadratic reciprocity.  For odd primes $p$ and $q$, we have that $$\left(\frac{p}{q}\right)\left(\frac{q}{p}\right)=(-1)^{\frac{p-1}{2}\frac{q-1}{2}}$$  That is to say, the Legendre symbols are the same unless $p$ and $q$ are both of the form $4k+3$.

Proof:

We will use a criterion of Eisenstein's that I introduced in class (see this article).  

Recall that $p$ and $q$ are odd primes. Now let  $$R=\sum_{\text{even }e,\; 0<e<p}\left\lfloor\frac{qe}{p}\right\rfloor\; $$ be the exponent in question, so that $$\left(\frac{q}{p}\right)=(-1)^R\; .$$  

 The key will be geometrically interpreting $\left\lfloor\frac{qe}{p}\right\rfloor$ as being the biggest integer less than $\frac{qe}{p}$.  The following features are present in the next graphic, which should clarify how we'll think of it geometrically.

It should be clear that each blue stack has the same height as $\left\lfloor\frac{qe}{p}\right\rfloor$ for some even $e$.

{{{id=1| var('x,y') @interact def _(p=(11,prime_range(3,100)),q=(7,prime_range(3,100))): E = [2,4..p-1] plot4 = plot((q/p)*x,(x,0,p),linestyle='--') plot3 = line([[0,0],[p,0],[p,q],[0,q],[0,0]],rgbcolor=(1,0,0)) plot2 = line([[0,0],[(p-1)/2,0],[(p-1)/2,(q-1)/2],[0,(q-1)/2],[0,0]],color='green') grid_pts_1 = [[i,j] for i in [1..p] for j in [1..q]] grid_pts_2 = [[i,j] for i in [1..(p-1)/2] for j in [1..(q-1)/2]] plot_grid_pts = points(grid_pts_1,rgbcolor=(0,0,0),pointsize=10) lattice_pts1 = [coords for coords in grid_pts_1 if (coords[0]*q-coords[1]*p>0 and coords[0]

0 and coords[0]>p/2)] num1, num2 = len(lattice_pts1), len(lattice_pts2) if len(lattice_pts1)!=0: plot_lattice_pts1 = points(lattice_pts1, rgbcolor = (0,0,1),pointsize=20) else: plot_lattice_pts1 = Graphics() if len(lattice_pts2)!=0: plot_lattice_pts2 = points(lattice_pts2, rgbcolor = (0,.5,0),pointsize=20) else: plot_lattice_pts2 = Graphics() show(plot2+plot3+plot4+plot_grid_pts+plot_lattice_pts1,xmax=p,ymax=q,ymin=0) forms = '$'+'+'.join(['\left\lfloor\\frac{%s\cdot %s}{%s}\\right\\rfloor'%(q,e,p) for e in E])+'$' html("The blue dots represent "+forms) forms2 = '$'+'+'.join(['\left\lfloor\\frac{%s}{%s}\\right\\rfloor'%(q*e,p) for e in E]) forms3 = '+'.join(['%s'%(floor(q*e/p)) for e in E])+'=%s\equiv%s\\text{ mod }(2)$'%(sum([floor(q*e/p) for e in E]),sum([floor(q*e/p) for e in E])%2) html("This simplifies to "+forms2+'='+forms3) /// }}}

What we will now do is try to convince ourselves that the number of blue points has the same parity as the total number of (positive) points in and on the green box under the dotted line.   If we can do that, the following steps finish the proof of quadratic reciprocity.

This would definitely complete the theorem!  And I prove this in the rest of class.

 

Why is this better? 

 

 

Analytic number theory is another great place to look - which makes sense, because a lot of what it does is make computations related to L-functions...  here's another excerpt, with some final graphics.  Of course, this is toward the end of class.  

 

Somewhat remarkably, the first people we know of compiling substantial data about this are Gauss and Legendre.  See the handout.

This should sound 100% crazy!  But Gauss and Legendre were no fools, and the accuracy is astounding.  Let's call $Li(x)=\int_0^\infty \frac{dt}{\ln(t)}$ (yes, this is a convergent integral).

{{{id=27| @interact def _(n=100): show(plot(prime_pi,3,n,color='black',legend_label='$\pi(x)$')+plot(x/ln(x),3,n,color='red',legend_label='$x/\ln(x)$')+plot(Li,3,n,color='green',legend_label='$Li(x)$')) /// }}}

Notice how much closer $Li(x)$ is than the $x/\ln(x)$ estimate.  It's usually closer by several orders of magnitude.

{{{id=28| @interact def _(n=[100,1000,1000000,1000000000]): P = prime_pi(n) html("$\pi(%s)=%s$"%(n,prime_pi(n))) html("The error with $%s/\ln(%s)$ is $\\approx %s$"%(n,n,P-(n/ln(n)).n())) html("The error with $Li(%s)$ is $\\approx %s$"%(n,P-Li(n))) /// }}}

Here's an even better estimate I talk about the last day or two, while students are working on take-home projects and don't want homework in addition.

{{{id=139| @interact def _(n=(1000,(1000,10^6)),k=(3,[1..10])): P = plot(prime_pi,n-1000,n,color='black',legend_label='$\pi(x)$') P += plot(Li,n-1000,n,color='green',legend_label='$Li(x)$') F = lambda x: sum([Li(x^(1/j))*moebius(j)/j for j in [1..k]]) P += plot(lambda x: Li(x)-.5*Li(sqrt(x)),n-1000,n,color='red',legend_label='$Li(x)-\\frac{1}{2}Li(\sqrt{x})$') P += plot(F,n-1000,n,color='blue',legend_label='$\sum_{j=1}^{%s}\\frac{\mu(j)}{j}Li(x^{1/j})$'%k) show(P) /// }}} {{{id=140| /// }}}

And here we see Riemann's exact formula for $\pi(x)$, through the zeros on the critical line up to $t=150$.

{{{id=141| import mpmath L = lcalc.zeros_in_interval(10,150,0.1) n=100 P = plot(prime_pi,n-50,n,color='black',legend_label='$\pi(x)$') P += plot(Li,n-50,n,color='green',legend_label='$Li(x)$') G = lambda x: sum([mpmath.li(x^(1/j))*moebius(j)/j for j in [1..3]]) P += plot(G,n-50,n,color='red',legend_label='$\sum_{j=1}^{%s}\\frac{\mu(j)}{j}Li(x^{1/j})$'%3) F = lambda x: sum([(mpmath.li(x^(1/j))-log(2)+numerical_integral(1/(y*(y^2-1)*log(y)),x^(1/j),oo)[0])*moebius(j)/j for j in [1..3]])-sum([(mpmath.ei(log(x)*((0.5+l[0]*i)/j))+mpmath.ei(log(x)*((0.5-l[0]*i)/j))).real for l in L for j in [1..3]]) P += plot(F,n-50,n,color='blue',legend_label='Really good estimate',plot_points=50) show(P) /// }}}

That's just for fun, really.  Thank you!

{{{id=148| /// }}}