Exercise 10.9
Recovery Factor So Far
Using your OOIP estimate from this chapter, calculate the current recovery factor (). Then research typical recovery factors for each drive mechanism (depletion, solution gas, water drive, gas cap). Is the current recovery factor consistent with the diagnosed drive mechanism?
---
The OOIP your material-balance work produced is only half the story. A reserves review wants to know how far into its life the reservoir has come, and that is the recovery factor:
RF = Np / Nwhere Np is the stock-tank oil produced so far (STB) and N is the original oil in place (STB). It is a pure fraction between 0 and 1.
You are looking at block OD-12 on OML 58. The same undersaturated pressure-history that gave you the Havlena-Odeh straight line is provided again below: a six-point survey from initial pressure Pi = 4000 psi down to 3000 psi, with the matching Np_stb, Bo, Rs, Bg, and the rock/fluid properties. Above the bubble point the underground withdrawal collapses to F = Np * Bo (the gas term cancels because Rp = Rs), and the drive plot F / (Eo + Efw) is essentially flat: the signature of a pure depletion drive.
Two MBE helpers are already wired in for you: calculate_mbe_terms(...) returns F, Eo, Eg, Efw, Rp, dP, and ooip_through_origin(x, F) fits the least-squares slope of F versus x through the origin (returning N and an R^2).
Write two functions:
recovery_factor(np_stb, ooip_stb)-> the fractionnp_stb / ooip_stb. Keep
it a plain divide so the hidden tests can call it on any numbers.
estimate_ooip()-> runcalculate_mbe_termson the data, build the
Havlena-Odeh abscissa x = Eo + Efw, drop the t = 0 point (where both F and x are zero), and return N from ooip_through_origin(x[1:], F[1:]).
Then set:
ooip_stb=estimate_ooip(): you should recoverN ≈ 50,000,000 STB.np_last_stb= the last entry ofNp_stb(954,191 STB, cumulative to
date).
rf_now=recovery_factor(np_last_stb, ooip_stb).
You should find rf_now ≈ 0.019, under 2 percent recovered. Put that next to the rules of thumb for ultimate recovery by drive mechanism:
| drive mechanism | typical ultimate RF |
|---|---|
| depletion / solution gas | 5 – 20 % |
| gas-cap expansion | 20 – 40 % |
| water drive | 35 – 60 % |
For a depletion-drive reservoir the ceiling is only 5 – 20 %, and OD-12 has produced under 2 % so far; it is barely into its life. That single fraction tells a reserves committee there is a long depletion runway ahead (and that pressure support, if it can be engineered, is where the upside lives).
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 (already correct - call them, don't edit) ────────────────
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
def ooip_through_origin(x, F):
x = np.asarray(x, float); F = np.asarray(F, float)
N = np.sum(x * F) / np.sum(x * x) # least-squares slope through origin
ss_res = np.sum((F - N * x) ** 2)
ss_tot = np.sum((F - np.mean(F)) ** 2)
r2 = 1.0 - ss_res / ss_tot
return N, r2
# ── OD-12 undersaturated pressure history (OML 58) ───────────────────────
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 recovery_factor(np_stb, ooip_stb):
"""Current recovery factor: produced oil over OOIP (a fraction 0..1)."""
return np_stb / ooip_stb
def estimate_ooip():
"""Havlena-Odeh OOIP: fit F vs (Eo + Efw) through the origin, skip t=0."""
F, Eo, Eg, Efw, Rp, dP = calculate_mbe_terms(
P_psi, Np_stb, Gp_scf, Wp_stb, Bo, Bg, Rs, Bw,
Boi, Bgi, Rsi, Swi, cw, cf, Pi)
x = Eo + Efw
N, r2 = ooip_through_origin(x[1:], F[1:]) # drop the t=0 point
return N
ooip_stb = estimate_ooip()
np_last_stb = float(Np_stb[-1])
rf_now = recovery_factor(np_last_stb, ooip_stb)
print(f"OOIP = {ooip_stb:,.0f} STB")
print(f"Np_last = {np_last_stb:,.0f} STB")
print(f"RF_now = {rf_now:.4f} ({rf_now * 100:.2f}% recovered)")
lockCopying code is a Full Access feature.