Exercise 13.2
Adding a Gas Constraint - When Gas Handling Bites Before the Pipeline
Extend Exercise 13.1 with GORs of 600, 1,500, and 400 scf/STB and a gas handling limit of 2.5 MMscf/day. How does the optimal allocation change?
---
In Exercise 13.1 three OML wells fed a single pipeline capped at 2,500 STB/d and the answer was simply "fill the pipeline." Now the facility also has a gas-handling limit, and that changes the character of the problem: a barrel of oil from a gas-heavy well costs more gas capacity than a barrel from a gas-lean well, so the optimizer must trade off oil against gas.
The three wells are: maximum oil rates MAX_OIL = [1000, 800, 1500] STB/d, with gas-oil ratios GOR = [600, 1500, 400] scf/STB. There is no water cut here, so liquid equals oil and the pipeline cap is PIPELINE_BPD = 2500.0 STB/d. The book's gas-handling limit is GAS_MAX_MMSCFD = 2.5 MMscf/d.
Write a function that solves the allocation as a linear program for any gas limit, so you can probe when the gas constraint actually bites:
def allocate_with_gas(max_oil, gor, pipeline, gas_max):
"""Maximize total oil subject to:
sum(rate) <= pipeline (liquid, STB/d)
sum(rate_i * gor_i) / 1e6 <= gas_max (MMscf/d)
0 <= rate_i <= max_oil_i
Return (rates, total_oil) where total_oil = float(sum(rates))."""Build it with scipy.optimize.linprog:
- Objective: maximize total oil = minimize
c = -np.ones(n)(linprog minimizes). - Inequality matrix
A_ub = np.array([np.ones(n), gor / 1e6])with right-hand
side b_ub = [pipeline, gas_max]: row 1 is the liquid cap, row 2 converts rate * GOR (scf/d) to MMscf/d and caps it at gas_max.
bounds = [(0, m) for m in max_oil].method="highs". Read the rates fromresult.x;total_oil = float(sum(rates)).
Then produce these module-scope variables the tests will read:
rates_book, total_oil_book = allocate_with_gas(MAX_OIL, GOR, PIPELINE_BPD, GAS_MAX_MMSCFD)
rates_tight, total_oil_tight = allocate_with_gas(MAX_OIL, GOR, PIPELINE_BPD, 0.8)> Think about it: at gas_max = 2.5 the optimal pipeline-filling allocation > only burns about 1.92 MMscf/d of gas, so the gas limit is slack and the > answer is still 2500 STB/d, exactly Exercise 13.1. Drop the gas cap to > 0.8 MMscf/d and the constraint binds: the optimizer keeps the gas-lean > well (GOR 400) wide open and chokes back the rest, landing below the pipeline > limit. Which well does linear programming fill first when gas, not liquid, is > the bottleneck, and why?
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 linprog
# ── OML three-well allocation constants (do not edit) ────────────────────
MAX_OIL = np.array([1000.0, 800.0, 1500.0]) # max oil rate per well, STB/d
GOR = np.array([600.0, 1500.0, 400.0]) # gas-oil ratio per well, scf/STB
PIPELINE_BPD = 2500.0 # liquid (= oil here, no water cut) cap, STB/d
GAS_MAX_MMSCFD = 2.5 # book gas-handling limit, MMscf/d
def allocate_with_gas(max_oil, gor, pipeline, gas_max):
"""Maximize total oil subject to:
sum(rate) <= pipeline (liquid, STB/d)
sum(rate_i * gor_i) / 1e6 <= gas_max (MMscf/d)
0 <= rate_i <= max_oil_i
Return (rates, total_oil) where total_oil = float(sum(rates)).
"""
max_oil = np.asarray(max_oil, float)
gor = np.asarray(gor, float)
n = len(max_oil)
c = -np.ones(n) # maximize oil = minimize -oil
A_ub = np.array([np.ones(n), gor / 1e6]) # row 0: liquid, row 1: gas (MMscf/d)
b_ub = np.array([pipeline, gas_max])
bounds = [(0.0, m) for m in max_oil]
result = linprog(c, A_ub=A_ub, b_ub=b_ub, bounds=bounds, method="highs")
rates = result.x
total_oil = float(np.sum(rates))
return rates, total_oil
rates_book, total_oil_book = allocate_with_gas(MAX_OIL, GOR, PIPELINE_BPD, GAS_MAX_MMSCFD)
rates_tight, total_oil_tight = allocate_with_gas(MAX_OIL, GOR, PIPELINE_BPD, 0.8)
print("book gas cap 2.5 MMscf/d -> total oil (STB/d):", total_oil_book)
print(" rates:", np.round(rates_book, 1),
" gas used (MMscf/d):", round(float((rates_book * GOR).sum() / 1e6), 3))
print("tight gas cap 0.8 MMscf/d -> total oil (STB/d):", total_oil_tight)
print(" rates:", np.round(rates_tight, 1),
" gas used (MMscf/d):", round(float((rates_tight * GOR).sum() / 1e6), 3))
lockCopying code is a Full Access feature.