Exercise 14.2
MSE Calculation - Drilling Efficiency from Mechanical Specific Energy
Calculate MSE for a well drilled with an 8.5" bit. Plot MSE vs. depth and identify the depth intervals where MSE exceeds 3× the estimated rock strength. What might be causing the inefficiency?
---
We're logging an 8.5" bit through a deep OML-58 hole. Mechanical Specific Energy (MSE) is the single best drilling-efficiency metric: it's the energy spent destroying a unit volume of rock, in psi. Teale's equation is
In a perfectly efficient system MSE would equal the rock's confined compressive strength (CCS). It never does: friction, vibration and dull cutters waste energy, so MSE always runs above CCS. When MSE climbs past 3× the rock strength, the bit is grinding, not cutting, and something is wrong.
The verified compute_mse(torque_ftlbs, rpm, wob_lbf, rop_fthr, bit_diameter_in) is embedded for you (do not edit it or re-derive the physics.) Note that it expects WOB in lbf, while field logs report it in klbf (thousands of lbf). A fixed, self-contained well DataFrame (300 rows, seed 142) is also provided with columns Depth (ft), Torque (ft-lbs), RPM, WOB (klbf) and ROP (ft/hr). The constant BIT_DIAMETER = 8.5 (inches).
Your tasks:
- Write
mse_profile(torque, rpm, wob_klbf, rop, db=BIT_DIAMETER):
- Convert WOB from klbf to lbf (multiply by 1000).
- Call
compute_mse(...)with the converted WOB and return its result
(a numpy.ndarray of MSE values in psi).
- Write
flag_inefficient(mse_arr, ccs_arr, factor=3.0):
- Return a boolean mask that is
Truewherevermse > factor * ccs
(the book's "3× rock strength" inefficiency threshold).
- Using the embedded
ccs_estimate(depth) = 5000 + 1.5 * depthand thewell
DataFrame, build these output variables:
mse_arr=mse_profileover the whole well (psi).mse_mean=float(np.mean(mse_arr)).ccs_arr=ccs_estimate(well["Depth (ft)"]).inefficient_mask=flag_inefficient(mse_arr, ccs_arr)(factor 3.0).inefficient_pct= percentage of cells flagged inefficient
(float(inefficient_mask.mean() * 100)).
> Think about it: the rotary term 480*T*RPM/(Db²*ROP) dominates. It's > inversely proportional to ROP, so a fast, clean cut keeps MSE low while a > stalled bit at low ROP sends MSE soaring. The small 4*WOB/(π*Db²) term is > only a few hundred psi here. Why does forgetting the klbf→lbf conversion barely > move MSE, yet still be a real bug you must get right?
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
# ── Verified Mechanical Specific Energy (Teale) - 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
# ── Constants and confined-compressive-strength model (do not edit) ───────
BIT_DIAMETER = 8.5 # inches
def ccs_estimate(depth):
"""Estimated confined compressive strength (psi), rising with depth."""
return 5000 + 1.5 * np.asarray(depth, dtype=float)
# ── Fixed, self-contained OML-58 drilling log (do not edit) ───────────────
np.random.seed(142)
_n = 300
well = pd.DataFrame({
"Depth (ft)": np.random.uniform(6000, 9000, _n),
"Torque (ft-lbs)": np.random.uniform(2000, 12000, _n),
"RPM": np.random.uniform(90, 150, _n),
"WOB (klbf)": np.random.uniform(18, 40, _n),
"ROP (ft/hr)": np.random.uniform(15, 90, _n),
})
def mse_profile(torque, rpm, wob_klbf, rop, db=BIT_DIAMETER):
"""MSE (psi) from field logs, converting WOB from klbf to lbf first."""
wob_lbf = np.asarray(wob_klbf, dtype=float) * 1000.0
return compute_mse(torque, rpm, wob_lbf, rop, db)
def flag_inefficient(mse_arr, ccs_arr, factor=3.0):
"""Boolean mask: True where MSE exceeds factor x rock strength (CCS)."""
return np.asarray(mse_arr) > factor * np.asarray(ccs_arr)
mse_arr = mse_profile(well["Torque (ft-lbs)"], well["RPM"],
well["WOB (klbf)"], well["ROP (ft/hr)"])
mse_mean = float(np.mean(mse_arr))
ccs_arr = ccs_estimate(well["Depth (ft)"])
inefficient_mask = flag_inefficient(mse_arr, ccs_arr)
inefficient_pct = float(inefficient_mask.mean() * 100)
print("mean MSE (psi):", mse_mean)
print("inefficient cells (%):", inefficient_pct)
lockCopying code is a Full Access feature.