scipy.sparse will be used for sparse matrix construction
import numpy as np
import scipy as sp
import matplotlib.pyplot as plt
import scipy.sparse as sparse
import scipy.sparse.linalg as sla
%matplotlib inline
V is a list of vertices. E is a list of elements (segments).
nx = 4 # number of poitns
V = np.linspace(0,1,nx) # list of vertices
E = np.zeros((nx-1,2), dtype=int) # list of elements
E[:,0] = np.arange(0,nx-1)
E[:,1] = np.arange(1,nx)
h = V[1] - V[0] # mesh spacing
Steps for each element:
Ak = (1/h) * np.array([1, -1, -1, 1])
Fk = (h/2) * np.array([1, 1])
rows = []
cols = []
vals = []
frows = []
fcols = [] # all zero
fvals = []
for tau in E:
rows += [tau[0], tau[0], tau[1], tau[1]]
cols += [tau[0], tau[1], tau[0], tau[1]]
vals += Ak.tolist()
frows += [tau[0], tau[1]]
fcols += [0, 0]
fvals += Fk.tolist()
A = sparse.coo_matrix((vals, (rows, cols))).tocsr()
F = sparse.coo_matrix((fvals, (frows, fcols))).tocsr().toarray()
Think about the line [tau[0], tau[0], tau[1], tau[1]]. For the first element this would be [0, 0, 1, 1] while [tau[0], tau[1], tau[0], tau[1]] would be [0, 1, 0, 1]
Also notice that rows and cols have some duplicate entries:
print(rows)
print(cols)
When we run the sparse matrix assembly:
A = sparse.coo_matrix((vals, (rows, cols)))
print(A.row)
print(A.col)
Notice the values are still duplicated. When converted to CSR (and then back to COO), then the duplicate entries are summed:
A = sparse.coo_matrix((vals, (rows, cols))).tocsr().tocoo()
print(A.row)
print(A.col)
This trick is used for the right-hand side even though the right-hand side F is a vector (i.e. a matrix with one column):
F = sparse.coo_matrix((fvals, (frows, fcols)))
print(F.row)
print(F.data)
F = sparse.coo_matrix((fvals, (frows, fcols))).tocsr().toarray()
print(F)
Here we want to make sure that the three element case makes sense. Since we constructed by hand: $$ A = \begin{bmatrix} 1 & -1 & 0 & 0\\ 0 & 2 &-1 & 0\\ 1 & -1 & 2 &-1\\ 0 & 0 &-1 & 1\\ \end{bmatrix} $$
print(A.toarray()*h)
print(F*2/h)
but this is singular!
nx = 100
V = np.linspace(0,1,nx)
E = np.zeros((nx-1,2), dtype=int)
E[:,0] = np.arange(0,nx-1)
E[:,1] = np.arange(1,nx)
h = V[1] - V[0]
Let's also add a more complex right-hand side:
def f(x):
return np.exp(-(x-0.5)**2 / 0.001)
plt.plot(V, f(V), 'o-', lw=3)
Here we're going to make sure that element tau doesn't have a boundary basis. If does, then we'll ignore it. So we'll loop over every combination of basis functions in tau. Here we convert Ak to an array to make indexing easier.
Ak = (1/h) * np.array([[1, -1], [-1, 1]])
Fk = (h/2) * np.array([1, 1])
rows = []
cols = []
vals = []
frows = []
fcols = []
fvals = []
for tau in E:
for k1 in range(2):
for k2 in range(2):
# tau[k1] will be the left basis function
# tau[k2] will be the right basis function
if tau[k1]!=0 and tau[k2]!=0 and tau[k1]!=(nx-1) and tau[k2]!=(nx-1):
rows += [tau[k1]]
cols += [tau[k2]]
vals += [Ak[k1,k2]]
if tau[k1]!=0 and tau[k1]!=(nx-1):
frows += [tau[k1]]
fcols += [0]
fvals += [(h/2) * f(V[tau[k1]])]
rows = np.array(rows)
cols = np.array(cols)
frows = np.array(frows)
fcols = np.array(fcols)
A = sparse.coo_matrix((vals, (rows-1, cols-1))).tocsr()
F = sparse.coo_matrix((fvals, (frows-1, fcols))).tocsr().toarray()
One thing to notice is that we need to shift the indices since zero is not a degree of freedom (due to the boundary condition). Thus rows-1 is used instead of rows.
u = sla.spsolve(A, F)
Here it's important to note that u is defined at the degrees of freedom. Since Dirichlet conditions are imposed on the left and on the right, this removed two dofs from the problem:
print(len(V))
print(len(u))
So we push a zero onto each end of u:
u = np.hstack(([0], u, [0]))
plt.plot(V, u, 'o-', lw=3)