Exercise 7.6
Multi-Well Comparison
A field-study screening run on OML 58: three wells, OD-001, OD-003, OD-007, each with the same triple-combo curves (GR, RT, RHOB, NPHI) but a different oil-pay sand thickness. You wrote the petrophysical pipeline once for a single well; now you run it on every well and rank them.
make_log(seed, pay_top, pay_base) is given; it returns a depth-indexed logs DataFrame for one well, with a clean oil-pay sand between pay_top and pay_base ft and shale / wet sand elsewhere. The three wells are already built for you in WELLS.
Write two functions:
evaluate_well(logs) -> dict: run the full single-well pipeline with the
base parameters and cutoffs, returning:
gross_ft: logged interval thickness =len(logs) * STEP_FTnet_pay_ft: feet passing all three cutoffs
(Vsh < 0.40 and PHIE > 0.08 and Sw < 0.60), counted at STEP_FT
n2g: net-to-gross =net_pay_ft / gross_ftphie_pay: mean effective porosity over the net-pay feet (0.0 if no pay)sw_pay: mean water saturation over the net-pay feet (0.0 if no pay)
compare_wells(wells) -> DataFrame: one row per well (awellcolumn plus
the five metrics above), sorted by net_pay_ft descending, index reset.
The pipeline (base params): linear Vshale from the GR P5/P95 endpoints, density porosity (rho_ma=2.65, rho_fl=1.0), PHIA = (PHID+NPHI)/2, PHIE = PHIA*(1-Vsh), then Archie Sw with a=0.81, m=2, n=2, Rw=0.04 (only where PHIE > 0.01; elsewhere Sw = 1).
The thickest pay sand should rank first. Which well would you recommend first for completion?
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
STEP_FT = 0.5
# Base petrophysical parameters / cutoffs (shared by every well).
RHO_MATRIX = 2.65
RHO_FLUID = 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 make_log(seed, pay_top, pay_base):
"""Synthetic triple-combo log for one OML 58 well.
Zones: 7000-7200 shale cap, 7200..pay_top wet sand,
pay_top..pay_base OIL PAY sand, pay_base..(pay_base+100) shale,
(pay_base+100)..7850 wet sand. STEP_FT = 0.5.
"""
rng = np.random.RandomState(seed)
depth = np.arange(7000.0, 7850.0, 0.5)
cond = [
depth < 7200,
depth < pay_top,
depth < pay_base,
depth < pay_base + 100,
depth >= pay_base + 100,
]
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])
return pd.DataFrame({
"DEPTH": depth,
"GR": gr + rng.normal(0, 4.0, len(depth)),
"RT": rt * np.exp(rng.normal(0, 0.10, len(depth))),
"RHOB": rhob + rng.normal(0, 0.02, len(depth)),
"NPHI": np.clip(nphi + rng.normal(0, 0.015, len(depth)), 0.0, 1.0),
})
# (well, seed, pay_top_ft, pay_base_ft) -> OD-007 has the thickest pay sand.
WELL_SPECS = [
("OD-001", 11, 7300.0, 7500.0), # 200 ft pay sand
("OD-003", 22, 7350.0, 7470.0), # 120 ft pay sand
("OD-007", 33, 7250.0, 7550.0), # 300 ft pay sand
]
WELLS = {name: make_log(seed, top, base) for name, seed, top, base in WELL_SPECS}
def evaluate_well(logs):
"""Run the full single-well pipeline; return the metrics dict."""
gr = logs["GR"]
rt = logs["RT"]
rhob = logs["RHOB"]
nphi = logs["NPHI"]
# Linear Vshale from the GR P5/P95 endpoints.
gr_clean = gr.quantile(0.05)
gr_shale = gr.quantile(0.95)
igr = (gr - gr_clean) / (gr_shale - gr_clean)
vsh_frac = igr.clip(0, 1)
# Density porosity, available porosity, effective porosity.
phid_frac = ((RHO_MATRIX - rhob) / (RHO_MATRIX - RHO_FLUID)).clip(0, 0.45)
phia_frac = (phid_frac + nphi) / 2
phie_frac = (phia_frac * (1 - vsh_frac)).clip(0, 0.40)
# Archie Sw only where porosity is meaningful; elsewhere Sw = 1.
sw_frac = pd.Series(np.ones(len(logs)), index=logs.index)
mask = phie_frac > 0.01
sw_frac[mask] = (
((A * RW) / (phie_frac[mask] ** M * rt[mask])) ** (1 / N)
).clip(0, 1)
# Net pay = feet passing all three cutoffs.
pay = (vsh_frac < VSH_CUT) & (phie_frac > PHIE_CUT) & (sw_frac < SW_CUT)
net_pay_ft = float(int(pay.sum()) * STEP_FT)
gross_ft = float(len(logs) * STEP_FT)
n2g = net_pay_ft / gross_ft if gross_ft > 0 else 0.0
phie_pay = float(phie_frac[pay].mean()) if pay.any() else 0.0
sw_pay = float(sw_frac[pay].mean()) if pay.any() else 0.0
return {
"gross_ft": gross_ft,
"net_pay_ft": net_pay_ft,
"n2g": n2g,
"phie_pay": phie_pay,
"sw_pay": sw_pay,
}
def compare_wells(wells):
"""Return one row per well, sorted by net_pay_ft descending."""
rows = []
for name, logs in wells.items():
row = {"well": name}
row.update(evaluate_well(logs))
rows.append(row)
cols = ["well", "gross_ft", "net_pay_ft", "n2g", "phie_pay", "sw_pay"]
df = pd.DataFrame(rows, columns=cols)
return df.sort_values("net_pay_ft", ascending=False).reset_index(drop=True)
table = compare_wells(WELLS)
print(table.to_string(index=False))
print("Recommend first:", table["well"].iloc[0])
lockCopying code is a Full Access feature.