Cross product of normal and test function in the variational form using ufl's FacetNormal?

For the variational form of the (inhomogeneous) shallow water equations, I need to find

u^\bot = \textbf{k}\times \textbf{u}

where \textbf{k} is the unit vector pointing normal to the surface. I tried to use ufl.FacetNormal taking inspiration from this example to create the unit vector normal to the mesh k = ufl.FacetNormal(mesh), and from this, I thought simply running u_n = ufl.cross(k,u) would suffice. Running the following compiled

from mpi4py import MPI
import meshio
import numpy as np
import ufl
import basix.ufl
import dolfinx as df
import scifem
from petsc4py import PETSc
from dolfinx.fem.petsc import apply_lifting, assemble_matrix, assemble_vector, set_bc
from Projector import Projector

order = 1
exact_case = 2
tmp_data = meshio.dolfin.read("supplement/examples/mixed-poisson/hdiv-l2/meshes/sphere_ico4.xml")

mesh = df.mesh.create_mesh(
    comm=MPI.COMM_WORLD,
    cells=tmp_data.cells_dict["triangle"],
    x=tmp_data.points,
    e=ufl.Mesh(basix.ufl.element("Lagrange", "triangle", 1, shape=(3,))),
)

def create_function_spaces(mesh, family, order):
    S = df.fem.functionspace(mesh, (family, order))
    V = df.fem.functionspace(mesh, ("DG", order-1))
    PlottingMesh = df.fem.functionspace(mesh, ("CG", order))
    return (S, V, PlottingMesh)

def initial_conditions(S, V):
    u0 = df.fem.Function(S)
    u0.interpolate(lambda x: np.zeros(x[:S.mesh.topology.dim].shape))
    f = ufl.exp(-((-x[1]-1)/0.1)**2)
    petsc_options = {"ksp_type": "preonly", "pc_type": "lu"}
    V_projector = Projector(V, petsc_options=petsc_options)
    D0 = V_projector.project(f)
    return (u0, D0)

# Define global normal
x = ufl.SpatialCoordinate(mesh)
global_normal = x # Not sure exactly what this is used for
family = "RT"
order = 1

# Define mixed mesh
(S,V,PlottingMesh) = create_function_spaces(mesh,family,order)
W = ufl.MixedFunctionSpace(*(S, V))

# Constants
dt = df.default_scalar_type(0.05)
H = 1.0
g = 1.0

# forcing term
f = x[2]

# Initial values of solutions u and D
u_, D_ = initial_conditions(S,V)

# normal unit vector
k = ufl.FacetNormal(mesh)

# u_n = k x u
u_n = ufl.cross(k,u)

F = (ufl.inner(u - u_, w) - dt*ufl.div(w)*g*D_mid + dt*f*ufl.inner(u_n, w)
    + ufl.inner(D - D_, phi) + dt*H*ufl.div(u_mid)*phi)*ufl.dx

(a, L) = ufl.system(F)

where the mesh is a 2D spherical manifold of radius 1 and the specific mesh used is found in the following file (make sure to extract the .gz file containing the mesh), and the code for the projector can be found here.

However, the code breaks when attempting to setup the problem

petsc_options = {
    "ksp_type": "preonly",
    "pc_type": "lu",
    "pc_factor_mat_solver_type": "mumps",
    "ksp_error_if_not_converged": True,
}
problem = df.fem.petsc.LinearProblem(
    ufl.extract_blocks(a),
    L,
    bcs=[],
    petsc_options=petsc_options,
    petsc_options_prefix="mixed_poisson_",
    kind="mpi",
)

with error

ValueError: Integral of type cell cannot contain a ReferenceNormal.

which I presume to mean that the cross product with k being in the variational form is causing the error. Are there things I can do to get around this, or should ufl.FacetNormal generally be avoided for what I’m trying to do?

You need the ufl.CellNormal, as you are integrating over cells, not facets.

1 Like

Okay that resolves that issue. Now I’m running into another being a timeout error which tracebacked to attempting create_form for L.

Running df.fem.form(a) also causes a timeout with the following traceback

---------------------------------------------------------------------------
FileExistsError                           Traceback (most recent call last)
File /dolfinx-env/lib/python3.12/site-packages/ffcx/codegeneration/jit.py:98, in get_cached_module(module_name, object_names, cache_dir, timeout)
     96 try:
     97     # Create C file with exclusive access
---> 98     with open(c_filename, "x"):
     99         pass

FileExistsError: [Errno 17] File exists: '/root/.cache/fenics/libffcx_forms_ef21e46178e68bfbcd19443e673c35419b88f2a3.c'

During handling of the above exception, another exception occurred:

TimeoutError                              Traceback (most recent call last)
Cell In[16], line 1
----> 1 df.fem.form(a)

File /usr/local/dolfinx-real/lib/python3.12/dist-packages/dolfinx/fem/forms.py:438, in form(form, dtype, form_compiler_options, jit_options, entity_maps)
    435     else:
    436         return form
--> 438 return _create_form(form)

File /usr/local/dolfinx-real/lib/python3.12/dist-packages/dolfinx/fem/forms.py:430, in form.<locals>._create_form(form)
    420 """Recursively convert ufl.Forms to dolfinx.fem.Form.
    421 
    422 Args:
   (...)    427     A ``dolfinx.fem.Form`` or a list of ``dolfinx.fem.Form``.
    428 """
    429 if isinstance(form, ufl.Form):
--> 430     return _form(form)
    431 elif isinstance(form, ufl.ZeroBaseForm):
    432     return _zero_form(form)

File /usr/local/dolfinx-real/lib/python3.12/dist-packages/dolfinx/fem/forms.py:339, in form.<locals>._form(form)
    337 if msh is None:
    338     raise RuntimeError("Expecting to find a Mesh in the form.")
--> 339 ufcx_form, module, code = jit.ffcx_jit(
    340     msh.comm, form, form_compiler_options=form_compiler_options, jit_options=jit_options
    341 )
    343 # For each argument in form extract its function space
    344 V = [arg.ufl_function_space()._cpp_object for arg in form.arguments()]

File /usr/local/dolfinx-real/lib/python3.12/dist-packages/dolfinx/jit.py:61, in mpi_jit_decorator.<locals>.mpi_jit(comm, *args, **kwargs)
     57 @functools.wraps(local_jit)
     58 def mpi_jit(comm, *args, **kwargs):
     59     # Just call JIT compiler when running in serial
     60     if comm.size == 1:
---> 61         return local_jit(*args, **kwargs)
     63     # Default status (0 == ok, 1 == fail)
     64     status = 0

File /usr/local/dolfinx-real/lib/python3.12/dist-packages/dolfinx/jit.py:216, in ffcx_jit(ufl_object, form_compiler_options, jit_options)
    214 # Switch on type and compile, returning cffi object
    215 if isinstance(ufl_object, ufl.Form):
--> 216     r = ffcx.codegeneration.jit.compile_forms([ufl_object], options=p_ffcx, **p_jit)
    217 elif isinstance(ufl_object, tuple) and isinstance(ufl_object[0], ufl.core.expr.Expr):
    218     r = ffcx.codegeneration.jit.compile_expressions([ufl_object], options=p_ffcx, **p_jit)

File /dolfinx-env/lib/python3.12/site-packages/ffcx/codegeneration/jit.py:189, in compile_forms(forms, options, cache_dir, timeout, cffi_extra_compile_args, cffi_verbose, cffi_debug, cffi_libraries, visualise)
    187 if cache_dir is not None:
    188     cache_dir = Path(cache_dir)
--> 189     obj, mod = get_cached_module(module_name, form_names, cache_dir, timeout)
    190     if obj is not None:
    191         return obj, mod, (None, None)

File /dolfinx-env/lib/python3.12/site-packages/ffcx/codegeneration/jit.py:123, in get_cached_module(module_name, object_names, cache_dir, timeout)
    121     logger.info(f"Waiting for {ready_name} to appear.")
    122     time.sleep(1)
--> 123 raise TimeoutError(
    124     "JIT compilation timed out, probably due to a failed previous compile. "
    125     f"Try cleaning cache (e.g. remove {c_filename}) or increase timeout option."
    126 )

TimeoutError: JIT compilation timed out, probably due to a failed previous compile. Try cleaning cache (e.g. remove /root/.cache/fenics/libffcx_forms_ef21e46178e68bfbcd19443e673c35419b88f2a3.c) or increase timeout option.

Do you know what might be causing this?

There is so much code missing from your example, that anything could be wrong at this point.

For instance, you never define most of the variables.

You also haven’t specified which version of DOLFINx, ufl etc that you are using.

Here is a code that runs in the nightly docker image:

from mpi4py import MPI
import meshio
import numpy as np
import ufl
import basix.ufl
import dolfinx as df
from petsc4py import PETSc

from typing import Optional

import numpy as np
import dolfinx.fem.petsc


class Projector:
    """
    Projector for a given function.
    Solves Ax=b, where

    .. highlight:: python
    .. code-block:: python

        u, v = ufl.TrialFunction(Space), ufl.TestFunction(space)
        dx = ufl.Measure("dx", metadata=metadata)
        A = inner(u, v) * dx
        b = inner(function, v) * dx(metadata=metadata)

    Args:
        function: UFL expression of function to project
        space: Space to project function into
        petsc_options: Options to pass to PETSc
        jit_options: Options to pass to just in time compiler
        form_compiler_options: Options to pass to the form compiler
        metadata: Data to pass to the integration measure
    """

    _A: PETSc.Mat  # The mass matrix
    _b: PETSc.Vec  # The rhs vector
    _lhs: dolfinx.fem.Form  # The compiled form for the mass matrix
    _ksp: PETSc.KSP  # The PETSc solver
    _x: dolfinx.fem.Function  # The solution vector
    _dx: ufl.Measure  # Integration measure

    def __init__(
        self,
        space: dolfinx.fem.FunctionSpace,
        petsc_options: Optional[dict] = None,
        jit_options: Optional[dict] = None,
        form_compiler_options: Optional[dict] = None,
        metadata: Optional[dict] = None,
    ):
        petsc_options = {} if petsc_options is None else petsc_options
        jit_options = {} if jit_options is None else jit_options
        form_compiler_options = (
            {} if form_compiler_options is None else form_compiler_options
        )

        # Assemble projection matrix once
        u = ufl.TrialFunction(space)
        v = ufl.TestFunction(space)
        self._dx = ufl.Measure("dx", domain=space.mesh, metadata=metadata)
        a = ufl.inner(u, v) * self._dx(metadata=metadata)
        self._lhs = dolfinx.fem.form(
            a, jit_options=jit_options, form_compiler_options=form_compiler_options
        )
        self._A = dolfinx.fem.petsc.assemble_matrix(self._lhs)
        self._A.assemble()

        # Create vectors to store right hand side and the solution
        self._x = dolfinx.fem.Function(space)
        self._b = dolfinx.fem.Function(space)

        # Create Krylov Subspace solver
        self._ksp = PETSc.KSP().create(space.mesh.comm)
        self._ksp.setOperators(self._A)

        # Set PETSc options
        prefix = f"projector_{id(self)}"
        opts = PETSc.Options()
        opts.prefixPush(prefix)
        for k, v in petsc_options.items():
            opts[k] = v
        opts.prefixPop()
        self._ksp.setFromOptions()
        for opt in opts.getAll().keys():
            del opts[opt]

        # Set matrix and vector PETSc options
        self._A.setOptionsPrefix(prefix)
        self._A.setFromOptions()
        self._b.x.petsc_vec.setOptionsPrefix(prefix)
        self._b.x.petsc_vec.setFromOptions()

    def reassemble_lhs(self):
        dolfinx.fem.petsc.assemble_matrix(self._A, self._lhs)
        self._A.assemble()

    def assemble_rhs(self, h: ufl.core.expr.Expr):
        """
        Assemble the right hand side of the problem
        """
        v = ufl.TestFunction(self._b.function_space)
        rhs = ufl.inner(h, v) * self._dx
        rhs_compiled = dolfinx.fem.form(rhs)
        self._b.x.array[:] = 0.0
        dolfinx.fem.petsc.assemble_vector(self._b.x.petsc_vec, rhs_compiled)
        self._b.x.petsc_vec.ghostUpdate(
            addv=PETSc.InsertMode.ADD_VALUES, mode=PETSc.ScatterMode.REVERSE
        )
        self._b.x.petsc_vec.ghostUpdate(
            addv=PETSc.InsertMode.INSERT_VALUES, mode=PETSc.ScatterMode.FORWARD
        )

    def project(self, h: ufl.core.expr.Expr) -> dolfinx.fem.Function:
        """
        Compute projection using a PETSc KSP solver

        Args:
            assemble_rhs: Re-assemble RHS and re-apply boundary conditions if true
        """
        self.assemble_rhs(h)
        self._ksp.solve(self._b.x.petsc_vec, self._x.x.petsc_vec)
        return self._x

    def __del__(self):
        self._A.destroy()
        self._ksp.destroy()


order = 1
exact_case = 2
tmp_data = meshio.dolfin.read(
    "supplement/examples/mixed-poisson/hdiv-l2/meshes/sphere_ico4.xml"
)

mesh = df.mesh.create_mesh(
    comm=MPI.COMM_WORLD,
    cells=tmp_data.cells_dict["triangle"],
    x=tmp_data.points,
    e=ufl.Mesh(basix.ufl.element("Lagrange", "triangle", 1, shape=(3,))),
)


def create_function_spaces(mesh, family, order):
    S = df.fem.functionspace(mesh, (family, order))
    V = df.fem.functionspace(mesh, ("DG", order - 1))
    PlottingMesh = df.fem.functionspace(mesh, ("CG", order))
    return (S, V, PlottingMesh)


def initial_conditions(S, V):
    u0 = df.fem.Function(S)
    u0.x.array[:] = 0.0
    f = ufl.exp(-(((-x[1] - 1) / 0.1) ** 2))
    petsc_options = {"ksp_type": "preonly", "pc_type": "lu"}
    V_projector = Projector(V, petsc_options=petsc_options)
    D0 = V_projector.project(f)
    return (u0, D0)


# Define global normal
x = ufl.SpatialCoordinate(mesh)
global_normal = x  # Not sure exactly what this is used for
family = "RT"
order = 1

# Define mixed mesh
(S, V, PlottingMesh) = create_function_spaces(mesh, family, order)
W = ufl.MixedFunctionSpace(*(S, V))

# Constants
dt = df.default_scalar_type(0.05)
H = 1.0
g = 1.0

# forcing term
f = x[2]

# Initial values of solutions u and D
u_, D_ = initial_conditions(S, V)
w, phi = ufl.TestFunctions(W)
D = dolfinx.fem.Function(V)
u = dolfinx.fem.Function(S)

D_mid = 0.5 * (D + D_)
u_mid = 0.5 * (u_ + u)
# normal unit vector
k = ufl.CellNormal(mesh)


# u_n = k x u
u_n = ufl.cross(k, u)

F = (
    ufl.inner(u - u_, w)
    - dt * ufl.div(w) * g * D_mid
    + dt * f * ufl.inner(u_n, w)
    + ufl.inner(D - D_, phi)
    + dt * H * ufl.div(u_mid) * phi
) * ufl.dx

(a, L) = ufl.system(F)

For further questions, please make an effort in making your example reproducible.

You’re right I realize I forgot to copy paste my code segment and run to make sure it worked before posting, otherwise I would have seen that I forgot to define many variables. It’s roughly 4 AM where I am, I should have saved my questions for the next day and I would have less likely made these mistakes.

And yes, not everyone here knows that I’m on the nightly version, so specifying my version should be something that I’ll make known in the future. I will add that to my question in an edit.

Yes the code you’ve created fills in several of the gaps. The only difference is the definitions of u and D

(u, D) = ufl.TrialFunctions(W)
(w, phi) = ufl.TestFunctions(W)

So the full code from start to finish for the code which successfully compiles is

from mpi4py import MPI
import meshio
import numpy as np
import ufl
import basix.ufl
import dolfinx as df
from petsc4py import PETSc

from typing import Optional

import numpy as np
import dolfinx.fem.petsc


class Projector:
    """
    Projector for a given function.
    Solves Ax=b, where

    .. highlight:: python
    .. code-block:: python

        u, v = ufl.TrialFunction(Space), ufl.TestFunction(space)
        dx = ufl.Measure("dx", metadata=metadata)
        A = inner(u, v) * dx
        b = inner(function, v) * dx(metadata=metadata)

    Args:
        function: UFL expression of function to project
        space: Space to project function into
        petsc_options: Options to pass to PETSc
        jit_options: Options to pass to just in time compiler
        form_compiler_options: Options to pass to the form compiler
        metadata: Data to pass to the integration measure
    """

    _A: PETSc.Mat  # The mass matrix
    _b: PETSc.Vec  # The rhs vector
    _lhs: dolfinx.fem.Form  # The compiled form for the mass matrix
    _ksp: PETSc.KSP  # The PETSc solver
    _x: dolfinx.fem.Function  # The solution vector
    _dx: ufl.Measure  # Integration measure

    def __init__(
        self,
        space: dolfinx.fem.FunctionSpace,
        petsc_options: Optional[dict] = None,
        jit_options: Optional[dict] = None,
        form_compiler_options: Optional[dict] = None,
        metadata: Optional[dict] = None,
    ):
        petsc_options = {} if petsc_options is None else petsc_options
        jit_options = {} if jit_options is None else jit_options
        form_compiler_options = (
            {} if form_compiler_options is None else form_compiler_options
        )

        # Assemble projection matrix once
        u = ufl.TrialFunction(space)
        v = ufl.TestFunction(space)
        self._dx = ufl.Measure("dx", domain=space.mesh, metadata=metadata)
        a = ufl.inner(u, v) * self._dx(metadata=metadata)
        self._lhs = dolfinx.fem.form(
            a, jit_options=jit_options, form_compiler_options=form_compiler_options
        )
        self._A = dolfinx.fem.petsc.assemble_matrix(self._lhs)
        self._A.assemble()

        # Create vectors to store right hand side and the solution
        self._x = dolfinx.fem.Function(space)
        self._b = dolfinx.fem.Function(space)

        # Create Krylov Subspace solver
        self._ksp = PETSc.KSP().create(space.mesh.comm)
        self._ksp.setOperators(self._A)

        # Set PETSc options
        prefix = f"projector_{id(self)}"
        opts = PETSc.Options()
        opts.prefixPush(prefix)
        for k, v in petsc_options.items():
            opts[k] = v
        opts.prefixPop()
        self._ksp.setFromOptions()
        for opt in opts.getAll().keys():
            del opts[opt]

        # Set matrix and vector PETSc options
        self._A.setOptionsPrefix(prefix)
        self._A.setFromOptions()
        self._b.x.petsc_vec.setOptionsPrefix(prefix)
        self._b.x.petsc_vec.setFromOptions()

    def reassemble_lhs(self):
        dolfinx.fem.petsc.assemble_matrix(self._A, self._lhs)
        self._A.assemble()

    def assemble_rhs(self, h: ufl.core.expr.Expr):
        """
        Assemble the right hand side of the problem
        """
        v = ufl.TestFunction(self._b.function_space)
        rhs = ufl.inner(h, v) * self._dx
        rhs_compiled = dolfinx.fem.form(rhs)
        self._b.x.array[:] = 0.0
        dolfinx.fem.petsc.assemble_vector(self._b.x.petsc_vec, rhs_compiled)
        self._b.x.petsc_vec.ghostUpdate(
            addv=PETSc.InsertMode.ADD_VALUES, mode=PETSc.ScatterMode.REVERSE
        )
        self._b.x.petsc_vec.ghostUpdate(
            addv=PETSc.InsertMode.INSERT_VALUES, mode=PETSc.ScatterMode.FORWARD
        )

    def project(self, h: ufl.core.expr.Expr) -> dolfinx.fem.Function:
        """
        Compute projection using a PETSc KSP solver

        Args:
            assemble_rhs: Re-assemble RHS and re-apply boundary conditions if true
        """
        self.assemble_rhs(h)
        self._ksp.solve(self._b.x.petsc_vec, self._x.x.petsc_vec)
        return self._x

    def __del__(self):
        self._A.destroy()
        self._ksp.destroy()


order = 1
exact_case = 2
tmp_data = meshio.dolfin.read(
    "supplement/examples/mixed-poisson/hdiv-l2/meshes/sphere_ico4.xml"
)

mesh = df.mesh.create_mesh(
    comm=MPI.COMM_WORLD,
    cells=tmp_data.cells_dict["triangle"],
    x=tmp_data.points,
    e=ufl.Mesh(basix.ufl.element("Lagrange", "triangle", 1, shape=(3,))),
)


def create_function_spaces(mesh, family, order):
    S = df.fem.functionspace(mesh, (family, order))
    V = df.fem.functionspace(mesh, ("DG", order - 1))
    PlottingMesh = df.fem.functionspace(mesh, ("CG", order))
    return (S, V, PlottingMesh)


def initial_conditions(S, V):
    u0 = df.fem.Function(S)
    u0.x.array[:] = 0.0
    f = ufl.exp(-(((-x[1] - 1) / 0.1) ** 2))
    petsc_options = {"ksp_type": "preonly", "pc_type": "lu"}
    V_projector = Projector(V, petsc_options=petsc_options)
    D0 = V_projector.project(f)
    return (u0, D0)


# Define global normal
x = ufl.SpatialCoordinate(mesh)
global_normal = x  # Not sure exactly what this is used for
family = "RT"
order = 1

# Define mixed mesh
(S, V, PlottingMesh) = create_function_spaces(mesh, family, order)
W = ufl.MixedFunctionSpace(*(S, V))

# Constants
dt = df.default_scalar_type(0.05)
H = 1.0
g = 1.0

# forcing term
f = x[2]

# Initial values of solutions u and D
u_, D_ = initial_conditions(S, V)
(u, D) = ufl.TrialFunctions(W)
(w, phi) = ufl.TestFunctions(W)

D_mid = 0.5 * (D + D_)
u_mid = 0.5 * (u_ + u)
# normal unit vector
k = ufl.CellNormal(mesh)


# u_n = k x u
u_n = ufl.cross(k, u)

F = (
    ufl.inner(u - u_, w)
    - dt * ufl.div(w) * g * D_mid
    + dt * f * ufl.inner(u_n, w)
    + ufl.inner(D - D_, phi)
    + dt * H * ufl.div(u_mid) * phi
) * ufl.dx

(a, L) = ufl.system(F)

Sorry again for flubbing that so badly.

1 Like

My current versions are

fenics-dolfinx             0.10.0.dev0
fenics-ufl                 2025.2.0.dev0

which is accessible on the Dolfinx nightly version