Exercise 13.5
Multi-Well Gas Lift - Constrained Compressor Allocation
Six wells compete for 3,500 Mscf/d of lift gas. Define response curves for each well and find the optimal distribution. Compare the optimized total against equal distribution and report the percentage improvement.
---
Six wells on an OML-58 manifold all want lift gas, but one compressor caps the field at 3,500 Mscf/d, and no single well can take more than 1,500 Mscf/d. Each well responds to gas with diminishing returns following the verified chapter curve gas_lift_response(q_base, gas_inj, a, b) = q_base + a*gas_inj/(b+gas_inj). The job is to split the compressor budget so the total field oil is as high as possible, and to prove that doing the optimization actually beats just handing every well an equal slice.
The verified gas_lift_response function and the six-well parameters are embedded for you. Do not modify them or re-derive the curve. The constants are:
WELL_NAMES = ['GL-01','GL-02','GL-03','GL-04','GL-05','GL-06']
Q_BASE = np.array([400.0, 200.0, 600.0, 350.0, 500.0, 300.0]) # natural rate, STB/d
A_GAIN = np.array([800.0, 500.0, 400.0, 700.0, 900.0, 600.0]) # max incremental gain, STB/d
B_HALF = np.array([300.0, 200.0, 500.0, 250.0, 400.0, 350.0]) # half-response gas, Mscf/d
TOTAL_GAS = 3500.0 # compressor capacity, Mscf/d
WELL_GAS_CAP = 1500.0 # per-well injection limit, Mscf/dYour task: write optimize_gas_lift(q_base, a, b, total_gas, well_cap):
- Use
scipy.optimize.minimize(method='SLSQP')to **minimize the negative total
oil** (i.e. maximize total oil).
- Constraint:
sum(g) <= total_gas(an inequality constraint). - Bounds:
0 <= g_i <= well_capfor every well. - Initial guess
x0: an equal split,total_gas / nper well. - Total oil at any allocation is
sum(gas_lift_response(q_base_i, g_i, a_i, b_i)) over the wells.
- Return the 4-tuple
(gas_alloc, total_opt_oil, total_equal_oil, pct_improvement): gas_alloc: the optimal per-well gas array (Mscf/d).total_opt_oil: field oil at the optimum (STB/d).total_equal_oil: field oil under a flat equal splittotal_gas / nper well.pct_improvement = (total_opt_oil - total_equal_oil) / total_equal_oil * 100.
Then call it once and unpack into the EXACT output names the tests read:
gas_alloc, total_opt_oil, total_equal_oil, pct_improvement = \
optimize_gas_lift(Q_BASE, A_GAIN, B_HALF, TOTAL_GAS, WELL_GAS_CAP)> Think about it: at the optimum the marginal oil gain per extra Mscf, > a_i*b_i/(b_i+g_i)**2, is the SAME for every well that isn't pinned at a bound; > that common value is the shadow price of compressor gas. Why does moving gas from > a low-marginal well to a high-marginal one always raise the total, and why does > that stop exactly when the marginals equalize?
Stuck? Reveal hints one at a time — they progress from nudge to near-solution.
visibilityReveal reference solutionexpand_more
Try solving it yourself first — the hints walk you through it. The solution below is one valid approach; yours may differ and still be correct.
import numpy as np
from scipy.optimize import minimize
# ── Verified gas-lift performance curve (do not edit) ────────────────────
def gas_lift_response(q_base, gas_inj, a, b):
"""
Gas lift performance curve.
Returns the total oil rate as a function of gas injection rate.
The response follows a diminishing-returns curve:
q_oil = q_base + a * gas_inj / (b + gas_inj)
Parameters
----------
q_base : float
Natural flow rate without gas lift (STB/day).
gas_inj : float
Gas injection rate (Mscf/day).
a : float
Maximum incremental oil gain (STB/day).
b : float
Gas rate at half-maximum response (Mscf/day).
"""
return q_base + a * gas_inj / (b + gas_inj)
# ── OML-58 six-well manifold constants (do not edit) ─────────────────────
WELL_NAMES = ['GL-01', 'GL-02', 'GL-03', 'GL-04', 'GL-05', 'GL-06']
Q_BASE = np.array([400.0, 200.0, 600.0, 350.0, 500.0, 300.0]) # natural rate, STB/d
A_GAIN = np.array([800.0, 500.0, 400.0, 700.0, 900.0, 600.0]) # max incremental gain, STB/d
B_HALF = np.array([300.0, 200.0, 500.0, 250.0, 400.0, 350.0]) # half-response gas, Mscf/d
TOTAL_GAS = 3500.0 # compressor capacity, Mscf/d
WELL_GAS_CAP = 1500.0 # per-well injection limit, Mscf/d
def optimize_gas_lift(q_base, a, b, total_gas, well_cap):
"""Distribute total_gas across wells to MAXIMIZE total oil.
Use scipy.optimize.minimize(method='SLSQP') minimizing negative total oil,
with constraint sum(g) <= total_gas, bounds 0 <= g_i <= well_cap, and
x0 = equal split (total_gas / n).
Returns (gas_alloc, total_opt_oil, total_equal_oil, pct_improvement):
gas_alloc = optimal per-well gas array (Mscf/d)
total_opt_oil = field oil at the optimum (STB/d)
total_equal_oil = field oil under an equal split total_gas/n per well (STB/d)
pct_improvement = (total_opt_oil - total_equal_oil) / total_equal_oil * 100
"""
q_base = np.asarray(q_base, float)
a = np.asarray(a, float)
b = np.asarray(b, float)
n = len(q_base)
def neg_total_oil(g):
return -np.sum(gas_lift_response(q_base, g, a, b))
constraints = [{"type": "ineq", "fun": lambda x: total_gas - np.sum(x)}]
bounds = [(0.0, well_cap) for _ in range(n)]
x0 = np.full(n, total_gas / n)
res = minimize(neg_total_oil, x0, method="SLSQP",
bounds=bounds, constraints=constraints)
gas_alloc = res.x
total_opt_oil = float(np.sum(gas_lift_response(q_base, gas_alloc, a, b)))
equal = np.full(n, total_gas / n)
total_equal_oil = float(np.sum(gas_lift_response(q_base, equal, a, b)))
pct_improvement = (total_opt_oil - total_equal_oil) / total_equal_oil * 100.0
return gas_alloc, total_opt_oil, total_equal_oil, pct_improvement
gas_alloc, total_opt_oil, total_equal_oil, pct_improvement = \
optimize_gas_lift(Q_BASE, A_GAIN, B_HALF, TOTAL_GAS, WELL_GAS_CAP)
print("optimal gas alloc (Mscf/d):", np.round(gas_alloc, 1))
print("total optimized oil (STB/d):", round(total_opt_oil, 1))
print("total equal-split oil (STB/d):", round(total_equal_oil, 1))
print("improvement (%):", round(pct_improvement, 3))
lockCopying code is a Full Access feature.