Hello, I met an issue when trying to convert my old dolfin code into DOLFINx. Here I was trying to assemble the Jacobian matrix with Dirichlet boundary conditions, but got different results with the two libraries. In the old dolfin code, I used the assemble_system()
which would preserve the symmetry of the Jacobian matrix (the A
matrix in the code example). While in DOLFINx, I used assemble_matrix()
from dolfinx.fem.petsc
, where the symmetry is also preserved, but the numerical values are different.
I also noticed that the ordering of function DOFs seems to be different in DOLFINx, but still that would only lead to index changes in the matrix while the values should stay the same.
Here are the minimal examples of my implementations:
DOLFINx code:
version - commit 92202566271a6c80a2ec3a2cade1bbb60c6e5b9e
import dolfinx
import ufl
import numpy as np
from mpi4py import MPI
n = 2
mesh = dolfinx.mesh.create_unit_square(MPI.COMM_WORLD, n, n)
V = dolfinx.fem.FunctionSpace(mesh, ('CG', 1))
VF = dolfinx.fem.FunctionSpace(mesh, ('DG', 0))
u = dolfinx.fem.Function(V)
v = ufl.TestFunction(V)
# Define the source term
class Expression_f:
def __init__(self):
self.alpha = 1e-6
def eval(self, x):
return (1/(1+self.alpha*4*np.power(np.pi,4))*
np.sin(np.pi*x[0])*np.sin(np.pi*x[1]))
f_analytic = Expression_f()
f = dolfinx.fem.Function(VF)
f.interpolate(f_analytic.eval)
# Apply zero boundary condition on the outer boundary
tdim = mesh.topology.dim
fdim = tdim - 1
mesh.topology.create_connectivity(fdim, tdim)
boundary_facets = np.flatnonzero(
dolfinx.mesh.compute_boundary_facets(
mesh.topology))
boundary_dofs = dolfinx.fem.locate_dofs_topological(V, fdim, boundary_facets)
ubc = dolfinx.fem.Function(V)
ubc.vector.set(0.0)
bc = [dolfinx.fem.dirichletbc(ubc, boundary_dofs)]
# Variational form of Poisson's equation
res = (ufl.inner(ufl.grad(u),ufl.grad(v))-f*v)*ufl.dx
# Quantity of interest: will be used as the Jacobian in adjoint method
dRdu = ufl.derivative(res, u)
# Option 1: assemble A and b seperately
A = dolfinx.fem.petsc.assemble_matrix(dolfinx.fem.form(dRdu), bcs=bc)
def convertToDense(A_petsc):
"""
Convert the PETSc matrix to a dense numpy array
for debugging purposes
"""
A_petsc.assemble()
A_dense = A_petsc.convert("dense")
return A_dense.getDenseArray()
print(" ------ Matrix A by DOLFINx ------- ")
print(convertToDense(A))
where the results are:
------ Matrix A by DOLFINx -------
[[1. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 1. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 1. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 4. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 1. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 1. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 1. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 1. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 1.]]
old dolfin code:
from dolfin import *
n = 2
mesh = UnitSquareMesh(n, n)
V = FunctionSpace(mesh, 'CG', 1)
VF = FunctionSpace(mesh, 'DG', 0)
u = Function(V)
v = TestFunction(V)
# Define the source term
w = Expression("sin(pi*x[0])*sin(pi*x[1])", degree=3)
alpha = Constant(1e-6)
f_analytic = Expression("1/(1+alpha*4*pow(pi,4))*w", w=w, alpha=alpha, degree=3)
f = interpolate(f_analytic, VF)
# Apply zero boundary condition on the outer boundary
bc = DirichletBC(V, Constant(0.0), "on_boundary")
# Variational form of Poisson's equation
res = (inner(grad(u),grad(v))-f*v)*dx
# Quantity of interest: will be used as the Jacobian in adjoint method
dRdu = derivative(res, u)
# Option 1: assemble A and b seperately
A = assemble(dRdu)
b = assemble(res)
bc.apply(A,b)
# Option 2: assemble the system
A_,b_ = assemble_system(dRdu, res, bcs=bc)
def convertToDense(A_petsc):
"""
Convert the PETSc matrix to a dense numpy array
for debugging purposes
"""
A_petsc.assemble()
A_dense = A_petsc.convert("dense")
return A_dense.getDenseArray()
print(" ------ Matrix A by old dolfin - assemble ------- ")
print(convertToDense(as_backend_type(A).mat()))
print(" ------ Matrix A by old dolfin - assemble_system ------- ")
print(" ----------- expected results ---------------- "
print(convertToDense(as_backend_type(A_).mat()))
which gives:
------ Matrix A by old dolfin - assemble -------
[[ 1. 0. 0. 0. 0. 0. 0. 0. 0.]
[ 0. 1. 0. 0. 0. 0. 0. 0. 0.]
[ 0. 0. 1. 0. 0. 0. 0. 0. 0.]
[ 0. 0. 0. 1. 0. 0. 0. 0. 0.]
[ 0. -1. -1. 0. 4. 0. -1. -1. 0.]
[ 0. 0. 0. 0. 0. 1. 0. 0. 0.]
[ 0. 0. 0. 0. 0. 0. 1. 0. 0.]
[ 0. 0. 0. 0. 0. 0. 0. 1. 0.]
[ 0. 0. 0. 0. 0. 0. 0. 0. 1.]]
------ Matrix A by old dolfin - assemble_system -------
----------- expected results ----------------
[[1. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 3. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 3. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 2. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 4. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 2. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 3. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 3. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 1.]]
The matrix by old dolfin using assemble_system
is what I want eventually in DOLFINx. I guess I’m mostly confused by the procedures of assemble_system
. My understanding of assembly in FEM would be removing/zero-ing rows and cols in the LHS matrix, and applying lifting to the RHS vector. So, I don’t quite understand how the numbers like 3s and 2s are computed since they were not there in the original stiffness matrix. And I was also looking at this related post, where someone explained the difference between assemble
+bc.apply()
and assemble_system
, but that still doesn’t explain the value changes.
Please leave a comment if you have any thoughts on the assemble
methods, or suggestions on how to get the same results in DOLFINx!
Thanks ahead!