Exercise 11.3
Injection-Production Pair - Pressure Support & Mass Balance
Place a water injector (constant rate = +500 STB/d) at cell 0 and a producer (constant BHP = 2000 psi) at cell 49. Run for 2 years. Plot the pressure profile and discuss the pressure support provided by injection.
---
We'll work the balanced version of this problem on OML 58: the case that used to expose a famous bug in a hand-rolled simulator. Instead of a BHP producer, drive both wells on rate: a water injector adds +q_stbd at the inlet cell (cell 0) and a producer pulls the same -q_stbd out of the outlet cell (cell nx-1). Every barrel injected is a barrel produced.
Because injection exactly balances production, mass conservation says the mean reservoir pressure cannot drift; it must sit at the initial pressure Pi forever (a few psi of numerical noise at most). What does develop is a steady pressure gradient: the injector cell rides above Pi, the producer cell sits below it, and the difference settles to a constant of order hundreds of psi. The old broken simulator got this catastrophically wrong; it reported gradients of order 1e7 psi (a unit-area / wrong-source-term bug). A correct, mass-conserving simulator does not.
The verified Reservoir1D simulator and the OML-58 reservoir constants are embedded for you. Do not modify them or re-derive the physics. The constants are: NX = 50 cells, LENGTH_FT = 2500, WIDTH_FT = 500, HEIGHT_FT = 40, K_MD = 150, PHI = 0.22, CT_PSI = 1.2e-5, MU_CP = 1.1, PI_PSI = 4000, BO = 1.2. Run for DAYS = 730 (about 2 years) with DT_DAYS = 10.
Your tasks:
- Write
run_pair(q_stbd, days=DAYS, dt_days=DT_DAYS):
- Build a
Reservoir1Dwith the OML-58 constants above. add_well(0, rate_stbd=+q_stbd): the injector at the inlet cell.add_well(NX - 1, rate_stbd=-q_stbd): the producer at the outlet cell.run(days, dt_days).- Take the final pressure array
P_profile = sim.P(lengthNX). - Compute
Pavg = float(np.mean(P_profile))(mean reservoir pressure, psi). - Compute
gradient = float(P_profile[0] - P_profile[-1]): injector-cell
pressure minus producer-cell pressure (psi).
- Return the tuple
(Pavg, gradient, P_profile)in that order.
- Call
run_pair(350.0)and unpack it intopavg_psi,gradient_psi, and
p_profile (the 50-cell pressure array).
> Think about it: with a balanced pair, pavg_psi should land on 4000 psi > to within rounding while gradient_psi comes out a few hundred psi (positive, > because the injector cell is the high-pressure end). If you ever see a gradient > in the millions, your transmissibility or source term is wrong; that is the > exact failure this exercise is built to catch. Why does a stronger injection > rate widen the gradient but leave the mean pressure untouched?
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
from scipy.sparse import diags
from scipy.sparse.linalg import spsolve
# ── Verified 1D single-phase implicit 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 reservoir constants (do not edit) ─────────────────────────────
NX = 50 # grid cells
LENGTH_FT = 2500.0 # reservoir length
WIDTH_FT = 500.0 # cross-section width
HEIGHT_FT = 40.0 # net pay thickness
K_MD = 150.0 # permeability
PHI = 0.22 # porosity
CT_PSI = 1.2e-5 # total compressibility, 1/psi
MU_CP = 1.1 # oil viscosity, cp
PI_PSI = 4000.0 # initial reservoir pressure
BO = 1.2 # formation volume factor, rb/stb
DAYS = 730.0 # ~2 years
DT_DAYS = 10.0 # time step
def run_pair(q_stbd, days=DAYS, dt_days=DT_DAYS):
"""Balanced injector/producer pair on OML 58.
Injector adds +q_stbd at cell 0, producer pulls -q_stbd at cell NX-1.
Returns (Pavg, gradient, P_profile):
Pavg = mean final reservoir pressure (psi); stays ~= PI_PSI by mass balance
gradient = P_profile[0] - P_profile[-1] (psi); a steady few-hundred-psi drop
P_profile = final pressure array of length NX
"""
sim = Reservoir1D(NX, LENGTH_FT, WIDTH_FT, HEIGHT_FT, K_MD, PHI,
CT_PSI, MU_CP, PI_PSI, Bo=BO)
sim.add_well(0, rate_stbd=+q_stbd) # injector at the inlet
sim.add_well(NX - 1, rate_stbd=-q_stbd) # producer at the outlet
sim.run(days, dt_days)
P_profile = sim.P
Pavg = float(np.mean(P_profile))
gradient = float(P_profile[0] - P_profile[-1])
return Pavg, gradient, P_profile
pavg_psi, gradient_psi, p_profile = run_pair(350.0)
print("Pavg (psi):", pavg_psi, " deviation from Pi:", pavg_psi - PI_PSI)
print("gradient P[0]-P[-1] (psi):", gradient_psi)
lockCopying code is a Full Access feature.