Exercise 10.3
Drive-Mechanism Diagnosis
Using the data from Exercise 10.2, calculate at each pressure step and plot it against cumulative production. Is the trend stable, rising, or falling? What does this tell you about the drive mechanism?
---
The material-balance equation does more than count barrels: read it the right way and it diagnoses the reservoir's drive mechanism. The trick is the ratio of underground withdrawal F to the total expansion the reservoir can muster on its own, Et = Eo + Efw (oil-plus-dissolved-gas expansion plus the rock-and-connate-water expansion).
Rearrange the MBE for a reservoir with no gas cap and no water influx and you get F = N * Et, i.e. F / Et = N, a constant equal to the OOIP. That is the Campbell / drive-diagnostic plot: chart F / Et against cumulative production and the shape of the curve names the drive:
- Flat -> the reservoir is living off its own expansion alone:
depletion drive (a.k.a. solution-gas / volumetric depletion above the bubble point). F / Et stays pinned at the OOIP.
- Rising -> an extra source of energy is pushing oil out beyond what
expansion explains: water drive (an aquifer We is doing work the right-hand side does not account for, so the apparent N inflates over time).
- Falling -> a gas cap is expanding and its energy is being mis-charged
to Et, dragging the apparent N down.
Well block OD-014 on OML 58 produced the history embedded in the starter. It is a classic undersaturated block: pressure stays above the bubble point, so Bo increases as pressure drops (the oil swells) and the free-gas term cancels (Rp - Rs = 0), leaving F = Np * Bo.
Write one function:
diagnose(P, Np, Gp, Wp, Bo, Bg, Rs, Bw, Boi, Bgi, Rsi, Swi, cw, cf, Pi)
-> (ratios_psi_free, label) where:
ratiosis the NumPy array ofF / Etat every step aftert = 0
(skip the first row; at initial conditions Et = 0 and the ratio is undefined).
labelis a string:"depletion"ifF / Etis essentially flat
(coefficient of variation, std / mean, below 0.02), "water drive" if the ratio is clearly rising, or "gas cap" if it is clearly falling.
Use calculate_mbe_terms(...) (provided) to get F, Eo, Efw, then form Et = Eo + Efw. Call diagnose(...) on the OD-014 inputs and store the result in ratios and label.
For OD-014 you should find every F / Et value sitting right around 50,000,000 STB (the OOIP), a coefficient of variation well under 0.001, and label == "depletion". A stable ratio means pure depletion: the reservoir is producing on its own expansion alone, with no aquifer and no gas cap feeding it external energy. That is a sober diagnosis: depletion drive recovers the least oil, so OD-014 is a candidate for pressure maintenance (water or gas injection) if the economics support it.
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 pandas as pd
# ── MBE helpers (provided - do not change) ───────────────────────────────
def calculate_mbe_terms(P, Np, Gp, Wp, Bo, Bg, Rs, Bw, Boi, Bgi, Rsi, Swi, cw, cf, Pi):
P = np.asarray(P, float); Np = np.asarray(Np, float); Gp = np.asarray(Gp, float)
Wp = np.asarray(Wp, float); Bo = np.asarray(Bo, float); Bg = np.asarray(Bg, float); Rs = np.asarray(Rs, float)
dP = Pi - P
Rp = np.where(Np > 0, Gp / Np, 0.0)
F = Np * (Bo + (Rp - Rs) * Bg) + Wp * Bw # underground withdrawal (RB)
Eo = (Bo - Boi) + (Rsi - Rs) * Bg # oil + dissolved-gas expansion
Efw = Boi * (cw * Swi + cf) * dP / (1.0 - Swi) # rock + connate-water expansion
Eg = Boi / Bgi * (Bg - Bgi) # gas-cap expansion (per unit m)
return F, Eo, Eg, Efw, Rp, dP
# ── OD-014 production + PVT history (OML 58, undersaturated block) ────────
# Pressure stays ABOVE the bubble point: Bo RISES as P drops (oil swells),
# Rs is constant, and the free-gas term cancels (Rp - Rs = 0) so F = Np * Bo.
P_psi = np.array([4000.0, 3800.0, 3600.0, 3400.0, 3200.0, 3000.0])
Np_stb = np.array([0.0, 192666.0, 384411.0, 575243.0, 765167.0, 954191.0])
Bo = np.array([1.31000, 1.31314, 1.31629, 1.31943, 1.32258, 1.32572]) # RISES as P drops
Rs = np.full(6, 600.0) # constant above the bubble point
Bg = np.full(6, 0.00090) # cancels above Pb (Rp - Rs = 0); present for the signature
Bw = 1.02
Wp_stb = np.zeros(6)
Gp_scf = Np_stb * 600.0 # Gp = Np * Rsi above Pb
Boi, Bgi, Rsi, Swi, cw, cf, Pi = 1.31000, 0.00087, 600.0, 0.22, 3.2e-6, 5.0e-6, 4000.0
def diagnose(P, Np, Gp, Wp, Bo, Bg, Rs, Bw, Boi, Bgi, Rsi, Swi, cw, cf, Pi):
"""Compute F/Et after t=0 and name the drive mechanism.
Returns (ratios, label) where:
ratios -> np.array of F / Et for every step after the first row,
label -> "depletion" if std/mean < 0.02 (flat), else "water drive"
if rising, else "gas cap" if falling.
"""
F, Eo, _Eg, Efw, _Rp, _dP = calculate_mbe_terms(
P, Np, Gp, Wp, Bo, Bg, Rs, Bw, Boi, Bgi, Rsi, Swi, cw, cf, Pi
)
Et = Eo + Efw
ratios = (F / Et)[1:] # skip t=0 where Et == 0
cov = ratios.std() / ratios.mean()
if cov < 0.02:
label = "depletion"
elif ratios[-1] > ratios[0]:
label = "water drive"
else:
label = "gas cap"
return ratios, label
ratios, label = diagnose(
P_psi, Np_stb, Gp_scf, Wp_stb, Bo, Bg, Rs, Bw,
Boi, Bgi, Rsi, Swi, cw, cf, Pi,
)
print(f"F/Et per step (STB): {np.array2string(ratios, precision=0)}")
print(f"mean F/Et = {ratios.mean():,.0f} STB "
f"CoV = {ratios.std() / ratios.mean():.5f}")
print(f"drive diagnosis -> {label.upper()}")
lockCopying code is a Full Access feature.