 CoCalc Public FilesSD109 - Tutorial.ipynb
Author: Fredrik Strömberg
Views : 200
Description: Tutorial made for the Virtual Global Sage Days 109

# Introduction to Python and SageMath

We will introduce some of the fundamental concepts in Python and SageMath.

## Learning outcomes

1. Introduction to SageMath
2. Learn how to run Sage command line or notebook
3. Basic expressions and variables in Python and SageMath
4. Introduce some basic packages for plotting etc.
5. For the full lessons see https://github.com/fredstro/sage-lesson-nt

## What is SageMath?

SageMath was originally developed by William Stein and first released in 2005. It was initially called SAGE, then Sage, and now SageMath, or Sage for short. The intention behind SageMath is to provide a free, open source alternative to Magma, Maple, Mathematica, MATLAB etc. with an easy to use interface (both for end-users and developers) built on Python (Cython) and which can integrate with other free or commercial packages (installed separately).

The default installation guide (wiki) or official page: installation contains many (over 150) different free packages, including:

• Pari/GP
• GAP
• Singular
• R
• Numpy
• matplotlib
• ...

For a full list see: https://www.sagemath.org/links-components.html

It can be integrated with existing commercial packages like

• Magma
• Mathematica
• Maple

Full documentation is included in the source of SageMath and is available online, together with a multitude of tutorials etc.
See for instance https://www.sagemath.org/tour.html

## Optional packages

Some packages that are not shipped with SageMath are available to install afterwards.

• In the terminal (on Windows, use the "SageMath Shell"):
• sage -i <package_name> - to install optional Sage package
• sage --pip install <py_package_name> - to install extra Python package using pip
• In a SageMath session (on Windows, use "SageMath" or "SageMath Notebook"):
• sage: installed_packages() - to list all installed packages
• sage: optional_packages() - to list installed and available optional packages
In [ ]:
For example,
- install additional GAP packages by running sage -i gap_packages
- install JupyterLab by running sage --pip install jupyterlab
- install Pandas by running sage --pip install pandas



## Running SageMath

For the Sage REPL (read-eval-print loop) or command-line interactive use:

• sage to run Sage
• sage --help to see all options, e.g. --python, --pip, etc.

For the Jupyter Notebook interface, run one of:

• sage -n jupyter --notebook-dir=<dir>
• sage --notebook=jupyter --notebook-dir=<dir>
• sage --jupyter notebook --notebook-dir=<dir>

For the JupyterLab interface (if installed -- see above), run one of:

• sage -n jupyterlab --notebook-dir=<dir>
• sage --notebook=jupyterlab --notebook-dir=<dir>
• sage --jupyter lab --notebook-dir=<dir>

Online

## Getting help with SageMath

• <function_name>? - to read documentation (docstring)
• <function_name>?? - to read the source (if available)
• <object_name>.<tab> - use tab completion to see available properties
• Help button in the toolbar links to the online documentation

Example: Try using these features for yourself below. Run a code cell by first selecting it and then either

• Press the Run button in the toolbar above
• Use the keyboard shortcut Ctrl+Enter or Shift+Enter
In [ ]:
gcd?

In [ ]:
x=1

In [ ]:
x.


## Programming in SageMath

Check that your kernel is set to SageMath (e.g. SageMath 9.0) in the top right of the toolbar.
You can change your kernel using the Kernel button on the toolbar to set the default to Python (2.7 or 3.x depending on your version of SageMath).
Since version 9.0 released in January 2020, SageMath is using Python 3.

Since the main functionality of SageMath is written in Python it means that most of the programming can be done using Python. There are however some subtle differences.

### Variable types in Python

• Numerical types: int, float, complex
• Strings: str
• Lists: list
• Tuple: tuple
• Dictionary: dict
• Set: set

Example: Run the cell below to explore basic data types in Python

In [ ]:
%%python3
# We run Jupyter cells in SageMath mode by default but can run individual cells in Python using Jupyter magics
# As Python 2 is still the default for older versions of SageMath it is better to be explicit about Python version.

x = 2
print(x, type(x))  # int

# Combine multiple statements with ';' -- avoid this in practice since it reduces readability
y = 2.0; print(y, type(y))  # float
r = 3/4; print(r, type(r))  # float
z = 1 + 1j; print(z, type(z))  # complex
s = "hello"; print(s, type(s))  # string
l = [1, 2, 2]; print(l, type(l))  # list
t = (1, 2, 2); print(t, type(t))  # tuple
d = {1: 'a', 'b': 2}; print(d, type(d))  # dictionary
A = set([1, 2, 3, 2]); print(A, type(A))  # set


Note that in Python 2, dividing two int variables uses "integer division with floor floor function"

In [ ]:
%%python2
r = 3/4; print(r, type(r))
r = -3/4; print(r, type(r))


### Variable types in SageMath

In SageMath defaults for many numeric types are overwritten. For example:

• Integers are by default cast to SageMath's Integer type, which has different behaviour from Python's int
• Instead of float, SageMath uses RealLiteral and Rational (where appropraite)
In [ ]:
x = 2 # Integer
y = 2.0 # Real/floating point numbers with RealLiteral
r = 3/4 # Rational
z_num = 1 + 1j # Numerical complex numbers with ComplexNumber
z_sym = 1 + 1*I # Symbolic complex numbers with Expression

# Reduce writing and improve readability by iterating over a list using a for loop
for variable in [x, y, r, z_num, z_sym]:
print(variable, type(variable))


# Sets

Sets are useful for making lists with unique elements, are order ambivalent, and can be defined using either braces {} or the set() constructor function.

In [ ]:
X = set([1, 2, 3, 4, 4, 4, 5])
Y = {5, 3, 2, 4, 2}
print(X)
print(Y)
print(list(X))


The set type features many useful methods such as unions and intersections. See the documentation here.

In [ ]:
A = set([1, 2, 3, 4])
B = set(['a', 'b', 'c', 1, 2, 8])
A.intersection(B)


Sage has some additional useful builtin-functions:

In [ ]:
list(cartesian_product([A,B]))

In [ ]:
list(powerset(B))


### Precision of variables in SageMath

SageMath stores variables to higher levels of precision than Python

• The RR keyword denotes the Real Field with 53 bits of precision (double precision)
• The CC keyword denotes the Complex Field with 53 bits of precision
• We can construct fields with higher precision using RealField(prec) and ComplexField(prec)
In [ ]:
for field in [RR, RealField(106), CC, ComplexField(106)]:
print(field)


We can contruct high precision complex numbers as field elements as follows:

In [ ]:
my_complex_field = ComplexField(106)
z1 = my_complex_field(1, 10)
z2 = my_complex_field(5, 5)

for num in [z1, z2, z1 + z2]:
print(num)


There are also builtin constants that can be given to arbitrary precision

In [ ]:
print(RR.pi())
print(RealField(106).pi())
high_prec_one = RealField(1000)(1)
print(high_prec_one)
print(exp(high_prec_one))  # the builtin exp function
print(high_prec_one.exp())  # the exp method of the RealNumber class


# Objects, Parents, Elements and categories

In Python and even more so in SageMath everything is an object and has properties and methods.
In SageMath most (every?) object is either an Element or a Parent

In [ ]:
F = RealField(53)  # Parent
x = F(2)  # Element
print(type(F))
print(type(x))
F.is_parent_of(x)  # True
print(x in F)  # True

In [ ]:
x.parent()  # Returns a copy of F

In [ ]:
x.category()  # The category of x, a category of elements

In [ ]:
F.category()  # The category of F

In [ ]:
In SageMath there is something called "coercion": natural operations are made using common parents.

In [ ]:
x=RR(1); y=CC(5)
type(x+y)


CC.coerce_map_from(RR)

### String formatting

In previous versions of Python there were more types of strings but in current Python there are basically only strings str and bytestrings bytes (which we do not discuss here)

We can control how strings are formatted when printing by using either the format method or by using formatted strings, which are strings with an f character before the first quotation mark. Please see the examples below, and follow this link for a reference on string formatting.

In [ ]:
x = RR(2)  # The real number 2, up to 53 bits of precision
print("x = {0}".format(x))    # format method
print(f"x = {x}")             # f-strings
print(f"exp(x) = {x.exp()}")  # Can call functions in f-strings
print(f"π is about {float(RR.pi()):0.5f}")  # As floating point number with 5 decimals


Exercise 1. Print $e^{\pi}$ as a floating point number to 225 decimal places.

Python solution: Use the decimal library (documentation)

In [ ]:
%%python3
import decimal, math
import numpy as np
decimal.getcontext().prec = 225
print(f"{decimal.Decimal(math.pi)}\n{decimal.Decimal(np.pi)}")


The cell above shows that there is not enough precision in the stored values of pi in the math and numpy libraries for our application, so we must use an algorithm to calculate it to a higher precision. We use the Gauss-Legendre algorithm for its fast convergence rate.

In [ ]:
%%python3
from decimal import Decimal, getcontext
import numpy as np
getcontext().prec = 227
num_iter = 8
one = Decimal(1)
two = Decimal(2)
four = Decimal(4)

def gauss_legendre_algorithm(num_iter):
r"""Implement the Gauss-Legendre algorithm for the specified number of iterations

Return an np.array of approximations to pi of increasing accuracy"""
a, b, t, = [one], [two.sqrt()/two], [one/four]
approx = list()
for i in range(num_iter):
a.append((a[i] + b[i]) / two)
b.append((a[i] * b[i]).sqrt())
t.append(t[i] - getcontext().power(two, Decimal(i)) * (a[i] - a[i+1]) * (a[i] - a[i+1]))
approx.append((a[i+1] + b[i+1]) * (a[i+1] + b[i+1]) / (four * t[i+1]))
return np.array(approx)
approx = gauss_legendre_algorithm(num_iter)
print(approx == approx[-1])  # Stabilised to 225 decimal places precision after 7 iterations
precise_pi = approx[-1]
result = precise_pi.exp()
print(result)
print(f"Number of decimal places = {len(str(result).split('.')[-1])}")  # Count digits after decimal point


SageMath solution:

• Note that this can be calculated using many fewer lines of code since there are builtin SageMath algorithms running behind the scenes.
• Note also that we must convert from bit (binary) precision to precision in decimal places by multiplying by $\log_{2}(10) \approx 3.32$
In [ ]:
required_bit_prec = 228 * RR(10).log2()  # Calculate required binary precision from decimal
exp_pi = RealField(required_bit_prec).pi().exp()
print(exp_pi)
print(f"Number of decimal places = {len(str(exp_pi).split('.')[-1])}")  # Count digits after decimal point


## Functions

• Function names should be descriptive and use "snake_case" (this is a Python convention)
• Variable names in Python should also be lower, snake_case
• In SageMath we also use mathematical conventions even if they break this, e.g. Ei for the exponential integral function
• For full document about coding conventions see: http://doc.sagemath.org/html/en/developer/coding_basics.html
In [ ]:
# A first function
def is_zero_mod_3(x):
return x % 3 == 0  # The % is the Python modulo operation: remainder in integer division

In [ ]:
is_zero_mod_3(5)

In [ ]:
# Does this make sense?
is_zero_mod_3(5.4)

In [ ]:
is_zero_mod_3(1+1j)  # Raises type error


Sage has an alternative modulo reduction function

In [ ]:
Mod(5, 3)

In [ ]:
type(Mod(5, 3))

In [ ]:
# Add handling of input
def is_zero_mod_3(x):
if not isinstance(x, int):
raise ValueError(f"Received input of type {type(x)}. This function needs an integer!")
return x % 3 == 0

In [ ]:
# Better and more informative error is raised
is_zero_mod_3(1 + 1j)

In [ ]:
is_zero_mod_3(int(6))

In [ ]:
# However, this doesn't work as intended
is_zero_mod_3(6)


The issue is that we checked for type int but in a SageMath environment the default type for integers is Integer. Need to check for both types!

In [ ]:
# Add handling of input
def is_zero_mod_3(x):
if not isinstance(x, (int, Integer)):  # Multiple types should be given as a tuple
raise ValueError(f"Received input of type {type(x)}. This function needs an integer!")
return x % 3 == 0

In [ ]:
is_zero_mod_3(6)


## Docstrings

When writing our own functions we can help future users check the expected inputs in advance by writing a docstring (documentation string).

In [ ]:
is_zero_mod_3?

In [ ]:
# Let's add a docstring
def is_zero_mod_3(x):
r"""
Return True if input is congruent to 0 mod 3, otherwise return False

INPUT:

- x -- integer

OUTPUT: A boolean describing whether the input is congruent to 0 mod 3 or not.
"""
if not isinstance(x, (Integer, int)):
raise ValueError(f"Received input of type {type(x)}. This function needs an integer!")
return x % 3 == 0

In [ ]:
# Now a potential user knows what input and output to expect.
is_zero_mod_3?

In [ ]:
is_zero_mod_3("test")


## Variables in functions:

In [ ]:
# Scalar values get passed by value into functions, i.e. what happens in the function stays in the function:
x = 1

x = x + 1
print(f"x = {x}")

y = 1
print(y)
print(y)
print(x)

In [ ]:
# Dictionaries get passed by reference, i.e. what happens in the function also happens outside.
d = {'a': 1}
print(d['a'])  # Get d['a'] if d has 'a' as a key, raise an error if not
print(d.get('b', 'default'))  # Get d['b'] if 'b' is a key, otherwise return 'default' (or None if no default value is given)

d['a'] = d.get('a') + 1
print(f"d = {d}")

print(d)
print(d)

In [ ]:
 + 1  # Addition of integers and lists in this way is not supported


### Type hints

In Python 3 it is also possible to add type hints (not enforced by Python).

In [ ]:
# Let's add a docstring
def is_zero_mod_3(x: int) -> bool:
r"""
Return True if input is congruent to 0 mod 3, otherwise return False

INPUT:

- x -- integer

OUTPUT: A boolean describing whether the input is congruent to 0 mod 3 or not.
"""
if not isinstance(x, (Integer, int)):
raise ValueError(f"Received input of type {type(x)}. This function needs an integer!")
return x % 3 == 0


Exercise Write a function is_square_residue with the following specifications:

1. Takes as input an integer $x$ and a positive integer $n$
2. Returns True if $x$ is congruent to a square modulo $n$ and otherwise False.
3. Handles errors in input with sensible error messages.
4. Includes a docstring which describes the function.
In [ ]:
def is_square_residue(x, n: int) -> bool:
r"""
Return True if $x$ is congruent to a squared integer modulo $n$, otherwise return False

INPUTS:
- x -- an integer
- n -- a positive integer

OUTPUT: A boolean describing whether there are any solutions $a$ to $x \equiv a^{2} \mod n$ for inputs x and n
"""
if not (isinstance(x, (Integer, int)) and isinstance(n, (Integer, int)) and n > 0):
raise ValueError(f"Received inputs (x, n) = ({x}, {n}) of type ({type(x)}, {type(n)}). Inputs should be integers, with n positive!")
return (x % n) in set(a**2 for a in range(n))

In [ ]:
n = 8
for x in range(n):
print(x, is_square_residue(x, n))


## Functions and objects

Functions are also objects and can be introspected via their properties. Class methods and imports etc. can be modified at runtime (monkey-patching)

In [ ]:
category(is_zero_mod_3)

In [ ]:
# some property...
is_zero_mod_3.__code__.co_varnames


Example of monkey patching

In [ ]:
%%python2
import six
class TestA(object):
def foo(self,x):
return x
if six.PY3:
TestA.foo = lambda self,x : x+1

print(TestA().foo(1))

In [ ]:
%%python3
import six
class TestA(object):
def foo(self,x):
return x
if six.PY3:
TestA.foo = lambda self,x : x+1

print(TestA().foo(1))


## Python Control Structures

Standard (used in most programming languages):

• Iteration with for and while loops
• If-then-else statements

More Python specific:

• Generator expressions
• List comprehensions
In [ ]:
# Iterate over a range of integers using the range function. Note that range starts from 0.
for i in range(5):
print(i)


The output of the range function is an example of a generator expression. It does not actually allocate all elements until needed.

In [ ]:
range(5, 12)  # Specify start and end points


We can cast a range to a list to see all of its elements. Note that range starts at the left endpoint and stops at the right endpoint without including it!

In [ ]:
list(range(5, 12))


Calling list(range(10^(10^10))) would run out of memory, but iterating over it is fine (although probably won't finish).
If evaluating the cell below, please call keyboard interrupt or select Kernel -> Interrupt from the toolbar above.

In [ ]:
for i in range(10^(10^10)):
pass


### Example: for loops and list comprehensions

We can evaluate $\zeta(2)$, the zeta function at s=2, using a loop, and compare to the known value $\frac{\pi}{6}$.

In [ ]:
# Calculate the partial sum of the first 100 terms of zeta(2)
result = 0
for n in range(1, 100):
result += n**(-2)
print(result)


We may also use a list comprehension, or directly use the generator expression to compute the partial sum

In [ ]:
sum([n**(-2) for n in range(1, 100)])  # List comprehension

In [ ]:
sum(n**(-2) for n in range(1, 100))  # Generator expression - most memory efficient


### Plotting in Sagemath with the plot function

The plot() function in SageMath takes as its first argument a function or list of functions. In the case that the passed functions take a single argument, the next two arguments, start and end, can be used to define the range over which to evaluate and plot them.

In [ ]:
plot?

In [ ]:
# A first plot of zeta along the horizontal line Im(z) = 1, 2 < Re(z) < 10
p = plot(lambda x: zeta(CC(x, 1)).real(), 2, 10)

In [ ]:
p


Most objects have a latex property which "pretty-prints" them in a format suitable for inclusion in a LaTeX document.

In [ ]:
latex(p)


## Symbolic expressions in SageMath

When starting Sage the name 'x' is defined as a symbolic variable and other variables have to be declared using the var('x, y, z, ...') command. Symbolic expressions can be treated in various ways:

• differentiation
• simplifications

When the notebook is started we have $x$ as a variable:

In [ ]:
x


In [ ]:
var('x, y, z')

In [ ]:
y*z


### Differentiation

In [ ]:
g = 1 / (x^2 + y^2)
print(f"{'g':20s} = {g}")
print(f"{'dg/dx':20s} = {diff(g, x)}")
print(f"{'d^2g/dxdy':20s} = {diff(g, x, y)}")

In [ ]:
g.differentiate(x, 2)


### Simplification

In [ ]:
f = x * y / (x^2 + y^2 )
z = diff(f, x, y)
z

In [ ]:
z.simplify_full()


## Substitution

In [ ]:
z1=z.substitute(x=2*y^2+1); z

In [ ]:
z1.simplify_full()


Exercise Determine the value of $\frac{\partial f}{\partial u}(0,1,1)$ for $f(s,t,u)=\frac{s+tu}{\sqrt{s^2+t^2+u^2}}$ Is it

• (a) ${\sqrt{2}}$

• (b) $\frac{1}{\sqrt{2}}$

• (c) $\frac{1}{2\sqrt{2}}$

• (d) $\frac{1}{5\sqrt{2}}$

• (e) None of the above?

In [ ]:
var('s,t,u')
f = (s+t*u)/sqrt(s**2+t**2+u**2)
diff(f,u).substitute(s=0,t=1,u=1)


We can now combine everything and compute the Bernoulli numbers. Recall that they can be defined by the generating series $\frac{x}{e^x - 1} = \sum_{m \ge 0} \frac{B_m x^m}{m!}$

In [ ]:
F = x / (e^x - 1); F  # The generating function


Try to find the first Taylor coefficient:

In [ ]:
g = derivative(F, x, 1); g  # another name for .diff()

In [ ]:
print(g.simplify_full())
# yes - there are other types of simplifications.
g.substitute(x=0)

In [ ]:
We can't just divide 0 / 0. We need L'Hopital's rule!

In [ ]:
# Differentiate the numerator  and denominator and divide again:
g.numerator().diff(x) / g.denominator().diff(x)


Still of the form 0 /0. Need one more derivative!

In [ ]:
# The second parameter gives the number of times we differentiate
p = g.numerator().diff(x, 2) / g.denominator().diff(x, 2)
print(p)
p = p.simplify_full()
print(p)
p.substitute(x=0)

In [ ]:
bernoulli(1)


So the first Bernoulli number $B_1=-\frac{1}{2}$. This method is a bit cumbersome but fortunately there is a builtin command in Sage for Taylor expansions

In [ ]:
F.taylor(x, 0, 10)


We can convert this to a polynomial over $\mathbb{Q}$:

In [ ]:
p = F.taylor(x, 0, 10).polynomial(QQ)
print(type(p))
print(p)
print(latex(p))


For a polynomial we can add a big-Oh

In [ ]:
q = p.add_bigoh(12); q

In [ ]:
print(q.parent())
type(q)

In [ ]:
x = q.parent().gen()
x

In [ ]:
q + (x + 1).add_bigoh(8)


We can get coefficients of certain terms in Taylor expansions

In [ ]:
F.taylor(x, 0, 10).coefficient(x^4)


We can now write a function that returns the j-th Bernoulli number

In [ ]:
def B(j):
F = x / (e^x - 1)
return F.taylor(x, 0, j).coefficient(x^j)*factorial(j)

[B(j) for j in range(1, 10)]


We can also work with polynomials in many variables

In [ ]:
F=GF(3)['x, y']
F

In [ ]:
x,y=F.gens(); x+4*y

In [ ]:
f=x*y+2*x**2
g=x**2+y*x**3

In [ ]:
f.gcd(g)


## Linear algebra can be done using "native" Sage or numpy

In [ ]:
m=Matrix(ZZ,[[1,1],[2,3]]); m

In [ ]:
m.characteristic_polynomial()

In [ ]:
m.eigenvalues() ## Root finding in the above


### Algebraic numbers

What is the "?" ?

In [ ]:
print(m.eigenvalues().parent())
print(type(m.eigenvalues()))


Can be evaluated to arbitrary precision

In [ ]:
m.eigenvalues().n(1000)


Can use Numpy

In [ ]:
m.numpy()

In [ ]:
import numpy
numpy.linalg.eigvals(m)


Can work over finite fields

In [ ]:
m=Matrix(GF(5),[[1,1],[2,3]]); m

In [ ]:
m.eigenvalues() # "exact"

In [ ]:
m.eigenvalues().parent()

In [ ]:
Does it work with interval arithmetic?

In [ ]:
RIF=RealIntervalField()
h=1e-10
x=(1-h,1+h)
x

In [ ]:
m=Matrix(RealIntervalField(53),[[RIF(1-h,1+h),RIF(1-h,1+h)],[RIF(2-h,2+h),RIF(3-h,3+h)]]); m

In [ ]:
m.eigenvalues() # Not implemented!


Can use Newton - Raphson to find the roots:

In [ ]:
p=m.charpoly()
p.newton_raphson(10,0.2)

In [ ]:
p.newton_raphson(10,3)

In [ ]:
_[-1].lower(),_[-1].upper()

In [ ]: