Exercise 9.6
Rate-Cumulative (q vs Np)
On OML 58 the well OD-014 has clean monthly allocated oil rates, but the production-database dates are a mess: meters tripped, allocation runs were re-stamped, and the calendar column is unreliable. A rate-time decline fit needs trustworthy dates; a rate-cumulative plot does not.
The trick: for exponential decline, the rate is a perfectly straight line in cumulative production. From
you can eliminate time and get
So a plot of q against Np is a straight line whose slope is and whose x-intercept () is EUR down to zero rate. No dates required: only rate and the running cumulative.
Mind the units. The rate qi_bopd is in bbl/DAY (bopd) but t and D_i are per-MONTH, so the monthly trapezoid of q comes out in bopd·months, not barrels. To report a volume in bbl, the cumulative array and the x-intercept EUR: multiply by DAYS_PER_MONTH = 30.44 (the book's convention). The slope stays , a per-month rate that is unit-agnostic, so do not scale it.
You are given the synthetic stream for OD-014 (qi_bopd = 1500, di_per_month = 0.10, 36 monthly points). Write three functions:
cumulative_production(t_months, q_bopd)→ a NumPy arraynp_bblof the
running cumulative produced at each sample, in barrels. Use the monthly trapezoidal rule so the first point is 0.0 and each later point adds the trapezoid between consecutive rates, then convert bopd·months → bbl with DAYS_PER_MONTH. (Hint: np.concatenate([[0.0], np.cumsum((q[:-1] + q[1:]) / 2.0)]) DAYS_PER_MONTH.)
rate_cum_fit(t_months, q_bopd)→ the tuple(slope, x_intercept_bbl)from
a straight-line fit of q vs the bopd·months cumulative with np.polyfit(...). The slope should land near -di_per_month (unscaled); the x_intercept_bbl (where the fitted line crosses q = 0, converted to barrels) should land near qi_bopd / di_per_month * DAYS_PER_MONTH.
plot_rate_cum(t_months, q_bopd)→ build a matplotlib figure with **Np on
the x-axis and q on the y-axis** (scatter the data points and draw the fitted straight line), label both axes, and return the figure.
Then run all three on OD-014 and store np_bbl, (slope, x_intercept_bbl), and the figure.
> Think about it: the x-intercept is EUR to zero rate. A real abandonment > rate q_abandon_bopd > 0 lands the recoverable volume a little to the left of > it. Why is the rate-cumulative line still the cleaner read on EUR when your > production dates can't be trusted?
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
import matplotlib.pyplot as plt
# ── Arps exponential decline (do not edit) ───────────────────────────────
def arps_exponential(t, qi, Di):
return qi * np.exp(-Di * t)
# Rates are bopd (bbl/DAY); t and Di are per-MONTH. The monthly trapezoid of
# q gives bopd·months, so multiply by days/month to report a VOLUME in bbl.
DAYS_PER_MONTH = 30.44
# ── OML 58 well OD-014: clean monthly rates, untrustworthy dates ─────────
qi_bopd = 1500.0
di_per_month = 0.10
t_months = np.arange(36)
q_bopd = arps_exponential(t_months, qi_bopd, di_per_month)
def cumulative_production(t_months, q_bopd):
"""Running cumulative produced (bbl) at each sample, monthly trapezoidal.
First point is 0.0; each later point adds the trapezoid between
consecutive rates. The trapezoid of a bopd rate over monthly steps is
bopd·months, so multiply by DAYS_PER_MONTH to report barrels.
"""
q = np.asarray(q_bopd, dtype=float)
cum_bopd_months = np.concatenate([[0.0], np.cumsum((q[:-1] + q[1:]) / 2.0)])
return cum_bopd_months * DAYS_PER_MONTH
def rate_cum_fit(t_months, q_bopd):
"""(slope, x_intercept_bbl) from a straight-line fit of q vs Np.
The slope is a per-month rate (-Di) and is unit-agnostic, so fit q against
the raw bopd·months cumulative. The x-intercept is a VOLUME (EUR to q = 0),
so report it in barrels via DAYS_PER_MONTH.
"""
q = np.asarray(q_bopd, dtype=float)
cum_bopd_months = np.concatenate([[0.0], np.cumsum((q[:-1] + q[1:]) / 2.0)])
slope, intercept = np.polyfit(cum_bopd_months, q, 1)
x_intercept = -intercept / slope # bopd·months, where the line hits q = 0
x_intercept_bbl = x_intercept * DAYS_PER_MONTH
return (float(slope), float(x_intercept_bbl))
def plot_rate_cum(t_months, q_bopd):
"""Figure of q (y) vs Np (x): scatter the data + draw the fitted line."""
q = np.asarray(q_bopd, dtype=float)
np_bbl = cumulative_production(t_months, q)
slope_bbl, intercept = np.polyfit(np_bbl, q, 1)
fig, ax = plt.subplots(figsize=(8, 5))
ax.scatter(np_bbl, q, s=24, label="OD-014 data")
ax.plot(np_bbl, slope_bbl * np_bbl + intercept, color="crimson",
label=f"fit: q = {intercept:.0f} {slope_bbl:+.5f}·Np")
ax.set_xlabel("Cumulative production Np (bbl)")
ax.set_ylabel("Oil rate q (bopd)")
ax.set_title("OD-014 rate-cumulative (q vs Np) - exponential decline")
ax.legend()
ax.grid(True, alpha=0.3)
return fig
np_bbl = cumulative_production(t_months, q_bopd)
slope, x_intercept_bbl = rate_cum_fit(t_months, q_bopd)
fig = plot_rate_cum(t_months, q_bopd)
print("Np final (bbl):", float(np_bbl[-1]))
print("slope (~ -Di):", slope)
print("x-intercept (~ qi/Di) bbl:", x_intercept_bbl)
lockCopying code is a Full Access feature.