Exerciseschevron_rightChapter 10chevron_right10.5
fitness_center

Exercise 10.5

Gas-Cap Effect on OOIP

Level 3
Chapter 10: Material Balance
descriptionProblem

# 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*Bw
  • Eo (oil + dissolved-gas expansion, RB/STB): (Bo - Boi) + (Rsi - Rs)*Bg
  • Efw (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 volume
  • N (OOIP, STB): the slope of F against 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 arrays F, 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:
keyvalue
n_with_capooip_with_gascap(..., m=0.4)
n_no_capooip_with_gascap(..., m=0.0)
overestimaten_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.

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