This program was contributed by Chih-Che Chueh (University of Victoria) and Wolfgang Bangerth. Results from this program are used and discussed in the following publications (in particular in the second one):
Chih-Che Chueh, Marc Secanell, Wolfgang Bangerth, Ned Djilali. Multi-level adaptive simulation of transient two-phase flow in heterogeneous porous media. Computers & Fluids, 39:1585-1596, 2010 (see [Chueh2010]).
Chih-Che Chueh, Ned Djilali, Wolfgang Bangerth. An h-adaptive operator splitting method for two-phase flow in 3D heterogeneous porous media. SIAM Journal on Scientific Computing, 35:B149-B175, 2013 (see [Chueh2013]).
The implementation discussed here uses and extends parts of the step-21 and step-31 tutorial programs.
The work of the Chih-Che Chueh was funded through the Canada Research Chairs Program and the MITACS Network of Centres of Excellence. Parts of the work by Wolfgang Bangerth were funded through Award No. KUS-C1-016-04, made by the King Abdullah University of Science and Technology, and through an Alfred P. Sloan Research Fellowship. This material is also in parts based upon work supported by the National Science Foundation under Award No. EAR-0426271 and The California Institute of Technology; and in a continuation by the National Science Foundation under Award No. EAR-0949446 and The University of California – Davis. Any opinions, findings, and conclusions or recommendations expressed in this publication are those of the author and do not necessarily reflect the views of the National Science Foundation, The California Institute of Technology, or of The University of California – Davis.
Introduction
The simulation of multiphase flow in porous media is a ubiquitous problem, and we have previously addressed it already in some form in step-20 and step-21. However, as was easy to see there, it faces two major difficulties: numerical accuracy and efficiency. The first is easy to see in the stationary solver step-20: using lowest order Raviart-Thomas elements can not be expected to yield highly accurate solutions. We need more accurate methods. The second reason is apparent from the time dependent step-21: that program is excruciatingly slow, and there is no hope to get highly accurate solutions in 3d within reasonable time frames.
In this program, in order to overcome these two problems, there are five areas which we are trying to improve for a high performance simulator:
Higher order spatial discretizations
Adaptive mesh refinement
Adaptive time stepping
Operator splitting
Efficient solver and preconditioning
Much inspiration for this program comes from step-31 but several of the techniques discussed here are original.
Advection-dominated two-phase flow mathematical model
We consider the flow of a two-phase immiscible, incompressible fluid. Capillary and gravity effects are neglected, and viscous effects are assumed dominant. The governing equations for such a flow that are identical to those used in step-21 and are
where is the saturation (volume fraction between zero and one) of the second (wetting) phase, is the pressure, is the permeability tensor, is the total mobility, is the porosity, is the fractional flow of the wetting phase, is the source term and is the total velocity. The total mobility, fractional flow of the wetting phase and total velocity are respectively given by
where subscripts represent the wetting and non-wetting phases, respectively.
For convenience, the porosity in the saturation equation, which can be considered a scaling factor for the time variable, is set to one. Following a commonly used prescription for the dependence of the relative permeabilities and on saturation, we use
The porous media equations above are augmented by initial conditions for the saturation and boundary conditions for the pressure. Since saturation and the gradient of the pressure uniquely determine the velocity, no boundary conditions are necessary for the velocity. Since the flow equations do not contain time derivatives, initial conditions for the velocity and pressure variables are not required. The flow field separates the boundary into inflow or outflow parts. Specifically,
and we arrive at a complete model by also imposing boundary values for the saturation variable on the inflow boundary .
Adaptive operator splitting and time stepping
As seen in step-21, solving the flow equations for velocity and pressure are the parts of the program that take far longer than the (explicit) updating step for the saturation variable once we know the flow variables. On the other hand, the pressure and velocity depend only weakly on saturation, so one may think about only solving for pressure and velocity every few time steps while updating the saturation in every step. If we can find a criterion for when the flow variables need to be updated, we call this splitting an "adaptive
operator splitting" scheme.
Here, we use the following a posteriori criterion to decide when to re-compute pressure and velocity variables (detailed derivations and descriptions can be found in [Chueh2013]):
where superscripts in parentheses denote the number of the saturation time step at which any quantity is defined and represents the last step where we actually computed the pressure and velocity. If exceeds a certain threshold we re-compute the flow variables; otherwise, we skip this computation in time step and only move the saturation variable one time step forward.
In short, the algorithm allows us to perform a number of saturation time steps of length until the criterion above tells us to re-compute velocity and pressure variables, leading to a macro time step of length
We choose the length of (micro) steps subject to the Courant-Friedrichs-Lewy (CFL) restriction according to the criterion
which we have confirmed to be stable for the choice of finite element and time stepping scheme for the saturation equation discussed below ( denotes the diameter of cell ). The result is a scheme where neither micro nor macro time steps are of uniform length, and both are chosen adaptively.
Time discretization
Using this time discretization, we obtain the following set of equations for each time step from the IMPES approach (see step-21):
Using the fact that , the time discrete saturation equation becomes
Weak form, space discretization for the pressure-velocity part
By multiplying the equations defining the total velocity and the equation that expresses its divergence in terms of source terms, with test functions and respectively and then integrating terms by parts as necessary, the weak form of the problem reads: Find so that for all test functions there holds
Here, represents the unit outward normal vector to and the pressure can be prescribed weakly on the open part of the boundary whereas on those parts where a velocity is prescribed (for example impermeable boundaries with the term disappears altogether because .
We use continuous finite elements to discretize the velocity and pressure equations. Specifically, we use mixed finite elements to ensure high order approximation for both vector (e.g. a fluid velocity) and scalar variables (e.g. pressure) simultaneously. For saddle point problems, it is well established that the so-called Babuska-Brezzi or Ladyzhenskaya-Babuska-Brezzi (LBB) conditions [BrezziFortin], [Chen2005] need to be satisfied to ensure stability of the pressure-velocity system. These stability conditions are satisfied in the present work by using elements for velocity that are one order higher than for the pressure, i.e. and , where , is the space dimension, and denotes the space of tensor product Lagrange polynomials of degree in each variable.
Stabilization, weak form and space discretization for the saturation transport equation
The chosen elements for the saturation equation do not lead to a stable discretization without upwinding or other kinds of stabilization, and spurious oscillations will appear in the numerical solution. Adding an artificial diffusion term is one approach to eliminating these oscillations [Chen2005]. On the other hand, adding too much diffusion smears sharp fronts in the solution and suffers from grid-orientation difficulties [Chen2005]. To avoid these effects, we use the artificial diffusion term proposed by [GuermondPasquetti2008] and validated in [Chueh2013] and [KHB12], as well as in step-31.
This method modifies the (discrete) weak form of the saturation equation to read
where is the artificial diffusion parameter and is an appropriately chosen numerical flux on the boundary of the domain (we choose the obvious full upwind flux for this).
Following [GuermondPasquetti2008] (and as detailed in [Chueh2013]), we use the parameter as a piecewise constant function set on each cell with the diameter as
where is a stabilization exponent and is a dimensionless user-defined stabilization constant. Following [GuermondPasquetti2008] as well as the implementation in step-31, the velocity and saturation global normalization constant, , and the residual are respectively given by
and
where is a second dimensionless user-defined constant, is the diameter of the domain and is the range of the present saturation values in the entire computational domain .
This stabilization scheme has a number of advantages over simpler schemes such as finite volume (or discontinuous Galerkin) methods or streamline upwind Petrov Galerkin (SUPG) discretizations. In particular, the artificial diffusion term acts primarily in the vicinity of discontinuities since the residual is small in areas where the saturation is smooth. It therefore provides for a higher degree of accuracy. On the other hand, it is nonlinear since depends on the saturation . We avoid this difficulty by treating all nonlinear terms explicitly, which leads to the following fully discrete problem at time step :
where is the velocity linearly extrapolated from and to the current time if while is if . Consequently, the equation is linear in and all that is required is to solve with a mass matrix on the saturation space.
Since the Dirichlet boundary conditions for saturation are only imposed on the inflow boundaries, the third term on the left hand side of the equation above needs to be split further into two parts:
where and represent inflow and outflow boundaries, respectively. We choose values using an upwind formulation, i.e. and correspond to the values taken from the present cell, while the values of and are those taken from the neighboring boundary .
Adaptive mesh refinement
Choosing meshes adaptively to resolve sharp saturation fronts is an essential ingredient to achieve efficiency in our algorithm. Here, we use the same shock-type refinement approach used in [Chueh2013] to select those cells that should be refined or coarsened. The refinement indicator for each cell of the triangulation is computed by
where is the gradient of the discrete saturation variable evaluated at the center of cell . This approach is analogous to ones frequently used in compressible flow problems, where density gradients are used to indicate refinement. That said, as we will discuss at the end of the results section, this turns out to not be a very useful criterion since it leads to refinement basically everywhere. We only show it here for illustrative purposes.
The linear system and its preconditioning
Following the discretization of the governing equations discussed above, we obtain a linear system of equations in time step of the following form:
where the individual matrices and vectors are defined as follows using shape functions for velocity, and for both pressure and saturation:
and as given in the definition of the stabilized transport equation.
The linear system above is of block triangular form if we consider the top left panel of matrices as one block. We can therefore first solve for the velocity and pressure (unless we decide to use in place of the velocity) followed by a solve for the saturation variable. The first of these steps requires us to solve
We apply the Generalized Minimal Residual (GMRES) method [Saad1986] to this linear system. The ideal preconditioner for the velocity-pressure system is
where is the Schur complement [Zhang2005] of the system. This preconditioner is optimal since
for which it can be shown that GMRES converges in two iterations.
However, we cannot of course expect to use exact inverses of the velocity mass matrix and the Schur complement. We therefore follow the approach by [SW94] originally proposed for the Stokes system. (See also the note in the "Possibilities for extensions" section of step-22.) Adapting it to the current set of equations yield the preconditioner
where a tilde indicates an approximation of the exact inverse matrix. In particular, since is a sparse symmetric and positive definite matrix, we choose for a single application of a sparse incomplete Cholesky decomposition of this matrix [GolubVanLoan]. We note that the Schur complement that corresponds to the porous media flow operator in non-mixed form, and should be a good approximation of the actual Schur complement matrix . Since both of these matrices are again symmetric and positive definite, we use an incomplete Cholesky decomposition of for . It is important to note that needs to be built with Dirichlet boundary conditions to ensure its invertibility.
Once the velocity is available, we can assemble and and solve for the saturations using
where the mass matrix is solved by the conjugate gradient method, using an incomplete Cholesky decomposition as preconditioner once more.
The test cases
Note
The implementation discussed here uses and extends parts of the step-21, step-31 and step-33 tutorial programs of this library. In particular, if you want to understand how it works, please consult step-21 for a discussion of the mathematical problem, and step-31 from which most of the implementation is derived. We will not discuss aspects of the implementation that have already been discussed in step-31.
We show numerical results for some two-phase flow equations augmented by appropriate initial and boundary conditions in conjunction with two different choices of the permeability model. In the problems considered, there is no internal source term ( ). As mentioned above, quantitative numerical results are presented in [Chueh2013].
For simplicity, we choose , though all methods (as well as our implementation) should work equally well on general unstructured meshes.
Initial conditions are only required for the saturation variable, and we choose , i.e. the porous medium is initially filled by a mixture of the non-wetting (80%) and wetting (20%) phases. This differs from the initial condition in step-21 where we had taken , but for complicated mathematical reasons that are mentioned there in a longish remark, the current method using an entropy-based artificial diffusion term does not converge to the viscosity solution with this initial condition without additional modifications to the method. We therefore choose this modified version for the current program.
Furthermore, we prescribe a linear pressure on the boundaries:
Pressure and saturation uniquely determine a velocity, and the velocity determines whether a boundary segment is an inflow or outflow boundary. On the inflow part of the boundary, , we impose
In other words, the domain is flooded by the wetting phase from the left. No boundary conditions for the saturation are required for the outflow parts of the boundary.
All the numerical and physical parameters used for the 2D/3D cases are listed in the following table:
Parameter
Symbol
Value
units
Porosity
1.0
-
Viscosity (wetting)
0.2
Viscosity (nonwetting)
1.0
Stabilization exponent
1.0
-
Stabilization constant
2D: 0.3; 3D: 0.27
-
Normalization constant
1.0
-
Number of high-permeability regions
50; 200
-
Operator splitting threshold
5.0
-
A note on the implementation
We have mentioned many areas above in which this program improves over the step-20 and step-21 programs upon which it is based. It also uses the Trilinos interfaces for its vectors and matrices; this specifically has the advantage that we can use an excellent implementation of the Incomplete Cholesky method we use as preconditioners for both the top left and bottom right blocks of the matrix that describes the flow portion of the coupled problem.
We note that while we use the TrilinosWrappers::MPI::BlockVector class to store vectors, the program does not actually use MPI (or any other way to run in parallel): There is no non-MPI vector class in the TrilinosWrappers namespace, but we can use the MPI version to also run a sequential code as we do here.
The commented program
Include files
The first step, as always, is to include the functionality of a number of deal.II and C++ header files.
The list includes some header files that provide vector, matrix, and preconditioner classes that implement interfaces to the respective Trilinos classes; some more information on these may be found in step-31.
At the end of this top-matter, we open a namespace for the current project into which all the following material will go, and then import all deal.II names into this namespace:
The implementations of all the physical quantities such as total mobility and fractional flow of water are taken from step-21 so again we don't have do any comment about them. Compared to step-21 we have added checks that the saturation passed to these functions is in fact within the physically valid range. Furthermore, given that the wetting phase moves at speed it is clear that must be greater or equal to zero, so we assert that as well to make sure that our calculations to get at the formula for the derivative made sense.
In this first part we define a number of classes that we need in the construction of linear solvers and preconditioners. This part is essentially the same as that used in step-31. The only difference is that the original variable name stokes_matrix is replaced by another name darcy_matrix to match our problem.
namespace LinearSolvers
{
template <class MatrixType, class PreconditionerType>
The definition of the class that defines the top-level logic of solving the time-dependent advection-dominated two-phase flow problem (or Buckley-Leverett problem [Buckley1942]) is mainly based on tutorial programs step-21 and step-33, and in particular on step-31 where we have used basically the same general structure as done here. As in step-31, the key routines to look for in the implementation below are the run() and solve() functions.
The main difference to step-31 is that, since adaptive operator splitting is considered, we need a couple more member variables to hold the last two computed Darcy (velocity/pressure) solutions in addition to the current one (which is either computed directly, or extrapolated from the previous two), and we need to remember the last two times we computed the Darcy solution. We also need a helper function that figures out whether we do indeed need to recompute the Darcy solution.
Unlike step-31, this step uses one more AffineConstraints object called darcy_preconditioner_constraints. This constraint object is used only for assembling the matrix for the Darcy preconditioner and includes hanging node constraints as well as Dirichlet boundary value constraints for the pressure variable. We need this because we are building a Laplace matrix for the pressure as an approximation of the Schur complement) which is only positive definite if boundary conditions are applied.
The collection of member functions and variables thus declared in this class is then rather similar to those in step-31:
This all is followed by the member variables, most of which are similar to the ones in step-31, with the exception of the ones that pertain to the macro time stepping for the velocity/pressure system:
At the very end we declare a variable that denotes the material model. Compared to step-21, we do this here as a member variable since we will want to use it in a variety of places and so having a central place where such a variable is declared will make it simpler to replace one class by another (e.g. replace RandomMedium::KInverse by SingleCurvingCrack::KInverse).
const RandomMedium::KInverse<dim> k_inverse;
};
TwoPhaseFlowProblem<dim>::TwoPhaseFlowProblem
The constructor of this class is an extension of the constructors in step-21 and step-31. We need to add the various variables that concern the saturation. As discussed in the introduction, we are going to use (Taylor-Hood) elements again for the Darcy system, an element combination that fulfills the Ladyzhenskaya-Babuska-Brezzi (LBB) conditions [Brezzi and Fortin 1991, Chen 2005], and elements for the saturation. However, by using variables that store the polynomial degree of the Darcy and temperature finite elements, it is easy to consistently modify the degree of the elements as well as all quadrature formulas used on them downstream. Moreover, we initialize the time stepping variables related to operator splitting as well as the option for matrix assembly and preconditioning:
This is the function that sets up the DoFHandler objects we have here (one for the Darcy part and one for the saturation part) as well as set to the right sizes the various objects required for the linear algebra in this program. Its basic operations are similar to what step-31 did.
The body of the function first enumerates all degrees of freedom for the Darcy and saturation systems. For the Darcy part, degrees of freedom are then sorted to ensure that velocities precede pressure DoFs so that we can partition the Darcy matrix into a matrix.
Then, we need to incorporate hanging node constraints and Dirichlet boundary value constraints into darcy_preconditioner_constraints. The boundary condition constraints are only set on the pressure component since the Schur complement preconditioner that corresponds to the porous media flow operator in non-mixed form, , acts only on the pressure variable. Therefore, we use a component_mask that filters out the velocity component, so that the condensation is performed on pressure degrees of freedom only.
After having done so, we count the number of degrees of freedom in the various blocks. This information is then used to create the sparsity pattern for the Darcy and saturation system matrices as well as the preconditioner matrix from which we build the Darcy preconditioner. As in step-31, we choose to create the pattern using the blocked version of DynamicSparsityPattern. So, for this, we follow the same way as step-31 did and we don't have to repeat descriptions again for the rest of the member function.
The next few functions are devoted to setting up the various system and preconditioner matrices and right hand sides that we have to deal with in this program.
This function assembles the matrix we use for preconditioning the Darcy system. What we need are a vector mass matrix weighted by on the velocity components and a mass matrix weighted by on the pressure component. We start by generating a quadrature object of appropriate order, the FEValues object that can give values and gradients at the quadrature points (together with quadrature weights). Next we create data structures for the cell matrix and the relation between local and global DoFs. The vectors phi_u and grad_phi_p are going to hold the values of the basis functions in order to faster build up the local matrices, as was already done in step-22. Before we start the loop over all active cells, we have to specify which components are pressure and which are velocity.
The creation of the local matrix is rather simple. There are only a term weighted by (on the velocity) and a Laplace matrix weighted by to be generated, so the creation of the local matrix is done in essentially two lines. Since the material model functions at the top of this file only provide the inverses of the permeability and mobility, we have to compute and by hand from the given values, once per quadrature point.
Once the local matrix is ready (loop over rows and columns in the local matrix on each quadrature point), we get the local DoF indices and write the local information into the global matrix. We do this by directly applying the constraints (i.e. darcy_preconditioner_constraints) that takes care of hanging node and zero Dirichlet boundary condition constraints. By doing so, we don't have to do that afterwards, and we later don't have to use AffineConstraints::condense and MatrixTools::apply_boundary_values, both functions that would need to modify matrix and vector entries and so are difficult to write for the Trilinos classes where we don't immediately have access to individual memory locations.
After calling the above functions to assemble the preconditioner matrix, this function generates the inner preconditioners that are going to be used for the Schur complement block preconditioner. The preconditioners need to be regenerated at every saturation time step since they depend on the saturation that varies with time.
In here, we set up the preconditioner for the velocity-velocity matrix and the Schur complement . As explained in the introduction, we are going to use an IC preconditioner based on the vector matrix and another based on the scalar Laplace matrix (which is spectrally close to the Schur complement of the Darcy matrix). Usually, the TrilinosWrappers::PreconditionIC class can be seen as a good black-box preconditioner which does not need any special knowledge of the matrix structure and/or the operator that's behind it.
This is the function that assembles the linear system for the Darcy system.
Regarding the technical details of implementation, the procedures are similar to those in step-22 and step-31. We reset matrix and vector, create a quadrature formula on the cells, and then create the respective FEValues object.
There is one thing that needs to be commented: since we have a separate finite element and DoFHandler for the saturation, we need to generate a second FEValues object for the proper evaluation of the saturation solution. This isn't too complicated to realize here: just use the saturation structures and set an update flag for the basis function values which we need for evaluation of the saturation solution. The only important part to remember here is that the same quadrature formula is used for both FEValues objects to ensure that we get matching information when we loop over the quadrature points of the two objects.
The declarations proceed with some shortcuts for array sizes, the creation of the local matrix, right hand side as well as the vector for the indices of the local dofs compared to the global system.
Next we need a vector that will contain the values of the saturation solution at the previous time level at the quadrature points to assemble the saturation dependent coefficients in the Darcy equations.
The set of vectors we create next hold the evaluations of the basis functions as well as their gradients that will be used for creating the matrices. Putting these into their own arrays rather than asking the FEValues object for this information each time it is needed is an optimization to accelerate the assembly process, see step-22 for details.
The last two declarations are used to extract the individual blocks (velocity, pressure, saturation) from the total FE system.
Now start the loop over all cells in the problem. We are working on two different DoFHandlers for this assembly routine, so we must have two different cell iterators for the two objects in use. This might seem a bit peculiar, but since both the Darcy system and the saturation system use the same grid we can assume that the two iterators run in sync over the cells of the two DoFHandler objects.
The first statements within the loop are again all very familiar, doing the update of the finite element data as specified by the update flags, zeroing out the local arrays and getting the values of the old solution at the quadrature points. At this point we also have to get the values of the saturation function of the previous time step at the quadrature points. To this end, we can use the FEValues::get_function_values (previously already used in step-9, step-14 and step-15), a function that takes a solution vector and returns a list of function values at the quadrature points of the present cell. In fact, it returns the complete vector-valued solution at each quadrature point, i.e. not only the saturation but also the velocities and pressure.
Then we are ready to loop over the quadrature points on the cell to do the integration. The formula for this follows in a straightforward way from what has been discussed in the introduction.
Once this is done, we start the loop over the rows and columns of the local matrix and feed the matrix with the relevant products.
The last step in the loop over all cells is to enter the local contributions into the global matrix and vector structures to the positions specified in local_dof_indices. Again, we let the AffineConstraints class do the insertion of the cell matrix elements to the global matrix, which already condenses the hanging node constraints.
auto cell = darcy_dof_handler.begin_active();
constauto endc = darcy_dof_handler.end();
auto saturation_cell = saturation_dof_handler.begin_active();
This function is to assemble the linear system for the saturation transport equation. It calls, if necessary, two other member functions: assemble_saturation_matrix() and assemble_saturation_rhs(). The former function then assembles the saturation matrix that only needs to be changed occasionally. On the other hand, the latter function that assembles the right hand side must be called at every saturation time step.
This function is easily understood since it only forms a simple mass matrix for the left hand side of the saturation linear system by basis functions phi_i_s and phi_j_s only. Finally, as usual, we enter the local contribution into the global matrix by specifying the position in local_dof_indices. This is done by letting the AffineConstraints class do the insertion of the cell matrix elements to the global matrix, which already condenses the hanging node constraints.
This function is to assemble the right hand side of the saturation transport equation. Before going about it, we have to create two FEValues objects for the Darcy and saturation systems respectively and, in addition, two FEFaceValues objects for the two systems because we have a boundary integral term in the weak form of saturation equation. For the FEFaceValues object of the saturation system, we also require normal vectors, which we request using the update_normal_vectors flag.
Next, before looping over all the cells, we have to compute some parameters (e.g. global_u_infty, global_S_variation, and global_Omega_diameter) that the artificial viscosity needs. This is largely the same as was done in step-31, so you may see there for more information.
The real works starts with the loop over all the saturation and Darcy cells to put the local contributions into the global vector. In this loop, in order to simplify the implementation, we split some of the work into two helper functions: assemble_saturation_rhs_cell_term and assemble_saturation_rhs_boundary_term. We note that we insert cell or boundary contributions into the global vector in the two functions rather than in this present function.
This function takes care of integrating the cell terms of the right hand side of the saturation equation, and then assembling it into the global right hand side vector. Given the discussion in the introduction, the form of these contributions is clear. The only tricky part is getting the artificial viscosity and all that is necessary to compute it. The first half of the function is devoted to this task.
The last part of the function is copying the local contributions into the global vector with position specified in local_dof_indices.
The next function is responsible for the boundary integral terms in the right hand side form of the saturation equation. For these, we have to compute the upwinding flux on the global boundary faces, i.e. we impose Dirichlet boundary conditions weakly only on inflow parts of the global boundary. As before, this has been described in step-21 so we refrain from giving more descriptions about that.
This function implements the operator splitting algorithm, i.e. in each time step it either re-computes the solution of the Darcy system or extrapolates velocity/pressure from previous time steps, then determines the size of the time step, and then updates the saturation variable. The implementation largely follows similar code in step-31. It is, next to the run() function, the central one in this program.
At the beginning of the function, we ask whether to solve the pressure-velocity part by evaluating the a posteriori criterion (see the following function). If necessary, we will solve the pressure-velocity part using the GMRES solver with the Schur complement block preconditioner as is described in the introduction.
On the other hand, if we have decided that we don't want to compute the solution of the Darcy system for the current time step, then we need to simply extrapolate the previous two Darcy solutions to the same time as we would have computed the velocity/pressure at. We do a simple linear extrapolation, i.e. given the current length of the macro time step from the time when we last computed the Darcy solution to now (given by current_macro_time_step), and the length of the last macro time step (given by old_macro_time_step), then we get , where and are the last two computed Darcy solutions. We can implement this formula using just two lines of code.
Note that the algorithm here only works if we have at least two previously computed Darcy solutions from which we can extrapolate to the current time, and this is ensured by requiring re-computation of the Darcy solution for the first 2 time steps.
...and then also update the length of the macro time steps we use while we're dealing with time step sizes. In particular, this involves: (i) If we have just recomputed the Darcy solution, then the length of the previous macro time step is now fixed and the length of the current macro time step is, up to now, simply the length of the current (micro) time step. (ii) If we have not recomputed the Darcy solution, then the length of the current macro time step has just grown by time_step.
if (solve_for_pressure_and_velocity == true)
{
old_macro_time_step = current_macro_time_step;
current_macro_time_step = time_step;
}
else
current_macro_time_step += time_step;
The last step in this function is to recompute the saturation solution based on the velocity field we've just obtained. This naturally happens in every time step, and we don't skip any of these computations. At the end of computing the saturation, we project back into the allowed interval to make sure our solution remains physical.
{
std::cout << " Solving saturation transport equation..." << std::endl;
The next function does the refinement and coarsening of the mesh. It does its work in three blocks: (i) Compute refinement indicators by looking at the gradient of a solution vector extrapolated linearly from the previous two using the respective sizes of the time step (or taking the only solution we have if this is the first time step). (ii) Flagging those cells for refinement and coarsening where the gradient is larger or smaller than a certain threshold, preserving minimal and maximal levels of mesh refinement. (iii) Transferring the solution from the old to the new mesh. None of this is particularly difficult.
This function implements the a posteriori criterion for adaptive operator splitting. The function is relatively straightforward given the way we have implemented other functions above and given the formula for the criterion derived in the paper.
If one decides that one wants the original IMPES method in which the Darcy equation is solved in every time step, then this can be achieved by setting the threshold value AOS_threshold (with a default of ) to zero, thereby forcing the function to always return true.
Finally, note that the function returns true unconditionally for the first two time steps to ensure that we have always solved the Darcy system at least twice when skipping its solution, thereby allowing us to extrapolate the velocity from the last two solutions in solve().
The next function simply makes sure that the saturation values always remain within the physically reasonable range of . While the continuous equations guarantee that this is so, the discrete equations don't. However, if we allow the discrete solution to escape this range we get into trouble because terms like and will produce unreasonable results (e.g. for , which would imply that the wetting fluid phase flows against the direction of the bulk fluid velocity)). Consequently, at the end of each time step, we simply project the saturation field back into the physically reasonable region.
for (unsignedint i = 0; i < saturation_solution.size(); ++i)
if (saturation_solution(i) < 0.2)
saturation_solution(i) = 0.2;
elseif (saturation_solution(i) > 1)
saturation_solution(i) = 1;
}
TwoPhaseFlowProblem<dim>::get_max_u_F_prime
Another simpler helper function: Compute the maximum of the total velocity times the derivative of the fraction flow function, i.e., compute . This term is used in both the computation of the time step as well as in normalizing the entropy-residual term in the artificial viscosity.
For computing the stabilization term, we need to know the range of the saturation variable. Unlike in step-31, this range is trivially bounded by the interval but we can do a bit better by looping over a collection of quadrature points and seeing what the values are there. If we can, i.e., if there are at least two timesteps around, we can even take the values extrapolated to the next time step.
As before, the function is taken with minimal modifications from step-31.
The final tool function is used to compute the artificial viscosity on a given cell. This isn't particularly complicated if you have the formula for it in front of you, and looking at the implementation in step-31. The major difference to that tutorial program is that the velocity here is not simply but and some of the formulas need to be adjusted accordingly.
This function is, besides solve(), the primary function of this program as it controls the time iteration as well as when the solution is written into output files and when to do mesh refinement.
With the exception of the startup code that loops back to the beginning of the function through the goto start_time_iteration label, everything should be relatively straightforward. In any case, it mimics the corresponding function in step-31.
The main function looks almost the same as in all other programs. The need to initialize the MPI subsystem for a program that uses Trilinos – even for programs that do not actually run in parallel – is explained in step-31.
The output of this program is not really much different from that of step-21: it solves the same problem, after all. Of more importance are quantitative metrics such as the accuracy of the solution as well as the time needed to compute it. These are documented in detail in the two publications listed at the top of this page and we won't repeat them here.
That said, no tutorial program is complete without a couple of good pictures, so here is some output of a run in 3d:
Velocity vectors of flow through the porous medium with random permeability model. Streaming paths of high permeability and resulting high velocity are clearly visible.
Streamlines colored by the saturation along the streamline path. Blue streamlines indicate low saturations, i.e., the flow along these streamlines must be slow or else more fluid would have been transported along them. On the other hand, green paths indicate high velocities since the fluid front has already reached further into the domain.
Streamlines with a volume rendering of the saturation, showing how far the fluid front has advanced at this time.
Surface of the mesh showing the adaptive refinement along the front.
Possibilities for extensions
The primary objection one may have to this program is that it is still too slow: 3d computations on reasonably fine meshes are simply too expensive to be done routinely and with reasonably quick turn-around. This is similar to the situation we were in when we wrote step-31, from which this program has taken much inspiration. The solution is similar as it was there as well: We need to parallelize the program in a way similar to how we derived step-32 out of step-31. In fact, all of the techniques used in step-32 would be transferable to this program as well, making the program run on dozens or hundreds of processors immediately.
A different direction is to make the program more relevant to many other porous media applications. Specifically, one avenue is to go to the primary user of porous media flow simulators, namely the oil industry. There, applications in this area are dominated by multiphase flow (i.e., more than the two phases we have here), and the reactions they may have with each other (or any other way phases may exchange mass, such as through dissolution in and bubbling out of gas from the oil phase). Furthermore, the presence of gas often leads to compressibility effects of the fluid. Jointly, these effects are typically formulated in the widely-used "black oil model". True reactions between multiple phases also play a role in oil reservoir modeling when considering controlled burns of oil in the reservoir to raise pressure and temperature. These are much more complex problems, though, and left for future projects.
Finally, from a mathematical perspective, we have derived the criterion for re-computing the velocity/pressure solution at a given time step under the assumption that we want to compare the solution we would get at the current time step with that computed the last time we actually solved this system. However, in the program, whenever we did not re-compute the solution, we didn't just use the previously computed solution but instead extrapolated from the previous two times we solved the system. Consequently, the criterion was pessimistically stated: what we should really compare is the solution we would get at the current time step with the extrapolated one. Re-stating the theorem in this regard is left as an exercise.
There are also other ways to extend the mathematical foundation of this program; for example, one may say that it isn't the velocity we care about, but in fact the saturation. Thus, one may ask whether the criterion we use here to decide whether needs to be recomputed is appropriate; one may, for example, suggest that it is also important to decide whether (and by how much) a wrong velocity field in fact affects the solution of the saturation equation. This would then naturally lead to a sensitivity analysis.
From an algorithmic viewpoint, we have here used a criterion for refinement that is often used in engineering, namely by looking at the gradient of the solution. However, if you inspect the solution, you will find that it quickly leads to refinement almost everywhere, even in regions where it is clearly not necessary: frequently used therefore does not need to imply that it is a useful criterion to begin with. On the other hand, replacing this criterion by a different and better one should not be very difficult. For example, the KellyErrorEstimator class used in many other programs should certainly be applicable to the current problem as well.