Dune-Fufem 2.11-git
Loading...
Searching...
No Matches
Bulk-surface coupling

Full code of this example This example on gitlab

Introduction

This example shows how to couple bulk and surface PDEs by presenting the coupling of elliptic problems in two bulk subdomains to an elliptic problem on the surface given by the interface between the subdomains.

Mathematical background

For a domain \(\Omega \subset \mathbb{R}^d\) let \(\Omega_A\) be an open subset with \(\overline{\Omega_A} \subset \Omega\) and set \(\Omega_B = \Omega \setminus \overline{\Omega_A}\). In the following we assume that the interface \(\Gamma = \overline{\Omega_A} \cap \overline{\Omega_B}\) is sufficiently smooth and consider bulk concentrations \(u_A : \Omega_A \to \mathbb{R}\) and \(u_B : \Omega_B \to \mathbb{R}\) and a surface concentration \(u_\Gamma : \Gamma \to \mathbb{R}\). In the bulk we impose the Poisson-problems

\[ \begin{align*} -\Delta u_A &= f_A && \text{ in } \Omega_A, \\ -\Delta u_B &= f_B && \text{ in } \Omega_B. \end{align*} \]

We assume that mass transfer to the interface is proportional to the concentration difference

\[ \begin{align*} -\frac{\partial u_A}{\partial n_A} &= \alpha (u_A - u_\Gamma) && \text{ on } \Gamma,\\ -\frac{\partial u_B}{\partial n_B} &= \alpha (u_B - u_\Gamma) && \text{ on } \Gamma, \end{align*} \]

where \(\alpha>0\) is a constant and \(n_A\) and \( n_B\) are the unit outward normals to \(\partial\Omega_A\) and \(\partial\Omega_B\), respectively. On the surface \(\Gamma\) we consider tangential diffusion for \(u_\Gamma\) with the outflow of the two bulk domains as source terms

\[ \begin{align*} -\delta\Delta_\Gamma u_\Gamma + \frac{\partial u_A}{\partial n_A} + \frac{\partial u_B}{\partial n_B} &= f_\Gamma && \text{ on } \Gamma. \end{align*} \]

On the outer boundary \(\partial \Omega = \partial \Omega_B\subset \Gamma\) we impose a Dirichlet boundary condition \(u_B=0\) as encoded in the space \( H_{\partial \Omega}^1(\Omega_B) := \{v \in H^1(\Omega_B)\,|\, v|_{\partial \Omega}=0\} \). Testing the equations with, \(v_A, v_B\), and \(v_\Gamma\), integrating over the respective domains, using partial integration, and inserting the boundary conditions we arrive at the following variational problem:

Find \( (u_A, u_B, u_\Gamma) \in \mathcal{H} := H^1(\Omega_A) \times H_{\partial \Omega}^1(\Omega_B) \times H^1(\Gamma)\) such that

\[ \small \begin{align*} \int_{\Omega_A}\nabla u_A \cdot \nabla v_A dx + \alpha\int_\Gamma (u_A - u_\Gamma) v_A d\sigma &= \int_{\Omega_A} f_A v_A dx && \forall v_A \in H_{\partial \Omega}^1(\Omega_A),\\ \int_{\Omega_B}\nabla u_B \cdot \nabla v_B dx + \alpha\int_\Gamma (u_B - u_\Gamma) v_B d\sigma &= \int_{\Omega_B} f_B v_B dx && \forall v_B \in H^1(\Omega_B),\\ \int_{\Gamma}\delta \nabla u_\Gamma \cdot \nabla v_\Gamma - \alpha(u_A - u_\Gamma) v_\Gamma - \alpha(u_B - u_\Gamma) v_\Gamma d\sigma &= \int_{\Gamma} f_\Gamma v_\Gamma d\sigma && \forall v_\Gamma \in H^1(\Gamma). \end{align*} \]

With the bilinear form

\[ \begin{multline} a(u,v) = \int_{\Omega_A}\nabla u_A \cdot \nabla v_A dx + \int_{\Omega_B}\nabla u_B \cdot \nabla v_B dx + \delta\int_{\Gamma} \nabla u_\Gamma \cdot \nabla v_\Gamma d\sigma \\ + \alpha\int_\Gamma (u_A - u_\Gamma) (v_A - v_\Gamma) + (u_B - u_\Gamma) (v_B - v_\Gamma) d\sigma \end{multline} \]

and linear form

\[ \begin{align*} \ell(v) &= \int_{\Omega_A}f_A v_B dx + \int_{\Omega_B}f_B v_B dx + \int_{\Gamma}f_\Gamma v_\Gamma d\sigma \end{align*} \]

for tuples \(u=(u_A,u_B,u_\Gamma)\) and \(v=(v_A,v_B,v_\Gamma)\) this can be compactly written as

\[ \begin{align*} u &\in \mathcal{H}: & a(u,v) &= \ell(v) && \forall v \in \mathcal{H}. \end{align*} \]

It is clear that the bilinear form is symmetric and continuous and that it satisfies

\[ a(v,v) \geq \|\nabla v_A\|_{L^2(\Omega_A)}^2 + \|\nabla v_B\|_{L^2(\Omega_B)}^2 + \delta \|\nabla_\Gamma v_\Gamma\|_{L^2(\Gamma)}^2. \]

Hence, by the classic Poincaré-inequality it is coercive on the orthogonal complement of the (component-wise) constant functions. On the other hand all functions in the kernel of \(a(\cdot,\cdot)\) must satisfy \(v_A=v_\Gamma=v_B\) on \(\Gamma\) and \(v_B=0\) on \(\partial \Omega\). Thus the kernel is trivial and the bilinear form is coercive (see, e.g., [9], Proposition 1) and the Lax-Milgram theorem provides well-posedness.

The discretization error of the finite element discretization use in the example has been investigated (for a slightly simpler problem with a single subdomain) in [6].

Setup and initialization

After including the required headers and defining a utility function that will be explained below, the main() function first creates a simple logger that allows to write log messages to std::cout (see the Poisson example for details).

106int main (int argc, char *argv[]) try
107{
108
109 auto log = Dune::Fufem::makeLogger(std::cout, "[% 8.3f] [% 8.3f] ");
110
111 Dune::MPIHelper::instance(argc, argv);
112
113 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 into a Dune::ParameterTree and extract the parameters for the mesh file, the number of grid refinements, the tolerance, maximal iteration count and verbosity of the iterative solver, and the problem parameters \(\alpha\) and \(\delta\). Furthermore we define the functions for the right-hand side of the equations in the bulk domains (A) and (B) and on the interface (I), and the Dirichlet values on the boundary.

114 auto parameters = Dune::ParameterTree();
115 Dune::ParameterTreeParser::readOptions(argc, argv, parameters);
116
117 auto meshFile = parameters.get<std::string>("meshFile", "bulk-surface.msh");
118 auto refinements = parameters.get<std::size_t>("refinements", 0);
119 auto tol = parameters.get<double>("tol", 1e-4);
120 auto maxIter = parameters.get<std::size_t>("maxIter", 1000);
121 auto verbosity = parameters.get<std::size_t>("verbosity", 2);
122 auto alpha = parameters.get<double>("alpha", 10.0);
123 auto delta = parameters.get<double>("delta", 2.0);
124
125 log("Command line processed");
126
127 auto rhs_A = [] (const auto& x) {
128 if (x[0]>1)
129 return 10;
130 else
131 return 0;
132 };
133 auto rhs_B = [] (const auto& x) { return 0; };
134 auto rhs_I = [] (const auto& x) { return 0; };
135
136 auto dirichletValues = [] (const auto& x) { return 0; };
double alpha() const
static void readOptions(int argc, char *argv[], ParameterTree &pt)

Grid creation

The grid is created by reading a Gmsh file into a GridFactory and extracting the element data that is used to define the two subdomains. Afterward the grid is refined and the element data is updated to follow the refinement. These steps as well as the handling of subdomains and interfaces and the restriction of finite element bases to (bulk) subdomains are described in more detail in the Stokes-Darcy example.

137 using Grid = Dune::UGGrid<2>;
138
139 auto factory = Dune::GridFactory<Grid>();
140 auto reader = Dune::GmshReader<Grid>(meshFile, factory);
141 auto gridPtr = factory.createGrid();
142 auto& grid = *gridPtr;
143 auto elementData = Dune::Fufem::ElementData(reader, factory, grid);
144
145 log("Grid created");
146
147 grid.globalRefine(refinements);
148 auto gridView = grid.leafGridView();
149 elementData.update(gridView);
150
151 log("Grid refined");
LeafGridView leafGridView() const
void globalRefine(int refCount)
Class for storing data associated to elements.
Definition elementdata.hh:47

Setup of subdomains and boundary patches

The element data is used to setup two SubDomain objects representing the respective subdomains \(\Omega_A\) and \(\Omega_B\). For the surface equation we create an interface object representing the interface \(\Gamma\) between the subdomains. Notice that the interface is oriented from subDomain_A to subDomain_B which will be important later on.

154
155 auto subDomain_A = SubDomain(gridView);
156 for(auto&& element : elements(gridView))
157 if (elementData(element) == 1)
158 subDomain_A.insertElement(element);
159
160 log(Dune::formatString("SubDomain A contains %d elements", subDomain_A.indexSet().size(0)));
161
162 auto subDomain_B = SubDomain(gridView);
163 for(auto&& element : elements(gridView))
164 if (elementData(element) == 2)
165 subDomain_B.insertElement(element);
166
167 log(Dune::formatString("SubDomain B contains %d elements", subDomain_B.indexSet().size(0)));
168
169 auto interface = SubDomainInterface(subDomain_A, subDomain_B);
static std::string formatString(const std::string &s, const T &... args)

Selection of finite element basis

Next we create the global finite element basis using first order finite elements in both bulk subdomains and on the interface.

For the interface we want to use the so called trace finite element approach which uses the trace of a bulk finite element space. To this end we create a basis that lives on an bulk domain, but will only assemble terms on the interface later on. This would make the assembled systems singular. Hence we will constrain the coefficients for all degrees of freedom that do not live on the interface to zero, similar to Dirichlet constraints. To reduce the number of DOFs to constrain, we start with a bulk basis defined on subDomain_A which contains all the DOFs we need for the trace space.

Note
Ideally one would be able to write restrict(lagrange<1>(), interface) to restricting the bulk basis to the interface and only get the required DOFs. However, the dune-functions module does not support the restriction to lower dimensional subsets yet. Hence we have to use the 'trick' described above.
170 using namespace Dune::Functions::BasisFactory;
172
173 auto basis = makeBasis(gridView,
174 composite(
175 restrict(lagrange<1>(), subDomain_A),
176 restrict(lagrange<1>(), subDomain_B),
177 restrict(lagrange<1>(), subDomain_A),
178 flatLexicographic()));
179
180 using namespace Dune::Indices;
181 auto subDomainBasis_A = Dune::Functions::subspaceBasis(basis, _0);
182 auto subDomainBasis_B = Dune::Functions::subspaceBasis(basis, _1);
183 auto interfaceBasis = Dune::Functions::subspaceBasis(basis, _2);
184
185 log(Dune::formatString("Basis created with dimension %d", basis.dimension()));
auto subspaceBasis(const RootBasis &rootBasis, const TypeTree::TreePath< PrefixTreeIndices... > &prefixPath)

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.

186 using Vector = Dune::BlockVector<double>;
188
189 auto sol = Vector();
190 auto rhs = Vector();
191 auto stiffnessMatrix = Matrix();
Y & rhs()

To handle the Dirichlet and trace basis constraints we create a constraints object and compute the respective constraints.

192 auto constraints = Dune::Fufem::makeAffineConstraints(basis);
193
194 computeBoundaryConstraints(constraints, subDomainBasis_B, dirichletValues);
195
196 log("Boundary constraints computed");
197
198 computeTraceConstraints(constraints, interfaceBasis, interface);
199
200 log("Trace constraints computed");
auto makeAffineConstraints(const Basis &basis)
Create and initialize an AffineConstraints object for a basis.
Definition affineconstraints.hh:825

While the `computeBoundaryConstraints()` function is provided by dune-fufem, computeTraceConstraints() is defined in the present example above the main() function as follows:

The function first marks all DOFs associated to intersections from the interface using Dune::Fufem::markIntersectionDofs. The vector for storing the respective flags is obtained by copying the one from the constraints. To access the vector with multi-indices of the basis (that may in general have multiple digits) it is wrapped into VectorBacked. Next the functions loops over all degrees of freedom of the respective (bulk) basis and marks the non-interface DOFs as constrained and sets the respective constraint value to zero.

template<class Constraints, class Basis, class IntersectionSet>
void computeTraceConstraints(Constraints& constraints, const Basis& basis, const IntersectionSet& intersectionSet)
{
auto isIntersectionDOF = constraints.isConstrained();
auto isIntersectionDOFBackend = istlVectorBackend(isIntersectionDOF);
Dune::Fufem::markIntersectionDofs(intersectionSet, basis, isIntersectionDOFBackend);
auto&& isConstrainedBackend = istlVectorBackend(constraints.isConstrained());
auto&& constraintValuesBackend = istlVectorBackend(constraints.constantTerm());
auto localView = basis.localView();
const auto& gridView = basis.gridView();
for(auto&& element : elements(gridView))
{
localView.bind(element);
for(auto i : Dune::range(localView.tree().size()))
{
auto i_local = localView.tree().localIndex(i);
auto i_global = localView.index(i_local);
if (not isIntersectionDOFBackend[i_global])
{
isConstrainedBackend[i_global] = true;
constraintValuesBackend[i_global] = 0;
}
}
}
constraints.resolveDependencies();
}
auto istlVectorBackend(Vector &v)
int size() const
void markIntersectionDofs(const IntersectionSet &intersectionSet, const Basis &basis, Vector &vector)
For a given basis and intersection set, determine all degrees of freedom on the patch.
Definition boundarydofs.hh:45

Global assembly of the problem

To assemble the matrix and right hand side vector we use the Dune::Fufem::Forms framework to define the respective variational forms. For an explanation how to define such forms within this framework and how to assemble the associated matrices and vectors using the dune-assembler module refer to the Poisson example. Advanced features like the computation of subdomain and interface terms are explained in the Stokes-Darcy example.

Given the explanations provided in the other examples, the definition of the variational forms used here is essentially straight-forward. We only note that for the interface term we can use the expressions u_A, v_A, u_I, and v_I directly, while u_B and v_B have to be wrapped in outside(...). This is a consequence of the fact that the interface is oriented from subDomain_A (called inside) to subDomain_B (called outside). All terms that should be evaluated on the inside of the interface can be used directly, while one has to explicitly declares if terms should be evaluated on the outside.

201 using namespace Dune::Fufem::Forms;
202
203 auto normal = faceNormal(gridView);
204 auto P = compose([&](auto n) {
205 constexpr auto dim = decltype(n)::size();
207 for(auto i : Dune::range(dim))
208 for(auto j : Dune::range(dim))
209 T[i][j] = (i==j) - n[i]*n[j];
210 return T;
211 }, normal);
212
213 auto u_A = trialFunction(subDomainBasis_A);
214 auto v_A = testFunction(subDomainBasis_A);
215
216 auto u_B = trialFunction(subDomainBasis_B);
217 auto v_B = testFunction(subDomainBasis_B);
218
219 auto u_I = trialFunction(interfaceBasis);
220 auto v_I = testFunction(interfaceBasis);
221
222 auto f_A = Coefficient(Dune::Functions::makeGridViewFunction(rhs_A, gridView));
223 auto f_B = Coefficient(Dune::Functions::makeGridViewFunction(rhs_B, gridView));
224 auto f_I = Coefficient(Dune::Functions::makeGridViewFunction(rhs_I, gridView));
225
226 auto a_A = integrate(dot(grad(u_A), grad(v_A)), subDomain_A);
227 auto a_B = integrate(dot(grad(u_B), grad(v_B)), subDomain_B);
228 auto a_I = integrate(delta*dot(P * grad(u_I), grad(v_I)), interface);
229
230 auto a_coupling = integrate(alpha*(u_A - u_I)*(v_A - v_I) + alpha*(outside(u_B) - u_I)*(outside(v_B) - v_I), interface);
231
232 auto a = a_A + a_B + a_I + a_coupling;
233
234 auto l_A = integrate(f_A*v_A, subDomain_A);
235 auto l_B = integrate(f_B*v_B, subDomain_B);
236 auto l_I = integrate(f_I*v_I, interface);
237
238 auto l = l_A + l_B + l_I;
field_type dot(const type &newv) const
std::decay_t< F > makeGridViewFunction(F &&f, const GridView &gridView)
size_type dim() const
Definition baseclass.hh:22
Coefficient function.
Definition coefficient.hh:42

The bilinear and linear form defined above can now be passed to the methods of the global Assembler class provided by the dune-assembler module to assemble the associated matrix and vector.

239 auto matrixBackend = Dune::Assembler::ISTLMatrixBackend(stiffnessMatrix);
240 auto rhsBackend = Dune::Assembler::ISTLVectorBackend(rhs);
241 auto assembler = Dune::Assembler::Assembler(basis, basis);
242
243 log("Assembler set up");
244
245 auto patternBuilder = matrixBackend.patternBuilder();
246 patternBuilder.resize(basis, basis);
247
248 assembler.assembleMatrixPattern(a, patternBuilder);
249
250 log("Matrix pattern assembled");
251
252 patternBuilder.setupMatrix();
253
254 log("Matrix set up");
255
256 assembler.assembleMatrixEntries(a, matrixBackend);
257
258 log("Matrix assembled");
259
260 rhsBackend.resize(basis);
261 rhsBackend.setZero();
262 assembler.assembleVectorEntries(l, rhsBackend);
263
264 log("RHS assembled");

Once the system is assembled, we can constrain it to the affine subspace satisfying the constraints that we defined earlier. This includes the Dirichlet boundary constraints and the constraints to the trace for the interface basis.

265 constraints.constrainLinearSystem(stiffnessMatrix, rhs);
266
267 log("Boundary condition incorporated into system");

Algebraic solution

Since the system is symmetric positive definite we use a preconditioned CG-method with an AMG preconditioner provided by the dune-istl module for the iterative solution.

268 Dune::Assembler::ISTLVectorBackend(sol).resize(basis);
269 Dune::Assembler::ISTLVectorBackend(sol).setZero();
270
271 auto matrixOperator = Dune::MatrixAdapter<Matrix,Vector,Vector>(stiffnessMatrix);
272
273 auto preconditioner = makeAMGPreconditioner<Vector>(stiffnessMatrix);
274
275 log("Preconditioner created");
276
277 auto solver = Dune::CGSolver<Vector>(
278 matrixOperator,
279 preconditioner,
280 tol,
281 maxIter,
282 verbosity
283 );
284
285 log("Solver created");
286
287 auto solverStatistics = Dune::InverseOperatorResult();
288
289 solver.apply(sol, rhs, solverStatistics);
290
291 log("Linear system solved");
292
293 constraints.interpolate(sol);

Writing the solution to VTK

Finally the solution is written to a VTK file understood by the ParaView visualization program. We write the three components of the bulk and interface solutions independently and for convenience additionally the sum of both bulk solutions. All the written functions are obtained by binding the respective Dune::Fufem::Forms expressions to the solution vector using the ...|sol operator. Since the solutions are discontinuous across the interface, we use the DiscontinuousLagrangeDataCollector which writes the solution independently for each grid element.

294 auto vtkWriter = Dune::Vtk::UnstructuredGridWriter(Dune::Vtk::DiscontinuousLagrangeDataCollector(basis.gridView(), 1));
295 vtkWriter.addPointData(u_A|sol, Dune::VTK::FieldInfo("sol_A", Dune::VTK::FieldInfo::Type::scalar, 1));
296 vtkWriter.addPointData(u_B|sol, Dune::VTK::FieldInfo("sol_B", Dune::VTK::FieldInfo::Type::scalar, 1));
297 vtkWriter.addPointData((u_A + u_B)|sol, Dune::VTK::FieldInfo("sol_bulk", Dune::VTK::FieldInfo::Type::scalar, 1));
298 vtkWriter.addPointData(u_I|sol, Dune::VTK::FieldInfo("sol_I", Dune::VTK::FieldInfo::Type::scalar, 1));
299 vtkWriter.write("bulk-surface");
300
301 log("Solution written to vtk file");

Error handling

In case an exception is thrown within main(), the error message is printed and the program is terminated.

302}
303catch (Dune::Exception& e)
304{
305 std::cout << e.what() << std::endl;
306 return 1;
307}
const char * what() const noexcept override
T endl(T... args)