deal.II version GIT relicensing-7040-g409d9ea789 2026-11-06 04:40:01+00:00
\(\newcommand{\dealvcentcolon}{\mathrel{\mathop{:}}}\) \(\newcommand{\dealcoloneq}{\dealvcentcolon\mathrel{\mkern-1.2mu}=}\) \(\newcommand{\jump}[1]{\left[\!\left[ #1 \right]\!\right]}\) \(\newcommand{\average}[1]{\left\{\!\left\{ #1 \right\}\!\right\}}\)
Loading...
Searching...
No Matches
Assembly of finite element operators
Collaboration diagram for Assembly of finite element operators:

Finite element operators are built from contributions on cells and, for some formulations, on faces. deal.II offers several ways to organize this computation. The choice concerns both the traversal of the mesh and whether the global matrix is stored explicitly.

Explicit cell loops

In step-3, an explicit loop visits each active cell. FEValues evaluates shape functions and quadrature data, nested loops compute a local matrix and right hand side, and their entries are added to the global objects. This exposes all parts of assembly and is a useful starting point for a new formulation. Applications with hanging nodes or other constraints can use AffineConstraints::distribute_local_to_global() to transfer local contributions, as shown in step-6.

For example, the cell loop for a scalar Laplace matrix looks as follows. Here fe is the finite element, constraints is closed, and system_matrix has the required size and sparsity pattern. FEValues requests gradients and quadrature weights; the local objects are reused between cells.

FEValues<dim> fe_values(fe, QGauss<dim>(fe.degree + 1),
const unsigned int n = fe.n_dofs_per_cell();
FullMatrix<double> cell_matrix(n, n);
std::vector<types::global_dof_index> local_dof_indices(n);
for (const auto &cell : dof_handler.active_cell_iterators())
{
fe_values.reinit(cell);
cell_matrix = 0;
for (const unsigned int q : fe_values.quadrature_point_indices())
for (unsigned int i = 0; i < n; ++i)
for (unsigned int j = 0; j < n; ++j)
cell_matrix(i, j) += fe_values.shape_grad(i, q) *
fe_values.shape_grad(j, q) * fe_values.JxW(q);
cell->get_dof_indices(local_dof_indices);
constraints.distribute_local_to_global(cell_matrix, local_dof_indices,
system_matrix);
}
*  *  for(const auto &cell :triangulation.active_cell_iterators())
@ update_JxW_values
Transformed quadrature weights.
@ update_gradients
Shape function gradients.

WorkStream

WorkStream::run() separates the computation of local contributions from their insertion into global objects. A worker uses scratch data to compute each cell's contribution and stores the result in copy data. A copier then inserts that result into the global matrix and right hand side. This separation allows local work to run on multiple threads. For the iterator-range overload, insertion into shared objects is serialized. The colored overload can also run copiers concurrently: its coloring must ensure that copiers within one color do not write to the same memory. The local finite element calculations can remain the same as in an explicit cell loop. See step-9 and the shared-memory parallelism documentation for examples and details of the WorkStream model.

The iterator-range overload replaces the explicit cell loop by a worker and a copier. In this sketch, ScratchData owns an FEValues object and CopyData owns the local matrix and DoF indices. The application function assemble_cell resets and fills the copy data using the local calculation above; it only writes to its scratch and copy arguments. Both data types must satisfy WorkStream's copying requirements (see step-9).

using Iterator = typename DoFHandler<dim>::active_cell_iterator;
const auto cell_worker = [&](const Iterator &cell, ScratchData &scratch,
CopyData &copy) {
assemble_cell(cell, scratch, copy);
};
const auto copier = [&](const CopyData &copy) {
constraints.distribute_local_to_global(copy.cell_matrix,
copy.local_dof_indices, system_matrix);
};
WorkStream::run(dof_handler.begin_active(), dof_handler.end(),
cell_worker, copier, scratch_data, copy_data);
typename ActiveSelector::active_cell_iterator active_cell_iterator
void run(const std::vector< std::vector< Iterator > > &colored_iterators, Worker worker, Copier copier, const ScratchData &sample_scratch_data, const CopyData &sample_copy_data, const unsigned int queue_length=2 *MultithreadInfo::n_threads(), const unsigned int chunk_size=8)

MeshWorker

In the scheme outlined above, one has a loop over all cells (either explicitly, or by letting WorkStream::run() iterate over them) and on each cell one assembles the necessary terms that pertain to this cell. Sometimes, as in the example above, these terms really only contain a contribution from each cell. But sometimes, one also has contributions from boundary faces (say, when dealing with Neumann or Robin boundary conditions) or interior faces (say, when dealing with discontinuous Galerkin schemes). In these cases, computing all of these contributions in one place might not be the best software design – it might be better to split different contributions into separate functions.

MeshWorker::mesh_loop() builds on WorkStream and provides separate callbacks for cells, boundary faces, and interior faces. Flags select which contributions to compute and how interior faces are visited. This is useful for formulations with face terms, such as discontinuous Galerkin methods: the application supplies the local integrals and the loop handles traversal. Scratch and copy data still separate local computation from insertion into global objects. See step-12 and the The MeshWorker interface module.

The traversal can include all three kinds of contributions in one call:

MeshWorker::mesh_loop(dof_handler.begin_active(), dof_handler.end(),
cell_worker, copy_cell_and_face_data,
scratch_data, copy_data,
boundary_worker, face_worker);
void mesh_loop(const CellIteratorType &begin, const CellIteratorType &end, const CellWorkerFunctionType &cell_worker, const CopierType &copier, const ScratchData &sample_scratch_data, const CopyData &sample_copy_data, const AssembleFlags flags=assemble_own_cells, const BoundaryWorkerFunctionType &boundary_worker=BoundaryWorkerFunctionType(), const FaceWorkerFunctionType &face_worker=FaceWorkerFunctionType(), const unsigned int queue_length=2 *MultithreadInfo::n_threads(), const unsigned int chunk_size=8)
Definition mesh_loop.h:279
@ assemble_boundary_faces
@ assemble_own_interior_faces_once

Here the application has also defined boundary_worker and face_worker with the signatures documented by MeshWorker::mesh_loop(). They append face contributions to CopyData; copy_cell_and_face_data inserts both cell and face contributions into global objects. This requires additional face storage in the scratch and copy data, as illustrated in step-12.

Matrix-free operator application

When an iterative solver only needs the action of an operator on a vector, it is also possible to avoid assembling and storing the global matrix. MatrixFree organizes cell and face loops and the data needed for them; FEEvaluation and FEFaceEvaluation evaluate finite element functions and integrate local contributions. These contributions are accumulated into a destination vector in each operator application. For tensor-product elements, sum factorization can reduce the cost of local evaluation, particularly at higher polynomial degrees. See step-37 and the Matrix-free infrastructure module.

This approach changes the operator representation, not just the assembly loop. It saves matrix storage but requires an operator implementation and a compatible preconditioner. An assembled matrix remains useful when a solver or preconditioner requires its entries. Applications can also combine matrix-free operator application with assembled auxiliary matrices. For the same constant-coefficient Laplace operator, the following member functions of an application class LaplaceOperator replace matrix assembly and multiplication. The class has an initialized MatrixFree<dim> member data, a compile-time polynomial degree fe_degree, and the alias VectorType for LinearAlgebra::distributed::Vector<double>. Its vectors use the layout supplied by MatrixFree::initialize_dof_vector().

void local_apply(const MatrixFree<dim> &mf, VectorType &dst,
const VectorType &src,
const std::pair<unsigned int, unsigned int> &range) const
{
for (unsigned int cell = range.first; cell < range.second; ++cell)
{
phi.reinit(cell);
phi.gather_evaluate(src, EvaluationFlags::gradients);
for (const unsigned int q : phi.quadrature_point_indices())
phi.submit_gradient(phi.get_gradient(q), q);
phi.integrate_scatter(EvaluationFlags::gradients, dst);
}
}
void vmult(VectorType &dst, const VectorType &src) const
{
data.cell_loop(&LaplaceOperator::local_apply, this, dst, src, true);
}
std::vector< index_type > data
Definition mpi.cc:734

The loop runs over cell batches. The final true requests zeroing dst before accumulating contributions; cell_loop() also handles the required communication for these distributed vectors. Boundary conditions and constraint treatment must match the operator used by the solver, as in step-37.