Exerciseschevron_rightChapter 7chevron_right7.10
fitness_center

Exercise 7.10

Automated Petrophysical Report

Level 3
Chapter 7: Well Log Analysis
descriptionProblem

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 deepest DEPTH.
  • 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 : mean PHIE over the net-pay samples.
  • pay_sw : mean SW over 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" when pay_phie > 0.15 and pay_sw < 0.35
  • "poor" when pay_phie < 0.08 or pay_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.

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

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.