Part III: Reservoir & Production Engineering

Chapter 11

Introduction to Reservoir Simulation with Python

schedule19 min readfitness_center10 exercises
auto_awesomeAI Key Takeaways

Get the TL;DR and the key concepts before you dive in — or as a quick review after.

Why This Chapter Exists

Material balance tells you how much oil is in the reservoir and what is driving it out. But it treats the entire reservoir as a single tank; it cannot tell you where the oil is, how the fluids move through the rock, or what happens when you change well locations, injection rates, or completion designs.

Reservoir simulation solves this problem. It divides the reservoir into thousands or millions of grid cells, assigns rock and fluid properties to each cell, and solves the equations of fluid flow at every cell and every time step. The result is a dynamic, spatial model of the reservoir that can predict how fluids move, where pressure fronts develop, and how different development strategies compare.

Commercial reservoir simulators (Eclipse, CMG, tNavigator) cost tens or hundreds of thousands of dollars per license. Understanding what they do under the hood (the physics, the math, and the numerical methods) makes you a better user of those tools and a better engineer. This chapter builds a reservoir simulator from scratch. It will be simple compared to commercial software, but the principles are identical.

infoWhat You'll Learn

  • The diffusivity equation governing fluid flow in porous media
  • Finite difference discretization of partial differential equations
  • Building a 1D single-phase reservoir simulator from scratch
  • Implicit vs. explicit time-stepping: stability, accuracy, and cost
  • Buckley-Leverett two-phase displacement theory
  • Working with output from commercial simulators
  • Validating simulation results against analytical solutions

The Physics of Flow in Porous Media

Every cell in the grid you are about to build needs an equation that says how fast fluid crosses its faces and how much its pressure changes as a result. Three pieces of physics supply that equation. Fluid flow through reservoir rock is governed by three fundamental principles: conservation of mass, Darcy's Law, and an equation of state.

Darcy's Law

In 1856, Henry Darcy showed experimentally that the flow rate of a fluid through a porous medium is proportional to the pressure gradient and inversely proportional to the fluid viscosity:

u=kμPxu = -\frac{k}{\mu} \frac{\partial P}{\partial x}

where uu is the superficial velocity (flow rate per unit area), kk is the rock permeability, μ\mu is the fluid viscosity, and P/x\partial P / \partial x is the pressure gradient.

The physical interpretation: fluid flows from high pressure to low pressure. The rate of flow depends on how permeable the rock is (how connected the pore spaces are) and how viscous the fluid is (how much it resists flowing). Darcy's Law is the petroleum engineering equivalent of Ohm's Law in electrical circuits: pressure difference drives flow, permeability is conductance, viscosity is resistance.

Conservation of Mass

The mass of fluid entering a volume element minus the mass leaving must equal the change in mass stored within the element:

x(kμPx)=ϕctPt\frac{\partial}{\partial x}\left(\frac{k}{\mu}\frac{\partial P}{\partial x}\right) = \phi c_t \frac{\partial P}{\partial t}

This is the diffusivity equation for single-phase, slightly compressible flow. It combines Darcy's Law with mass conservation and a compressibility equation of state.

  • ϕ\phi is porosity (the fraction of rock volume that is pore space)
  • ctc_t is total compressibility (how much the fluid and rock compress per unit pressure drop)
  • The left side represents spatial variation in flow
  • The right side represents temporal change in pressure

This equation is a partial differential equation (PDE). It cannot be solved analytically for most realistic reservoir geometries and boundary conditions. Numerical methods are required.

Finite Difference Discretization

The diffusivity equation has no closed-form solution for a real reservoir, so a computer cannot integrate it directly; it can only add and multiply numbers in arrays. Discretization is what turns the continuous equation into exactly that. To solve the diffusivity equation numerically, we discretize it: convert the continuous PDE into a system of algebraic equations that a computer can solve.

Spatial Discretization

Divide the reservoir into nn grid cells of width Δx\Delta x. Replace the spatial derivative with a central difference approximation:

x(kμPx)Ti+1/2(Pi+1Pi)Ti1/2(PiPi1)Δx2\frac{\partial}{\partial x}\left(\frac{k}{\mu}\frac{\partial P}{\partial x}\right) \approx \frac{T_{i+1/2}(P_{i+1} - P_i) - T_{i-1/2}(P_i - P_{i-1})}{\Delta x^2}

where Ti+1/2=βki+1/2AμΔxT_{i+1/2} = \dfrac{\beta\, k_{i+1/2}\, A}{\mu\, \Delta x} is the transmissibility between cells ii and i+1i+1 (RB/d/psi). AA is the cross-sectional area, ki+1/2k_{i+1/2} is the harmonic mean of the two cells' permeabilities (the correct average for series flow), and β=1.127×103\beta = 1.127\times10^{-3} is the Darcy field-unit constant. Flux, accumulation, and the well source are all carried in reservoir barrels, so the formation volume factor BoB_o enters only on the well term (converting STB/d to RB/d), not here. Multiplying the equation through by the cell bulk volume gives the finite-volume form the code solves.

Temporal Discretization

Replace the time derivative with a forward difference:

PtPin+1PinΔt\frac{\partial P}{\partial t} \approx \frac{P_i^{n+1} - P_i^n}{\Delta t}

How you combine spatial and temporal discretization determines whether the scheme is explicit or implicit.

Building a 1D Single-Phase Simulator

The class below assembles exactly the finite-volume system from the last section. Each grid cell contributes one row to a tridiagonal matrix: the diagonal holds its accumulation term plus the transmissibilities to its neighbours, the off-diagonals hold those neighbour transmissibilities, and the right-hand side carries the old pressure and any well source. spsolve then returns the new pressure field in one step.

main.py
Code output

Running the Simulator

main.py

The pressure profiles show the characteristic shape of a drawdown: pressure drops sharply near the well and the disturbance propagates outward over time. Early time steps show the pressure front still traveling through the reservoir (transient flow). At late times, the pressure front has reached the boundary and the entire reservoir is depleting (boundary-dominated flow).

This transition from transient to boundary-dominated flow is fundamental in reservoir engineering. It affects decline curve behavior, well test interpretation, and spacing decisions.

Implicit vs. Explicit Methods

The simulator above uses implicit time-stepping, meaning it solves a linear system at each time step. The alternative is explicit time-stepping, where the new pressure is calculated directly from the old pressure without solving a system.

Explicit Method

Pin+1=Pin+ΔtϕctΔx2[Ti+1/2(Pi+1nPin)Ti1/2(PinPi1n)]P_i^{n+1} = P_i^n + \frac{\Delta t}{\phi c_t \Delta x^2} \left[ T_{i+1/2}(P_{i+1}^n - P_i^n) - T_{i-1/2}(P_i^n - P_{i-1}^n) \right]

The explicit method is simpler to implement but has a critical limitation: it is only stable if the time step satisfies the CFL condition:

Δt<ϕμctΔx22k6.328×103\Delta t < \frac{\phi \mu c_t \Delta x^2}{2k \cdot 6.328 \times 10^{-3}}

If you exceed this limit, the solution oscillates and diverges. In practical terms, explicit methods require very small time steps for high-permeability reservoirs or fine grids, making them prohibitively slow.

Implicit Method

The implicit method has no stability restriction: you can take arbitrarily large time steps without divergence. The cost is that you must solve a linear system (APn+1=bA \cdot P^{n+1} = b) at each step. For a tridiagonal system (1D problem), this is efficient. For 3D problems with millions of cells, it requires iterative solvers and sophisticated linear algebra.

main.py

Buckley-Leverett Two-Phase Displacement

When water is injected into an oil reservoir, two immiscible fluids flow simultaneously through the same rock. The displacement process is governed by the Buckley-Leverett equation, which predicts the position of the water front as a function of time.

The key concept is fractional flow: the fraction of total flow that is water at any point in the reservoir.

fw=11+kroμwkrwμof_w = \frac{1}{1 + \frac{k_{ro} \mu_w}{k_{rw} \mu_o}}

where krok_{ro} and krwk_{rw} are the relative permeabilities of oil and water, which depend on water saturation.

main.py
Buckley-Leverett water saturation profile showing the displacement front.
Buckley-Leverett water saturation profile showing the displacement front.

The Buckley-Leverett solution reveals a sharp saturation discontinuity, the shock front, where water abruptly replaces oil. Behind the front, the rock is at near-residual oil saturation. Ahead of the front, only connate water is present. The front velocity depends on the derivative of the fractional flow curve, which is why the viscosity ratio between oil and water is so important for displacement efficiency.

Coupling Pressure and Saturation: an IMPES Waterflood

Fractional-flow theory is analytical: it tells you the shape of the answer. A real simulator couples it to the pressure solution. The standard scheme is IMPES (IMplicit Pressure, Explicit Saturation): each step solves the pressure equation with the current total mobility, computes the inter-cell flows, then advances water saturation explicitly with upstream weighting (the saturation of a face is taken from the cell the fluid is flowing from, central differencing of a hyperbolic transport term produces non-physical oscillations). This is the actual engine behind a waterflood forecast, and it reproduces the Buckley-Leverett front rather than a hand-drawn sketch.

main.py
IMPES waterflood: the simulated saturation front advancing toward the producer.
IMPES waterflood: the simulated saturation front advancing toward the producer.

The simulated front lands within a few percent of the analytical Buckley-Leverett position; the small lag is numerical dispersion, the smearing a first-order upwind scheme adds to a true shock. Refining the grid (larger nx) sharpens the front toward the analytical discontinuity, the displacement-engineering version of the grid-refinement study in Exercise 11.1.

Summary

  • Reservoir simulation solves the equations of fluid flow at every point in space and time, providing spatial resolution that material balance cannot.
  • Darcy's Law relates flow rate to pressure gradient, permeability, and viscosity. It is the fundamental flow equation in porous media.
  • The diffusivity equation combines Darcy's Law with mass conservation. It is a PDE that must be solved numerically for realistic reservoirs.
  • Finite differences convert the continuous PDE into discrete algebraic equations by approximating derivatives on a grid.
  • Implicit methods have no stability restriction and allow large time steps. Explicit methods are simpler but require time steps below the CFL limit.
  • Buckley-Leverett theory predicts the displacement of oil by water. The shock front saturation and velocity are determined by the fractional flow curve.
  • Commercial simulators use the same principles but extend them to 3D, multiphase flow, complex geology, and sophisticated well models. The leap to a field model is one of scale and bookkeeping, not of new physics; the mass-balance and Darcy core you built here is unchanged.

Exercises

fitness_center
Exercise 11.1Practice

: Grid Refinement Study

Run the 1D simulator from this chapter with 10, 25, 50, 100, and 200 grid cells. Plot the pressure profile at t=180 days for each case. At what grid r...

arrow_forward
codePythonSolve Nowarrow_forward
fitness_center
Exercise 11.2Practice

: Permeability Heterogeneity

Modify the simulator to use a non-uniform permeability field: cells 1–25 have k=200 md, cells 26–50 have k=20 md. Run the simulation and compare the p...

arrow_forward
codePythonSolve Nowarrow_forward
fitness_center
Exercise 11.3Practice

: Injection-Production Pair

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 ...

arrow_forward
codePythonSolve Nowarrow_forward
fitness_center
Exercise 11.4Practice

: CFL Condition

Implement the explicit time-stepping method. Calculate the CFL limit for the reservoir parameters in this chapter. Verify by running the explicit meth...

arrow_forward
codePythonSolve Nowarrow_forward
fitness_center
Exercise 11.5Practice

: Time Step Sensitivity

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 ...

arrow_forward
codePythonSolve Nowarrow_forward
fitness_center
Exercise 11.6Practice

: Fractional Flow Sensitivity

Plot the fractional flow curve for three different oil viscosities: 1 cp (light oil), 5 cp (medium), and 50 cp (heavy oil). How does the shock front s...

arrow_forward
codePythonSolve Nowarrow_forward
fitness_center
Exercise 11.7Practice

: Analytical Validation

The line-source solution for pressure drawdown in an infinite reservoir is P(r,t)=Pi+qμ4πkhEi(−ϕμctr24kt)P(r,t) = P_i + \frac{q\mu}{4\pi kh}Ei\left(-\...

arrow_forward
codePythonSolve Nowarrow_forward
fitness_center
Exercise 11.8Practice

: Boundary Conditions

The simulator uses no-flow boundaries by default (no flux at x=0 and x=L). Modify it to support a constant-pressure boundary at x=L (representing an a...

arrow_forward
codePythonSolve Nowarrow_forward
fitness_center
Exercise 11.9Practice

: Corey Exponent Sensitivity

Using the Buckley-Leverett analysis, investigate how the Corey exponents (non_ono​ and nwn_wnw​) affect the shape of the fractional flow curve and the...

arrow_forward
codePythonSolve Nowarrow_forward
fitness_center
Exercise 11.10Practice

: Build a 2D Simulator

Extend the 1D simulator to 2D by adding a second spatial dimension. Use a 20×20 grid. Place an injector in one corner and a producer in the opposite c...

arrow_forward
codePythonSolve Nowarrow_forward