I am trying to compute sensitivity of boundary condition using the adjoint method but getting the procedure wrong somewhere which I am unable to guess. I will appreciate if someone could point out my mistake.
For the MWE, I solve Laplace equation on a domain with two electrodes (holes) where the potential is applied. Due to geometrical symmetry and equal but opposite potential on the two, I expect the calculated sensitivity to also be equal but opposite in sign. But the result from the adjoint method suggests dissimilar magnitude. Finite difference calculation seems to pass the sanity check and suggests a number that is order of magnitude larger than the one suggested by adjoint method.
Thanks in advance for the help! Below is the MWE evaluated on dolfinx 0.9
import gmsh, dolfinx, ufl, mpi4py, petsc4py
from dolfinx.io import gmshio
import dolfinx.fem.petsc, dolfinx.nls.petsc
import matplotlib.pyplot as plt
import scifem
import numpy as np
gmsh.finalize()
gmsh.initialize()
gmsh.option.setNumber("General.Terminal", 0) # disable output messages
gdim = 2
occ = gmsh.model.occ
c1 = occ.addDisk(-0.2, 0, 0, 0.1, 0.1)
c2 = occ.addDisk(0.2, 0, 0, 0.1, 0.1)
fov = occ.addDisk(0, 0, 0, 1, 1)
cut = occ.cut([(gdim, fov)], [(gdim, c1), (gdim, c2)])
occ.synchronize()
# add_sub physical tags
all_doms = occ.getEntities(gdim)
for j, dom in enumerate(all_doms):
gmsh.model.addPhysicalGroup(dom[0], [dom[1]], j + 1) # create the main group/node
# number all boundaries
all_edges = occ.getEntities(gdim-1)
for j, edge in enumerate(all_edges):
gmsh.model.addPhysicalGroup(edge[0], [edge[1]], edge[1]) # create the main group/node
gmsh.model.mesh.generate(2)
gmsh.model.mesh.refine()
mesh, ct, ft = gmshio.model_to_mesh(gmsh.model, mpi4py.MPI.COMM_WORLD, 0, gdim)
Omega_V = dolfinx.fem.functionspace(mesh, ("CG", 1))
Omega_V0 = scifem.create_real_functionspace(mesh) # for electrodes
V_sol = dolfinx.fem.Function(Omega_V)
V = ufl.TrialFunction(Omega_V)
Vt = ufl.TestFunction(Omega_V)
n = ufl.FacetNormal(mesh)
ds = ufl.Measure("ds", domain=mesh, subdomain_data=ft)
dx = ufl.Measure("dx", domain=mesh, subdomain_data=ct)
R = ufl.inner(ufl.grad(V_sol), ufl.grad(Vt)) * dx
R += ufl.inner(dolfinx.fem.Constant(mesh, 0.0), Vt) * dx
V1 = dolfinx.fem.Function(Omega_V0) # electrode 1 (physical tag 1)
V2 = dolfinx.fem.Function(Omega_V0) # electrode 2 (physical tag 2)
V1.x.array[:] = 1.0
V2.x.array[:] = -1.0
alpha = 10
hE = ufl.Circumradius(mesh)
R += - ufl.inner(ufl.dot(n, ufl.grad(V_sol)), Vt) * ds((1,2)) - ufl.inner(V_sol, ufl.dot(n, ufl.grad(Vt))) * ds((1,2)) + alpha/hE*ufl.inner(V_sol, Vt) * ds((1,2))
R += ufl.inner(n, ufl.grad(Vt)) * V1 * ds(1) - alpha/hE*ufl.inner(V1, Vt) * ds(1)
R += ufl.inner(n, ufl.grad(Vt)) * V2 * ds(2) - alpha/hE*ufl.inner(V2, Vt) * ds(2)
# forward problem
dbc = []
problem = dolfinx.fem.petsc.NonlinearProblem(R, V_sol, dbc)
solver = dolfinx.nls.petsc.NewtonSolver(mpi4py.MPI.COMM_WORLD, problem)
solver.solve(V_sol)
# calculate energy / objective
J = 1/2*ufl.dot(ufl.grad(V_sol), ufl.grad(V_sol)) * dx
J0 = dolfinx.fem.assemble_scalar(dolfinx.fem.form(J))
print("Energy: {}".format(J0))
# solve the adjoint problem
lmbda = dolfinx.fem.Function(Omega_V)
dRdu = ufl.derivative(R, V_sol, V)
dRdu_adj = ufl.adjoint(dRdu)
dJdu = ufl.derivative(J, V_sol, Vt)
V_dbc = dolfinx.fem.Function(Omega_V)
ext_facets = dolfinx.mesh.exterior_facet_indices(mesh.topology)
ext_dofs = dolfinx.fem.locate_dofs_topological(Omega_V, gdim-1, ext_facets)
dbc_adj = [dolfinx.fem.dirichletbc(dolfinx.fem.Constant(mesh, 0.0), ext_dofs, Omega_V)]
adj_problem = dolfinx.fem.petsc.LinearProblem(ufl.replace(dRdu_adj, {V_sol: Vt}), -dJdu, u=lmbda, bcs=dbc_adj)
adj_problem.solve()
dJdV1 = dolfinx.fem.petsc.assemble_vector(dolfinx.fem.form(ufl.derivative(J, V1)))
dJdV1.assemble()
dJdV2 = dolfinx.fem.petsc.assemble_vector(dolfinx.fem.form(ufl.derivative(J, V2)))
dJdV2.assemble()
dRdV1_form = dolfinx.fem.form(ufl.adjoint(ufl.derivative(R, V1)))
dRdV1 = dolfinx.fem.petsc.assemble_matrix(dRdV1_form) # partial derivative
dRdV1.assemble()
dRdV2_form = dolfinx.fem.form(ufl.adjoint(ufl.derivative(R, V2)))
dRdV2 = dolfinx.fem.petsc.assemble_matrix(dRdV2_form) # partial derivative
dRdV2.assemble()
dJdV1 += dRdV1*lmbda.x.petsc_vec
dJdV1.ghostUpdate(addv=petsc4py.PETSc.InsertMode.INSERT, mode=petsc4py.PETSc.ScatterMode.FORWARD)
print("sensitivity of V1 from adjoint method:", dJdV1.array)
dJdV2 += dRdV2*lmbda.x.petsc_vec
dJdV2.ghostUpdate(addv=petsc4py.PETSc.InsertMode.INSERT, mode=petsc4py.PETSc.ScatterMode.FORWARD)
print("sensitivity of V2 from adjoint method:", dJdV2.array)
# cross-validation by forward difference
eps = 1e-6
V1.x.array[:] = 1+eps
V2.x.array[:] = -1
solver.solve(V_sol)
J1 = dolfinx.fem.assemble_scalar(dolfinx.fem.form(J))
print("Energy after perturbing V1: {}".format(J1))
print("Sensitivity of V1 from finite difference: {}".format((J1-J0)/eps))
V1.x.array[:] = 1
V2.x.array[:] = -1+eps
solver.solve(V_sol)
J2 = dolfinx.fem.assemble_scalar(dolfinx.fem.form(J))
print("Energy after perturbing V2: {}".format(J2))
print("Sensitivity of V2 from finite difference: {}".format((J2-J0)/eps))