Exerciseschevron_rightChapter 13chevron_right13.2
fitness_center

Exercise 13.2

Adding a Gas Constraint - When Gas Handling Bites Before the Pipeline

Level 2
Chapter 13: Production Optimization
descriptionProblem

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 from result.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?

lightbulbHints (0/3)

Stuck? Reveal hints one at a time — they progress from nudge to near-solution.

codeYour solution
main.py
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.