Quadrature elements and boundary integration

Hello everyone,
I am currently facing difficulties with the use of quadrature elements. I have an internal variable (damage) that I would like to only define at the integration points. I am performing explicit dynamic simulations and need to form the residual Res = a(u, u_) - l(u_). My issue lies in the definition of the right-hand side, especially when there are loads on a boundary of the domain. Indeed, I encounter an error message when trying to construct form(Res) with a surface measure ds. I have provided a MWE below illustrating the problem:

from dolfinx.fem import Function, FunctionSpace, form
from dolfinx.mesh import create_unit_interval
from ufl import Measure, TestFunction, FiniteElement, inner
from mpi4py.MPI import COMM_WORLD

mesh = create_unit_interval(COMM_WORLD, 1)
V = FunctionSpace(mesh, ("CG", 1))

working = False
if working:
    V_d  = FunctionSpace(mesh, ("DG", 0))
if not working:
    W0e = FiniteElement("Quadrature", mesh.ufl_cell(), degree = 1, quad_scheme="default")
    V_d  = FunctionSpace(mesh, W0e)
lmbda = 2
mu = 3

def eps_xx(v):
    return v.dx(0)

def sigma_xx(v, d):
    return (1-d)**2 * (lmbda * v.dx(0) + 2 * mu * v.dx(0))

u = Function(V, name = "displacement")
u_ = TestFunction(V)
d = Function(V_d, name = "damage")
metadata = {"quadrature_rule": "default", "quadrature_degree": 1}
dx = Measure("dx", domain = mesh, metadata = metadata)
ds = Measure('ds')
a = inner(sigma_xx(u, d), eps_xx(u_)) * dx
l = 1 * u_ * ds
residual = a - l
residual_form = form(residual)

I suspect that this has to do with the position of the integration points.
EDIT: Here is the error:

  File "/home/bouteillerp/.local/lib/python3.10/site-packages/spyder_kernels/py3compat.py", line 356, in compat_exec
    exec(code, globals, locals)

  File "/home/bouteillerp/Bureau/Codes/Dolfinx/Debug_Fenicsx/Melange_quadrature.py", line 33, in <module>
    residual_form = form(residual)

  File "/usr/lib/petsc/lib/python3/dist-packages/dolfinx/fem/forms.py", line 188, in form
    return _create_form(form)

  File "/usr/lib/petsc/lib/python3/dist-packages/dolfinx/fem/forms.py", line 183, in _create_form
    return _form(form)

  File "/usr/lib/petsc/lib/python3/dist-packages/dolfinx/fem/forms.py", line 141, in _form
    ufcx_form, module, code = jit.ffcx_jit(mesh.comm, form,

  File "/usr/lib/petsc/lib/python3/dist-packages/dolfinx/jit.py", line 56, in mpi_jit
    return local_jit(*args, **kwargs)

  File "/usr/lib/petsc/lib/python3/dist-packages/dolfinx/jit.py", line 204, in ffcx_jit
    r = ffcx.codegeneration.jit.compile_forms([ufl_object], options=p_ffcx, **p_jit)

  File "/usr/lib/python3/dist-packages/ffcx/codegeneration/jit.py", line 199, in compile_forms
    raise e

  File "/usr/lib/python3/dist-packages/ffcx/codegeneration/jit.py", line 190, in compile_forms
    impl = _compile_objects(decl, forms, form_names, module_name, p, cache_dir,

  File "/usr/lib/python3/dist-packages/ffcx/codegeneration/jit.py", line 260, in _compile_objects
    _, code_body = ffcx.compiler.compile_ufl_objects(ufl_objects, prefix=module_name, options=options)

  File "/usr/lib/python3/dist-packages/ffcx/compiler.py", line 102, in compile_ufl_objects
    ir = compute_ir(analysis, object_names, prefix, options, visualise)

  File "/usr/lib/python3/dist-packages/ffcx/ir/representation.py", line 197, in compute_ir
    irs = [_compute_integral_ir(fd, i, analysis.element_numbers, integral_names, finite_element_names,

  File "/usr/lib/python3/dist-packages/ffcx/ir/representation.py", line 197, in <listcomp>
    irs = [_compute_integral_ir(fd, i, analysis.element_numbers, integral_names, finite_element_names,

  File "/usr/lib/python3/dist-packages/ffcx/ir/representation.py", line 492, in _compute_integral_ir
    integral_ir = compute_integral_ir(itg_data.domain.ufl_cell(), itg_data.integral_type,

  File "/usr/lib/python3/dist-packages/ffcx/ir/integral.py", line 84, in compute_integral_ir
    mt_table_reference = build_optimized_tables(

  File "/usr/lib/python3/dist-packages/ffcx/ir/elementtables.py", line 361, in build_optimized_tables
    t = get_ffcx_table_values(quadrature_rule.points, cell,

  File "/usr/lib/python3/dist-packages/ffcx/ir/elementtables.py", line 124, in get_ffcx_table_values
    entity_points = map_integral_points(points, integral_type, cell, entity)

  File "/usr/lib/python3/dist-packages/ffcx/ir/representationutils.py", line 92, in map_integral_points
    assert points.shape[1] == tdim - 1

AssertionError

I am currently using DG0 elements for internal variables and CG1 for the displacement, but I would like to raise the interpolation degree so that it would require Quadrature elements for the internal variables.

Do you have any guidance on how to resolve this issue?
Sincerely,
Paul

Hello Paul,
this is related to this issue

which should be fixed in v0.7, see for instance this plasticity demo:

Elasto-plastic analysis of a 2D von Mises material — Computational Mechanics Numerical Tours with FEniCSx

However, there were still some issues left (see below) with basix Quadrature elements in v0.7 which are fixed in the current v0.8 version

2 Likes

Hello Jeremy and all,

I encountered this issue again when I tried to integrate quadrature element over boundary in dolfinx v0.9.0.

Enviroment

My fenicsx is installed using conda and has been updated to latest version before testing.

Package Version
fenics-basix 0.9.0
fenics-basix-nanobind-abi 0.2.1.13
fenics-dolfinx 0.9.0
fenics-ffcx 0.9.0
fenics-libbasix 0.9.0
fenics-libdolfinx 0.9.0
fenics-ufcx 0.9.0
fenics-ufl 2024.2.0

MWE

import numpy as np
import basix.ufl
import ufl
from dolfinx import fem, mesh
from mpi4py import MPI

domain = mesh.create_rectangle(MPI.COMM_WORLD, [[0, 0], [2, 2]], [10, 10])

quad_degree = 0
quadrature_element = basix.ufl.quadrature_element(
    domain.basix_cell(), value_shape=(), degree=quad_degree
)

Q = fem.functionspace(domain, quadrature_element )

ds = ufl.ds(degree=quad_degree)

# scalar function `f(x)`
f = fem.Function(Q)

# assert fails here
# --------------------------------------------
fem.form(f * ds)
# --------------------------------------------
Full traceback
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[2], line 3
      1 # assert fails here
      2 # --------------------------------------------
----> 3 fem.form(f * ds)

File ~/miniconda3/envs/fenicsx/lib/python3.13/site-packages/dolfinx/fem/forms.py:337, in form(form, dtype, form_compiler_options, jit_options, entity_maps)
    334     else:
    335         return form
--> 337 return _create_form(form)

File ~/miniconda3/envs/fenicsx/lib/python3.13/site-packages/dolfinx/fem/forms.py:331, in form.<locals>._create_form(form)
    329         return None
    330     else:
--> 331         return _form(form)
    332 elif isinstance(form, collections.abc.Iterable):
    333     return list(map(lambda sub_form: _create_form(sub_form), form))

File ~/miniconda3/envs/fenicsx/lib/python3.13/site-packages/dolfinx/fem/forms.py:254, in form.<locals>._form(form)
    252 if mesh is None:
    253     raise RuntimeError("Expecting to find a Mesh in the form.")
--> 254 ufcx_form, module, code = jit.ffcx_jit(
    255     mesh.comm, form, form_compiler_options=form_compiler_options, jit_options=jit_options
    256 )
    258 # For each argument in form extract its function space
    259 V = [arg.ufl_function_space()._cpp_object for arg in form.arguments()]

File ~/miniconda3/envs/fenicsx/lib/python3.13/site-packages/dolfinx/jit.py:62, in mpi_jit_decorator.<locals>.mpi_jit(comm, *args, **kwargs)
     58 @functools.wraps(local_jit)
     59 def mpi_jit(comm, *args, **kwargs):
     60     # Just call JIT compiler when running in serial
     61     if comm.size == 1:
---> 62         return local_jit(*args, **kwargs)
     64     # Default status (0 == ok, 1 == fail)
     65     status = 0

File ~/miniconda3/envs/fenicsx/lib/python3.13/site-packages/dolfinx/jit.py:212, in ffcx_jit(ufl_object, form_compiler_options, jit_options)
    210 # Switch on type and compile, returning cffi object
    211 if isinstance(ufl_object, ufl.Form):
--> 212     r = ffcx.codegeneration.jit.compile_forms([ufl_object], options=p_ffcx, **p_jit)
    213 elif isinstance(ufl_object, ufl.Mesh):
    214     r = ffcx.codegeneration.jit.compile_coordinate_maps([ufl_object], options=p_ffcx, **p_jit)

File ~/miniconda3/envs/fenicsx/lib/python3.13/site-packages/ffcx/codegeneration/jit.py:225, in compile_forms(forms, options, cache_dir, timeout, cffi_extra_compile_args, cffi_verbose, cffi_debug, cffi_libraries, visualise)
    223     except Exception:
    224         pass
--> 225     raise e
    227 obj, module = _load_objects(cache_dir, module_name, form_names)
    228 return obj, module, (decl, impl)

File ~/miniconda3/envs/fenicsx/lib/python3.13/site-packages/ffcx/codegeneration/jit.py:205, in compile_forms(forms, options, cache_dir, timeout, cffi_extra_compile_args, cffi_verbose, cffi_debug, cffi_libraries, visualise)
    202     for name in form_names:
    203         decl += form_template.format(name=name)
--> 205     impl = _compile_objects(
    206         decl,
    207         forms,
    208         form_names,
    209         module_name,
    210         p,
    211         cache_dir,
    212         cffi_extra_compile_args,
    213         cffi_verbose,
    214         cffi_debug,
    215         cffi_libraries,
    216         visualise=visualise,
    217     )
    218 except Exception as e:
    219     try:
    220         # remove c file so that it will not timeout next time

File ~/miniconda3/envs/fenicsx/lib/python3.13/site-packages/ffcx/codegeneration/jit.py:330, in _compile_objects(decl, ufl_objects, object_names, module_name, options, cache_dir, cffi_extra_compile_args, cffi_verbose, cffi_debug, cffi_libraries, visualise)
    326 libraries = _libraries + cffi_libraries if cffi_libraries is not None else _libraries
    328 # JIT uses module_name as prefix, which is needed to make names of all struct/function
    329 # unique across modules
--> 330 _, code_body = ffcx.compiler.compile_ufl_objects(
    331     ufl_objects, prefix=module_name, options=options, visualise=visualise
    332 )
    334 # Raise error immediately prior to compilation if no support for C99
    335 # _Complex. Doing this here allows FFCx to be used for complex codegen on
    336 # Windows.
    337 if sys.platform.startswith("win32"):

File ~/miniconda3/envs/fenicsx/lib/python3.13/site-packages/ffcx/compiler.py:113, in compile_ufl_objects(ufl_objects, options, object_names, prefix, visualise)
    111 # Stage 2: intermediate representation
    112 cpu_time = time()
--> 113 ir = compute_ir(analysis, _object_names, _prefix, options, visualise)
    114 _print_timing(2, time() - cpu_time)
    116 # Stage 3: code generation

File ~/miniconda3/envs/fenicsx/lib/python3.13/site-packages/ffcx/ir/representation.py:135, in compute_ir(analysis, object_names, prefix, options, visualise)
    129     for itg_index, itg_data in enumerate(fd.integral_data):
    130         integral_names[(fd_index, itg_index)] = naming.integral_name(
    131             fd.original_form, itg_data.integral_type, fd_index, itg_data.subdomain_id, prefix
    132         )
    134 irs = [
--> 135     _compute_integral_ir(
    136         fd,
    137         i,
    138         analysis.element_numbers,
    139         integral_names,
    140         finite_element_hashes,
    141         options,
    142         visualise,
    143     )
    144     for (i, fd) in enumerate(analysis.form_data)
    145 ]
    146 ir_integrals = list(itertools.chain(*irs))
    148 ir_forms = [
    149     _compute_form_ir(
    150         fd,
   (...)    159     for (i, fd) in enumerate(analysis.form_data)
    160 ]

File ~/miniconda3/envs/fenicsx/lib/python3.13/site-packages/ffcx/ir/representation.py:396, in _compute_integral_ir(form_data, form_index, element_numbers, integral_names, finite_element_hashes, options, visualise)
    391 integrand_map: dict[QuadratureRule, ufl.core.expr.Expr] = {
    392     rule: integral.integrand() for rule, integral in sorted_integrals.items()
    393 }
    395 # Build more specific intermediate representation
--> 396 integral_ir = compute_integral_ir(
    397     itg_data.domain.ufl_cell(),
    398     itg_data.integral_type,
    399     expression_ir["entity_type"],
    400     integrand_map,
    401     expression_ir["tensor_shape"],
    402     options,
    403     visualise,
    404 )
    406 expression_ir.update(integral_ir)
    408 # Fetch name

File ~/miniconda3/envs/fenicsx/lib/python3.13/site-packages/ffcx/ir/integral.py:91, in compute_integral_ir(cell, integral_type, entity_type, integrands, argument_shape, p, visualise)
     88     if domain.topological_dimension() != cell.topological_dimension():
     89         is_mixed_dim = True
---> 91 mt_table_reference = build_optimized_tables(
     92     quadrature_rule,
     93     cell,
     94     integral_type,
     95     entity_type,
     96     initial_terminals.values(),
     97     ir["unique_tables"],
     98     use_sum_factorization=p["sum_factorization"],
     99     is_mixed_dim=is_mixed_dim,
    100     rtol=p["table_rtol"],
    101     atol=p["table_atol"],
    102 )
    104 # Fetch unique tables for this quadrature rule
    105 table_types = {v.name: v.ttype for v in mt_table_reference.values()}

File ~/miniconda3/envs/fenicsx/lib/python3.13/site-packages/ffcx/ir/elementtables.py:444, in build_optimized_tables(quadrature_rule, cell, integral_type, entity_type, modified_terminals, existing_tables, use_sum_factorization, is_mixed_dim, rtol, atol)
    442             t["array"] = np.vstack([td["array"] for td in new_table])
    443 else:
--> 444     t = get_ffcx_table_values(
    445         quadrature_rule.points,
    446         cell,
    447         integral_type,
    448         element,
    449         avg,
    450         entity_type,
    451         local_derivatives,
    452         flat_component,
    453         codim,
    454     )
    455 # Clean up table
    456 tbl = clamp_table_small_numbers(t["array"], rtol=rtol, atol=atol)

File ~/miniconda3/envs/fenicsx/lib/python3.13/site-packages/ffcx/ir/elementtables.py:147, in get_ffcx_table_values(points, cell, integral_type, element, avg, entity_type, derivative_counts, flat_component, codim)
    145 for entity in range(num_entities):
    146     if codim == 0:
--> 147         entity_points = map_integral_points(points, integral_type, cell, entity)
    148     elif codim == 1:
    149         entity_points = points

File ~/miniconda3/envs/fenicsx/lib/python3.13/site-packages/ffcx/ir/representationutils.py:118, in map_integral_points(points, integral_type, cell, entity)
    116     return np.asarray(points)
    117 elif entity_dim == tdim - 1:
--> 118     assert points.shape[1] == tdim - 1
    119     return np.asarray(map_facet_points(points, entity, cell.cellname()))
    120 elif entity_dim == 0:

AssertionError: 

I did some debug. Here lists all arguments in most recent call, which may help finding some clues.

  • In map_integral_points(points, integral_type, cell, entity)
    points = array([[0.33333333, 0.33333333]])
    integral_type = 'exterior_facet'
    cell = triangle
    entity = 0
    
  • In get_ffcx_table_values(...)
    points = array([[0.33333333, 0.33333333]])
    cell = triangle
    integral_type = 'exterior_facet'
    element = blocked element (Basix element (P, triangle, 1, gll_warped, unset, False, float64, []), (2,))
    avg = None
    entity_type = 'facet'
    derivative_counts = (1, 0)
    flat_component = 0
    codim = 0
    

Hello,

I think your integral is not legitimate because you’re attempting to integrate a function that is only defined at quadrature points (and therefore within the element interior) over a facet. FEniCSx needs to somehow understand the interpolation scheme in order to determine the function’s value at the boundary—for example, by using DG0 elements.

Your problem is quite different from the initial post, which integrated quadrature elements over the volume (where the elements are indeed well-defined) and CG1 elements on the facets (which are well-defined there due to linear interpolation).

2 Likes

Hi @BouteillerP,

Thank you for your reply. You are right, I apologize for oversimplifying the MWE—it seems I removed the very part that was causing the problem.

My actual goal is to calculate the traction on the boundary facets. This requires integrating an expression involving a Quadrature element over ds. Here is a snippet that better represents my issue:

n = ufl.FacetNormal(mesh)
traction = ufl.dot(sigma_xx(u, d) * n, n) * ds

# The program fails here with an assertion error
# --------------------------------------------
traction_form = form(traction)
# --------------------------------------------

My question is: Is it fundamentally possible to integrate a Quadrature element-based expression over a boundary measure (ds) in FEniCSx? The assertion failure suggests this might not be supported directly.

If that’s the case, what would be the recommended workflow for this type of calculation?

Thank you again for your help.

Evaluating the reaction effort using stress data at Gauss points is quite tricky. But Jeremy proposes in his tutorial:
https://bleyerj.github.io/comet-fenicsx/tips/computing_reactions/computing_reactions.html
a very elegant solution involving the work of internal forces - wouldn’t that be what you’re trying to do?

2 Likes

Wow, thank you for this fantastic suggestion! That is an incredibly elegant solution you’ve pointed me to.

The idea of using the principle of virtual work to compute reaction forces, rather than directly integrating stresses at quadrature points on the boundary, is a much cleaner and more theoretically sound approach. It neatly sidesteps the very issue of evaluating quadrature data on facets that I was struggling with.

Thank you for taking the time to share this valuable resource. This helps me immensely.