Exerciseschevron_rightChapter 10chevron_right10.9
fitness_center

Exercise 10.9

Recovery Factor So Far

Level 1
Chapter 10: Material Balance
descriptionProblem

Using your OOIP estimate from this chapter, calculate the current recovery factor (RF=Np/NR_F = N_p / N). 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 / N

where 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 fraction np_stb / ooip_stb. Keep

it a plain divide so the hidden tests can call it on any numbers.

  • estimate_ooip() -> run calculate_mbe_terms on 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 recover N ≈ 50,000,000 STB.
  • np_last_stb = the last entry of Np_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 mechanismtypical ultimate RF
depletion / solution gas5 – 20 %
gas-cap expansion20 – 40 %
water drive35 – 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).

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