Exerciseschevron_rightChapter 9chevron_right9.2
fitness_center

Exercise 9.2

The b-Factor

Level 1
Chapter 9: Decline Curve Analysis
descriptionProblem

Using the well data from Exercise 9.1, what is the fitted bb-factor? What does its value tell you about the decline behavior? Research: what range of bb-factors is typical for (a) conventional vertical wells, (b) horizontal wells in tight oil, and (c) coal seam gas wells?

---

In Exercise 9.1 you fit a hyperbolic decline to well OD-001 on OML 58. The hyperbolic model has three knobs (qi, Di, and the b-factor) and of the three, b is the one reserves engineers argue about most. It controls how fast the decline itself decelerates: b = 0 is a pure exponential (constant fractional decline), while larger b means a flatter, longer tail and therefore more booked reserves.

Here is OD-001's 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]

The Arps hyperbolic rate law is already embedded for you. Write two functions:

  • fit_b(t, q) -> b_factor: fit arps_hyperbolic to the history with

curve_fit (start guess p0=[2000, 0.1, 0.5], bounds ([0, 0, 1e-6], [1e5, 5, 1]) so the fit can only return a physical b in (0, 1)) and return only the fitted b-factor.

  • classify_b(b) -> str: turn a b-factor into a one-line interpretation:
conditionreturned string
b < 0.3'exponential-like'
0.3 <= b < 0.7'typical conventional'
0.7 <= b < 1.0'hyperbolic/unconventional'
b >= 1.0'invalid: EUR diverges'

Then fit OD-001 with fit_b(T, Q12), store the result in b_od001, and store classify_b(b_od001) in verdict.

### Typical b-factor ranges (rules of thumb)

reservoir / drivetypical b-factor
vertical conventional, solution-gas~0.0 – 0.4
tight-oil / shale horizontals~0.5 – 1.2 (capped at 1)
coal-seam / CBM (CSG) gas~0.5 – 1.0

> Why the b >= 1 guard? The hyperbolic EUR integral > (qi^b / (Di(1-b)))·(qi^(1-b) - q_ab^(1-b)) has (1 - b) in the > denominator: at b = 1 it blows up and for b > 1 the model predicts an > infinite recovery. Booking reserves off a b >= 1 fit is a red flag; in > practice you cap b at 1 (as the bounds above do) or switch to a modified > hyperbolic that bends back to exponential late in life.

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 hyperbolic rate law (do not edit) ──────────────────────
def arps_hyperbolic(t, qi, Di, b):
    return qi / (1.0 + b * Di * t) ** (1.0 / b)


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


def fit_b(t, q):
    """Fit arps_hyperbolic and return ONLY the fitted b-factor."""
    t = np.asarray(t, dtype=float)
    q = np.asarray(q, dtype=float)
    popt, _ = curve_fit(
        arps_hyperbolic, t, q,
        p0=[2000.0, 0.1, 0.5],
        bounds=([0.0, 0.0, 1e-6], [1e5, 5.0, 1.0]),
        maxfev=10000,
    )
    return float(popt[2])


def classify_b(b):
    """One-line interpretation of a b-factor (see the table in the prompt)."""
    b = float(b)
    if b < 0.3:
        return "exponential-like"
    if b < 0.7:
        return "typical conventional"
    if b < 1.0:
        return "hyperbolic/unconventional"
    return "invalid: EUR diverges"


b_od001 = fit_b(T, Q12)
verdict = classify_b(b_od001)

print("OD-001 b-factor:", b_od001)
print("verdict:", verdict)

lockCopying code is a Full Access feature.