Exercise 8.10
Complete Fluid Characterization
A wireline crew has just logged a new discovery on OML 58 and the lab turned around a separator test overnight. Before anyone books a barrel or sizes a facility, the reservoir engineer runs one calculation: a fluid characterization that tells you what kind of oil you have and (critically) whether the reservoir sits above or below its bubble point. That single fact decides everything downstream: whether free gas is coming out of solution in the reservoir, how you model the OOIP, and how the well will behave on first draw-down.
This is the discovery summary. You are given the cards on the table:
- API gravity: 29° (a medium crude)
- Gas specific gravity
gamma_g: 0.82 - Reservoir temperature
T_F: 195 °F - Initial reservoir pressure
Pi: 4800 psia - Initial solution GOR
Rs: 480 scf/STB
Write one function that bundles the whole characterization:
characterize(API, gamma_g, T_F, Pi, Rs) -> dictIt must return a dict with exactly these keys:
gamma_o: oil specific gravity fromapi_to_sg(API).pb_psia: bubble-point pressure fromstanding_bubble_point(Rs, gamma_g, T_F, API).bo_at_pb: oil FVF at the bubble point,standing_Bo(Rs, gamma_g, gamma_o, T_F)
(Standing's Bo is evaluated at bubble-point conditions; it uses the oil gravity you just computed, not the API).
mu_at_pb: live-oil viscosity at the bubble point: take the dead-oil
viscosity beggs_robinson_dead_oil(T_F, API), then apply the gas-in-solution correction beggs_robinson_live_oil(mu_od, Rs).
state: the string"undersaturated"ifPi > pb_psia, else"saturated".undersaturation_psi: the cushionPi - pb_psia(how far the reservoir
sits above its bubble point).
Then call characterize on the discovery data above and store the result in report, and pull state out into discovery_state for a quick read.
For this fluid the bubble point lands well below 4800 psia, so the reservoir is undersaturated: no free gas in the reservoir yet, and that undersaturation_psi cushion is the pressure you can produce before gas starts breaking out.
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
# ── OML 58 new-discovery data (separator test + initial conditions) ──────
API = 29.0 # oil API gravity, degrees
GAMMA_G = 0.82 # gas specific gravity (air = 1)
T_F = 195.0 # reservoir temperature, deg F
PI_PSIA = 4800.0 # initial reservoir pressure, psia
RS_SCF_STB = 480.0 # initial solution GOR, scf/STB
# ── PVT correlations (provided - do not edit) ────────────────────────────
def api_to_sg(API):
return 141.5 / (API + 131.5)
def standing_bubble_point(Rs, gamma_g, T_F, API):
exponent = 0.00091 * T_F - 0.0125 * API
return 18.2 * ((Rs / gamma_g) ** 0.83 * 10 ** exponent - 1.4)
def standing_Bo(Rs, gamma_g, gamma_o, T_F):
F = Rs * np.sqrt(gamma_g / gamma_o) + 1.25 * T_F
return 0.972 + 1.47e-4 * F ** 1.175
def beggs_robinson_dead_oil(T_F, API):
Y = 10 ** (3.0324 - 0.02023 * API)
X = Y * T_F ** (-1.163)
return 10 ** X - 1
def beggs_robinson_live_oil(mu_od, Rs):
A = 10.715 * (Rs + 100) ** (-0.515)
B = 5.44 * (Rs + 150) ** (-0.338)
return A * mu_od ** B
# ── Your task ────────────────────────────────────────────────────────────
def characterize(API, gamma_g, T_F, Pi, Rs):
"""One-call new-discovery fluid summary -> dict."""
gamma_o = api_to_sg(API)
pb_psia = standing_bubble_point(Rs, gamma_g, T_F, API)
bo_at_pb = standing_Bo(Rs, gamma_g, gamma_o, T_F)
mu_od = beggs_robinson_dead_oil(T_F, API)
mu_at_pb = beggs_robinson_live_oil(mu_od, Rs)
state = "undersaturated" if Pi > pb_psia else "saturated"
undersaturation_psi = Pi - pb_psia
return {
"gamma_o": float(gamma_o),
"pb_psia": float(pb_psia),
"bo_at_pb": float(bo_at_pb),
"mu_at_pb": float(mu_at_pb),
"state": state,
"undersaturation_psi": float(undersaturation_psi),
}
report = characterize(API, GAMMA_G, T_F, PI_PSIA, RS_SCF_STB)
discovery_state = report["state"]
print("characterization:", report)
print("reservoir state:", discovery_state)
lockCopying code is a Full Access feature.