SciPy - Library of scientific algorithms for Python

References

This notebook has been gathered and inspired by the Lecture-3-Scipy notebook by J.R. Johansson (robert@riken.jp): http://dml.riken.jp/~rob/

The original version of the notebook can be found here: Lecture-3-Scipy.ipynb.


In [1]:
# what is this line all about?
%pylab inline
from IPython.display import Image


Populating the interactive namespace from numpy and matplotlib

Introduction

The SciPy framework builds on top of the low-level NumPy framework for multidimensional arrays, and provides a large number of higher-level scientific algorithms. Some of the topics that SciPy covers are:

Each of these submodules provides a number of functions and classes that can be used to solve problems in their respective topics.

In this lecture we will look at how to use some of these subpackages.

To access the SciPy package in a Python program, we start by importing everything from the scipy module.


In [2]:
from scipy import *

If we only need to use part of the SciPy framework we can selectively include only those modules we are interested in. For example, to include the linear algebra package under the name la, we can do:


In [3]:
import scipy.linalg as la

Special functions

A large number of mathematical special functions are important for many computional physics problems. SciPy provides implementations of a very extensive set of special functions. For details, see the list of functions in the reference documention at http://docs.scipy.org/doc/scipy/reference/special.html#module-scipy.special.


In [1]:
from scipy.special import factorial, binom

In [8]:
factorial(6)


Out[8]:
array(720.0)

In [9]:
binom(6, 2)


Out[9]:
15.0

Since $\frac{n!}{k!(n-k)!} = \binom{n}{k}$


In [10]:
factorial(6)/(factorial(2)*factorial(6-2)) == binom(6, 2)


Out[10]:
True

In [11]:
binom??
Type: ufunc String form: File: /Users/valerio/anaconda/lib/python3.4/site-packages/numpy/__init__.py Definition: binom(self, *args, **kwargs) Docstring: binom(x1, x2[, out]) binom(n, k) Binomial coefficient Class docstring: Functions that operate element by element on whole arrays. To see the documentation for a specific ufunc, use np.info(). For example, np.info(np.sin). Because ufuncs are written in C (for speed) and linked into Python with NumPy's ufunc facility, Python's help() function finds this page whenever help() is called on a ufunc. A detailed explanation of ufuncs can be found in the "ufuncs.rst" file in the NumPy reference guide. Unary ufuncs: ============= op(X, out=None) Apply op to X elementwise Parameters ---------- X : array_like Input array. out : array_like An array to store the output. Must be the same shape as `X`. Returns ------- r : array_like `r` will have the same shape as `X`; if out is provided, `r` will be equal to out. Binary ufuncs: ============== op(X, Y, out=None) Apply `op` to `X` and `Y` elementwise. May "broadcast" to make the shapes of `X` and `Y` congruent. The broadcasting rules are: * Dimensions of length 1 may be prepended to either array. * Arrays may be repeated along dimensions of length 1. Parameters ---------- X : array_like First input array. Y : array_like Second input array. out : array_like An array to store the output. Must be the same shape as the output would have. Returns ------- r : array_like The return value; if out is provided, `r` will be equal to out.

Integration

Numerical integration: quadrature

Numerical evaluation of a function of the type

$\displaystyle \int_a^b f(x) dx$

is called numerical quadrature, or simply quadature. SciPy provides a series of functions for different kind of quadrature, for example the quad, dblquad and tplquad for single, double and triple integrals, respectively.


In [8]:
from scipy.integrate import quad, dblquad, tplquad

The quad function takes a large number of optional arguments, which can be used to fine-tune the behaviour of the function (try help(quad) for details).

The basic usage is as follows:


In [9]:
# define a simple function for the integrand
def f(x):
    return x

In [10]:
x_lower = 0 # the lower limit of x
x_upper = 1 # the upper limit of x

val, abserr = quad(f, x_lower, x_upper)

print "integral value =", val, ", absolute error =", abserr


integral value = 0.5 , absolute error = 5.55111512313e-15

If we need to pass extra arguments to integrand function we can use the args keyword argument:


In [11]:
def integrand(x, n):
    """
    Bessel function of first kind and order n. 
    """
    return jn(n, x)


x_lower = 0  # the lower limit of x
x_upper = 10 # the upper limit of x

val, abserr = quad(integrand, x_lower, x_upper, args=(3,))

print val, abserr


0.736675137081 9.38925687719e-13

For simple functions we can use a lambda function (name-less function) instead of explicitly defining a function for the integrand:


In [12]:
val, abserr = quad(lambda x: exp(-x ** 2), -Inf, Inf)

print "numerical  =", val, abserr

analytical = sqrt(pi)
print "analytical =", analytical


numerical  = 1.77245385091 1.42026367809e-08
analytical = 1.77245385091

As show in the example above, we can also use 'Inf' or '-Inf' as integral limits.

Higher-dimensional integration works in the same way:


In [13]:
def integrand(x, y):
    return exp(-x**2-y**2)

x_lower = 0  
x_upper = 10
y_lower = 0
y_upper = 10

val, abserr = dblquad(integrand, x_lower, x_upper, lambda x : y_lower, lambda x: y_upper)

print val, abserr


0.785398163397 1.63822994214e-13

Note how we had to pass lambda functions for the limits for the y integration, since these in general can be functions of x.

Ordinary differential equations (ODEs)

SciPy provides two different ways to solve ODEs: An API based on the function odeint, and object-oriented API based on the class ode. Usually odeint is easier to get started with, but the ode class offers some finer level of control.

Here we will use the odeint functions. For more information about the class ode, try help(ode). It does pretty much the same thing as odeint, but in an object-oriented fashion.

To use odeint, first import it from the scipy.integrate module


In [14]:
from scipy.integrate import odeint, ode

A system of ODEs are usually formulated on standard form before it is attacked numerically. The standard form is:

$y' = f(y, t)$

where

$y = [y_1(t), y_2(t), ..., y_n(t)]$

and $f$ is some function that gives the derivatives of the function $y_i(t)$. To solve an ODE we need to know the function $f$ and an initial condition, $y(0)$.

Note that higher-order ODEs can always be written in this form by introducing new variables for the intermediate derivatives.

Once we have defined the Python function f and array y_0 (that is $f$ and $y(0)$ in the mathematical formulation), we can use the odeint function as:

y_t = odeint(f, y_0, t)

where t is and array with time-coordinates for which to solve the ODE problem. y_t is an array with one row for each point in time in t, where each column corresponds to a solution y_i(t) at that point in time.

We will see how we can implement f and y_0 in Python code in the examples below.

Fourier transform

Fourier transforms are one of the universal tools in computational physics, which appear over and over again in different contexts. SciPy provides functions for accessing the classic FFTPACK library from NetLib, which is an efficient and well tested FFT library written in FORTRAN. The SciPy API has a few additional convenience functions, but overall the API is closely related to the original FORTRAN library.

To use the fftpack module in a python program, include it using:


In [29]:
from scipy.fftpack import *

To demonstrate how to do a fast Fourier transform with SciPy, let's look at the FFT of the solution to the damped oscillator from the previous section:


In [30]:
N = len(t)
dt = t[1]-t[0]

# calculate the fast fourier transform
# y2 is the solution to the under-damped oscillator from the previous section
F = fft(y2[:,0]) 

# calculate the frequencies for the components in F
w = fftfreq(N, dt)

In [31]:
fig, ax = subplots(figsize=(9,3))
ax.plot(w, abs(F));


Since the signal is real, the spectrum is symmetric. We therefore only need to plot the part that corresponds to the postive frequencies. To extract that part of the w and F we can use some of the indexing tricks for NumPy arrays that we saw in the Numpy Lecture:


In [32]:
indices = where(w > 0) # select only indices for elements that corresponds to positive frequencies
w_pos = w[indices]
F_pos = F[indices]

In [33]:
fig, ax = subplots(figsize=(9,3))
ax.plot(w_pos, abs(F_pos))
ax.set_xlim(0, 5);


Linear algebra

The linear algebra module contains a lot of matrix related functions, including linear equation solving, eigenvalue solvers, matrix functions (for example matrix-exponentiation), a number of different decompositions (SVD, LU, cholesky), etc.

Detailed documetation is available at: http://docs.scipy.org/doc/scipy/reference/linalg.html

Here we will look at how to use some of these functions:

Linear equation systems

Linear equation systems on the matrix form

$A x = b$

where $A$ is a matrix and $x,b$ are vectors can be solved like:


In [34]:
A = array([[1,2,3], [4,5,6], [7,8,9]])
b = array([1,2,3])

In [35]:
x = solve(A, b)

x


Out[35]:
array([-0.33333333,  0.66666667,  0.        ])

In [36]:
# check
dot(A, x) - b


Out[36]:
array([ -1.11022302e-16,   0.00000000e+00,   0.00000000e+00])

We can also do the same with

$A X = B$

where $A, B, X$ are matrices:


In [37]:
A = rand(3,3)
B = rand(3,3)

In [38]:
X = solve(A, B)

In [39]:
X


Out[39]:
array([[ 2.28587973,  5.88845235,  1.6750663 ],
       [-4.88205838, -5.26531274, -1.37990347],
       [ 1.75135926, -2.05969998, -0.09859636]])

In [40]:
# check
norm(dot(A, X) - B)


Out[40]:
6.2803698347351007e-16

Eigenvalues and eigenvectors

The eigenvalue problem for a matrix $A$:

$\displaystyle A v_n = \lambda_n v_n$

where $v_n$ is the $n$th eigenvector and $\lambda_n$ is the $n$th eigenvalue.

To calculate eigenvalues of a matrix, use the eigvals and for calculating both eigenvalues and eigenvectors, use the function eig:


In [41]:
evals = eigvals(A)

In [42]:
evals


Out[42]:
array([ 1.06633891+0.j        , -0.12420467+0.10106325j,
       -0.12420467-0.10106325j])

In [43]:
evals, evecs = eig(A)

In [44]:
evals


Out[44]:
array([ 1.06633891+0.j        , -0.12420467+0.10106325j,
       -0.12420467-0.10106325j])

In [45]:
evecs


Out[45]:
array([[ 0.89677688+0.j        , -0.30219843-0.30724366j,
        -0.30219843+0.30724366j],
       [ 0.35446145+0.j        ,  0.79483507+0.j        ,  0.79483507+0.j        ],
       [ 0.26485526+0.j        , -0.20767208+0.37334563j,
        -0.20767208-0.37334563j]])

The eigenvectors corresponding to the $n$th eigenvalue (stored in evals[n]) is the $n$th column in evecs, i.e., evecs[:,n]. To verify this, let's try mutiplying eigenvectors with the matrix and compare to the product of the eigenvector and the eigenvalue:


In [46]:
n = 1

norm(dot(A, evecs[:,n]) - evals[n] * evecs[:,n])


Out[46]:
1.3964254612015911e-16

There are also more specialized eigensolvers, like the eigh for Hermitian matrices.

Matrix operations


In [47]:
# the matrix inverse
inv(A)


Out[47]:
array([[-1.38585633,  1.36837431,  6.03633364],
       [ 3.80855289, -4.76960426, -5.2571037 ],
       [ 0.0689213 ,  2.4652602 , -2.5948838 ]])

In [48]:
# determinant
det(A)


Out[48]:
0.027341548212627968

In [49]:
# norms of various orders
norm(A, ord=2), norm(A, ord=Inf)


Out[49]:
(1.1657807164173386, 1.7872032588446576)

Sparse matrices

Sparse matrices are often useful in numerical simulations dealing with large systems, if the problem can be described in matrix form where the matrices or vectors mostly contains zeros. Scipy has a good support for sparse matrices, with basic linear algebra operations (such as equation solving, eigenvalue calculations, etc).

There are many possible strategies for storing sparse matrices in an efficient way. Some of the most common are the so-called coordinate form (COO), list of list (LIL) form, and compressed-sparse column CSC (and row, CSR). Each format has some advantanges and disadvantages. Most computational algorithms (equation solving, matrix-matrix multiplication, etc) can be efficiently implemented using CSR or CSC formats, but they are not so intuitive and not so easy to initialize. So often a sparse matrix is initially created in COO or LIL format (where we can efficiently add elements to the sparse matrix data), and then converted to CSC or CSR before used in real calcalations.

For more information about these sparse formats, see e.g. http://en.wikipedia.org/wiki/Sparse_matrix

When we create a sparse matrix we have to choose which format it should be stored in. For example,


In [50]:
from scipy.sparse import *

In [51]:
# dense matrix
M = array([[1,0,0,0], [0,3,0,0], [0,1,1,0], [1,0,0,1]]); M


Out[51]:
array([[1, 0, 0, 0],
       [0, 3, 0, 0],
       [0, 1, 1, 0],
       [1, 0, 0, 1]])

In [52]:
# convert from dense to sparse
A = csr_matrix(M); A


Out[52]:
<4x4 sparse matrix of type '<type 'numpy.int64'>'
	with 6 stored elements in Compressed Sparse Row format>

In [53]:
# convert from sparse to dense
A.todense()


Out[53]:
matrix([[1, 0, 0, 0],
        [0, 3, 0, 0],
        [0, 1, 1, 0],
        [1, 0, 0, 1]])

More efficient way to create sparse matrices: create an empty matrix and populate with using matrix indexing (avoids creating a potentially large dense matrix)


In [54]:
A = lil_matrix((4,4)) # empty 4x4 sparse matrix
A[0,0] = 1
A[1,1] = 3
A[2,2] = A[2,1] = 1
A[3,3] = A[3,0] = 1
A


Out[54]:
<4x4 sparse matrix of type '<type 'numpy.float64'>'
	with 6 stored elements in LInked List format>

In [55]:
A.todense()


Out[55]:
matrix([[ 1.,  0.,  0.,  0.],
        [ 0.,  3.,  0.,  0.],
        [ 0.,  1.,  1.,  0.],
        [ 1.,  0.,  0.,  1.]])

Converting between different sparse matrix formats:


In [56]:
A


Out[56]:
<4x4 sparse matrix of type '<type 'numpy.float64'>'
	with 6 stored elements in LInked List format>

In [57]:
A = csr_matrix(A); A


Out[57]:
<4x4 sparse matrix of type '<type 'numpy.float64'>'
	with 6 stored elements in Compressed Sparse Row format>

In [58]:
A = csc_matrix(A); A


Out[58]:
<4x4 sparse matrix of type '<type 'numpy.float64'>'
	with 6 stored elements in Compressed Sparse Column format>

We can compute with sparse matrices like with dense matrices:


In [59]:
A.todense()


Out[59]:
matrix([[ 1.,  0.,  0.,  0.],
        [ 0.,  3.,  0.,  0.],
        [ 0.,  1.,  1.,  0.],
        [ 1.,  0.,  0.,  1.]])

In [60]:
(A * A).todense()


Out[60]:
matrix([[ 1.,  0.,  0.,  0.],
        [ 0.,  9.,  0.,  0.],
        [ 0.,  4.,  1.,  0.],
        [ 2.,  0.,  0.,  1.]])

In [61]:
dot(A, A).todense()


Out[61]:
matrix([[ 1.,  0.,  0.,  0.],
        [ 0.,  9.,  0.,  0.],
        [ 0.,  4.,  1.,  0.],
        [ 2.,  0.,  0.,  1.]])

In [62]:
v = array([1,2,3,4])[:,newaxis]; v


Out[62]:
array([[1],
       [2],
       [3],
       [4]])

In [63]:
# sparse matrix - dense vector multiplication
A * v


Out[63]:
array([[ 1.],
       [ 6.],
       [ 5.],
       [ 5.]])

In [64]:
# same result with dense matrix - dense vector multiplcation
A.todense() * v


Out[64]:
matrix([[ 1.],
        [ 6.],
        [ 5.],
        [ 5.]])

Optimization

Optimization (finding minima or maxima of a function) is a large field in mathematics, and optimization of complicated functions or in many variables can be rather involved. Here we will only look at a few very simple cases. For a more detailed introduction to optimization with SciPy see: http://scipy-lectures.github.com/advanced/mathematical_optimization/index.html

To use the optimization module in scipy first include the optimize module:


In [65]:
from scipy import optimize

Finding a minima

Let's first look at how to find the minima of a simple function of a single variable:


In [66]:
def f(x):
    return 4*x**3 + (x-2)**2 + x**4

In [67]:
fig, ax  = subplots()
x = linspace(-5, 3, 100)
ax.plot(x, f(x));


We can use the fmin_bfgs function (i.e., Broyden–Fletcher–Goldfarb–Shanno algorithm) to find the minima of a function:

fmin_bfgs(f, x0)

In [68]:
x_min = optimize.fmin_bfgs(f, -2)
x_min


Optimization terminated successfully.
         Current function value: -3.506641
         Iterations: 6
         Function evaluations: 30
         Gradient evaluations: 10
Out[68]:
array([-2.67298167])

In [69]:
optimize.fmin_bfgs(f, 0.5)


Optimization terminated successfully.
         Current function value: 2.804988
         Iterations: 3
         Function evaluations: 15
         Gradient evaluations: 5
Out[69]:
array([ 0.46961745])

We can also use the brent or fminbound functions. They have a bit different syntax and use different algorithms.


In [70]:
optimize.brent(f)


Out[70]:
0.46961743402759754

In [71]:
optimize.fminbound(f, -4, 2)


Out[71]:
-2.6729822917513886

Finding a solution to a function

To find the root for a function of the form $f(x) = 0$ we can use the fsolve function. It requires an initial guess:


In [100]:
omega_c = 3.0
def f(omega):
    # a transcendental equation: resonance frequencies of a low-Q SQUID terminated microwave resonator
    return tan(2*pi*omega) - omega_c/omega

In [104]:
fig, ax  = subplots(figsize=(10,4))
x = linspace(0, 3, 1000)
y = f(x)
mask = where(abs(y) > 50)
x[mask] = y[mask] = NaN # get rid of vertical line when the function flip sign
ax.plot(x, y)
ax.plot([0, 3], [0, 0], 'k')
ax.set_ylim(-5,5);



In [105]:
optimize.fsolve(f, 0.1)


Out[105]:
array([ 0.23743014])

In [108]:
optimize.fsolve(f, 0.6)


Out[108]:
array([ 0.71286972])

In [107]:
optimize.fsolve(f, 1.1)


Out[107]:
array([ 1.18990285])

Interpolation

Interpolation is simple and convenient in scipy: The interp1d function, when given arrays describing X and Y data, returns and object that behaves like a function that can be called for an arbitrary value of x (in the range covered by X), and it returns the corresponding interpolated y value:


In [110]:
from scipy.interpolate import *

In [111]:
def f(x):
    return sin(x)

In [112]:
n = arange(0, 10)  
x = linspace(0, 9, 100)

y_meas = f(n) + 0.1 * randn(len(n)) # simulate measurement with noise
y_real = f(x)

linear_interpolation = interp1d(n, y_meas)
y_interp1 = linear_interpolation(x)

cubic_interpolation = interp1d(n, y_meas, kind='cubic')
y_interp2 = cubic_interpolation(x)

In [114]:
fig, ax = subplots(figsize=(10,4))
ax.plot(n, y_meas, 'bs', label='noisy data')
ax.plot(x, y_real, 'k', lw=2, label='true function')
ax.plot(x, y_interp1, 'r', label='linear interp')
ax.plot(x, y_interp2, 'g', label='cubic interp')
ax.legend(loc=3);


Statistics

The scipy.stats module contains a large number of statistical distributions, statistical functions and tests. For a complete documentation of its features, see http://docs.scipy.org/doc/scipy/reference/stats.html.

There is also a very powerful python package for statistical modelling called statsmodels. See http://statsmodels.sourceforge.net for more details.


In [81]:
from scipy import stats

In [82]:
# create a (discreet) random variable with poissionian distribution

X = stats.poisson(3.5) # Poisson distribution for n=3.5

In [83]:
n = arange(0,15)

fig, axes = subplots(3,1, sharex=True)

# plot the probability mass function (PMF)
axes[0].step(n, X.pmf(n))

# plot the commulative distribution function (CDF)
axes[1].step(n, X.cdf(n))

# plot histogram of 1000 random realizations of the stochastic variable X
axes[2].hist(X.rvs(size=1000));



In [84]:
# create a (continous) random variable with normal distribution
Y = stats.norm()

In [85]:
x = linspace(-5,5,100)

fig, axes = subplots(3,1, sharex=True)

# plot the probability distribution function (PDF)
axes[0].plot(x, Y.pdf(x))

# plot the commulative distributin function (CDF)
axes[1].plot(x, Y.cdf(x));

# plot histogram of 1000 random realizations of the stochastic variable Y
axes[2].hist(Y.rvs(size=1000), bins=50);


Statistics:


In [86]:
X.mean(), X.std(), X.var() # poission distribution


Out[86]:
(3.5, 1.8708286933869707, 3.5)

In [87]:
Y.mean(), Y.std(), Y.var() # normal distribution


Out[87]:
(0.0, 1.0, 1.0)

Statistical tests

Test if two sets of (independent) random data comes from the same distribution:


In [88]:
t_statistic, p_value = stats.ttest_ind(X.rvs(size=1000), X.rvs(size=1000))

print "t-statistic =", t_statistic
print "p-value =", p_value


t-statistic = -0.244622880865
p-value = 0.806773564698

Since the p value is very large we cannot reject the hypothesis that the two sets of random data have different means.

To test if the mean of a single sample of data has mean 0.1 (the true mean is 0.0):


In [89]:
stats.ttest_1samp(Y.rvs(size=1000), 0.1)


Out[89]:
(-4.4661322772225356, 8.8726783620609218e-06)

Low p-value means that we can reject the hypothesis that the mean of Y is 0.1.


In [90]:
Y.mean()


Out[90]:
0.0

In [91]:
stats.ttest_1samp(Y.rvs(size=1000), Y.mean())


Out[91]:
(0.51679431628006112, 0.60541413382728715)

Further reading