Source code for welltest_pta.analysis.reservoir

r"""
welltest_pta.analysis.reservoir
===============================
Reservoir parameter calculation from PTA results — field oilfield units.

All formulae use **field oilfield units**:

==================  ====================  ====================
Quantity            Symbol                Units
==================  ====================  ====================
Flow rate           q                     STB/D (oil) or Mscf/D (gas)
Viscosity           µ                     cp
FVF                 B                     RB/STB (oil) or RB/Mscf
Permeability        k                     mD
Net pay             h                     ft
Slope (semilog)     m                     psi / log-cycle
Wellbore radius     rw                    ft
Total compress.     ct                    psi⁻¹
Porosity            φ                     fraction
Wellbore storage    C                     bbl/psi
==================  ====================  ====================

Standard radial-flow equations (oil):

.. math::
    k\,h = \frac{162.6\, q\, \mu\, B}{|m|}

.. math::
    k = \frac{k\,h}{h}

.. math::
    S = 1.151 \left[
        \frac{p_{1\text{hr}} - p_{wf}(\Delta t = 0)}{|m|}
        - \log_{10}\!\left(\frac{k}{\phi\, \mu\, c_t\, r_w^2}\right)
        + 3.23
    \right]

For drawdowns, replace :math:`p_{1\text{hr}} - p_{wf}(\Delta t = 0)` with
:math:`p_i - p_{1\text{hr}}` and switch sign convention. Wellbore storage
constant from a unit-slope early-time line:

.. math::
    C = \frac{q\, B}{24\, m_{\text{slope-1}}} \quad\text{[bbl/psi]}

where :math:`m_{\text{slope-1}}` is the early-time linear slope of
:math:`\Delta p` vs :math:`\Delta t` (psi/hr).
"""

from __future__ import annotations

from typing import Optional

import numpy as np


[docs] def reservoir_parameters( slope_m: float, p_1hr: float, p_wf_at_shut_in: float, q: float, mu: float, B: float, h: float, phi: float, ct: float, rw: float, early_storage_slope: Optional[float] = None, ) -> dict[str, float]: r""" Compute :math:`kh`, :math:`k`, :math:`S`, and (optionally) :math:`C`. Parameters ---------- slope_m Semilog slope (psi/cycle) — magnitude doesn't matter, sign is handled internally. p_1hr Pressure at :math:`\Delta t = 1` hr on the IARF straight line (psi). For Horner: :math:`p_{1\text{hr}} = m \log_{10}(t_p+1)+P^*`. For MDH: just the intercept. p_wf_at_shut_in Flowing pressure at the instant of shut-in (psi). Skin is computed from :math:`(p_{1\text{hr}} - p_{wf})`. q Flow rate (STB/D for oil — convert gas to its own equation). mu Viscosity (cp). B Formation volume factor (RB/STB). h Net pay (ft). phi Porosity (fraction, 0–1). ct Total compressibility (1/psi). rw Wellbore radius (ft). early_storage_slope Optional — :math:`d\Delta p/dt` at very early time (psi/hr). If provided, wellbore storage :math:`C` (bbl/psi) is also returned. Returns ------- dict with keys ``kh``, ``k``, ``skin``, ``C`` (None if not supplied), plus ``p_skin`` (additional pressure drop due to skin, psi). """ if slope_m == 0 or not np.isfinite(slope_m): return {"kh": np.nan, "k": np.nan, "skin": np.nan, "C": None, "p_skin": np.nan} abs_m = abs(slope_m) kh = 162.6 * q * mu * B / abs_m k = kh / h if h > 0 else np.nan skin = np.nan if h > 0 and phi > 0 and mu > 0 and ct > 0 and rw > 0: skin = 1.151 * ( (p_1hr - p_wf_at_shut_in) / abs_m - np.log10(k / (phi * mu * ct * rw ** 2)) + 3.23 ) p_skin = 0.87 * abs_m * skin if np.isfinite(skin) else np.nan C: Optional[float] = None if early_storage_slope and early_storage_slope > 0: # Unit-slope WBS line: Δp = (q·B / 24·C) · Δt → C = q·B / (24·slope) C = q * B / (24.0 * early_storage_slope) return { "kh": float(kh), "k": float(k), "skin": float(skin) if np.isfinite(skin) else np.nan, "C": float(C) if C is not None else None, "p_skin": float(p_skin) if np.isfinite(p_skin) else np.nan, }
[docs] def dimensionless_storage(C: float, h: float, phi: float, ct: float, rw: float) -> float: r""" Dimensionless wellbore storage :math:`C_D = 0.8936\, C / (\phi\, h\, c_t\, r_w^2)` """ return 0.8936 * C / (phi * h * ct * rw ** 2)
[docs] def radius_of_investigation( k: float, t_hr: float, phi: float, mu: float, ct: float ) -> float: r""" Radius of investigation (Lee 1982): :math:`r_i = 0.0325 \sqrt{k\,t/(\phi\,\mu\,c_t)}` in feet (oilfield units). """ if k <= 0 or t_hr <= 0 or phi <= 0 or mu <= 0 or ct <= 0: return np.nan return 0.0325 * np.sqrt(k * t_hr / (phi * mu * ct))