Skip to content

Consistent and high-resolution: WENO, NTPFA and AvgMPFA

The following example demonstrates how to set up a reservoir model with different discretizations for flow and transport for a skewed grid. The model setup is taken from 6.1.2 in the MRST book [1].

The example is a two-dimensional model with a single layer. The domain itself is completely symmetric, with producer in either lower corner and a single injector in the middle of the domain. From a flow perspective, the model is completely symmetric, and exactly the same results should be obtained for both producer wells. The grid is intentionally distorted to be skewed towards one producer, which will lead to consistency issues for the standard two-point flux approximation used by the solvers.

The fluid model is also quite simple, with two immiscible phases that have linear relative permeability functions. A consequence of this is that the fluid fronts are not self-sharpening and are expected to be smeared out due to the numerical diffusion of the standard first-point upwind scheme.

julia
using Jutul, JutulDarcy, GLMakie
Darcy, kg, meter, day, bar = si_units(:darcy, :kg, :meter, :day, :bar)
ny = 20
nx = 2*ny + 1
nz = 1

gcart = CartesianMesh((nx, ny, nz), (2.0, 1.0, 1.0))
g = UnstructuredMesh(gcart)

for (i, pt) in enumerate(g.node_points)
    x, y, z = pt
    pt += [0.4*(1.0-(x-1.0)^2)*(1.0-y), 0, 0]
    g.node_points[i] = pt .* [1000.0, 1000.0, 1.0]
end

c_i = cell_index(g, (nx÷2+1, ny, 1))
c_p1 = cell_index(g, (1, 1, 1))
c_p2 = cell_index(g, (nx, 1, 1))

fig = Figure()
ax = Axis(fig[1, 1])
Jutul.plot_mesh_edges!(ax, g)
plot_mesh!(ax, g, cells = c_i, color = :red)
plot_mesh!(ax, g, cells = [c_p1, c_p2], color = :blue)
fig

Define wells, fluid system and controls

julia
reservoir = reservoir_domain(g, permeability = 0.1Darcy, porosity = 0.1)

c_i1 = cell_index(g, (nx÷2+1, ny, 1))

I1 = setup_vertical_well(reservoir, nx÷2+1, ny, name = :I1, simple_well = true)
P1 = setup_vertical_well(reservoir, 1, 1, name = :P1, simple_well = true)
P2 = setup_vertical_well(reservoir, nx, 1, name = :P2, simple_well = true)

phases = (AqueousPhase(), LiquidPhase())
rhoWS = rhoLS = 1000.0
rhoS = [rhoWS, rhoLS] .* kg/meter^3
sys = ImmiscibleSystem(phases, reference_densities = rhoS)

dt = repeat([30.0]*day, 100)
pv = pore_volume(reservoir)
inj_rate = sum(pv)/sum(dt)

rate_target = TotalRateTarget(inj_rate)
I_ctrl = InjectorControl(rate_target, [1.0, 0.0], density = rhoWS)
bhp_target = BottomHolePressureTarget(50*bar)
P_ctrl = ProducerControl(bhp_target)
controls = Dict()
controls[:I1] = I_ctrl
controls[:P1] = P_ctrl
controls[:P2] = P_ctrl
ProducerControl{BottomHolePressureTarget{Float64}, Float64}(BottomHolePressureTarget with value 50.0 [bar], 1.0)

Define functions to perform the simulations

We are going to perform a number of simulations with different discretizations and it is therefore convenient to setup functions that can be called with the desired discretizations as arguments. The results are stored in a dictionary for easy retrieval after the simulations are done.

julia
all_results = Dict()
Dict{Any, Any}()

Function to perform simulation

The function simulate_with_discretizations sets up the reservoir model with the requested discretizations. The default behavior mirrors the default behavior of JutulDarcy by using the industry standard single-point upwind, two-point flux approximation.

julia
function simulate_with_discretizations(upwind = :spu, kgrad = :tpfa)
    model, parameters = setup_reservoir_model(reservoir, sys,
        kgrad = kgrad,
        upwind = upwind,
        wells = [P1, P2, I1]
    )
    kr = BrooksCoreyRelativePermeabilities(sys, 1.0, 0.0, 1.0)
    replace_variables!(model, RelativePermeabilities = kr)
    forces = setup_reservoir_forces(model, control = controls)
    state0 = setup_reservoir_state(model, Pressure = 100*bar, Saturations = [0.0, 1.0])
    result = simulate_reservoir(state0, model, dt, info_level = 0, parameters = parameters, forces = forces);
end
simulate_with_discretizations (generic function with 3 methods)

Function to plot the results

We create a function plot_discretization_result that takes the result from a simultation and plots the saturation field after 75 steps.

julia
function plot_discretization_result(result, name)
    ws, states = result
    sg = states[75][:Saturations][1, :]
    fig = Figure()
    ax = Axis(fig[1, 1], title = name)
    plot_cell_data!(ax, g, sg, colormap = :seaborn_icefire_gradient)
    Jutul.plot_mesh_edges!(ax, g)
    return fig
end
plot_discretization_result (generic function with 1 method)

Function to simulate and plot discretizations

The function simulate_and_plot_discretizations is a convenience function that calls the simulate_with_discretizations function and then plots the result.

julia
function simulate_and_plot_discretizations(upwind, kgrad)
    result = simulate_with_discretizations(upwind, kgrad)
    ustr = uppercase("$upwind")
    if kgrad == :avgmpfa
        kgstr = "AverageMPFA"
    else
        kgstr = uppercase("$kgrad")
    end
    descr = "$ustr with $kgstr"
    all_results[descr] = result
    plot_discretization_result(result, descr)
end
simulate_and_plot_discretizations (generic function with 1 method)

Simulate SPU-TPFA

We start by simulating the model with the standard single-point upwind and the two-point flux approximation. This is the default discretization used by JutulDarcy. The schemes are robust (easy to implement and obtain nonlinear convergence) but suffer for consistency issues on skewed and non-K-orthogonal grids. We can observe both significant smearing (the fluid front is not sharp) and a significant bias towards the producer in the lower right corner.

julia
simulate_and_plot_discretizations(:spu, :tpfa)

Simulate SPU-AvgMPFA

The next simulation uses a type of multi-point flux approximation (AverageMPFA or AvgMPFA), which is one class of consistent discretizations. As the scheme is consistent, the effect of the skewed grid is significantly reduced, with the remaining error being due to the coarse mesh chosen for illustrative purposes. Using AverageMPFA does not alleviate the smearing of the fluid front, however.

julia
simulate_and_plot_discretizations(:spu, :avgmpfa)

Simulate SPU-TPFA

The next simulation uses a nonlinear two-point flux approximation (NTPFA). This scheme is consistent and monotone, and can be derived from the AverageMPFA scheme by allowing the ratio between the two half-face fluxes at each interface to vary based on pressure.

julia
simulate_and_plot_discretizations(:spu, :ntpfa)

Simulate WENO-TPFA

The penultimate simulation uses a high-resolution scheme (WENO) for the flow, but reverts back to the inconsistent TPFA scheme for the pressure gradient. We now see significantly sharper fronts, but the bias towards the producer in the lower right corner is now back.

julia
simulate_and_plot_discretizations(:weno, :tpfa)

Simulate WENO-AvgMPFA

The final simulation combines the high-resolution WENO scheme with the consistent AverageMPFA scheme. The result is a sharp fluid front with no bias towards either producer. This is the most accurate simulation of the four for this intentionally badly gridded problem.

julia
simulate_and_plot_discretizations(:weno, :avgmpfa)

Compare the results

We can now compare the results of the different discretizations. We start by plotting the well curves in the two producers. Recall that the model is, from a flow perspective, completely symmetric, and the water cut in the two producers should be identical if the problem is fully resolved. We observe this for the consistent schemes, and delayed breakthrough for the SPU solves.

julia
fig = Figure()
ax1 = Axis(fig[1, 1], title = "P1 water cut")
ax2 = Axis(fig[2, 1], title = "P2 water cut")
colors = Makie.wong_colors()
for (i, pr) in enumerate(all_results)
    name, result = pr
    ws, states = result
    wcut1 = ws[:P1][:wcut]
    wcut2 = ws[:P2][:wcut]
    lines!(ax1, wcut1, label = "$name", color = colors[i])
    lines!(ax2, wcut2, label = "$name", color = colors[i])
end
axislegend(ax1, position = :lt)
fig

Compare the saturation fields as contours

We can also compare the saturation fields for the different discretizations to see the spatial differences. To make life a bit easier, we make a function to do the comparison plots for us.

julia
function compare_contours(name1, name2, title)
    cmp = :seaborn_icefire_gradient
    r1 = all_results[name1]
    r2 = all_results[name2]
    fig = Figure(size = (1200, 800))
    ax = Axis(fig[1, 1], title = title)
    s1 = reshape(r1.states[75][:Saturations][1, :], nx, ny)
    s2 = reshape(r2.states[75][:Saturations][1, :], nx, ny)

    contourf!(ax, s2, colormap = cmp, label = name2)
    contour!(ax, s1, color = :white, linewidth = 5)
    contour!(ax, s1, colormap = cmp, linewidth = 3, label = name1)
    axislegend(position = :ct)

    axd = Axis(fig[1, 2], title = "Difference")
    plt = contourf!(axd, s1-s2,
        colormap = :seismic,
        label = name2,
        levels = range(-1, 1, length = 10),
        colorscale = (-1.0, 1.0)
    )
    Colorbar(fig[1, 3], limits = (-1, 1), colormap = :seismic)
    Colorbar(fig[2, 1:3], limits = (-1, 1), colormap = cmp, vertical = false)

    ax1 = Axis(fig[3, 1], title = name1)
    contourf!(ax1, s1, colormap = cmp)

    ax2 = Axis(fig[3, 2], title = name2)
    contourf!(ax2, s2, colormap = cmp)
    fig
end
compare_contours (generic function with 1 method)

Compare SPU-TPFA and SPU-AvgMPFA

julia
compare_contours("SPU with AverageMPFA", "SPU with TPFA", "Consistent (AvgMPFA) vs inconsistent (TPFA)")

Compare WENO-TPFA and SPU-TPFA

julia
compare_contours("WENO with TPFA", "SPU with TPFA", "High resolution (WENO) vs first order (SPU)")

Compare WENO-AvgMPFA and SPU-TPFA

julia
compare_contours("WENO with AverageMPFA", "SPU with TPFA", "High resolution + consistent vs first order + inconsistent")

Conclusion

The example demonstrates how different discretizations can affect the results of a simulation. The example is intentionally set up to show the differences in the discretizations. The results show that the consistent discretizations (AvgMPFA and NTPFA) can reduce the bias from grid orientation, and, while not present here, permeability tensor effects. The high-resolution WENO scheme can be employed to reduce smearing of the fluid fronts, which is important for problems that are not self-sharpening (e.g. compositional flow and miscible displacements).

These schemes are not a pancea, however, as they require more computational effort to linearize and can have robustness issues where nonlinear convergence rates deteriorate and shorter timesteps may be required. They are accessible in JutulDarcy through the high-level interface and can be applied to any mesh and physics combination supported by the rest of the solvers.

We plot a summary of the saturation fields for all combinations of a pair of consistent and a pair of inconsistent discretizations to demonstrate the key differences.

julia
fig = Figure(size = (1200, 800))
function plot_sat!(i, j, name)
    sg = all_results[name].states[75][:Saturations][1, :]
    ax = Axis(fig[i,j])
    hidespines!(ax)
    hidedecorations!(ax)
    plt = plot_cell_data!(ax, g, sg, colormap = :seaborn_icefire_gradient, colorrange = (0.0, 1.0))
    Jutul.plot_mesh_edges!(ax, g)
    return plt
end
Label(fig[0, 1], "Single-point upwind", fontsize = 30, tellheight = true, tellwidth = false)
Label(fig[0, 2], "High resolution", fontsize = 30, tellheight = true, tellwidth = false)

Label(fig[1, 0], "TPFA", fontsize = 30, rotation = pi/2, tellheight = false, tellwidth = true)
Label(fig[2, 0], "AverageMPFA", fontsize = 30, rotation = pi/2, tellheight = false, tellwidth = true)

plot_sat!(1, 1, "SPU with TPFA")
plot_sat!(2, 1, "SPU with AverageMPFA")
plot_sat!(1, 2, "WENO with TPFA")
plt = plot_sat!(2, 2, "WENO with AverageMPFA")
Colorbar(fig[:, 3], plt)
fig

Example on GitHub

If you would like to run this example yourself, it can be downloaded from the JutulDarcy.jl GitHub repository as a script, or as a Jupyter Notebook

This example took 61.862253822 seconds to complete.

This page was generated using Literate.jl.