Exerciseschevron_rightChapter 14chevron_right14.10
fitness_center

Exercise 14.10

Drilling Performance Dashboard - One-Page Well Summary

Level 3
Chapter 14: Drilling Analytics
descriptionProblem

Build a comprehensive drilling dashboard for a single well that displays: time-depth curve vs. plan and offset, ROP vs. depth with formation tops, MSE and efficiency vs. depth, stick-slip severity vs. depth, and a cost summary table. Format it as a one-page report.

---

This is the capstone for the drilling-analytics chapter. The exercise text asks for a full one-page report with plots; here we build the numeric engine behind that report: a single aggregator that reduces a whole drilling dataset to the scalar KPIs a drilling supervisor reads at the top of the morning report. (Plots are not gradable; the summary numbers are.)

The verified compute_mse and detect_stick_slip functions from the chapter are embedded for you, along with the exact OML drilling-data generator (np.random.seed(42), 5000 rows) and these constants:

  • BIT_DIAMETER = 8.5 (inches)
  • TIME_INTERVAL_HR = 2 / 60 (2-minute samples, in hours)
  • SPREAD_RATE_PER_HR = 280000 / 24 (a 280k/dayrigspread,in280k/day rig spread, in /hr)
  • ccs_estimate(depth) = 5000 + 1.5 * depth (confined compressive strength, psi)

Do not modify the embedded functions, generator, or constants. Use them as given.

Your task is to write the aggregator:

def build_dashboard(df, db=BIT_DIAMETER, interval_hr=TIME_INTERVAL_HR,
                    spread_rate=SPREAD_RATE_PER_HR):
    ...

It returns a single dict of scalar summary metrics with exactly these 7 keys (all plain Python floats):

keymeaning
total_footageDepth (ft) max minus min (total interval drilled)
total_hourslen(df) * interval_hr (hours on bottom)
avg_ropmean of ROP (ft/hr)
avg_msemean MSE via compute_mse (pass WOB (klbf) * 1000 as lbf)
avg_efficiency_pctmean of clip(ccs_estimate(depth) / mse * 100, 0, 100)
stick_slip_pct100 * detect_stick_slip(Torque, RPM).mean()
total_cost_usdspread_rate * total_hours

Then run it on the embedded seed-42 drilling_data and expose these output variables for the grader:

  • dashboard: the full dict returned by build_dashboard(drilling_data)
  • total_footage, avg_rop, avg_mse, stick_slip_pct, total_cost_usd:

the individual floats pulled out of dashboard

> Think about it: total_cost_usd must equal spread_rate total_hours > exactly, and total_hours must equal len(df) interval_hr exactly; these > are definitions, not approximations. Why does avg_efficiency_pct have to sit > inside [0, 100] no matter how rough the data is, and what does an avg_mse > many times larger than the rock's ccs_estimate tell you about how the well > was drilled?

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


# ── Verified compute_mse (chapter 14, do not edit) ───────────────────────
def compute_mse(torque_ftlbs, rpm, wob_lbf, rop_fthr, bit_diameter_in):
    """
    Calculate Mechanical Specific Energy.

    Parameters
    ----------
    torque_ftlbs : array-like
        Torque (ft-lbs).
    rpm : array-like
        Rotary speed (rev/min).
    wob_lbf : array-like
        Weight on bit (lbf). Note: input in lbf, not klbf.
    rop_fthr : array-like
        Rate of penetration (ft/hr).
    bit_diameter_in : float
        Bit diameter (inches).

    Returns
    -------
    numpy.ndarray
        MSE values (psi).
    """
    torque = np.asarray(torque_ftlbs, dtype=float)
    r = np.asarray(rpm, dtype=float)
    w = np.asarray(wob_lbf, dtype=float)
    rop = np.asarray(rop_fthr, dtype=float)
    db = bit_diameter_in

    # Avoid division by zero
    rop_safe = np.where(rop > 0, rop, 1e-6)

    rotary_term = 480 * torque * r / (db**2 * rop_safe)
    wob_term = 4 * w / (np.pi * db**2)

    return rotary_term + wob_term


# ── Verified detect_stick_slip (chapter 14, do not edit) ─────────────────
def detect_stick_slip(torque, rpm, window=50, torque_cv_threshold=0.3):
    """
    Detect stick-slip vibration from surface measurements.

    Stick-slip is indicated by high coefficient of variation
    in torque at relatively stable surface RPM.

    Parameters
    ----------
    torque : array-like
        Torque measurements (ft-lbs).
    rpm : array-like
        Surface RPM measurements.
    window : int
        Rolling window size.
    torque_cv_threshold : float
        Coefficient of variation threshold for flagging.

    Returns
    -------
    pandas.Series
        Boolean mask where True indicates stick-slip.
    """
    tq = pd.Series(torque)
    r = pd.Series(rpm)

    tq_mean = tq.rolling(window).mean()
    tq_std = tq.rolling(window).std()
    rpm_std = r.rolling(window).std()

    # Coefficient of variation in torque
    tq_cv = tq_std / tq_mean.replace(0, np.nan)

    # RPM should be relatively stable (driller holding steady)
    rpm_stable = rpm_std < 15

    return (tq_cv > torque_cv_threshold) & rpm_stable


# ── Verified OML drilling-data generator (chapter 14, do not edit) ────────
np.random.seed(42)
n_points = 5000

depth_start = 500
rop_base = np.piecewise(
    np.linspace(0, 1, n_points),
    [np.linspace(0, 1, n_points) < 0.3,
     (np.linspace(0, 1, n_points) >= 0.3) & (np.linspace(0, 1, n_points) < 0.6),
     np.linspace(0, 1, n_points) >= 0.6],
    [lambda x: 120 - 80*x,        # soft formation, fast drilling
     lambda x: 45 + 10*np.sin(20*x),  # medium formation, variable
     lambda x: 25 - 10*(x-0.6)]    # hard formation, slow
)
rop = rop_base + np.random.normal(0, 5, n_points)
rop = np.clip(rop, 5, 200)

time_interval_hr = 2 / 60  # 2 minutes in hours
depth = depth_start + np.cumsum(rop * time_interval_hr)

wob = np.where(depth < 4000, 15 + np.random.normal(0, 2, n_points),
       np.where(depth < 7000, 25 + np.random.normal(0, 3, n_points),
                35 + np.random.normal(0, 3, n_points)))
wob = np.clip(wob, 5, 50)

rpm = np.where(depth < 4000, 140 + np.random.normal(0, 10, n_points),
       np.where(depth < 7000, 120 + np.random.normal(0, 8, n_points),
                100 + np.random.normal(0, 8, n_points)))
rpm = np.clip(rpm, 40, 180)

bit_diameter_in = 8.5
torque = 0.125 * (wob * 1000.0) * (bit_diameter_in / 12.0) + np.random.normal(0, 250, n_points)
torque = np.clip(torque, 800, 30000)

spp = 2000 + 0.15 * depth + np.random.normal(0, 100, n_points)

stick_slip_mask = (depth > 6000) & (depth < 6500)
torque[stick_slip_mask] = (
    torque[stick_slip_mask] + 3500 * np.sin(np.linspace(0, 60, stick_slip_mask.sum()))
)

drilling_data = pd.DataFrame({
    "Depth (ft)": np.round(depth, 1),
    "ROP (ft/hr)": np.round(rop, 1),
    "WOB (klbf)": np.round(wob, 1),
    "RPM": np.round(rpm, 0).astype(int),
    "Torque (ft-lbs)": np.round(torque, 0).astype(int),
    "SPP (psi)": np.round(spp, 0).astype(int),
})


# ── Dashboard constants (do not edit) ────────────────────────────────────
BIT_DIAMETER = 8.5               # bit diameter, inches
TIME_INTERVAL_HR = 2 / 60        # 2-minute sample interval, hours
SPREAD_RATE_PER_HR = 280000 / 24  # $280k/day rig spread, $/hr


def ccs_estimate(depth):
    """Confined compressive strength of the rock (psi), rising with depth."""
    return 5000 + 1.5 * depth


def build_dashboard(df, db=BIT_DIAMETER, interval_hr=TIME_INTERVAL_HR,
                    spread_rate=SPREAD_RATE_PER_HR):
    """Reduce a drilling dataset to a dict of scalar summary KPIs.

    Returns a dict with exactly these 7 float keys:
      total_footage      = Depth max - Depth min (ft)
      total_hours        = len(df) * interval_hr (hr)
      avg_rop            = mean ROP (ft/hr)
      avg_mse            = mean MSE (psi), via compute_mse with WOB in lbf
      avg_efficiency_pct = mean of clip(ccs/mse*100, 0, 100) (%)
      stick_slip_pct     = 100 * detect_stick_slip(Torque, RPM).mean() (%)
      total_cost_usd     = spread_rate * total_hours ($)
    """
    depth = df["Depth (ft)"]
    rop = df["ROP (ft/hr)"]
    wob = df["WOB (klbf)"]
    rpm = df["RPM"]
    torque = df["Torque (ft-lbs)"]
    n = len(df)

    total_footage = float(depth.max() - depth.min())
    total_hours = float(n * interval_hr)
    avg_rop = float(rop.mean())

    mse = compute_mse(torque, rpm, wob * 1000.0, rop, db)
    avg_mse = float(np.mean(mse))

    ccs = ccs_estimate(depth)
    efficiency = np.clip(ccs / mse * 100.0, 0, 100)
    avg_efficiency_pct = float(np.mean(efficiency))

    stick_slip_pct = float(100.0 * detect_stick_slip(torque, rpm).mean())

    total_cost_usd = float(spread_rate * total_hours)

    return {
        "total_footage": total_footage,
        "total_hours": total_hours,
        "avg_rop": avg_rop,
        "avg_mse": avg_mse,
        "avg_efficiency_pct": avg_efficiency_pct,
        "stick_slip_pct": stick_slip_pct,
        "total_cost_usd": total_cost_usd,
    }


dashboard = build_dashboard(drilling_data)
total_footage = dashboard["total_footage"]
avg_rop = dashboard["avg_rop"]
avg_mse = dashboard["avg_mse"]
stick_slip_pct = dashboard["stick_slip_pct"]
total_cost_usd = dashboard["total_cost_usd"]

print("DRILLING PERFORMANCE DASHBOARD")
print("=" * 45)
print(f"  Total footage:     {dashboard['total_footage']:,.1f} ft")
print(f"  Total hours:       {dashboard['total_hours']:,.1f} hr")
print(f"  Avg ROP:           {dashboard['avg_rop']:.2f} ft/hr")
print(f"  Avg MSE:           {dashboard['avg_mse']:,.0f} psi")
print(f"  Avg efficiency:    {dashboard['avg_efficiency_pct']:.1f}%")
print(f"  Stick-slip:        {dashboard['stick_slip_pct']:.1f}% of time")
print(f"  Total cost:        ${dashboard['total_cost_usd']/1e6:.2f}M")

lockCopying code is a Full Access feature.