Exercise 15.1
Z-Factor Sensitivity - Gas Gravity & the DAK Dip
Calculate and plot Z vs. pressure (100–10,000 psia) for gas gravities of 0.55, 0.65, 0.75, and 0.85 at 200°F. How does gas composition (heavier = higher SG) affect the Z-factor behaviour?
---
You'll build the Z-factor sensitivity sweep for a typical Niger Delta dry-gas stream on OML 58. The gas compressibility factor Z controls everything downstream (Bg, gas-in-place, deliverability), so getting its shape right is the foundation of the whole gas-engineering chapter.
The two verified chapter functions are embedded for you; do not modify them or re-derive the physics. pseudo_critical_properties(gas_sg) returns the Sutton (1985) pseudo-criticals (Ppc, Tpc), and z_factor_DAK(Ppr, Tpr) solves the eleven-coefficient Dranchuk–Abou-Kassem equation for Z at a reduced-coordinate pair.
Your job is the thin wrapper that turns real field pressures into a Z curve:
- Write
z_curve(gas_sg, T_F, pressures):
- Get
Ppc, Tpc = pseudo_critical_properties(gas_sg). - Convert the reservoir temperature to a pseudo-reduced temperature:
Tpr = (T_F + 459.67) / Tpc (Rankine over pseudo-critical temperature).
- For each pressure
Pinpressures, the pseudo-reduced pressure is
Ppr = P / Ppc; evaluate z_factor_DAK(Ppr, Tpr).
- Return a numpy array of the
Zvalues, one per input pressure.
- Using the embedded constants
T_F = 200.0(deg F),
GRAVITIES = [0.55, 0.65, 0.75, 0.85], and PRESSURES = np.array([1000.0, 3000.0, 5000.0, 8000.0, 10000.0]):
- Assign
z_sg65 = z_curve(0.65, T_F, PRESSURES): the 5-element Z array for
the 0.65-gravity gas.
- Sweep a fine pressure grid
np.arange(100.0, 10001.0, 100.0)for the
0.85-gravity gas at 200 deg F and assign min_z_sg85 = float(...): the minimum Z over that sweep, i.e. the deepest point of the Z dip.
> Think about it: the Z curve is not monotonic; it dips below 1 at > low-to-mid pressure (real-gas attraction wins) and then climbs above 1 at high > pressure (repulsion / reduced volume wins). A heavier gas (higher gravity) has > a lower pseudo-critical temperature, so at fixed reservoir temperature its Tpr > is smaller and it dips deeper. Why does the mean reservoir temperature, not > the absolute pressure, decide how far the curve sags?
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
from scipy.optimize import brentq
# ── Verified pseudo-critical correlation - Sutton (1985) (do not edit) ────
def pseudo_critical_properties(gas_sg):
"""
Estimate pseudo-critical properties from gas specific gravity.
Uses the Sutton (1985) correlations.
Parameters
----------
gas_sg : float
Gas specific gravity (air = 1.0). Typical range: 0.55 to 0.90.
Returns
-------
tuple
(Ppc in psia, Tpc in deg R)
"""
Ppc = 756.8 - 131.0 * gas_sg - 3.6 * gas_sg**2
Tpc = 169.2 + 349.5 * gas_sg - 74.0 * gas_sg**2
return Ppc, Tpc
# ── Verified DAK (1975) Z-factor correlation (do not edit) ────────────────
def z_factor_DAK(Ppr, Tpr):
"""
Calculate gas Z-factor using the Dranchuk-Abou-Kassem (1975)
equation of state.
This correlation fits the Standing-Katz chart with 11 empirical
coefficients. Valid for 1.0 ≤ Tpr ≤ 3.0 and 0.2 ≤ Ppr ≤ 15.
Parameters
----------
Ppr : float
Pseudo-reduced pressure (dimensionless).
Tpr : float
Pseudo-reduced temperature (dimensionless).
Returns
-------
float
Compressibility factor Z.
"""
# DAK (1975) coefficients
A1, A2, A3 = 0.3265, -1.0700, -0.5339
A4, A5, A6 = 0.01569, -0.05165, 0.5475
A7, A8 = -0.7361, 0.1844
A9, A10, A11 = 0.1056, 0.6134, 0.7210
def residual(rho_r):
"""DAK residual function. Z = 0.27 * Ppr / (rho_r * Tpr)"""
Z = 0.27 * Ppr / (rho_r * Tpr)
F = (
A1 + A2/Tpr + A3/Tpr**3 + A4/Tpr**4 + A5/Tpr**5
) * rho_r + (
A6 + A7/Tpr + A8/Tpr**2
) * rho_r**2 - A9 * (
A7/Tpr + A8/Tpr**2
) * rho_r**5 + A10 * (
1 + A11*rho_r**2
) * (rho_r**2 / Tpr**3) * np.exp(-A11*rho_r**2) + 1 - Z
return F
# Solve for reduced density
rho_r = brentq(residual, 0.001, 5.0)
Z = 0.27 * Ppr / (rho_r * Tpr)
return Z
# ── OML-58 gas-stream constants (do not edit) ─────────────────────────────
T_F = 200.0 # reservoir temperature, deg F
GRAVITIES = [0.55, 0.65, 0.75, 0.85] # gas specific gravities to sweep
PRESSURES = np.array([1000.0, 3000.0, 5000.0, 8000.0, 10000.0]) # psia anchors
def z_curve(gas_sg, T_F, pressures):
"""Z-factor vs pressure for one gas gravity at temperature T_F (deg F).
Returns a numpy array of Z values, one per input pressure.
Ppc, Tpc = pseudo_critical_properties(gas_sg)
Tpr = (T_F + 459.67) / Tpc # Rankine / pseudo-critical temperature
Z[i] = z_factor_DAK(pressures[i] / Ppc, Tpr)
"""
Ppc, Tpc = pseudo_critical_properties(gas_sg)
Tpr = (T_F + 459.67) / Tpc
return np.array([z_factor_DAK(P / Ppc, Tpr) for P in pressures])
z_sg65 = z_curve(0.65, T_F, PRESSURES)
_sweep = np.arange(100.0, 10001.0, 100.0)
min_z_sg85 = float(np.min(z_curve(0.85, T_F, _sweep)))
print("z_sg65:", z_sg65)
print("min_z_sg85 (deepest dip, 0.85 gas):", min_z_sg85)
lockCopying code is a Full Access feature.