Exerciseschevron_rightChapter 9chevron_right9.6
fitness_center

Exercise 9.6

Rate-Cumulative (q vs Np)

Level 2
Chapter 9: Decline Curve Analysis
descriptionProblem

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

q(t)=qieDit,Np(t)=0tqdt=qiq(t)Diq(t) = q_i\,e^{-D_i t}, \qquad N_p(t)=\int_0^t q\,dt = \frac{q_i - q(t)}{D_i}

you can eliminate time and get

q=qiDiNp\boxed{\,q = q_i - D_i\,N_p\,}

So a plot of q against Np is a straight line whose slope is Di-D_i and whose x-intercept (q ⁣= ⁣0q\!=\!0) is qi/Di=q_i/D_i = 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 Di-D_i, 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:

  1. cumulative_production(t_months, q_bopd) → a NumPy array np_bbl of 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.)

  1. 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.

  1. 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?

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
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.