├── __init__.py ├── perm.npy ├── main_pressure.py ├── LICENSE ├── main_singlephase.py ├── main_twophase.py ├── utils.py ├── README.md └── ressim.py /__init__.py: -------------------------------------------------------------------------------- 1 | from .ressim import * 2 | from . import utils 3 | -------------------------------------------------------------------------------- /perm.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chanshing/ressim/HEAD/perm.npy -------------------------------------------------------------------------------- /main_pressure.py: -------------------------------------------------------------------------------- 1 | """ Solve pressure equation. """ 2 | 3 | import numpy as np 4 | import ressim 5 | 6 | import matplotlib 7 | matplotlib.use('Agg') 8 | matplotlib.rcParams['image.cmap'] = 'jet' 9 | import matplotlib.pyplot as plt 10 | 11 | np.random.seed(42) # for reproducibility 12 | 13 | grid = ressim.Grid(nx=64, ny=64, lx=1.0, ly=1.0) # unit square, 64x64 grid 14 | k = np.exp(np.load('perm.npy').reshape(grid.shape)) # load log-permeability, convert to absolute with exp() 15 | q = np.zeros(grid.shape); q[0,0]=1; q[-1,-1]=-1 # source term: corner-to-corner flow (a.k.a. quarter-five spot) 16 | 17 | # instantiate solver 18 | solver = ressim.PressureEquation(grid=grid, q=q, k=k) 19 | # solve 20 | solver.step() 21 | p = solver.p 22 | 23 | # visualize 24 | plt.figure() 25 | plt.imshow(p) 26 | plt.colorbar() 27 | plt.savefig('pressure.png') 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Shing Chan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /main_singlephase.py: -------------------------------------------------------------------------------- 1 | """ Transient single-phase flow """ 2 | 3 | from time import time 4 | 5 | import numpy as np 6 | import scipy.optimize 7 | 8 | import ressim 9 | 10 | import matplotlib 11 | matplotlib.use('Agg') 12 | matplotlib.rcParams['image.cmap'] = 'jet' 13 | import matplotlib.pyplot as plt 14 | 15 | np.random.seed(42) # for reproducibility 16 | 17 | grid = ressim.Grid(nx=64, ny=64, lx=1.0, ly=1.0) # unit square, 64x64 grid 18 | k = np.exp(np.load('perm.npy').reshape(grid.shape)) # load log-permeability, convert to absolute with exp() 19 | q = np.zeros(grid.shape); q[0,0]=1; q[-1,-1]=-1 # source term: corner-to-corner flow (a.k.a. quarter-five spot) 20 | 21 | phi = np.ones(grid.shape)*0.2 # uniform porosity 22 | s0 = np.zeros(grid.shape) # initial water saturation 23 | dt = 1e-2 # timestep 24 | 25 | def f_fn(s): return s # water fractional flow; for single-phase flow, f(s) = s 26 | 27 | # (Optional) derivative of water fractional flow; in single-phase flow, f'(s) = 1 28 | # This is to compute the jacobian of the residual to accelerate the 29 | # saturation solver. If not provided, the jacobian is approximated in the 30 | # solver. 31 | def df_fn(s): return np.ones(len(s)) 32 | 33 | # instantiate solvers 34 | solverP = ressim.PressureEquation(grid, q=q, k=k) 35 | solverS = ressim.SaturationEquation(grid, q=q, phi=phi, s=s0, f_fn=f_fn, df_fn=df_fn) 36 | 37 | # solve for 25 timesteps 38 | nstep=25 39 | # solve pressure; in single-phase, we only need to solve it once 40 | solverP.step() 41 | solverS.v = solverP.v 42 | s_list = [] 43 | for i in range(nstep): 44 | before = time() 45 | # solve saturation 46 | solverS.step(dt) 47 | after = time() 48 | print '[{}/{}]: this loop took {} secs'.format(i+1, nstep, after - before) 49 | 50 | s_list.append(solverS.s) 51 | 52 | # visualize 53 | fig, axs = plt.subplots(5,5, figsize=(8,8)) 54 | fig.subplots_adjust(wspace=.1, hspace=.1, left=0, right=1, bottom=0, top=1) 55 | for ax, s in zip(axs.ravel(), s_list): 56 | ax.imshow(s) 57 | ax.axis('off') 58 | fig.savefig('saturations.png', bbox_inches=0, pad_inches=0) 59 | -------------------------------------------------------------------------------- /main_twophase.py: -------------------------------------------------------------------------------- 1 | """ Transient two-phase (oil-water) flow """ 2 | 3 | from time import time 4 | 5 | import numpy as np 6 | import functools 7 | 8 | import ressim 9 | import utils 10 | 11 | import matplotlib 12 | matplotlib.use('Agg') 13 | matplotlib.rcParams['image.cmap'] = 'jet' 14 | import matplotlib.pyplot as plt 15 | 16 | np.random.seed(42) # for reproducibility 17 | 18 | grid = ressim.Grid(nx=64, ny=64, lx=1.0, ly=1.0) # unit square, 64x64 grid 19 | k = np.exp(np.load('perm.npy').reshape(grid.shape)) # load log-permeability, convert to absolute with exp() 20 | q = np.zeros(grid.shape); q[0,0]=1; q[-1,-1]=-1 # source term: corner-to-corner flow (a.k.a. quarter-five spot) 21 | 22 | mu_w, mu_o = 1.0, 10. # viscosities 23 | s_wir, s_oir = 0.2, 0.2 # irreducible saturations 24 | 25 | phi = np.ones(grid.shape)*0.2 # uniform porosity 26 | s0 = np.ones(grid.shape) * s_wir # initial water saturation equals s_wir 27 | dt = 1e-3 # timestep 28 | 29 | mobi_fn = functools.partial(utils.quadratic_mobility, mu_w=mu_w, mu_o=mu_o, s_wir=s_wir, s_oir=s_oir) # quadratic mobility model 30 | lamb_fn = functools.partial(utils.lamb_fn, mobi_fn=mobi_fn) # total mobility function 31 | f_fn = functools.partial(utils.f_fn, mobi_fn=mobi_fn) # water fractional flow function 32 | 33 | # (Optional) derivative of water fractional flow 34 | # This is to compute the jacobian of the residual to accelerate the 35 | # saturation solver. If not provided, the jacobian is approximated in the 36 | # solver. 37 | df_fn = functools.partial(utils.df_fn, mobi_fn=mobi_fn) 38 | 39 | # instantiate solvers 40 | solverP = ressim.PressureEquation(grid, q=q, k=k, lamb_fn=lamb_fn) 41 | solverS = ressim.SaturationEquation(grid, q=q, phi=phi, s=s0, f_fn=f_fn, df_fn=df_fn) 42 | 43 | # solve for 25 timesteps 44 | nstep = 25 45 | s_list = [] 46 | for i in range(nstep): 47 | before = time() 48 | 49 | # solve pressure 50 | solverP.s = solverS.s 51 | solverP.step() 52 | 53 | # solve saturation 54 | solverS.v = solverP.v 55 | solverS.step(dt) 56 | 57 | after = time() 58 | print '[{}/{}]: this loop took {} secs'.format(i+1, nstep, after - before) 59 | 60 | s_list.append(solverS.s) 61 | 62 | # visualize 63 | fig, axs = plt.subplots(5,5, figsize=(8,8)) 64 | fig.subplots_adjust(wspace=.1, hspace=.1, left=0, right=1, bottom=0, top=1) 65 | for ax, s in zip(axs.ravel(), s_list): 66 | ax.imshow(s) 67 | ax.axis('off') 68 | fig.savefig('saturations.png', bbox_inches=0, pad_inches=0) 69 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | """ Useful functions for reservoir simulation tasks """ 2 | 3 | import numpy 4 | 5 | def linear_mobility(s, mu_w, mu_o, s_wir, s_oir, deriv=False): 6 | """ Linear mobility model 7 | 8 | Parameters 9 | ---------- 10 | s : ndarray, shape (ny, nx) | (ny*nx,) 11 | Saturation 12 | 13 | mu_w : float 14 | Viscosity of water 15 | 16 | mu_o : float 17 | Viscosity of oil 18 | 19 | s_wir : float 20 | Irreducible water saturation 21 | 22 | s_oir : float 23 | Irreducible oil saturation 24 | 25 | deriv : bool 26 | If True, also return derivatives 27 | 28 | Returns 29 | ------- 30 | if deriv=False, 31 | lamb_w, lamb_o : (2x) ndarray, shape (ny, nx) | (ny*nx,) 32 | lamb_w : water mobility 33 | lamb_o : oil mobility 34 | 35 | if deriv=True, 36 | lamb_w, lamb_o, dlamb_w, dlamb_o : (4x) ndarray, shape (ny, nx) | (ny*nx,) 37 | lamb_w : water mobility 38 | lamb_o : oil mobility 39 | dlamb_w : derivative of water mobility 40 | dlamb_o : derivative of oil mobility 41 | """ 42 | mu_w, mu_o, s_wir, s_oir = float(mu_w), float(mu_o), float(s_wir), float(s_oir) 43 | _s = (s-s_wir)/(1.0-s_wir-s_oir) 44 | lamb_w = _s/mu_w 45 | lamb_o = (1.0-_s)/mu_o 46 | 47 | if deriv: 48 | dlamb_w = 1.0/(mu_w*(1.0-s_wir-s_oir)) 49 | dlamb_o = -1.0/(mu_o*(1.0-s_wir-s_oir)) 50 | return lamb_w, lamb_o, dlamb_w, dlamb_o 51 | 52 | return lamb_w, lamb_o 53 | 54 | def quadratic_mobility(s, mu_w, mu_o, s_wir, s_oir, deriv=False): 55 | """ Quadratic mobility model 56 | 57 | Parameters 58 | ---------- 59 | s : ndarray, shape (ny, nx) | (ny*nx,) 60 | Saturation 61 | 62 | mu_w : float 63 | Viscosity of water 64 | 65 | mu_o : float 66 | Viscosity of oil 67 | 68 | s_wir : float 69 | Irreducible water saturation 70 | 71 | s_oir : float 72 | Irreducible oil saturation 73 | 74 | deriv : bool 75 | If True, also return derivatives 76 | 77 | Returns 78 | ------- 79 | if deriv=False, 80 | lamb_w, lamb_o : (2x) ndarray, shape (ny, nx) | (ny*nx,) 81 | lamb_w : water mobility 82 | lamb_o : oil mobility 83 | 84 | if deriv=True, 85 | lamb_w, lamb_o, dlamb_w, dlamb_o : (4x) ndarray, shape (ny, nx) | (ny*nx,) 86 | lamb_w : water mobility 87 | lamb_o : oil mobility 88 | dlamb_w : derivative of water mobility 89 | dlamb_o : derivative of oil mobility 90 | """ 91 | 92 | mu_w, mu_o, s_wir, s_oir = float(mu_w), float(mu_o), float(s_wir), float(s_oir) 93 | _s = (s-s_wir)/(1.0-s_wir-s_oir) 94 | lamb_w = _s**2/mu_w 95 | lamb_o = (1.0-_s)**2/mu_o 96 | 97 | if deriv: 98 | dlamb_w = 2.0*_s/(mu_w*(1.0-s_wir-s_oir)) 99 | dlamb_o = -2.0*(1.0-_s)/(mu_o*(1.0-s_wir-s_oir)) 100 | return lamb_w, lamb_o, dlamb_w, dlamb_o 101 | 102 | return lamb_w, lamb_o 103 | 104 | def f_fn(s, mobi_fn): 105 | """ Water fractional flow 106 | 107 | Parameters 108 | ---------- 109 | s : ndarray, shape (ny, nx) | (ny*nx,) 110 | Saturation 111 | 112 | mobi_fn : callable 113 | Mobility function lamb_w, lamb_o = mobi_fn(s) where: 114 | lamb_w : water mobility 115 | lamb_o : oil mobility 116 | """ 117 | lamb_w, lamb_o = mobi_fn(s) 118 | return lamb_w / (lamb_w + lamb_o) 119 | 120 | def df_fn(s, mobi_fn): 121 | """ Derivative (element-wise) of water fractional flow 122 | 123 | Parameters 124 | ---------- 125 | s : ndarray, shape (ny, nx) | (ny*nx,) 126 | Saturation 127 | 128 | mobi_fn : callable 129 | Mobility function lamb_w, lamb_o, dlamb_w, dlamb_o = mobi_fn(s, deriv=True) where: 130 | lamb_w : water mobility 131 | lamb_o : oil mobility 132 | dlamb_w : derivative of water mobility 133 | dlamb_o : derivative of oil mobility 134 | """ 135 | lamb_w, lamb_o, dlamb_w, dlamb_o = mobi_fn(s, deriv=True) 136 | return dlamb_w / (lamb_w + lamb_o) - lamb_w * (dlamb_w + dlamb_o) / (lamb_w + lamb_o)**2 137 | 138 | def lamb_fn(s, mobi_fn): 139 | """ Total mobility 140 | 141 | Parameters 142 | ---------- 143 | s : ndarray, shape (ny, nx) | (ny*nx,) 144 | Saturation 145 | 146 | mobi_fn : callable 147 | Mobility function lamb_w, lamb_o = mobi_fn(s) where: 148 | lamb_w : water mobility 149 | lamb_o : oil mobility 150 | """ 151 | lamb_w, lamb_o = mobi_fn(s) 152 | return lamb_w + lamb_o 153 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ressim: reservoir simulation in python 2 | Reservoir simulation in Python. It currently supports 2D rectangular grids and isotropic permeability. 3 | 4 | ## Modules 5 | - `ressim.py`: main module containing classes to define the grid, and to model and solve the pressure and saturation equations. 6 | 7 | - `utils.py`: module containing useful function definitions, e.g. linear and quadratic mobility functions, fractional flow function, etc. 8 | 9 | ## Usage 10 | See `main_*.py` for examples. 11 | 12 | # `ressim.py` 13 | A module for reservoir simulation in Python. 14 | ## Grid 15 | ```python 16 | Grid(self, nx, ny, lx=1.0, ly=1.0) 17 | ``` 18 | A class to define a rectangular grid. Given `nx, ny, lx, ly`, it maintains an update of attributes `vol, dx, dy, ncell, shape`. 19 | 20 | __Attributes__ 21 | 22 | - `nx, ny`: `int` 23 | Grid resolution. 24 | 25 | - `lx, ly`: `float` 26 | Grid physical dimensions. Default is `lx=1.0, ly=1.0`, i.e. unit square. 27 | 28 | - `vol`: `float` 29 | Cell volume. 30 | 31 | - `dx, dy`: `float` 32 | Cell dimensions. 33 | 34 | - `ncell`: `int` 35 | Number of cells. 36 | 37 | - `shape`: `int` 38 | Grid shape, i.e. `(ny, nx)`. 39 | 40 | ## PressureEquation 41 | ```python 42 | PressureEquation(self, grid=None, q=None, k=None, diri=None, lamb_fn=None, s=None) 43 | ``` 44 | 45 | A class to model and solve the pressure equation, 46 | 47 | ![pressure](https://i.imgur.com/PLoJ0bj.gif) 48 | 49 | with no-flow boundary conditions (closed reservoir). 50 | 51 | __Attributes__ 52 | 53 | - `grid`: `Grid` 54 | Simulation grid. 55 | 56 | - `q`: `ndarray, shape (ny, nx) | (ny*nx,)` 57 | Integrated source term. 58 | 59 | - `k`: `ndarray, shape (ny, nx)` 60 | Permeability 61 | 62 | - `diri`: `list` of `(int, float)` tuples 63 | Dirichlet boundary conditions, e.g. `[(i1, val1), (i2, val2), ...]` 64 | means pressure values `val1` at cell `i1`, `val2` at cell `i2`, etc. Defaults 65 | to `[(ny*nx/2, 0.0)]`, i.e. zero pressure at center of the grid. 66 | 67 | - `lamb_fn`: `callable` 68 | Total mobility function `lamb_fn(s)` 69 | 70 | - `s`: `ndarray, (ny, nx) | (ny*nx,)` 71 | Water saturation 72 | 73 | - `p`: `ndarray, (ny, nx)` 74 | Pressure 75 | 76 | - `v`: `dict` of `ndarray` 77 | - `'x' `: `ndarray, shape (ny, nx+1)` 78 | Flux in x-direction 79 | - `'y' `: `ndarray, shape (ny+1, nx)` 80 | Flux in y-direction 81 | 82 | __Methods__ 83 | 84 | - `step()`: 85 | Solve the pressure equation to obtain pressure and flux. Update 86 | `self.p` and `self.v`. 87 | 88 | - `solve(mat, q)`: 89 | Method to solve the system of linear equations. Default is 90 | `scipy.sparse.linalg.spsolve(mat, q)`. You can override this method to 91 | use a different solver. 92 | 93 | ## SaturationEquation 94 | ```python 95 | SaturationEquation(self, grid=None, q=None, phi=None, s=None, f_fn=None, v=None, df_fn=None) 96 | ``` 97 | A class to model and solve the (water) saturation equation under water injection, 98 | 99 | ![Water saturation](https://i.imgur.com/qswqrcK.gif) 100 | 101 | __Attributes__ 102 | - `grid`: `Grid` 103 | Simulation grid 104 | 105 | - `q`: `ndarray, (ny, nx) | (ny*nx,)` 106 | Integrated source term. 107 | 108 | - `phi`: `ndarray, (ny, nx) | (ny*nx,)` 109 | Porosity 110 | 111 | - `f_fn`: `callable` 112 | Water fractional flow function `f_fn(s)` 113 | 114 | - `v`: `dict` of `ndarray` 115 | - `'x'` : `ndarray, (ny, nx+1)` 116 | Flux in x-direction 117 | - `'y'` : `ndarray, (ny+1, nx)` 118 | Flux in y-direction 119 | 120 | - `df_fn`: `callable` (optional) 121 | Derivative (element-wise) of water fractional flow function `df_fn(s)`. 122 | It is used to compute the jacobian of the residual function. If `None`, 123 | the jacobian is approximated by the solver (which can be slow). 124 | 125 | - `s` : `ndarray, (ny, nx) | (ny*nx,)` 126 | Water saturation 127 | 128 | __Methods__ 129 | - `step(dt)`: 130 | Solve saturation forward in time by `dt`. Update `self.s`. 131 | 132 | - `solve(residual, s0, residual_jac=None)`: 133 | Method to perform the minimization of the residual. Default is 134 | `scipy.optimize.nonlin.nonlin_solve(residual, s0, jacobian=residual_jac)`. 135 | If `residual_jac` is `None`, defaults to `'krylov'`. 136 | You can override this method to use a different solver. 137 | 138 | # `utils.py` 139 | Useful functions for reservoir simulation tasks. 140 | 141 | ## linear_mobility 142 | ```python 143 | linear_mobility(s, mu_w, mu_o, s_wir, s_oir, deriv=False) 144 | ``` 145 | Function to compute water and oil mobility with a *linear* model. 146 | 147 | ![](https://i.imgur.com/kTSiUd4.gif) 148 | 149 | __Parameters__ 150 | 151 | - `s`: `ndarray, (ny, nx) | (ny*nx,)` 152 | Water saturation 153 | 154 | - `mu_w`: `float` 155 | Viscosity of water 156 | 157 | - `mu_o`: `float` 158 | Viscosity of oil 159 | 160 | - `s_wir`: `float` 161 | Irreducible water saturation 162 | 163 | - `s_oir`: `float` 164 | Irreducible oil saturation 165 | 166 | - `deriv`: `bool` 167 | If `True`, also return derivatives 168 | 169 | __Returns__ 170 | 171 | `if deriv=False:` 172 | - `lamb_w, lamb_o`: `ndarray, (ny, nx) | (ny*nx,)` 173 | - `lamb_w`: water mobility 174 | - `lamb_o`: oil mobility 175 | 176 | `if deriv=True:` 177 | - `lamb_w, lamb_o, dlamb_w, dlamb_o`: `ndarray, (ny, nx) | (ny*nx,)` 178 | - `lamb_w`: water mobility 179 | - `lamb_o`: oil mobility 180 | - `dlamb_w`: derivative of water mobility 181 | - `dlamb_o`: derivative of oil mobility 182 | 183 | ## quadratic_mobility 184 | ```python 185 | quadratic_mobility(s, mu_w, mu_o, s_wir, s_oir, deriv=False) 186 | ``` 187 | Function to compute water and oil mobility with a *quadratic* model. 188 | 189 | ![](https://i.imgur.com/DbPnjSk.gif) 190 | 191 | __Parameters__ 192 | - `s`: `ndarray, (ny, nx) | (ny*nx,)` 193 | Water saturation 194 | 195 | - `mu_w`: `float` 196 | Viscosity of water 197 | 198 | - `mu_o`: `float` 199 | Viscosity of oil 200 | 201 | - `s_wir`: `float` 202 | Irreducible water saturation 203 | 204 | - `s_oir`: `float` 205 | Irreducible oil saturation 206 | 207 | - `deriv`: `bool` 208 | If `True`, also return derivatives 209 | 210 | __Returns__ 211 | 212 | `if deriv=False:` 213 | - `lamb_w, lamb_o`: `ndarray, (ny, nx) | (ny*nx,)` 214 | - `lamb_w`: water mobility 215 | - `lamb_o`: oil mobility 216 | 217 | `if deriv=True:` 218 | - `lamb_w, lamb_o, dlamb_w, dlamb_o`: `ndarray, (ny, nx) | (ny*nx,)` 219 | - `lamb_w`: water mobility 220 | - `lamb_o`: oil mobility 221 | - `dlamb_w`: derivative of water mobility 222 | - `dlamb_o`: derivative of oil mobility 223 | 224 | ## f_fn 225 | ```python 226 | f_fn(s, mobi_fn) 227 | ``` 228 | Water fractional flow function. 229 | 230 | ![](https://i.imgur.com/2grtPNR.gif) 231 | 232 | __Parameters__ 233 | - `s`: `ndarray, (ny, nx) | (ny*nx,)` 234 | Water saturation 235 | 236 | - `mobi_fn`: `callable` 237 | Mobility function `lamb_w, lamb_o = mobi_fn(s)` where: 238 | - `lamb_w`: water mobility 239 | - `lamb_o`: oil mobility 240 | 241 | __Returns__ 242 | - `ndarray, (ny, nx) | (ny*nx,)` 243 | Fractional flow 244 | 245 | ## df_fn 246 | ```python 247 | df_fn(s, mobi_fn) 248 | ``` 249 | Derivative function (element-wise) of water fractional flow. 250 | 251 | ![](https://i.imgur.com/bwz8RQ1.gif) 252 | 253 | __Parameters__ 254 | - `s`: `ndarray, (ny, nx) | (ny*nx,)` 255 | Water saturation 256 | 257 | - `mobi_fn`: `callable` 258 | Mobility function `lamb_w, lamb_o, dlamb_w, dlamb_o = mobi_fn(s, deriv=True)` where: 259 | - `lamb_w`: water mobility 260 | - `lamb_o`: oil mobility 261 | - `dlamb_w`: derivative of water mobility 262 | - `dlamb_o`: derivative of oil mobility 263 | 264 | __Returns__ 265 | - `ndarray, (ny, nx) | (ny*nx,)` 266 | Fractional flow derivative 267 | 268 | ## lamb_fn 269 | ```python 270 | lamb_fn(s, mobi_fn) 271 | ``` 272 | Total mobility function. 273 | 274 | ![](https://i.imgur.com/YCKjZ8N.gif) 275 | 276 | __Parameters__ 277 | - `s`: `ndarray, (ny, nx) | (ny*nx,)` 278 | Water saturation 279 | 280 | - `mobi_fn`: `callable` 281 | Mobility function `lamb_w, lamb_o = mobi_fn(s)` where: 282 | - `lamb_w`: water mobility 283 | - `lamb_o`: oil mobility 284 | 285 | __Returns__ 286 | - `ndarray, (ny, nx) | (ny*nx,)` Total mobility 287 | 288 | # References 289 | Aarnes J.E., Gimse T., Lie KA. (2007) An Introduction to the Numerics of Flow in Porous Media using Matlab. In: Hasle G., Lie KA., Quak E. (eds) Geometric Modelling, Numerical Simulation, and Optimization. Springer, Berlin, Heidelberg -------------------------------------------------------------------------------- /ressim.py: -------------------------------------------------------------------------------- 1 | """ A Module for reservoir simulation in Python """ 2 | 3 | import numpy as np 4 | import scipy.sparse as spa 5 | import scipy.sparse.linalg 6 | import scipy.optimize 7 | 8 | __all__ = ['Grid', 'Parameters', 'PressureEquation', 'SaturationEquation', 'transmi', 'convecti', 'impose_diri', 'csr_row_set_nz_to_val'] 9 | 10 | class Grid(object): 11 | """ 12 | Simple rectangular grid. 13 | 14 | Attributes 15 | ---------- 16 | nx, ny : int, int 17 | Grid resolution 18 | 19 | lx, ly : float, float, optional 20 | Grid physical dimensions. (default lx=1.0, ly=1.0, i.e. unit square) 21 | 22 | vol : float 23 | cell volume 24 | 25 | dx, dy : float, float 26 | cell dimensions 27 | 28 | ncell : int 29 | number of cells 30 | 31 | shape : int 32 | grid shape, i.e. (ny, nx) 33 | """ 34 | def __init__(self, nx, ny, lx=1.0, ly=1.0): 35 | self.nx, self.ny = nx, ny 36 | self.lx, self.ly = lx, ly 37 | 38 | @property 39 | def shape(self): 40 | return (self.ny, self.nx) 41 | 42 | @property 43 | def ncell(self): 44 | return self.nx*self.ny 45 | 46 | @property 47 | def vol(self): 48 | return self.dx*self.dy 49 | 50 | @property 51 | def dx(self): 52 | return self.lx/self.nx 53 | 54 | @property 55 | def dy(self): 56 | return self.ly/self.ny 57 | 58 | @property 59 | def nx(self): 60 | return self.__nx 61 | 62 | @property 63 | def ny(self): 64 | return self.__ny 65 | 66 | @property 67 | def lx(self): 68 | return self.__lx 69 | 70 | @property 71 | def ly(self): 72 | return self.__ly 73 | 74 | @nx.setter 75 | def nx(self, nx): 76 | self.__nx = int(nx) 77 | 78 | @ny.setter 79 | def ny(self, ny): 80 | self.__ny = int(ny) 81 | 82 | @lx.setter 83 | def lx(self, lx): 84 | self.__lx = float(lx) 85 | 86 | @ly.setter 87 | def ly(self, ly): 88 | self.__ly = float(ly) 89 | 90 | class Parameters(object): 91 | """ Container for equation paremeters with minimal checks """ 92 | 93 | @property 94 | def grid(self): 95 | return self.__grid 96 | 97 | @property 98 | def k(self): 99 | return self.__k 100 | 101 | @property 102 | def q(self): 103 | return self.__q 104 | 105 | @property 106 | def s(self): 107 | return self.__s 108 | 109 | @property 110 | def phi(self): 111 | return self.__phi 112 | 113 | @property 114 | def v(self): 115 | return self.__v 116 | 117 | @property 118 | def lamb_fn(self): 119 | return self.__lamb_fn 120 | 121 | @property 122 | def f_fn(self): 123 | return self.__f_fn 124 | 125 | @grid.setter 126 | def grid(self, grid): 127 | if grid is not None: 128 | assert isinstance(grid, Grid) 129 | self.__grid = grid 130 | 131 | @k.setter 132 | def k(self, k): 133 | if k is not None: 134 | assert isinstance(k, np.ndarray) 135 | assert np.all(k > 0), "Non-positive permeability. Perhaps forgot to exp(k)?" 136 | self.__k = k 137 | 138 | @q.setter 139 | def q(self, q): 140 | if q is not None: 141 | assert isinstance(q, np.ndarray) 142 | assert np.sum(q) == 0, "Unbalanced source term" 143 | self.__q = q 144 | 145 | @s.setter 146 | def s(self, s): 147 | if s is not None: 148 | assert isinstance(s, np.ndarray) 149 | assert np.all(s >= 0) and np.all(s <= 1), "Water saturation not in [0,1]" 150 | self.__s = s 151 | 152 | @phi.setter 153 | def phi(self, phi): 154 | if phi is not None: 155 | assert isinstance(phi, np.ndarray) 156 | assert np.all(phi >= 0) and np.all(phi <= 1), "Porosity not in [0,1]" 157 | self.__phi = phi 158 | 159 | @v.setter 160 | def v(self, v): 161 | if v is not None: 162 | assert isinstance(v, dict) 163 | assert isinstance(v['x'], np.ndarray) 164 | assert isinstance(v['y'], np.ndarray) 165 | self.__v = v 166 | 167 | @lamb_fn.setter 168 | def lamb_fn(self, lamb_fn): 169 | if lamb_fn is not None: 170 | assert callable(lamb_fn) 171 | self.__lamb_fn = lamb_fn 172 | 173 | @f_fn.setter 174 | def f_fn(self, f_fn): 175 | if f_fn is not None: 176 | assert callable(f_fn) 177 | self.__f_fn = f_fn 178 | 179 | class PressureEquation(Parameters): 180 | """ 181 | Pressure equation 182 | 183 | Attributes 184 | ---------- 185 | grid : 186 | Grid object defining the domain 187 | 188 | q : ndarray, shape (ny, nx) | (ny*nx,) 189 | Integrated source term. 190 | 191 | k : ndarray, shape (ny, nx) 192 | Permeability 193 | 194 | diri : list of (int, float) tuples 195 | Dirichlet boundary conditions, e.g. [(i1, val1), (i2, val2), ...] 196 | means pressure values val1 at cell i1, val2 at cell i2, etc. Defaults 197 | to [(ny*nx/2, 0.0)], i.e. zero pressure at center of the grid. 198 | 199 | lamb_fn : callable 200 | Total mobility function lamb_fn(s) 201 | 202 | s : ndarray, shape (ny, nx) | (ny*nx,) 203 | Water saturation 204 | 205 | p : ndarray, shape (ny, nx) 206 | Pressure 207 | 208 | v : dict of ndarray 209 | 'x' : ndarray, shape (ny, nx+1) 210 | Flux in x-direction 211 | 'y' : ndarray, shape (ny+1, nx) 212 | Flux in y-direction 213 | 214 | Methods 215 | ------- 216 | step() : 217 | Solve the pressure equation to obtain pressure and flux. Update 218 | self.p and self.v 219 | 220 | solve(mat, q): 221 | Method to solve the system of linear equations. Default is 222 | scipy.sparse.linalg.spsolve(mat, q) 223 | You can override this method to use a different solver. 224 | """ 225 | def __init__(self, grid=None, q=None, k=None, diri=None, lamb_fn=None, s=None): 226 | self.grid, self.q, self.k = grid, q, k 227 | self.diri = diri 228 | self.lamb_fn = lamb_fn 229 | self.s = s 230 | 231 | @property 232 | def diri(self): 233 | """ Default to zero at center of the grid """ 234 | if self.__diri is None: 235 | return [(int(self.grid.ncell/2), 0.0)] 236 | return self.__diri 237 | 238 | @diri.setter 239 | def diri(self, diri): 240 | self.__diri = diri 241 | 242 | def step(self): 243 | grid, q, k = self.grid, self.q, self.k 244 | diri = self.diri 245 | 246 | if hasattr(self, 'lamb_fn'): 247 | k = k * self.lamb_fn(self.s).reshape(*grid.shape) 248 | 249 | mat, tx, ty = transmi(grid, k) 250 | q = np.copy(q).reshape(grid.ncell) 251 | impose_diri(mat, q, diri) # inplace op on mat, q 252 | 253 | # pressure 254 | p = self.solve(mat, q) 255 | p = p.reshape(*grid.shape) 256 | # flux 257 | nx, ny = grid.nx, grid.ny 258 | v = {'x':np.zeros((ny,nx+1)), 'y':np.zeros((ny+1,nx))} 259 | v['x'][:,1:nx] = (p[:,0:nx-1]-p[:,1:nx])*tx[:,1:nx] 260 | v['y'][1:ny,:] = (p[0:ny-1,:]-p[1:ny,:])*ty[1:ny,:] 261 | 262 | self.p, self.v = p, v 263 | 264 | def solve(self, mat, q, **kws): 265 | return scipy.sparse.linalg.spsolve(mat, q, **kws) 266 | 267 | class SaturationEquation(Parameters): 268 | """ 269 | Water saturation equation 270 | 271 | Attributes 272 | ---------- 273 | grid : 274 | Grid object defining the domain 275 | 276 | q : ndarray, shape (ny, nx) | (ny*nx,) 277 | Integrated source term. 278 | 279 | phi : ndarray, shape (ny, nx) | (ny*nx,) 280 | Porosity 281 | 282 | f_fn : callable 283 | Water fractional flow function f_fn(s) 284 | 285 | v : dict of ndarray 286 | 'x' : ndarray, shape (ny, nx+1) 287 | Flux in x-direction 288 | 'y' : ndarray, shape (ny+1, nx) 289 | Flux in y-direction 290 | 291 | df_fn : callable (optional) 292 | Derivative (element-wise) of water fractional flow function df_fn(s). 293 | It is used to compute the jacobian of the residual function. If None, 294 | the jacobian is approximated by the solver (which can be slow). 295 | 296 | s : ndarray, shape (ny, nx) | (ny*nx,) 297 | Water saturation 298 | 299 | Methods 300 | ------- 301 | step(dt) : 302 | Solve saturation forward in time by dt. Update self.s 303 | 304 | solve(residual, s0, residual_jac=None) : 305 | Method to perform the minimization of the residual. Default is 306 | scipy.optimize.nonlin.nonlin_solve(residual, s0, jacobian=residual_jac). 307 | If residual_jac is None, defaults to 'krylov'. 308 | You can override this method to use a different solver. 309 | """ 310 | def __init__(self, grid=None, q=None, phi=None, s=None, f_fn=None, v=None, df_fn=None): 311 | self.grid, self.q, self.phi, self.s, self.f_fn = grid, q, phi, s, f_fn 312 | self.v = v 313 | self.df_fn = df_fn 314 | 315 | @property 316 | def df_fn(self): 317 | return self.__df_fn 318 | 319 | @df_fn.setter 320 | def df_fn(self, df_fn): 321 | if df_fn is not None: 322 | assert(callable(df_fn)) 323 | self.__df_fn = df_fn 324 | 325 | def step(self, dt): 326 | grid, q, phi, s = self.grid, self.q, self.phi, self.s 327 | v = self.v 328 | f_fn = self.f_fn 329 | 330 | alpha = float(dt) / (grid.vol * phi) 331 | mat = convecti(grid, v) 332 | 333 | s = s.reshape(grid.ncell) 334 | q = q.reshape(grid.ncell) 335 | alpha = alpha.reshape(grid.ncell) 336 | 337 | def residual(s1): 338 | f = f_fn(s1) 339 | qp = np.maximum(q,0) 340 | qn = np.minimum(q,0) 341 | r = s1 - s + alpha * (mat.dot(f) - (qp + f*qn)) 342 | return r 343 | 344 | residual_jac = None 345 | if hasattr(self, 'df_fn'): 346 | def residual_jac(s1): 347 | df = self.df_fn(s1) 348 | qn = np.minimum(q,0) 349 | eye = spa.eye(len(s1)) 350 | df_eye = spa.diags(df, 0, shape=(len(s1), len(s1))) 351 | alpha_eye = spa.diags(alpha, 0, shape=(len(s1), len(s1))) 352 | qn_eye = spa.diags(qn, 0, shape=(len(s1), len(s1))) 353 | dr = eye + (alpha_eye.dot(mat - qn_eye)).dot(df_eye) 354 | return dr 355 | 356 | s = self.solve(residual, s0=s, residual_jac=residual_jac) 357 | self.s = np.clip(s, 0., 1.).reshape(*grid.shape) # clip to ensure within [0, 1] 358 | 359 | # def solve(self, residual, s0, residual_jac=None): 360 | # if residual_jac is None: 361 | # residual_jac = '2-point' 362 | # sol = scipy.optimize.least_squares(residual, s0, jac=residual_jac, method='trf', tr_solver='lsmr', verbose=0) 363 | # return sol.x 364 | 365 | def solve(self, residual, s0, residual_jac=None): 366 | if residual_jac is None: 367 | residual_jac = 'krylov' 368 | else: 369 | # Wrap function into a Jacobian object. See https://github.com/scipy/scipy/blob/master/scipy/optimize/nonlin.py 370 | residual_jac = scipy.optimize.nonlin.asjacobian(residual_jac) 371 | residual_jac.x = s0 372 | return scipy.optimize.nonlin.nonlin_solve(residual, s0, jacobian=residual_jac) 373 | 374 | def transmi(grid, k): 375 | """ Construct transmisibility matrix with two point flux approximation """ 376 | nx, ny = grid.nx, grid.ny 377 | dx, dy = grid.dx, grid.dy 378 | n = grid.ncell 379 | 380 | k = k.reshape(*grid.shape) 381 | kinv = 1.0/k 382 | 383 | ax = 2*dy/dx; tx = np.zeros((ny,nx+1)) 384 | ay = 2*dx/dy; ty = np.zeros((ny+1,nx)) 385 | 386 | tx[:,1:nx] = ax/(kinv[:,0:nx-1]+kinv[:,1:nx]) 387 | ty[1:ny,:] = ay/(kinv[0:ny-1,:]+kinv[1:ny,:]) 388 | 389 | x1 = tx[:,0:nx].reshape(n); x2 = tx[:,1:nx+1].reshape(n) 390 | y1 = ty[0:ny,:].reshape(n); y2 = ty[1:ny+1,:].reshape(n) 391 | 392 | data = [-y2, -x2, x1+x2+y1+y2, -x1, -y1] 393 | diags = [-nx, -1, 0, 1, nx] 394 | mat = spa.spdiags(data, diags, n, n, format='csr') 395 | 396 | return mat, tx, ty 397 | 398 | def convecti(grid, v): 399 | """ Construct convection matrix with upwind scheme """ 400 | nx, ny = grid.nx, grid.ny 401 | n = grid.ncell 402 | 403 | xn = np.minimum(v['x'], 0); x1 = xn[:,0:nx].reshape(n) 404 | yn = np.minimum(v['y'], 0); y1 = yn[0:ny,:].reshape(n) 405 | xp = np.maximum(v['x'], 0); x2 = xp[:,1:nx+1].reshape(n) 406 | yp = np.maximum(v['y'], 0); y2 = yp[1:ny+1,:].reshape(n) 407 | 408 | data = [-y2, -x2, x2-x1+y2-y1, x1, y1] 409 | diags = [-nx, -1, 0, 1, nx] 410 | mat = spa.spdiags(data, diags, n, n, format='csr') 411 | 412 | return mat 413 | 414 | def impose_diri(mat, q, diri): 415 | """ Impose Dirichlet boundary conditions. NOTE: inplace operation on mat, q 416 | For example, to impose a pressure value 99 at the first cell: 417 | 418 | mat = [[ 1 0 ... 0 ] 419 | [ a21 a22 ... a2n ] 420 | ... 421 | [ an1 an2 ... ann ]] 422 | 423 | q = [99 q2 ... qn] 424 | """ 425 | for i, val in diri: 426 | csr_row_set_nz_to_val(mat, i, 0.0) 427 | mat[i,i] = 1.0 428 | q[i] = val 429 | mat.eliminate_zeros() 430 | 431 | def csr_row_set_nz_to_val(csr, row, value=0): 432 | """ Set all nonzero elements (elements currently in the sparsity pattern) 433 | to the given value. Useful to set to 0 mostly. """ 434 | if not isinstance(csr, spa.csr_matrix): 435 | raise ValueError('Matrix given must be of CSR format.') 436 | csr.data[csr.indptr[row]:csr.indptr[row+1]] = value 437 | --------------------------------------------------------------------------------