Coupled problem with codim 1 submesh wrong derivative

Hi all,

I’m trying to solve the following problem where \Omega is a 2D domain and \Gamma_1 is a 1D submesh (bottom boundary).

The governing equation on \Omega is diffusion with coupling term J and the governing equation on \Gamma_1 is diffusion-advection with coupling term J.

\Delta u + J = 0 \ \text{, on} \ \Omega\\ \Delta u_{sub} + v \nabla u_{sub} -J = 0 \text{, on} \ \Gamma_1\\ J = h_l (u_{sub} - u)

Boundary conditions:

u = 0 \ \text{, on} \ \Gamma_2 \\ u_{sub} = 1 \ \text{, on} \ x=0

The solution looks something like:

The problem I’m facing is that the derivative u_sub.dx(0) seems to be ignored in the formulation and only setting the advection term with u_sub.dx(1) seems to work:

vel_x = 10

# Option 1: Full grad with 2D vector. Works but odd that we need a 2D velocity
vel = dolfinx.fem.Constant(submesh, PETSc.ScalarType([vel_x, vel_x]))
F += ufl.inner(ufl.dot(ufl.grad(u_sub), vel), v_sub) * ds(1)

# Option 2: just du/dx * vel_x. Doesn't work at all
# F += ufl.inner(u_sub.dx(0) * vel_x, v_sub) * ds(1)

# Option 3: just du/dy * vel_x. Works but doesn't make sense since d/dy....
# F += ufl.inner(u_sub.dx(1) * vel_x, v_sub) * ds(1)

I would appreciate any pointer! Maybe this is a bug in the submesh interface (i doubt it…)?

thanks in advance!

Remi

Here’s the MWE:

Full code
from mpi4py import MPI
from petsc4py import PETSc

import dolfinx
import dolfinx.fem.petsc
import matplotlib.pyplot as plt
import numpy as np
import pyvista
import ufl
from dolfinx import plot

dx = 1 / 5
L = 100

nx = int(L / dx)
mesh = dolfinx.mesh.create_rectangle(
    MPI.COMM_WORLD,
    [np.array([0, 0]), np.array([L, 1])],
    [nx, 10],
    cell_type=dolfinx.mesh.CellType.quadrilateral,
)
vdim = mesh.topology.dim
fdim = mesh.topology.dim - 1

mesh.topology.create_connectivity(fdim, vdim)


# facet meshtags top and bottom
tag_to_marker = {
    1: lambda x: np.isclose(x[1], 0),  # bottom
    2: lambda x: np.isclose(x[1], 1),  # top
    3: lambda x: np.isclose(x[0], 0),  # left
    4: lambda x: np.isclose(x[0], L),  # right
}

facets = np.array([], dtype=np.int64)
tags = np.array([], dtype=np.int32)
for tag, marker in tag_to_marker.items():
    facet_indices = dolfinx.mesh.locate_entities(mesh, fdim, marker)
    facets = np.concatenate((facets, facet_indices))
    tags = np.concatenate((tags, np.full_like(facet_indices, tag, dtype=np.int32)))
facet_tags = dolfinx.mesh.meshtags(mesh, fdim, facets, tags)

cell_tags = dolfinx.mesh.meshtags(
    mesh,
    vdim,
    np.arange(mesh.topology.index_map(vdim).size_local),
    np.ones(mesh.topology.index_map(vdim).size_local, dtype=np.int32),
)

with dolfinx.io.XDMFFile(mesh.comm, "results/facet_tags.xdmf", "w") as xdmf:
    xdmf.write_mesh(mesh)
    xdmf.write_meshtags(facet_tags, x=mesh.geometry)

with dolfinx.io.XDMFFile(mesh.comm, "results/cell_tags.xdmf", "w") as xdmf:
    xdmf.write_mesh(mesh)
    xdmf.write_meshtags(cell_tags, x=mesh.geometry)

# make submesh of the bottom boundary
submesh, cmap, vmap, nmap = dolfinx.mesh.create_submesh(
    mesh, dim=fdim, entities=facet_tags.find(1)
)
submesh.topology.create_connectivity(0, 1)

# Function spaces and functions
V_bulk = dolfinx.fem.functionspace(mesh, ("CG", 1))
V_sub = dolfinx.fem.functionspace(submesh, ("CG", 1))

W = ufl.MixedFunctionSpace(V_bulk, V_sub)

u = dolfinx.fem.Function(V_bulk)
u.name = "u"
u_sub = dolfinx.fem.Function(V_sub)
u_sub.name = "u_sub"

v, v_sub = ufl.TestFunctions(W)

# Formulation
dx = ufl.dx(domain=mesh, subdomain_data=cell_tags)
ds = ufl.ds(domain=mesh, subdomain_data=facet_tags)

F = ufl.inner(ufl.grad(u), ufl.grad(v)) * dx
F += ufl.inner(ufl.grad(u_sub), ufl.grad(v_sub)) * ds(1)


# advection term NOTE: this seems to be ignored....
# chaning vel_x doesn't change the solution u_sub at the outlet
# we would expect that increasing vel_x would decrease u_sub at the outlet

vel_x = 10

# Option 1: Full grad with 2D vector. Works but odd that we need a 2D velocity
vel = dolfinx.fem.Constant(submesh, PETSc.ScalarType([vel_x, vel_x]))
F += ufl.inner(ufl.dot(ufl.grad(u_sub), vel), v_sub) * ds(1)

# Option 2: just du/dx * vel_x. Doesn't work at all
# F += ufl.inner(u_sub.dx(0) * vel_x, v_sub) * ds(1)

# Option 3: just du/dy * vel_x. Works but doesn't make sense since d/dy....
# F += ufl.inner(u_sub.dx(1) * vel_x, v_sub) * ds(1)

# coupling term
h_l = dolfinx.fem.Constant(mesh, 0.4)
flux = h_l * (u - u_sub)

F += flux * v * ds(1)
F += -flux * v_sub * ds(1)

forms = ufl.extract_blocks(F)

# Dirichlet BC left
bc_top_dofs = dolfinx.fem.locate_dofs_topological(
    V_bulk,
    mesh.topology.dim - 1,
    dolfinx.mesh.locate_entities(
        mesh, mesh.topology.dim - 1, lambda x: np.isclose(x[1], 1)
    ),
)
bc_top = dolfinx.fem.dirichletbc(
    dolfinx.default_scalar_type(0.0),
    bc_top_dofs,
    V_bulk,
)

bc_left_dofs = dolfinx.fem.locate_dofs_topological(
    V_sub, 0, dolfinx.mesh.locate_entities(submesh, 0, lambda x: np.isclose(x[0], 0))
)
bc_left = dolfinx.fem.dirichletbc(
    dolfinx.default_scalar_type(1.0),
    bc_left_dofs,
    V_sub,
)
# Nonlinear problem

problem = dolfinx.fem.petsc.NonlinearProblem(
    forms,
    [u, u_sub],
    bcs=[
        bc_top,
        bc_left,
    ],
    petsc_options_prefix="codim1_prob",
    entity_maps=[cmap],
)

problem.solve()

# Post processing

with dolfinx.io.VTXWriter(mesh.comm, "results/u.bp", [u]) as writer:
    writer.write(0.0)

with dolfinx.io.VTXWriter(submesh.comm, "results/u_sub.bp", [u_sub]) as writer:
    writer.write(0.0)

topology, cell_types, geometry = plot.vtk_mesh(u.function_space)
grid = pyvista.UnstructuredGrid(topology, cell_types, geometry)
grid.point_data["c"] = u.x.array
grid.set_active_scalars("c")

plotter = pyvista.Plotter()

plotter.add_mesh(grid)
plotter.view_xy()

if not pyvista.OFF_SCREEN:
    plotter.show()
else:
    figure = plotter.screenshot("u.png")

topology, cell_types, geometry = plot.vtk_mesh(u_sub.function_space)
grid = pyvista.UnstructuredGrid(topology, cell_types, geometry)
grid.point_data["c"] = u_sub.x.array
grid.set_active_scalars("c")

# Make two points to construct the line between
a = [0, 0, 0]
b = [L, 0, 0]
sample = grid.sample_over_line(a, b, resolution=100)

plt.plot(sample["Distance"], sample["c"])
plt.ylim(0, 1)
plt.xlabel("x")
plt.ylabel("u_sub")
plt.show()

The problem is that ufl.inner(ufl.dot(ufl.grad(u_sub), vel), v_sub) * ds(1) is not well-defined, as what you would like is the manifold derivative, which one woudl get by using ufl.grad(u_sub)..... * ufl.dx(domain=submesh).

Hi @dokken, hope you’re doing well. I tried changing all 3 lines of the formulation in @RemDelaporteMathurin’s MWE that rely on u_sub/v_sub to use ufl.dx(domain=submesh) instead of ds(1), as I believe you were indicating in your diagnosis.

Full Modified MWE
from mpi4py import MPI
from petsc4py import PETSc

import dolfinx
import dolfinx.fem.petsc
import matplotlib.pyplot as plt
import numpy as np
import pyvista
import ufl
from dolfinx import plot

dx = 1 / 5
L = 100

nx = int(L / dx)
mesh = dolfinx.mesh.create_rectangle(
    MPI.COMM_WORLD,
    [np.array([0, 0]), np.array([L, 1])],
    [nx, 10],
    cell_type=dolfinx.mesh.CellType.quadrilateral,
)
vdim = mesh.topology.dim
fdim = mesh.topology.dim - 1

mesh.topology.create_connectivity(fdim, vdim)

# facet meshtags top and bottom
tag_to_marker = {
    1: lambda x: np.isclose(x[1], 0),  # bottom
    2: lambda x: np.isclose(x[1], 1),  # top
    3: lambda x: np.isclose(x[0], 0),  # left
    4: lambda x: np.isclose(x[0], L),  # right
}

facets = np.array([], dtype=np.int64)
tags = np.array([], dtype=np.int32)
for tag, marker in tag_to_marker.items():
    facet_indices = dolfinx.mesh.locate_entities(mesh, fdim, marker)
    facets = np.concatenate((facets, facet_indices))
    tags = np.concatenate((tags, np.full_like(facet_indices, tag, dtype=np.int32)))
facet_tags = dolfinx.mesh.meshtags(mesh, fdim, facets, tags)

cell_tags = dolfinx.mesh.meshtags(
    mesh,
    vdim,
    np.arange(mesh.topology.index_map(vdim).size_local),
    np.ones(mesh.topology.index_map(vdim).size_local, dtype=np.int32),
)

with dolfinx.io.XDMFFile(mesh.comm, "results/facet_tags.xdmf", "w") as xdmf:
    xdmf.write_mesh(mesh)
    xdmf.write_meshtags(facet_tags, x=mesh.geometry)

with dolfinx.io.XDMFFile(mesh.comm, "results/cell_tags.xdmf", "w") as xdmf:
    xdmf.write_mesh(mesh)
    xdmf.write_meshtags(cell_tags, x=mesh.geometry)

# make submesh of the bottom boundary
submesh, cmap, vmap, nmap = dolfinx.mesh.create_submesh(
    mesh, dim=fdim, entities=facet_tags.find(1)
)
submesh.topology.create_connectivity(0, 1)

# Function spaces and functions
V_bulk = dolfinx.fem.functionspace(mesh, ("CG", 1))
V_sub = dolfinx.fem.functionspace(submesh, ("CG", 1))

W = ufl.MixedFunctionSpace(V_bulk, V_sub)

u = dolfinx.fem.Function(V_bulk)
u.name = "u"
u_sub = dolfinx.fem.Function(V_sub)
u_sub.name = "u_sub"

v, v_sub = ufl.TestFunctions(W)

# Formulation
dx = ufl.dx(domain=mesh, subdomain_data=cell_tags)
ds = ufl.ds(domain=mesh, subdomain_data=facet_tags)
dx_sub = ufl.dx(domain=submesh)

F = ufl.inner(ufl.grad(u), ufl.grad(v)) * dx
F += ufl.inner(ufl.grad(u_sub), ufl.grad(v_sub)) * dx_sub  # <-- CHANGED

vel_x = 10

# Option 1: Full grad with 2D vector. Works but odd that we need a 2D velocity
vel = dolfinx.fem.Constant(submesh, PETSc.ScalarType([vel_x, vel_x]))
F += ufl.inner(ufl.dot(ufl.grad(u_sub), vel), v_sub) * dx_sub  # <-- CHANGED

# coupling term
h_l = dolfinx.fem.Constant(mesh, 0.4)
flux = h_l * (u - u_sub)

F += flux * v * ds(1)
F += -flux * v_sub * dx_sub  # <-- CHANGED

forms = ufl.extract_blocks(F)

# Dirichlet BC left
bc_top_dofs = dolfinx.fem.locate_dofs_topological(
    V_bulk,
    mesh.topology.dim - 1,
    dolfinx.mesh.locate_entities(
        mesh, mesh.topology.dim - 1, lambda x: np.isclose(x[1], 1)
    ),
)
bc_top = dolfinx.fem.dirichletbc(
    dolfinx.default_scalar_type(0.0),
    bc_top_dofs,
    V_bulk,
)

bc_left_dofs = dolfinx.fem.locate_dofs_topological(
    V_sub, 0, dolfinx.mesh.locate_entities(submesh, 0, lambda x: np.isclose(x[0], 0))
)
bc_left = dolfinx.fem.dirichletbc(
    dolfinx.default_scalar_type(1.0),
    bc_left_dofs,
    V_sub,
)
# Nonlinear problem

problem = dolfinx.fem.petsc.NonlinearProblem(
    forms,
    [u, u_sub],
    bcs=[
        bc_top,
        bc_left,
    ],
    petsc_options_prefix="codim1_prob",
    entity_maps=[cmap],
)

problem.solve()

# Post processing
with dolfinx.io.VTXWriter(mesh.comm, "results/u.bp", [u]) as writer:
    writer.write(0.0)

with dolfinx.io.VTXWriter(submesh.comm, "results/u_sub.bp", [u_sub]) as writer:
    writer.write(0.0)

topology, cell_types, geometry = plot.vtk_mesh(u.function_space)
grid = pyvista.UnstructuredGrid(topology, cell_types, geometry)
grid.point_data["c"] = u.x.array
grid.set_active_scalars("c")

plotter = pyvista.Plotter()

plotter.add_mesh(grid)
plotter.view_xy()

if not pyvista.OFF_SCREEN:
    plotter.show()
else:
    figure = plotter.screenshot("u.png")

topology, cell_types, geometry = plot.vtk_mesh(u_sub.function_space)
grid = pyvista.UnstructuredGrid(topology, cell_types, geometry)
grid.point_data["c"] = u_sub.x.array
grid.set_active_scalars("c")

# Make two points to construct the line between
a = [0, 0, 0]
b = [L, 0, 0]
sample = grid.sample_over_line(a, b, resolution=100)

plt.plot(sample["Distance"], sample["c"])
plt.ylim(0, 1)
plt.xlabel("x")
plt.ylabel("u_sub")
plt.show()
However, it threw the error
 File ".../lib/python3.11/site-packages/ffcx/ir/elementtables.py", line 527, in build_optimized_tables
    tbl = clamp_table_small_numbers(t["array"], rtol=rtol, atol=atol)
                                    ^
UnboundLocalError: cannot access local variable 't' where it is not associated with a value
Full traceback
Traceback (most recent call last):
  File ".../MWE_revised.py", line 132, in <module>
    problem = dolfinx.fem.petsc.NonlinearProblem(
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../lib/python3.11/site-packages/dolfinx/fem/petsc.py", line 1239, in __init__
    self._F = _create_form(
              ^^^^^^^^^^^^^
  File ".../lib/python3.11/site-packages/dolfinx/fem/forms.py", line 449, in form
    return _create_form(form)
           ^^^^^^^^^^^^^^^^^^
  File ".../lib/python3.11/site-packages/dolfinx/fem/forms.py", line 445, in _create_form
    return list(map(lambda sub_form: _create_form(sub_form), form))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../lib/python3.11/site-packages/dolfinx/fem/forms.py", line 445, in <lambda>
    return list(map(lambda sub_form: _create_form(sub_form), form))
                                     ^^^^^^^^^^^^^^^^^^^^^^
  File ".../lib/python3.11/site-packages/dolfinx/fem/forms.py", line 441, in _create_form
    return _form(form)
           ^^^^^^^^^^^
  File ".../lib/python3.11/site-packages/dolfinx/fem/forms.py", line 361, in _form
    ufcx_form, module, code = jit.ffcx_jit(
                              ^^^^^^^^^^^^^
  File ".../lib/python3.11/site-packages/dolfinx/jit.py", line 60, in mpi_jit
    return local_jit(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "..../lib/python3.11/site-packages/dolfinx/jit.py", line 215, in ffcx_jit
    r = ffcx.codegeneration.jit.compile_forms([ufl_object], options=p_ffcx, **p_jit)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../lib/python3.11/site-packages/ffcx/codegeneration/jit.py", line 244, in compile_forms
    raise e
  File ".../lib/python3.11/site-packages/ffcx/codegeneration/jit.py", line 224, in compile_forms
    impl = _compile_objects(
           ^^^^^^^^^^^^^^^^^
  File ".../lib/python3.11/site-packages/ffcx/codegeneration/jit.py", line 349, in _compile_objects
    _, code_body = ffcx.compiler.compile_ufl_objects(
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../lib/python3.11/site-packages/ffcx/compiler.py", line 113, in compile_ufl_objects
    ir = compute_ir(analysis, _object_names, _prefix, options, visualise)
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../lib/python3.11/site-packages/ffcx/ir/representation.py", line 150, in compute_ir
    irs = [
          ^
  File ".../python3.11/site-packages/ffcx/ir/representation.py", line 151, in <listcomp>
    _compute_integral_ir(
  File ".../lib/python3.11/site-packages/ffcx/ir/representation.py", line 402, in _compute_integral_ir
    integral_ir = compute_integral_ir(
                  ^^^^^^^^^^^^^^^^^^^^
  File ".../lib/python3.11/site-packages/ffcx/ir/integral.py", line 170, in compute_integral_ir
    mt_table_reference = build_optimized_tables(
                         ^^^^^^^^^^^^^^^^^^^^^^^
  File ".../lib/python3.11/site-packages/ffcx/ir/elementtables.py", line 527, in build_optimized_tables
    tbl = clamp_table_small_numbers(t["array"], rtol=rtol, atol=atol)
                                    ^
UnboundLocalError: cannot access local variable 't' where it is not associated with a value
Exception ignored in: <function NonlinearProblem.__del__ at 0x77a1def66520>
Traceback (most recent call last):
  File ".../lib/python3.11/site-packages/dolfinx/fem/petsc.py", line 1361, in __del__
    lambda obj: obj is not None, (self._snes, self._A, self._b, self._x, self._P_mat)
                                  ^^^^^^^^^^
AttributeError: 'NonlinearProblem' object has no attribute '_snes'

Looking at the full traceback appears to indicate some sort of form compilation error with UFL. Are my changes not the intended fix?

Thank you for any clarification!

It is kind of a subtle issue that you are encountering.
The change of integration measure is only required for the gradient terms (to ensure that the manifold gradient is well defined).
However, the flux terms that couple u and u_sub should use the parent mesh as measure.
There is a limitation in DOLFINx at the moment that means that you need to work around the standard solver/compiler interface to do this.

Here is a code I made a while ago for @RemDelaporteMathurin to bypass this issue:

Hi @dokken, great, thank you for the response and insight. This is good to know.

Should the workaround be treated as a temporary fix, i.e. are there plans for the capability of your script to be merged into dolfinx at some point in the not-too-distant future?

Hi again @dokken ,

I was going through this gist that @RemDelaporteMathurin created, in which he defines a co-dimensional coupling between two, 2D subdomains and the 1D interface between them (you may already be familiar with this?). Essentially, I would like to adapt the workaround that you linked above to the case where we have multiple submeshes. I started following along with the general setup that you created in your script above, using your method of passing the entity maps to each Jacobian:


residual_1 = ufl.extract_blocks(F)
residual_2 = ufl.extract_blocks(F_coupling)

F_1 = dolfinx.fem.form(residual_1, entity_maps=emaps)
F_2 = dolfinx.fem.form(residual_2, entity_maps=emaps)
for i, f in enumerate(F_2):
    if f is None:
        F_2[i] = dolfinx.fem.form(ufl.ZeroBaseForm((vh[i],)))

du = ufl.TrialFunctions(W)

J_1 = ufl.extract_blocks(ufl.derivative(F, (c_Be, c_BeO, c_int), du))
J_2 = ufl.extract_blocks(ufl.derivative(F_coupling, (c_Be, c_BeO, c_int), du))

jacobian_1 = dolfinx.fem.form(J_1, entity_maps=emaps)
jacobian_2 = dolfinx.fem.form(J_2, entity_maps=emaps)
Full code

from mpi4py import MPI

import dolfinx
import dolfinx.fem.petsc
import numpy as np
import ufl
from petsc_solver_for_manifold_derivatives import (
    custom_assemble_jacobian,
    custom_assemble_residual,
)  # file with the two custom functions that you wrote

# Parameters
L = 10.0
x_int = 5.0
D_Be = 1.0
D_BeO = 1.0
k1 = 1.0
k2 = 1.0
k3 = 1.0
k4 = 1.0
lam = 1.0
c_int_max = 1.0
D_int = 1.0  # diffusivity along the interface

dt = 0.1
T = 10.0

# Mesh, cell tags (Be / BeO) and facet tags (interface)
mesh = dolfinx.mesh.create_rectangle(
    MPI.COMM_WORLD,
    [np.array([0.0, 0.0]), np.array([L, 1.0])],
    [20, 10],
    cell_type=dolfinx.mesh.CellType.quadrilateral,
)


vdim = mesh.topology.dim
fdim = vdim - 1
mesh.topology.create_connectivity(fdim, vdim)

eps = 1e-10
BE_TAG, BEO_TAG = 1, 2
be_cells = dolfinx.mesh.locate_entities(mesh, vdim, lambda x: x[0] <= x_int + eps)
beo_cells = dolfinx.mesh.locate_entities(mesh, vdim, lambda x: x[0] >= x_int - eps)
cell_indices = np.concatenate([be_cells, beo_cells])
cell_values = np.concatenate(
    [np.full_like(be_cells, BE_TAG), np.full_like(beo_cells, BEO_TAG)]
).astype(np.int32)
sort = np.argsort(cell_indices)
cell_tags = dolfinx.mesh.meshtags(mesh, vdim, cell_indices[sort], cell_values[sort])

INT_TAG = 3
int_facets = dolfinx.mesh.locate_entities(mesh, fdim, lambda x: np.isclose(x[0], x_int))
facet_tags = dolfinx.mesh.meshtags(
    mesh, fdim, int_facets, np.full_like(int_facets, INT_TAG, dtype=np.int32)
)


# Submeshes (two bulk + one interface) and their entity maps to parent
mesh_Be, Be_emap, _, _ = dolfinx.mesh.create_submesh(mesh, vdim, cell_tags.find(BE_TAG))
mesh_BeO, BeO_emap, _, _ = dolfinx.mesh.create_submesh(
    mesh, vdim, cell_tags.find(BEO_TAG)
)
mesh_int, int_emap, _, _ = dolfinx.mesh.create_submesh(
    mesh, fdim, facet_tags.find(INT_TAG)
)

# ----- build interface integration entities ordered so "+" = Be, "-" = BeO -----
imap = mesh.topology.index_map(vdim)
n_cells = imap.size_local + imap.num_ghosts
cell_marker = np.zeros(n_cells, dtype=np.int32)
cell_marker[cell_tags.indices] = cell_tags.values

f2c = mesh.topology.connectivity(fdim, vdim)
c2f = mesh.topology.connectivity(vdim, fdim)
ints = []
for f in facet_tags.find(INT_TAG):
    c0, c1 = f2c.links(f)
    if cell_marker[c0] == BEO_TAG:  # ensure Be cell first
        c0, c1 = c1, c0
    lf0 = np.where(c2f.links(c0) == f)[0][0]
    lf1 = np.where(c2f.links(c1) == f)[0][0]
    ints += [c0, lf0, c1, lf1]
ints = np.array(ints, dtype=np.int32)


# Function spaces / functions
V_Be = dolfinx.fem.functionspace(mesh_Be, ("CG", 1))
V_BeO = dolfinx.fem.functionspace(mesh_BeO, ("CG", 1))
V_int = dolfinx.fem.functionspace(mesh_int, ("CG", 1))

W = ufl.MixedFunctionSpace(V_Be, V_BeO, V_int)

c_Be = dolfinx.fem.Function(V_Be, name="c_Be")
c_BeO = dolfinx.fem.Function(V_BeO, name="c_BeO")
c_int = dolfinx.fem.Function(V_int, name="c_int")

# previous time step (initial condition = 0)
c_Be_n = dolfinx.fem.Function(V_Be)
c_BeO_n = dolfinx.fem.Function(V_BeO)
c_int_n = dolfinx.fem.Function(V_int)

vh = ufl.TestFunctions(W)
v_Be, v_BeO, v_int = vh

# Measures
dx_Be = ufl.Measure("dx", domain=mesh, subdomain_data=cell_tags, subdomain_id=BE_TAG)
dx_BeO = ufl.Measure("dx", domain=mesh, subdomain_data=cell_tags, subdomain_id=BEO_TAG)
dx_int = ufl.Measure("dx", domain=mesh_int)
dS = ufl.Measure("dS", domain=mesh, subdomain_data=facet_tags)

# Residual (backward Euler)
P, M = "+", "-"  # P = Be side, M = BeO side

theta_P = c_int(P) / c_int_max
theta_M = c_int(M) / c_int_max

# --- group 1: integration domain = parent mesh ---------------------
F = 0
F += ((c_Be - c_Be_n) / dt) * v_Be * dx_Be
F += D_Be * ufl.inner(ufl.grad(c_Be), ufl.grad(v_Be)) * dx_Be
F += (k1 * c_Be(P) * (1 - theta_P) - k2 * c_int(P)) * v_Be(P) * dS(INT_TAG)

F += ((c_BeO - c_BeO_n) / dt) * v_BeO * dx_BeO
F += D_BeO * ufl.inner(ufl.grad(c_BeO), ufl.grad(v_BeO)) * dx_BeO
F += (k3 * c_BeO(M) * (1 - theta_M) - k4 * c_int(M)) * v_BeO(M) * dS(INT_TAG)

# --- group 2: integration domain = mesh_int ------------------------
F += lam * ((c_int - c_int_n) / dt) * v_int * dx_int  # trapping
F += D_int * ufl.inner(ufl.grad(c_int), ufl.grad(v_int)) * dx_int  # diffusion

# interface coupling: stays on dS
F_coupling = (
    -(
        k1 * c_Be(P) * (1 - theta_P)
        - k2 * c_int(P)
        + k3 * c_BeO(M) * (1 - theta_M)
        - k4 * c_int(M)
    )
    * v_int(P)
    * dS(INT_TAG)
)

emaps = [Be_emap, BeO_emap, int_emap]

residual_1 = ufl.extract_blocks(F)
residual_2 = ufl.extract_blocks(F_coupling)

F_1 = dolfinx.fem.form(residual_1, entity_maps=emaps)
F_2 = dolfinx.fem.form(residual_2, entity_maps=emaps)
for i, f in enumerate(F_2):
    if f is None:
        F_2[i] = dolfinx.fem.form(ufl.ZeroBaseForm((vh[i],)))

du = ufl.TrialFunctions(W)

J_1 = ufl.extract_blocks(ufl.derivative(F, (c_Be, c_BeO, c_int), du))
J_2 = ufl.extract_blocks(ufl.derivative(F_coupling, (c_Be, c_BeO, c_int), du))

jacobian_1 = dolfinx.fem.form(J_1, entity_maps=emaps)
jacobian_2 = dolfinx.fem.form(J_2, entity_maps=emaps)
for i in range(len(jacobian_2)):
    if jacobian_2[i][i] is None:
        jacobian_2[i][i] = dolfinx.fem.form(ufl.ZeroBaseForm((du[i], vh[i])))


# Dirichlet BCs:  left (Be, x=0) c=1   ;   right (BeO, x=L) c=0
left_dofs = dolfinx.fem.locate_dofs_geometrical(V_Be, lambda x: np.isclose(x[0], 0.0))
bc_left = dolfinx.fem.dirichletbc(dolfinx.default_scalar_type(1.0), left_dofs, V_Be)

right_dofs = dolfinx.fem.locate_dofs_geometrical(V_BeO, lambda x: np.isclose(x[0], L))
bc_right = dolfinx.fem.dirichletbc(dolfinx.default_scalar_type(0.0), right_dofs, V_BeO)

problem = dolfinx.fem.petsc.NonlinearProblem(
    residual_1,
    [c_Be, c_BeO, c_int],
    J=J_1,
    bcs=[bc_left, bc_right],
    petsc_options_prefix="be_beo_",
    entity_maps=emaps,
)

_blocks = problem.b.getAttr("_blocks")
problem.solver.setFunction(
    custom_assemble_residual,
    problem.b,
    kargs={
        "u": (c_Be, c_BeO, c_int),
        "residual": [F_1, F_2],
        "jacobian": [jacobian_1, jacobian_2],
        "bcs": [bc_left, bc_right],
        "_blocks": _blocks,
    },
)
problem.solver.setJacobian(
    custom_assemble_jacobian,
    problem.A,
    problem.P_mat,
    kargs={
        "u": (c_Be, c_BeO, c_int),
        "jacobian": [jacobian_1, jacobian_2],
        "preconditioner": None,
        "bcs": [bc_left, bc_right],
    },
)

# Time loop
writer_Be = dolfinx.io.VTXWriter(mesh_Be.comm, "results/c_Be.bp", [c_Be])
writer_BeO = dolfinx.io.VTXWriter(mesh_BeO.comm, "results/c_BeO.bp", [c_BeO])
writer_int = dolfinx.io.VTXWriter(mesh_int.comm, "results/c_int.bp", [c_int])

t = 0.0
writer_Be.write(t)
writer_BeO.write(t)
writer_int.write(t)

n_steps = round(T / dt)
for step in range(n_steps):
    t += dt
    problem.solve()

    c_Be_n.x.array[:] = c_Be.x.array
    c_BeO_n.x.array[:] = c_BeO.x.array
    c_int_n.x.array[:] = c_int.x.array

    writer_Be.write(t)
    writer_BeO.write(t)
    writer_int.write(t)

    if mesh.comm.rank == 0:
        print(f"t = {t:.2f}")

writer_Be.close()
writer_BeO.close()
writer_int.close()
print(c_int.x.array)

However, upon running this I get the error RuntimeError: Incompatible mesh. argument entity_maps must be provided.

I think the issue stems from entity_maps only being defined between a submesh and its parent, but the Jacobians as written contain derivatives between submeshes, which don’t have corresponding entries in entity_maps.

Wrapping the code for J_1 and J_2 with the function below appears to be a workaround to this issue, although likely not an ideal one. Is there another modification you might recommend for multiple submeshes? Thank you again for your assistance!

# This is a somewhat hacky workaround but it runs and gives the same result
# as the original mwe (see inteface_trapping.py)
def prune_zero_blocks(J):
    """Replace structurally zero Jacobian blocks by None.

    ``ufl.extract_blocks`` returns (zero) derivative forms for every block.
    Blocks like dJ(interface)/dc_Be couple two *sibling* submeshes with no
    entity map relating them, so they cannot be compiled; since they are
    identically zero they are dropped instead.
    """
    J = [list(row) for row in J]
    for i in range(len(J)):
        for j in range(len(J[i])):
            if (
                J[i][j] is not None
                and ufl.algorithms.expand_derivatives(J[i][j]).empty()
            ):
                J[i][j] = None
    return J


J_1 = prune_zero_blocks(ufl.extract_blocks(ufl.derivative(F, (c_Be, c_BeO, c_int), du)))
J_2 = prune_zero_blocks(
    ufl.extract_blocks(ufl.derivative(F_coupling, (c_Be, c_BeO, c_int), du))
)

Update: it appears simply applying ufl.algorithms.expand_derivatives directly to J_1 and J_2 before ufl.extract_blocks fixes this issue in a much more succinct way:

J_1 = ufl.extract_blocks(
    expand_derivatives(ufl.derivative(F, (c_Be, c_BeO, c_int), du))
)
J_2 = ufl.extract_blocks(
    expand_derivatives(ufl.derivative(F_coupling, (c_Be, c_BeO, c_int), du))
)

Per the docstring of expand_derivatives,

In the returned expression g which is mathematically equivalent to expr, there are no VariableDerivative or CoefficientDerivative objects left, and Grad objects have been propagated to Terminal nodes.

So I guess my issue above was the persistence of a CoefficientDerivative object that shouldn’t have been there - perhaps a term that should evaluate to zero but was still technically a non-empty expression. extract_blocks() couldn’t recognize it was mathematically zero and thereby didn’t assign it a value of None, dolfinx.fem.form got this non-None expression and looked for the entity map relation between sibling submeshes which didn’t exist, and threw the error.