Solve poisson equation with three submeshes

Hi all,

I’m trying to leverage the entity_maps introduced in dolfinx 0.8.0 to solve a poisson equation on a unit square but with three submeshes codim 0.

Ultimately, I would like to model interface discontinuities using a DG method but have CG elements within the domains to obtain this:

For now, I’m simply trying to understand how to solve a poisson equation (without these discontinuities) on three submeshes.

This is the code that I have:

from dolfinx import mesh, fem
import dolfinx.fem.petsc
import dolfinx.nls.petsc
from mpi4py import MPI
import ufl
from ufl import grad, dot, jump, avg
import numpy as np
from dolfinx.nls.petsc import NewtonSolver


msh = mesh.create_unit_square(MPI.COMM_WORLD, 100, 100)
V = fem.functionspace(msh, ("DG", 1))

tol = 1e-16


def left_domain(x):
    return x[0] < 0.5 + tol


def top_right_domain(x):
    return np.logical_and(x[0] > 0.5 - tol, x[1] > 0.5 - tol)


def bottom_right_domain(x):
    return np.logical_and(x[0] > 0.5 - tol, x[1] < 0.5 + tol)


tdim = msh.topology.dim
fdim = tdim - 1
msh.topology.create_connectivity(fdim, tdim)
c_to_f = msh.topology.connectivity(tdim, tdim - 1)
f_to_c = msh.topology.connectivity(tdim - 1, tdim)
facet_map = msh.topology.index_map(tdim - 1)
cell_imap = msh.topology.index_map(tdim)
left_cells = mesh.locate_entities(msh, tdim, left_domain)
top_right_cells = mesh.locate_entities(msh, tdim, top_right_domain)
bottom_right_cells = mesh.locate_entities(msh, tdim, bottom_right_domain)

left_submesh, left_sub_to_parent, _, _ = dolfinx.mesh.create_submesh(
    msh, msh.topology.dim, left_cells
)
top_right_submesh, top_right_sub_to_parent, _, _ = dolfinx.mesh.create_submesh(
    msh, msh.topology.dim, top_right_cells
)
bottom_right_submesh, bottom_right_sub_to_parent, _, _ = dolfinx.mesh.create_submesh(
    msh, msh.topology.dim, bottom_right_cells
)

# Create three function spaces on each submesh
V_left = fem.functionspace(left_submesh, ("CG", 1))
V_top_right = fem.functionspace(top_right_submesh, ("CG", 1))
V_bottom_right = fem.functionspace(bottom_right_submesh, ("CG", 1))

# create functions and test functions
u_left = fem.Function(V_left)
v_left = ufl.TestFunction(V_left)
u_top_right = fem.Function(V_top_right)
v_top_right = ufl.TestFunction(V_top_right)
u_bottom_right = fem.Function(V_bottom_right)
v_bottom_right = ufl.TestFunction(V_bottom_right)

# create dx measures
cell_map = msh.topology.index_map(msh.topology.dim)
num_cells = cell_map.size_local + cell_map.num_ghosts
marker = np.arange(num_cells, dtype=np.int32)
mt = dolfinx.mesh.meshtags(msh, msh.topology.dim, marker, marker)

dx = ufl.Measure("dx", domain=msh, subdomain_data=mt)
dx_left = ufl.Measure("dx", domain=left_submesh)
dx_top_right = ufl.Measure("dx", domain=top_right_submesh)
dx_bottom_right = ufl.Measure("dx", domain=bottom_right_submesh)

# Define variational problem
F = 0
F += dot(grad(v_left), grad(u_left)) * dx_left
F += dot(grad(v_top_right), grad(u_top_right)) * dx_top_right
F += dot(grad(v_bottom_right), grad(u_bottom_right)) * dx_bottom_right

# source
f = 1
F += -v_left * f * dx_left
F += -v_top_right * f * dx_top_right
F += -v_bottom_right * f * dx_bottom_right

# boundary conditions
boundary_facets = mesh.exterior_facet_indices(msh.topology)
boundary_dofs = fem.locate_dofs_topological(V, tdim - 1, boundary_facets)
uD = fem.Function(V)
uD.interpolate(lambda x: np.full(x[0].shape, 0.0))
bc = fem.dirichletbc(uD, boundary_dofs)

# TODO add dS coupling terms for interfaces

# make entity maps
parent_to_sub_left = np.full(num_cells, -1, dtype=np.int32)
parent_to_sub_left[left_sub_to_parent] = np.arange(
    len(left_sub_to_parent), dtype=np.int32
)
entity_map = {left_submesh._cpp_object: parent_to_sub_left}

parent_to_sub_right = np.full(num_cells, -1, dtype=np.int32)
parent_to_sub_right[top_right_sub_to_parent] = np.arange(
    len(top_right_sub_to_parent), dtype=np.int32
)
entity_map.update({top_right_submesh._cpp_object: parent_to_sub_right})

parent_to_sub_bottom = np.full(num_cells, -1, dtype=np.int32)
parent_to_sub_bottom[bottom_right_sub_to_parent] = np.arange(
    len(bottom_right_sub_to_parent), dtype=np.int32
)
entity_map.update({bottom_right_submesh._cpp_object: parent_to_sub_bottom})

# NonLinearProblem
formulation = dolfinx.fem.form(F == 0, entity_maps=entity_map)

u = fem.Function(V)
problem = fem.petsc.NonlinearProblem(
    formulation,
    u,
    bcs=bc,
)
solver = NewtonSolver(MPI.COMM_WORLD, problem)

solver.solve(u)

At the moment the code doesn’t run and produces the following error:

Traceback (most recent call last):
  File "/home/remidm/FESTIM/interface_submeshes.py", line 118, in <module>
    problem = fem.petsc.NonlinearProblem(
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/remidm/miniconda3/envs/dolfinx_0.8/lib/python3.12/site-packages/dolfinx/fem/petsc.py", line 905, in __init__
    J = ufl.derivative(F, u, du)
        ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/remidm/miniconda3/envs/dolfinx_0.8/lib/python3.12/site-packages/ufl/formoperators.py", line 404, in derivative
    raise ValueError(f"Invalid argument type {type(form)}.")
ValueError: Invalid argument type <class 'ufl.equation.Equation'>.

I couldn’t find a demo for this application case so I was wondering if the approach was at least the right one?

Thanks for any pointers!

Cheers
Rem

Using non-linear problem for mixed dimensional methods is not stream-lined. Here is a NewtonSolver that I made recently for mixed dim problems:

# Newton solver for mixed dim problems
# Author: Jørgen S. Dokken
# SPDX License identifier: MIT
class NewtonSolver():
    max_iterations: int
    bcs: list[dolfinx.fem.DirichletBC]
    A: PETSc.Mat
    b: PETSc.Vec
    J: dolfinx.fem.Form
    b: dolfinx.fem.Form
    dx: PETSc.Vec
    def __init__(self, F:list[dolfinx.fem.form], J:list[list[dolfinx.fem.form]], w: list[dolfinx.fem.Function], 
                 bcs: list[dolfinx.fem.DirichletBC]|None=None, max_iterations:int=5,
                 petsc_options: dict[str, str|float|int|None]=None,
                 problem_prefix = "newton"):
        self.max_iterations = max_iterations
        self.bcs = [] if bcs is None else bcs
        self.b = dolfinx.fem.petsc.create_vector_block(F)
        self.F = F
        self.J = J
        self.A = dolfinx.fem.petsc.create_matrix_block(J)
        self.dx = self.A.createVecLeft()
        self.w = w
        self.x = dolfinx.fem.petsc.create_vector_block(F)

        # Set PETSc options
        opts = PETSc.Options()
        opts.prefixPush(problem_prefix)
        if petsc_options is not None:
            for k, v in petsc_options.items():
                opts[k] = v
        opts.prefixPop()

        # Define KSP solver    
        self._solver = PETSc.KSP().create(self.b.getComm().tompi4py())
        self._solver.setOperators(self.A)
        self._solver.setFromOptions()

        # Set matrix and vector PETSc options
        self.A.setOptionsPrefix(problem_prefix)
        self.A.setFromOptions()
        self.b.setOptionsPrefix(problem_prefix)
        self.b.setFromOptions()

   
    def solve(self, tol=1e-6, beta=1.0):
        i = 0


        while i < self.max_iterations:
            dolfinx.cpp.la.petsc.scatter_local_vectors(
                self.x,
                [si.x.petsc_vec.array_r for si in self.w],
                [
                    (si.function_space.dofmap.index_map, si.function_space.dofmap.index_map_bs)
                    for si in self.w
                ])
            self.x.ghostUpdate(addv=PETSc.InsertMode.INSERT, mode=PETSc.ScatterMode.FORWARD)

            # Assemble F(u_{i-1}) - J(u_D - u_{i-1}) and set du|_bc= u_D - u_{i-1}
            self.b.zeroEntries()
            dolfinx.fem.petsc.assemble_vector_block(self.b, self.F,self.J, bcs=self.bcs,x0=self.x, scale=-1.0)
            self.b.ghostUpdate(PETSc.InsertMode.INSERT_VALUES, PETSc.ScatterMode.FORWARD)
            # Assemble Jacobian
            self.A.zeroEntries()
            dolfinx.fem.petsc.assemble_matrix_block(self.A, self.J, bcs=self.bcs)
            self.A.assemble()
            
            
            self._solver.solve(self.b, self.dx)
            assert self._solver.getConvergedReason() > 0, "Linear solver did not converge"
            offset_start = 0
            for s in self.w:
                num_sub_dofs = s.function_space.dofmap.index_map.size_local * s.function_space.dofmap.index_map_bs
                s.x.array[:num_sub_dofs] -= beta*self.dx.array_r[offset_start:offset_start+num_sub_dofs]
                s.x.scatter_forward()
                offset_start += num_sub_dofs
            # Compute norm of update

            correction_norm = self.dx.norm(0)
            print(f"Iteration {i}: Correction norm {correction_norm}")
            if correction_norm < tol:
                break
            i += 1

Thanks for this!

I’ve modified the code to the following:

formulation = dolfinx.fem.form(F == 0, entity_maps=entity_map)

u = fem.Function(V)

solver = NewtonSolver(
    F=formulation, w=[u_left, u_top_right, u_bottom_right], bcs=bc, J=[]  # not sure how to compute J here
)

solver.solve(u)

But now have this error:

Traceback (most recent call last):
  File "/home/remidm/FESTIM/interface_submeshes.py", line 125, in <module>
    solver = NewtonSolver(
             ^^^^^^^^^^^^^
  File "/home/remidm/FESTIM/newton_solver_mixed_dim.py", line 29, in __init__
    self.b = dolfinx.fem.petsc.create_vector_block(F)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/remidm/miniconda3/envs/dolfinx_0.8/lib/python3.12/site-packages/dolfinx/fem/petsc.py", line 124, in create_vector_block
    maps = [
           ^
TypeError: 'Equation' object is not iterable

I thought maybe the F==0 in form was causing the issue but when only setting dolfinx.fem.form(F, entity_maps=entity_map) I get:

Traceback (most recent call last):
  File "/home/remidm/FESTIM/interface_submeshes.py", line 117, in <module>
    formulation = dolfinx.fem.form(F, entity_maps=entity_map)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/remidm/miniconda3/envs/dolfinx_0.8/lib/python3.12/site-packages/dolfinx/fem/forms.py", line 249, in form
    return _create_form(form)
           ^^^^^^^^^^^^^^^^^^
  File "/home/remidm/miniconda3/envs/dolfinx_0.8/lib/python3.12/site-packages/dolfinx/fem/forms.py", line 244, in _create_form
    return _form(form)
           ^^^^^^^^^^^
  File "/home/remidm/miniconda3/envs/dolfinx_0.8/lib/python3.12/site-packages/dolfinx/fem/forms.py", line 178, in _form
    (domain,) = list(sd.keys())  # Assuming single domain
    ^^^^^^^^^
ValueError: too many values to unpack (expected 1)

You would not make an equation in that sense, you would do something like:

F0 = F00 + F01 
F1 = F10 + F11
residual_0 = dolfinx.fem.form(F0, entity_maps=entity_maps)
residual_1 = dolfinx.fem.form(F1, entity_maps=entity_maps)
jac00 = ufl.derivative(F0, u)
jac01 = ufl.derivative(F0, psi)
jac10 = ufl.derivative(F1, u)
jac11 = ufl.derivative(F1, psi)
J00 = dolfinx.fem.form(jac00, entity_maps=entity_maps)
J01 = dolfinx.fem.form(jac01, entity_maps=entity_maps)
J10 = dolfinx.fem.form(jac10, entity_maps=entity_maps)
J11 = dolfinx.fem.form(jac11, entity_maps=entity_maps)

J = [[J00, J01], [J10, J11]]
F = [residual_0, residual_1]

For a two-domain problem

It’s unclear to me what F00 is with respect to F01

Moreover, is psi the test function?

is u a function defined over the whole domain not only a submesh?

Hi Remi,

Sorry for the lack of clarity (my example is a bit experimental, using an exotic variational formulation Im not ready to share yet).
However, I can give you the context:
In my example u is a function on the whole mesh, while psi is a function on the subset of the boundary (using Joe’s meshview branches).
So u and psi are the two unknowns

After a bit of trial and error this is where I am.
I’ve simplified the problem to only a two domain problem (left and right).

  • I created a new Function psi since the test function wasn’t accepted. But maybe I did this wrong…
  • It’s unclear to me if I need to create functions on the submeshes and test functions on the whole mesh, or the opposite.

This is my code (apologies for the length…):

from dolfinx import mesh, fem
import dolfinx.fem.petsc
import dolfinx.nls.petsc
from mpi4py import MPI
import ufl
from ufl import grad, dot, jump, avg
import numpy as np

from newton_solver_mixed_dim import NewtonSolver

msh = mesh.create_unit_square(MPI.COMM_WORLD, 100, 100)
V = fem.functionspace(msh, ("DG", 1))

tol = 1e-16


def left_domain(x):
    return x[0] < 0.5 + tol


def right_domain(x):
    return x[0] > 0.5 - tol


def interface(x):
    return np.isclose(x[0], 0.5)


tdim = msh.topology.dim
fdim = tdim - 1
msh.topology.create_connectivity(fdim, tdim)
c_to_f = msh.topology.connectivity(tdim, tdim - 1)
f_to_c = msh.topology.connectivity(tdim - 1, tdim)
facet_map = msh.topology.index_map(tdim - 1)
cell_imap = msh.topology.index_map(tdim)
left_cells = mesh.locate_entities(msh, tdim, left_domain)
right_cells = mesh.locate_entities(msh, tdim, right_domain)
boundary_facets = mesh.exterior_facet_indices(msh.topology)
interface_facets = mesh.locate_entities(msh, fdim, interface)

num_facets = facet_map.size_local + facet_map.num_ghosts
indices = np.arange(0, num_facets)
values = np.zeros(indices.shape, dtype=np.intc)  # all facets are tagged with zero

interface_id = 2
values[boundary_facets] = 1
values[interface_facets] = interface_id

mesh_tags_facets = mesh.meshtags(msh, fdim, indices, values)

left_submesh, left_sub_to_parent, _, _ = dolfinx.mesh.create_submesh(
    msh, msh.topology.dim, left_cells
)
right_submesh, right_sub_to_parent, _, _ = dolfinx.mesh.create_submesh(
    msh, msh.topology.dim, right_cells
)

# Create three function spaces on each submesh
V_left = fem.functionspace(left_submesh, ("CG", 1))
V_right = fem.functionspace(right_submesh, ("CG", 1))

# create functions and test functions
u_left = fem.Function(V_left)
v_left = ufl.TestFunction(V_left)
u_right = fem.Function(V_right)
v_right = ufl.TestFunction(V_right)
u = fem.Function(V)
v = ufl.TestFunction(V)

# create measures
cell_map = msh.topology.index_map(msh.topology.dim)
num_cells = cell_map.size_local + cell_map.num_ghosts
indices = np.arange(0, num_cells)
values_cells = np.zeros(indices.shape, dtype=np.intc)
index_left = 1
index_right = 2
values_cells[left_cells] = index_left
values_cells[right_cells] = index_right

mesh_tags_cells = mesh.meshtags(msh, tdim, indices, values_cells)
mt = dolfinx.mesh.meshtags(msh, msh.topology.dim, indices, values_cells)
dx = ufl.Measure("dx", domain=msh, subdomain_data=mt)
dx_left = ufl.Measure("dx", domain=left_submesh)
dx_right = ufl.Measure("dx", domain=right_submesh)
dS = ufl.Measure("dS", domain=msh, subdomain_data=mesh_tags_facets)

# Define variational problem
F0 = dot(grad(v), grad(u_left)) * dx(index_left)
F1 = dot(grad(v), grad(u_right)) * dx(index_right)

# source
f = 1
F0 += -v * f * dx(index_left)
F1 += -v * f * dx(index_right)

# boundary conditions
boundary_facets = mesh.exterior_facet_indices(msh.topology)
boundary_dofs = fem.locate_dofs_topological(V, tdim - 1, boundary_facets)
uD = fem.Function(V)
uD.interpolate(lambda x: np.full(x[0].shape, 0.0))
bc = fem.dirichletbc(uD, boundary_dofs)

# TODO add dS coupling terms for interfaces
h = ufl.CellDiameter(msh)
n = ufl.FacetNormal(msh)
alpha = 1000
F0 += (
    -dot(avg(grad(v)), jump(u, n)) * dS(interface_id)
    - dot(jump(v, n), avg(grad(u))) * dS(interface_id)
    + alpha / avg(h) * dot(jump(v, n), jump(u, n)) * dS(interface_id)
)

# make entity maps
parent_to_sub_left = np.full(num_cells, -1, dtype=np.int32)
parent_to_sub_left[left_sub_to_parent] = np.arange(
    len(left_sub_to_parent), dtype=np.int32
)

parent_to_sub_right = np.full(num_cells, -1, dtype=np.int32)
parent_to_sub_right[right_sub_to_parent] = np.arange(
    len(right_sub_to_parent), dtype=np.int32
)

entity_maps = {
    left_submesh._cpp_object: parent_to_sub_left,
    right_submesh._cpp_object: parent_to_sub_right,
}

# NonLinearProblem
residual_0 = dolfinx.fem.form(F0, entity_maps=entity_maps)
residual_1 = dolfinx.fem.form(F1, entity_maps=entity_maps)

psi = dolfinx.fem.Function(V)

jac00 = ufl.derivative(F0, u)
jac01 = ufl.derivative(F0, psi)
jac10 = ufl.derivative(F1, u)
jac11 = ufl.derivative(F1, psi)
J00 = dolfinx.fem.form(jac00, entity_maps=entity_maps)
J01 = dolfinx.fem.form(jac01, entity_maps=entity_maps)
J10 = dolfinx.fem.form(jac10, entity_maps=entity_maps)
J11 = dolfinx.fem.form(jac11, entity_maps=entity_maps)

J = [[J00, J01], [J10, J11]]

solver = NewtonSolver(F=[residual_0, residual_1], w=[u_left, u_right], bcs=[bc], J=J)

solver.solve()

Giving:

Traceback (most recent call last):
  File "/home/remidm/FESTIM/interface_submeshes.py", line 148, in <module>
    solver.solve()
  File "/home/remidm/FESTIM/newton_solver_mixed_dim.py", line 88, in solve
    self._solver.solve(self.b, self.dx)
  File "petsc4py/PETSc/KSP.pyx", line 1774, in petsc4py.PETSc.KSP.solve
petsc4py.PETSc.Error: error code 73
[0] KSPSolve() at /home/conda/feedstock_root/build_artifacts/petsc_1717048443064/work/src/ksp/ksp/interface/itfunc.c:1078
[0] KSPSolve_Private() at /home/conda/feedstock_root/build_artifacts/petsc_1717048443064/work/src/ksp/ksp/interface/itfunc.c:831
[0] KSPSetUp() at /home/conda/feedstock_root/build_artifacts/petsc_1717048443064/work/src/ksp/ksp/interface/itfunc.c:415
[0] PCSetUp() at /home/conda/feedstock_root/build_artifacts/petsc_1717048443064/work/src/ksp/pc/interface/precon.c:1079
[0] PCSetUp_ILU() at /home/conda/feedstock_root/build_artifacts/petsc_1717048443064/work/src/ksp/pc/impls/factor/ilu/ilu.c:135
[0] MatILUFactorSymbolic() at /home/conda/feedstock_root/build_artifacts/petsc_1717048443064/work/src/mat/interface/matrix.c:7036
[0] MatILUFactorSymbolic_SeqAIJ() at /home/conda/feedstock_root/build_artifacts/petsc_1717048443064/work/src/mat/impls/aij/seq/aijfact.c:1745
[0] Object is in wrong state
[0] Matrix is missing diagonal entry 0

So obviously something is wrong :frowning:

Should be pretty much straightforward to with multiphenicsx, considering that the third tutorial (interface case) basically has the same structure.

However, the downside is that you would be installing an additional library, rather than using Joe’s functionalities which are built-in in dolfinx.