Exercise 9.2
The b-Factor
Using the well data from Exercise 9.1, what is the fitted -factor? What does its value tell you about the decline behavior? Research: what range of -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: fitarps_hyperbolicto 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:
| condition | returned 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 / drive | typical 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.
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 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.