Exercise 8.7
Build a PVT Report
Write a function pvt_report(API, gamma_g, T_F, Rs_at_Pb, pressures) that generates a complete PVT property table showing Rs, Bo, μo, and fluid state (undersaturated/saturated) at each pressure in the input list.
Format the output as a report-ready table with headers and units. Test it with at least two different fluids.
---
This is the capstone for the PVT chapter: one function that turns four fluid parameters and a pressure schedule into a reservoir-engineering PVT report: exactly the table you would hand a material-balance or nodal-analysis run.
The base fluid is the OML 58 black oil: API = 32, gamma_g = 0.75, T_F = 185, with Rs_at_Pb = 550 scf/STB. Its bubble point lands near 2525 psia (Standing). Above that pressure the oil is undersaturated; all the gas is dissolved and Rs is pinned at Rs_at_Pb. Below it the oil is saturated: free gas has broken out and the dissolved Rs falls with pressure.
Write pvt_report(API, gamma_g, T_F, Rs_at_Pb, pressures) returning a list of dicts, one per pressure (same order as pressures), each with:
| key | meaning |
|---|---|
p_psia | the pressure (float) |
rs_scf_stb | solution GOR at that pressure |
bo_rb_stb | oil formation volume factor (standing_Bo) |
mu_cp | live-oil viscosity (Beggs-Robinson) |
state | 'undersaturated' if P >= Pb else 'saturated' |
Rules at each pressure P:
- Compute the bubble point once with
standing_bubble_point(Rs_at_Pb, ...). - If
P >= Pb:state = 'undersaturated',rs_scf_stb = Rs_at_Pb
(constant; gas stays in solution).
- If
P < Pb:state = 'saturated',
rs_scf_stb = vasquez_beggs_Rs(P, T_F, API, gamma_g) (gas comes out of solution as pressure drops).
bo_rb_stb = standing_Bo(rs_scf_stb, gamma_g, gamma_o, T_F)where
gamma_o = api_to_sg(API).
mu_cp = beggs_robinson_live_oil(mu_od, rs_scf_stb)where
mu_od = beggs_robinson_dead_oil(T_F, API) is the dead-oil viscosity.
Then build the report for the base fluid over the PRESSURES schedule already defined for you (it straddles the bubble point) and store it in report.
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
# ── Embedded PVT correlations (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 vasquez_beggs_Rs(P, T_F, API, gamma_gs):
if API <= 30:
C1, C2, C3 = 0.0362, 1.0937, 25.724
else:
C1, C2, C3 = 0.0178, 1.187, 23.931
return C1 * gamma_gs * P ** C2 * np.exp(C3 * API / (T_F + 460))
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
# ── OML 58 base black oil + pressure schedule (straddles Pb ≈ 2525 psia) ─
BASE_API = 32.0
BASE_GAMMA_G = 0.75
BASE_T_F = 185.0
BASE_RS_AT_PB = 550.0
PRESSURES = [4000.0, 3000.0, 2000.0, 1000.0, 500.0]
def pvt_report(API, gamma_g, T_F, Rs_at_Pb, pressures):
"""List of per-pressure PVT dicts (p_psia, rs_scf_stb, bo_rb_stb, mu_cp, state)."""
gamma_o = api_to_sg(API)
mu_od = beggs_robinson_dead_oil(T_F, API)
pb_psia = standing_bubble_point(Rs_at_Pb, gamma_g, T_F, API)
rows = []
for p in pressures:
p_psia = float(p)
if p_psia >= pb_psia:
state = "undersaturated"
rs_scf_stb = float(Rs_at_Pb)
else:
state = "saturated"
rs_scf_stb = float(vasquez_beggs_Rs(p_psia, T_F, API, gamma_g))
bo_rb_stb = float(standing_Bo(rs_scf_stb, gamma_g, gamma_o, T_F))
mu_cp = float(beggs_robinson_live_oil(mu_od, rs_scf_stb))
rows.append({
"p_psia": p_psia,
"rs_scf_stb": rs_scf_stb,
"bo_rb_stb": bo_rb_stb,
"mu_cp": mu_cp,
"state": state,
})
return rows
report = pvt_report(BASE_API, BASE_GAMMA_G, BASE_T_F, BASE_RS_AT_PB, PRESSURES)
print("PVT report rows:", len(report))
print("first row:", report[0])
lockCopying code is a Full Access feature.