#It is safe to ignore this for now, it just sets some parameters for plots
from matplotlib import rc
rc('lines', linewidth=5, markersize=10)
rc('axes', titlesize=20, labelsize=20)
rc('xtick', labelsize=16)
rc('ytick', labelsize=16)
rc('figure', figsize=[7,4])
This notebook contains implementations of some simple algorithms from numerical analysis. It is intended as a reference for students in Math 651.
Most scripts that you write for this class will begin by importing the numpy module, a collection of tools for numerics. You will probably also want matplotlib.pyplot, a library of functions for producing plots.
import numpy as np
from matplotlib import pyplot as plt
from sklearn.linear_model import LinearRegression
If you import these modules with the commands above, you will access their contents with the prefixes np.
and plt.
For example, the number $\pi$ comes with the numpy module, and to get it you write
np.pi
On the other hand, from sklearn.linear_model import LinearRegression
imports only the class LinearRegression
from the sklearn module, and this class is available without any prefix.
The array is the most useful data structure in scientific computing. If you are not familiar with arrays, think of them as vectors, matrices, or tensors whose entries are floating point numbers. For example, the following command defines something like the vector \begin{equation*} v= \begin{pmatrix} 1 \\ 2 \\ \end{pmatrix}. \end{equation*}
v=np.array([1,2])
Numpy arrays are indexed from 0
, and the i'th element of an array is accessed like v[i]
:
v[0]
v[1]
The index of an array of N
elements cannot exceed N-1
.
v[2]
However, array indices can be negative. Here, v[-1]
is the last element of v
, v[-2]
is the second to last element, etc.
v[-1]
In the example above, [1,2]
is a list. The function np.array()
converts the list to a numpy array. The list and array data structures are similar, but not interchangeable. For now, you should use exclusively arrays. Code written with arrays is more efficient than code with lists. (What really distinguishes the two structures is that arrays are stored contiguously in memory and all entries have the same type. Lists need not have either property. Thus, it is more efficient to loop over arrays but easier to change the size of a list.)
Arrays can have two or more dimensions, in which case they behave like matrices or tensors. To define a 2-dimensional array, pass the function np.array()
a list of rows:
M=np.array([[1,2],[6,7]])
M
Here, [[1,2],[6,7]]
is simply a list whose entries are are the rows of $M$. (Of course, the rows of $M$ are themselves lists.)
To access the entries of a multi-dimensional array, use expressions like M[i,j]
.
M[0,1]
In python, one does not use *
to multiply matrices. Instead, use the function np.dot
. For example:
Mv=np.dot(M,v)
Mv
This is very tricky because *
does act on arrays, but it doesn't do the same thing as matrix multiplication!
M*v
Note that M*v
is the same as $M \rm{diag}(v)$.
Every array has a shape which is a python tuple. (For our purposes, a tuple is a list enclosed in round instead of square brackets.)
M.shape
v.shape
Note well the shape of v
. (2,)
is the shape of a one-dimensional array of length 2, (2,1)
is the shape of a column vector (2-d array with one column) of length 2, and (1,2)
is the shape of a row vector (2-d array with one row) of length 2. Some functions will only accept a one-dimensional array, some only a row vector, and some only a column vector. See the example below concerning LinearRegression.fit
It is often convenient to make an array of a certain shape consisting of all zeros or all ones. Here is how to do it:
np.zeros((2,4))
np.ones((3,))
Often one wants to extract part of an array, e.g. one row, every third element, the last half. Here are some examples:
a=np.array([3,4,5,6,7])
a[m:n]
gives the m'th through the (n-1)'st elements of a.
a[1:3]
Note well that a[m:n]
does not include a[n]!
If you would like every k
'th element of an array with index between m
and n
, use a[m:n:k]
.
b=np.array([3,4,5,6,7,8,9,10,11,12,13,14,15])
b[0:13:3]
It is also easy to the i
'th row (a[i,:]
) or i
'th column (a[:,i]
) of an array.
c=np.array([[1,2],[3,4],[5,6]])
c
c[:,0]
c[2,:]
We perform a convergence study for the composite midpoint rule quadrature (uniform grid), which is given by the formula
\begin{align*} M^N_{[a,b]}(f) &= h \sum_{n=0}^{N-1} f \left ( a+ \left (n+\frac12 \right )h \right)\\ h &= \frac{b-a}{N}. \end{align*}First, we define a sample function to take the place of $f$ above.
def g(x):
return np.exp(-x**2)
The code above defines \begin{equation*} g(x) = \exp(-x^2). \end{equation*}
np.
---see the 'Boilerplate' section above.**
to compute powers, not caret ^
. (In python, ^
means bitwise XOR.)We now implement the quadrature $M^N_{[a,b]}$.
def M(f,a,b,N):
"""
Compute composite midpoint rule quadrature defined above.
Inputs
------
* f, function, the integrand
* a, float, the left endpoint of the integral
* b, float, the right endpoint of the integral
* N, natural number, the number of grid points
Output
------
* float, composite midpoint quadrature estimate
"""
h=(b-a)/N
M=0.0
for n in range(0,N):
M=M+f(a+(n+0.5)*h)
M=h*M
return M
#
. Longer comments, like the docstrings that are sometimes part of a function definition, are enclosed in """
. range(0,N)
in the for loop produces a list of numbers from 0 up to N-1. range(0,N)
is like the slice a[0:N]
in that N is not in range(0,N)
. In python, for loops are always expressed as loops over lists like range(0,N)
.We perform a convergence study to test that the algorithm behaves as expected. We choose $a=0$, $b=1$, and $N \in \{2^k; k=1, \dots, 14\}$ for the study.
a=0.0
b=1.0
Ns=np.array([2**k for k in range(0,15)])
[2**k for k in range(0,15)]
is called a list comprehension. It is more or less equivalent to
Ns=[] #initialize empty list
for k in range(0,15):
Ns.append(2**k) #add 2**k to the list Ns as the last element
Ns=np.array([2**k for k in range(0,15)])
is more or less equivalent to
Ns=np.zeros(15) #initialize 15 element numpy array with all entries equal to zero
for k in range(0,15):
Ns[k]=2**k
We want to see how the error of the quadrature depends on $N$. To be precise, we would like to compute a plot showing $\log_{10}(N)$ vs. $\log_{10}(\rm{err}(N))$, where \begin{equation*} \rm{err}(N) = M^N_{[0,1]}(g) - \int_0^1 g(x) \, dx. \end{equation*}
Of course, in this case, we do not know the exact integral, so we use the best approximation $M^{2^{14}}_{[0,1]}(g)$ instead when computing the error.
Ms=np.array([M(g,a,b,N) for N in Ns])
err=np.array([np.abs(I-Ms[-1]) for I in Ms[0:-1]])
range(m,n)
. In python, you can loop over pretty much any list or array. np.abs
is the absolute value functionMs[-1]
is the last element of Ms
, Ms[-2]
is second to last, etc.Ms[0:-1]
consists of all elements of Ms
except the last.Finally, we plot the results with appropriate labels and a title.
plt.plot(np.log10(Ns[0:-1]),np.log10(err))
plt.xlabel(r'$\log_{10}(N)$')
plt.ylabel(r'$\log_{10}({\rm err}(N))$')
plt.title(r'Quadrature Error vs. Grid Size for the Midpoint Rule')
r
.We should probably check to see whether the slope of the line above is close to the slope predicted by theory, which is $m=-2$.
fit_line=LinearRegression().fit(np.log10(Ns[0:-1]).reshape(-1,1),np.log10(err))
print('Slope of least square linear fit is m={0:1.2f}'.format(fit_line.coef_[0]))
LinearRegression
is a class and fit
is one of its methods, but for now you can imagine that LinearRegression().fit
is a function with an odd name. LinearRegression().fit
takes two arguments: X
and y
, the independent and dependent variables. It is designed to handle the case where the independent variable is multi-dimensional, so X
must be a two-dimensional array! (Each row X[i,:]
of X
is a single value of the multi-dimensional independent variable. ) The array np.log10(Ns[0:-1]).reshape(-1,1)
is a version of np.log10(Ns[0:-1])
with shape (13,1)
instead of (13,)
. This can be a bit technical...but see the error message below to confirm that the fit won't work unless we reshape the array.LinearRegression().fit
returns a one-dimensional array of regression coefficients. In our case, the array contains only one element, but we still have to extract that one element. Otherwise, the format method will give us an error, since it is expecting a float not an array of floats containing a single element. That is why I have fit_line.coef_[0]
above instead of just fit_line.coef_
.Here is the error from LinearRegression().fit
if you give inputs with the wrong number of dimensions.
fit_line=LinearRegression().fit(np.log10(Ns[0:-1]),np.log10(err))
Notice that since scikit-learn is a well written piece of software, it tells you what is wrong with your arguments and what to do about it.
Now here is the error you get from format
if you pass an array with one element instead of a scalar:
print('Slope of least square linear fit is m={0:1.2f}'.format(fit_line.coef_))
I guess this might be harder to debug, since the message is not as instructive. But it at least tells you that you have passed format()
an argument of the wrong type.