Gradient-based optimization of net present value (NPV)
One usage of a differentiable simulator is control optimization. A counterpart to history matching, control optimization is the process of finding the optimal controls for a given objective and simulation model. In this example, we will optimize the injection rates for a simplified version of the EGG model to maximize the net present value (NPV) of the reservoir.
Setting up a coarse model
We start off by loading the Egg model and coarsening it to a 20x20x3 grid. We limit the optimization to the first 50 timesteps to speed up the optimization process. If you want to run the optimization for all timesteps on the fine model directly, you can remove the slicing of the case and replace the coarse case with the fine case.
using Jutul, JutulDarcy, GLMakie, GeoEnergyIO, HYPRE
data_dir = GeoEnergyIO.test_input_file_path("EGG")
data_pth = joinpath(data_dir, "EGG.DATA")
fine_case = setup_case_from_data_file(data_pth)
fine_case = fine_case[1:50];
coarse_case = coarsen_reservoir_case(fine_case, (20, 20, 3), method = :ijk);
Set up the rate optimization
We use an utility to set up the rate optimization problem. The utility sets up the objective function, constraints, and initial guess for the optimization. The optimization is set up to maximize the NPV of the reservoir. The contribution to the NPV for a given time
Here,
We set the prices and costs (per barrel) as well as a discount rate of 5% per year. The base rate will be used for all injectors initially, which matches the base case for the Egg model. The utility function takes in a steps
argument that can be used to set how often the rates are allowed to change.
The values used here are arbitrary for the purposes of the example. You are encouraged to play around with the values to see how the outcome changes in the optimization. Generally higher discount rates prioritze immediate income, while lower or zero discount rates prioritize maximizing the oil production over the entire simulation period.
ctrl = coarse_case.forces[1][:Facility].control
base_rate = ctrl[:INJECT1].target.value
function optimize_rates(steps)
setup = JutulDarcy.setup_rate_optimization_objective(coarse_case, base_rate,
max_rate_factor = 10,
oil_price = 100.0,
water_price = -10.0,
water_cost = 5.0,
discount_rate = 0.05,
steps = steps
)
obj_best, x_best, hist = Jutul.unit_box_bfgs(setup.x0,
setup.obj,
maximize = true,
lin_eq = setup.lin_eq
)
return (setup.case, hist.val, x_best)
end
optimize_rates (generic function with 1 method)
Optimize the rates
We optimize the rates for two different strategies. The first strategy is to optimize constant rates for the entire period. The second strategy is to optimize the rates for each report step.
Optimize with a single set of rates
case1, hist1, x1 = optimize_rates(:first);
Rate optimization: steps=:first selected, optimizing one set of controls for all steps.
Rate optimization: 8 injectors and 4 producers selected.
It: 0 | val: 2.131e+08 | ls-its: NaN | pgrad: 8.044e+12
It: 1 | val: 2.136e+08 | ls-its: 1 | pgrad: 2.438e+11
It: 2 | val: 2.144e+08 | ls-its: 2 | pgrad: 2.221e+11
LBFGS: Line search unable to succeed in 5 iterations ...
It: 3 | val: 2.155e+08 | ls-its: 5 | pgrad: 2.268e+11
It: 4 | val: 2.162e+08 | ls-its: 1 | pgrad: 1.855e+11
It: 5 | val: 2.164e+08 | ls-its: 1 | pgrad: 5.402e+10
LBFGS: Line search unable to succeed in 5 iterations ...
LBFGS: Hessian not updated during iteration 6
It: 6 | val: 2.171e+08 | ls-its: 5 | pgrad: 5.614e+10
LBFGS: Line search unable to succeed in 5 iterations ...
It: 7 | val: 2.171e+08 | ls-its: 5 | pgrad: 7.844e+10
LBFGS: Line search unable to succeed in 5 iterations ...
LBFGS: Hessian not updated during iteration 8
It: 8 | val: 2.171e+08 | ls-its: 5 | pgrad: 7.436e+10
Plot rate allocation for the constant case
fig = Figure()
ax = Axis(fig[1, 1], xlabel = "Injector number", ylabel = "Rate fraction (of max injection rate)")
barplot!(ax, x1)
ax.xticks = eachindex(x1)
fig
Optimize with varying rates per time-step
case2, hist2, x2 = optimize_rates(:each);
Rate optimization: steps=:each selected, optimizing controls for all 50 steps separately.
Rate optimization: 8 injectors and 4 producers selected.
It: 0 | val: 2.131e+08 | ls-its: NaN | pgrad: 1.329e+12
LBFGS: Line search unable to succeed in 5 iterations ...
It: 1 | val: 2.133e+08 | ls-its: 5 | pgrad: 1.962e+10
LBFGS: Line search unable to succeed in 5 iterations ...
It: 2 | val: 2.152e+08 | ls-its: 5 | pgrad: 1.968e+10
LBFGS: Line search at max step size, Wolfe conditions not satisfied for this step
It: 3 | val: 2.165e+08 | ls-its: 1 | pgrad: 3.037e+10
LBFGS: Line search at max step size, Wolfe conditions not satisfied for this step
It: 4 | val: 2.179e+08 | ls-its: 1 | pgrad: 6.590e+10
LBFGS: Line search at max step size, Wolfe conditions not satisfied for this step
It: 5 | val: 2.185e+08 | ls-its: 1 | pgrad: 6.455e+10
LBFGS: Line search at max step size, Wolfe conditions not satisfied for this step
LBFGS: Hessian not updated during iteration 6
It: 6 | val: 2.189e+08 | ls-its: 1 | pgrad: 3.060e+10
LBFGS: Line search at max step size, Wolfe conditions not satisfied for this step
LBFGS: Hessian not updated during iteration 7
It: 7 | val: 2.193e+08 | ls-its: 2 | pgrad: 3.168e+10
LBFGS: Line search at max step size, Wolfe conditions not satisfied for this step
It: 8 | val: 2.195e+08 | ls-its: 1 | pgrad: 3.325e+10
LBFGS: Line search at max step size, Wolfe conditions not satisfied for this step
It: 9 | val: 2.198e+08 | ls-its: 1 | pgrad: 1.751e+10
LBFGS: Line search at max step size, Wolfe conditions not satisfied for this step
It: 10 | val: 2.200e+08 | ls-its: 1 | pgrad: 6.951e+10
LBFGS: Line search unable to succeed in 5 iterations ...
It: 11 | val: 2.200e+08 | ls-its: 5 | pgrad: 6.611e+10
LBFGS: Line search unable to succeed in 5 iterations ...
LBFGS: Hessian not updated during iteration 12
It: 12 | val: 2.200e+08 | ls-its: 5 | pgrad: 6.609e+10
Plot rate allocation per well
allocs = reshape(x2, length(x1), :)
fig = Figure()
ax = Axis(fig[1, 1], xlabel = "Report step", ylabel = "Rate fraction (of max injection rate)")
for i in axes(allocs, 1)
lines!(ax, allocs[i, :], label = "Injector #$i")
end
axislegend()
fig
Plot the evolution of the NPV
We plot the evolution of the NPV for the two strategies to compare the results. Note that the optimization produces a higher NPV for the varying rates. This is expected, as the optimization can adjust the rates to the changes in the mobility field during the progress of the simulation.
fig = Figure()
ax = Axis(fig[1, 1], xlabel = "LBFGS iteration", ylabel = "Net present value (million USD)")
scatter!(ax, 1:length(hist1), hist1./1e6, label = "Constant rates")
scatter!(ax, 1:length(hist2), hist2./1e6, marker = :x, label = "Varying rates")
axislegend(position = :rb)
fig
Simulate the results
We finally simulate all the cases to compare the results.
ws0, states0 = simulate_reservoir(coarse_case, info_level = -1)
ws1, states1 = simulate_reservoir(case1, info_level = -1)
ws2, states2 = simulate_reservoir(case2, info_level = -1)
ReservoirSimResult with 50 entries:
wells (12 present):
:PROD4
:INJECT3
:PROD2
:PROD3
:INJECT5
:INJECT4
:INJECT8
:PROD1
:INJECT6
:INJECT1
:INJECT2
:INJECT7
Results per well:
:lrat => Vector{Float64} of size (50,)
:wrat => Vector{Float64} of size (50,)
:Aqueous_mass_rate => Vector{Float64} of size (50,)
:orat => Vector{Float64} of size (50,)
:control => Vector{Symbol} of size (50,)
:bhp => Vector{Float64} of size (50,)
:Liquid_mass_rate => Vector{Float64} of size (50,)
:wcut => Vector{Float64} of size (50,)
:mass_rate => Vector{Float64} of size (50,)
:rate => Vector{Float64} of size (50,)
states (Vector with 50 entries, reservoir variables for each state)
:Saturations => Matrix{Float64} of size (2, 975)
:Pressure => Vector{Float64} of size (975,)
:TotalMasses => Matrix{Float64} of size (2, 975)
time (report time for each state)
Vector{Float64} of length 50
result (extended states, reports)
SimResult with 50 entries
extra
Dict{Any, Any} with keys :simulator, :config
Completed at Mar. 30 2025 16:03 after 805 milliseconds, 184 microseconds, 748 nanoseconds.
Compute measurables and compare
NPV favors early production, as later income/costs have less relative value. We compute the field measurables to be able to compare the oil and water production.
f0 = reservoir_measurables(coarse_case.model, ws0)
f1 = reservoir_measurables(case1.model, ws1)
f2 = reservoir_measurables(case2.model, ws2)
Dict{Symbol, Any} with 16 entries:
:fwit => (values = [636.0, 3180.0, 6341.01, 12701.0, 19061.0, 25421.0, 31781.…
:fwpt => (values = [0.150372, 0.150372, 0.150372, 0.150372, 0.150372, 0.15037…
:flpr => (values = [0.00721402, 0.00735963, 0.00731461, 0.00736045, 0.0073611…
:time => [86400.0, 432000.0, 864000.0, 1.728e6, 2.592e6, 3.456e6, 4.32e6, 5.1…
:fwct => (values = [0.000241254, 2.7453e-12, 4.75662e-18, 2.33518e-21, 1.9949…
:fopr => (values = [0.00721228, 0.00735963, 0.00731461, 0.00736045, 0.0073611…
:fwir => (values = [0.00736111, 0.00736111, 0.00731716, 0.00736111, 0.0073611…
:foir => (values = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 … 0.0,…
:fgpr => (values = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 … 0.0,…
:fgir => (values = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 … 0.0,…
:fwpr => (values = [1.74041e-6, 2.02044e-14, 3.47928e-20, 1.7188e-23, 1.46854…
:fgit => (values = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 … 0.0,…
:fgor => (values = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 … 0.0,…
:fopt => (values = [623.141, 3166.63, 6326.54, 12686.0, 19046.0, 25405.3, 317…
:foit => (values = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 … 0.0,…
:fgpt => (values = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 … 0.0,…
Plot the cumulative field oil production
The optimized cases produce more oil than the base case.
bbl = si_unit(:stb)
fig = Figure()
ax = Axis(fig[1, 1], xlabel = "Time / days", ylabel = "Field oil production (accumulated, barrels)", title = "Base case")
t = ws0.time./si_unit(:day)
lines!(ax, t, f0[:fopt].values./bbl, label = "Base case")
lines!(ax, t, f1[:fopt].values./bbl, label = "Constant rates")
lines!(ax, t, f2[:fopt].values./bbl, label = "Varying rates")
axislegend(position = :rb)
fig
Plot the cumulative field water production
The optimized cases produce less water than the base case.
fig = Figure()
ax = Axis(fig[1, 1], xlabel = "Time / days", ylabel = "Field water production (accumulated, barrels)", title = "Base case")
t = ws0.time./si_unit(:day)
lines!(ax, t, f0[:fwpt].values./bbl, label = "Base case")
lines!(ax, t, f1[:fwpt].values./bbl, label = "Constant rates")
lines!(ax, t, f2[:fwpt].values./bbl, label = "Varying rates")
axislegend(position = :rb)
fig
Plot the differences in water saturation
The optimized cases have a different water saturation distribution compared to the base case.
reservoir = reservoir_domain(coarse_case.model)
g = physical_representation(reservoir)
function plot_diff!(ax, s)
plt = plot_cell_data!(ax, g, s0 - s1, colorrange = (-0.5, 0.5), colormap = :balance)
for (w, wd) in get_model_wells(coarse_case.model)
plot_well!(ax, g, wd, top_factor = 0.8, fontsize = 10)
end
ax.elevation[] = 1.03
ax.azimuth[] = 3.75
return plt
end
s0 = states0[end][:Saturations][1, :]
s1 = states1[end][:Saturations][1, :]
s2 = states2[end][:Saturations][1, :]
fig = Figure(size = (1600, 800))
ax = Axis3(fig[1, 1], zreversed = true, title = "Water saturation difference, constant rates")
plt = plot_diff!(ax, s1)
ax = Axis3(fig[1, 2], zreversed = true, title = "Water saturation difference, varying rates")
plot_diff!(ax, s2)
Colorbar(fig[2, :], plt, vertical = false)
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 110.794130326 seconds to complete.
This page was generated using Literate.jl.