Exerciseschevron_rightChapter 10chevron_right10.1
fitness_center

Exercise 10.1

MBE Term Identification

Level 2
Chapter 10: Material Balance
descriptionProblem

# MBE Term Identification

The generalized material balance equation (GMBE) is a volume ledger: every reservoir barrel that leaves the rock must be balanced by something expanding to fill the void it left behind. Havlena and Odeh wrote it as

F = N * (Eo + m*Eg + Efw) + We

Before you can fit a straight line through it, you have to read each term and know what physics it carries. This exercise is about identifying the terms, not solving for OOIP; that comes next chapter-section.

Block OD-014 on OML 58 is a clean undersaturated oil reservoir: the initial pressure (4,000 psi) sits well above the bubble point, and every step in the production history stays above it. So solution gas never breaks out, the solution GOR stays constant (Rs == Rsi == 600 SCF/STB), and the produced GOR equals it (Rp == Rs). That single fact collapses most of the ledger.

## The four terms

For each pressure step the helper calculate_mbe_terms(...) returns:

  • F (underground withdrawal, RB). The reservoir volume actually produced:

Np*(Bo + (Rp - Rs)*Bg) + Wp*Bw. Above the bubble point Rp - Rs == 0 and Wp == 0, so this simplifies to F = Np * Bo: the gas term cancels.

  • Eo (oil + dissolved-gas expansion, RB/STB). (Bo - Boi) + (Rsi - Rs)*Bg.

Above the bubble point Rsi - Rs == 0, so Eo = Bo - Boi. As pressure drops, the under-saturated oil expands, so Bo rises toward its bubble-point maximum and Eo is strictly positive and increasing. (This is the textbook trap: below the bubble point gas leaves solution and Bo falls, but we are above it, so Bo grows.)

  • Efw (rock + connate-water expansion, RB/STB). Boi*(cw*Swi + cf)*dP / (1 - Swi).

As pressure drops, dP = Pi - P grows, the grains and the irreducible water expand into the pore space, and Efw grows with it; small but always positive in a depletion drive.

  • Eg (gas-cap expansion, RB/STB per unit m). Boi/Bgi * (Bg - Bgi).

OD-014 has no gas cap (m = 0), so this term is multiplied out of the ledger; we still compute it for completeness.

With no aquifer and no gas cap, the ledger for OD-014 reduces to the depletion-drive form F = N * (Eo + Efw): pure expansion of oil, rock, and connate water.

## Your job

Embed the dataset and the two helpers (already in starter.py), then write:

  • mbe_terms(...): call calculate_mbe_terms(...) with the OD-014 inputs and

return a dict {"F": F, "Eo": Eo, "Eg": Eg, "Efw": Efw} (the four arrays, one value per pressure step).

  • dominant_drive_term(Eo, Efw): at the last pressure step (deepest

drawdown), return the string "Eo" if oil expansion is the larger term, or "Efw" if rock + water expansion is larger. For an undersaturated oil the oil expansion dwarfs the rock/water term, so this should report "Eo".

Store the dict in terms and the drive string in drive. You should find that above the bubble point F == Np*Bo exactly, that Eo[-1] > Eo[0] and every Eo is positive, that Efw climbs from 0 at the initial pressure, and that oil expansion (Eo) is the dominant drive term at the last step.

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 (use verbatim - 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


# ── OD-014 undersaturated reservoir (OML 58) - all pressures above Pb ─────
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 mbe_terms():
    """Return {"F", "Eo", "Eg", "Efw"} arrays for OD-014 (one value per step)."""
    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 dominant_drive_term(Eo, Efw):
    """At the LAST step, "Eo" if oil expansion is larger, else "Efw"."""
    return "Eo" if Eo[-1] >= Efw[-1] else "Efw"


terms = mbe_terms()
drive = dominant_drive_term(terms["Eo"], terms["Efw"])

df = pd.DataFrame({
    "P_psi": P_psi,
    "F_rb": terms["F"],
    "Eo": terms["Eo"],
    "Efw": terms["Efw"],
    "Eg": terms["Eg"],
})
print(df.to_string(index=False))
print(f"\ndominant drive term at last step: {drive}")

lockCopying code is a Full Access feature.