Exerciseschevron_rightChapter 11chevron_right11.5
fitness_center

Exercise 11.5

Time-Step Sensitivity of the Implicit Simulator

Level 2
Chapter 11: Reservoir Simulation
descriptionProblem

Using the implicit simulator, run the same problem with dt = 0.1, 1, 10, and 30 days. Compare the final pressure profiles. How large can you make the time step before accuracy degrades noticeably?

---

Field OML 58 has a single producer on cell 0 of the 1D model, drawing the reservoir down against a fixed bottom-hole pressure of 1000 psi. You want to run the same 180-day depletion several times, each with a different time step dt_days, and ask the engineer's perennial question: how big can I make the time step before the answer goes wrong?

The simulator here is fully implicit, which means it is unconditionally stable: unlike an explicit scheme, it will not blow up no matter how coarse the time step is. But stable is not the same as accurate: a coarse dt_days still solves the equations, it just solves them for a slightly different problem, so the booked average pressure drifts.

The Reservoir1D simulator and a build_model() helper are embedded; do not modify them. Reservoir constants are at the top of the file (NX, K_MD, PI_PSI, etc.). The producer is a BHP-controlled well, so the rate it pulls depends on the evolving pressure field; that is exactly what makes the answer sensitive to dt_days.

Your tasks:

  1. Write final_pavg(dt_days).
  • Build a fresh model with build_model() (always start from PI_PSI).
  • Add the producer: m.add_well(cell=WELL_CELL, bhp_psi=BHP_PSI).
  • Run the depletion: m.run(SIM_DAYS, dt_days) (SIM_DAYS = 180).
  • Return the mean reservoir pressure float(np.mean(m.P)) in psi.
  1. Write sweep(dt_list) returning a dict mapping each dt_days to its

final_pavg(dt_days) ({dt: final_pavg(dt) for dt in dt_list}).

  1. Call sweep(DT_LIST) with DT_LIST = [1, 10, 30, 90] and store it in

pavg_by_dt.

> Think about it: every dt_days returns a finite, physical pressure > (between the 1000 psi BHP and the 3000 psi initial pressure); that is the > unconditional stability of the implicit method. The fine-step answers > converge: final_pavg(1) and final_pavg(10) agree to a few psi out of an > ~800 psi drawdown. The coarse dt_days = 90 is still perfectly stable but > reads about 56 psi too high; it under-resolves the early transient and > under-depletes the field. Compare this with the explicit method from > Exercise 11.4, whose CFL limit for this grid is only about 0.16 days: an > explicit run at dt_days = 1 (let alone 90) would oscillate and diverge. The > implicit method buys you giant, stable time steps; accuracy is the only end > thing you trade away.

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
from scipy.sparse import diags
from scipy.sparse.linalg import spsolve


# ── Embedded 1D implicit single-phase simulator (do not edit) ────────────
class Reservoir1D:
    """1D single-phase implicit simulator (harmonic-mean T, real area, Peaceman wells)."""
    def __init__(self, nx, length_ft, width_ft, height_ft, k_md, phi, ct_psi, mu_cp, Pi_psi, Bo=1.0):
        self.nx = nx; self.dx = length_ft / nx
        self.area = width_ft * height_ft; self.vb = self.area * self.dx; self.height = height_ft
        self.k = np.full(nx, k_md, float) if np.isscalar(k_md) else np.asarray(k_md, float)
        self.phi = phi; self.ct = ct_psi; self.mu = mu_cp; self.Bo = Bo
        self.P = np.full(nx, Pi_psi, float); self.Pi = Pi_psi; self.beta = 1.127e-3; self.wells = []
    def _T(self, i, j):
        kh = 2*self.k[i]*self.k[j]/(self.k[i]+self.k[j])
        return self.beta*kh*self.area/(self.mu*self.dx)
    def _wi(self, w):
        re = 0.2*self.dx
        return 2*np.pi*self.beta*self.k[w["cell"]]*self.height/(self.mu*(np.log(re/w["rw"])+w["skin"]))
    def add_well(self, cell, rate_stbd=None, bhp_psi=None, rw_ft=0.354, skin=0.0):
        self.wells.append(dict(cell=cell, rate=rate_stbd, bhp=bhp_psi, rw=rw_ft, skin=skin))
    def step(self, dt):
        n = self.nx; C = self.vb*self.phi*self.ct/(5.615*dt)
        lo = np.zeros(n); di = np.zeros(n); up = np.zeros(n); b = np.zeros(n)
        for i in range(n):
            tl = self._T(i, i-1) if i > 0 else 0.0; tr = self._T(i, i+1) if i < n-1 else 0.0
            di[i] = C + tl + tr
            if i > 0: lo[i] = -tl
            if i < n-1: up[i] = -tr
            b[i] = C*self.P[i]
        for w in self.wells:
            i = w["cell"]
            if w["rate"] is not None: b[i] += w["rate"]*self.Bo
            elif w["bhp"] is not None: wi = self._wi(w); di[i] += wi; b[i] += wi*w["bhp"]
        self.P = spsolve(diags([lo[1:], di, up[:-1]], [-1, 0, 1], format="csc"), b)
    def run(self, days, dt):
        for _ in range(int(days/dt)): self.step(dt)


# ── OML 58 depletion problem (do not edit) ───────────────────────────────
NX = 50               # grid cells
LENGTH_FT = 5000.0    # reservoir length
WIDTH_FT = 1000.0     # cross-section width
HEIGHT_FT = 50.0      # net pay
K_MD = 20.0           # permeability (md)
PHI = 0.20            # porosity
CT_PSI = 1.0e-5       # total compressibility (1/psi)
MU_CP = 2.0           # oil viscosity (cp)
PI_PSI = 3000.0       # initial reservoir pressure
BO = 1.2              # formation volume factor

WELL_CELL = 0         # producer on the edge cell
BHP_PSI = 1000.0      # constant bottom-hole pressure
SIM_DAYS = 180.0      # depletion period
DT_LIST = [1, 10, 30, 90]  # time steps to compare (days)


def build_model():
    """Fresh Reservoir1D at initial pressure PI_PSI (call once per run)."""
    return Reservoir1D(NX, LENGTH_FT, WIDTH_FT, HEIGHT_FT, K_MD, PHI,
                       CT_PSI, MU_CP, PI_PSI, Bo=BO)


def final_pavg(dt_days):
    """Mean reservoir pressure (psi) after SIM_DAYS, run at time step dt_days."""
    m = build_model()
    m.add_well(cell=WELL_CELL, bhp_psi=BHP_PSI)
    m.run(SIM_DAYS, dt_days)
    return float(np.mean(m.P))


def sweep(dt_list):
    """Dict mapping each dt_days -> its final mean pressure (psi)."""
    return {dt: final_pavg(dt) for dt in dt_list}


pavg_by_dt = sweep(DT_LIST)

print("mean pressure by dt (days):", pavg_by_dt)

lockCopying code is a Full Access feature.