Exerciseschevron_rightChapter 13chevron_right13.8
fitness_center

Exercise 13.8

Decline-Aware Allocation - 12-Month Re-Optimization

Level 3
Chapter 13: Production Optimization
descriptionProblem

Production allocation should account for the fact that wells decline at different rates. Modify the allocation model so that each well's maximum rate decreases monthly according to its own Arps decline parameters. Re-optimize the allocation every month for a 12-month period and track how the optimal allocation shifts over time.

---

We'll work this on a 4-well OML block. The facility limits never change, but every well declines on its own clock: each well's deliverability falls month to month according to its own Arps exponential decline. So the allocation you solved last month is already stale this month. You must re-optimize every month for a full year and watch the optimal split drift.

The decline is plain Arps exponential, stated in the book exercise: a well's maximum rate month periods from now is qi exp(-dm month), with month = 0 being the initial rate. The single-month allocation is exactly the chapter's LP: maximize total oil subject to three facility constraints (liquid 1/(1-wc), gas gor/1e6, water wc/(1-wc)) with per-well bounds 0 <= rate <= qmax.

The 4-well field and facility constants are embedded for you. The wells are:

Wellqi (STB/d)dm (1/mo)GOR (scf/STB)WC (frac)
115000.025000.10
210000.059000.25
312000.014000.40
48000.0811000.05

Facility limits: MAX_LIQUID = 4000 bbl/d, MAX_GAS = 2.5 MMscf/d, MAX_WATER = 600 bbl/d. Run N_MONTHS = 12.

Write three functions:

  1. qmax_at_month(qi, dm, month): Arps exponential, element-wise:

returns qi np.exp(-dm month) (so month = 0 returns the initial qi).

  1. optimize_month(qi, dm, gor, wc, month, max_liquid, max_gas, max_water):

compute that month's per-well max with qmax_at_month, then run the chapter's LP (scipy.optimize.linprog, method='highs') to maximize total oil under the three facility constraints, with bounds 0 <= rate <= qmax. Return the tuple (rates, total_oil) where rates is the per-well array and total_oil = float(rates.sum()).

  1. schedule(qi, dm, gor, wc, n_months, max_liquid, max_gas, max_water):

call optimize_month for month = 0, 1, ..., n_months-1 and return a numpy array of length n_months holding each month's total_oil.

Then build the module-scope outputs the tests read:

monthly_totals = schedule(QI, DM, GOR, WC, N_MONTHS, MAX_LIQUID, MAX_GAS, MAX_WATER)
total_month0  = float(monthly_totals[0])
total_month11 = float(monthly_totals[11])

> Think about it: the field's optimal oil declines every single month; the > wells lose deliverability and the allocation can only divide a shrinking pie, > never recover lost capacity. If you switched off decline (dm = 0 for every > well), what would the 12-month schedule look like, 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


# ── 4-well OML block: per-well decline + facility limits (do not edit) ────
QI = np.array([1500.0, 1000.0, 1200.0, 800.0])   # initial max rate, STB/d
DM = np.array([0.02, 0.05, 0.01, 0.08])           # monthly nominal exponential decline, 1/mo
GOR = np.array([500.0, 900.0, 400.0, 1100.0])     # gas-oil ratio, scf/STB
WC  = np.array([0.10, 0.25, 0.40, 0.05])          # water-cut, fraction
MAX_LIQUID = 4000.0   # liquid-handling limit, bbl/d
MAX_GAS = 2.5         # gas-handling limit, MMscf/d
MAX_WATER = 600.0     # water-handling limit, bbl/d
N_MONTHS = 12         # planning horizon


def qmax_at_month(qi, dm, month):
    """Arps exponential decline, element-wise: qi * exp(-dm * month).

    month = 0 returns the initial rate qi.
    """
    return qi * np.exp(-dm * month)


def optimize_month(qi, dm, gor, wc, month, max_liquid, max_gas, max_water):
    """Single-month allocation LP for the given decline month.

    Returns (rates, total_oil) with total_oil = float(rates.sum()).
    """
    qmax = qmax_at_month(qi, dm, month)
    n = len(qi)
    c = -np.ones(n)                       # linprog minimizes -> negate to maximize oil
    liquid_coeffs = 1.0 / (1.0 - wc)
    gas_coeffs = gor / 1e6
    water_coeffs = wc / (1.0 - wc)
    A_ub = np.array([liquid_coeffs, gas_coeffs, water_coeffs])
    b_ub = np.array([max_liquid, max_gas, max_water])
    bounds = [(0, m) for m in qmax]
    result = linprog(c, A_ub=A_ub, b_ub=b_ub, bounds=bounds, method='highs')
    rates = result.x
    total_oil = float(rates.sum())
    return rates, total_oil


def schedule(qi, dm, gor, wc, n_months, max_liquid, max_gas, max_water):
    """Re-optimize the allocation each month for n_months.

    Returns a numpy array of length n_months whose entry m is the optimal
    total oil rate for decline month m (m = 0, 1, ..., n_months - 1).
    """
    totals = np.zeros(n_months)
    for m in range(n_months):
        _, total_oil = optimize_month(qi, dm, gor, wc, m,
                                      max_liquid, max_gas, max_water)
        totals[m] = total_oil
    return totals


monthly_totals = schedule(QI, DM, GOR, WC, N_MONTHS, MAX_LIQUID, MAX_GAS, MAX_WATER)
total_month0  = float(monthly_totals[0])
total_month11 = float(monthly_totals[11])

print("monthly_totals (STB/d):", np.round(monthly_totals, 1))
print("total_month0:", total_month0, "  total_month11:", total_month11)

lockCopying code is a Full Access feature.