This is certainly true, but often “adding a constraint” is understood to go hand-in-hand with the neccessity of incorporating a Lagrange multiplier that acts like the forcing to satisfy said constraint. Indeed, per your comment on solution uniqueness, the new solution (with constraint and Lagrange multiplier) does naturally not satisfy the PDE without the Lagrange multiplier forcing.
Applying that to @fea’s problem, you’d have to realize that what you’d actually be solving when you’re adding the constraint is:
\nabla^2 u = -1 + \lambda
u = 0 \ \ \ \ \forall (x,y) \in \partial\Omega
\int_\Omega u = - 0.035
With some constant \lambda.
If that is acceptable, then here follows the fixed version of the code. You were only missing the bcs in the assembly of the matrix:
from mpi4py import MPI
import dolfinx as df
import ufl
from petsc4py import PETSc
import numpy as np
from scifem import create_real_functionspace
from scifem.petsc import apply_lifting_and_set_bc
M = 20
mesh = df.mesh.create_unit_square(
MPI.COMM_WORLD, M, M, df.mesh.CellType.triangle, dtype=np.float64
)
V = df.fem.functionspace(mesh, ("Lagrange", 1))
#---- define boundary condition ----
fdim, tdim = mesh.topology.dim-1, mesh.topology.dim
mesh.topology.create_connectivity(fdim,tdim)
exterior_facets = df.mesh.exterior_facet_indices(mesh.topology)
exterior_dofs = df.fem.locate_dofs_topological(V,fdim,exterior_facets)
bc = df.fem.dirichletbc(df.fem.Constant(mesh,df.default_scalar_type(0)),exterior_dofs,V)
#---- Integral constraint ----
h = -0.035
# Forcing term
f = df.fem.Constant(mesh, df.default_scalar_type(-1))
# Real space
R = create_real_functionspace(mesh)
# Mixed space
W = ufl.MixedFunctionSpace(V, R)
u, lmbda = ufl.TrialFunctions(W)
du, dl = ufl.TestFunctions(W)
zero = df.fem.Constant(mesh, df.default_scalar_type(0.0))
#--------------------------------------------------
#---- Build Matrix --------------------------------
#--------------------------------------------------
# ---- Build components: bilinear form ----
a00 = ufl.inner(ufl.grad(u), ufl.grad(du)) * ufl.dx
a01 = ufl.inner(lmbda, du) * ufl.dx
a10 = ufl.inner(u, dl) * ufl.dx
# ---- Build components: linear form ----
L0 = ufl.inner(f, du) * ufl.dx
L1 = ufl.inner(zero, dl) * ufl.dx
# ---- Build matrix: A ----
bcs = [bc]
a = [[a00, a01], [a10, None]]
a_compiled = df.fem.form(a)
A = df.fem.petsc.assemble_matrix(a_compiled, bcs=bcs)
A.assemble()
# ---- Build matrix: L ----
L = [L0, L1]
L_compiled = df.fem.form(L)
#--------------------------------------------------
#---- Apply boundary conditions -------------------
#--------------------------------------------------
b = df.fem.petsc.assemble_vector(L_compiled, kind="mpi")
apply_lifting_and_set_bc(b, a_compiled, bcs=bcs)
#--------------------------------------------------
#---- Solve ---------------------------------------
#--------------------------------------------------
uh = df.fem.Function(V, name="u")
#---- Insert constraint into R ----
rh = df.fem.Function(R)
rh.x.array[0] = h
# Next we need to add this value to the existing right hand side vector.
# Therefore we create assign 0s to the primal space
b_real_space = b.duplicate()
uh.x.array[:] = 0
# Transfer the data to `b_real_space`
df.fem.petsc.assign([uh, rh], b_real_space)
# And accumulate the values in the right hand side vector
b.axpy(1, b_real_space)
# We destroy the temporary work vector after usage
b_real_space.destroy()
#---- KSP solver ----
ksp = PETSc.KSP().create(mesh.comm)
ksp.setOperators(A)
ksp.setType("preonly")
pc = ksp.getPC()
pc.setType("lu")
pc.setFactorSolverType("mumps")
xh = b.duplicate()
ksp.solve(b, xh)
xh.ghostUpdate(addv=PETSc.InsertMode.INSERT, mode=PETSc.ScatterMode.FORWARD)
uh = df.fem.Function(V, name="u")
df.fem.petsc.assign(xh, [uh, rh])
import pyvista4dolfinx as p4d
p4d.plot(uh, show=True)
b.destroy()
xh.destroy()
A.destroy()
ksp.destroy()
I’ve added
import pyvista4dolfinx as p4d
p4d.plot(uh, show=True)
For plotting, yielding: