Dune-Fufem 2.11-git
Loading...
Searching...
No Matches
Poisson equation

Full code of this example This example on gitlab

Introduction

The following example shows how to solve a Poisson problem with Dirichlet boundary conditions using second order finite elements.

Setup and initialization

After including the required headers the main() function first creates a simple logger that allows to write log messages to std::cout. The logger will prefix log messages with the time passed since the program was started and since the last log message using the given format string. Afterwards MPI is initialized, passing the command line arguments.

54int main (int argc, char *argv[]) try
55{
56
57 auto log = Dune::Fufem::makeLogger(std::cout, "[% 8.3f] [% 8.3f] ");
58
59 Dune::MPIHelper::instance(argc, argv);
60
61 log("MPI initialized");
int main(int argc, char **argv)
auto makeLogger(std::ostream &stream, std::string format)
Create a simple logger callback.
Definition logger.hh:42
static DUNE_EXPORT MPIHelper & instance(int &argc, char **&argv)

Problem data

Next we parse the command line arguments. To this end we make use of Dune::ParameterTree which is a tree-of-strings data structure. Here parameters.get<T>("key", defaultValue) reads the value with the respective key from the ParameterTree and converts it to the type T if it exists, otherwise the defaultValue is used. By parsing the command line into the ParameterTree we can pass parameters using command line arguments of the form -refinements 4. The supported arguments allow to specify the number of grid refinement steps, the number of in parallel algorithms, and the tolerance, maximal iteration count and verbosity of the iterative solver.

62 auto parameters = Dune::ParameterTree();
63 Dune::ParameterTreeParser::readOptions(argc, argv, parameters);
64
65 auto refinements = parameters.get<std::size_t>("refinements", 4);
66 auto threadCount = parameters.get<std::size_t>("threads", std::thread::hardware_concurrency());
67 auto tol = parameters.get<double>("tol", 1e-4);
68 auto maxIter = parameters.get<std::size_t>("maxIter", 1000);
69 auto verbosity = parameters.get<std::size_t>("verbosity", 2);
70
71 log("Command line processed");
static void readOptions(int argc, char *argv[], ParameterTree &pt)
T hardware_concurrency(T... args)

The example solves the Poisson equation with Dirichlet boundary conditions. The problem data consisting of the right-hand-side function and the Dirichlet values are defined using lambda expressions.

72 auto rightHandSide = [] (const auto& x) { return 10;};
73 auto dirichletValues = [](const auto& x){ return std::sin(2*std::numbers::pi*x[0]); };
T sin(T... args)

Grid creation

Instead of creating a uniform grid using a factory or reading a grid from a file, the example manually creates a mixed grid of type UGGrid<2> using a GridFactory. The grid implementation UGGrid<2> supports unstructured grid in two dimensions. After inserting nine uniformly placed vertices on the unit square, the method inserts two quadrilateral elements in the lower half and four triangular elements in the upper half of the domain. Finally, the call to factory.createGrid() returns a std::unique_ptr referring to the newly creates grid. For simplicity the example also stores a reference to the grid. The coarse grid is then refined globally and copy of the leaf grid view is stored.

Note
A GridView is a view to a subset of the grid's elements, vertices, ... that should be stored by value and can be copied cheaply. Grids in dune are in general hierarchical and composed by elements on several levels. The discretization usually lives on the set of most refined elements that is denote the leaf GridView in dune.
74 using Grid = Dune::UGGrid<2>;
75
76 auto factory = Dune::GridFactory<Grid>();
77 for(unsigned int k : Dune::range(9))
78 factory.insertVertex({0.5*(k%3), 0.5*(k/3)});
79 factory.insertElement(Dune::GeometryTypes::cube(2), {0, 1, 3, 4});
80 factory.insertElement(Dune::GeometryTypes::cube(2), {1, 2, 4, 5});
81 factory.insertElement(Dune::GeometryTypes::simplex(2), {3, 4, 6});
82 factory.insertElement(Dune::GeometryTypes::simplex(2), {4, 7, 6});
83 factory.insertElement(Dune::GeometryTypes::simplex(2), {4, 5, 7});
84 factory.insertElement(Dune::GeometryTypes::simplex(2), {5, 8, 7});
85
86 auto gridPtr = factory.createGrid();
87 auto& grid = *gridPtr;
88
89 log("Grid created");
90
91 grid.globalRefine(refinements);
92
93 log(Dune::formatString("Grid refined %d times", refinements));
94
95 auto gridView = grid.leafGridView();
static std::string formatString(const std::string &s, const T &... args)
const Grid & grid() const
LeafGridView leafGridView() const
void globalRefine(int refCount)
T log(T... args)

Preparing thread-parallelism

Dune-fufem supports thread-parallel assembly based on a coloring of the grid view. To use this feature we need to compute such a coloring. The coloredElementRange() function from the dune-assembler module computes such a coloring and returns a partition of the grid view's elements according to the coloring. The ColoredRangeExecutor class then encapsulates the information for executing parallel algorithms based on this partition.

96 auto coloredRange = Dune::Assembler::Experimental::coloredElementRange(gridView);
97
98 log("Grid coloring computed");
99
100 auto executor = Dune::Assembler::Experimental::ColoredRangeExecutor(coloredRange, threadCount);
101
102 log(Dune::formatString("Using parallelism with %d threads", threadCount));

Create finite element basis

As a next step the program creates a global finite element basis on the GridView. The helper function lagrange<2>() declares that we want to use Lagrange finite elements of order two, while the makeBasis() function creates the basis of this type on the given GridView. Both functions are from the namespace Dune::Functions::BasisFactory which is included for simplicity.

103 using namespace Dune::Functions::BasisFactory;
104 auto basis = makeBasis(gridView, lagrange<2>());
105
106 log(Dune::formatString("Basis created with dimension %d", basis.dimension()));

Create matrix, vector, and constraints data structures

Since the basis' indices are plain integers we next define types for flat vectors and matrices of reals numbers and create a solution and a right-hand-side vector and a stiffness matrix.

107 using Vector = Dune::BlockVector<double>;
109
110 auto sol = Vector();
111 auto rhs = Vector();
112 auto stiffnessMatrix = Matrix();
Y & rhs()

To handle the Dirichlet boundary conditions we then create an object for storing affine constraints with respect to the basis and then fill the object by computing the actual constraints values on the boundary from the Dirichlet value function.

113 auto constraints = Dune::Fufem::makeAffineConstraints(basis);
114
115 Dune::Fufem::computeBoundaryConstraints(constraints, basis, dirichletValues);
116
117 log("Boundary constraints computed");
void computeBoundaryConstraints(AffineConstraints< BV, V, MI, C > &constraints, const Basis &basis, Function &&f, const IntersectionSet &intersectionSet)
Compute constraints for essential boundary conditions.
Definition boundaryconstraints.hh:51
auto makeAffineConstraints(const Basis &basis)
Create and initialize an AffineConstraints object for a basis.
Definition affineconstraints.hh:825

Definition of the variational problem

Inspired by the UFL language of the Fenics project project, dune-fufem allows to define local assemblers for linear and bilinear forms using simple expressions from the Dune::Fufem::Forms namespace. After including the namespace, we first obtain expressions for trial and test functions associated to the basis. The only difference between trialFunction() and testFunction() is that the resulting objects later refer to column and row indices, respectively. In order to use the right-hand-side lambda functions within a form expression we first wrap it into a GridView-functions and then declare this as coefficient function, i.e. a function not depending on test and trial functions. Now we can define the bilinear and linear form in a straight forward way. The defined bilinear form and linear form objects satisfy the interfaces for local matrix and vector assemblers to be passed to a global assembler.

118 using namespace Dune::Fufem::Forms;
119
120 auto u = trialFunction(basis);
121 auto v = testFunction(basis);
122 auto f = Coefficient(Dune::Functions::makeGridViewFunction(rightHandSide, gridView));
123
124 auto a = integrate(dot(grad(u), grad(v)));
125 auto l = integrate(f*v);
field_type dot(const type &newv) const
std::decay_t< F > makeGridViewFunction(F &&f, const GridView &gridView)
Definition baseclass.hh:22
Coefficient function.
Definition coefficient.hh:42

Global assembly of the problem

For global assembly we use the dune-assembler module. This module allows to use different linear algebra frameworks through a unified matrix- and vector-backend interface. To use the matrix- and vector-classes from dune-istl, we wrap them into corresponding ISTL-backends.

126 auto matrixBackend = Dune::Assembler::ISTLMatrixBackend(stiffnessMatrix);
127 auto rhsBackend = Dune::Assembler::ISTLVectorBackend(rhs);

Next we create a global assembler object using the same basis for the test and trial space. In order to use thread-parallel assembly, we additionally pass the parallel executor which encapsulates the grid coloring and requested thread count to the assembler.

128 auto assembler = Dune::Assembler::Assembler(basis, basis, executor);
129
130 log("Assembler set up");

As a first step we need to assemble the matrix pattern. To this end we obtain a patternBuilder object from the matrix backend and initialize its size according to the test and trial basis. Then we use the global assemblers assembleMatrixPattern() method to assemble the pattern. While local matrices are often fully populated, this is not necessarily the case. Hence the local assembler also defines a local pattern and thus has to be passed when assembling the pattern, too.

131 auto patternBuilder = matrixBackend.patternBuilder();
132
133 patternBuilder.resize(basis, basis);
134
135 assembler.assembleMatrixPattern(a, patternBuilder);
136
137 log("Matrix pattern assembled");
Note
There are cases where the constraints object requires to enlarge the computed pattern. This is especially the case for constraints enforcing \(H^1\)-conforming finite element spaces on grids with hanging nodes. In the present example this is not the case. Hence we can skip this step here.

Now we are ready to assemble the problem. To this end we let the patternBuilder initialize the matrix size and pattern and then call the actual assembly method for the matrix.

138 patternBuilder.setupMatrix();
139
140 log("Matrix set up");
141
142 assembler.assembleMatrixEntries(a, matrixBackend);
143
144 log("Matrix assembled");

Similarly we assemble the right hand side by first resizing the vector according to the test basis and initializing it with zero and then calling the corresponding assembly method.

145 rhsBackend.resize(basis);
146 rhsBackend.setZero();
147
148 assembler.assembleVectorEntries(l, rhsBackend);
149
150 log("RHS assembled");

Once the system is assembled, we can constrain it to the affine subspace satisfying the constraints that we defined earlier.

151 constraints.constrainLinearSystem(stiffnessMatrix, rhs);
152
153 log("Boundary condition incorporated into system");

Algebraic solution

Before solving the linear system, we have to initialize the solution vector. While we could directly use the corresponding resize method and assignment operator, we here demonstrate how the ISTLVectorBackend from dune-assembler can be used, since this approach would also work for basis using multi-indices and corresponding nested vector containers.

Note
There is also a utility Dune::Functions::istlVectorBackend() provided by the dune-functions module. The Dune::Assemble::ISTLVectorBackend extends the latter by a few methods required for assembly. Here we could have used the version from the dune-functions module as well with a slightly different syntax.
154 Dune::Assembler::ISTLVectorBackend(sol).resize(basis);
155 Dune::Assembler::ISTLVectorBackend(sol).setZero();

To use the matrix in the solvers from the dune-istl module we first wrap it into a suitable operator interface. Then we create an AMG preconditioner and pass the operator and preconditioner to a CG solver. The apply() method of the latter solves the linear system up to the given tolerance and writes some solver statistics to the respective object that needs to be passed additionally.

156 auto matrixOperator = Dune::MatrixAdapter<Matrix,Vector,Vector>(stiffnessMatrix);
157
158 auto preconditioner = makeAMGPreconditioner<Vector>(stiffnessMatrix);
159
160 log("Preconditioner created");
161
162 auto solver = Dune::CGSolver<Vector>(
163 matrixOperator,
164 preconditioner,
165 tol,
166 maxIter,
167 verbosity
168 );
169
170 log("Solver created");
171
172 auto solverStatistics = Dune::InverseOperatorResult();
173
174 solver.apply(sol, rhs, solverStatistics);
175
176 log("Linear system solved");

Write solution to VTK

Finally we can write the solution to a VTK file understood by the ParaView visualization program. To this end we need to create a finite element function by combining the basis with the coefficient vector. Here this is obtained using the expression u|sol which binds the linear operator u (i.e. the identity on the finite element space) to the coefficient vector sol. We create a writer object capable of writing unstructured grids and tell it to write polynomial data of order 2 and pass the function to the writer, specifying the name and data type of the resulting field in the output.

177 auto vtkWriter = Dune::Vtk::UnstructuredGridWriter(Dune::Vtk::LagrangeDataCollector(basis.gridView(), 2));
178 vtkWriter.addPointData(u|sol, Dune::VTK::FieldInfo("sol", Dune::VTK::FieldInfo::Type::scalar, 1));
179 vtkWriter.write("poisson-pq2");
180
181 log("Solution written to vtk file");

Error handling

In case an exception is thrown within main() it is followed by a catch block that will print the error message and exit.

182}
183catch (Dune::Exception& e)
184{
185 std::cout << e.what() << std::endl;
186 return 1;
187}
const char * what() const noexcept override
T endl(T... args)