Skip to content

Quarter-five-spot example

Immiscible   Introduction  

The quarter-five-spot is a standard test problem that simulates 1/4 of the five spot well pattern by assuming axial symmetry. The problem contains an injector in one corner and the producer in the opposing corner, with a significant volume of fluids injected into the domain.

julia
using JutulDarcy, Jutul, HYPRE, Statistics
nx = 50;

Setup

We define a function that, for a given porosity field, computes a solution with an estimated permeability field. For assumptions and derivation of the specific form of the Kozeny-Carman relation used in this example, see Lie, Knut-Andreas. An introduction to reservoir simulation using MATLAB/GNU Octave: User guide for the MATLAB Reservoir Simulation Toolbox (MRST). Cambridge University Press, 2019, Section 2.5.2

julia
function perm_kozeny_carman(Φ)
    return ((Φ^3)*(1e-5)^2)/(0.81*72*(1-Φ)^2);
end

function simulate_qfs(porosity = 0.3)
    Dx = 1000.0
    Dz = 10.0
    bar, kg, meter, day = si_units(:bar, :kilogram, :meter, :day)

    mesh = CartesianMesh((nx, nx, 1), (Dx, Dx, Dz))
    K = perm_kozeny_carman.(porosity)
    domain = reservoir_domain(mesh, permeability = K, porosity = porosity)
    Inj = setup_vertical_well(domain, 1, 1, name = :Injector);
    Prod = setup_vertical_well(domain, nx, nx, name = :Producer);
    phases = (LiquidPhase(), VaporPhase())
    rhoLS = 1000.0*kg/meter^3
    rhoGS = 700.0*kg/meter^3
    rhoS = [rhoLS, rhoGS]
    sys = ImmiscibleSystem(phases, reference_densities = rhoS)
    model, parameters = setup_reservoir_model(domain, sys, wells = [Inj, Prod])
    c = [1e-6/bar, 1e-6/bar]
    ρ = ConstantCompressibilityDensities(p_ref = 150*bar, density_ref = rhoS, compressibility = c)
    kr = BrooksCoreyRelativePermeabilities(sys, [2.0, 2.0])
    replace_variables!(model, PhaseMassDensities = ρ, RelativePermeabilities = kr);

    state0 = setup_reservoir_state(model, Pressure = 150*bar, Saturations = [1.0, 0.0])
    dt = repeat([30.0]*day, 12*10)
    dt = vcat([0.1, 1.0, 10.0], dt)
    inj_rate = Dx*Dx*Dz*0.3/sum(dt) # 1 PVI if average porosity is 0.3

    rate_target = TotalRateTarget(inj_rate)
    I_ctrl = InjectorControl(rate_target, [0.0, 1.0], density = rhoGS)
    bhp_target = BottomHolePressureTarget(50*bar)
    P_ctrl = ProducerControl(bhp_target)
    controls = Dict()
    controls[:Injector] = I_ctrl
    controls[:Producer] = P_ctrl
    forces = setup_reservoir_forces(model, control = controls)
    return simulate_reservoir(state0, model, dt, parameters = parameters, forces = forces, info_level = -1)
end
simulate_qfs (generic function with 2 methods)

Simulate base case

This will give the solution with uniform porosity of 0.3.

julia
ws, states, report_time = simulate_qfs();

Plot the solution of the base case

We observe a radial flow pattern initially, before coning occurs near the producer well once the fluid has reached the opposite corner. The uniform permeability and porosity gives axial symmetry at x=y.

julia
using GLMakie
to_2d(x) = reshape(vec(x), nx, nx)
get_sat(state) = to_2d(state[:Saturations][2, :])
nt = length(report_time)
fig = Figure()
h = nothing
ax = Axis(fig[1, 1])
h = contourf!(ax, get_sat(states[nt÷3]))
ax = Axis(fig[1, 2])
h = contourf!(ax, get_sat(states[nt]))
Colorbar(fig[1, end+1], h)
fig

Create 10 realizations

We create a small set of realizations of the same model, with porosity that is uniformly varying between 0.1 and 0.3. The main idea is to get significantly different flow patterns as the porosity and permeability changes, and we will return to more realistic porosity fields later in this example.

julia
function simulate_porosities(porosities)
    wellsols = []
    s = []
    report_step = Int(ceil(0.5*nt))
    for poro in porosities
        ws_i, states_i, rt = simulate_qfs(poro)
        push!(wellsols, ws_i)
        push!(s, get_sat(states_i[report_step]))
    end
    return (wellsols, s)
end
N = 10
porosities_uniform = []
for i in 1:N
    push!(porosities_uniform, 0.1 .+ 0.2*rand(Float64, (nx*nx)))
end
wells, saturations = simulate_porosities(porosities_uniform);

Plot the gas rate at the producer over the ensemble

julia
using Statistics
function plot_wells(wellsols)
    fig = Figure(size = (1000, 600))
    ax = Axis(
        fig[1, 1],
        xlabel = "Time [days]",
        ylabel = "Gas rate [m³/s]",
        title = "Producer gas rate",
    )
    t = wellsols[1].time./si_units(:day)
    avg_rate = zeros(length(t))
    for ws in wellsols
        q = abs.(ws[:Producer][:grat])
        avg_rate += q
        lines!(ax, t, q, color = :grey)
    end
    avg_rate ./= length(wellsols)
    lines!(ax, t, avg_rate, color = :red, linewidth = 3, label = "Mean")
    axislegend(ax, position = :lt)
    xlims!(ax, [0.5mean(t), t[end]])
    fig
end

plot_wells(wells)

Plot the average saturation over the ensemble

julia
avg = mean(saturations)
fig = Figure()
h = nothing
ax = Axis(fig[1, 1])
h = heatmap!(ax, avg, colorrange = (0.0, 1.0))
fig

Plot a few realizations of porosity and resulting gas saturation

Note that the porosity fields are uniformly random without any spatial correlation.

julia
function plot_realizations(sat, poro)
    fig = Figure(size = (1000, 400))
    poro_crange = (0.15, 0.25)
    sat_crange = (0.5, 1.0)
    h1 = h2 = nothing
    n_to_plot = 5
    for i in 1:n_to_plot
        ax = Axis(fig[1, i], title = "Gas saturation realization $i")
        h1 = heatmap!(ax, sat[i], colorrange = sat_crange)
        ax_poro = Axis(fig[2, i], title = "Porosity realization $i")
        h2 = heatmap!(ax_poro, to_2d(poro[i]), colorrange = poro_crange)
    end
    Colorbar(fig[1, n_to_plot+1], h1)
    Colorbar(fig[2, n_to_plot+1], h2)
    return fig
end
plot_realizations(saturations, porosities_uniform)

Use GeoStats.jl for more realistic porosity fields

Taking uniformly random samples is not a very realistic way to generate porosity fields. A more realistic approach is to use geostatistical methods from GeoStats.jl.

We here use a Gaussian process with a spherical covariance to generate the porosity fields. The setup is taken from the GeoStats.jl documentation which has more details on the approach.

julia
import GeoStats: CartesianGrid, GaussianProcess, GaussianVariogram, SphericalCovariance, viz!
N = 30
grid = CartesianGrid(nx, nx)
proc = GaussianProcess(SphericalCovariance(range=30.0), 0.0)
real = rand(proc, grid, N)
2D Ensemble
  domain:    50×50 CartesianGrid
  variables: field
  N° reals:  30

Plot mean and variance of the realizations

julia
m, v = mean(real), var(real)
fig = Figure(size = (800, 400))
axl = Axis(fig[1, 1], title = "Mean")
axr = Axis(fig[1, 2], title = "Variance")
viz!(axl, m.geometry, color = m.field)
viz!(axr, v.geometry, color = v.field)
fig

Run simulations with the new porosity fields

We here map the realizations to porosity values between 0.05 and 0.195, and run the simulations. Note that in a more realistic workflow we would condition the process on data instead of taking unconditional realizations.

julia
to_poro(x) = 0.2 + 0.1*clamp(x, -2.0, 2.0)/4.0
porosities_gaussian = map(i -> to_poro.(real[i].field), 1:N)
wells_gaussian, saturations_gaussian = simulate_porosities(porosities_gaussian);

Plot the producer rate over the ensemble

julia
plot_wells(wells_gaussian)

Plot a few realizations for the Gaussian porosity fields

We observe that the porosity fields now have spatial correlation.

julia
plot_realizations(saturations_gaussian, porosities_gaussian)

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 160.406525145 seconds to complete.

This page was generated using Literate.jl.