minires.geostat
Gaussian random fields, for synthetic permeabilities and priors.
A stationary, isotropic Gaussian field over the given points -- typically the cell
centres of a minires.Grid2D.mesh -- defined by its variogram, $ γ(h) $, i.e.
the covariance $ c(h) = 1 - γ(h) $ as a function of the distance $ h $. Here the
Gaussian one, variogram_gauss, whose one parameter of note is the range, r
(the correlation drops to $ e^{-3} $ at $ h = r $, for the default a).
cov_sqrt forms the covariance matrix of the field over the points and returns
its (upper) Cholesky factor, $ \mathbf{C} = \mathbf{C}_{1/2}^T \mathbf{C}_{1/2} $,
so that gaussian_fields samples the field as $ \mathbf{z} \, \mathbf{C}_{1/2} $,
with $ \mathbf{z} $ standard normal -- N of them at a time, each of unit
variance and zero mean.
>>> from minires import Grid2D
>>> grid = Grid2D(Lx=1, Ly=1, Nx=8, Ny=8)
>>> C12 = cov_sqrt(grid.mesh, r=.3)
>>> C12.shape
(64, 64)
>>> Cov = 1 - variogram_gauss(dist_euclid(vectorize(*grid.mesh)), r=.3)
>>> bool(np.allclose(C12.T @ C12, Cov))
True
>>> fields = gaussian_fields(grid.mesh, N=3, r=.3, rng=np.random.default_rng(0))
>>> fields.shape
(3, 64)
The covariance matrix is dense, (nPts, nPts), and factorized outright (with a
tiny jitter on the diagonal, the Gaussian covariance being nearly singular), so
this is for the toy grids of this package -- a few thousand cells at most --
and not a geostatistics library. The fields being Gaussian, they serve as
log-permeabilities (K = exp(logK)), or as the Gaussian prior of an ensemble
method (ref examples.random_fields, and the tutorials of
HistoryMatching, which also use
cov_sqrt as the change of variables of a variational method).
funm_psd is a general matrix function for such (positive semi-definite)
covariances, e.g. the symmetric square root.
1"""Gaussian random fields, for synthetic permeabilities and priors. 2 3A stationary, isotropic Gaussian field over the given points -- typically the cell 4centres of a `minires.Grid2D.mesh` -- defined by its *variogram*, $ γ(h) $, i.e. 5the covariance $ c(h) = 1 - γ(h) $ as a function of the distance $ h $. Here the 6Gaussian one, `variogram_gauss`, whose one parameter of note is the range, `r` 7(the correlation drops to $ e^{-3} $ at $ h = r $, for the default `a`). 8`cov_sqrt` forms the covariance matrix of the field over the points and returns 9its (upper) Cholesky factor, $ \\mathbf{C} = \\mathbf{C}_{1/2}^T \\mathbf{C}_{1/2} $, 10so that `gaussian_fields` samples the field as $ \\mathbf{z} \\, \\mathbf{C}_{1/2} $, 11with $ \\mathbf{z} $ standard normal -- `N` of them at a time, each of unit 12variance and zero mean. 13 14>>> from minires import Grid2D 15>>> grid = Grid2D(Lx=1, Ly=1, Nx=8, Ny=8) 16>>> C12 = cov_sqrt(grid.mesh, r=.3) 17>>> C12.shape 18(64, 64) 19>>> Cov = 1 - variogram_gauss(dist_euclid(vectorize(*grid.mesh)), r=.3) 20>>> bool(np.allclose(C12.T @ C12, Cov)) 21True 22>>> fields = gaussian_fields(grid.mesh, N=3, r=.3, rng=np.random.default_rng(0)) 23>>> fields.shape 24(3, 64) 25 26The covariance matrix is *dense*, `(nPts, nPts)`, and factorized outright (with a 27tiny jitter on the diagonal, the Gaussian covariance being nearly singular), so 28this is for the toy grids of this package -- a few thousand cells at most -- 29and not a geostatistics library. The fields being Gaussian, they serve as 30*log*-permeabilities (`K = exp(logK)`), or as the Gaussian prior of an ensemble 31method (ref `examples.random_fields`, and the tutorials of 32[HistoryMatching](https://github.com/patnr/HistoryMatching), which also use 33`cov_sqrt` as the change of variables of a variational method). 34`funm_psd` is a general matrix function for such (positive semi-definite) 35covariances, e.g. the symmetric square root. 36""" 37 38import numpy as np 39import scipy.linalg as sla 40 41 42def variogram_gauss(xx: np.ndarray, r: float, n: float = 0, a: float = 1 / 3) -> np.ndarray: 43 """Gaussian variogram at the distances `xx`. 44 45 Params: range `r`, nugget `n`, and `a`, which scales the range 46 (for the default, the covariance $ 1 - γ $ is $ e^{-3} $ at `r`). 47 Ref: <https://en.wikipedia.org/wiki/Variogram#Variogram_models> 48 49 >>> xx = np.array([0, 1, 2]) 50 >>> variogram_gauss(xx, 1, n=0.1, a=1) 51 array([0. , 0.6689085 , 0.98351593]) 52 """ 53 # Gauss 54 gamma = 1 - np.exp(-(xx**2) / r**2 / a) 55 # Sill (=1) 56 gamma *= 1 - n 57 # Nugget 58 gamma[xx != 0] += n 59 return gamma 60 61 62def vectorize(*XYZ: np.ndarray) -> np.ndarray: 63 """Reshape coordinate arrays (`nDim` of them, of equal `shape`) to `(nPts, nDim)`.""" 64 return np.stack(XYZ).reshape((len(XYZ), -1)).T 65 66 67def dist_euclid(X: np.ndarray) -> np.ndarray: 68 """Pairwise distances between the rows of `X`, like `squareform(pdist(X))`.""" 69 diff = X[:, None, :] - X 70 d2 = np.sum(diff**2, axis=-1) 71 return np.sqrt(d2) 72 73 74def funm_psd(C, fun, rk=None, rtol=1e-8, sym_square=True, **kwargs): 75 """Matrix function evaluation for a positive semi-definite matrix. 76 77 Adapted from the `scipy.linalg.funm` doc. 78 79 Note: small `rk` and `driver="evx"` should be faster, 80 but in my (simple but hopefully relevant) trials 81 sticking with the default "evr" and `rk=None` is usually faster. 82 Alternatively, could try iterative algorithms from `sparse.linalg`. 83 84 Example 85 ------- 86 >>> def sqrtm(C): 87 ... return funm_psd(C, np.sqrt) 88 """ 89 # EVD -- possibly truncated (for speed) 90 idx = [max(0, len(C) - rk), len(C) - 1] if rk else None 91 ews, V = sla.eigh(C, subset_by_index=idx, **kwargs) 92 93 # Truncate (for stability) -- NB: ordering low-->high! 94 nNull = sum(ews <= rtol * ews.max()) 95 ews = ews[nNull:] 96 V = V[:, nNull:] 97 98 # Apply 99 ews = fun(ews) 100 101 # Reconstruct 102 funC = V * ews 103 if sym_square: 104 # Optional, since not necessary e.g. for cholesky factors (for sampling) 105 # But, without it the sqrtm is beholden to the "vagaries" of lapack version? 106 funC = funC @ V.T 107 return funC 108 109 110def cov_sqrt(pts: tuple, r: float = 0.2) -> np.ndarray: 111 """Cholesky factor, `C12`, of the covariance (Gaussian variogram) over `pts`. 112 113 `pts` are coordinate arrays, e.g. `Grid2D.mesh` (or `(xx,)` in 1D). 114 The factor is upper triangular, such that `Cov == C12.T @ C12`, 115 and so `z @ C12` has covariance `Cov` if `z` is standard normal. 116 """ 117 dists = dist_euclid(vectorize(*pts)) 118 Cov = 1 - variogram_gauss(dists, r) 119 # C12 = sla.sqrtm(Cov).real.T # unstable for n >≈ 20 120 # C12 = funm_psd(Cov, np.sqrt, sym_square=True).T # too slow for n >= 50^2 121 C12 = sla.cholesky(Cov + 1e-10 * np.eye(len(Cov))) 122 return C12 123 124 125def gaussian_fields(pts: tuple, N: int = 1, r: float = 0.2, rng=None) -> np.ndarray: 126 """Sample `N` Gaussian random fields over `pts`, as `(N, nPts)`. 127 128 Each of zero mean and unit variance, with the covariance of `cov_sqrt` 129 (Gaussian variogram of range `r`). The draws come from `rng` 130 (a `numpy.random.Generator`), or the global `numpy.random` state if `None`. 131 """ 132 rng = np.random if rng is None else rng 133 C12 = cov_sqrt(pts, r) 134 fields = rng.standard_normal((N, len(C12))) @ C12 135 return fields
43def variogram_gauss(xx: np.ndarray, r: float, n: float = 0, a: float = 1 / 3) -> np.ndarray: 44 """Gaussian variogram at the distances `xx`. 45 46 Params: range `r`, nugget `n`, and `a`, which scales the range 47 (for the default, the covariance $ 1 - γ $ is $ e^{-3} $ at `r`). 48 Ref: <https://en.wikipedia.org/wiki/Variogram#Variogram_models> 49 50 >>> xx = np.array([0, 1, 2]) 51 >>> variogram_gauss(xx, 1, n=0.1, a=1) 52 array([0. , 0.6689085 , 0.98351593]) 53 """ 54 # Gauss 55 gamma = 1 - np.exp(-(xx**2) / r**2 / a) 56 # Sill (=1) 57 gamma *= 1 - n 58 # Nugget 59 gamma[xx != 0] += n 60 return gamma
Gaussian variogram at the distances xx.
Params: range r, nugget n, and a, which scales the range
(for the default, the covariance $ 1 - γ $ is $ e^{-3} $ at r).
Ref: https://en.wikipedia.org/wiki/Variogram#Variogram_models
>>> xx = np.array([0, 1, 2])
>>> variogram_gauss(xx, 1, n=0.1, a=1)
array([0. , 0.6689085 , 0.98351593])
63def vectorize(*XYZ: np.ndarray) -> np.ndarray: 64 """Reshape coordinate arrays (`nDim` of them, of equal `shape`) to `(nPts, nDim)`.""" 65 return np.stack(XYZ).reshape((len(XYZ), -1)).T
Reshape coordinate arrays (nDim of them, of equal shape) to (nPts, nDim).
68def dist_euclid(X: np.ndarray) -> np.ndarray: 69 """Pairwise distances between the rows of `X`, like `squareform(pdist(X))`.""" 70 diff = X[:, None, :] - X 71 d2 = np.sum(diff**2, axis=-1) 72 return np.sqrt(d2)
Pairwise distances between the rows of X, like squareform(pdist(X)).
75def funm_psd(C, fun, rk=None, rtol=1e-8, sym_square=True, **kwargs): 76 """Matrix function evaluation for a positive semi-definite matrix. 77 78 Adapted from the `scipy.linalg.funm` doc. 79 80 Note: small `rk` and `driver="evx"` should be faster, 81 but in my (simple but hopefully relevant) trials 82 sticking with the default "evr" and `rk=None` is usually faster. 83 Alternatively, could try iterative algorithms from `sparse.linalg`. 84 85 Example 86 ------- 87 >>> def sqrtm(C): 88 ... return funm_psd(C, np.sqrt) 89 """ 90 # EVD -- possibly truncated (for speed) 91 idx = [max(0, len(C) - rk), len(C) - 1] if rk else None 92 ews, V = sla.eigh(C, subset_by_index=idx, **kwargs) 93 94 # Truncate (for stability) -- NB: ordering low-->high! 95 nNull = sum(ews <= rtol * ews.max()) 96 ews = ews[nNull:] 97 V = V[:, nNull:] 98 99 # Apply 100 ews = fun(ews) 101 102 # Reconstruct 103 funC = V * ews 104 if sym_square: 105 # Optional, since not necessary e.g. for cholesky factors (for sampling) 106 # But, without it the sqrtm is beholden to the "vagaries" of lapack version? 107 funC = funC @ V.T 108 return funC
Matrix function evaluation for a positive semi-definite matrix.
Adapted from the scipy.linalg.funm doc.
Note: small rk and driver="evx" should be faster,
but in my (simple but hopefully relevant) trials
sticking with the default "evr" and rk=None is usually faster.
Alternatively, could try iterative algorithms from sparse.linalg.
Example
>>> def sqrtm(C):
... return funm_psd(C, np.sqrt)
111def cov_sqrt(pts: tuple, r: float = 0.2) -> np.ndarray: 112 """Cholesky factor, `C12`, of the covariance (Gaussian variogram) over `pts`. 113 114 `pts` are coordinate arrays, e.g. `Grid2D.mesh` (or `(xx,)` in 1D). 115 The factor is upper triangular, such that `Cov == C12.T @ C12`, 116 and so `z @ C12` has covariance `Cov` if `z` is standard normal. 117 """ 118 dists = dist_euclid(vectorize(*pts)) 119 Cov = 1 - variogram_gauss(dists, r) 120 # C12 = sla.sqrtm(Cov).real.T # unstable for n >≈ 20 121 # C12 = funm_psd(Cov, np.sqrt, sym_square=True).T # too slow for n >= 50^2 122 C12 = sla.cholesky(Cov + 1e-10 * np.eye(len(Cov))) 123 return C12
Cholesky factor, C12, of the covariance (Gaussian variogram) over pts.
pts are coordinate arrays, e.g. Grid2D.mesh (or (xx,) in 1D).
The factor is upper triangular, such that Cov == C12.T @ C12,
and so z @ C12 has covariance Cov if z is standard normal.
126def gaussian_fields(pts: tuple, N: int = 1, r: float = 0.2, rng=None) -> np.ndarray: 127 """Sample `N` Gaussian random fields over `pts`, as `(N, nPts)`. 128 129 Each of zero mean and unit variance, with the covariance of `cov_sqrt` 130 (Gaussian variogram of range `r`). The draws come from `rng` 131 (a `numpy.random.Generator`), or the global `numpy.random` state if `None`. 132 """ 133 rng = np.random if rng is None else rng 134 C12 = cov_sqrt(pts, r) 135 fields = rng.standard_normal((N, len(C12))) @ C12 136 return fields
Sample N Gaussian random fields over pts, as (N, nPts).
Each of zero mean and unit variance, with the covariance of cov_sqrt
(Gaussian variogram of range r). The draws come from rng
(a numpy.random.Generator), or the global numpy.random state if None.