Exerciseschevron_rightChapter 14chevron_right14.9
fitness_center

Exercise 14.9

Cost per Foot Optimization - When to Trip for a Fresh Bit

Level 3
Chapter 14: Drilling Analytics
descriptionProblem

Bit replacement requires a round trip (pull out and run back in), which costs time. A dull bit drills slower but avoids the trip cost. Write a function that calculates the optimal footage to drill before replacing the bit, given: bit cost, trip time, spread rate, and the ROP decline curve of the current bit.

---

We'll work the OML-58 bit-economics version of this problem. A PDC bit drilling a hard interval loses bite as its cutters wear, so model its ROP decline as exponential in the footage f already drilled on this run:

ROP(f) = rop0 * exp(-decline * f)        # ft/hr

The bit starts fast at rop0 and slows as f grows. Drilling the next interval takes longer and longer. The time to drill footage F on one bit run is the integral of 1/ROP from 0 to F, which has a closed form:

drill_hours(F) = (exp(decline * F) - 1) / (rop0 * decline)

Replacing the bit forces a round trip (trip_hours of pure non-productive time), and the rig burns money the whole time at the spread rate. The total time charged to this bit run is trip_hours + drill_hours(F), and the cost per foot of drilling F ft before tripping is:

CPF(F) = (bit_cost + spread_rate * (trip_hours + drill_hours(F))) / F

Trip too early and the fixed trip cost is amortized over too little footage (CPF high). Trip too late and you grind ahead with a worn, slow bit, burning spread rate per foot (CPF high again). The optimum is the interior bottom of this U-shaped curve.

The OML-58 constants are: BIT_COST = 25000.0 (USD), TRIP_HOURS = 18.0 (hr), SPREAD_RATE_PER_HR = 280000 / 24 (USD/hr, from a $280k/day rig), ROP0 = 80.0 (ft/hr), DECLINE = 0.00035 (1/ft).

Your tasks:

  1. Write cost_per_foot(F, bit_cost, trip_hours, spread_rate, rop0, decline):
  • Return the closed-form CPF(F) above as a float (USD/ft).
  1. Write `optimal_footage(bit_cost, trip_hours, spread_rate, rop0, decline,

f_grid=np.arange(500, 8000, 10.0))`:

  • Evaluate cost_per_foot over f_grid and return the tuple

(F_opt, cpf_min): the grid footage that minimizes CPF and that minimum cost per foot.

  1. Compute the output variables:
  • cpf_at_2300 = cost_per_foot(2300.0, BIT_COST, TRIP_HOURS, SPREAD_RATE_PER_HR, ROP0, DECLINE)
  • f_opt, cpf_min = optimal_footage(BIT_COST, TRIP_HOURS, SPREAD_RATE_PER_HR, ROP0, DECLINE)

> Think about it: with these constants the optimum lands near 2300 ft at > about $326/ft. Why does a cheaper trip (trip_hours smaller) push the > optimum to a shorter run, i.e. you trip more often when tripping is cheap? > And why does a faster-wearing bit (bigger decline) want to be replaced > sooner?

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


# ── OML-58 bit-economics constants (do not edit) ─────────────────────────
BIT_COST = 25000.0              # cost of one PDC bit, USD
TRIP_HOURS = 18.0              # round-trip time to change the bit, hr
SPREAD_RATE_PER_HR = 280000 / 24  # rig spread rate, USD/hr ($280k/day)
ROP0 = 80.0                    # initial rate of penetration, ft/hr
DECLINE = 0.00035             # ROP exponential decline, 1/ft


def cost_per_foot(F, bit_cost, trip_hours, spread_rate, rop0, decline):
    """Cost per foot of drilling F ft on one bit run before tripping.

    ROP declines exponentially with footage f: ROP(f) = rop0 * exp(-decline*f).
    Drilling time is the closed-form integral of 1/ROP from 0 to F:
        drill_hours = (exp(decline*F) - 1) / (rop0 * decline)
    Total charged time = trip_hours + drill_hours, and
        CPF = (bit_cost + spread_rate * total_hours) / F      [USD/ft]
    """
    drill_hours = (np.exp(decline * F) - 1.0) / (rop0 * decline)
    total_hours = trip_hours + drill_hours
    return float((bit_cost + spread_rate * total_hours) / F)


def optimal_footage(bit_cost, trip_hours, spread_rate, rop0, decline,
                    f_grid=np.arange(500, 8000, 10.0)):
    """Footage that minimizes cost per foot, scanned over f_grid.

    Returns (F_opt, cpf_min): the grid footage with the lowest CPF and that
    minimum cost per foot (USD/ft).
    """
    cpfs = np.array([
        cost_per_foot(F, bit_cost, trip_hours, spread_rate, rop0, decline)
        for F in f_grid
    ])
    i = int(np.argmin(cpfs))
    return float(f_grid[i]), float(cpfs[i])


cpf_at_2300 = cost_per_foot(2300.0, BIT_COST, TRIP_HOURS,
                            SPREAD_RATE_PER_HR, ROP0, DECLINE)
f_opt, cpf_min = optimal_footage(BIT_COST, TRIP_HOURS,
                                 SPREAD_RATE_PER_HR, ROP0, DECLINE)

print("CPF at 2300 ft (USD/ft):", cpf_at_2300)
print("optimal footage (ft):", f_opt, "  min CPF (USD/ft):", cpf_min)

lockCopying code is a Full Access feature.