Exercise 7.10
Automated Petrophysical Report
This is the function you hand your team lead. Everything you built in this chapter (Vshale, effective porosity, Archie water saturation, the net-pay cutoffs) collapses into one summary dict per well. Run it on OD-007 (OML 58) and the morning report writes itself.
Write petrophysical_report(logs) returning a dict with exactly these keys:
depth_range:(top_ft, base_ft)tuple: shallowest and deepestDEPTH.gr_endpoints:(gr_clean, gr_shale)tuple: the 5th / 95th percentile of
GR used as the linear-Vshale endpoints.
net_pay_ft: feet of net pay under the base cutoffs
(VSHALE < 0.40 and PHIE > 0.08 and SW < 0.60), i.e. count(passing) × STEP_FT.
n2g: net-to-gross:net_pay_ft / gross_ft, where
gross_ft = (DEPTH.max() − DEPTH.min()) + STEP_FT is the full logged interval.
pay_phie: meanPHIEover the net-pay samples.pay_sw: meanSWover the net-pay samples.pay_hpt: hydrocarbon pore thickness of the pay:
net_pay_ft × pay_phie × (1 − pay_sw).
quality: a flag from the pay-zone properties:"good"whenpay_phie > 0.15andpay_sw < 0.35"poor"whenpay_phie < 0.08orpay_sw > 0.60"moderate"otherwise.
Run the full pipeline yourself (linear Vshale from the GR percentiles, density porosity → PHIA → effective PHIE, Archie SW only where PHIE > 0.01). Use the chapter base constants: RHO_MATRIX=2.65, RHO_FLUID=1.0, Archie a=0.81, m=2.0, n=2.0, Rw=0.04.
On the canonical log this is a clean, oil-charged sand: low water saturation, healthy porosity, so the report should come back "good". Make the dict complete and the thresholds sensible; this is the artifact a real evaluation hands off.
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
import pandas as pd
np.random.seed(7)
depth = np.arange(7000.0, 7850.0, 0.5)
_cond = [depth < 7200, depth < 7400, depth < 7600, depth < 7700, depth >= 7700]
_gr = np.select(_cond, [105.0, 40.0, 32.0, 110.0, 45.0])
_rt = np.select(_cond, [4.0, 2.2, 35.0, 4.0, 2.5])
_rhob = np.select(_cond, [2.54, 2.31, 2.26, 2.55, 2.33])
_nphi = np.select(_cond, [0.33, 0.21, 0.17, 0.34, 0.22])
logs = pd.DataFrame({
"DEPTH": depth,
"GR": _gr + np.random.normal(0, 4.0, len(depth)),
"RT": _rt * np.exp(np.random.normal(0, 0.10, len(depth))),
"RHOB": _rhob + np.random.normal(0, 0.02, len(depth)),
"NPHI": np.clip(_nphi + np.random.normal(0, 0.015, len(depth)), 0.0, 1.0),
})
STEP_FT = 0.5
# Petrophysical constants (chapter base case).
RHO_MATRIX, RHO_FLUID = 2.65, 1.0
A, M, N, RW = 0.81, 2.0, 2.0, 0.04
VSH_CUT, PHIE_CUT, SW_CUT = 0.40, 0.08, 0.60
def petrophysical_report(logs):
"""Return the one-well summary dict (see prompt for the required keys)."""
df = logs
# Linear Vshale from GR percentile endpoints.
gr_clean = float(df["GR"].quantile(0.05))
gr_shale = float(df["GR"].quantile(0.95))
igr = (df["GR"] - gr_clean) / (gr_shale - gr_clean)
vsh = igr.clip(0.0, 1.0)
# Density porosity -> average -> effective porosity.
phid = ((RHO_MATRIX - df["RHOB"]) / (RHO_MATRIX - RHO_FLUID)).clip(0.0, 0.45)
phia = (phid + df["NPHI"]) / 2.0
phie = (phia * (1.0 - vsh)).clip(0.0, 0.40)
# Archie water saturation, only where porosity is meaningful.
sw = np.where(
phie > 0.01,
((A * RW) / (phie ** M * df["RT"])) ** (1.0 / N),
1.0,
)
sw = pd.Series(sw, index=df.index).clip(0.0, 1.0)
# Base net-pay cutoffs.
pay = (vsh < VSH_CUT) & (phie > PHIE_CUT) & (sw < SW_CUT)
net_pay_ft = int(pay.sum()) * STEP_FT
# Net-to-gross over the full logged interval.
top_ft = float(df["DEPTH"].min())
base_ft = float(df["DEPTH"].max())
gross_ft = (base_ft - top_ft) + STEP_FT
n2g = net_pay_ft / gross_ft
# Pay-zone properties (no pay -> a dry, "poor" well).
if net_pay_ft > 0:
pay_phie = float(phie[pay].mean())
pay_sw = float(sw[pay].mean())
else:
pay_phie = 0.0
pay_sw = 1.0
pay_hpt = net_pay_ft * pay_phie * (1.0 - pay_sw)
if pay_phie > 0.15 and pay_sw < 0.35:
quality = "good"
elif pay_phie < 0.08 or pay_sw > 0.60:
quality = "poor"
else:
quality = "moderate"
return {
"depth_range": (top_ft, base_ft),
"gr_endpoints": (gr_clean, gr_shale),
"net_pay_ft": net_pay_ft,
"n2g": n2g,
"pay_phie": pay_phie,
"pay_sw": pay_sw,
"pay_hpt": pay_hpt,
"quality": quality,
}
report = petrophysical_report(logs)
print(report)
print(f"net pay = {report['net_pay_ft']:.1f} ft "
f"N/G = {report['n2g']:.3f} "
f"PHIE = {report['pay_phie']:.3f} Sw = {report['pay_sw']:.3f} "
f"HPT = {report['pay_hpt']:.2f} ft -> {report['quality'].upper()}")
lockCopying code is a Full Access feature.