Skip to content

Model coarsening

Running a model at full resolution can be computationally expensive. In many cases, it is possible to coarsen the model to reduce the computational cost. This example demonstrates how to coarsen a model using JutulDarcy.jl. The example uses the Egg model, which is a small oil-water model with heterogeneous permeability. The model is coarsened using different methods and partition sizes, and the results are compared to the fine-scale model. The example demonstrates how to coarsen a model and simulate it using JutulDarcy.jl. The example also demonstrates how to compare the results of the coarse-scale model to the fine-scale model.

The example is intended to show the workflow of coarsening a model, and represents a starting point for more advanced techniques like upscaling, coarse-model calibration and history matching. The model is therefore intentionally simple and very coarse for quick simulations, and not necessarily accurate model responses.

julia
using Jutul, JutulDarcy, HYPRE
using GLMakie
using GeoEnergyIO
data_dir = GeoEnergyIO.test_input_file_path("EGG")
data_pth = joinpath(data_dir, "EGG.DATA")
fine_case = setup_case_from_data_file(data_pth);

Simulate the base case

We simulate the fine case to get a reference solution to compare against. We also extract the mesh and reservoir for plotting.

julia
fine_model = fine_case.model
fine_reservoir = reservoir_domain(fine_model)
fine_mesh = physical_representation(fine_reservoir)
ws, states = simulate_reservoir(fine_case, info_level = -1);

Coarsen the model and plot partition

We coarsen the model using a partition size of 20x20x2 and the IJK method where the underlying structure of the mesh is used to subdivide the blocks. This function automatically handles inactive cells and disconnected blocks and can therefore also work on more complex models.

We pass a triplet of integers to specify the partition size. This will give an essentially structured partition. Later on, we we will look at graph partitioners.

julia
coarse_case = coarsen_reservoir_case(fine_case, (20, 20, 2), method = :ijk)
coarse_model = coarse_case.model
coarse_reservoir = reservoir_domain(coarse_case)
coarse_mesh = physical_representation(coarse_reservoir)

p = coarse_mesh.partition

fig, = plot_cell_data(fine_mesh, p, colormap = :lipariS)
fig

Compare fine-scale and coarse-scale permeability

The fine-scale and coarse-scale permeability fields are compared visually. The coarsening uses a static upscaling, where the permeability is harmonically averaged per direction when coarsening. This is a simple method that can be effective enough for many cases.

The fine-scale permeability is shown on the left, and the coarse-scale is shown on the right, with the same color axis.

julia
K_f = fine_reservoir[:permeability][1, :]
K_c = coarse_reservoir[:permeability][1, :]

kcaxis = extrema(K_f)

fig = Figure(size = (1200, 500))
axf = Axis3(fig[1, 1], title = "Fine scale permeability", zreversed = true)
plot_cell_data!(axf, fine_mesh, K_f, colorrange = kcaxis, colormap = :turbo)

axc = Axis3(fig[1, 2], title = "Coarse scale permeability", zreversed = true)
plt = plot_cell_data!(axc, coarse_mesh, K_c, colorrange = kcaxis, colormap = :turbo)
Colorbar(fig[1, 3], plt)
fig

Simulate the coarse-scale model

The coarse scale model can be simulated just as the fine-scale model was, but the runtime should be significantly reduced down to around a second.

julia
@time ws_c, states_c = simulate_reservoir(coarse_case, info_level = -1);
  1.118194 seconds (3.76 M allocations: 314.147 MiB, 3.91% gc time, 3.51% compilation time: 100% of which was recompilation)

Plot and compare the coarse-scale and fine-scale solutions

We plot the pressure field for the fine-scale and coarse-scale models. The model has little pressure variation, but we see that there are substantial differences between our very coarse model and the original fine-scale.

julia
using Statistics
wells = JutulDarcy.get_model_wells(fine_model)

p_c = states_c[end][:Pressure]
p_f = states[end][:Pressure]

caxis = extrema([extrema(p_c)..., extrema(p_f)...])

fig = Figure(size = (1200, 500))
axf = Axis3(fig[1, 1], title = "Fine scale", zreversed = true)
plot_cell_data!(axf, fine_mesh, p_f, colorrange = caxis, colormap = :turbo)

for (k, w) in wells
    plot_well!(axf, fine_mesh, w, fontsize = 0)
end

axc = Axis3(fig[1, 2], title = "Coarse scale", zreversed = true)
plt = plot_cell_data!(axc, coarse_mesh, p_c, colorrange = caxis, colormap = :turbo)

for (k, w) in wells
    plot_well!(axc, fine_mesh, w, fontsize = 0)
end
Colorbar(fig[1, 3], plt)
fig

Plot and compare the saturation fields

We observe that the saturation fields are quite different between the coarse-scale and fine scale, with the coarse-scale model showing a more uniform saturation field as the leading shock is smeared away.

julia
s_c = states_c[end][:Saturations][1, :]
s_f = states[end][:Saturations][1, :]

scaxis = extrema([extrema(s_c)..., extrema(s_f)...])

fig = Figure(size = (1200, 500))
axf = Axis3(fig[1, 1], title = "Fine scale", zreversed = true)
plot_cell_data!(axf, fine_mesh, s_f, colorrange = scaxis, colormap = :turbo)

for (k, w) in wells
    plot_well!(axf, fine_mesh, w, fontsize = 0)
end

axc = Axis3(fig[1, 2], title = "Coarse scale", zreversed = true)
plt = plot_cell_data!(axc, coarse_mesh, s_c, colorrange = scaxis, colormap = :turbo)

for (k, w) in wells
    plot_well!(axc, fine_mesh, w, fontsize = 0)
end
Colorbar(fig[1, 3], plt)
fig

Plot the average field scale pressure evolution

julia
fig = Figure()
axf_p = Axis(fig[1, 1], ylabel = "Avg. pressure / bar")
lines!(axf_p, map(x -> mean(x[:Pressure])/1e5, states), label = "Fine")
lines!(axf_p, map(x -> mean(x[:Pressure])/1e5, states_c), label = "Coarse")
axislegend()
fig

Plot the wells interactively

We can plot the well results in the interactive viewer using the comparison feature that allows multiple results to be superimposed in the same figure.

julia
plot_well_results([ws, ws_c], names = ["Fine", "Coarse"], field = :orat, accumulated = true)

Plot field scale measurables over time interactively

The field-scale quantities match fairly well between the coarse-scale and fine-scale models. There is always a trade-off between accuracy and quality in numerical simulations, where the goal is to find the right balance between accuracy in quantities of interest and computational cost.

julia
fine_m = reservoir_measurables(fine_case, ws, states)
coarse_m = reservoir_measurables(coarse_case, ws_c, states_c)
m = copy(fine_m)
for (k, v) in pairs(coarse_m)
    if k != :time
        m[Symbol("coarse_$k")] = v
    end
end

plot_reservoir_measurables(m, left = :fopr, right = :coarse_fopr, accumulated = true)

Compare different partitioning methods

We have only so far tested a single partitioning method. We can quickly generate a few other coarse models using different partitioning methods and coarsening values. We highlight that we can also use the centroids instead of the IJK indices to partition, for when the mesh may not have a structured background mesh. In addition, we can call different graph partitioners by passing the desired number of blocks. Here, we call a simple METIS-based transmissibility coarsening, but the code contains options to use other weights and partitioners.

julia
partition_variants = [
    (:centroids, (3, 3, 2)),
    (:ijk, (5, 5, 1)),
    (:metis, 10),
    (:metis, 50)
]

fig = Figure(size = (1200, 600))
layout = GridLayout()
fig[1, 1] = layout
rowwidth = Int(floor(length(partition_variants)/2))
for (no, variant) in enumerate(partition_variants)
    if no > rowwidth
        row = 2
        pix = no - rowwidth
    else
        row = 1
        pix = no
    end
    cmethod, cdim = variant
    variant_case = coarsen_reservoir_case(fine_case, cdim, method = cmethod)
    r = reservoir_domain(variant_case)
    m = physical_representation(r)
    p = m.partition

    ax = Axis3(fig, title = "$cmethod - $cdim", azimuth = 0.3, elevation = 1.0, zreversed = true)
    plot_cell_data!(ax, fine_mesh, p, colormap = :lipariS)
    layout[row, 2*(pix-1) + 1] = ax

    _, variant_states = simulate_reservoir(variant_case, info_level = -1)
    pres = variant_states[end][:Pressure]
    axp = Axis3(fig, title = "Pressure", azimuth = 0.3, elevation = 1.0, zreversed = true)
    for (k, w) in wells
        plot_well!(axp, fine_mesh, w, fontsize = 0)
    end
    plot_cell_data!(axp, m, pres, colorrange = caxis, colormap = :turbo)

    layout[row, 2*(pix-1) + 2] = axp
end
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 54.347172281 seconds to complete.

This page was generated using Literate.jl.