Scipy.sparse and problems

There are seven available sparse matrix types in scipy.sparse:

  • csc_matrix: Compressed Sparse Column format
  • csr_matrix: Compressed Sparse Row format
  • bsr_matrix: Block Sparse Row format
  • lil_matrix: List of Lists format
  • dok_matrix: Dictionary of Keys format
  • coo_matrix: COOrdinate format (aka IJV, triplet format)
  • dia_matrix: DIAgonal format

As indicated by the excellent documentation, the dok_matrix or lil_matrix formats are preferable to construct matrices as they support basic slicing and indexing similar to a standard NumPy array.

You will notice that the FD matrix we have constructed for the Poisson problem is composed entirely of diagonal elements, as is often the case. If you were constructing a similar matrix in MATLAB, you would use the spdiags function, and scipy.sparse has its own equivalent. However, all the scipy.sparse formats also have special methods setdiag which provide a more object-orientated method of doing the same thing.

Scipy has a few different direct solvers for sparse matrics, given below:

spsolve: This solves Ax=bAx=b where AA is converted into CSC or CSR form

spsolve_triangular: Solves Ax=bAx=b, where AA is assumed to be triangular.

factorized: This computes the LULU decomposition of the input matrix AA, returning a Python function that can be called to solve Ax=bAx = b

splu: This computes the LULU decomposition of the input matrix AA using the popular SuperLU library. It returns a Python object of class SuperLU, that has a solve method you can use to solve Ax=bAx = b

Note, scipy.sparse.linalg also has many iterative solvers, which we will investigate further in the next chapter.

Problems

Your goal for this problem is to construct the FD matrix AA given above, using scipy.sparse, and:

Question
  1. Visualise the matrix AA using the Matplotlib spy plot
Expand for solution
Solution
import scipy.sparse.linalg
import scipy.sparse as sp
import matplotlib.pylab as plt
import numpy as np

N = 100
e = np.ones(N, dtype=float)
A = sp.spdiags([e, -2*e, e], [-1, 0, 1], N, N, format='csc')

plt.spy(A)
plt.show()
Question
  1. Solve the Poisson problem using:
    • f(x)=2cos(x)/exf(x) = 2 \cos(x) / e^x, with boundary conditions g(0)=0g(0) = 0 and g(2π)=0g(2 \pi)=0. The analytical solution is ua(x)=sin(x)/exu_{a}(x) = -\sin(x) / e^x.
    • f(x)=2sin(x)/exf(x) = 2 \sin(x) / e^x, with boundary conditions g(0)=1g(0) = 1 and g(2π)=1/e2πg(2 \pi)=1 / e^{2 \pi}. The analytical solution is ua(x)=cos(x)/exu_{a}(x) = \cos(x) / e^x
Expand for solution
Solution
L = 2*np.pi
x = np.linspace(0, L, N+2)
h = x[1] - x[0]
fcos = 2 * np.cos(x) / np.exp(x)
analytical_cos = -np.sin(x) / np.exp(x)
fsin = 2 * np.sin(x) / np.exp(x)
analytical_sin = np.cos(x) / np.exp(x)

# Use sparse lu decomposition, matrix is tridiagonal so no fill-in
splu = sp.linalg.splu(A / h**2)
fig, axs = plt.subplots(1, 2)
axs[0].spy(splu.L)
axs[0].set_title('L')
axs[1].spy(splu.U)
axs[1].set_title('U')
plt.show()

# Use decomposition to solve both problems
for f, a in zip([fcos, fsin], [analytical_cos, analytical_sin]):
    b = f[1:-1]
    b[0] -= a[0] / h**2
    b[-1] -= a[-1] / h**2
    v = splu.solve(b)
    plt.plot(x[1:-1], v, label='solution')
    plt.plot(x, a, label='analytical')
    plt.legend()
    plt.show()
Question
  1. Vary the number of discretisation points NN and calculate AAAA using both sparse and dense matrices. For each NN calculate the time to calculate the matix multiplicatiion using Python's time.perf_counter, and plot execution time versus NN for dense and sparse matrix multiplication. Comment on how the time varies with NN.
Expand for solution
Solution
import time

num = 100
times = np.empty(num, dtype=float)
times_dense = np.empty(num, dtype=float)
times_dense[:] = np.nan
Ns = np.logspace(0.5, 6, num=num, dtype=int)
for i, N in enumerate(Ns):
    e = np.ones(N, dtype=float)
    A = sp.spdiags([e, -2*e, e], [-1, 0, 1], N, N, format='csc')

    t0 = time.perf_counter()
    AA = A @ A
    t1 = time.perf_counter()
    times[i] = t1 - t0

    if N < 2000:
        Adense = A.toarray()
        t0 = time.perf_counter()
        AA = Adense @ Adense
        t1 = time.perf_counter()
        times_dense[i] = t1 - t0

plt.clf()
plt.loglog(Ns, times, label='sparse')
plt.loglog(Ns, times_dense, label='dense')
plt.xlabel('N')
plt.ylabel('time taken')
plt.legend()
plt.show()
Question
  1. Vary the number of discretisation points NN and solve the Poisson problem with varying NN, and with using both the sparse and direct LULU solvers. For each NN record the time taken for both the dense and sparse solvers, and record the numerical error vva2||\mathbf{v} - \mathbf{v}_a||_2. Generate plots of both error and time versus NN, and comment on how they vary with NN
Expand for solution
Solution
times = np.empty(num, dtype=float)
times_dense = np.empty(num, dtype=float)
times_dense[:] = np.nan

for i, N in enumerate(Ns):
    e = np.ones(N, dtype=float)
    A = sp.spdiags([e, -2*e, e], [-1, 0, 1], N, N, format='csc')

    x = np.linspace(0, L, N+2)
    h = x[1] - x[0]
    fcos = 2 * np.cos(x) / np.exp(x)
    analytical_cos = -np.sin(x) / np.exp(x)

    A /= h**2

    b = fcos[1:-1]
    b[0] -= analytical_cos[0] / h**2
    b[-1] -= analytical_cos[-1] / h**2

    t0 = time.perf_counter()
    splu = sp.linalg.splu(A)
    v = splu.solve(b)
    t1 = time.perf_counter()
    times[i] = t1 - t0
    if N < 2000:
        Adense = A.toarray()
        t0 = time.perf_counter()
        lu = scipy.linalg.lu_factor(Adense)
        v = scipy.linalg.lu_solve(lu, b)
        t1 = time.perf_counter()
        times_dense[i] = t1 - t0

plt.clf()
plt.loglog(Ns, times, label='sparse LU')
plt.loglog(Ns, times_dense, label='dense LU')
plt.xlabel('N')
plt.ylabel('time taken')
plt.legend()
plt.show()