Exerciseschevron_rightChapter 9chevron_right9.1
fitness_center

Exercise 9.1

Exponential vs Hyperbolic

Level 2
Chapter 9: Decline Curve Analysis
descriptionProblem

A well has the following monthly production data (bbl/d): 1800, 1650, 1520, 1400, 1295, 1200, 1110, 1030, 958, 892, 832, 778. Fit both exponential and hyperbolic models. Which fits better (lower RMSE)? Calculate the EUR for each model assuming an abandonment rate of 25 bbl/d.

---

Well OD-001 on OML 58 gave you its first twelve months of oil rate (bopd), one reading per month at t = 0, 1, ..., 11:

Q12 = [1800, 1650, 1520, 1400, 1295, 1200, 1110, 1030, 958, 892, 832, 778]

You will fit two Arps decline models to this history, decide which one the data prefers, and turn each fit into an EUR (Estimated Ultimate Recovery) at an abandonment rate of q_abandon_bopd = 25 bopd.

Rates are in bopd (bbl/day) and time t (and the decline Di) is in months, so the embedded EUR integrals come out in bopd·months. Multiply each EUR by DAYS_PER_MONTH = 30.44 to report it as a volume in bbl. That mirrors the book's own convention.

The Arps rate laws and the closed-form EUR integrals are already embedded for you; do not re-derive them. Write four functions:

  • fit_exponential(t, q) -> (qi_bopd, di_per_month): fit

arps_exponential with curve_fit, starting guess p0=[2000, 0.1].

  • fit_hyperbolic(t, q) -> (qi_bopd, di_per_month, b_factor): fit

arps_hyperbolic with curve_fit, p0=[2000, 0.1, 0.5] and bounds=([0, 0, 1e-6], [1e5, 5, 1]) so the b-factor stays in (0, 1).

  • rmse(model_q, q) -> float: root-mean-square error between a model's

rate vector and the observed rates.

  • compare(t, q, q_abandon_bopd=25) -> dict: fit both models, then return
keymeaning
rmse_expRMSE of the exponential fit against q
rmse_hypRMSE of the hyperbolic fit against q
eur_exp_bblEUR from the exponential fit, in bbl (eur_exponential × DAYS_PER_MONTH)
eur_hyp_bblEUR from the hyperbolic fit, in bbl (eur_hyperbolic × DAYS_PER_MONTH)

Then call compare(T, Q12) on the OD-001 history and store the dict in result.

> Think about it: the hyperbolic model has one extra free parameter > (b_factor), so it can never fit worse than the exponential. Its RMSE > is at most the exponential's. But a flatter late-life decline (b > 0) also > means more reserves stay recoverable, so its EUR is the more optimistic > reserves number. Which one would you book?

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 curve_fit


# ── Embedded Arps decline laws + EUR closed forms (do not edit) ──────────
def arps_exponential(t, qi, Di):
    return qi * np.exp(-Di * t)


def arps_hyperbolic(t, qi, Di, b):
    return qi / (1.0 + b * Di * t) ** (1.0 / b)


def eur_exponential(qi, Di, q_abandon):
    if qi <= q_abandon:
        return 0.0
    return (qi - q_abandon) / Di


def eur_hyperbolic(qi, Di, b, q_abandon):
    if qi <= q_abandon or b >= 1:
        return float("nan")
    return (qi ** b / (Di * (1.0 - b))) * (qi ** (1.0 - b) - q_abandon ** (1.0 - b))


# Rates are bopd (bbl/DAY); t and Di are per-MONTH. The Arps EUR closed forms
# return bopd*months, so multiply by DAYS_PER_MONTH to report a volume in bbl.
DAYS_PER_MONTH = 30.44

# ── OD-001 (OML 58): first 12 months of oil rate, bopd, at t = 0..11 ─────
Q12 = [1800, 1650, 1520, 1400, 1295, 1200, 1110, 1030, 958, 892, 832, 778]
T = np.arange(12)


def fit_exponential(t, q):
    """Fit arps_exponential -> (qi_bopd, di_per_month)."""
    popt, _ = curve_fit(arps_exponential, t, q, p0=[2000, 0.1])
    qi_bopd, di_per_month = popt
    return float(qi_bopd), float(di_per_month)


def fit_hyperbolic(t, q):
    """Fit arps_hyperbolic -> (qi_bopd, di_per_month, b_factor), b in (0,1)."""
    popt, _ = curve_fit(
        arps_hyperbolic, t, q,
        p0=[2000, 0.1, 0.5],
        bounds=([0, 0, 1e-6], [1e5, 5, 1]),
    )
    qi_bopd, di_per_month, b_factor = popt
    return float(qi_bopd), float(di_per_month), float(b_factor)


def rmse(model_q, q):
    """Root-mean-square error between modelled and observed rates."""
    model_q = np.asarray(model_q, dtype=float)
    q = np.asarray(q, dtype=float)
    return float(np.sqrt(np.mean((model_q - q) ** 2)))


def compare(t, q, q_abandon_bopd=25):
    """Fit both models; return dict rmse_exp, rmse_hyp, eur_exp_bbl, eur_hyp_bbl."""
    t = np.asarray(t, dtype=float)
    q = np.asarray(q, dtype=float)

    qi_e, di_e = fit_exponential(t, q)
    qi_h, di_h, b_h = fit_hyperbolic(t, q)

    rmse_exp = rmse(arps_exponential(t, qi_e, di_e), q)
    rmse_hyp = rmse(arps_hyperbolic(t, qi_h, di_h, b_h), q)

    # eur_* return bopd*months; * DAYS_PER_MONTH gives a volume in bbl.
    eur_exp_bbl = float(eur_exponential(qi_e, di_e, q_abandon_bopd)) * DAYS_PER_MONTH
    eur_hyp_bbl = float(eur_hyperbolic(qi_h, di_h, b_h, q_abandon_bopd)) * DAYS_PER_MONTH

    return {
        "rmse_exp": rmse_exp,
        "rmse_hyp": rmse_hyp,
        "eur_exp_bbl": eur_exp_bbl,
        "eur_hyp_bbl": eur_hyp_bbl,
    }


result = compare(T, Q12)

print("compare result:", result)

lockCopying code is a Full Access feature.