Exerciseschevron_rightChapter 7chevron_right7.6
fitness_center

Exercise 7.6

Multi-Well Comparison

Level 3
Chapter 7: Well Log Analysis
descriptionProblem

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:

  1. 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_FT
  • net_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_ft
  • phie_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)
  1. compare_wells(wells) -> DataFrame: one row per well (a well column 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?

lightbulbHints (0/3)

Stuck? Reveal hints one at a time — they progress from nudge to near-solution.

codeYour solution
main.py
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.