Exerciseschevron_rightChapter 15chevron_right15.1
fitness_center

Exercise 15.1

Z-Factor Sensitivity - Gas Gravity & the DAK Dip

Level 1
Chapter 15: Gas Engineering
descriptionProblem

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:

  1. 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 P in pressures, the pseudo-reduced pressure is

Ppr = P / Ppc; evaluate z_factor_DAK(Ppr, Tpr).

  • Return a numpy array of the Z values, one per input pressure.
  1. 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?

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