Exerciseschevron_rightChapter 7chevron_right7.1
fitness_center

Exercise 7.1

GR Endpoint Sensitivity

Level 2
Chapter 7: Well Log Analysis
descriptionProblem

On OD-003 (OML 58) the volume of shale you compute (and therefore your net sand) hinges on two numbers you pick by eye: the clean (sand) and shale gamma-ray endpoints. Move them a few API units and the sand count swings. This exercise makes that sensitivity explicit.

You are given the canonical OD-003 log (logs). Write three functions:

  1. vshale_linear(gr, gr_clean, gr_shale): linear gamma-ray index, clipped

to [0, 1]. Works on a single value, a numpy array, or a Series:

`` IGR = (GR - gr_clean) / (gr_shale - gr_clean) Vsh = clip(IGR, 0, 1) ``

  1. gr_endpoints(gr, lo=0.05, hi=0.95): pick endpoints from the data by

quantile: return (gr_clean, gr_shale) as the lo and hi quantiles of gr. This is the "let the histogram choose" approach.

  1. net_sand_ft(logs, gr_clean, gr_shale, vsh_cutoff=0.40, step_ft=STEP_FT):

compute Vsh from logs["GR"] with the given endpoints and return the footage where Vsh < vsh_cutoff, i.e. count(passing) * step_ft.

Then compare: the percentile endpoints versus a tighter hand-picked pair (30 clean / 65 shale). The lower shale endpoint lifts Vsh everywhere, so more feet fail the cutoff and the net sand shrinks. That gap is the risk hiding behind a pick-by-eye endpoint.

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


def vshale_linear(gr, gr_clean, gr_shale):
    """Linear GR index, clipped to [0, 1]. Works on a value/array/Series."""
    igr = (gr - gr_clean) / (gr_shale - gr_clean)
    return np.clip(igr, 0.0, 1.0)


def gr_endpoints(gr, lo=0.05, hi=0.95):
    """Return (gr_clean, gr_shale) as the lo and hi quantiles of gr."""
    gr = pd.Series(gr)
    return float(gr.quantile(lo)), float(gr.quantile(hi))


def net_sand_ft(logs, gr_clean, gr_shale, vsh_cutoff=0.40, step_ft=STEP_FT):
    """Footage where Vsh < vsh_cutoff = count(passing) * step_ft."""
    vsh = vshale_linear(logs["GR"], gr_clean, gr_shale)
    n_pass = int((vsh < vsh_cutoff).sum())
    return n_pass * step_ft


gr_clean, gr_shale = gr_endpoints(logs["GR"])
print(f"percentile endpoints: clean={gr_clean:.1f}, shale={gr_shale:.1f}")
print(f"net sand (percentile): {net_sand_ft(logs, gr_clean, gr_shale)} ft")
print(f"net sand (30/65):      {net_sand_ft(logs, 30.0, 65.0)} ft")

lockCopying code is a Full Access feature.