well-test-analysis
SKILL.md
Pressure Transient Analysis
Computational skill for well test design and interpretation covering drawdown, buildup (Horner), and log-log derivative analysis. Calibrated for Appalachian gas and oil wells.
Field units throughout. Rates in STB/day (oil) or Mscf/day (gas), pressure in psia, permeability in md, length in ft, viscosity in cp.
Module 1 — Radial Flow: Ei Solution (Drawdown)
import math
def ei_function(x):
"""
Exponential integral approximation: Ei(-x) for x > 0.
Valid for small x (|x| < 10).
Uses Abramowitz and Stegun series approximation.
Ei(-x) ≈ ln(x) + gamma + x + x^2/4 + ... (for small x)
In well testing, Ei approximated as ln(x) + 0.80907 for x < 0.01 (log approx).
"""
if x <= 0:
raise ValueError("x must be positive for Ei(-x)")
if x > 10.0:
return 0.0 # negligible for large argument
# Log approximation valid for x < 0.01 (well testing "infinite acting" condition)
if x < 0.01:
return -(math.log(x) + 0.57721566) # = -Ei(-x) approx ln(1/x) - gamma
# Numerical integration approximation (Horner's method for Ei)
# Using the series: Ei(x) = gamma + ln(|x|) + sum(x^n / (n * n!))
result = math.log(x) + 0.57721566
term = x
factorial = 1.0
for n in range(1, 25):
factorial *= n
result += term / (n * factorial)
term *= x
if abs(term / (n * factorial)) < 1e-10:
break
return result
def drawdown_pressure_psi(p_i, q_bpd, B, mu_cp, k_md, h_ft,
phi, ct_psi, r_ft, t_hr):
"""
Ei solution for constant rate drawdown (infinite-acting radial flow):
p_wf = p_i - 141.2*q*B*mu/kh * [-Ei(-r^2*phi*mu*ct/(0.000264*k*t))]
p_i: initial reservoir pressure (psia)
q_bpd: production rate (STB/day for oil; Mscf/day for gas, use q_g*Bg)
B: formation volume factor (RB/STB or Rcf/scf)
mu_cp: viscosity (cp)
k_md: effective permeability (md)
h_ft: net pay thickness (ft)
phi: porosity (fraction)
ct_psi: total compressibility (1/psi)
r_ft: radius at which pressure is calculated (ft); r_w for wellbore
t_hr: elapsed time since start of production (hours)
Returns: flowing pressure at radius r (psia)
"""
x = (948.0 * phi * mu_cp * ct_psi * r_ft**2) / (k_md * t_hr)
if x < 1e-12:
return p_i
ei_val = ei_function(x) # positive value = -Ei(-x)
delta_p = 141.2 * q_bpd * B * mu_cp / (k_md * h_ft) * ei_val
return p_i - delta_p
def log_approximation_valid(phi, mu_cp, ct_psi, r_ft, k_md, t_hr):
"""
Check if log approximation (x < 0.01) is valid.
Condition: t > 94800 * phi * mu * ct * r^2 / k
Returns: True if log approximation applies (standard radial flow analysis valid)
"""
x = (948.0 * phi * mu_cp * ct_psi * r_ft**2) / (k_md * t_hr)
return x < 0.01
Module 2 — Horner Plot: Buildup Test Interpretation
def horner_time_ratio(t_p_hr, delta_t_hr):
"""
Horner time ratio: HTR = (t_p + delta_t) / delta_t
t_p_hr: producing time before shut-in (hours)
delta_t_hr: shut-in time (hours)
Returns: Horner time ratio (dimensionless, plot on x-axis, log scale)
"""
return (t_p_hr + delta_t_hr) / delta_t_hr
def horner_p_star(m_slope, p_1hr, HTR_at_1hr=None):
"""
Extrapolate Horner straight line to HTR=1 to get P* (false pressure).
The semi-log straight line: P_ws = P* - m * log(HTR)
At HTR=1 (infinite shut-in): P_ws = P*
m_slope: slope of P_ws vs. log(HTR) plot (psi/cycle); NEGATIVE for buildup
p_at_x: P_ws at a known log(HTR) value on the straight line
"""
# P* is the y-intercept at log(HTR)=0 -> HTR=1
# Usually read directly from the semi-log plot extrapolation
# If we know P_1hr (P_ws at delta_t = 1 hr on MTR line):
# P* = P_1hr - m * log(t_p + 1) + m*log(t_p) for large t_p:
# P* ≈ P_1hr (when t_p >> 1 hr)
# Return a note that P* requires graphical extrapolation
return {"note": "P* is the y-intercept of Horner semi-log straight line at HTR=1",
"P_1hr_approx": p_1hr}
def permeability_from_horner(q_bpd, B, mu_cp, m_psi_per_cycle, h_ft):
"""
Permeability from Horner MTR slope.
k = 162.6 * q * B * mu / (m * h)
m_psi_per_cycle: magnitude of slope (positive value; slope is negative for buildup)
Returns: effective permeability (md)
"""
return 162.6 * q_bpd * B * mu_cp / (m_psi_per_cycle * h_ft)
def skin_factor_horner(P_1hr_psia, P_wf_psia, m_psi_per_cycle,
k_md, phi, mu_cp, ct_psi, r_w_ft):
"""
Skin factor from Horner plot.
S = 1.1513 * [(P_1hr - P_wf) / m - log(k / (phi*mu*ct*r_w^2)) + 3.2275]
P_1hr_psia: pressure on the MTR straight line at delta_t = 1 hr
P_wf_psia: flowing wellbore pressure just before shut-in
m: Horner slope (magnitude, psi/log cycle)
Returns: skin factor (dimensionless; positive = damage, negative = stimulation)
"""
log_term = math.log10(k_md / (phi * mu_cp * ct_psi * r_w_ft**2))
S = 1.1513 * ((P_1hr_psia - P_wf_psia) / m_psi_per_cycle - log_term + 3.2275)
return S
def flow_efficiency(S, P_avg_psia, P_wf_psia, r_e_ft, r_w_ft):
"""
Flow efficiency: FE = (ideal drawdown) / (actual drawdown)
FE = (P_avg - P_wf - ΔP_skin) / (P_avg - P_wf)
where ΔP_skin = 0.87 * m * S
FE > 1: stimulated well; FE < 1: damaged well
"""
# Need m to compute ΔP_skin = 0.87 * m * S, but we can express as:
# Ideal: Darcy radial flow with S=0
# FE expressed via ln(re/rw):
ln_ratio = math.log(r_e_ft / r_w_ft) - 0.75
FE = (ln_ratio) / (ln_ratio + S)
actual_drawdown = P_avg_psia - P_wf_psia
delta_p_skin = actual_drawdown * (1.0 - FE)
return {"FE": round(FE, 3),
"delta_p_skin_psi": round(delta_p_skin, 1),
"interpretation": "stimulated" if FE > 1.0 else ("damaged" if FE < 1.0 else "undamaged")}
Module 3 — Bourdet Pressure Derivative
def bourdet_derivative(delta_t_list, delta_p_list):
"""
Bourdet pressure derivative: delta_p' = delta_t * d(delta_p)/d(delta_t)
In log-log coordinates: derivative = d(delta_p)/d(ln delta_t)
Uses symmetric three-point numerical differentiation where possible.
Input lists should be in chronological order (increasing delta_t).
Returns: list of (delta_t, delta_p, derivative) tuples
Flow regime signatures (log-log plot):
Wellbore storage: unit slope (delta_p' = 0.5 * delta_p on unit slope line)
Radial flow: flat derivative (delta_p' = m/2.303; m = 162.6*qBmu/kh)
Linear flow: half-slope (delta_p' proportional to sqrt(delta_t))
Bilinear flow: quarter-slope
Closed boundary: derivative rises above radial value
"""
n = len(delta_t_list)
result = []
for i in range(n):
t = delta_t_list[i]
p = delta_p_list[i]
# Forward difference (first point)
if i == 0:
if n > 1:
ln_dt = math.log(delta_t_list[1] / delta_t_list[0])
dp = delta_p_list[1] - delta_p_list[0]
deriv = dp / ln_dt if ln_dt > 1e-12 else 0.0
else:
deriv = 0.0
# Backward difference (last point)
elif i == n - 1:
ln_dt = math.log(delta_t_list[i] / delta_t_list[i-1])
dp = delta_p_list[i] - delta_p_list[i-1]
deriv = dp / ln_dt if ln_dt > 1e-12 else 0.0
# Symmetric (interior points, preferred)
else:
ln1 = math.log(delta_t_list[i] / delta_t_list[i-1])
ln2 = math.log(delta_t_list[i+1] / delta_t_list[i])
dp1 = (delta_p_list[i] - delta_p_list[i-1]) / ln1 if ln1 > 1e-12 else 0.0
dp2 = (delta_p_list[i+1] - delta_p_list[i]) / ln2 if ln2 > 1e-12 else 0.0
deriv = (dp1 * ln2 + dp2 * ln1) / (ln1 + ln2)
result.append((t, p, round(deriv, 4)))
return result
def identify_radial_flow_window(derivatives):
"""
Identify the MTR (middle time region) where the Bourdet derivative is flat.
Looks for consecutive points where derivative stabilizes within 10% of median.
Returns: (t_start, t_end, mean_derivative) or None if no flat region found
"""
if len(derivatives) < 3:
return None
deriv_vals = [d[2] for d in derivatives]
med = sorted(deriv_vals)[len(deriv_vals)//2]
flat = [(d[0], d[2]) for d in derivatives if abs(d[2] - med)/med < 0.10]
if len(flat) < 2:
return None
return {"t_start_hr": flat[0][0], "t_end_hr": flat[-1][0],
"mean_derivative": round(sum(f[1] for f in flat) / len(flat), 4)}
Module 4 — Wellbore Storage Coefficient
def wellbore_storage_from_unit_slope(q_bpd, B, delta_t_hr, delta_p_psi):
"""
WBS coefficient from unit-slope log-log behavior (early time).
On unit slope: delta_p = (q*B / 24*C) * delta_t =>
C = q*B*delta_t / (24*delta_p)
q_bpd: production rate (STB/day)
B: formation volume factor (RB/STB)
Returns: C (bbl/psi)
"""
return q_bpd * B * delta_t_hr / (24.0 * delta_p_psi)
def wellbore_storage_from_volume(V_wellbore_bbl, c_wellbore_psi):
"""
WBS from wellbore compressibility:
C = V_wb * c_wb
V_wellbore_bbl: wellbore fluid volume from perforations to surface (bbl)
c_wellbore_psi: compressibility of wellbore fluid (1/psi)
- Single-phase liquid: ~3e-6 to 15e-6 /psi
- Gas/liquid mix: much higher
Returns: C (bbl/psi)
"""
return V_wellbore_bbl * c_wellbore_psi
def wellbore_storage_end_time(C_bbl_psi, phi, ct_psi, h_ft, r_w_ft, S=0.0):
"""
Approximate end of wellbore storage distortion.
t_wbs_end ≈ 200,000 * C * exp(0.14*S) / (phi * ct * h * r_w^2) [hours]
After this time, the MTR (radial flow) should be identifiable.
"""
numerator = 200000.0 * C_bbl_psi * math.exp(0.14 * S)
denominator = phi * ct_psi * h_ft * r_w_ft**2
return numerator / denominator
Learning Resources
Textbooks
| Author(s) | Title | ISBN | Notes |
|---|---|---|---|
| Lee, Rollins & Spivey | Pressure Transient Testing, SPE Vol. 9 (2003) | 978-1-55563-099-6 | Best undergrad reference |
| Horne, R.N. | Modern Well Test Analysis, 2nd ed. (1995) | 978-0-9626992-1-7 | Stanford course text; beginner-friendly |
| Bourdet, D. | Well Test Analysis (2002) | 978-0-444-50968-2 | Derivative analysis bible |
| Earlougher, R.C. | Advances in Well Test Analysis, SPE Monograph 5 (1977) | 978-0-89520-204-4 | Foundational monograph |
Free Online Resources
| Resource | URL | Content |
|---|---|---|
| SPE PetroWiki — Well Testing | petrowiki.spe.org/Well_testing | Equations, flow regimes, free |
| Kappa Engineering tech notes | kappaeng.com/papers | Rigorous PTA theory, free PDFs |
| Stanford ERE 272 materials | pangea.stanford.edu/ERE | Roland Horne course materials |
| WVU research repository | researchrepository.wvu.edu | Marcellus well test M.S. theses |
WVU Courses
- PNGE 321 — Darcy radial flow, skin concept, material balance
- PNGE 361 — Production engineering context for well test design
Output Format
## Well Test Analysis — [Well Name / API]
### Test Design
| Parameter | Value | Notes |
|-----------|-------|-------|
| Test type | Buildup / Drawdown / DST | |
| Producing time (t_p) | hrs | Before shut-in |
| Shut-in duration | hrs | |
| Rate before shut-in | STB/d or Mscf/d | |
### Horner Analysis (MTR)
| Parameter | Value | Units |
|-----------|-------|-------|
| Slope (m) | psi/cycle | |
| P* (extrapolated) | psia | |
| k | md | |
| kh | md-ft | |
| Skin (S) | dimensionless | |
| Flow efficiency (FE) | dimensionless | |
| Apparent wellbore radius (r_wa) | ft | r_wa = r_w * exp(-S) |
### Log-Log Derivative Diagnosis
[Flow regime identification from derivative plot]
- Wellbore storage period: [t range]
- Radial flow (MTR): [t range], derivative ≈ [value] psi
- Boundary effects: [none / closed boundary / linear / etc.]
**Certainty:** HIGH if MTR clearly identified | MEDIUM if WBS masks MTR
**Bias:** Single-layer radial flow assumed; multilayer/dual-porosity systems require type curve matching
Error Handling
| Condition | Cause | Action |
|---|---|---|
| No flat derivative region | WBS too large or test too short | Extend shut-in; check test duration vs. WBS end time |
| Negative skin | Stimulated well (fracture) | Use fracture type curves (Cinco-Ley, uniform flux) |
| k unreasonably high (>100 md) | Wrong h or rate unit | Check input units; verify net pay |
| P* below P_res | Boundary or depletion effects | Not infinite-acting; use bounded reservoir analysis |
Caveats
- Single-layer infinite-acting radial flow assumed. Multilayer, dual-porosity, or naturally fractured reservoirs require specialized type curves.
- Gas wells require pseudo-pressure. Ei and Horner equations above apply strictly to liquid; for gas, replace p with real-gas pseudo-pressure m(p) for rigorous analysis at high pressure variation.
- Skin includes all near-wellbore damage. Perforation skin, partial penetration, phase-relative permeability reduction, and mechanical damage all combine into the single skin value from Horner analysis.
- MDH plot (Miller-Dyes-Hutchinson) is used when t_p is very long; uses log(delta_t) instead of Horner ratio.
- Production data before shut-in must be at constant rate for Horner analysis. Variable rate history requires superposition or specialized methods.
Weekly Installs
1
Repository
jpfielding/claude.pngeFirst Seen
4 days ago
Security Audits
Installed on
amp1
cline1
opencode1
cursor1
kimi-cli1
codex1