TPFA_ResSim.grid
Tools for working with model grid coordinates.
Most functions here are barely in use, mostly serving as reference. After all, it is surprisingly hard to remember which direction and index is for x and which is for y.
The index ordering is "C-style" (numpy default).
This choice means that x is the 1st coord., y is 2nd,
and is hardcoded in the reservoir simulator model code
(in what takes place between np.ravel and np.reshape,
both of which are configured to use row-major index ordering.
"F-style" (column-major) indexing implementation is perfectly possible,
but would imply an undue amount hassle).
Conveniently, it also means that x and y tend to occur in alphabetic order.
Thus, in printing a matrix of a field, the x coordinate corresponds to the row index.
By contrast, the plotting module depicts x from left to right, y from bottom to top.
1"""Tools for working with model grid coordinates. 2 3Most functions here are barely in use, mostly serving as reference. 4After all, it is surprisingly hard to remember 5which direction and index is for x and which is for y. 6 7The index ordering is "C-style" (numpy default). 8This choice means that `x` is the 1st coord., `y` is 2nd, 9and is hardcoded in the reservoir simulator model code 10(in what takes place **between** `np.ravel` and `np.reshape`, 11both of which are configured to use row-major index ordering. 12"F-style" (column-major) indexing implementation is perfectly possible, 13but would imply an undue amount hassle). 14Conveniently, it also means that `x` and `y` tend to occur in alphabetic order. 15Thus, in printing a matrix of a field, the `x` coordinate corresponds to the row index. 16By contrast, the plotting module depicts `x` from left to right, `y` from bottom to top. 17""" 18 19from dataclasses import dataclass 20from typing import NamedTuple, overload 21 22import numpy as np 23import numpy.typing as npt 24 25 26class Fluxes(NamedTuple): 27 """Data container with dot (attr) access for cell face fluxes. Mimicks matlab code. 28 29 Positive is in the direction of increasing index. The fluxes through the 30 *boundary* faces are `0`: the reservoir is closed (no-flow) all around. 31 """ 32 33 x: np.ndarray 34 """Fluxes through the x-normal faces. Shape `(Nx+1, Ny)`.""" 35 y: np.ndarray 36 """Fluxes through the y-normal faces. Shape `(Nx, Ny+1)`.""" 37 38 39@dataclass 40class Grid2D: 41 """Defines a 2D rectangular grid. 42 43 Example (2 x-nodes, 5 y-nodes): 44 >>> grid = Grid2D(Lx=6, Ly=10, Nx=3, Ny=5) 45 46 The nodes are centered in the cells: 47 >>> X, Y = grid.mesh 48 >>> X 49 array([[1., 1., 1., 1., 1.], 50 [3., 3., 3., 3., 3.], 51 [5., 5., 5., 5., 5.]]) 52 53 You can compute cell boundaries (i.e. non-central nodes) by adding or subtracting 54 `hx`/2 and `hy`/2 (i.e. you will miss either boundary at 0 or `Lx` or `Ly`). 55 56 Test of round-trip capability of grid mapping computations: 57 >>> ij = (0, 4) 58 >>> grid.xy2sub(X[ij], Y[ij]) == ij 59 array([ True, True]) 60 61 >>> grid.sub2xy(*ij) == (X[ij], Y[ij]) 62 array([ True, True]) 63 """ 64 65 Lx: float = 1.0 66 """Physical x-length of domain.""" 67 Ly: float = 1.0 68 """Physical y-length of domain.""" 69 Nx: int = 32 70 """Number of grid cells (and their centres) in x dir.""" 71 Ny: int = 32 72 """Number of grid cells (and their centres) in y dir.""" 73 74 @property 75 def shape(self) -> tuple: 76 """`(Nx, Ny)`""" 77 return self.Nx, self.Ny 78 79 @property 80 def size(self) -> int: 81 """Total number of elements.""" 82 return int(np.prod(self.shape)) 83 84 @property 85 def domain(self) -> tuple: 86 """`((0, 0), (Lx, Ly))`""" 87 return ((0, 0), (self.Lx, self.Ly)) 88 89 @property 90 def Nxy(self) -> int: 91 """`Nx` * `Ny`""" 92 return int(np.prod(self.shape)) 93 94 @property 95 def hx(self) -> float: 96 """x-length of cells""" 97 return self.Lx / self.Nx 98 99 @property 100 def hy(self) -> float: 101 """y-length of cells""" 102 return self.Ly / self.Ny 103 104 @property 105 def h2(self) -> float: 106 """`hx` * `hy`""" 107 return self.hx * self.hy 108 109 @property 110 def mesh(self) -> tuple: 111 """Generate 2D coordinate grid of cell centres.""" 112 xx = np.linspace(0, self.Lx, self.Nx, endpoint=False) + self.hx / 2 113 yy = np.linspace(0, self.Ly, self.Ny, endpoint=False) + self.hy / 2 114 return np.meshgrid(xx, yy, indexing="ij") 115 116 def sub2ind( 117 self, ix: int | np.ndarray, iy: int | np.ndarray 118 ) -> np.intp | np.ndarray: 119 """Convert index `(ix, iy)` to index in flattened array.""" 120 idx = np.ravel_multi_index((ix, iy), self.shape) 121 return idx 122 123 def ind2sub(self, ind: int | np.intp | np.ndarray) -> np.ndarray: 124 """Inv. of `self.sub2ind`.""" 125 ix, iy = np.unravel_index(ind, self.shape) 126 return np.asarray([ix, iy]) 127 128 def xy2sub(self, x: npt.ArrayLike, y: npt.ArrayLike) -> np.ndarray: 129 """Convert physical coordinate tuple to `(ix, iy)`, ix ∈ {0, ..., Nx-1}. 130 131 .. warning:: `xy2sub` and `xy2ind` *round* to nearest cell center. 132 133 I.e. they are not injective. 134 The alternative would be to return some kind 135 of interpolation weights distributing `(x, y)` over multiple nodes. 136 This was tried and rejected (not worth it), ref "Missing features" 137 section of docs. 138 """ 139 x = np.asarray(x) 140 y = np.asarray(y) 141 # Don't silence errors! Validation is useful in optimisation (e.g.) 142 assert np.all(x <= self.Lx) 143 assert np.all(y <= self.Ly) 144 # Set upper border values to slightly interior. 145 # NB: the nudge is *relative*, `Lx` being of whatever magnitude the 146 # units imply (ref `TPFA_ResSim.ResSim.cdarcy`). 147 x = x.clip(max=self.Lx * (1 - 1e-12)) 148 y = y.clip(max=self.Ly * (1 - 1e-12)) 149 ix = np.floor(x / self.Lx * self.Nx).astype(int) 150 iy = np.floor(y / self.Ly * self.Ny).astype(int) 151 return np.asarray([ix, iy]) 152 153 # Overloaded so that the (array-valued) well lookups of `ResSim` type-check 154 @overload 155 def xy2ind(self, x: float, y: float) -> np.intp: ... 156 @overload 157 def xy2ind(self, x: np.ndarray, y: np.ndarray) -> np.ndarray: ... 158 159 def xy2ind(self, x: npt.ArrayLike, y: npt.ArrayLike) -> np.intp | np.ndarray: 160 """Convert physical coord to flat indx.""" 161 i, j = self.xy2sub(x, y) 162 return self.sub2ind(i, j) 163 164 def sub2xy(self, ix: npt.ArrayLike, iy: npt.ArrayLike) -> np.ndarray: 165 """Inverse of `self.xy2sub`.""" 166 x = (np.asarray(ix) + 0.5) * self.hx 167 y = (np.asarray(iy) + 0.5) * self.hy 168 return np.asarray([x, y]) 169 170 def ind2xy(self, ind: int | np.intp | np.ndarray) -> np.ndarray: 171 """Inverse of `self.xy2ind`.""" 172 i, j = self.ind2sub(ind) 173 return self.sub2xy(i, j) 174 175 def _crossings(self, p0: np.ndarray, d: np.ndarray) -> np.ndarray: 176 """Parameters $t ∈ [0, 1]$ at which `p0 + t*d` crosses a cell boundary.""" 177 ts = [0.0, 1.0] 178 for ax, h in enumerate([self.hx, self.hy]): 179 if d[ax] == 0: 180 continue 181 lo, hi = sorted([p0[ax], p0[ax] + d[ax]]) 182 ts += [ 183 (i * h - p0[ax]) / d[ax] 184 for i in range(int(np.floor(lo / h)) + 1, int(np.ceil(hi / h))) 185 ] 186 return np.unique(np.clip(ts, 0, 1)) # NB: `unique` also sorts
27class Fluxes(NamedTuple): 28 """Data container with dot (attr) access for cell face fluxes. Mimicks matlab code. 29 30 Positive is in the direction of increasing index. The fluxes through the 31 *boundary* faces are `0`: the reservoir is closed (no-flow) all around. 32 """ 33 34 x: np.ndarray 35 """Fluxes through the x-normal faces. Shape `(Nx+1, Ny)`.""" 36 y: np.ndarray 37 """Fluxes through the y-normal faces. Shape `(Nx, Ny+1)`."""
Data container with dot (attr) access for cell face fluxes. Mimicks matlab code.
Positive is in the direction of increasing index. The fluxes through the
boundary faces are 0: the reservoir is closed (no-flow) all around.
40@dataclass 41class Grid2D: 42 """Defines a 2D rectangular grid. 43 44 Example (2 x-nodes, 5 y-nodes): 45 >>> grid = Grid2D(Lx=6, Ly=10, Nx=3, Ny=5) 46 47 The nodes are centered in the cells: 48 >>> X, Y = grid.mesh 49 >>> X 50 array([[1., 1., 1., 1., 1.], 51 [3., 3., 3., 3., 3.], 52 [5., 5., 5., 5., 5.]]) 53 54 You can compute cell boundaries (i.e. non-central nodes) by adding or subtracting 55 `hx`/2 and `hy`/2 (i.e. you will miss either boundary at 0 or `Lx` or `Ly`). 56 57 Test of round-trip capability of grid mapping computations: 58 >>> ij = (0, 4) 59 >>> grid.xy2sub(X[ij], Y[ij]) == ij 60 array([ True, True]) 61 62 >>> grid.sub2xy(*ij) == (X[ij], Y[ij]) 63 array([ True, True]) 64 """ 65 66 Lx: float = 1.0 67 """Physical x-length of domain.""" 68 Ly: float = 1.0 69 """Physical y-length of domain.""" 70 Nx: int = 32 71 """Number of grid cells (and their centres) in x dir.""" 72 Ny: int = 32 73 """Number of grid cells (and their centres) in y dir.""" 74 75 @property 76 def shape(self) -> tuple: 77 """`(Nx, Ny)`""" 78 return self.Nx, self.Ny 79 80 @property 81 def size(self) -> int: 82 """Total number of elements.""" 83 return int(np.prod(self.shape)) 84 85 @property 86 def domain(self) -> tuple: 87 """`((0, 0), (Lx, Ly))`""" 88 return ((0, 0), (self.Lx, self.Ly)) 89 90 @property 91 def Nxy(self) -> int: 92 """`Nx` * `Ny`""" 93 return int(np.prod(self.shape)) 94 95 @property 96 def hx(self) -> float: 97 """x-length of cells""" 98 return self.Lx / self.Nx 99 100 @property 101 def hy(self) -> float: 102 """y-length of cells""" 103 return self.Ly / self.Ny 104 105 @property 106 def h2(self) -> float: 107 """`hx` * `hy`""" 108 return self.hx * self.hy 109 110 @property 111 def mesh(self) -> tuple: 112 """Generate 2D coordinate grid of cell centres.""" 113 xx = np.linspace(0, self.Lx, self.Nx, endpoint=False) + self.hx / 2 114 yy = np.linspace(0, self.Ly, self.Ny, endpoint=False) + self.hy / 2 115 return np.meshgrid(xx, yy, indexing="ij") 116 117 def sub2ind( 118 self, ix: int | np.ndarray, iy: int | np.ndarray 119 ) -> np.intp | np.ndarray: 120 """Convert index `(ix, iy)` to index in flattened array.""" 121 idx = np.ravel_multi_index((ix, iy), self.shape) 122 return idx 123 124 def ind2sub(self, ind: int | np.intp | np.ndarray) -> np.ndarray: 125 """Inv. of `self.sub2ind`.""" 126 ix, iy = np.unravel_index(ind, self.shape) 127 return np.asarray([ix, iy]) 128 129 def xy2sub(self, x: npt.ArrayLike, y: npt.ArrayLike) -> np.ndarray: 130 """Convert physical coordinate tuple to `(ix, iy)`, ix ∈ {0, ..., Nx-1}. 131 132 .. warning:: `xy2sub` and `xy2ind` *round* to nearest cell center. 133 134 I.e. they are not injective. 135 The alternative would be to return some kind 136 of interpolation weights distributing `(x, y)` over multiple nodes. 137 This was tried and rejected (not worth it), ref "Missing features" 138 section of docs. 139 """ 140 x = np.asarray(x) 141 y = np.asarray(y) 142 # Don't silence errors! Validation is useful in optimisation (e.g.) 143 assert np.all(x <= self.Lx) 144 assert np.all(y <= self.Ly) 145 # Set upper border values to slightly interior. 146 # NB: the nudge is *relative*, `Lx` being of whatever magnitude the 147 # units imply (ref `TPFA_ResSim.ResSim.cdarcy`). 148 x = x.clip(max=self.Lx * (1 - 1e-12)) 149 y = y.clip(max=self.Ly * (1 - 1e-12)) 150 ix = np.floor(x / self.Lx * self.Nx).astype(int) 151 iy = np.floor(y / self.Ly * self.Ny).astype(int) 152 return np.asarray([ix, iy]) 153 154 # Overloaded so that the (array-valued) well lookups of `ResSim` type-check 155 @overload 156 def xy2ind(self, x: float, y: float) -> np.intp: ... 157 @overload 158 def xy2ind(self, x: np.ndarray, y: np.ndarray) -> np.ndarray: ... 159 160 def xy2ind(self, x: npt.ArrayLike, y: npt.ArrayLike) -> np.intp | np.ndarray: 161 """Convert physical coord to flat indx.""" 162 i, j = self.xy2sub(x, y) 163 return self.sub2ind(i, j) 164 165 def sub2xy(self, ix: npt.ArrayLike, iy: npt.ArrayLike) -> np.ndarray: 166 """Inverse of `self.xy2sub`.""" 167 x = (np.asarray(ix) + 0.5) * self.hx 168 y = (np.asarray(iy) + 0.5) * self.hy 169 return np.asarray([x, y]) 170 171 def ind2xy(self, ind: int | np.intp | np.ndarray) -> np.ndarray: 172 """Inverse of `self.xy2ind`.""" 173 i, j = self.ind2sub(ind) 174 return self.sub2xy(i, j) 175 176 def _crossings(self, p0: np.ndarray, d: np.ndarray) -> np.ndarray: 177 """Parameters $t ∈ [0, 1]$ at which `p0 + t*d` crosses a cell boundary.""" 178 ts = [0.0, 1.0] 179 for ax, h in enumerate([self.hx, self.hy]): 180 if d[ax] == 0: 181 continue 182 lo, hi = sorted([p0[ax], p0[ax] + d[ax]]) 183 ts += [ 184 (i * h - p0[ax]) / d[ax] 185 for i in range(int(np.floor(lo / h)) + 1, int(np.ceil(hi / h))) 186 ] 187 return np.unique(np.clip(ts, 0, 1)) # NB: `unique` also sorts
Defines a 2D rectangular grid.
Example (2 x-nodes, 5 y-nodes):
>>> grid = Grid2D(Lx=6, Ly=10, Nx=3, Ny=5)
The nodes are centered in the cells:
>>> X, Y = grid.mesh
>>> X
array([[1., 1., 1., 1., 1.],
[3., 3., 3., 3., 3.],
[5., 5., 5., 5., 5.]])
You can compute cell boundaries (i.e. non-central nodes) by adding or subtracting
hx/2 and hy/2 (i.e. you will miss either boundary at 0 or Lx or Ly).
Test of round-trip capability of grid mapping computations:
>>> ij = (0, 4)
>>> grid.xy2sub(X[ij], Y[ij]) == ij
array([ True, True])
>>> grid.sub2xy(*ij) == (X[ij], Y[ij])
array([ True, True])
80 @property 81 def size(self) -> int: 82 """Total number of elements.""" 83 return int(np.prod(self.shape))
Total number of elements.
85 @property 86 def domain(self) -> tuple: 87 """`((0, 0), (Lx, Ly))`""" 88 return ((0, 0), (self.Lx, self.Ly))
((0, 0), (Lx, Ly))
110 @property 111 def mesh(self) -> tuple: 112 """Generate 2D coordinate grid of cell centres.""" 113 xx = np.linspace(0, self.Lx, self.Nx, endpoint=False) + self.hx / 2 114 yy = np.linspace(0, self.Ly, self.Ny, endpoint=False) + self.hy / 2 115 return np.meshgrid(xx, yy, indexing="ij")
Generate 2D coordinate grid of cell centres.
117 def sub2ind( 118 self, ix: int | np.ndarray, iy: int | np.ndarray 119 ) -> np.intp | np.ndarray: 120 """Convert index `(ix, iy)` to index in flattened array.""" 121 idx = np.ravel_multi_index((ix, iy), self.shape) 122 return idx
Convert index (ix, iy) to index in flattened array.
124 def ind2sub(self, ind: int | np.intp | np.ndarray) -> np.ndarray: 125 """Inv. of `self.sub2ind`.""" 126 ix, iy = np.unravel_index(ind, self.shape) 127 return np.asarray([ix, iy])
Inv. of self.sub2ind.
129 def xy2sub(self, x: npt.ArrayLike, y: npt.ArrayLike) -> np.ndarray: 130 """Convert physical coordinate tuple to `(ix, iy)`, ix ∈ {0, ..., Nx-1}. 131 132 .. warning:: `xy2sub` and `xy2ind` *round* to nearest cell center. 133 134 I.e. they are not injective. 135 The alternative would be to return some kind 136 of interpolation weights distributing `(x, y)` over multiple nodes. 137 This was tried and rejected (not worth it), ref "Missing features" 138 section of docs. 139 """ 140 x = np.asarray(x) 141 y = np.asarray(y) 142 # Don't silence errors! Validation is useful in optimisation (e.g.) 143 assert np.all(x <= self.Lx) 144 assert np.all(y <= self.Ly) 145 # Set upper border values to slightly interior. 146 # NB: the nudge is *relative*, `Lx` being of whatever magnitude the 147 # units imply (ref `TPFA_ResSim.ResSim.cdarcy`). 148 x = x.clip(max=self.Lx * (1 - 1e-12)) 149 y = y.clip(max=self.Ly * (1 - 1e-12)) 150 ix = np.floor(x / self.Lx * self.Nx).astype(int) 151 iy = np.floor(y / self.Ly * self.Ny).astype(int) 152 return np.asarray([ix, iy])
Convert physical coordinate tuple to (ix, iy), ix ∈ {0, ..., Nx-1}.
160 def xy2ind(self, x: npt.ArrayLike, y: npt.ArrayLike) -> np.intp | np.ndarray: 161 """Convert physical coord to flat indx.""" 162 i, j = self.xy2sub(x, y) 163 return self.sub2ind(i, j)
Convert physical coord to flat indx.
165 def sub2xy(self, ix: npt.ArrayLike, iy: npt.ArrayLike) -> np.ndarray: 166 """Inverse of `self.xy2sub`.""" 167 x = (np.asarray(ix) + 0.5) * self.hx 168 y = (np.asarray(iy) + 0.5) * self.hy 169 return np.asarray([x, y])
Inverse of self.xy2sub.