Skip to content

Solving the equations

By default, Jutul solves a system as a fully-coupled implicit system of equations discretized with a two-point flux approximation with single-point upwind.

Newton's method

The standard way of solving a system of non-linear equations is by Newton's method (also known as Newton-Raphson's method). A quick recap: For a vector valued residual r(x) of the primary variable vector x we can defined a Newton update:

xk+1=rkJ1r(xk),Jij=rixj.

JutulDarcy solves systems that generally have both non-smooth behavior and physical constraints on the values for x. For that reason, we modify Newton's method slightly:

xk+1=rk+ω(Δx)

Here, ω is a function that limits the variables so that they do not change too much (e.g. Appleyard chopping, limiting of pressure, saturation and composition updates) and that they are within the prescribed limits. There are also options for automated global dampening in the presence of convergence issues. The update is then defined from inverting the Jacobian:

Δx=J1r(xk),Jij=rixj.

Starting with x0as some initial guess taken from the previous time-step, we can solve the system by iterating upon this loop.

Linear solvers and linear systems

For most practical applications it is not feasible or efficient to invert the Jacobian. JutulDarcy uses preconditioned iterative solvers by default, but it is possible to use direct solvers as well when working with smaller models. The high level interface for setting up a reservoir model setup_reservoir_model has an optional block_backend=true keyword argument that determines the matrix format, and consequently the linear solver type to be used.

Direct solvers

If block_backend is set to false, Jutul will assemble into the standard Julia CSC sparse matrix with Float64 elements and Julia's default direct solver will be used. It is also possible to use other Julia solvers on this system, but the default preconditioners assume that block backend is enabled.

Iterative solver

If block_backend is set to true, Jutul will by default use a constrained-pressure residual (CPR) preconditioner for BiCGStab. Jutul relies on Krylov.jl for iterative solvers. The main function that selects the linear solver is reservoir_linsolve that allows for the selection of different preconditioners and linear solvers.

JutulDarcy.reservoir_linsolve Function
julia
reservoir_linsolve(model, precond = :cpr; <keyword arguments>)

Set up iterative linear solver for a reservoir model from setup_reservoir_model.

Arguments

  • model: Reservoir model that will linearize the equations for the linear solver

  • precond=:cpr: Preconditioner type to use: Either :cpr (Constrained-Pressure-Residual) or :ilu0 (block-incomplete-LU) (no effect if solver = :direct).

  • v=0: verbosity (can lead to a large amount of output)

  • solver=:bicgstab: the symbol of a Krylov.jl solver (typically :gmres or :bicgstab)

  • update_interval=:once: how often the CPR AMG hierarchy is reconstructed (:once, :iteration, :ministep, :step)

  • update_interval_partial=:iteration: how often the pressure system is updated in CPR

  • max_coarse: max size of coarse level if using AMG

  • cpr_type=nothing: type of CPR (:true_impes, :quasi_impes or nothing for automatic)

  • partial_update=true: perform partial update of CPR preconditioner outside of AMG update (see above)

  • rtol=1e-3: relative tolerance for the linear solver

  • max_iterations=100: limit for linear solver iterations

Additional keywords are passed onto the linear solver constructor.

source

Jutul.GenericKrylov Type
julia
GenericKrylov(solver = :gmres; preconditioner = nothing; <kwarg>)

Solver that wraps Krylov.jl with support for preconditioning.

source

Single model (only porous medium)

If the model is a single model (e.g. only a reservoir) the matrix format is a block-CSC matrix that combines Julia's builtin sparse matrix format with statically sized elements from the StaticArrays.jl package. If we consider the two-phase immiscible system from Multi-phase, immiscible flow we have a pair of equations Rn,Rw together with the corresponding primary variables pressure and first saturation p,Sn defined for all Nc cells. Let us simplify the notation a bit so that the subscripts of the primary variables are p,s and define a Nc×Nc block Jacobian linear system where the entires are given by:

Jij=[(rnp)ij(rns)ij(rwp)ij(rws)ij]=[JnpJnsJwpJws]ij

This block system has several advantages:

  • We immediately get access to more powerful version of standard Julia preconditioners provided that all operations used are applicable for matrices and are applied in the right commutative order. For example, JutulDarcy uses the ILUZero.jl package when a CSC linear system is preconditioned with incomplete LU factorization with zero fill-in.

  • Sparse matrix vector products are much more efficient as less indicies need to be looked up for each element wise multiplication.

  • Performing local reductions over variables is much easier when they are located in a local matrix.

Constrained Pressure Residual

The CPR preconditioner [3, 4] CPRPreconditioner is a multi-stage physics-informed preconditioner that seeks to decouple the global pressure part of the system from the local transport part. In the limits of incompressible flow without gravity it can be thought of as an elliptic / hyperbolic splitting. We also implement a special variant for the adjoint system that is similar to the treatment described in [5].

JutulDarcy.CPRPreconditioner Type
julia
CPRPreconditioner(p = default_psolve(), s = ILUZeroPreconditioner(); strategy = :quasi_impes, weight_scaling = :unit, update_frequency = 1, update_interval = :iteration, partial_update = true)

Construct a constrained pressure residual (CPR) preconditioner.

By default, this is a AMG-BILU(0) version (algebraic multigrid for pressure, block-ILU(0) for the global system).

source

The short version of the CPR preconditioner can be motivated by our test system:

rn=t((1Sw)ρnϕ)+(ρnvn)ρnqn=0,rw=t(Swρwϕ)+(ρwvw)ρwqw=0.

For simplicity, we assume that there is no gravity, source terms, or compressibility. Each equation can then be divided by their respective densities and summed up to produce a pressure equation:

rp=t((1Sw)ϕ)+vn+t(Swϕ)+vw=t((SwSw)ϕ)+(vn+vw)=(vn+vw)=K(krw/μw+krn/μn)p=Kλtp=0

The final equation is the variable coefficient Poisson equation and is referred to as the incompressible pressure equation for a porous media. We know that algebraic multigrid preconditioners (AMG) are highly efficient for linear systems made by discretizing this equation. The idea in CPR is to exploit this by constructing an approximate pressure equation that is suited for AMG inside the preconditioner.

Constructing the preconditioner is done in two stages:

  1. First, weights for each equation is found locally in each cell that decouples the time derivative from the non-pressure variables. In the above example, this was the true IMPES weights (dividing by density). JutulDarcy supports analytical true IMPES weights for some systems, numerical true IMPES weights for all systems and quasi IMPES weights for all systems.

  2. A pressure equation is formed by weighting each equation by the respective weights and summing. We then have two systems: The pressure system rp with scalar entries and the full system r that has block structure.

During the linear solve, the preconditioner is then made up of two broad stages: First, a preconditioner is applied to the pressure part (typically AMG), then the full system is preconditioned (typically ILU(0)) after the residual has been corrected by the pressure estimate:

  1. Form weighted pressure residual rp=iwiri.

  2. Apply pressure preconditioer Mp: Δp=Mp1rp.

  3. Correct global residual r=rJP(Δp) where P expands the pressure update to the full system vector, with zero entries outside the pressure indices.

  4. Precondition the full system Δx=M1r

  5. Correct the global update with the pressure to obtain the final update: Δx=Δx+P(Δp)

Multi model (porous medium with wells)

If a model is a porous medium with wells, the same preconditioners can be used, but an additional step is required to incorporate the well system. In practical terms, this means that our linearized system is expanded to multiple linear systems:

JΔx=[JrrJrwJwrJww][ΔxrΔxw]=[rrrw]

Here, Jrr is the reservoir equations differentiated with respect to the reservoir primary variables, i.e. the Jacobian from the previous section. Jww is the well system differentiated with respect to the well primary variables. The cross terms, Jrwand Jwr, are the same equations differentiated with respect to the primary variables of the other system.

The well system is generally much smaller than the reservoir system and can be solved by a direct solver. We would like to reuse the block preconditioners defined for the base system. The approach we use is a Schur complement approach to solve the full system. If we linearly eliminate the dependence of the reservoir equations on the well primary variables, we obtain the reduced system:

JΔx=[JrrJrwJww1Jwr0JwrJww][ΔxrΔxw]=[rrJrwJww1rwrw]

We can then solve the system in terms of the reservoir degrees of freedom where the system is a block linear system and we already have a working preconditioner:

(JrrJrwJww1Jwr)xr=rrJrwJww1rw

Once that system is solved for xr, we can recover the well degrees of freedom rw directly:

rw=Jww1(rwJwrxr)

Efficiency of Schur complement

Explicitly forming the matrix JrrJrwJww1Jwr will generally lead to a lot of fill-in in the linear system. JutulDarcy instead uses the action of JrrJrwJww1Jwr as a linear operator from LinearOperators.jl. This means that we must apply the inverse of the well system every time we need to compute the residual or action of the system matrix, but fortunately performing the action of the Schur complement is inexpensive as long as Jww is small and the factorization can be stored.