Exercise 13.10
Full Field Optimization Report - Constrained Oil & Profit Allocation
Build a complete field optimization for 8 wells with realistic parameters. Include: optimal rate allocation with facility constraints, gas lift optimization, a surveillance dashboard for each well, and a monthly re-optimization schedule. Present the results as a report with tables and plots.
---
This is the capstone for the optimization chapter. You run a full-field allocation study on an 8-well Niger Delta field (OML framing), composing the chapter's linear-programming machinery into a single report. The plots and dashboards the book mentions are yours to enjoy, but the graded core is the two allocation totals: max-oil and max-profit.
The field has eight wells, each with a max oil rate, gas-oil ratio (GOR), water cut (WC), and operating cost. Three shared facilities cap the whole field: total liquid handling, gas processing, and water disposal. The constraint-coefficient structure is the verified one from the chapter: for each well the liquid load is 1/(1-WC), the gas load is GOR/1e6 (MMscf per STB of oil), and the water load is WC/(1-WC). Do not re-derive or "simplify" it.
The embedded constants (do not edit them):
WELL_NAMES = ['A-01','A-02','A-03','B-01','B-02','B-03','C-01','C-02']
MAX_OIL = np.array([1200.,800.,600.,1500.,900.,1100.,700.,1000.]) # STB/d
GOR = np.array([450.,1200.,300.,800.,600.,950.,350.,700.]) # scf/STB
WC = np.array([0.15,0.05,0.45,0.10,0.30,0.20,0.50,0.25]) # frac
OP_COST = np.array([12.,18.,8.,15.,10.,14.,7.,11.]) # $/STB
MAX_LIQUID = 6000.0 # bbl/d
MAX_GAS = 4.0 # MMscf/d
MAX_WATER = 700.0 # bbl/d
OIL_PRICE = 75.0 # $/bblYour tasks:
- Write the objective-agnostic allocation engine:
``python def field_allocate(objective_coeffs, max_oil, gor, wc, max_liquid, max_gas, max_water): """Maximize sum(objective_coeffs_i rate_i) subject to the three facility constraints and 0 <= rate_i <= max_oil_i. Return (rates, achieved_objective, total_oil).""" ` Build A_ub = np.array([1/(1-wc), gor/1e6, wc/(1-wc)]) and b_ub = np.array([max_liquid, max_gas, max_water]), negate the objective (c = -objective_coeffs) because linprog minimizes, set per-well bounds (0, max_oil_i), and solve with method='highs'. Return the optimal rates array, the achieved objective sum(objective_coeffs rates), and the total oil sum(rates)`.
- Write the report wrapper:
``python def field_report(max_oil, gor, wc, op_cost, price, max_liquid, max_gas, max_water): """Run two allocations and return a dict.""" ``
- max-oil allocation:
objective_coeffs = np.ones(...). - max-profit allocation:
objective_coeffs = price - op_cost(net dollars
per STB of oil for each well).
- Return a dict with keys:
'oil_rates', 'total_oil', 'profit_rates', 'total_profit', 'naive_oil' (= sum(max_oil)), and, for the max-oil case, 'gas_used', 'water_used', 'liquid_used' (recomputed from the oil allocation as sum(rate*gor/1e6), sum(rate*wc/(1-wc)), sum(rate/(1-wc))).
- At module scope, call the report and expose the three output variables the
tests read: ``python report = field_report(MAX_OIL, GOR, WC, OP_COST, OIL_PRICE, MAX_LIQUID, MAX_GAS, MAX_WATER) total_oil = report['total_oil'] total_profit = report['total_profit'] naive_oil = report['naive_oil'] ``
> Think about it: the naive plan (every well wide open) sums to 7,800 STB/d, > but the facilities can only carry about 4,613 STB/d; the constraints cost you > roughly 3,200 STB/d. Which facility actually binds here? (Check which of > gas_used / water_used / liquid_used sits exactly on its limit.) And why > does the max-profit plan land on the same total oil as the max-oil plan > for this particular field?
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
# ── Verified LP constraint structure from Chapter 13 (do not edit) ───────
# For an oil allocation x (STB/d per well), the chapter's facility loads are:
# liquid load per well = 1 / (1 - wc) -> total liquid = sum(x / (1-wc))
# gas load per well = gor / 1e6 -> total gas = sum(x * gor/1e6) [MMscf/d]
# water load per well = wc / (1 - wc) -> total water = sum(x * wc/(1-wc))
# linprog MINIMIZES, so to maximize an objective we pass c = -objective_coeffs.
# (These are the exact coefficients used in the chapter's max-oil allocation.)
# ── 8-well Niger Delta field constants (do not edit) ─────────────────────
WELL_NAMES = ['A-01', 'A-02', 'A-03', 'B-01', 'B-02', 'B-03', 'C-01', 'C-02']
MAX_OIL = np.array([1200., 800., 600., 1500., 900., 1100., 700., 1000.]) # STB/d
GOR = np.array([450., 1200., 300., 800., 600., 950., 350., 700.]) # scf/STB
WC = np.array([0.15, 0.05, 0.45, 0.10, 0.30, 0.20, 0.50, 0.25]) # frac
OP_COST = np.array([12., 18., 8., 15., 10., 14., 7., 11.]) # $/STB
MAX_LIQUID = 6000.0 # bbl/d liquid handling
MAX_GAS = 4.0 # MMscf/d gas processing
MAX_WATER = 700.0 # bbl/d water disposal
OIL_PRICE = 75.0 # $/bbl
def field_allocate(objective_coeffs, max_oil, gor, wc, max_liquid, max_gas, max_water):
"""Maximize sum(objective_coeffs_i * rate_i) subject to the three facility
constraints and 0 <= rate_i <= max_oil_i.
Return (rates, achieved_objective, total_oil).
"""
objective_coeffs = np.asarray(objective_coeffs, float)
# Verified Chapter-13 constraint-coefficient structure:
A_ub = np.array([1.0 / (1.0 - wc), gor / 1e6, wc / (1.0 - wc)])
b_ub = np.array([max_liquid, max_gas, max_water])
c = -objective_coeffs # linprog minimizes -> negate to maximize
bounds = [(0, m) for m in max_oil] # 0 <= rate_i <= max_oil_i
res = linprog(c, A_ub=A_ub, b_ub=b_ub, bounds=bounds, method='highs')
rates = res.x
achieved = float(np.dot(objective_coeffs, rates))
total_oil = float(np.sum(rates))
return rates, achieved, total_oil
def field_report(max_oil, gor, wc, op_cost, price, max_liquid, max_gas, max_water):
"""Run two allocations (max-oil and max-profit) and return a report dict.
Keys: 'oil_rates','total_oil','profit_rates','total_profit','naive_oil',
'gas_used','water_used','liquid_used' (facility usage for the max-oil case).
"""
n = len(max_oil)
# Allocation 1: maximize total oil (every well counts equally).
oil_rates, _, total_oil = field_allocate(
np.ones(n), max_oil, gor, wc, max_liquid, max_gas, max_water)
# Allocation 2: maximize profit (net $/STB = price - operating cost).
profit_coeffs = price - op_cost
profit_rates, total_profit, _ = field_allocate(
profit_coeffs, max_oil, gor, wc, max_liquid, max_gas, max_water)
naive_oil = float(np.sum(max_oil))
# Facility usage recomputed from the max-oil allocation.
gas_used = float(np.sum(oil_rates * gor / 1e6))
water_used = float(np.sum(oil_rates * wc / (1.0 - wc)))
liquid_used = float(np.sum(oil_rates / (1.0 - wc)))
return dict(
oil_rates=oil_rates,
total_oil=total_oil,
profit_rates=profit_rates,
total_profit=total_profit,
naive_oil=naive_oil,
gas_used=gas_used,
water_used=water_used,
liquid_used=liquid_used,
)
report = field_report(MAX_OIL, GOR, WC, OP_COST, OIL_PRICE,
MAX_LIQUID, MAX_GAS, MAX_WATER)
total_oil = report['total_oil']
total_profit = report['total_profit']
naive_oil = report['naive_oil']
print("FULL FIELD OPTIMIZATION REPORT")
print(f" Naive total (all wells wide open): {naive_oil:,.0f} STB/d")
print(f" Constrained max-oil total: {total_oil:,.1f} STB/d")
print(f" Constrained max-profit: ${total_profit:,.0f}/d")
print(f" Facility usage (max-oil): liquid {report['liquid_used']:,.0f} / {MAX_LIQUID:,.0f},"
f" gas {report['gas_used']:.3f} / {MAX_GAS}, water {report['water_used']:,.0f} / {MAX_WATER:,.0f}")
lockCopying code is a Full Access feature.