Integral Constrains in FEniCSx

Hi all,

I wanted to try imposing an integral constrain in a 3D Poisson problem. I tried to start from a simple case as demonstrated here for the legacy FEniCS.

Basically consider a 1D line in the interval [0,1] and the Laplace equation in this domain -\nabla^2u(x)=0 where we have the Dirichet Boundary Condition u(1)=1.5 and the integral constrain \int_{0}^{1}dxu(x)=C_0=1. The analytical answer is u(x)=x+0.5.

I defined the Lagrange multiplier \lambda and the Lagrangian L=\int_{0}^{1}dx\left(\frac{1}{2}\left|\nabla u(x)\right|^2-\lambda\left( \int_{0}^{1}dxu(x)-C_0\right)\right) and functional minimization with respect to u(x) and \lambda gives the following set of equations \nabla^2 u(x)=\lambda and the constrain \int_{0}^{1}dxu(x)=C_0.
So, seems that I need a Lagrange finite element for u(x) and a real one for \lambda. So, my questions for discussion are

  1. Does the above formulation seem correct?
  2. It seems, from what I read in the forum, that real finite elements are not incorporated in FEniCSx since the code I give below gives the error
    Exception has occurred: ValueError Unknown element family: Real with cell type interval
  3. Any ideas on if I can solve the problem by introducing Lagrange finite elements for the Lagrange multiplier? I tried it but the code outputs a wrong result.

Minimal Code for Real Element

from mpi4py import MPI
import dolfinx
import numpy as np
import ufl
from petsc4py.PETSc import ScalarType
import petsc4py.PETSc

domain = dolfinx.mesh.create_unit_interval(MPI.COMM_WORLD, 10)

element1 = ufl.FiniteElement("Lagrange", domain.ufl_cell(), 1)
element2 = ufl.FiniteElement("Real", domain.ufl_cell(), 0)
mel = ufl.MixedElement([element1, element2])
V = dolfinx.fem.FunctionSpace(domain, mel)
V0, submap = V.sub(0).collapse()

# Test and Trial functions
v, r = ufl.TestFunctions(V)
uh = dolfinx.fem.Function(V)
u, m = ufl.TrialFunctions(V)

def right_boundary(x):
    return np.isclose(x[0], 1)
right_facets = dolfinx.fem.locate_dofs_geometrical((V.sub(0),V0), right_boundary)
bc_r = dolfinx.fem.dirichletbc(ScalarType(1.5), right_facets[0], V.sub(0))

C0 = dolfinx.fem.Constant(domain, ScalarType(1))
f = dolfinx.fem.Constant(domain, ScalarType(0))

a = ufl.dot(ufl.grad(u), ufl.grad(v)) * ufl.dx + m*v*ufl.dx + u * r * ufl.dx
L = f * v * ufl.dx  +  C0 * r * ufl.dx
problem = dolfinx.fem.petsc.LinearProblem(a, L, bcs=[bc_r],u=uh)
uh = problem.solve()
u1, u2 = uh.split()
print(u1.x.array)
print(u2.x.array)

Minimal Code for Lagrange Element

from mpi4py import MPI
import dolfinx
import numpy as np
import ufl
from petsc4py.PETSc import ScalarType
import petsc4py.PETSc

domain = dolfinx.mesh.create_unit_interval(MPI.COMM_WORLD, 10)

element1 = ufl.FiniteElement("Lagrange", domain.ufl_cell(), 1)
mel = ufl.MixedElement([element1, element1])
V = dolfinx.fem.FunctionSpace(domain, mel)
V0, submap = V.sub(0).collapse()

# Test and test functions
v, r = ufl.TestFunctions(V)
uh = dolfinx.fem.Function(V)
u, m = ufl.TrialFunctions(V)#

def right_boundary(x):
    return np.isclose(x[0], 1)
right_facets = dolfinx.fem.locate_dofs_geometrical((V.sub(0),V0), right_boundary)
bc_r = dolfinx.fem.dirichletbc(ScalarType(1.5), right_facets[0], V.sub(0))

C0 = dolfinx.fem.Constant(domain, ScalarType(1))
f = dolfinx.fem.Constant(domain, ScalarType(0))

a = ufl.dot(ufl.grad(u), ufl.grad(v)) * ufl.dx + m*v*ufl.dx + u * r * ufl.dx
L = f * v * ufl.dx  +  C0 * r * ufl.dx
problem = dolfinx.fem.petsc.LinearProblem(a, L, bcs=[bc_r],u=uh)
uh = problem.solve()
u1, u2 = uh.split()
print(u1.x.array)
print(u2.x.array)

Seems that the Real element family is not implemented in fenicsx yet. If anyone has any input on how to solve the above problem, I would very much appreciate it.

Your formulation above is mostly correct. For the Lagrange multiplier, a Real element (not a Lagrange element) is the correct formulation. However, as you noted, Real elements do not exist in FEniCSx (aside from a quick-and-dirty implementation by @jackhale: GitHub - FEniCS/dolfinx at jhale/real-space). A hacky way to implement a Real element would be to implement the Lagrange multiplier using Lagrange elements, and use @dokken’s dolfinx_mpc library to constrain all the dofs in that subspace to a single dof. E.g.:

from mpi4py import MPI
import dolfinx
import dolfinx_mpc
import numpy as np
import ufl
from petsc4py.PETSc import ScalarType
comm = MPI.COMM_WORLD

### Create mesh and function spaces
domain = dolfinx.mesh.create_unit_interval(comm, 15)
element1 = ufl.FiniteElement("Lagrange", domain.ufl_cell(), 1)
V = dolfinx.fem.FunctionSpace(domain, ufl.MixedElement([element1, element1]))
V0c, submap = V.sub(0).collapse()
V1 = V.sub(1)

### Create test and test functions
v, r = ufl.TestFunctions(V)
uh = dolfinx.fem.Function(V)
u, m = ufl.TrialFunctions(V)#

### Mark boundaries
def right_boundary(x): return np.isclose(x[0], 1.0)
def left_boundary(x): return np.isclose(x[0], 0.0)
def domain_nodes(x): return ~np.logical_or(left_boundary(x), right_boundary(x))

node_left = dolfinx.mesh.locate_entities_boundary(domain, 0, left_boundary)
node_right = dolfinx.mesh.locate_entities_boundary(domain, 0, right_boundary)
node_middle = dolfinx.mesh.locate_entities_boundary(domain, 0, domain_nodes)
tags = {"left": 2, "right": 3, "middle": 1}
marker_left = np.full_like(node_left, tags["left"])
marker_right = np.full_like(node_right, tags["right"])
marker_middle = np.full_like(node_middle, tags["middle"])
nodes = np.concatenate((node_left, node_right, node_middle))
markers = np.concatenate((marker_left, marker_right, marker_middle))
idx = np.argsort(nodes)

ft = dolfinx.mesh.meshtags(domain, 0, nodes[idx], markers[idx])

### Create Dirichlet bcs
right_facets = dolfinx.fem.locate_dofs_geometrical((V.sub(0), V0c), right_boundary)
bc_r = dolfinx.fem.dirichletbc(ScalarType(1.5), right_facets[0], V.sub(0))

### MultiPointConstraint
mpc = dolfinx_mpc.MultiPointConstraint(V)

# Find the master dof (x = 0.0) and owning process
nodes = dolfinx.mesh.locate_entities_boundary(domain, 0, lambda x: np.isclose(x[0], 0.0))
master_dof = dolfinx.fem.locate_dofs_topological(V1, 0, nodes, remote=False)
master_owner = np.full_like(master_dof, comm.rank)
master_dof = np.concatenate(comm.allgather(master_dof), dtype=np.int64)
master_owner = np.concatenate(comm.allgather(master_owner), dtype=np.int32)

# Check for correct number of master dofs (should be 1)
if master_dof.size != 1:
    raise RuntimeError(f"Expected to find 1 master dof; found {master_dof.size}")
else:
    print(f"[{comm.rank}]: master_dof = {master_dof}")
    print(f"[{comm.rank}]: master_owner = {master_owner}")

# Find the slave nodes
nodes = dolfinx.mesh.locate_entities(domain, 0, lambda x: ~np.isclose(x[0], 0.0))
slave_dof = dolfinx.fem.locate_dofs_topological(V1, 0, nodes, remote=False)
print(f"[{comm.rank}]: slave_dof = {slave_dof}")

# Create arrays for use by mpc.add_constraint
masters = np.broadcast_to(master_dof, slave_dof.shape)
owners = np.broadcast_to(master_owner, slave_dof.shape)
coeffs = np.ones_like(masters, dtype=ScalarType)
offsets = np.arange(masters.size + 1, dtype=np.int32)

# Create constraint and finalize mpc
mpc.add_constraint(V, slave_dof, masters, coeffs, owners, offsets)
mpc.finalize()

### Create variational form and solve
C0 = dolfinx.fem.Constant(domain, ScalarType(1))
f = dolfinx.fem.Constant(domain, ScalarType(1))

# a = ufl.dot(ufl.grad(u), ufl.grad(v)) * ufl.dx + m*v*ufl.dx + u * r * ufl.dx
n = ufl.FacetNormal(domain)
ds = ufl.Measure("ds", domain, subdomain_data=ft)
a = (
    ufl.inner(ufl.grad(u), ufl.grad(v))*ufl.dx
    + ufl.inner(m, v)*ufl.dx
    + ufl.inner(u, r)*ufl.dx
    - ufl.inner(ufl.grad(u), n * v)*ds(tags["left"])
)
L = ufl.inner(f, v)*ufl.dx  +  ufl.inner(C0, r)*ufl.dx
problem = dolfinx_mpc.LinearProblem(
    a, L, mpc, bcs=[bc_r],
    petsc_options={
        "ksp_type": "preonly",
        "pc_type": "lu",
        "pc_factor_mat_solver_type": "mumps",
    }
)
uh = problem.solve()
print(uh.x.array)

This works in serial for me (and in parallel, although I get SIGSEGV errors for certain mesh sizes and numbers of processors).

3 Likes

Thanks, that is what I had in mind to do.

Can you provide feedback on how to install dolfinx_mpc in Ubuntu? I have fenicsx installed via Ubuntu PPA in Ubuntu 22.04.2 LTS that I run in wls2 (although this shouldn’t matter)

Thanks again, I really appreciate the help!

The easiest method (in my opinion) is to use conda or docker. I recently installed dolfinx_mpc (inside WSL) using

conda create -n dolfinx_mpc "dolfinx_mpc[build=...]"

where I specified the build that I wanted, in order to get the Python version, MPI implementation, and PETSc scalar types that I wanted. (There are conda builds of dolfinx_mpc for Python 3.8, 3.9, and 3.10, for MPICH and OpenMPI, and for real and complex PETSc.) If you decide to try conda, you may want to remove your current installation first.

If you want to stick with your dolfinx installed through apt, it sounds like there is a package python3-dolfinx-mpc for that. You can find some discussion on how to do that here: Explicitly assembling matrices with dolfinx_mpc - #7 by BillS

Thanks!!

sudo apt install python3-dolfinx-mpc

installs dolfinx-mpc and you code works perfectly reproducing the expected result.

1 Like