Source code for welltest_pta.analysis.mdh
"""
welltest_pta.analysis.mdh
=========================
Miller–Dyes–Hutchinson (1950) semi-log buildup analysis.
The MDH plot is :math:`p_{ws}` vs :math:`\\log_{10}(\\Delta t)`. It is the
short-shut-in approximation of the Horner method, valid when
:math:`\\Delta t \\ll t_p` (i.e. when :math:`\\log\\big[(t_p+\\Delta t)/\\Delta t\\big]
\\approx \\log(t_p) - \\log(\\Delta t)`).
The MDH slope is **identical in magnitude** to the Horner slope and is used
for permeability and skin calculation. MDH is preferred when :math:`t_p` is
ill-defined (multiple drawdown periods) and the user wishes to avoid Horner-
time bias.
"""
from __future__ import annotations
import numpy as np
[docs]
def mdh_extrapolation(
p: np.ndarray,
t_hr: np.ndarray,
fit_start_frac: float = 0.30,
fit_end_frac: float = 0.85,
) -> dict[str, float]:
"""
Linear regression of :math:`p_{ws}` vs :math:`\\log_{10}(\\Delta t)` on
the IARF window.
Returns dict with ``slope_m`` (psi/cycle), ``intercept_p1hr``
(extrapolated pressure at :math:`\\Delta t=1` hr — used for skin),
and ``r2``.
"""
p = np.asarray(p, dtype=float)
t_hr = np.asarray(t_hr, dtype=float)
dt = t_hr - t_hr[0]
dt = np.where(dt <= 0, 1e-6, dt)
log_dt = np.log10(dt)
n_seg = len(p)
i0 = max(1, int(n_seg * fit_start_frac))
i1 = max(i0 + 5, int(n_seg * fit_end_frac))
if i1 - i0 < 5:
return {"slope_m": np.nan, "intercept_p1hr": np.nan, "r2": np.nan,
"fit_start_idx": -1, "fit_end_idx": -1}
x_fit, y_fit = log_dt[i0:i1], p[i0:i1]
A = np.vstack([x_fit, np.ones_like(x_fit)]).T
(m, b), *_ = np.linalg.lstsq(A, y_fit, rcond=None)
y_pred = m * x_fit + b
ss_res = float(np.sum((y_fit - y_pred) ** 2))
ss_tot = float(np.sum((y_fit - y_fit.mean()) ** 2))
r2 = 1.0 - ss_res / ss_tot if ss_tot > 0 else 0.0
# p_1hr = m * log10(1) + b = b (intercept at Δt = 1 hr)
return {
"slope_m": float(m),
"intercept_p1hr": float(b),
"r2": float(r2),
"fit_start_idx": int(i0),
"fit_end_idx": int(i1),
}