Exercise 10.5
Gas-Cap Effect on OOIP
# Gas-Cap Effect on OOIP: OML 58 block "Egret-D"
The material-balance equation (MBE) is an accounting identity: the reservoir underground withdrawal F must equal the expansion of everything left behind. For a reservoir with a gas cap, the Havlena–Odeh straight-line form is
F = N * (Eo + Efw + m * Eg)where
F(underground withdrawal, RB):Np*(Bo + (Rp - Rs)*Bg) + Wp*BwEo(oil + dissolved-gas expansion, RB/STB):(Bo - Boi) + (Rsi - Rs)*BgEfw(rock + connate-water expansion, RB/STB)Eg(gas-cap expansion per unit m, RB/STB):Boi/Bgi * (Bg - Bgi)m(gas-cap size): reservoir gas-cap pore volume / oil-zone pore volumeN(OOIP, STB): the slope ofFagainst the expansion group.
The Egret-D block on OML 58 has a known gas cap, m = 0.4. As pressure drops, the gas cap swells, so Bg rises with declining pressure, and the m*Eg term is no longer negligible. The pressure/production history is already defined for you (P_psi, Np_stb, Bo, Rs, Bg, ...).
A junior engineer fits the data with no gas-cap term (m = 0) and reports an OOIP. You are asked to redo it with the gas cap (m = 0.4) and show what ignoring a real gas cap does to the estimate.
Write three functions:
mbe_terms()-> a dict with arraysF,Eo,Eg,Efw(each length 6),
computed from the canonical data with calculate_mbe_terms(...) (already provided, so call it). Skip nothing here; return the full 6-point arrays.
ooip_with_gascap(F, Eo, Efw, Eg, m)-> a float OOIP. Build the
regression variable x = Eo + Efw + m*Eg, then fit F against x through the origin, skipping the t = 0 point (where F = 0 and x = 0). Use the provided ooip_through_origin(x, F) helper, which returns (N, r2); return just N.
compare_gascap()-> a dict:
| key | value |
|---|---|
n_with_cap | ooip_with_gascap(..., m=0.4) |
n_no_cap | ooip_with_gascap(..., m=0.0) |
overestimate | n_no_cap - n_with_cap (STB the junior over-books) |
Then call compare_gascap() and store the result in result.
The physics you must surface. Eg = Boi/Bgi*(Bg - Bgi) is positive and grows as Bg rises. Adding m*Eg makes the regression variable x larger at every pressure step. Fitting F against a larger x through the origin gives a smaller slope, so n_with_cap < n_no_cap. In plain terms:
> Ignoring a real gas cap OVERESTIMATES OOIP. The gas cap is doing some of > the producing: its expansion supplies energy and volume that the no-cap fit > mistakenly attributes to a larger oil zone. Book the gas cap, and the honest > oil-in-place comes down.
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 machinery (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
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
# ── Egret-D block (OML 58) - gas-cap reservoir, Bg RISES as P drops ──────
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.array([0.00087, 0.00092, 0.00097, 0.00103, 0.00109, 0.00116]) # gas cap swells
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
M_GASCAP = 0.4 # known gas-cap size for Egret-D
def mbe_terms():
"""Return {'F','Eo','Eg','Efw'} as length-6 arrays from the canonical data."""
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,
)
return {"F": F, "Eo": Eo, "Eg": Eg, "Efw": Efw}
def ooip_with_gascap(F, Eo, Efw, Eg, m):
"""OOIP (STB): fit F vs x = Eo + Efw + m*Eg through the origin (skip t=0)."""
F = np.asarray(F, float); Eo = np.asarray(Eo, float)
Efw = np.asarray(Efw, float); Eg = np.asarray(Eg, float)
x = Eo + Efw + m * Eg
N, r2 = ooip_through_origin(x[1:], F[1:]) # skip the t=0 point (F=0, x=0)
return float(N)
def compare_gascap():
"""Compare OOIP with the gas cap (m=0.4) vs ignoring it (m=0)."""
t = mbe_terms()
n_with_cap = ooip_with_gascap(t["F"], t["Eo"], t["Efw"], t["Eg"], M_GASCAP)
n_no_cap = ooip_with_gascap(t["F"], t["Eo"], t["Efw"], t["Eg"], 0.0)
return {
"n_with_cap": float(n_with_cap),
"n_no_cap": float(n_no_cap),
"overestimate": float(n_no_cap - n_with_cap),
}
result = compare_gascap()
print(f"N (with gas cap, m=0.4) = {result['n_with_cap']:,.0f} STB")
print(f"N (ignoring gas cap) = {result['n_no_cap']:,.0f} STB")
print(f"over-estimate = {result['overestimate']:,.0f} STB")
lockCopying code is a Full Access feature.