Exercise 9.5
Modified Hyperbolic - Bounding a b>1 Shale Well
A shale gas well has from a standard hyperbolic fit. Implement the Modified Hyperbolic model with values of 0.3%, 0.5%, and 1.0% per month. Compare the EUR from each assumption. Why is the choice of so consequential for unconventional reserves?
---
Shale well OD-014 on OML 58 was fit with a standard Arps hyperbolic decline and came back with b_factor = 1.4, a value greater than 1. That is physically common for transient-flow unconventional wells, but it breaks the reserves math: the closed-form hyperbolic EUR
diverges for (the term flips sign and the well never stops declining fast enough). You cannot book infinite reserves. The industry fix is the modified hyperbolic model: the well declines hyperbolically until its instantaneous decline rate falls to a terminal floor , then switches to exponential decline at forever after. The exponential tail makes the integral converge, so EUR is finite, and the value of you assume controls how much you book.
The well parameters: qi_bopd = 3000 (bbl per day), di_per_month = 0.30, b_factor = 1.4. Time t and the decline rates are per month.
Your tasks (the Arps + modified_hyperbolic functions are embedded; do not re-derive them):
- Write
eur_modified(qi, Di, b, Dmin, q_abandon=10.0, t_max=3000).
- Build a fine monthly time grid
t = np.linspace(0, t_max, ...)(use at
least a few thousand points for a smooth integral). Make t_max long enough that the rate actually falls below q_abandon for every Dmin: the integral should be abandonment-limited, not chopped off by the window. (A 0.3%/month tail takes a long time to die; t_max = 3000 months is comfortably sufficient.)
- Evaluate rate with
modified_hyperbolic(t, qi, Di, b, Dmin). - Numerically integrate the producing portion only: keep the points
where q >= q_abandon and integrate them with np.trapz(q_keep, t_keep).
qis in bbl/day buttis in months, so the trapz area is in
bopd·months. Multiply by DAYS_PER_MONTH = 30.44 to convert to barrels and return the cumulative volume in bbl as eur_bbl.
- Write
compare_dmin(qi, Di, b, dmin_list)returning a dict mapping each
Dmin to its eur_bbl ({dmin: eur_modified(qi, Di, b, dmin)}).
- Call
compare_dmin(3000, 0.30, 1.4, [0.003, 0.005, 0.01])and store it in
eur_by_dmin (the three values are 0.3%, 0.5%, and 1.0% per month).
> Think about it: a lower terminal decline means a flatter, longer > tail: more booked barrels (roughly 2–3 million bbl here once converted to > barrels). Confirm the standard eur_hyperbolic(3000, 0.30, 1.4, 10) returns > nan (its guard), and that every modified EUR is a finite number. > Why is the assumption one of the most litigated numbers in > unconventional reserves reporting?
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
# ── Embedded Arps + modified-hyperbolic functions (do not edit) ──────────
def arps_hyperbolic(t, qi, Di, b):
return qi / (1.0 + b * Di * t) ** (1.0 / b)
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))
def modified_hyperbolic(t, qi, Di, b, Dmin):
t = np.asarray(t, dtype=float)
q = np.zeros_like(t)
for i, ti in enumerate(t):
D_t = Di / (1.0 + b * Di * ti)
if D_t > Dmin:
q[i] = qi / (1.0 + b * Di * ti) ** (1.0 / b)
else:
t_switch = (Di / Dmin - 1.0) / (b * Di)
q_switch = qi / (1.0 + b * Di * t_switch) ** (1.0 / b)
q[i] = q_switch * np.exp(-Dmin * (ti - t_switch))
return q
# ── OD-014 shale well: standard hyperbolic fit returned b = 1.4 (b > 1!) ─
QI_BOPD = 3000.0 # qi is in bbl/DAY (bopd); t and Di are per-MONTH
DI_PER_MONTH = 0.30
B_FACTOR = 1.4
DMIN_LIST = [0.003, 0.005, 0.01] # 0.3% / 0.5% / 1.0% per month
DAYS_PER_MONTH = 30.44 # bopd * months -> bbl
def eur_modified(qi, Di, b, Dmin, q_abandon=10.0, t_max=3000):
"""EUR (bbl) by numerically integrating modified_hyperbolic over months.
qi is in bopd and t/Di are per month, so trapz gives bopd*months; multiply
by DAYS_PER_MONTH to report barrels. t_max is large enough that the integral
is abandonment-limited (q drops below q_abandon) for every Dmin, not cut off
by the window.
"""
t = np.linspace(0.0, float(t_max), 6001)
q = modified_hyperbolic(t, qi, Di, b, Dmin)
mask = q >= q_abandon
if not np.any(mask):
return 0.0
return float(np.trapz(q[mask], t[mask]) * DAYS_PER_MONTH)
def compare_dmin(qi, Di, b, dmin_list):
"""Dict mapping each Dmin -> its modified-hyperbolic EUR (bbl)."""
return {dmin: eur_modified(qi, Di, b, dmin) for dmin in dmin_list}
eur_by_dmin = compare_dmin(QI_BOPD, DI_PER_MONTH, B_FACTOR, DMIN_LIST)
print("standard hyperbolic EUR (b=1.4):", eur_hyperbolic(QI_BOPD, DI_PER_MONTH, B_FACTOR, 10.0))
print("modified EUR by Dmin:", eur_by_dmin)
lockCopying code is a Full Access feature.