From e4032d715220fd235cd1ffa5624b063f2f4c35a0 Mon Sep 17 00:00:00 2001 From: LSchueler Date: Wed, 8 Mar 2023 21:15:28 +0100 Subject: [PATCH 001/102] initial commit of Fourier generator --- src/gstools/field/generator.py | 287 ++++++++++++++++++++++++++++++++- src/gstools/field/srf.py | 4 +- src/gstools/field/summator.pyx | 30 +++- 3 files changed, 317 insertions(+), 4 deletions(-) diff --git a/src/gstools/field/generator.py b/src/gstools/field/generator.py index 5db7fae9..3446fc60 100644 --- a/src/gstools/field/generator.py +++ b/src/gstools/field/generator.py @@ -11,6 +11,7 @@ Generator RandMeth IncomprRandMeth + Fourier """ # pylint: disable=C0103, W0222, C0412, W0231 import warnings @@ -22,14 +23,15 @@ from gstools import config from gstools.covmodel.base import CovModel from gstools.random.rng import RNG +from gstools.tools.geometric import generate_grid if config.USE_RUST: # pragma: no cover # pylint: disable=E0401 from gstools_core import summate, summate_incompr else: - from gstools.field.summator import summate, summate_incompr + from gstools.field.summator import summate, summate_incompr, summate_fourier -__all__ = ["Generator", "RandMeth", "IncomprRandMeth"] +__all__ = ["Generator", "RandMeth", "IncomprRandMeth", "Fourier"] SAMPLING = ["auto", "inversion", "mcmc"] @@ -538,3 +540,284 @@ def _create_unit_vector(self, broadcast_shape, axis=0): e1 = np.zeros(shape) e1[axis] = 1.0 return e1 + + +class Fourier(Generator): + r"""Fourier method for calculating isotropic random fields. + + Parameters + ---------- + model : :any:`CovModel` + Covariance model + mode_truncation : :class:`list` + cut-off value of the Fourier modes. + mode_no : :class:`list` + number of Fourier modes per dimension. + seed : :class:`int` or :any:`None`, optional + The seed of the random number generator. + If "None", a random seed is used. Default: :any:`None` + verbose : :class:`bool`, optional + Be chatty during the generation. + Default: :any:`False` + + **kwargs + Placeholder for keyword-args + + Notes + ----- + The Fourier method is used to generate isotropic + spatial random fields characterized by a given covariance model. + The calculation looks like [Hesse2014]_: # TODO + + .. math:: + u\left(x\right)= + \sqrt{2\sigma^{2}}\cdot + \sum_{i=1}^{N}\sqrt{E(k_{i}k_{i}}\left( + Z_{1,i}\cdot\cos\left(\left\langle k_{i},x\right\rangle \right)+ + Z_{2,i}\cdot\sin\left(\left\langle k_{i},x\right\rangle \right) + \right) \sqrt{\Delta k} + + where: + + * :math:`N` : fourier mode number + * :math:`Z_{j,i}` : random samples from a normal distribution + * :math:`k_i` : the spectral density of the covariance model + + References + ---------- + .. [Hesse2014] Heße, F., Prykhodko, V., Schlüter, S., and Attinger, S., + "Generating random fields with a truncated power-law variogram: + A comparison of several numerical methods", + Environmental Modelling & Software, 55, 32-48., (2014) + """ + + def __init__( + self, + model, + modes_truncation, + modes_no, + seed=None, + verbose=False, + **kwargs, + ): + if kwargs: + warnings.warn("gstools.Fourier: **kwargs are ignored") + # initialize attributes + self._modes_truncation = np.array(modes_truncation) + self._modes_no = np.array(modes_no) + self._modes = [] + self._modes_delta = [] + # TODO clean up here + for d in range(model.dim): + self._modes.append( + np.linspace( + -self._modes_truncation[d], + self._modes_truncation[d], + modes_no[d], + ).T + ) + self._modes_delta.append(self._modes[-1][1] - self._modes[-1][0]) + self._modes_delta = np.asarray(self._modes_delta) + self._modes = generate_grid(self._modes) + + self._verbose = bool(verbose) + # initialize private attributes + self._model = None + self._seed = None + self._rng = None + self._z_1 = None + self._z_2 = None + # TODO what to do about cov_samples? + self._spectral_density_sqrt = None + self._value_type = "scalar" + # set model and seed + self.update(model, seed) + + def __call__(self, pos, add_nugget=True): + """Calculate the modes for the Fourier method. + + This method calls the `summate_*` Cython methods, which are the + heart of the randomization method. + + Parameters + ---------- + pos : (d, n), :class:`numpy.ndarray` + the position tuple with d dimensions and n points. + add_nugget : :class:`bool` + Whether to add nugget noise to the field. + + Returns + ------- + :class:`numpy.ndarray` + the random modes + """ + pos = np.asarray(pos, dtype=np.double) + domain_size = pos.max(axis=1) - pos.min(axis=1) + self._modes *= ( + self._modes_no[:, np.newaxis] / + self._modes_truncation[:, np.newaxis] / + domain_size[:, np.newaxis] + ) + # pre calc. the spectral density for all wave numbers + # they are handed over to Cython + k_norm = np.linalg.norm(self._modes, axis=0) + self._spectral_density_sqrt = np.sqrt(self._model.spectral_density(k_norm)) + summed_modes = summate_fourier( + self._spectral_density_sqrt, + self._modes, + self._z_1, + self._z_2, + pos, + ) + nugget = self.get_nugget(summed_modes.shape) if add_nugget else 0.0 + return ( + np.sqrt(2.0 * self.model.var) * + summed_modes * + np.sqrt(np.prod(self._modes_delta)) + + nugget + ) + + def get_nugget(self, shape): + """ + Generate normal distributed values for the nugget simulation. + + Parameters + ---------- + shape : :class:`tuple` + the shape of the summed modes + + Returns + ------- + nugget : :class:`numpy.ndarray` + the nugget in the same shape as the summed modes + """ + if self.model.nugget > 0: + nugget = np.sqrt(self.model.nugget) * self._rng.random.normal( + size=shape + ) + else: + nugget = 0.0 + return nugget + + def update(self, model=None, seed=np.nan): + """Update the model and the seed. + + If model and seed are not different, nothing will be done. + + Parameters + ---------- + model : :any:`CovModel` or :any:`None`, optional + covariance model. Default: :any:`None` + seed : :class:`int` or :any:`None` or :any:`numpy.nan`, optional + the seed of the random number generator. + If :any:`None`, a random seed is used. If :any:`numpy.nan`, + the actual seed will be kept. Default: :any:`numpy.nan` + """ + # check if a new model is given + if isinstance(model, CovModel): + if self.model != model: + self._model = dcp(model) + if seed is None or not np.isnan(seed): + self.reset_seed(seed) + else: + self.reset_seed(self._seed) + # just update the seed, if its a new one + elif seed is None or not np.isnan(seed): + self.seed = seed + # or just update the seed, when no model is given + elif model is None and (seed is None or not np.isnan(seed)): + if isinstance(self._model, CovModel): + self.seed = seed + else: + raise ValueError( + "gstools.field.generator.RandMeth: no 'model' given" + ) + # if the user tries to trick us, we beat him! + elif model is None and np.isnan(seed): + if ( + isinstance(self._model, CovModel) + and self._z_1 is not None + and self._z_2 is not None + and self._spectral_density_sqrt is not None + ): + if self.verbose: + print("RandMeth.update: Nothing will be done...") + else: + raise ValueError( + "gstools.field.generator.RandMeth: " + "neither 'model' nor 'seed' given!" + ) + # wrong model type + else: + raise ValueError( + "gstools.field.generator.RandMeth: 'model' is not an " + "instance of 'gstools.CovModel'" + ) + + def reset_seed(self, seed=np.nan): + """ + Recalculate the random values with the given seed. + + Parameters + ---------- + seed : :class:`int` or :any:`None` or :any:`numpy.nan`, optional + the seed of the random number generator. + If :any:`None`, a random seed is used. If :any:`numpy.nan`, + the actual seed will be kept. Default: :any:`numpy.nan` + + Notes + ----- + Even if the given seed is the present one, modes will be recalculated. + """ + if seed is None or not np.isnan(seed): + self._seed = seed + self._rng = RNG(self._seed) + # normal distributed samples for randmeth + self._z_1 = self._rng.random.normal(size=self._modes.shape[1]) + self._z_2 = self._rng.random.normal(size=self._modes.shape[1]) + + @property + def seed(self): + """:class:`int`: Seed of the master RNG. + + Notes + ----- + If a new seed is given, the setter property not only saves the + new seed, but also creates new random modes with the new seed. + """ + return self._seed + + # TODO get setters, getters right + @seed.setter + def seed(self, new_seed): + if new_seed is not self._seed: + self.reset_seed(new_seed) + + @property + def model(self): + """:any:`CovModel`: Covariance model of the spatial random field.""" + return self._model + + @model.setter + def model(self, model): + self.update(model) + + @property + def verbose(self): + """:class:`bool`: Verbosity of the generator.""" + return self._verbose + + @verbose.setter + def verbose(self, verbose): + self._verbose = bool(verbose) + + @property + def value_type(self): + """:class:`str`: Type of the field values (scalar, vector).""" + return self._value_type + + def __repr__(self): + """Return String representation.""" + return ( + f"{self.name}(model={self.model}, seed={self.seed})" + ) diff --git a/src/gstools/field/srf.py b/src/gstools/field/srf.py index a8a1e575..77285a02 100644 --- a/src/gstools/field/srf.py +++ b/src/gstools/field/srf.py @@ -13,7 +13,7 @@ import numpy as np from gstools.field.base import Field -from gstools.field.generator import Generator, IncomprRandMeth, RandMeth +from gstools.field.generator import Generator, IncomprRandMeth, RandMeth, Fourier from gstools.field.upscaling import var_coarse_graining, var_no_scaling __all__ = ["SRF"] @@ -23,6 +23,7 @@ "IncomprRandMeth": IncomprRandMeth, "VectorField": IncomprRandMeth, "VelocityField": IncomprRandMeth, + "Fourier": Fourier, } """dict: Standard generators for spatial random fields.""" @@ -74,6 +75,7 @@ class SRF(Field): See: :any:`IncomprRandMeth` * "VectorField" : an alias for "IncomprRandMeth" * "VelocityField" : an alias for "IncomprRandMeth" + * "Fourier" : the periodic Fourier method Default: "RandMeth" **generator_kwargs diff --git a/src/gstools/field/summator.pyx b/src/gstools/field/summator.pyx index cad20e1d..87916ab3 100644 --- a/src/gstools/field/summator.pyx +++ b/src/gstools/field/summator.pyx @@ -10,7 +10,7 @@ cimport cython from cython.parallel import prange cimport numpy as np -from libc.math cimport cos, sin +from libc.math cimport pi, cos, sin, sqrt def summate( @@ -79,3 +79,31 @@ def summate_incompr( summed_modes[d, i] += proj[d] * (z_1[j] * cos(phase) + z_2[j] * sin(phase)) return np.asarray(summed_modes) + + +def summate_fourier( + const double[:] spectral_density_sqrt, + const double[:, :] modes, + const double[:] z_1, + const double[:] z_2, + const double[:, :] pos + ): + cdef int i, j, d + cdef double phase + cdef int dim = pos.shape[0] + + cdef int X_len = pos.shape[1] + cdef int N = modes.shape[1] + + cdef double[:] summed_modes = np.zeros(X_len, dtype=float) + + for i in prange(X_len, nogil=True): + for j in range(N): + phase = 0. + for d in range(dim): + phase += modes[d, j] * pos[d, i] + # OpenMP doesn't like *= after +=... seems to be a compiler specific thing + phase = phase * 2. * pi + summed_modes[i] += spectral_density_sqrt[j] * (z_1[j] * cos(phase) + z_2[j] * sin(phase)) + + return np.asarray(summed_modes) From ed0d0bf84dc1ae23d3fb518a07e7a4c026b15b87 Mon Sep 17 00:00:00 2001 From: LSchueler Date: Sun, 2 Apr 2023 19:17:10 +0200 Subject: [PATCH 002/102] Fix k and delta k generation --- src/gstools/field/generator.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/gstools/field/generator.py b/src/gstools/field/generator.py index 3446fc60..2ac456a1 100644 --- a/src/gstools/field/generator.py +++ b/src/gstools/field/generator.py @@ -611,14 +611,11 @@ def __init__( for d in range(model.dim): self._modes.append( np.linspace( - -self._modes_truncation[d], - self._modes_truncation[d], - modes_no[d], + -self._modes_truncation[d]/2, + self._modes_truncation[d]/2, + self._modes_no[d], ).T ) - self._modes_delta.append(self._modes[-1][1] - self._modes[-1][0]) - self._modes_delta = np.asarray(self._modes_delta) - self._modes = generate_grid(self._modes) self._verbose = bool(verbose) # initialize private attributes @@ -653,11 +650,17 @@ def __call__(self, pos, add_nugget=True): """ pos = np.asarray(pos, dtype=np.double) domain_size = pos.max(axis=1) - pos.min(axis=1) - self._modes *= ( - self._modes_no[:, np.newaxis] / - self._modes_truncation[:, np.newaxis] / - domain_size[:, np.newaxis] - ) + self._modes = [ + self._modes[d] / domain_size[d] + for d in range(self._model.dim) + ] + + self._modes_delta = [ + self._modes[d][1] - self._modes[d][0] for d in range(self._model.dim) + ] + self._modes_delta = np.asarray(self._modes_delta) + self._modes = generate_grid(self._modes) + # pre calc. the spectral density for all wave numbers # they are handed over to Cython k_norm = np.linalg.norm(self._modes, axis=0) @@ -773,8 +776,8 @@ def reset_seed(self, seed=np.nan): self._seed = seed self._rng = RNG(self._seed) # normal distributed samples for randmeth - self._z_1 = self._rng.random.normal(size=self._modes.shape[1]) - self._z_2 = self._rng.random.normal(size=self._modes.shape[1]) + self._z_1 = self._rng.random.normal(size=np.prod(self._modes_no)) + self._z_2 = self._rng.random.normal(size=np.prod(self._modes_no)) @property def seed(self): From f61799c3fc399830e4aa115d95cc6a1e16698bfd Mon Sep 17 00:00:00 2001 From: LSchueler Date: Tue, 15 Aug 2023 21:16:24 +0200 Subject: [PATCH 003/102] Fix len-scale problem --- src/gstools/field/generator.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/gstools/field/generator.py b/src/gstools/field/generator.py index 2ac456a1..00e3ca1e 100644 --- a/src/gstools/field/generator.py +++ b/src/gstools/field/generator.py @@ -606,7 +606,6 @@ def __init__( self._modes_truncation = np.array(modes_truncation) self._modes_no = np.array(modes_no) self._modes = [] - self._modes_delta = [] # TODO clean up here for d in range(model.dim): self._modes.append( @@ -614,6 +613,7 @@ def __init__( -self._modes_truncation[d]/2, self._modes_truncation[d]/2, self._modes_no[d], + endpoint=False, ).T ) @@ -655,10 +655,6 @@ def __call__(self, pos, add_nugget=True): for d in range(self._model.dim) ] - self._modes_delta = [ - self._modes[d][1] - self._modes[d][0] for d in range(self._model.dim) - ] - self._modes_delta = np.asarray(self._modes_delta) self._modes = generate_grid(self._modes) # pre calc. the spectral density for all wave numbers @@ -674,9 +670,8 @@ def __call__(self, pos, add_nugget=True): ) nugget = self.get_nugget(summed_modes.shape) if add_nugget else 0.0 return ( - np.sqrt(2.0 * self.model.var) * - summed_modes * - np.sqrt(np.prod(self._modes_delta)) + + np.sqrt(2.0 * self.model.var / np.prod(domain_size)) * + summed_modes + nugget ) From 79f1fc2e0b288e2d871404a19b1b9fa2536083f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lennart=20Sch=C3=BCler?= Date: Wed, 8 Nov 2023 14:14:20 +0200 Subject: [PATCH 004/102] Add optional arg `period_len` to `Fourier` gen --- src/gstools/field/generator.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/gstools/field/generator.py b/src/gstools/field/generator.py index 00e3ca1e..ace93409 100644 --- a/src/gstools/field/generator.py +++ b/src/gstools/field/generator.py @@ -553,6 +553,9 @@ class Fourier(Generator): cut-off value of the Fourier modes. mode_no : :class:`list` number of Fourier modes per dimension. + period_len : :class:`list` or :any:`None`, optional + the period length of the field in each dimension as a factor of the + domain size seed : :class:`int` or :any:`None`, optional The seed of the random number generator. If "None", a random seed is used. Default: :any:`None` @@ -596,6 +599,7 @@ def __init__( model, modes_truncation, modes_no, + period_len=None, seed=None, verbose=False, **kwargs, @@ -617,6 +621,19 @@ def __init__( ).T ) + dim = model.dim + if period_len is None: + period_len = 1.0 + self.period_len = np.array(period_len, dtype=np.double) + self.period_len = np.atleast_1d(self.period_len)[:dim] + if len(self.period_len) > dim: + raise ValueError(f"Fourier: len(period_len) <= {dim=} not fulfilled") + # fill up period_len with period_len[-1], such that len()==dim + if len(self.period_len) < dim: + self.period_len = np.pad( + self.period_len, (0, dim - len(self.period_len)), "edge" + ) + self._verbose = bool(verbose) # initialize private attributes self._model = None @@ -651,7 +668,7 @@ def __call__(self, pos, add_nugget=True): pos = np.asarray(pos, dtype=np.double) domain_size = pos.max(axis=1) - pos.min(axis=1) self._modes = [ - self._modes[d] / domain_size[d] + self._modes[d] / domain_size[d] * self.period_len[d] for d in range(self._model.dim) ] From d011d2619e11c141f4a87442a4f56d92f3402600 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lennart=20Sch=C3=BCler?= Date: Wed, 8 Nov 2023 15:58:26 +0200 Subject: [PATCH 005/102] Add optional arg `period_offset` to `Fourier` gen --- src/gstools/field/generator.py | 35 ++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/gstools/field/generator.py b/src/gstools/field/generator.py index ace93409..ca121d92 100644 --- a/src/gstools/field/generator.py +++ b/src/gstools/field/generator.py @@ -553,9 +553,11 @@ class Fourier(Generator): cut-off value of the Fourier modes. mode_no : :class:`list` number of Fourier modes per dimension. - period_len : :class:`list` or :any:`None`, optional + period_len : :class:`float` or :class:`list`, optional the period length of the field in each dimension as a factor of the domain size + period_offset : :class:`float` or :class:`list`, optional + the period offset by which the field will be shifted seed : :class:`int` or :any:`None`, optional The seed of the random number generator. If "None", a random seed is used. Default: :any:`None` @@ -600,6 +602,7 @@ def __init__( modes_truncation, modes_no, period_len=None, + period_offset=None, seed=None, verbose=False, **kwargs, @@ -622,17 +625,24 @@ def __init__( ) dim = model.dim - if period_len is None: - period_len = 1.0 - self.period_len = np.array(period_len, dtype=np.double) - self.period_len = np.atleast_1d(self.period_len)[:dim] - if len(self.period_len) > dim: - raise ValueError(f"Fourier: len(period_len) <= {dim=} not fulfilled") - # fill up period_len with period_len[-1], such that len()==dim - if len(self.period_len) < dim: - self.period_len = np.pad( - self.period_len, (0, dim - len(self.period_len)), "edge" - ) + + def fill_to_dim(dim, values, dtype, default_value): + r = values + if values is None: + r = default_value + r = np.array(r, dtype=dtype) + r = np.atleast_1d(r)[:dim] + if len(r) > dim: + raise ValueError(f"Fourier: len(values) <= {dim=} not fulfilled") + # fill up values with values[-1], such that len()==dim + if len(r) < dim: + r = np.pad( + r, (0, dim - len(r)), "edge" + ) + return r + + self.period_len = fill_to_dim(model.dim, period_len, np.double, 1.0) + self.period_offset = fill_to_dim(model.dim, period_offset, np.double, 0.0) self._verbose = bool(verbose) # initialize private attributes @@ -666,6 +676,7 @@ def __call__(self, pos, add_nugget=True): the random modes """ pos = np.asarray(pos, dtype=np.double) + pos -= self.period_offset[:, None] domain_size = pos.max(axis=1) - pos.min(axis=1) self._modes = [ self._modes[d] / domain_size[d] * self.period_len[d] From a5e7574cb58a8170eeec4cae0ad091ce123a217b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lennart=20Sch=C3=BCler?= Date: Wed, 8 Nov 2023 19:13:49 +0200 Subject: [PATCH 006/102] Cleanup --- src/gstools/field/generator.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/gstools/field/generator.py b/src/gstools/field/generator.py index ca121d92..2ff6821e 100644 --- a/src/gstools/field/generator.py +++ b/src/gstools/field/generator.py @@ -613,18 +613,14 @@ def __init__( self._modes_truncation = np.array(modes_truncation) self._modes_no = np.array(modes_no) self._modes = [] - # TODO clean up here - for d in range(model.dim): - self._modes.append( - np.linspace( - -self._modes_truncation[d]/2, - self._modes_truncation[d]/2, - self._modes_no[d], - endpoint=False, - ).T - ) - - dim = model.dim + [self._modes.append( + np.linspace( + -self._modes_truncation[d]/2, + self._modes_truncation[d]/2, + self._modes_no[d], + endpoint=False, + ).T + ) for d in range(model.dim)] def fill_to_dim(dim, values, dtype, default_value): r = values @@ -651,7 +647,6 @@ def fill_to_dim(dim, values, dtype, default_value): self._rng = None self._z_1 = None self._z_2 = None - # TODO what to do about cov_samples? self._spectral_density_sqrt = None self._value_type = "scalar" # set model and seed @@ -813,7 +808,6 @@ def seed(self): """ return self._seed - # TODO get setters, getters right @seed.setter def seed(self, new_seed): if new_seed is not self._seed: From 232190028617570150db68808d73ce2eae1920e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lennart=20Sch=C3=BCler?= Date: Wed, 8 Nov 2023 20:06:28 +0200 Subject: [PATCH 007/102] Add getters & setters --- src/gstools/field/generator.py | 59 ++++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/src/gstools/field/generator.py b/src/gstools/field/generator.py index 2ff6821e..f6b23539 100644 --- a/src/gstools/field/generator.py +++ b/src/gstools/field/generator.py @@ -622,20 +622,6 @@ def __init__( ).T ) for d in range(model.dim)] - def fill_to_dim(dim, values, dtype, default_value): - r = values - if values is None: - r = default_value - r = np.array(r, dtype=dtype) - r = np.atleast_1d(r)[:dim] - if len(r) > dim: - raise ValueError(f"Fourier: len(values) <= {dim=} not fulfilled") - # fill up values with values[-1], such that len()==dim - if len(r) < dim: - r = np.pad( - r, (0, dim - len(r)), "edge" - ) - return r self.period_len = fill_to_dim(model.dim, period_len, np.double, 1.0) self.period_offset = fill_to_dim(model.dim, period_offset, np.double, 0.0) @@ -797,6 +783,20 @@ def reset_seed(self, seed=np.nan): self._z_1 = self._rng.random.normal(size=np.prod(self._modes_no)) self._z_2 = self._rng.random.normal(size=np.prod(self._modes_no)) + def _fill_to_dim(self, dim, values, dtype, default_value): + """Fill an array with last element up to len(dim).""" + r = values + if values is None: + r = default_value + r = np.array(r, dtype=dtype) + r = np.atleast_1d(r)[:dim] + if len(r) > dim: + raise ValueError(f"Fourier: len(values) <= {dim=} not fulfilled") + # fill up values with values[-1], such that len()==dim + if len(r) < dim: + r = np.pad(r, (0, dim - len(r)), "edge") + return r + @property def seed(self): """:class:`int`: Seed of the master RNG. @@ -822,6 +822,37 @@ def model(self): def model(self, model): self.update(model) + @property + def modes_truncation(self): + """:class:`list`: Cut-off values of the Fourier modes.""" + return self._modes_truncation + + @modes_truncation.setter + def modes_truncation(self, modes_truncation): + self._modes_truncation = modes_truncation + + @property + def period_len(self): + """:class:`list`: Period length of the field in each dim.""" + return self._period_len + + @period_len.setter + def period_len(self, period_len): + self._period_len = self._fill_to_dim( + self._model.dim, period_len, np.double, 1.0 + ) + + @property + def period_offset(self): + """:class:`list`: Period offset of the field in each dim.""" + return self._period_offset + + @period_offset.setter + def period_offset(self, period_offset): + self._period_offset = self._fill_to_dim( + self._model.dim, period_offset, np.double, 0.0 + ) + @property def verbose(self): """:class:`bool`: Verbosity of the generator.""" From 724e2b80e23f2c8b5e0839b8ada96c000f83aab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lennart=20Sch=C3=BCler?= Date: Wed, 8 Nov 2023 20:07:01 +0200 Subject: [PATCH 008/102] Cleanup --- src/gstools/field/generator.py | 78 +++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 35 deletions(-) diff --git a/src/gstools/field/generator.py b/src/gstools/field/generator.py index f6b23539..a342c4a0 100644 --- a/src/gstools/field/generator.py +++ b/src/gstools/field/generator.py @@ -29,7 +29,11 @@ # pylint: disable=E0401 from gstools_core import summate, summate_incompr else: - from gstools.field.summator import summate, summate_incompr, summate_fourier + from gstools.field.summator import ( + summate, + summate_incompr, + summate_fourier, + ) __all__ = ["Generator", "RandMeth", "IncomprRandMeth", "Fourier"] @@ -549,16 +553,15 @@ class Fourier(Generator): ---------- model : :any:`CovModel` Covariance model - mode_truncation : :class:`list` - cut-off value of the Fourier modes. mode_no : :class:`list` - number of Fourier modes per dimension. + Number of Fourier modes per dimension. + mode_truncation : :class:`list` + Cut-off values of the Fourier modes. period_len : :class:`float` or :class:`list`, optional - the period length of the field in each dimension as a factor of the - domain size + Period length of the field in each dim as a factor of the domain size. period_offset : :class:`float` or :class:`list`, optional - the period offset by which the field will be shifted - seed : :class:`int` or :any:`None`, optional + The period offset by which the field will be shifted. + seed : :class:`int`, optional The seed of the random number generator. If "None", a random seed is used. Default: :any:`None` verbose : :class:`bool`, optional @@ -572,21 +575,21 @@ class Fourier(Generator): ----- The Fourier method is used to generate isotropic spatial random fields characterized by a given covariance model. - The calculation looks like [Hesse2014]_: # TODO + The calculation looks like [Hesse2014]_: # TODO check different source .. math:: u\left(x\right)= \sqrt{2\sigma^{2}}\cdot - \sum_{i=1}^{N}\sqrt{E(k_{i}k_{i}}\left( - Z_{1,i}\cdot\cos\left(\left\langle k_{i},x\right\rangle \right)+ - Z_{2,i}\cdot\sin\left(\left\langle k_{i},x\right\rangle \right) + \sum_{i=1}^{N}\sqrt{E(k_{i})}\left( + Z_{1,i}\cdot\cos\left(2\pi\left\langle k_{i},x\right\rangle \right)+ + Z_{2,i}\cdot\sin\left(2\pi\left\langle k_{i},x\right\rangle \right) \right) \sqrt{\Delta k} where: * :math:`N` : fourier mode number * :math:`Z_{j,i}` : random samples from a normal distribution - * :math:`k_i` : the spectral density of the covariance model + * :math:`k_i` : the equidistant spectral density of the covariance model References ---------- @@ -599,8 +602,8 @@ class Fourier(Generator): def __init__( self, model, - modes_truncation, modes_no, + modes_truncation, period_len=None, period_offset=None, seed=None, @@ -613,18 +616,24 @@ def __init__( self._modes_truncation = np.array(modes_truncation) self._modes_no = np.array(modes_no) self._modes = [] - [self._modes.append( - np.linspace( - -self._modes_truncation[d]/2, - self._modes_truncation[d]/2, - self._modes_no[d], - endpoint=False, - ).T - ) for d in range(model.dim)] - + [ + self._modes.append( + np.linspace( + -self._modes_truncation[d] / 2, + self._modes_truncation[d] / 2, + self._modes_no[d], + endpoint=False, + ).T + ) + for d in range(model.dim) + ] - self.period_len = fill_to_dim(model.dim, period_len, np.double, 1.0) - self.period_offset = fill_to_dim(model.dim, period_offset, np.double, 0.0) + self._period_len = self._fill_to_dim( + model.dim, period_len, np.double, 1.0 + ) + self._period_offset = self._fill_to_dim( + model.dim, period_offset, np.double, 0.0 + ) self._verbose = bool(verbose) # initialize private attributes @@ -657,11 +666,11 @@ def __call__(self, pos, add_nugget=True): the random modes """ pos = np.asarray(pos, dtype=np.double) - pos -= self.period_offset[:, None] + pos -= self._period_offset[:, None] domain_size = pos.max(axis=1) - pos.min(axis=1) self._modes = [ - self._modes[d] / domain_size[d] * self.period_len[d] - for d in range(self._model.dim) + self._modes[d] / domain_size[d] * self._period_len[d] + for d in range(self._model.dim) ] self._modes = generate_grid(self._modes) @@ -669,7 +678,9 @@ def __call__(self, pos, add_nugget=True): # pre calc. the spectral density for all wave numbers # they are handed over to Cython k_norm = np.linalg.norm(self._modes, axis=0) - self._spectral_density_sqrt = np.sqrt(self._model.spectral_density(k_norm)) + self._spectral_density_sqrt = np.sqrt( + self._model.spectral_density(k_norm) + ) summed_modes = summate_fourier( self._spectral_density_sqrt, self._modes, @@ -679,9 +690,8 @@ def __call__(self, pos, add_nugget=True): ) nugget = self.get_nugget(summed_modes.shape) if add_nugget else 0.0 return ( - np.sqrt(2.0 * self.model.var / np.prod(domain_size)) * - summed_modes + - nugget + np.sqrt(2.0 * self.model.var / np.prod(domain_size)) * summed_modes + + nugget ) def get_nugget(self, shape): @@ -869,6 +879,4 @@ def value_type(self): def __repr__(self): """Return String representation.""" - return ( - f"{self.name}(model={self.model}, seed={self.seed})" - ) + return f"{self.name}(model={self.model}, seed={self.seed})" From f4a8a9d9f3186708708a1704a1684090eb3626bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Tue, 6 Jun 2023 12:15:46 +0200 Subject: [PATCH 009/102] Covmodel: all kwargs after dim are now keyword only --- src/gstools/covmodel/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gstools/covmodel/base.py b/src/gstools/covmodel/base.py index 686768fa..5a1a1f5d 100644 --- a/src/gstools/covmodel/base.py +++ b/src/gstools/covmodel/base.py @@ -123,6 +123,7 @@ class CovModel: def __init__( self, dim=3, + *, var=1.0, len_scale=1.0, nugget=0.0, From 5bf5404d159715a4ff7c38c73160571aafeb9091 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Tue, 6 Jun 2023 14:21:17 +0200 Subject: [PATCH 010/102] tests: minimal black fixes --- tests/test_krige.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_krige.py b/tests/test_krige.py index a37bf1e1..d702b0ee 100644 --- a/tests/test_krige.py +++ b/tests/test_krige.py @@ -132,7 +132,6 @@ def test_universal(self): ) def test_detrended(self): - for Model in self.cov_models: for dim in self.dims: model = Model( From 970847d01fd9be61e56e60314cd406ffdc946981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Tue, 6 Jun 2023 14:21:29 +0200 Subject: [PATCH 011/102] covmodel: add time attribute --- src/gstools/covmodel/base.py | 33 ++++++++++++++++++++++++------ src/gstools/covmodel/fit.py | 1 + src/gstools/covmodel/tools.py | 9 +++++---- src/gstools/tools/geometric.py | 37 ++++++++++++++++++++++++---------- 4 files changed, 59 insertions(+), 21 deletions(-) diff --git a/src/gstools/covmodel/base.py b/src/gstools/covmodel/base.py index 5a1a1f5d..3a923c1c 100644 --- a/src/gstools/covmodel/base.py +++ b/src/gstools/covmodel/base.py @@ -103,6 +103,12 @@ class CovModel: disabled. `rescale` can be set to e.g. earth's radius, to have a meaningful `len_scale` parameter. Default: False + time : :class:`bool`, optional + Create a metric spatio-temporal covariance model. + Setting this to true will increase `dim` and `field_dim` by 1. + `spatial_dim` will be `field_dim - 1`. + The time-dimension is appended, meaning the pos tuple is (x,y,z,...,t). + Default: False var_raw : :class:`float` or :any:`None`, optional raw variance of the model which will be multiplied with :any:`CovModel.var_factor` to result in the actual variance. @@ -132,6 +138,7 @@ def __init__( integral_scale=None, rescale=None, latlon=False, + time=False, var_raw=None, hankel_kw=None, **opt_arg, @@ -157,11 +164,13 @@ def __init__( self._nugget_bounds = None self._anis_bounds = None self._opt_arg_bounds = {} - # Set latlon first + # Set latlon and time first self._latlon = bool(latlon) + self._time = bool(time) # SFT class will be created within dim.setter but needs hankel_kw self.hankel_kw = hankel_kw - self.dim = dim + # using time increases model dimension + self.dim = dim + int(self.time) # optional arguments for the variogram-model set_opt_args(self, opt_arg) @@ -177,7 +186,9 @@ def __init__( # set anisotropy and len_scale, disable anisotropy for latlon models self._len_scale, anis = set_len_anis(self.dim, len_scale, anis) if self.latlon: + # keep time anisotropy for metric spatio-temporal model self._anis = np.array((self.dim - 1) * [1], dtype=np.double) + self._anis[-1] = anis[-1] if self.time else 1.0 self._angles = np.array(self.dim * [0], dtype=np.double) else: self._anis = anis @@ -531,14 +542,14 @@ def isometrize(self, pos): """Make a position tuple ready for isotropic operations.""" pos = np.asarray(pos, dtype=np.double).reshape((self.field_dim, -1)) if self.latlon: - return latlon2pos(pos) + return latlon2pos(pos, time=self.time) return np.dot(matrix_isometrize(self.dim, self.angles, self.anis), pos) def anisometrize(self, pos): """Bring a position tuple into the anisotropic coordinate-system.""" pos = np.asarray(pos, dtype=np.double).reshape((self.dim, -1)) if self.latlon: - return pos2latlon(pos) + return pos2latlon(pos, time=self.time) return np.dot( matrix_anisometrize(self.dim, self.angles, self.anis), pos ) @@ -863,6 +874,11 @@ def arg_bounds(self): res.update(self.opt_arg_bounds) return res + @property + def time(self): + """:class:`bool`: Whether the model is a metric spatio-temporal one.""" + return self._time + # geographical coordinates related @property @@ -872,8 +888,13 @@ def latlon(self): @property def field_dim(self): - """:class:`int`: The field dimension of the model.""" - return 2 if self.latlon else self.dim + """:class:`int`: The (parametric) field dimension of the model (with time).""" + return 2 + int(self._time) if self.latlon else self.dim + + @property + def spatial_dim(self): + """:class:`int`: The spatial field dimension of the model (without time).""" + return 2 if self.latlon else self.dim - int(self._time) # standard parameters diff --git a/src/gstools/covmodel/fit.py b/src/gstools/covmodel/fit.py index 2ff5398b..963e6a33 100755 --- a/src/gstools/covmodel/fit.py +++ b/src/gstools/covmodel/fit.py @@ -522,6 +522,7 @@ def logistic_weights(p=0.1, mean=0.7): # pragma: no cover callable Weighting function. """ + # define the callable weights function def func(x_data): """Callable function for the weights.""" diff --git a/src/gstools/covmodel/tools.py b/src/gstools/covmodel/tools.py index 98ed3b8a..37a5dae7 100644 --- a/src/gstools/covmodel/tools.py +++ b/src/gstools/covmodel/tools.py @@ -498,13 +498,13 @@ def set_dim(model, dim): AttributeWarning, ) dim = model.fix_dim() - if model.latlon and dim != 3: + if model.latlon and dim != (3 + int(model.time)): raise ValueError( f"{model.name}: using fixed dimension {model.fix_dim()}, " - "which is not compatible with a latlon model." + f"which is not compatible with a latlon model (with time={model.time})." ) - # force dim=3 for latlon models - dim = 3 if model.latlon else dim + # force dim=3 (or 4 when time=True) for latlon models + dim = (3 + int(model.time)) if model.latlon else dim # set the dimension if dim < 1: raise ValueError("Only dimensions of d >= 1 are supported.") @@ -551,6 +551,7 @@ def compare(this, that): equal &= np.all(np.isclose(this.angles, that.angles)) equal &= np.isclose(this.rescale, that.rescale) equal &= this.latlon == that.latlon + equal &= this.time == that.time for opt in this.opt_arg: equal &= np.isclose(getattr(this, opt), getattr(that, opt)) return equal diff --git a/src/gstools/tools/geometric.py b/src/gstools/tools/geometric.py index afdcacaf..c05d15ab 100644 --- a/src/gstools/tools/geometric.py +++ b/src/gstools/tools/geometric.py @@ -624,60 +624,75 @@ def ang2dir(angles, dtype=np.double, dim=None): return vec -def latlon2pos(latlon, radius=1.0, dtype=np.double): +def latlon2pos(latlon, radius=1.0, dtype=np.double, time=False): """Convert lat-lon geo coordinates to 3D position tuple. Parameters ---------- latlon : :class:`list` of :class:`numpy.ndarray` latitude and longitude given in degrees. + May includes an appended time axis if `time=True`. radius : :class:`float`, optional Earth radius. Default: `1.0` dtype : data-type, optional The desired data-type for the array. If not given, then the type will be determined as the minimum type required to hold the objects in the sequence. Default: None + time : bool, optional + Whether latlon includes an appended time axis. + Default: False Returns ------- :class:`numpy.ndarray` the 3D position array """ - latlon = np.asarray(latlon, dtype=dtype).reshape((2, -1)) + latlon = np.asarray(latlon, dtype=dtype).reshape((3 if time else 2, -1)) + if time: + timeax = latlon[2] + latlon = latlon[:2] lat, lon = np.deg2rad(latlon) - return np.array( - ( - radius * np.cos(lat) * np.cos(lon), - radius * np.cos(lat) * np.sin(lon), - radius * np.sin(lat) * np.ones_like(lon), - ), - dtype=dtype, + pos_tuple = ( + radius * np.cos(lat) * np.cos(lon), + radius * np.cos(lat) * np.sin(lon), + radius * np.sin(lat) * np.ones_like(lon), ) + if time: + return np.array(pos_tuple + (timeax,), dtype=dtype) + return np.array(pos_tuple, dtype=dtype) -def pos2latlon(pos, radius=1.0, dtype=np.double): +def pos2latlon(pos, radius=1.0, dtype=np.double, time=False): """Convert 3D position tuple from sphere to lat-lon geo coordinates. Parameters ---------- pos : :class:`list` of :class:`numpy.ndarray` The position tuple containing points on a unit-sphere. + May includes an appended time axis if `time=True`. radius : :class:`float`, optional Earth radius. Default: `1.0` dtype : data-type, optional The desired data-type for the array. If not given, then the type will be determined as the minimum type required to hold the objects in the sequence. Default: None + time : bool, optional + Whether latlon includes an appended time axis. + Default: False Returns ------- :class:`numpy.ndarray` the 3D position array """ - pos = np.asarray(pos, dtype=dtype).reshape((3, -1)) + pos = np.asarray(pos, dtype=dtype).reshape((4 if time else 3, -1)) # prevent numerical errors in arcsin lat = np.arcsin(np.maximum(np.minimum(pos[2] / radius, 1.0), -1.0)) lon = np.arctan2(pos[1], pos[0]) + if time: + timeax = pos[3] + lat, lon = np.rad2deg((lat, lon), dtype=dtype) + return np.array((lat, lon, timeax), dtype=dtype) return np.rad2deg((lat, lon), dtype=dtype) From 0855f591682e045138e804e37881063527837b17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Tue, 6 Jun 2023 15:15:43 +0200 Subject: [PATCH 012/102] time: add time axis to plotter --- src/gstools/field/base.py | 5 +++++ src/gstools/field/plot.py | 44 ++++++++++++++++++++++++++++++--------- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/gstools/field/base.py b/src/gstools/field/base.py index 903f3893..6098e219 100755 --- a/src/gstools/field/base.py +++ b/src/gstools/field/base.py @@ -678,6 +678,11 @@ def latlon(self): """:class:`bool`: Whether the field depends on geographical coords.""" return False if self.model is None else self.model.latlon + @property + def time(self): + """:class:`bool`: Whether the field depends on time.""" + return False if self.model is None else self.model.time + @property def name(self): """:class:`str`: The name of the class.""" diff --git a/src/gstools/field/plot.py b/src/gstools/field/plot.py index 528cdcc3..f6242535 100644 --- a/src/gstools/field/plot.py +++ b/src/gstools/field/plot.py @@ -54,7 +54,14 @@ def plot_field( if fld.dim == 1: return plot_1d(fld.pos, fld[field], fig, ax, **kwargs) return plot_nd( - fld.pos, fld[field], fld.mesh_type, fig, ax, fld.latlon, **kwargs + fld.pos, + fld[field], + fld.mesh_type, + fig, + ax, + fld.latlon, + fld.time, + **kwargs, ) @@ -104,6 +111,7 @@ def plot_nd( fig=None, ax=None, latlon=False, + time=False, resolution=128, ax_names=None, aspect="quad", @@ -136,6 +144,11 @@ def plot_nd( ValueError will be raised, if a direction was specified. Bin edges need to be given in radians in this case. Default: False + time : :class:`bool`, optional + Indicate a metric spatio-temporal covariance model. + The time-dimension is assumed to be appended, + meaning the pos tuple is (x,y,z,...,t) or (lat, lon, t). + Default: False resolution : :class:`int`, optional Resolution of the imshow plot. The default is 128. ax_names : :class:`list` of :class:`str`, optional @@ -159,14 +172,20 @@ def plot_nd( """ dim = len(pos) assert dim > 1 - assert not latlon or dim == 2 + assert not latlon or dim == 2 + int(bool(time)) if dim == 2 and contour_plot: return _plot_2d( pos, field, mesh_type, fig, ax, latlon, ax_names, **kwargs ) - pos = pos[::-1] if latlon else pos - field = field.T if (latlon and mesh_type != "unstructured") else field - ax_names = _ax_names(dim, latlon, ax_names) + if latlon: + # swap lat-lon to lon-lat (x-y) + if time: + pos = (pos[1], pos[0], pos[2]) + else: + pos = (pos[1], pos[0]) + if mesh_type != "unstructured": + field = np.moveaxis(field, [0, 1], [1, 0]) + ax_names = _ax_names(dim, latlon, time, ax_names) # init planes planes = rotation_planes(dim) plane_names = [f" {ax_names[p[0]]} - {ax_names[p[1]]}" for p in planes] @@ -323,15 +342,20 @@ def plot_vec_field(fld, field="field", fig=None, ax=None): # pragma: no cover return ax -def _ax_names(dim, latlon=False, ax_names=None): +def _ax_names(dim, latlon=False, time=False, ax_names=None): + t_fac = int(bool(time)) if ax_names is not None: assert len(ax_names) >= dim return ax_names[:dim] - if dim == 2 and latlon: - return ["lon", "lat"] + if dim == 2 + t_fac and latlon: + return ["lon", "lat"] + t_fac * ["time"] if dim <= 3: - return ["$x$", "$y$", "$z$"][:dim] + (dim == 1) * ["field"] - return [f"$x_{{{i}}}$" for i in range(dim)] + return ( + ["$x$", "$y$", "$z$"][:dim] + + t_fac * ["time"] + + (dim == 1) * ["field"] + ) + return [f"$x_{{{i}}}$" for i in range(dim - t_fac)] + t_fac * ["time"] def _plot_2d( From befa9e49151e4129daddba41d23986d2f76220e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Tue, 6 Jun 2023 15:17:47 +0200 Subject: [PATCH 013/102] Examples: update examples with new time attribute --- examples/09_spatio_temporal/01_precip_1d.py | 4 +-- examples/09_spatio_temporal/02_precip_2d.py | 4 +-- .../03_geografic_coordinates.py | 34 +++++++++++++++++++ 3 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 examples/09_spatio_temporal/03_geografic_coordinates.py diff --git a/examples/09_spatio_temporal/01_precip_1d.py b/examples/09_spatio_temporal/01_precip_1d.py index 4671b2f7..2c431c7f 100644 --- a/examples/09_spatio_temporal/01_precip_1d.py +++ b/examples/09_spatio_temporal/01_precip_1d.py @@ -26,13 +26,11 @@ # half daily timesteps over three months t = np.arange(0.0, 90.0, 0.5) -# total spatio-temporal dimension -st_dim = 1 + 1 # space-time anisotropy ratio given in units d / km st_anis = 0.4 # an exponential variogram with a corr. lengths of 2d and 5km -model = gs.Exponential(dim=st_dim, var=1.0, len_scale=5.0, anis=st_anis) +model = gs.Exponential(dim=1, time=True, var=1.0, len_scale=5.0, anis=st_anis) # create a spatial random field instance srf = gs.SRF(model, seed=seed) diff --git a/examples/09_spatio_temporal/02_precip_2d.py b/examples/09_spatio_temporal/02_precip_2d.py index 049225d3..d3d781b3 100644 --- a/examples/09_spatio_temporal/02_precip_2d.py +++ b/examples/09_spatio_temporal/02_precip_2d.py @@ -27,13 +27,11 @@ # half daily timesteps over three months t = np.arange(0.0, 90.0, 0.5) -# total spatio-temporal dimension -st_dim = 2 + 1 # space-time anisotropy ratio given in units d / km st_anis = 0.4 # an exponential variogram with a corr. lengths of 5km, 5km, and 2d -model = gs.Exponential(dim=st_dim, var=1.0, len_scale=5.0, anis=st_anis) +model = gs.Exponential(dim=2, time=True, var=1.0, len_scale=5.0, anis=st_anis) # create a spatial random field instance srf = gs.SRF(model, seed=seed) diff --git a/examples/09_spatio_temporal/03_geografic_coordinates.py b/examples/09_spatio_temporal/03_geografic_coordinates.py new file mode 100644 index 00000000..ea65d128 --- /dev/null +++ b/examples/09_spatio_temporal/03_geografic_coordinates.py @@ -0,0 +1,34 @@ +""" +Working with spatio-temporal lat-lon fields +------------------------------------------- + +In this example, we demonstrate how to generate a spatio-temporal +random field on geographical coordinates. + +First we setup a model, with ``latlon=True`` and ``time=True``, +to get the associated spatio-temporal Yadrenko model. + +In addition, we will use the earth radius provided by :any:`EARTH_RADIUS`, +to have a meaningful length scale in km. + +To generate the field, we simply pass ``(lat, lon, time)`` as the position tuple +to the :any:`SRF` class. + +The anisotropy factor of `0.1` will result in a time length-scale of `77.7` days. +""" +import gstools as gs + +model = gs.Gaussian( + latlon=True, + time=True, + var=1, + len_scale=777, + anis=0.1, + rescale=gs.EARTH_RADIUS, +) + +lat = lon = range(-80, 81) +time = range(0, 110, 10) +srf = gs.SRF(model, seed=1234) +field = srf.structured((lat, lon, time)) +srf.plot() From 8dc919c9233632c0f48ca9dcfe4e0ae79e7e9995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Tue, 6 Jun 2023 15:26:56 +0200 Subject: [PATCH 014/102] pylint: ignore 'use-dict-literal', increase max limits --- pyproject.toml | 4 ++-- src/gstools/transform/field.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 41090458..3a6c7ec0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -147,9 +147,9 @@ target-version = [ max-args = 20 max-locals = 50 max-branches = 30 - max-statements = 80 + max-statements = 85 max-attributes = 25 - max-public-methods = 75 + max-public-methods = 80 [tool.cibuildwheel] # Switch to using build diff --git a/src/gstools/transform/field.py b/src/gstools/transform/field.py index 9ac33b6c..81824739 100644 --- a/src/gstools/transform/field.py +++ b/src/gstools/transform/field.py @@ -26,7 +26,7 @@ normal_to_arcsin normal_to_uquad """ -# pylint: disable=C0103, C0123, R0911 +# pylint: disable=C0103, C0123, R0911, R1735 import numpy as np from gstools.normalizer import ( From eb1213a8cfd43fb6b4668300b2d615bdbb90b480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Thu, 8 Jun 2023 16:46:40 +0200 Subject: [PATCH 015/102] CovModel: add radius property; correctly scale time axis for latlon; add DEGREE_SCALE --- src/gstools/__init__.py | 5 ++++- src/gstools/covmodel/base.py | 33 ++++++++++++++++++++++++++++----- src/gstools/covmodel/fit.py | 2 +- src/gstools/covmodel/plot.py | 6 +++--- src/gstools/tools/__init__.py | 6 ++++++ src/gstools/tools/geometric.py | 32 +++++++++++++++++++------------- 6 files changed, 61 insertions(+), 23 deletions(-) diff --git a/src/gstools/__init__.py b/src/gstools/__init__.py index 60dd1e33..8e72fdfa 100644 --- a/src/gstools/__init__.py +++ b/src/gstools/__init__.py @@ -125,6 +125,7 @@ .. autosummary:: EARTH_RADIUS + DEGREE_SCALE """ # Hooray! from gstools import ( @@ -161,6 +162,7 @@ from gstools.field import SRF, CondSRF from gstools.krige import Krige from gstools.tools import ( + DEGREE_SCALE, EARTH_RADIUS, generate_grid, generate_st_grid, @@ -188,7 +190,7 @@ __all__ = ["__version__"] __all__ += ["covmodel", "field", "variogram", "krige", "random", "tools"] -__all__ += ["transform", "normalizer"] +__all__ += ["transform", "normalizer", "config"] __all__ += [ "CovModel", "Gaussian", @@ -226,6 +228,7 @@ "generate_grid", "generate_st_grid", "EARTH_RADIUS", + "DEGREE_SCALE", "vtk_export", "vtk_export_structured", "vtk_export_unstructured", diff --git a/src/gstools/covmodel/base.py b/src/gstools/covmodel/base.py index 3a923c1c..a2740acf 100644 --- a/src/gstools/covmodel/base.py +++ b/src/gstools/covmodel/base.py @@ -103,6 +103,12 @@ class CovModel: disabled. `rescale` can be set to e.g. earth's radius, to have a meaningful `len_scale` parameter. Default: False + radius : :class:`float`, optional + Sphere radius in case of latlon coordinates to get a meaningful length + scale. By default, len_scale is assumed to be in radians with latlon=True. + Can be set to :any:`EARTH_RADIUS` to have len_scale in km or + :any:`DEGREE_SCALE` to have len_scale in degree. + Default: ``1.0`` time : :class:`bool`, optional Create a metric spatio-temporal covariance model. Setting this to true will increase `dim` and `field_dim` by 1. @@ -138,6 +144,7 @@ def __init__( integral_scale=None, rescale=None, latlon=False, + radius=1.0, time=False, var_raw=None, hankel_kw=None, @@ -167,6 +174,7 @@ def __init__( # Set latlon and time first self._latlon = bool(latlon) self._time = bool(time) + self._radius = abs(float(radius)) # SFT class will be created within dim.setter but needs hankel_kw self.hankel_kw = hankel_kw # using time increases model dimension @@ -254,15 +262,15 @@ def cor_axis(self, r, axis=0): def vario_yadrenko(self, zeta): r"""Yadrenko variogram for great-circle distance from latlon-pos.""" - return self.variogram(2 * np.sin(zeta / 2)) + return self.variogram(2 * np.sin(zeta / 2) * self.radius) def cov_yadrenko(self, zeta): r"""Yadrenko covariance for great-circle distance from latlon-pos.""" - return self.covariance(2 * np.sin(zeta / 2)) + return self.covariance(2 * np.sin(zeta / 2) * self.radius) def cor_yadrenko(self, zeta): r"""Yadrenko correlation for great-circle distance from latlon-pos.""" - return self.correlation(2 * np.sin(zeta / 2)) + return self.correlation(2 * np.sin(zeta / 2) * self.radius) def vario_spatial(self, pos): r"""Spatial variogram respecting anisotropy and rotation.""" @@ -542,14 +550,24 @@ def isometrize(self, pos): """Make a position tuple ready for isotropic operations.""" pos = np.asarray(pos, dtype=np.double).reshape((self.field_dim, -1)) if self.latlon: - return latlon2pos(pos, time=self.time) + return latlon2pos( + pos, + radius=self.radius, + time=self.time, + time_scale=self.anis[-1], + ) return np.dot(matrix_isometrize(self.dim, self.angles, self.anis), pos) def anisometrize(self, pos): """Bring a position tuple into the anisotropic coordinate-system.""" pos = np.asarray(pos, dtype=np.double).reshape((self.dim, -1)) if self.latlon: - return pos2latlon(pos, time=self.time) + return pos2latlon( + pos, + radius=self.radius, + time=self.time, + time_scale=self.anis[-1], + ) return np.dot( matrix_anisometrize(self.dim, self.angles, self.anis), pos ) @@ -886,6 +904,11 @@ def latlon(self): """:class:`bool`: Whether the model depends on geographical coords.""" return self._latlon + @property + def radius(self): + """:class:`float`: Sphere radius for geographical coords.""" + return self._radius + @property def field_dim(self): """:class:`int`: The (parametric) field dimension of the model (with time).""" diff --git a/src/gstools/covmodel/fit.py b/src/gstools/covmodel/fit.py index 963e6a33..1ab04e9f 100755 --- a/src/gstools/covmodel/fit.py +++ b/src/gstools/covmodel/fit.py @@ -341,7 +341,7 @@ def _check_vario(model, x_data, y_data): ) if model.latlon: # convert to yadrenko model - x_data = 2 * np.sin(x_data / 2) + x_data = 2 * np.sin(x_data / 2) * model.radius return x_data, y_data, is_dir_vario diff --git a/src/gstools/covmodel/plot.py b/src/gstools/covmodel/plot.py index efcc2630..cab3091f 100644 --- a/src/gstools/covmodel/plot.py +++ b/src/gstools/covmodel/plot.py @@ -150,7 +150,7 @@ def plot_vario_yadrenko( """Plot Yadrenko variogram of a given CovModel.""" fig, ax = get_fig_ax(fig, ax) if x_max is None: - x_max = min(3 * model.len_rescaled, np.pi) + x_max = min(3 * model.len_scale / model.radius, np.pi) x_s = np.linspace(x_min, x_max) kwargs.setdefault("label", f"{model.name} Yadrenko variogram") ax.plot(x_s, model.vario_yadrenko(x_s), **kwargs) @@ -165,7 +165,7 @@ def plot_cov_yadrenko( """Plot Yadrenko covariance of a given CovModel.""" fig, ax = get_fig_ax(fig, ax) if x_max is None: - x_max = min(3 * model.len_rescaled, np.pi) + x_max = min(3 * model.len_scale / model.radius, np.pi) x_s = np.linspace(x_min, x_max) kwargs.setdefault("label", f"{model.name} Yadrenko covariance") ax.plot(x_s, model.cov_yadrenko(x_s), **kwargs) @@ -180,7 +180,7 @@ def plot_cor_yadrenko( """Plot Yadrenko correlation function of a given CovModel.""" fig, ax = get_fig_ax(fig, ax) if x_max is None: - x_max = min(3 * model.len_rescaled, np.pi) + x_max = min(3 * model.len_scale / model.radius, np.pi) x_s = np.linspace(x_min, x_max) kwargs.setdefault("label", f"{model.name} Yadrenko correlation") ax.plot(x_s, model.cor_yadrenko(x_s), **kwargs) diff --git a/src/gstools/tools/__init__.py b/src/gstools/tools/__init__.py index bd329576..79319c49 100644 --- a/src/gstools/tools/__init__.py +++ b/src/gstools/tools/__init__.py @@ -58,10 +58,13 @@ .. autosummary:: EARTH_RADIUS + DEGREE_SCALE ---- .. autodata:: EARTH_RADIUS + +.. autodata:: DEGREE_SCALE """ from gstools.tools.export import ( @@ -103,6 +106,9 @@ EARTH_RADIUS = 6371.0 """float: earth radius for WGS84 ellipsoid in km""" +DEGREE_SCALE = 57.29577951308232 +"""float: radius for unit sphere in degree""" + __all__ = [ "vtk_export", diff --git a/src/gstools/tools/geometric.py b/src/gstools/tools/geometric.py index c05d15ab..4734dcbb 100644 --- a/src/gstools/tools/geometric.py +++ b/src/gstools/tools/geometric.py @@ -624,7 +624,9 @@ def ang2dir(angles, dtype=np.double, dim=None): return vec -def latlon2pos(latlon, radius=1.0, dtype=np.double, time=False): +def latlon2pos( + latlon, radius=1.0, dtype=np.double, time=False, time_scale=1.0 +): """Convert lat-lon geo coordinates to 3D position tuple. Parameters @@ -638,9 +640,12 @@ def latlon2pos(latlon, radius=1.0, dtype=np.double, time=False): The desired data-type for the array. If not given, then the type will be determined as the minimum type required to hold the objects in the sequence. Default: None - time : bool, optional + time : :class:`bool`, optional Whether latlon includes an appended time axis. Default: False + time_scale : :class:`float`, optional + Scaling factor (e.g. anisotropy) for the time axis. + Default: `1.0` Returns ------- @@ -648,21 +653,18 @@ def latlon2pos(latlon, radius=1.0, dtype=np.double, time=False): the 3D position array """ latlon = np.asarray(latlon, dtype=dtype).reshape((3 if time else 2, -1)) - if time: - timeax = latlon[2] - latlon = latlon[:2] - lat, lon = np.deg2rad(latlon) + lat, lon = np.deg2rad(latlon[:2]) pos_tuple = ( radius * np.cos(lat) * np.cos(lon), radius * np.cos(lat) * np.sin(lon), radius * np.sin(lat) * np.ones_like(lon), ) if time: - return np.array(pos_tuple + (timeax,), dtype=dtype) + return np.array(pos_tuple + (latlon[2] / time_scale,), dtype=dtype) return np.array(pos_tuple, dtype=dtype) -def pos2latlon(pos, radius=1.0, dtype=np.double, time=False): +def pos2latlon(pos, radius=1.0, dtype=np.double, time=False, time_scale=1.0): """Convert 3D position tuple from sphere to lat-lon geo coordinates. Parameters @@ -676,9 +678,12 @@ def pos2latlon(pos, radius=1.0, dtype=np.double, time=False): The desired data-type for the array. If not given, then the type will be determined as the minimum type required to hold the objects in the sequence. Default: None - time : bool, optional + time : :class:`bool`, optional Whether latlon includes an appended time axis. Default: False + time_scale : :class:`float`, optional + Scaling factor (e.g. anisotropy) for the time axis. + Default: `1.0` Returns ------- @@ -689,11 +694,12 @@ def pos2latlon(pos, radius=1.0, dtype=np.double, time=False): # prevent numerical errors in arcsin lat = np.arcsin(np.maximum(np.minimum(pos[2] / radius, 1.0), -1.0)) lon = np.arctan2(pos[1], pos[0]) + latlon = np.rad2deg((lat, lon), dtype=dtype) if time: - timeax = pos[3] - lat, lon = np.rad2deg((lat, lon), dtype=dtype) - return np.array((lat, lon, timeax), dtype=dtype) - return np.rad2deg((lat, lon), dtype=dtype) + return np.array( + (latlon[0], latlon[1], pos[3] * time_scale), dtype=dtype + ) + return latlon def chordal_to_great_circle(dist): From 005f89fb0fd40f1d19efa8e55745c4c442e8de37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Thu, 8 Jun 2023 16:51:23 +0200 Subject: [PATCH 016/102] examples: use radius instead of rescale for latlon models now --- examples/03_variogram/06_auto_bin_latlon.py | 2 +- examples/08_geo_coordinates/01_dwd_krige.py | 4 ++-- examples/08_geo_coordinates/README.rst | 4 ++-- ...fic_coordinates.py => 03_geographic_coordinates.py} | 10 ++++++---- 4 files changed, 11 insertions(+), 9 deletions(-) rename examples/09_spatio_temporal/{03_geografic_coordinates.py => 03_geographic_coordinates.py} (78%) diff --git a/examples/03_variogram/06_auto_bin_latlon.py b/examples/03_variogram/06_auto_bin_latlon.py index 70c0a09b..f0e9d8d2 100644 --- a/examples/03_variogram/06_auto_bin_latlon.py +++ b/examples/03_variogram/06_auto_bin_latlon.py @@ -45,7 +45,7 @@ # data-variance as additional information during the fit of the variogram. emp_v = gs.vario_estimate(pos, field, latlon=True) -sph = gs.Spherical(latlon=True, rescale=gs.EARTH_RADIUS) +sph = gs.Spherical(latlon=True, radius=gs.EARTH_RADIUS) sph.fit_variogram(*emp_v, sill=np.var(field)) ax = sph.plot(x_max=2 * np.max(emp_v[0])) ax.scatter(*emp_v, label="Empirical variogram") diff --git a/examples/08_geo_coordinates/01_dwd_krige.py b/examples/08_geo_coordinates/01_dwd_krige.py index b37e7fa0..4c665b9d 100755 --- a/examples/08_geo_coordinates/01_dwd_krige.py +++ b/examples/08_geo_coordinates/01_dwd_krige.py @@ -86,7 +86,7 @@ def get_dwd_temperature(date="2020-06-09 12:00:00"): # Now we can use this estimated variogram to fit a model to it. # Here we will use a :any:`Spherical` model. We select the ``latlon`` option # to use the `Yadrenko` variant of the model to gain a valid model for lat-lon -# coordinates and we rescale it to the earth-radius. Otherwise the length +# coordinates and we set the radius to the earth-radius. Otherwise the length # scale would be given in radians representing the great-circle distance. # # We deselect the nugget from fitting and plot the result afterwards. @@ -97,7 +97,7 @@ def get_dwd_temperature(date="2020-06-09 12:00:00"): # still holds the ordinary routine that is not respecting the great-circle # distance. -model = gs.Spherical(latlon=True, rescale=gs.EARTH_RADIUS) +model = gs.Spherical(latlon=True, radius=gs.EARTH_RADIUS) model.fit_variogram(bin_c, vario, nugget=False) ax = model.plot("vario_yadrenko", x_max=bins[-1]) ax.scatter(bin_c, vario) diff --git a/examples/08_geo_coordinates/README.rst b/examples/08_geo_coordinates/README.rst index 87b419df..895cd250 100644 --- a/examples/08_geo_coordinates/README.rst +++ b/examples/08_geo_coordinates/README.rst @@ -22,14 +22,14 @@ in your desired model (see :any:`CovModel`): By doing so, the model will use the associated `Yadrenko` model on a sphere (see `here `_). The `len_scale` is given in radians to scale the arc-length. -In order to have a more meaningful length scale, one can use the ``rescale`` +In order to have a more meaningful length scale, one can use the ``radius`` argument: .. code-block:: python import gstools as gs - model = gs.Gaussian(latlon=True, var=2, len_scale=500, rescale=gs.EARTH_RADIUS) + model = gs.Gaussian(latlon=True, var=2, len_scale=500, radius=gs.EARTH_RADIUS) Then ``len_scale`` can be interpreted as given in km. diff --git a/examples/09_spatio_temporal/03_geografic_coordinates.py b/examples/09_spatio_temporal/03_geographic_coordinates.py similarity index 78% rename from examples/09_spatio_temporal/03_geografic_coordinates.py rename to examples/09_spatio_temporal/03_geographic_coordinates.py index ea65d128..f1d2959d 100644 --- a/examples/09_spatio_temporal/03_geografic_coordinates.py +++ b/examples/09_spatio_temporal/03_geographic_coordinates.py @@ -14,8 +14,10 @@ To generate the field, we simply pass ``(lat, lon, time)`` as the position tuple to the :any:`SRF` class. -The anisotropy factor of `0.1` will result in a time length-scale of `77.7` days. +The anisotropy factor of `0.1` (days/km) will result in a time length-scale of `77.7` days. """ +import numpy as np + import gstools as gs model = gs.Gaussian( @@ -24,11 +26,11 @@ var=1, len_scale=777, anis=0.1, - rescale=gs.EARTH_RADIUS, + radius=gs.EARTH_RADIUS, ) -lat = lon = range(-80, 81) -time = range(0, 110, 10) +lat = lon = np.linspace(-80, 81, 50) +time = np.linspace(0, 777, 50) srf = gs.SRF(model, seed=1234) field = srf.structured((lat, lon, time)) srf.plot() From 4fa6f7ca30e04e532604d0426badfa38ef05804a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Thu, 8 Jun 2023 17:56:26 +0200 Subject: [PATCH 017/102] CovModel: update __repr__ --- src/gstools/covmodel/tools.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/gstools/covmodel/tools.py b/src/gstools/covmodel/tools.py index 37a5dae7..5ac3bbd2 100644 --- a/src/gstools/covmodel/tools.py +++ b/src/gstools/covmodel/tools.py @@ -569,21 +569,29 @@ def model_repr(model): # pragma: no cover m = model p = model._prec opt_str = "" + t_str = ", time=True" if m.time else "" if not np.isclose(m.rescale, m.default_rescale()): opt_str += f", rescale={m.rescale:.{p}}" for opt in m.opt_arg: opt_str += f", {opt}={getattr(m, opt):.{p}}" - # only print anis and angles if model is anisotropic or rotated - ani_str = "" if m.is_isotropic else f", anis={list_format(m.anis, p)}" - ang_str = f", angles={list_format(m.angles, p)}" if m.do_rotation else "" if m.latlon: + ani_str = ( + "" if m.is_isotropic or not m.time else f", anis={m.anis[-1]:.{p}}" + ) + r_str = "" if np.isclose(m.radius, 1) else f", radius={m.radius:.{p}}" repr_str = ( - f"{m.name}(latlon={m.latlon}, var={m.var:.{p}}, " - f"len_scale={m.len_scale:.{p}}, nugget={m.nugget:.{p}}{opt_str})" + f"{m.name}(latlon={m.latlon}{t_str}, var={m.var:.{p}}, " + f"len_scale={m.len_scale:.{p}}, nugget={m.nugget:.{p}}" + f"{ani_str}{r_str}{opt_str})" ) else: + # only print anis and angles if model is anisotropic or rotated + ani_str = "" if m.is_isotropic else f", anis={list_format(m.anis, p)}" + ang_str = ( + f", angles={list_format(m.angles, p)}" if m.do_rotation else "" + ) repr_str = ( - f"{m.name}(dim={m.dim}, var={m.var:.{p}}, " + f"{m.name}(dim={m.spatial_dim}{t_str}, var={m.var:.{p}}, " f"len_scale={m.len_scale:.{p}}, nugget={m.nugget:.{p}}" f"{ani_str}{ang_str}{opt_str})" ) From 11bd0e0f06399d0cd4c60c6eb82b4eee7e40fe72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Mon, 12 Jun 2023 09:19:27 +0200 Subject: [PATCH 018/102] CovModel: rename 'radius' to 'geo_scale' and add more scales --- examples/03_variogram/06_auto_bin_latlon.py | 2 +- .../08_geo_coordinates/00_field_generation.py | 8 +++--- examples/08_geo_coordinates/01_dwd_krige.py | 4 +-- examples/08_geo_coordinates/README.rst | 4 +-- .../03_geographic_coordinates.py | 12 ++++----- src/gstools/__init__.py | 4 +++ src/gstools/covmodel/base.py | 26 +++++++++---------- src/gstools/covmodel/fit.py | 2 +- src/gstools/covmodel/plot.py | 6 ++--- src/gstools/covmodel/tools.py | 6 ++++- src/gstools/tools/__init__.py | 9 +++++++ src/gstools/tools/geometric.py | 4 +-- 12 files changed, 53 insertions(+), 34 deletions(-) diff --git a/examples/03_variogram/06_auto_bin_latlon.py b/examples/03_variogram/06_auto_bin_latlon.py index f0e9d8d2..83216b2a 100644 --- a/examples/03_variogram/06_auto_bin_latlon.py +++ b/examples/03_variogram/06_auto_bin_latlon.py @@ -45,7 +45,7 @@ # data-variance as additional information during the fit of the variogram. emp_v = gs.vario_estimate(pos, field, latlon=True) -sph = gs.Spherical(latlon=True, radius=gs.EARTH_RADIUS) +sph = gs.Spherical(latlon=True, geo_scale=gs.EARTH_RADIUS) sph.fit_variogram(*emp_v, sill=np.var(field)) ax = sph.plot(x_max=2 * np.max(emp_v[0])) ax.scatter(*emp_v, label="Empirical variogram") diff --git a/examples/08_geo_coordinates/00_field_generation.py b/examples/08_geo_coordinates/00_field_generation.py index b5685c7d..e2530f36 100755 --- a/examples/08_geo_coordinates/00_field_generation.py +++ b/examples/08_geo_coordinates/00_field_generation.py @@ -8,15 +8,17 @@ First we setup a model, with ``latlon=True``, to get the associated Yadrenko model. -In addition, we will use the earth radius provided by :any:`EARTH_RADIUS`, -to have a meaningful length scale in km. +In addition, we will use the earth radius provided by :any:`EARTH_RADIUS` +as ``geo_scale`` to have a meaningful length scale in km. To generate the field, we simply pass ``(lat, lon)`` as the position tuple to the :any:`SRF` class. """ import gstools as gs -model = gs.Gaussian(latlon=True, var=1, len_scale=777, rescale=gs.EARTH_RADIUS) +model = gs.Gaussian( + latlon=True, var=1, len_scale=777, geo_scale=gs.EARTH_RADIUS +) lat = lon = range(-80, 81) srf = gs.SRF(model, seed=1234) diff --git a/examples/08_geo_coordinates/01_dwd_krige.py b/examples/08_geo_coordinates/01_dwd_krige.py index 4c665b9d..39de1b55 100755 --- a/examples/08_geo_coordinates/01_dwd_krige.py +++ b/examples/08_geo_coordinates/01_dwd_krige.py @@ -86,7 +86,7 @@ def get_dwd_temperature(date="2020-06-09 12:00:00"): # Now we can use this estimated variogram to fit a model to it. # Here we will use a :any:`Spherical` model. We select the ``latlon`` option # to use the `Yadrenko` variant of the model to gain a valid model for lat-lon -# coordinates and we set the radius to the earth-radius. Otherwise the length +# coordinates and we set the ``geo_scale`` to the earth-radius. Otherwise the length # scale would be given in radians representing the great-circle distance. # # We deselect the nugget from fitting and plot the result afterwards. @@ -97,7 +97,7 @@ def get_dwd_temperature(date="2020-06-09 12:00:00"): # still holds the ordinary routine that is not respecting the great-circle # distance. -model = gs.Spherical(latlon=True, radius=gs.EARTH_RADIUS) +model = gs.Spherical(latlon=True, geo_scale=gs.EARTH_RADIUS) model.fit_variogram(bin_c, vario, nugget=False) ax = model.plot("vario_yadrenko", x_max=bins[-1]) ax.scatter(bin_c, vario) diff --git a/examples/08_geo_coordinates/README.rst b/examples/08_geo_coordinates/README.rst index 895cd250..76083ce8 100644 --- a/examples/08_geo_coordinates/README.rst +++ b/examples/08_geo_coordinates/README.rst @@ -22,14 +22,14 @@ in your desired model (see :any:`CovModel`): By doing so, the model will use the associated `Yadrenko` model on a sphere (see `here `_). The `len_scale` is given in radians to scale the arc-length. -In order to have a more meaningful length scale, one can use the ``radius`` +In order to have a more meaningful length scale, one can use the ``geo_scale`` argument: .. code-block:: python import gstools as gs - model = gs.Gaussian(latlon=True, var=2, len_scale=500, radius=gs.EARTH_RADIUS) + model = gs.Gaussian(latlon=True, var=2, len_scale=500, geo_scale=gs.EARTH_RADIUS) Then ``len_scale`` can be interpreted as given in km. diff --git a/examples/09_spatio_temporal/03_geographic_coordinates.py b/examples/09_spatio_temporal/03_geographic_coordinates.py index f1d2959d..39cc31ff 100644 --- a/examples/09_spatio_temporal/03_geographic_coordinates.py +++ b/examples/09_spatio_temporal/03_geographic_coordinates.py @@ -8,25 +8,25 @@ First we setup a model, with ``latlon=True`` and ``time=True``, to get the associated spatio-temporal Yadrenko model. -In addition, we will use the earth radius provided by :any:`EARTH_RADIUS`, -to have a meaningful length scale in km. +In addition, we will use the earth radius provided by :any:`EARTH_RADIUS` + as ``geo_scale`` to have a meaningful length scale in km. To generate the field, we simply pass ``(lat, lon, time)`` as the position tuple to the :any:`SRF` class. -The anisotropy factor of `0.1` (days/km) will result in a time length-scale of `77.7` days. +The anisotropy factor of `0.1` (days/km) will result in a time length-scale of `100` days. """ import numpy as np import gstools as gs -model = gs.Gaussian( +model = gs.Matern( latlon=True, time=True, var=1, - len_scale=777, + len_scale=1000, anis=0.1, - radius=gs.EARTH_RADIUS, + geo_scale=gs.EARTH_RADIUS, ) lat = lon = np.linspace(-80, 81, 50) diff --git a/src/gstools/__init__.py b/src/gstools/__init__.py index 8e72fdfa..2404a4ff 100644 --- a/src/gstools/__init__.py +++ b/src/gstools/__init__.py @@ -164,6 +164,8 @@ from gstools.tools import ( DEGREE_SCALE, EARTH_RADIUS, + KM_SCALE, + RADIAN_SCALE, generate_grid, generate_st_grid, rotated_main_axes, @@ -228,7 +230,9 @@ "generate_grid", "generate_st_grid", "EARTH_RADIUS", + "KM_SCALE", "DEGREE_SCALE", + "RADIAN_SCALE", "vtk_export", "vtk_export_structured", "vtk_export_unstructured", diff --git a/src/gstools/covmodel/base.py b/src/gstools/covmodel/base.py index a2740acf..4bbfd78f 100644 --- a/src/gstools/covmodel/base.py +++ b/src/gstools/covmodel/base.py @@ -100,11 +100,11 @@ class CovModel: :math:`2\sin(\alpha/2)`, where :math:`\alpha` is the great-circle distance, which is equal to the spatial distance of two points in 3D. As a consequence, `dim` will be set to `3` and anisotropy will be - disabled. `rescale` can be set to e.g. earth's radius, + disabled. `geo_scale` can be set to e.g. earth's radius, to have a meaningful `len_scale` parameter. Default: False - radius : :class:`float`, optional - Sphere radius in case of latlon coordinates to get a meaningful length + geo_scale : :class:`float`, optional + Geographic scaling in case of latlon coordinates to get a meaningful length scale. By default, len_scale is assumed to be in radians with latlon=True. Can be set to :any:`EARTH_RADIUS` to have len_scale in km or :any:`DEGREE_SCALE` to have len_scale in degree. @@ -144,7 +144,7 @@ def __init__( integral_scale=None, rescale=None, latlon=False, - radius=1.0, + geo_scale=1.0, time=False, var_raw=None, hankel_kw=None, @@ -174,7 +174,7 @@ def __init__( # Set latlon and time first self._latlon = bool(latlon) self._time = bool(time) - self._radius = abs(float(radius)) + self._geo_scale = abs(float(geo_scale)) # SFT class will be created within dim.setter but needs hankel_kw self.hankel_kw = hankel_kw # using time increases model dimension @@ -262,15 +262,15 @@ def cor_axis(self, r, axis=0): def vario_yadrenko(self, zeta): r"""Yadrenko variogram for great-circle distance from latlon-pos.""" - return self.variogram(2 * np.sin(zeta / 2) * self.radius) + return self.variogram(2 * np.sin(zeta / 2) * self.geo_scale) def cov_yadrenko(self, zeta): r"""Yadrenko covariance for great-circle distance from latlon-pos.""" - return self.covariance(2 * np.sin(zeta / 2) * self.radius) + return self.covariance(2 * np.sin(zeta / 2) * self.geo_scale) def cor_yadrenko(self, zeta): r"""Yadrenko correlation for great-circle distance from latlon-pos.""" - return self.correlation(2 * np.sin(zeta / 2) * self.radius) + return self.correlation(2 * np.sin(zeta / 2) * self.geo_scale) def vario_spatial(self, pos): r"""Spatial variogram respecting anisotropy and rotation.""" @@ -552,7 +552,7 @@ def isometrize(self, pos): if self.latlon: return latlon2pos( pos, - radius=self.radius, + radius=self.geo_scale, time=self.time, time_scale=self.anis[-1], ) @@ -564,7 +564,7 @@ def anisometrize(self, pos): if self.latlon: return pos2latlon( pos, - radius=self.radius, + radius=self.geo_scale, time=self.time, time_scale=self.anis[-1], ) @@ -905,9 +905,9 @@ def latlon(self): return self._latlon @property - def radius(self): - """:class:`float`: Sphere radius for geographical coords.""" - return self._radius + def geo_scale(self): + """:class:`float`: Geographic scaling for geographical coords.""" + return self._geo_scale @property def field_dim(self): diff --git a/src/gstools/covmodel/fit.py b/src/gstools/covmodel/fit.py index 1ab04e9f..00b8b2e5 100755 --- a/src/gstools/covmodel/fit.py +++ b/src/gstools/covmodel/fit.py @@ -341,7 +341,7 @@ def _check_vario(model, x_data, y_data): ) if model.latlon: # convert to yadrenko model - x_data = 2 * np.sin(x_data / 2) * model.radius + x_data = 2 * np.sin(x_data / 2) * model.geo_scale return x_data, y_data, is_dir_vario diff --git a/src/gstools/covmodel/plot.py b/src/gstools/covmodel/plot.py index cab3091f..414a95d8 100644 --- a/src/gstools/covmodel/plot.py +++ b/src/gstools/covmodel/plot.py @@ -150,7 +150,7 @@ def plot_vario_yadrenko( """Plot Yadrenko variogram of a given CovModel.""" fig, ax = get_fig_ax(fig, ax) if x_max is None: - x_max = min(3 * model.len_scale / model.radius, np.pi) + x_max = min(3 * model.len_scale / model.geo_scale, np.pi) x_s = np.linspace(x_min, x_max) kwargs.setdefault("label", f"{model.name} Yadrenko variogram") ax.plot(x_s, model.vario_yadrenko(x_s), **kwargs) @@ -165,7 +165,7 @@ def plot_cov_yadrenko( """Plot Yadrenko covariance of a given CovModel.""" fig, ax = get_fig_ax(fig, ax) if x_max is None: - x_max = min(3 * model.len_scale / model.radius, np.pi) + x_max = min(3 * model.len_scale / model.geo_scale, np.pi) x_s = np.linspace(x_min, x_max) kwargs.setdefault("label", f"{model.name} Yadrenko covariance") ax.plot(x_s, model.cov_yadrenko(x_s), **kwargs) @@ -180,7 +180,7 @@ def plot_cor_yadrenko( """Plot Yadrenko correlation function of a given CovModel.""" fig, ax = get_fig_ax(fig, ax) if x_max is None: - x_max = min(3 * model.len_scale / model.radius, np.pi) + x_max = min(3 * model.len_scale / model.geo_scale, np.pi) x_s = np.linspace(x_min, x_max) kwargs.setdefault("label", f"{model.name} Yadrenko correlation") ax.plot(x_s, model.cor_yadrenko(x_s), **kwargs) diff --git a/src/gstools/covmodel/tools.py b/src/gstools/covmodel/tools.py index 5ac3bbd2..a1bef143 100644 --- a/src/gstools/covmodel/tools.py +++ b/src/gstools/covmodel/tools.py @@ -578,7 +578,11 @@ def model_repr(model): # pragma: no cover ani_str = ( "" if m.is_isotropic or not m.time else f", anis={m.anis[-1]:.{p}}" ) - r_str = "" if np.isclose(m.radius, 1) else f", radius={m.radius:.{p}}" + r_str = ( + "" + if np.isclose(m.geo_scale, 1) + else f", geo_scale={m.geo_scale:.{p}}" + ) repr_str = ( f"{m.name}(latlon={m.latlon}{t_str}, var={m.var:.{p}}, " f"len_scale={m.len_scale:.{p}}, nugget={m.nugget:.{p}}" diff --git a/src/gstools/tools/__init__.py b/src/gstools/tools/__init__.py index 79319c49..a5977839 100644 --- a/src/gstools/tools/__init__.py +++ b/src/gstools/tools/__init__.py @@ -106,9 +106,15 @@ EARTH_RADIUS = 6371.0 """float: earth radius for WGS84 ellipsoid in km""" +KM_SCALE = 6371.0 +"""float: earth radius for WGS84 ellipsoid in km""" + DEGREE_SCALE = 57.29577951308232 """float: radius for unit sphere in degree""" +RADIAN_SCALE = 1.0 +"""float: radius for unit sphere""" + __all__ = [ "vtk_export", @@ -141,4 +147,7 @@ "generate_grid", "generate_st_grid", "EARTH_RADIUS", + "KM_SCALE", + "DEGREE_SCALE", + "RADIAN_SCALE", ] diff --git a/src/gstools/tools/geometric.py b/src/gstools/tools/geometric.py index 4734dcbb..dcb85cb3 100644 --- a/src/gstools/tools/geometric.py +++ b/src/gstools/tools/geometric.py @@ -635,7 +635,7 @@ def latlon2pos( latitude and longitude given in degrees. May includes an appended time axis if `time=True`. radius : :class:`float`, optional - Earth radius. Default: `1.0` + Sphere radius. Default: `1.0` dtype : data-type, optional The desired data-type for the array. If not given, then the type will be determined as the minimum type @@ -673,7 +673,7 @@ def pos2latlon(pos, radius=1.0, dtype=np.double, time=False, time_scale=1.0): The position tuple containing points on a unit-sphere. May includes an appended time axis if `time=True`. radius : :class:`float`, optional - Earth radius. Default: `1.0` + Sphere radius. Default: `1.0` dtype : data-type, optional The desired data-type for the array. If not given, then the type will be determined as the minimum type From 7f70c1068d2ca33340c5f3b671eb38f30d9ab471 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Mon, 12 Jun 2023 09:30:10 +0200 Subject: [PATCH 019/102] Better geo_scale documentation --- examples/08_geo_coordinates/00_field_generation.py | 4 +--- src/gstools/__init__.py | 2 ++ src/gstools/covmodel/base.py | 5 +++-- src/gstools/tools/__init__.py | 6 ++++++ 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/examples/08_geo_coordinates/00_field_generation.py b/examples/08_geo_coordinates/00_field_generation.py index e2530f36..9feef660 100755 --- a/examples/08_geo_coordinates/00_field_generation.py +++ b/examples/08_geo_coordinates/00_field_generation.py @@ -16,9 +16,7 @@ """ import gstools as gs -model = gs.Gaussian( - latlon=True, var=1, len_scale=777, geo_scale=gs.EARTH_RADIUS -) +model = gs.Gaussian(latlon=True, len_scale=777, geo_scale=gs.EARTH_RADIUS) lat = lon = range(-80, 81) srf = gs.SRF(model, seed=1234) diff --git a/src/gstools/__init__.py b/src/gstools/__init__.py index 2404a4ff..6d62d558 100644 --- a/src/gstools/__init__.py +++ b/src/gstools/__init__.py @@ -125,7 +125,9 @@ .. autosummary:: EARTH_RADIUS + KM_SCALE DEGREE_SCALE + RADIAN_SCALE """ # Hooray! from gstools import ( diff --git a/src/gstools/covmodel/base.py b/src/gstools/covmodel/base.py index 4bbfd78f..2e965841 100644 --- a/src/gstools/covmodel/base.py +++ b/src/gstools/covmodel/base.py @@ -31,6 +31,7 @@ set_opt_args, spectral_rad_pdf, ) +from gstools.tools import RADIAN_SCALE from gstools.tools.geometric import ( latlon2pos, matrix_anisometrize, @@ -108,7 +109,7 @@ class CovModel: scale. By default, len_scale is assumed to be in radians with latlon=True. Can be set to :any:`EARTH_RADIUS` to have len_scale in km or :any:`DEGREE_SCALE` to have len_scale in degree. - Default: ``1.0`` + Default: :any:`RADIAN_SCALE` time : :class:`bool`, optional Create a metric spatio-temporal covariance model. Setting this to true will increase `dim` and `field_dim` by 1. @@ -144,7 +145,7 @@ def __init__( integral_scale=None, rescale=None, latlon=False, - geo_scale=1.0, + geo_scale=RADIAN_SCALE, time=False, var_raw=None, hankel_kw=None, diff --git a/src/gstools/tools/__init__.py b/src/gstools/tools/__init__.py index a5977839..1f68dbaf 100644 --- a/src/gstools/tools/__init__.py +++ b/src/gstools/tools/__init__.py @@ -58,13 +58,19 @@ .. autosummary:: EARTH_RADIUS + KM_SCALE DEGREE_SCALE + RADIAN_SCALE ---- .. autodata:: EARTH_RADIUS +.. autodata:: KM_SCALE + .. autodata:: DEGREE_SCALE + +.. autodata:: RADIAN_SCALE """ from gstools.tools.export import ( From 923c002dc320bc0d83c8da07263df1b7caf345e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Mon, 12 Jun 2023 10:50:51 +0200 Subject: [PATCH 020/102] examples: fix typo --- examples/09_spatio_temporal/03_geographic_coordinates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/09_spatio_temporal/03_geographic_coordinates.py b/examples/09_spatio_temporal/03_geographic_coordinates.py index 39cc31ff..3ec8d28c 100644 --- a/examples/09_spatio_temporal/03_geographic_coordinates.py +++ b/examples/09_spatio_temporal/03_geographic_coordinates.py @@ -9,7 +9,7 @@ to get the associated spatio-temporal Yadrenko model. In addition, we will use the earth radius provided by :any:`EARTH_RADIUS` - as ``geo_scale`` to have a meaningful length scale in km. +as ``geo_scale`` to have a meaningful length scale in km. To generate the field, we simply pass ``(lat, lon, time)`` as the position tuple to the :any:`SRF` class. From 7c387eb6730e09cfe7284517768fa11c6a54c45f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Mon, 12 Jun 2023 10:55:35 +0200 Subject: [PATCH 021/102] vario: rename 'bin_centres' to 'bin_center' following doc-string --- src/gstools/variogram/variogram.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/gstools/variogram/variogram.py b/src/gstools/variogram/variogram.py index b57e0354..f259bf00 100644 --- a/src/gstools/variogram/variogram.py +++ b/src/gstools/variogram/variogram.py @@ -250,18 +250,18 @@ def vario_estimate( """ if bin_edges is not None: bin_edges = np.array(bin_edges, ndmin=1, dtype=np.double, copy=False) - bin_centres = (bin_edges[:-1] + bin_edges[1:]) / 2.0 + bin_center = (bin_edges[:-1] + bin_edges[1:]) / 2.0 # allow multiple fields at same positions (ndmin=2: first axis -> field ID) # need to convert to ma.array, since list of ma.array is not recognised field = np.ma.array(field, ndmin=2, dtype=np.double, copy=True) masked = np.ma.is_masked(field) or np.any(mask) # catch special case if everything is masked if masked and np.all(mask): - bin_centres = np.empty(0) if bin_edges is None else bin_centres - estimates = np.zeros_like(bin_centres) + bin_center = np.empty(0) if bin_edges is None else bin_center + estimates = np.zeros_like(bin_center) if return_counts: - return bin_centres, estimates, np.zeros_like(estimates, dtype=int) - return bin_centres, estimates + return bin_center, estimates, np.zeros_like(estimates, dtype=int) + return bin_center, estimates if not masked: field = field.filled() # check mesh shape @@ -334,7 +334,7 @@ def vario_estimate( # create bining if not given if bin_edges is None: bin_edges = standard_bins(pos, dim, latlon) - bin_centres = (bin_edges[:-1] + bin_edges[1:]) / 2.0 + bin_center = (bin_edges[:-1] + bin_edges[1:]) / 2.0 # normalize field norm_field_out = remove_trend_norm_mean( *(pos, field, mean, normalizer, trend), @@ -371,7 +371,7 @@ def vario_estimate( if dir_no == 1: estimates, counts = estimates[0], counts[0] est_out = (estimates, counts) - return (bin_centres,) + est_out[: 2 if return_counts else 1] + norm_out + return (bin_center,) + est_out[: 2 if return_counts else 1] + norm_out def vario_estimate_axis( From dcc69fd21095160b5f55cf55fccd8a8d9d5aeb58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Mon, 12 Jun 2023 12:10:44 +0200 Subject: [PATCH 022/102] tools: add great_circle_to_chordal; add radius to chordal_to_great_circle --- src/gstools/tools/geometric.py | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/src/gstools/tools/geometric.py b/src/gstools/tools/geometric.py index dcb85cb3..fcbd7c68 100644 --- a/src/gstools/tools/geometric.py +++ b/src/gstools/tools/geometric.py @@ -27,6 +27,7 @@ latlon2pos pos2latlon chordal_to_great_circle + great_circle_to_chordal """ # pylint: disable=C0103 import numpy as np @@ -702,14 +703,16 @@ def pos2latlon(pos, radius=1.0, dtype=np.double, time=False, time_scale=1.0): return latlon -def chordal_to_great_circle(dist): +def chordal_to_great_circle(dist, radius=1.0): """ Calculate great circle distance corresponding to given chordal distance. Parameters ---------- dist : array_like - Chordal distance of two points on the unit-sphere. + Chordal distance of two points on the sphere. + radius : :class:`float`, optional + Sphere radius. Default: `1.0` Returns ------- @@ -718,6 +721,29 @@ def chordal_to_great_circle(dist): Notes ----- - If given values are not in [0, 1], they will be truncated. + If given values are not in [0, 2 * radius], they will be truncated. + """ + diameter = 2 * radius + return diameter * np.arcsin( + np.maximum(np.minimum(np.divide(dist, diameter), 1), 0) + ) + + +def great_circle_to_chordal(dist, radius=1.0): + """ + Calculate chordal distance corresponding to given great circle distance. + + Parameters + ---------- + dist : array_like + Great circle distance of two points on the sphere. + radius : :class:`float`, optional + Sphere radius. Default: `1.0` + + Returns + ------- + :class:`numpy.ndarray` + Chordal distance corresponding to given great circle distance. """ - return 2 * np.arcsin(np.maximum(np.minimum(np.divide(dist, 2), 1), 0)) + diameter = 2 * radius + return diameter * np.sin(np.divide(dist, diameter)) From 9fe9f58030c19b6baf6cb54317fb1e42dc2addbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Mon, 12 Jun 2023 12:11:59 +0200 Subject: [PATCH 023/102] vario: add geo_scale to variogram estimation routines --- src/gstools/covmodel/base.py | 7 ++++--- src/gstools/covmodel/fit.py | 4 ++-- src/gstools/covmodel/plot.py | 6 +++--- src/gstools/krige/base.py | 5 ++++- src/gstools/variogram/binning.py | 12 ++++++++++-- src/gstools/variogram/variogram.py | 13 ++++++++++++- 6 files changed, 35 insertions(+), 12 deletions(-) diff --git a/src/gstools/covmodel/base.py b/src/gstools/covmodel/base.py index 2e965841..8502be99 100644 --- a/src/gstools/covmodel/base.py +++ b/src/gstools/covmodel/base.py @@ -33,6 +33,7 @@ ) from gstools.tools import RADIAN_SCALE from gstools.tools.geometric import ( + great_circle_to_chordal, latlon2pos, matrix_anisometrize, matrix_isometrize, @@ -263,15 +264,15 @@ def cor_axis(self, r, axis=0): def vario_yadrenko(self, zeta): r"""Yadrenko variogram for great-circle distance from latlon-pos.""" - return self.variogram(2 * np.sin(zeta / 2) * self.geo_scale) + return self.variogram(great_circle_to_chordal(zeta, self.geo_scale)) def cov_yadrenko(self, zeta): r"""Yadrenko covariance for great-circle distance from latlon-pos.""" - return self.covariance(2 * np.sin(zeta / 2) * self.geo_scale) + return self.covariance(great_circle_to_chordal(zeta, self.geo_scale)) def cor_yadrenko(self, zeta): r"""Yadrenko correlation for great-circle distance from latlon-pos.""" - return self.correlation(2 * np.sin(zeta / 2) * self.geo_scale) + return self.correlation(great_circle_to_chordal(zeta, self.geo_scale)) def vario_spatial(self, pos): r"""Spatial variogram respecting anisotropy and rotation.""" diff --git a/src/gstools/covmodel/fit.py b/src/gstools/covmodel/fit.py index 00b8b2e5..dc2d5a3a 100755 --- a/src/gstools/covmodel/fit.py +++ b/src/gstools/covmodel/fit.py @@ -13,7 +13,7 @@ from scipy.optimize import curve_fit from gstools.covmodel.tools import check_arg_in_bounds, default_arg_from_bounds -from gstools.tools.geometric import set_anis +from gstools.tools.geometric import great_circle_to_chordal, set_anis __all__ = ["fit_variogram"] @@ -341,7 +341,7 @@ def _check_vario(model, x_data, y_data): ) if model.latlon: # convert to yadrenko model - x_data = 2 * np.sin(x_data / 2) * model.geo_scale + x_data = great_circle_to_chordal(x_data, model.geo_scale) return x_data, y_data, is_dir_vario diff --git a/src/gstools/covmodel/plot.py b/src/gstools/covmodel/plot.py index 414a95d8..fa526612 100644 --- a/src/gstools/covmodel/plot.py +++ b/src/gstools/covmodel/plot.py @@ -150,7 +150,7 @@ def plot_vario_yadrenko( """Plot Yadrenko variogram of a given CovModel.""" fig, ax = get_fig_ax(fig, ax) if x_max is None: - x_max = min(3 * model.len_scale / model.geo_scale, np.pi) + x_max = 3 * model.len_scale x_s = np.linspace(x_min, x_max) kwargs.setdefault("label", f"{model.name} Yadrenko variogram") ax.plot(x_s, model.vario_yadrenko(x_s), **kwargs) @@ -165,7 +165,7 @@ def plot_cov_yadrenko( """Plot Yadrenko covariance of a given CovModel.""" fig, ax = get_fig_ax(fig, ax) if x_max is None: - x_max = min(3 * model.len_scale / model.geo_scale, np.pi) + x_max = 3 * model.len_scale x_s = np.linspace(x_min, x_max) kwargs.setdefault("label", f"{model.name} Yadrenko covariance") ax.plot(x_s, model.cov_yadrenko(x_s), **kwargs) @@ -180,7 +180,7 @@ def plot_cor_yadrenko( """Plot Yadrenko correlation function of a given CovModel.""" fig, ax = get_fig_ax(fig, ax) if x_max is None: - x_max = min(3 * model.len_scale / model.geo_scale, np.pi) + x_max = 3 * model.len_scale x_s = np.linspace(x_min, x_max) kwargs.setdefault("label", f"{model.name} Yadrenko correlation") ax.plot(x_s, model.cor_yadrenko(x_s), **kwargs) diff --git a/src/gstools/krige/base.py b/src/gstools/krige/base.py index 77c6832e..4d1ee824 100755 --- a/src/gstools/krige/base.py +++ b/src/gstools/krige/base.py @@ -527,7 +527,10 @@ def set_condition( sill = np.var(field) if self.model.is_isotropic: emp_vario = vario_estimate( - self.cond_pos, field, latlon=self.model.latlon + self.cond_pos, + field, + latlon=self.model.latlon, + geo_scale=self.model.geo_scale, ) else: axes = rotated_main_axes(self.model.dim, self.model.angles) diff --git a/src/gstools/variogram/binning.py b/src/gstools/variogram/binning.py index be490110..891b39e5 100644 --- a/src/gstools/variogram/binning.py +++ b/src/gstools/variogram/binning.py @@ -10,6 +10,7 @@ """ import numpy as np +from gstools.tools import RADIAN_SCALE from gstools.tools.geometric import ( chordal_to_great_circle, format_struct_pos_dim, @@ -31,6 +32,7 @@ def standard_bins( mesh_type="unstructured", bin_no=None, max_dist=None, + geo_scale=RADIAN_SCALE, ): r""" Get standard binning. @@ -62,6 +64,12 @@ def standard_bins( Cut of length for the bins. If None is given, it will be set to one third of the box-diameter from the given points. Default: None + geo_scale : :class:`float`, optional + Geographic scaling in case of latlon coordinates to get meaningful bins. + By default, bins are assumed to be given in radians with latlon=True. + Can be set to :any:`EARTH_RADIUS` to have units in km or + :any:`DEGREE_SCALE` to have units in degree. + Default: :any:`RADIAN_SCALE` Returns ------- @@ -80,7 +88,7 @@ def standard_bins( pos = generate_grid(format_struct_pos_dim(pos, dim)[0]) else: pos = np.asarray(pos, dtype=np.double).reshape(dim, -1) - pos = latlon2pos(pos) if latlon else pos + pos = latlon2pos(pos, radius=geo_scale) if latlon else pos pnt_cnt = len(pos[0]) box = [] for axis in pos: @@ -88,7 +96,7 @@ def standard_bins( box = np.asarray(box) diam = np.linalg.norm(box[:, 0] - box[:, 1]) # convert diameter to great-circle distance if using latlon - diam = chordal_to_great_circle(diam) if latlon else diam + diam = chordal_to_great_circle(diam, geo_scale) if latlon else diam bin_no = _sturges(pnt_cnt) if bin_no is None else int(bin_no) max_dist = diam / 3 if max_dist is None else float(max_dist) return np.linspace(0, max_dist, num=bin_no + 1, dtype=np.double) diff --git a/src/gstools/variogram/variogram.py b/src/gstools/variogram/variogram.py index f259bf00..e179dc28 100644 --- a/src/gstools/variogram/variogram.py +++ b/src/gstools/variogram/variogram.py @@ -14,6 +14,7 @@ from gstools import config from gstools.normalizer.tools import remove_trend_norm_mean +from gstools.tools import RADIAN_SCALE from gstools.tools.geometric import ( ang2dir, format_struct_pos_shape, @@ -92,6 +93,7 @@ def vario_estimate( normalizer=None, trend=None, fit_normalizer=False, + geo_scale=RADIAN_SCALE, ): r""" Estimates the empirical variogram. @@ -222,6 +224,12 @@ def vario_estimate( fit_normalizer : :class:`bool`, optional Whether to fit the data-normalizer to the given (detrended) field. Default: False + geo_scale : :class:`float`, optional + Geographic scaling in case of latlon coordinates to get meaningful bins. + By default, bins are assumed to be given in radians with latlon=True. + Can be set to :any:`EARTH_RADIUS` to have units in km or + :any:`DEGREE_SCALE` to have units in degree. + Default: :any:`RADIAN_SCALE` Returns ------- @@ -333,8 +341,11 @@ def vario_estimate( pos = pos[:, sampled_idx] # create bining if not given if bin_edges is None: - bin_edges = standard_bins(pos, dim, latlon) + bin_edges = standard_bins(pos, dim, latlon, geo_scale=geo_scale) bin_center = (bin_edges[:-1] + bin_edges[1:]) / 2.0 + if latlon: + # internally we always use radians + bin_edges /= geo_scale # normalize field norm_field_out = remove_trend_norm_mean( *(pos, field, mean, normalizer, trend), From ecaf9142f7c0056876cd8de30703eeb45b3217d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Mon, 12 Jun 2023 12:20:28 +0200 Subject: [PATCH 024/102] vario: forward kwargs to standard_bins routine --- src/gstools/variogram/variogram.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/gstools/variogram/variogram.py b/src/gstools/variogram/variogram.py index e179dc28..8c5785c6 100644 --- a/src/gstools/variogram/variogram.py +++ b/src/gstools/variogram/variogram.py @@ -94,6 +94,7 @@ def vario_estimate( trend=None, fit_normalizer=False, geo_scale=RADIAN_SCALE, + **std_bins, ): r""" Estimates the empirical variogram. @@ -230,6 +231,9 @@ def vario_estimate( Can be set to :any:`EARTH_RADIUS` to have units in km or :any:`DEGREE_SCALE` to have units in degree. Default: :any:`RADIAN_SCALE` + **std_bins + Optional arguments that are forwarded to the :any:`standard_bins` routine + if no bins are given (bin_no, max_dist). Returns ------- @@ -341,7 +345,9 @@ def vario_estimate( pos = pos[:, sampled_idx] # create bining if not given if bin_edges is None: - bin_edges = standard_bins(pos, dim, latlon, geo_scale=geo_scale) + bin_edges = standard_bins( + pos, dim, latlon, geo_scale=geo_scale, **std_bins + ) bin_center = (bin_edges[:-1] + bin_edges[1:]) / 2.0 if latlon: # internally we always use radians From 42d33dff33af418fc7231feef944ab3ba711e9d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Mon, 12 Jun 2023 12:20:49 +0200 Subject: [PATCH 025/102] examples: update examples for geo_scale --- .../08_geo_coordinates/00_field_generation.py | 9 ++++++--- examples/08_geo_coordinates/01_dwd_krige.py | 15 ++++++++------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/examples/08_geo_coordinates/00_field_generation.py b/examples/08_geo_coordinates/00_field_generation.py index 9feef660..83700d61 100755 --- a/examples/08_geo_coordinates/00_field_generation.py +++ b/examples/08_geo_coordinates/00_field_generation.py @@ -14,6 +14,8 @@ To generate the field, we simply pass ``(lat, lon)`` as the position tuple to the :any:`SRF` class. """ +import numpy as np + import gstools as gs model = gs.Gaussian(latlon=True, len_scale=777, geo_scale=gs.EARTH_RADIUS) @@ -32,7 +34,7 @@ # # As we will see, everthing went well... phew! -bin_edges = [0.01 * i for i in range(30)] +bin_edges = np.linspace(0, 777 * 3, 30) bin_center, emp_vario = gs.vario_estimate( (lat, lon), field, @@ -41,11 +43,12 @@ mesh_type="structured", sampling_size=2000, sampling_seed=12345, + geo_scale=gs.EARTH_RADIUS, ) -ax = model.plot("vario_yadrenko", x_max=0.3) +ax = model.plot("vario_yadrenko", x_max=max(bin_center)) model.fit_variogram(bin_center, emp_vario, nugget=False) -model.plot("vario_yadrenko", ax=ax, label="fitted", x_max=0.3) +model.plot("vario_yadrenko", ax=ax, label="fitted", x_max=max(bin_center)) ax.scatter(bin_center, emp_vario, color="k") print(model) diff --git a/examples/08_geo_coordinates/01_dwd_krige.py b/examples/08_geo_coordinates/01_dwd_krige.py index 39de1b55..f032d0e3 100755 --- a/examples/08_geo_coordinates/01_dwd_krige.py +++ b/examples/08_geo_coordinates/01_dwd_krige.py @@ -76,11 +76,11 @@ def get_dwd_temperature(date="2020-06-09 12:00:00"): ############################################################################### # First we will estimate the variogram of our temperature data. -# As the maximal bin distance we choose 8 degrees, which corresponds to a -# chordal length of about 900 km. +# As the maximal bin distance we choose 900 km. -bins = gs.standard_bins((lat, lon), max_dist=np.deg2rad(8), latlon=True) -bin_c, vario = gs.vario_estimate((lat, lon), temp, bins, latlon=True) +bin_center, vario = gs.vario_estimate( + (lat, lon), temp, latlon=True, geo_scale=gs.EARTH_RADIUS, max_dist=900 +) ############################################################################### # Now we can use this estimated variogram to fit a model to it. @@ -98,9 +98,9 @@ def get_dwd_temperature(date="2020-06-09 12:00:00"): # distance. model = gs.Spherical(latlon=True, geo_scale=gs.EARTH_RADIUS) -model.fit_variogram(bin_c, vario, nugget=False) -ax = model.plot("vario_yadrenko", x_max=bins[-1]) -ax.scatter(bin_c, vario) +model.fit_variogram(bin_center, vario, nugget=False) +ax = model.plot("vario_yadrenko", x_max=max(bin_center)) +ax.scatter(bin_center, vario) print(model) ############################################################################### @@ -171,3 +171,4 @@ def north_south_drift(lat, lon): ############################################################################### # Interpretion of the results is now up to you! ;-) +plt.show() From a91bac25bfd68eb74264499c8af249b53eb70c98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Mon, 12 Jun 2023 12:26:20 +0200 Subject: [PATCH 026/102] debug fix --- examples/08_geo_coordinates/01_dwd_krige.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/08_geo_coordinates/01_dwd_krige.py b/examples/08_geo_coordinates/01_dwd_krige.py index f032d0e3..98c570b4 100755 --- a/examples/08_geo_coordinates/01_dwd_krige.py +++ b/examples/08_geo_coordinates/01_dwd_krige.py @@ -171,4 +171,3 @@ def north_south_drift(lat, lon): ############################################################################### # Interpretion of the results is now up to you! ;-) -plt.show() From 7e034a4b2a229d0f3ae58f5d93bb892eef20c01d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Mon, 12 Jun 2023 12:26:39 +0200 Subject: [PATCH 027/102] update latlon auto-bin example with geo_scale --- examples/03_variogram/06_auto_bin_latlon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/03_variogram/06_auto_bin_latlon.py b/examples/03_variogram/06_auto_bin_latlon.py index 83216b2a..7f3b97fb 100644 --- a/examples/03_variogram/06_auto_bin_latlon.py +++ b/examples/03_variogram/06_auto_bin_latlon.py @@ -44,10 +44,10 @@ # Since the overall range of these meteo-stations is too low, we can use the # data-variance as additional information during the fit of the variogram. -emp_v = gs.vario_estimate(pos, field, latlon=True) +emp_v = gs.vario_estimate(pos, field, latlon=True, geo_scale=gs.EARTH_RADIUS) sph = gs.Spherical(latlon=True, geo_scale=gs.EARTH_RADIUS) sph.fit_variogram(*emp_v, sill=np.var(field)) -ax = sph.plot(x_max=2 * np.max(emp_v[0])) +ax = sph.plot("vario_yadrenko", x_max=2 * np.max(emp_v[0])) ax.scatter(*emp_v, label="Empirical variogram") ax.legend() print(sph) From 8edd1e49b5a9a5ca636ac7b8586036653b7061d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Mon, 12 Jun 2023 12:49:34 +0200 Subject: [PATCH 028/102] examples: update readme for geo_scale --- examples/08_geo_coordinates/README.rst | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/examples/08_geo_coordinates/README.rst b/examples/08_geo_coordinates/README.rst index 76083ce8..b664512f 100644 --- a/examples/08_geo_coordinates/README.rst +++ b/examples/08_geo_coordinates/README.rst @@ -37,20 +37,21 @@ A `Yadrenko` model :math:`C` is derived from a valid isotropic covariance model in 3D :math:`C_{3D}` by the following relation: .. math:: - C(\zeta)=C_{3D}\left(2 \cdot \sin\left(\frac{\zeta}{2}\right)\right) + C(\zeta)=C_{3D}\left(2r \cdot \sin\left(\frac{\zeta}{2r}\right)\right) Where :math:`\zeta` is the -`great-circle distance `_. +`great-circle distance `_ +and :math:`r` is the ``geo_scale``. .. note:: ``lat`` and ``lon`` are given in degree, whereas the great-circle distance - :math:`zeta` is given in radians. + :math:`zeta` is given in units of the ``geo_scale``. -Note, that :math:`2 \cdot \sin(\frac{\zeta}{2})` is the +Note, that :math:`2r \cdot \sin(\frac{\zeta}{2r})` is the `chordal distance `_ -of two points on a sphere, which means we simply think of the earth surface -as a sphere, that is cut out of the surrounding three dimensional space, +of two points on a sphere with radius :math:`r`, which means we simply think of the +earth surface as a sphere, that is cut out of the surrounding three dimensional space, when using the `Yadrenko` model. .. note:: From 1901c8916b40fc2fa382bae2dec67765c25cba7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Mon, 12 Jun 2023 16:35:33 +0200 Subject: [PATCH 029/102] krige: auto fitting not possible for spatio-temporal latlon models; update docs --- src/gstools/krige/base.py | 19 +++++++++++++---- src/gstools/krige/methods.py | 40 +++++++++++++++++++++++++++--------- 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/src/gstools/krige/base.py b/src/gstools/krige/base.py index 4d1ee824..774d2b58 100755 --- a/src/gstools/krige/base.py +++ b/src/gstools/krige/base.py @@ -113,8 +113,12 @@ class Krige(Field): Default: False fit_variogram : :class:`bool`, optional Whether to fit the given variogram model to the data. - This is done by using isotropy settings of the given model, - assuming the sill to be the data variance and with the + Directional variogram fitting is triggered by setting + any anisotropy factor of the model to anything unequal 1 + but the main axes of correlation are taken from the model + rotation angles. If the model is a spatio-temporal latlon + model, this will raise an error. + This assumes the sill to be the data variance and with standard bins provided by the :any:`standard_bins` routine. Default: False @@ -496,8 +500,12 @@ def set_condition( Default: False fit_variogram : :class:`bool`, optional Whether to fit the given variogram model to the data. - This is done by using isotropy settings of the given model, - assuming the sill to be the data variance and with the + Directional variogram fitting is triggered by setting + any anisotropy factor of the model to anything unequal 1 + but the main axes of correlation are taken from the model + rotation angles. If the model is a spatio-temporal latlon + model, this will raise an error. + This assumes the sill to be the data variance and with standard bins provided by the :any:`standard_bins` routine. Default: False """ @@ -522,6 +530,9 @@ def set_condition( self.normalizer.fit(self.cond_val - self.cond_trend) if fit_variogram: # fitting model to empirical variogram of data # normalize field + if self.model.latlon and self.model.time: + msg = "Krige: can't fit variogram for spatio-temporal latlon data." + raise ValueError(msg) field = self.normalizer.normalize(self.cond_val - self.cond_trend) field -= self.cond_mean sill = np.var(field) diff --git a/src/gstools/krige/methods.py b/src/gstools/krige/methods.py index 653785f8..b258a02d 100644 --- a/src/gstools/krige/methods.py +++ b/src/gstools/krige/methods.py @@ -77,8 +77,12 @@ class Simple(Krige): Default: False fit_variogram : :class:`bool`, optional Whether to fit the given variogram model to the data. - This is done by using isotropy settings of the given model, - assuming the sill to be the data variance and with the + Directional variogram fitting is triggered by setting + any anisotropy factor of the model to anything unequal 1 + but the main axes of correlation are taken from the model + rotation angles. If the model is a spatio-temporal latlon + model, this will raise an error. + This assumes the sill to be the data variance and with standard bins provided by the :any:`standard_bins` routine. Default: False """ @@ -171,8 +175,12 @@ class Ordinary(Krige): Default: False fit_variogram : :class:`bool`, optional Whether to fit the given variogram model to the data. - This is done by using isotropy settings of the given model, - assuming the sill to be the data variance and with the + Directional variogram fitting is triggered by setting + any anisotropy factor of the model to anything unequal 1 + but the main axes of correlation are taken from the model + rotation angles. If the model is a spatio-temporal latlon + model, this will raise an error. + This assumes the sill to be the data variance and with standard bins provided by the :any:`standard_bins` routine. Default: False """ @@ -275,8 +283,12 @@ class Universal(Krige): Default: False fit_variogram : :class:`bool`, optional Whether to fit the given variogram model to the data. - This is done by using isotropy settings of the given model, - assuming the sill to be the data variance and with the + Directional variogram fitting is triggered by setting + any anisotropy factor of the model to anything unequal 1 + but the main axes of correlation are taken from the model + rotation angles. If the model is a spatio-temporal latlon + model, this will raise an error. + This assumes the sill to be the data variance and with standard bins provided by the :any:`standard_bins` routine. Default: False """ @@ -376,8 +388,12 @@ class ExtDrift(Krige): Default: False fit_variogram : :class:`bool`, optional Whether to fit the given variogram model to the data. - This is done by using isotropy settings of the given model, - assuming the sill to be the data variance and with the + Directional variogram fitting is triggered by setting + any anisotropy factor of the model to anything unequal 1 + but the main axes of correlation are taken from the model + rotation angles. If the model is a spatio-temporal latlon + model, this will raise an error. + This assumes the sill to be the data variance and with standard bins provided by the :any:`standard_bins` routine. Default: False """ @@ -467,8 +483,12 @@ class Detrended(Krige): Default: `"pinv"` fit_variogram : :class:`bool`, optional Whether to fit the given variogram model to the data. - This is done by using isotropy settings of the given model, - assuming the sill to be the data variance and with the + Directional variogram fitting is triggered by setting + any anisotropy factor of the model to anything unequal 1 + but the main axes of correlation are taken from the model + rotation angles. If the model is a spatio-temporal latlon + model, this will raise an error. + This assumes the sill to be the data variance and with standard bins provided by the :any:`standard_bins` routine. Default: False """ From f68c990cc3954b3cf61984ca17930714b4a42b18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Mon, 12 Jun 2023 17:09:56 +0200 Subject: [PATCH 030/102] CovModel: spatial vario/cov/cor now also use xyz with latlon models --- src/gstools/covmodel/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/gstools/covmodel/base.py b/src/gstools/covmodel/base.py index 8502be99..fe0fc3e9 100644 --- a/src/gstools/covmodel/base.py +++ b/src/gstools/covmodel/base.py @@ -580,7 +580,9 @@ def main_axes(self): def _get_iso_rad(self, pos): """Isometrized radians.""" - return np.linalg.norm(self.isometrize(pos), axis=0) + pos = np.asarray(pos, dtype=np.double).reshape((self.dim, -1)) + iso = np.dot(matrix_isometrize(self.dim, self.angles, self.anis), pos) + return np.linalg.norm(iso, axis=0) # fitting routine From b150275240858abff9d9c56f93a90de99f90c014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Mon, 12 Jun 2023 17:10:13 +0200 Subject: [PATCH 031/102] plot: minor fixes for latlon --- src/gstools/covmodel/plot.py | 16 ++++++++-------- src/gstools/field/plot.py | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/gstools/covmodel/plot.py b/src/gstools/covmodel/plot.py index fa526612..168dc1b4 100644 --- a/src/gstools/covmodel/plot.py +++ b/src/gstools/covmodel/plot.py @@ -52,12 +52,12 @@ # plotting routines ####################################################### -def _plot_spatial(dim, pos, field, fig, ax, latlon, **kwargs): +def _plot_spatial(dim, pos, field, fig, ax, time, **kwargs): from gstools.field.plot import plot_1d, plot_nd if dim == 1: return plot_1d(pos, field, fig, ax, **kwargs) - return plot_nd(pos, field, "structured", fig, ax, latlon, **kwargs) + return plot_nd(pos, field, "structured", fig, ax, time=time, **kwargs) def plot_vario_spatial( @@ -70,7 +70,7 @@ def plot_vario_spatial( pos = [x_s] * model.dim shp = tuple(len(p) for p in pos) fld = model.vario_spatial(generate_grid(pos)).reshape(shp) - return _plot_spatial(model.dim, pos, fld, fig, ax, model.latlon, **kwargs) + return _plot_spatial(model.dim, pos, fld, fig, ax, model.time, **kwargs) def plot_cov_spatial( @@ -83,7 +83,7 @@ def plot_cov_spatial( pos = [x_s] * model.dim shp = tuple(len(p) for p in pos) fld = model.cov_spatial(generate_grid(pos)).reshape(shp) - return _plot_spatial(model.dim, pos, fld, fig, ax, model.latlon, **kwargs) + return _plot_spatial(model.dim, pos, fld, fig, ax, model.time, **kwargs) def plot_cor_spatial( @@ -96,7 +96,7 @@ def plot_cor_spatial( pos = [x_s] * model.dim shp = tuple(len(p) for p in pos) fld = model.cor_spatial(generate_grid(pos)).reshape(shp) - return _plot_spatial(model.dim, pos, fld, fig, ax, model.latlon, **kwargs) + return _plot_spatial(model.dim, pos, fld, fig, ax, model.time, **kwargs) def plot_variogram( @@ -150,7 +150,7 @@ def plot_vario_yadrenko( """Plot Yadrenko variogram of a given CovModel.""" fig, ax = get_fig_ax(fig, ax) if x_max is None: - x_max = 3 * model.len_scale + x_max = min(3 * model.len_scale, model.geo_scale * np.pi) x_s = np.linspace(x_min, x_max) kwargs.setdefault("label", f"{model.name} Yadrenko variogram") ax.plot(x_s, model.vario_yadrenko(x_s), **kwargs) @@ -165,7 +165,7 @@ def plot_cov_yadrenko( """Plot Yadrenko covariance of a given CovModel.""" fig, ax = get_fig_ax(fig, ax) if x_max is None: - x_max = 3 * model.len_scale + x_max = min(3 * model.len_scale, model.geo_scale * np.pi) x_s = np.linspace(x_min, x_max) kwargs.setdefault("label", f"{model.name} Yadrenko covariance") ax.plot(x_s, model.cov_yadrenko(x_s), **kwargs) @@ -180,7 +180,7 @@ def plot_cor_yadrenko( """Plot Yadrenko correlation function of a given CovModel.""" fig, ax = get_fig_ax(fig, ax) if x_max is None: - x_max = 3 * model.len_scale + x_max = min(3 * model.len_scale, model.geo_scale * np.pi) x_s = np.linspace(x_min, x_max) kwargs.setdefault("label", f"{model.name} Yadrenko correlation") ax.plot(x_s, model.cor_yadrenko(x_s), **kwargs) diff --git a/src/gstools/field/plot.py b/src/gstools/field/plot.py index f6242535..38d744ff 100644 --- a/src/gstools/field/plot.py +++ b/src/gstools/field/plot.py @@ -349,9 +349,9 @@ def _ax_names(dim, latlon=False, time=False, ax_names=None): return ax_names[:dim] if dim == 2 + t_fac and latlon: return ["lon", "lat"] + t_fac * ["time"] - if dim <= 3: + if dim - t_fac <= 3: return ( - ["$x$", "$y$", "$z$"][:dim] + ["$x$", "$y$", "$z$"][: dim - t_fac] + t_fac * ["time"] + (dim == 1) * ["field"] ) From 0c6a12425b0733283f1edf6814d8cca8b3b7d217 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Tue, 13 Jun 2023 17:47:12 +0200 Subject: [PATCH 032/102] CovModel: rename 'time' to 'temporal' --- examples/09_spatio_temporal/01_precip_1d.py | 2 +- examples/09_spatio_temporal/02_precip_2d.py | 2 +- .../03_geographic_coordinates.py | 4 +-- examples/09_spatio_temporal/README.rst | 32 +++++++++++++------ src/gstools/covmodel/base.py | 24 +++++++------- src/gstools/covmodel/plot.py | 18 ++++++++--- src/gstools/covmodel/tools.py | 16 ++++++---- src/gstools/field/base.py | 4 +-- src/gstools/field/plot.py | 16 +++++----- src/gstools/tools/geometric.py | 20 +++++++----- 10 files changed, 83 insertions(+), 55 deletions(-) diff --git a/examples/09_spatio_temporal/01_precip_1d.py b/examples/09_spatio_temporal/01_precip_1d.py index 2c431c7f..2da77783 100644 --- a/examples/09_spatio_temporal/01_precip_1d.py +++ b/examples/09_spatio_temporal/01_precip_1d.py @@ -30,7 +30,7 @@ st_anis = 0.4 # an exponential variogram with a corr. lengths of 2d and 5km -model = gs.Exponential(dim=1, time=True, var=1.0, len_scale=5.0, anis=st_anis) +model = gs.Exponential(dim=1, temporal=True, var=1, len_scale=5, anis=st_anis) # create a spatial random field instance srf = gs.SRF(model, seed=seed) diff --git a/examples/09_spatio_temporal/02_precip_2d.py b/examples/09_spatio_temporal/02_precip_2d.py index d3d781b3..3406803b 100644 --- a/examples/09_spatio_temporal/02_precip_2d.py +++ b/examples/09_spatio_temporal/02_precip_2d.py @@ -31,7 +31,7 @@ st_anis = 0.4 # an exponential variogram with a corr. lengths of 5km, 5km, and 2d -model = gs.Exponential(dim=2, time=True, var=1.0, len_scale=5.0, anis=st_anis) +model = gs.Exponential(dim=2, temporal=True, var=1, len_scale=5, anis=st_anis) # create a spatial random field instance srf = gs.SRF(model, seed=seed) diff --git a/examples/09_spatio_temporal/03_geographic_coordinates.py b/examples/09_spatio_temporal/03_geographic_coordinates.py index 3ec8d28c..63ac29b0 100644 --- a/examples/09_spatio_temporal/03_geographic_coordinates.py +++ b/examples/09_spatio_temporal/03_geographic_coordinates.py @@ -5,7 +5,7 @@ In this example, we demonstrate how to generate a spatio-temporal random field on geographical coordinates. -First we setup a model, with ``latlon=True`` and ``time=True``, +First we setup a model, with ``latlon=True`` and ``temporal=True``, to get the associated spatio-temporal Yadrenko model. In addition, we will use the earth radius provided by :any:`EARTH_RADIUS` @@ -22,7 +22,7 @@ model = gs.Matern( latlon=True, - time=True, + temporal=True, var=1, len_scale=1000, anis=0.1, diff --git a/examples/09_spatio_temporal/README.rst b/examples/09_spatio_temporal/README.rst index 07aa5faa..08cc2fcd 100644 --- a/examples/09_spatio_temporal/README.rst +++ b/examples/09_spatio_temporal/README.rst @@ -5,28 +5,42 @@ Spatio-Temporal modelling can provide insights into time dependent processes like rainfall, air temperature or crop yield. GSTools provides the metric spatio-temporal model for all covariance models -by enhancing the spatial model dimension with a time dimension to result in -the spatio-temporal dimension ``st_dim`` and setting a -spatio-temporal anisotropy ratio with ``st_anis``: +by setting ``temporal=True``, which enhances the spatial model dimension with +a time dimension to result in the spatio-temporal dimension and setting a +spatio-temporal anisotropy ratio like this: .. code-block:: python import gstools as gs dim = 3 # spatial dimension - st_dim = dim + 1 st_anis = 0.4 - st_model = gs.Exponential(dim=st_dim, anis=st_anis) + st_model = gs.Exponential(dim=dim, temporal=True, anis=st_anis) -Since it is given in the name "spatio-temporal", -we will always treat the time as last dimension. -This enables us to have spatial anisotropy and rotation defined as in +Since it is given in the name "spatio-temporal", time is always treated as last dimension. +There are three different dimension attributes giving information about (i) the +model dimension (``dim``), (ii) the field dimension (``field_dim``, including time) and +(iii) the spatial dimension (``spatial_dim`` always 1 less than ``field_dim`` for temporal models). +Model and field dimension can differ in case of geographic coordinates where the model dimension is 3, +but the field or parametric dimension is 2. +If the model is spatio-temporal one with geographic coordinates, the model dimension is 4, +the field dimension is 3 and the spatial dimension is 2. + +In the case above we get: + +.. code-block:: python + + st_model.dim == 4 + st_model.field_dim == 4 + st_model.spatial_dim == 3 + +This formulation enables us to have spatial anisotropy and rotation defined as in non-temporal models, without altering the behavior in the time dimension: .. code-block:: python anis = [0.4, 0.2] # spatial anisotropy in 3D angles = [0.5, 0.4, 0.3] # spatial rotation in 3D - st_model = gs.Exponential(dim=st_dim, anis=anis+[st_anis], angles=angles) + st_model = gs.Exponential(dim=dim, temporal=True, anis=anis+[st_anis], angles=angles) In order to generate spatio-temporal position tuples, GSTools provides a convenient function :any:`generate_st_grid`. The output can be used for diff --git a/src/gstools/covmodel/base.py b/src/gstools/covmodel/base.py index fe0fc3e9..fad19671 100644 --- a/src/gstools/covmodel/base.py +++ b/src/gstools/covmodel/base.py @@ -111,7 +111,7 @@ class CovModel: Can be set to :any:`EARTH_RADIUS` to have len_scale in km or :any:`DEGREE_SCALE` to have len_scale in degree. Default: :any:`RADIAN_SCALE` - time : :class:`bool`, optional + temporal : :class:`bool`, optional Create a metric spatio-temporal covariance model. Setting this to true will increase `dim` and `field_dim` by 1. `spatial_dim` will be `field_dim - 1`. @@ -147,7 +147,7 @@ def __init__( rescale=None, latlon=False, geo_scale=RADIAN_SCALE, - time=False, + temporal=False, var_raw=None, hankel_kw=None, **opt_arg, @@ -173,14 +173,14 @@ def __init__( self._nugget_bounds = None self._anis_bounds = None self._opt_arg_bounds = {} - # Set latlon and time first + # Set latlon and temporal first self._latlon = bool(latlon) - self._time = bool(time) + self._temporal = bool(temporal) self._geo_scale = abs(float(geo_scale)) # SFT class will be created within dim.setter but needs hankel_kw self.hankel_kw = hankel_kw # using time increases model dimension - self.dim = dim + int(self.time) + self.dim = dim + int(self.temporal) # optional arguments for the variogram-model set_opt_args(self, opt_arg) @@ -198,7 +198,7 @@ def __init__( if self.latlon: # keep time anisotropy for metric spatio-temporal model self._anis = np.array((self.dim - 1) * [1], dtype=np.double) - self._anis[-1] = anis[-1] if self.time else 1.0 + self._anis[-1] = anis[-1] if self.temporal else 1.0 self._angles = np.array(self.dim * [0], dtype=np.double) else: self._anis = anis @@ -555,7 +555,7 @@ def isometrize(self, pos): return latlon2pos( pos, radius=self.geo_scale, - time=self.time, + temporal=self.temporal, time_scale=self.anis[-1], ) return np.dot(matrix_isometrize(self.dim, self.angles, self.anis), pos) @@ -567,7 +567,7 @@ def anisometrize(self, pos): return pos2latlon( pos, radius=self.geo_scale, - time=self.time, + temporal=self.temporal, time_scale=self.anis[-1], ) return np.dot( @@ -897,9 +897,9 @@ def arg_bounds(self): return res @property - def time(self): + def temporal(self): """:class:`bool`: Whether the model is a metric spatio-temporal one.""" - return self._time + return self._temporal # geographical coordinates related @@ -916,12 +916,12 @@ def geo_scale(self): @property def field_dim(self): """:class:`int`: The (parametric) field dimension of the model (with time).""" - return 2 + int(self._time) if self.latlon else self.dim + return 2 + int(self.temporal) if self.latlon else self.dim @property def spatial_dim(self): """:class:`int`: The spatial field dimension of the model (without time).""" - return 2 if self.latlon else self.dim - int(self._time) + return 2 if self.latlon else self.dim - int(self.temporal) # standard parameters diff --git a/src/gstools/covmodel/plot.py b/src/gstools/covmodel/plot.py index 168dc1b4..43f94df6 100644 --- a/src/gstools/covmodel/plot.py +++ b/src/gstools/covmodel/plot.py @@ -52,12 +52,14 @@ # plotting routines ####################################################### -def _plot_spatial(dim, pos, field, fig, ax, time, **kwargs): +def _plot_spatial(dim, pos, field, fig, ax, temporal, **kwargs): from gstools.field.plot import plot_1d, plot_nd if dim == 1: return plot_1d(pos, field, fig, ax, **kwargs) - return plot_nd(pos, field, "structured", fig, ax, time=time, **kwargs) + return plot_nd( + pos, field, "structured", fig, ax, temporal=temporal, **kwargs + ) def plot_vario_spatial( @@ -70,7 +72,9 @@ def plot_vario_spatial( pos = [x_s] * model.dim shp = tuple(len(p) for p in pos) fld = model.vario_spatial(generate_grid(pos)).reshape(shp) - return _plot_spatial(model.dim, pos, fld, fig, ax, model.time, **kwargs) + return _plot_spatial( + model.dim, pos, fld, fig, ax, model.temporal, **kwargs + ) def plot_cov_spatial( @@ -83,7 +87,9 @@ def plot_cov_spatial( pos = [x_s] * model.dim shp = tuple(len(p) for p in pos) fld = model.cov_spatial(generate_grid(pos)).reshape(shp) - return _plot_spatial(model.dim, pos, fld, fig, ax, model.time, **kwargs) + return _plot_spatial( + model.dim, pos, fld, fig, ax, model.temporal, **kwargs + ) def plot_cor_spatial( @@ -96,7 +102,9 @@ def plot_cor_spatial( pos = [x_s] * model.dim shp = tuple(len(p) for p in pos) fld = model.cor_spatial(generate_grid(pos)).reshape(shp) - return _plot_spatial(model.dim, pos, fld, fig, ax, model.time, **kwargs) + return _plot_spatial( + model.dim, pos, fld, fig, ax, model.temporal, **kwargs + ) def plot_variogram( diff --git a/src/gstools/covmodel/tools.py b/src/gstools/covmodel/tools.py index a1bef143..2c466dc5 100644 --- a/src/gstools/covmodel/tools.py +++ b/src/gstools/covmodel/tools.py @@ -498,13 +498,13 @@ def set_dim(model, dim): AttributeWarning, ) dim = model.fix_dim() - if model.latlon and dim != (3 + int(model.time)): + if model.latlon and dim != (3 + int(model.temporal)): raise ValueError( f"{model.name}: using fixed dimension {model.fix_dim()}, " - f"which is not compatible with a latlon model (with time={model.time})." + f"which is not compatible with a latlon model (with temporal={model.temporal})." ) - # force dim=3 (or 4 when time=True) for latlon models - dim = (3 + int(model.time)) if model.latlon else dim + # force dim=3 (or 4 when temporal=True) for latlon models + dim = (3 + int(model.temporal)) if model.latlon else dim # set the dimension if dim < 1: raise ValueError("Only dimensions of d >= 1 are supported.") @@ -551,7 +551,7 @@ def compare(this, that): equal &= np.all(np.isclose(this.angles, that.angles)) equal &= np.isclose(this.rescale, that.rescale) equal &= this.latlon == that.latlon - equal &= this.time == that.time + equal &= this.temporal == that.temporal for opt in this.opt_arg: equal &= np.isclose(getattr(this, opt), getattr(that, opt)) return equal @@ -569,14 +569,16 @@ def model_repr(model): # pragma: no cover m = model p = model._prec opt_str = "" - t_str = ", time=True" if m.time else "" + t_str = ", temporal=True" if m.temporal else "" if not np.isclose(m.rescale, m.default_rescale()): opt_str += f", rescale={m.rescale:.{p}}" for opt in m.opt_arg: opt_str += f", {opt}={getattr(m, opt):.{p}}" if m.latlon: ani_str = ( - "" if m.is_isotropic or not m.time else f", anis={m.anis[-1]:.{p}}" + "" + if m.is_isotropic or not m.temporal + else f", anis={m.anis[-1]:.{p}}" ) r_str = ( "" diff --git a/src/gstools/field/base.py b/src/gstools/field/base.py index 6098e219..bb514141 100755 --- a/src/gstools/field/base.py +++ b/src/gstools/field/base.py @@ -679,9 +679,9 @@ def latlon(self): return False if self.model is None else self.model.latlon @property - def time(self): + def temporal(self): """:class:`bool`: Whether the field depends on time.""" - return False if self.model is None else self.model.time + return False if self.model is None else self.model.temporal @property def name(self): diff --git a/src/gstools/field/plot.py b/src/gstools/field/plot.py index 38d744ff..37af18bd 100644 --- a/src/gstools/field/plot.py +++ b/src/gstools/field/plot.py @@ -60,7 +60,7 @@ def plot_field( fig, ax, fld.latlon, - fld.time, + fld.temporal, **kwargs, ) @@ -111,7 +111,7 @@ def plot_nd( fig=None, ax=None, latlon=False, - time=False, + temporal=False, resolution=128, ax_names=None, aspect="quad", @@ -144,7 +144,7 @@ def plot_nd( ValueError will be raised, if a direction was specified. Bin edges need to be given in radians in this case. Default: False - time : :class:`bool`, optional + temporal : :class:`bool`, optional Indicate a metric spatio-temporal covariance model. The time-dimension is assumed to be appended, meaning the pos tuple is (x,y,z,...,t) or (lat, lon, t). @@ -172,20 +172,20 @@ def plot_nd( """ dim = len(pos) assert dim > 1 - assert not latlon or dim == 2 + int(bool(time)) + assert not latlon or dim == 2 + int(bool(temporal)) if dim == 2 and contour_plot: return _plot_2d( pos, field, mesh_type, fig, ax, latlon, ax_names, **kwargs ) if latlon: # swap lat-lon to lon-lat (x-y) - if time: + if temporal: pos = (pos[1], pos[0], pos[2]) else: pos = (pos[1], pos[0]) if mesh_type != "unstructured": field = np.moveaxis(field, [0, 1], [1, 0]) - ax_names = _ax_names(dim, latlon, time, ax_names) + ax_names = _ax_names(dim, latlon, temporal, ax_names) # init planes planes = rotation_planes(dim) plane_names = [f" {ax_names[p[0]]} - {ax_names[p[1]]}" for p in planes] @@ -342,8 +342,8 @@ def plot_vec_field(fld, field="field", fig=None, ax=None): # pragma: no cover return ax -def _ax_names(dim, latlon=False, time=False, ax_names=None): - t_fac = int(bool(time)) +def _ax_names(dim, latlon=False, temporal=False, ax_names=None): + t_fac = int(bool(temporal)) if ax_names is not None: assert len(ax_names) >= dim return ax_names[:dim] diff --git a/src/gstools/tools/geometric.py b/src/gstools/tools/geometric.py index fcbd7c68..7f2ea10e 100644 --- a/src/gstools/tools/geometric.py +++ b/src/gstools/tools/geometric.py @@ -626,7 +626,7 @@ def ang2dir(angles, dtype=np.double, dim=None): def latlon2pos( - latlon, radius=1.0, dtype=np.double, time=False, time_scale=1.0 + latlon, radius=1.0, dtype=np.double, temporal=False, time_scale=1.0 ): """Convert lat-lon geo coordinates to 3D position tuple. @@ -641,7 +641,7 @@ def latlon2pos( The desired data-type for the array. If not given, then the type will be determined as the minimum type required to hold the objects in the sequence. Default: None - time : :class:`bool`, optional + temporal : :class:`bool`, optional Whether latlon includes an appended time axis. Default: False time_scale : :class:`float`, optional @@ -653,19 +653,23 @@ def latlon2pos( :class:`numpy.ndarray` the 3D position array """ - latlon = np.asarray(latlon, dtype=dtype).reshape((3 if time else 2, -1)) + latlon = np.asarray(latlon, dtype=dtype).reshape( + (3 if temporal else 2, -1) + ) lat, lon = np.deg2rad(latlon[:2]) pos_tuple = ( radius * np.cos(lat) * np.cos(lon), radius * np.cos(lat) * np.sin(lon), radius * np.sin(lat) * np.ones_like(lon), ) - if time: + if temporal: return np.array(pos_tuple + (latlon[2] / time_scale,), dtype=dtype) return np.array(pos_tuple, dtype=dtype) -def pos2latlon(pos, radius=1.0, dtype=np.double, time=False, time_scale=1.0): +def pos2latlon( + pos, radius=1.0, dtype=np.double, temporal=False, time_scale=1.0 +): """Convert 3D position tuple from sphere to lat-lon geo coordinates. Parameters @@ -679,7 +683,7 @@ def pos2latlon(pos, radius=1.0, dtype=np.double, time=False, time_scale=1.0): The desired data-type for the array. If not given, then the type will be determined as the minimum type required to hold the objects in the sequence. Default: None - time : :class:`bool`, optional + temporal : :class:`bool`, optional Whether latlon includes an appended time axis. Default: False time_scale : :class:`float`, optional @@ -691,12 +695,12 @@ def pos2latlon(pos, radius=1.0, dtype=np.double, time=False, time_scale=1.0): :class:`numpy.ndarray` the 3D position array """ - pos = np.asarray(pos, dtype=dtype).reshape((4 if time else 3, -1)) + pos = np.asarray(pos, dtype=dtype).reshape((4 if temporal else 3, -1)) # prevent numerical errors in arcsin lat = np.arcsin(np.maximum(np.minimum(pos[2] / radius, 1.0), -1.0)) lon = np.arctan2(pos[1], pos[0]) latlon = np.rad2deg((lat, lon), dtype=dtype) - if time: + if temporal: return np.array( (latlon[0], latlon[1], pos[3] * time_scale), dtype=dtype ) From 7e30a54f54084e22224e99b0b65989c741ea8d98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Tue, 13 Jun 2023 18:10:05 +0200 Subject: [PATCH 033/102] geo_scale: better docs; always use KM_SCALE in examples --- README.md | 2 +- docs/source/index.rst | 2 +- examples/03_variogram/06_auto_bin_latlon.py | 4 ++-- examples/08_geo_coordinates/00_field_generation.py | 8 +++++--- examples/08_geo_coordinates/01_dwd_krige.py | 4 ++-- examples/08_geo_coordinates/README.rst | 2 +- .../09_spatio_temporal/03_geographic_coordinates.py | 11 ++++++----- src/gstools/covmodel/base.py | 9 +++++---- src/gstools/variogram/binning.py | 9 +++++---- src/gstools/variogram/variogram.py | 9 +++++---- tests/test_latlon.py | 8 ++++---- 11 files changed, 37 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 2740aa9f..69f1f3ec 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ import cartopy.crs as ccrs import gstools as gs # define a structured field by latitude and longitude lat = lon = range(-80, 81) -model = gs.Gaussian(latlon=True, len_scale=777, rescale=gs.EARTH_RADIUS) +model = gs.Gaussian(latlon=True, len_scale=777, geo_scale=gs.KM_SCALE) srf = gs.SRF(model, seed=12345) field = srf.structured((lat, lon)) # Orthographic plotting with cartopy diff --git a/docs/source/index.rst b/docs/source/index.rst index 86ec0671..df583778 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -200,7 +200,7 @@ This works perfectly well with `cartopy Date: Tue, 13 Jun 2023 18:24:07 +0200 Subject: [PATCH 034/102] Plot: minor fixes for st plots --- src/gstools/covmodel/plot.py | 2 +- src/gstools/field/plot.py | 26 +++++++++++++++++++++----- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/gstools/covmodel/plot.py b/src/gstools/covmodel/plot.py index 43f94df6..334ccbe2 100644 --- a/src/gstools/covmodel/plot.py +++ b/src/gstools/covmodel/plot.py @@ -56,7 +56,7 @@ def _plot_spatial(dim, pos, field, fig, ax, temporal, **kwargs): from gstools.field.plot import plot_1d, plot_nd if dim == 1: - return plot_1d(pos, field, fig, ax, **kwargs) + return plot_1d(pos, field, fig, ax, temporal, **kwargs) return plot_nd( pos, field, "structured", fig, ax, temporal=temporal, **kwargs ) diff --git a/src/gstools/field/plot.py b/src/gstools/field/plot.py index 37af18bd..d06a22a1 100644 --- a/src/gstools/field/plot.py +++ b/src/gstools/field/plot.py @@ -52,7 +52,7 @@ def plot_field( Forwarded to the plotting routine. """ if fld.dim == 1: - return plot_1d(fld.pos, fld[field], fig, ax, **kwargs) + return plot_1d(fld.pos, fld[field], fig, ax, fld.temporal, **kwargs) return plot_nd( fld.pos, fld[field], @@ -65,7 +65,9 @@ def plot_field( ) -def plot_1d(pos, field, fig=None, ax=None, ax_names=None): # pragma: no cover +def plot_1d( + pos, field, fig=None, ax=None, temporal=False, ax_names=None +): # pragma: no cover """ Plot a 1D field. @@ -76,6 +78,11 @@ def plot_1d(pos, field, fig=None, ax=None, ax_names=None): # pragma: no cover or the axes descriptions (for mesh_type='structured') field : :class:`numpy.ndarray` Field values. + temporal : :class:`bool`, optional + Indicate a metric spatio-temporal covariance model. + The time-dimension is assumed to be appended, + meaning the pos tuple is (x,y,z,...,t) or (lat, lon, t). + Default: False fig : :class:`Figure` or :any:`None`, optional Figure to plot the axes on. If `None`, a new one will be created. Default: `None` @@ -95,7 +102,7 @@ def plot_1d(pos, field, fig=None, ax=None, ax_names=None): # pragma: no cover x = pos[0] x = x.flatten() arg = np.argsort(x) - ax_names = _ax_names(1, ax_names=ax_names) + ax_names = _ax_names(1, temporal=temporal, ax_names=ax_names) ax.plot(x[arg], field.ravel()[arg]) ax.set_xlabel(ax_names[0]) ax.set_ylabel(ax_names[1]) @@ -175,7 +182,15 @@ def plot_nd( assert not latlon or dim == 2 + int(bool(temporal)) if dim == 2 and contour_plot: return _plot_2d( - pos, field, mesh_type, fig, ax, latlon, ax_names, **kwargs + pos, + field, + mesh_type, + fig, + ax, + latlon, + temporal, + ax_names, + **kwargs, ) if latlon: # swap lat-lon to lon-lat (x-y) @@ -365,6 +380,7 @@ def _plot_2d( fig=None, ax=None, latlon=False, + temporal=False, ax_names=None, levels=64, antialias=True, @@ -372,7 +388,7 @@ def _plot_2d( """Plot a 2d field with a contour plot.""" fig, ax = get_fig_ax(fig, ax) title = f"Field 2D {mesh_type}: {field.shape}" - ax_names = _ax_names(2, latlon, ax_names=ax_names) + ax_names = _ax_names(2, latlon, temporal, ax_names=ax_names) x, y = pos[::-1] if latlon else pos if mesh_type == "unstructured": cont = ax.tricontourf(x, y, field.ravel(), levels=levels) From 1049cc9238b7cd601d476cc00b506359c62daa52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Tue, 13 Jun 2023 18:25:32 +0200 Subject: [PATCH 035/102] Krige: bugfix for renamed attribute temporal --- src/gstools/krige/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gstools/krige/base.py b/src/gstools/krige/base.py index 774d2b58..336f4cc0 100755 --- a/src/gstools/krige/base.py +++ b/src/gstools/krige/base.py @@ -530,7 +530,7 @@ def set_condition( self.normalizer.fit(self.cond_val - self.cond_trend) if fit_variogram: # fitting model to empirical variogram of data # normalize field - if self.model.latlon and self.model.time: + if self.model.latlon and self.model.temporal: msg = "Krige: can't fit variogram for spatio-temporal latlon data." raise ValueError(msg) field = self.normalizer.normalize(self.cond_val - self.cond_trend) From d0232cd24361e296a603690470d5511586dea65e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Tue, 13 Jun 2023 19:14:26 +0200 Subject: [PATCH 036/102] CovModel: prevent rotation between spatial and temporal dims --- src/gstools/covmodel/base.py | 35 ++++++++++------------- src/gstools/covmodel/tools.py | 54 +++++++++++++++++++++++++++++++++-- 2 files changed, 66 insertions(+), 23 deletions(-) diff --git a/src/gstools/covmodel/base.py b/src/gstools/covmodel/base.py index f5d90d14..7ed5960d 100644 --- a/src/gstools/covmodel/base.py +++ b/src/gstools/covmodel/base.py @@ -28,6 +28,7 @@ set_arg_bounds, set_dim, set_len_anis, + set_model_angles, set_opt_args, spectral_rad_pdf, ) @@ -39,7 +40,6 @@ matrix_isometrize, pos2latlon, rotated_main_axes, - set_angles, ) __all__ = ["CovModel"] @@ -194,16 +194,15 @@ def __init__( # set parameters self.rescale = rescale self._nugget = float(nugget) + # set anisotropy and len_scale, disable anisotropy for latlon models - self._len_scale, anis = set_len_anis(self.dim, len_scale, anis) - if self.latlon: - # keep time anisotropy for metric spatio-temporal model - self._anis = np.array((self.dim - 1) * [1], dtype=np.double) - self._anis[-1] = anis[-1] if self.temporal else 1.0 - self._angles = np.array(self.dim * [0], dtype=np.double) - else: - self._anis = anis - self._angles = set_angles(self.dim, angles) + self._len_scale, self._anis = set_len_anis( + self.dim, len_scale, anis, self.latlon, self.temporal + ) + self._angles = set_model_angles( + self.dim, angles, self.latlon, self.temporal + ) + # set var at last, because of the var_factor (to be right initialized) if var_raw is None: self._var = None @@ -1004,12 +1003,9 @@ def anis(self): @anis.setter def anis(self, anis): - if self.latlon: - self._anis = np.array((self.dim - 1) * [1], dtype=np.double) - else: - self._len_scale, self._anis = set_len_anis( - self.dim, self.len_scale, anis - ) + self._len_scale, self._anis = set_len_anis( + self.dim, self.len_scale, anis, self.latlon, self.temporal + ) self.check_arg_bounds() @property @@ -1019,10 +1015,9 @@ def angles(self): @angles.setter def angles(self, angles): - if self.latlon: - self._angles = np.array(self.dim * [0], dtype=np.double) - else: - self._angles = set_angles(self.dim, angles) + self._angles = set_model_angles( + self.dim, angles, self.latlon, self.temporal + ) self.check_arg_bounds() @property diff --git a/src/gstools/covmodel/tools.py b/src/gstools/covmodel/tools.py index 2c466dc5..17c76531 100644 --- a/src/gstools/covmodel/tools.py +++ b/src/gstools/covmodel/tools.py @@ -30,7 +30,7 @@ from scipy import special as sps from scipy.optimize import root -from gstools.tools.geometric import set_angles, set_anis +from gstools.tools.geometric import no_of_angles, set_angles, set_anis from gstools.tools.misc import list_format __all__ = [ @@ -38,6 +38,7 @@ "rad_fac", "set_opt_args", "set_len_anis", + "set_model_angles", "check_bounds", "check_arg_in_bounds", "default_arg_from_bounds", @@ -183,7 +184,7 @@ def set_opt_args(model, opt_arg): setattr(model, opt_name, float(opt_arg[opt_name])) -def set_len_anis(dim, len_scale, anis): +def set_len_anis(dim, len_scale, anis, latlon=False, temporal=False): """Set the length scale and anisotropy factors for the given dimension. Parameters @@ -194,6 +195,13 @@ def set_len_anis(dim, len_scale, anis): the length scale of the SRF in x direction or in x- (y-, ...) direction anis : :class:`float` or :class:`list` the anisotropy of length scales along the transversal axes + latlon : :class:`bool`, optional + Whether the model is describing 2D fields on earths surface described + by latitude and longitude. + Default: False + temporal : :class:`bool`, optional + Whether a time-dimension is appended. + Default: False Returns ------- @@ -235,9 +243,47 @@ def set_len_anis(dim, len_scale, anis): raise ValueError( "anisotropy-ratios needs to be > 0, " + "got: " + str(out_anis) ) + # no anisotropy for latlon (only when temporal, but then only in time-dimension) + if latlon: + out_anis[: dim - (2 if temporal else 1)] = 1.0 return out_len_scale, out_anis +def set_model_angles(dim, angles, latlon=False, temporal=False): + """Set the model angles for the given dimension. + + Parameters + ---------- + dim : :class:`int` + spatial dimension + angles : :class:`float` or :class:`list` + the angles of the SRF + latlon : :class:`bool`, optional + Whether the model is describing 2D fields on earths surface described + by latitude and longitude. + Default: False + temporal : :class:`bool`, optional + Whether a time-dimension is appended. + Default: False + + Returns + ------- + angles : :class:`float` + the angles fitting to the dimension + + Notes + ----- + If too few angles are given, they are filled up with `0`. + """ + if latlon: + return np.array(no_of_angles(dim) * [0], dtype=np.double) + out_angles = set_angles(dim, angles) + if temporal: + # no rotation between spatial dimensions and temporal dimension + out_angles[no_of_angles(dim - 1) :] = 0.0 + return out_angles + + def check_bounds(bounds): """ Check if given bounds are valid. @@ -522,7 +568,9 @@ def set_dim(model, dim): model.dim, model._len_scale, model._anis ) if model._angles is not None: - model._angles = set_angles(model.dim, model._angles) + model._angles = set_model_angles( + model.dim, model._angles, model.latlon, model.temporal + ) model.check_arg_bounds() From 66740a36a00a911f62a3c94f3dee03037ea0d0c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Tue, 13 Jun 2023 19:24:00 +0200 Subject: [PATCH 037/102] CovModel: saver setting of anis --- src/gstools/covmodel/base.py | 8 +++++--- src/gstools/covmodel/tools.py | 13 +++++-------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/gstools/covmodel/base.py b/src/gstools/covmodel/base.py index 7ed5960d..b6a533f7 100644 --- a/src/gstools/covmodel/base.py +++ b/src/gstools/covmodel/base.py @@ -197,7 +197,7 @@ def __init__( # set anisotropy and len_scale, disable anisotropy for latlon models self._len_scale, self._anis = set_len_anis( - self.dim, len_scale, anis, self.latlon, self.temporal + self.dim, len_scale, anis, self.latlon ) self._angles = set_model_angles( self.dim, angles, self.latlon, self.temporal @@ -974,7 +974,9 @@ def len_scale(self): @len_scale.setter def len_scale(self, len_scale): - self._len_scale, anis = set_len_anis(self.dim, len_scale, self.anis) + self._len_scale, anis = set_len_anis( + self.dim, len_scale, self.anis, self.latlon + ) if self.latlon: self._anis = np.array((self.dim - 1) * [1], dtype=np.double) else: @@ -1004,7 +1006,7 @@ def anis(self): @anis.setter def anis(self, anis): self._len_scale, self._anis = set_len_anis( - self.dim, self.len_scale, anis, self.latlon, self.temporal + self.dim, self.len_scale, anis, self.latlon ) self.check_arg_bounds() diff --git a/src/gstools/covmodel/tools.py b/src/gstools/covmodel/tools.py index 17c76531..a029512f 100644 --- a/src/gstools/covmodel/tools.py +++ b/src/gstools/covmodel/tools.py @@ -184,7 +184,7 @@ def set_opt_args(model, opt_arg): setattr(model, opt_name, float(opt_arg[opt_name])) -def set_len_anis(dim, len_scale, anis, latlon=False, temporal=False): +def set_len_anis(dim, len_scale, anis, latlon=False): """Set the length scale and anisotropy factors for the given dimension. Parameters @@ -197,10 +197,7 @@ def set_len_anis(dim, len_scale, anis, latlon=False, temporal=False): the anisotropy of length scales along the transversal axes latlon : :class:`bool`, optional Whether the model is describing 2D fields on earths surface described - by latitude and longitude. - Default: False - temporal : :class:`bool`, optional - Whether a time-dimension is appended. + by latitude and longitude. In this case there is no spatial anisotropy. Default: False Returns @@ -241,11 +238,11 @@ def set_len_anis(dim, len_scale, anis, latlon=False, temporal=False): for ani in out_anis: if not ani > 0.0: raise ValueError( - "anisotropy-ratios needs to be > 0, " + "got: " + str(out_anis) + f"anisotropy-ratios needs to be > 0, " + "got: {out_anis}" ) - # no anisotropy for latlon (only when temporal, but then only in time-dimension) + # no spatial anisotropy for latlon if latlon: - out_anis[: dim - (2 if temporal else 1)] = 1.0 + out_anis[:2] = 1.0 return out_len_scale, out_anis From c961fd2d542a3ef39af2806cb63c27be3d260e4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Tue, 13 Jun 2023 19:27:27 +0200 Subject: [PATCH 038/102] minor f-string fix --- src/gstools/covmodel/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gstools/covmodel/tools.py b/src/gstools/covmodel/tools.py index a029512f..8fcca7a9 100644 --- a/src/gstools/covmodel/tools.py +++ b/src/gstools/covmodel/tools.py @@ -238,7 +238,7 @@ def set_len_anis(dim, len_scale, anis, latlon=False): for ani in out_anis: if not ani > 0.0: raise ValueError( - f"anisotropy-ratios needs to be > 0, " + "got: {out_anis}" + f"anisotropy-ratios needs to be > 0, got: {out_anis}" ) # no spatial anisotropy for latlon if latlon: From 5d7790f24637d5700b330f86182d0ec698097bb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Wed, 14 Jun 2023 12:01:11 +0200 Subject: [PATCH 039/102] test temporal related stuff --- tests/test_latlon.py | 2 +- tests/test_temporal.py | 43 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 tests/test_temporal.py diff --git a/tests/test_latlon.py b/tests/test_latlon.py index 05cb9176..bd347281 100644 --- a/tests/test_latlon.py +++ b/tests/test_latlon.py @@ -144,7 +144,7 @@ def test_cond_srf(self): for i, dat in enumerate(self.data[:, 2]): self.assertAlmostEqual(field[i], dat, 3) - def error_test(self): + def test_error(self): # try fitting directional variogram mod = gs.Gaussian(latlon=True) with self.assertRaises(ValueError): diff --git a/tests/test_temporal.py b/tests/test_temporal.py new file mode 100644 index 00000000..800743e5 --- /dev/null +++ b/tests/test_temporal.py @@ -0,0 +1,43 @@ +""" +This is the unittest for temporal related routines. +""" + +import unittest + +import numpy as np + +import gstools as gs + + +class TestTemporal(unittest.TestCase): + def setUp(self): + ... + + def test_latlon(self): + mod = gs.Gaussian( + latlon=True, temporal=True, angles=[1, 2, 3, 4, 5, 6] + ) + self.assertEqual(mod.dim, 4) + self.assertEqual(mod.field_dim, 3) + self.assertEqual(mod.spatial_dim, 2) + self.assertTrue(np.allclose(mod.angles, 0)) + + mod1 = gs.Gaussian(latlon=True, temporal=True, len_scale=[10, 5]) + mod2 = gs.Gaussian(latlon=True, temporal=True, len_scale=10, anis=0.5) + + self.assertTrue(np.allclose(mod1.anis, mod2.anis)) + self.assertAlmostEqual(mod1.len_scale, mod2.len_scale) + + def test_rotation(self): + mod = gs.Gaussian(dim=3, temporal=True, angles=[1, 2, 3, 4, 5, 6]) + self.assertTrue(np.allclose(mod.angles, [1, 2, 3, 0, 0, 0])) + + def test_krige(self): + mod = gs.Gaussian(latlon=True, temporal=True) + # auto-fitting latlon-temporal model in kriging not possible + with self.assertRaises(ValueError): + kri = gs.Krige(mod, 3 * [[1, 2, 3]], [1, 2, 3], fit_variogram=True) + + +if __name__ == "__main__": + unittest.main() From 250bee02b04efa3039cb540a130c90a0b4559134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Wed, 14 Jun 2023 13:46:32 +0200 Subject: [PATCH 040/102] more temporal tests --- tests/test_latlon.py | 16 ++++++++++------ tests/test_temporal.py | 38 +++++++++++++++++++++++++++++++++++--- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/tests/test_latlon.py b/tests/test_latlon.py index bd347281..98088db8 100644 --- a/tests/test_latlon.py +++ b/tests/test_latlon.py @@ -24,7 +24,7 @@ def fix_dim(self): class TestLatLon(unittest.TestCase): def setUp(self): self.cmod = gs.Gaussian( - latlon=True, var=2, len_scale=777, rescale=gs.KM_SCALE + latlon=True, var=2, len_scale=777, geo_scale=gs.KM_SCALE ) self.lat = self.lon = range(-80, 81) @@ -66,7 +66,10 @@ def test_conv(self): 6, self.cmod.anisometrize(self.cmod.isometrize((8, 6)))[1, 0] ) self.assertAlmostEqual( - 1, self.cmod.isometrize(self.cmod.anisometrize((1, 0, 0)))[0, 0] + gs.EARTH_RADIUS, + self.cmod.isometrize( + self.cmod.anisometrize((gs.EARTH_RADIUS, 0, 0)) + )[0, 0], ) def test_cov_model(self): @@ -91,15 +94,16 @@ def test_vario_est(self): srf = gs.SRF(self.cmod, seed=12345) field = srf.structured((self.lat, self.lon)) - bin_edges = [0.01 * i for i in range(30)] + bin_edges = np.linspace(0, 3 * 777, 30) bin_center, emp_vario = gs.vario_estimate( *((self.lat, self.lon), field, bin_edges), latlon=True, mesh_type="structured", sampling_size=2000, sampling_seed=12345, + geo_scale=gs.KM_SCALE, ) - mod = gs.Gaussian(latlon=True, rescale=gs.KM_SCALE) + mod = gs.Gaussian(latlon=True, geo_scale=gs.KM_SCALE) mod.fit_variogram(bin_center, emp_vario, nugget=False) # allow 10 percent relative error self.assertLess(_rel_err(mod.var, self.cmod.var), 0.1) @@ -114,7 +118,7 @@ def test_krige(self): bin_edges, latlon=True, ) - mod = gs.Spherical(latlon=True, rescale=gs.KM_SCALE) + mod = gs.Spherical(latlon=True, geo_scale=gs.KM_SCALE) mod.fit_variogram(*emp_vario, nugget=False) kri = gs.krige.Ordinary( mod, @@ -134,7 +138,7 @@ def test_cond_srf(self): bin_edges, latlon=True, ) - mod = gs.Spherical(latlon=True, rescale=gs.KM_SCALE) + mod = gs.Spherical(latlon=True, geo_scale=gs.KM_SCALE) mod.fit_variogram(*emp_vario, nugget=False) krige = gs.krige.Ordinary( mod, (self.data[:, 0], self.data[:, 1]), self.data[:, 2] diff --git a/tests/test_temporal.py b/tests/test_temporal.py index 800743e5..98893693 100644 --- a/tests/test_temporal.py +++ b/tests/test_temporal.py @@ -11,7 +11,13 @@ class TestTemporal(unittest.TestCase): def setUp(self): - ... + self.mod = gs.Gaussian( + latlon=True, + temporal=True, + len_scale=1000, + anis=0.5, + geo_scale=gs.KM_SCALE, + ) def test_latlon(self): mod = gs.Gaussian( @@ -28,15 +34,41 @@ def test_latlon(self): self.assertTrue(np.allclose(mod1.anis, mod2.anis)) self.assertAlmostEqual(mod1.len_scale, mod2.len_scale) + def test_latlon2pos(self): + self.assertAlmostEqual( + 8, self.mod.anisometrize(self.mod.isometrize((8, 6, 9)))[0, 0] + ) + self.assertAlmostEqual( + 6, self.mod.anisometrize(self.mod.isometrize((8, 6, 9)))[1, 0] + ) + self.assertAlmostEqual( + 9, self.mod.anisometrize(self.mod.isometrize((8, 6, 9)))[2, 0] + ) + self.assertAlmostEqual( + gs.EARTH_RADIUS, + self.mod.isometrize( + self.mod.anisometrize((gs.EARTH_RADIUS, 0, 0, 10)) + )[0, 0], + ) + self.assertAlmostEqual( + 10, + self.mod.isometrize( + self.mod.anisometrize((gs.EARTH_RADIUS, 0, 0, 10)) + )[3, 0], + ) + def test_rotation(self): mod = gs.Gaussian(dim=3, temporal=True, angles=[1, 2, 3, 4, 5, 6]) self.assertTrue(np.allclose(mod.angles, [1, 2, 3, 0, 0, 0])) def test_krige(self): - mod = gs.Gaussian(latlon=True, temporal=True) # auto-fitting latlon-temporal model in kriging not possible with self.assertRaises(ValueError): - kri = gs.Krige(mod, 3 * [[1, 2, 3]], [1, 2, 3], fit_variogram=True) + kri = gs.Krige(self.mod, 3 * [[1, 2]], [1, 2], fit_variogram=True) + + def test_field(self): + srf = gs.SRF(self.mod) + self.assertTrue(srf.temporal) if __name__ == "__main__": From aab696742c9302a7b864d44c1791fbf17b05be9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Wed, 14 Jun 2023 16:36:14 +0200 Subject: [PATCH 041/102] CovModel: add 'spatial_dim' argument --- examples/09_spatio_temporal/01_precip_1d.py | 4 +++- examples/09_spatio_temporal/02_precip_2d.py | 4 +++- examples/09_spatio_temporal/README.rst | 17 +++++++++++------ src/gstools/covmodel/base.py | 17 ++++++++++++++--- tests/test_temporal.py | 5 ++++- 5 files changed, 35 insertions(+), 12 deletions(-) diff --git a/examples/09_spatio_temporal/01_precip_1d.py b/examples/09_spatio_temporal/01_precip_1d.py index 2da77783..4b4c6b8a 100644 --- a/examples/09_spatio_temporal/01_precip_1d.py +++ b/examples/09_spatio_temporal/01_precip_1d.py @@ -30,7 +30,9 @@ st_anis = 0.4 # an exponential variogram with a corr. lengths of 2d and 5km -model = gs.Exponential(dim=1, temporal=True, var=1, len_scale=5, anis=st_anis) +model = gs.Exponential( + temporal=True, spatial_dim=1, var=1, len_scale=5, anis=st_anis +) # create a spatial random field instance srf = gs.SRF(model, seed=seed) diff --git a/examples/09_spatio_temporal/02_precip_2d.py b/examples/09_spatio_temporal/02_precip_2d.py index 3406803b..81c78964 100644 --- a/examples/09_spatio_temporal/02_precip_2d.py +++ b/examples/09_spatio_temporal/02_precip_2d.py @@ -31,7 +31,9 @@ st_anis = 0.4 # an exponential variogram with a corr. lengths of 5km, 5km, and 2d -model = gs.Exponential(dim=2, temporal=True, var=1, len_scale=5, anis=st_anis) +model = gs.Exponential( + temporal=True, spatial_dim=2, var=1, len_scale=5, anis=st_anis +) # create a spatial random field instance srf = gs.SRF(model, seed=seed) diff --git a/examples/09_spatio_temporal/README.rst b/examples/09_spatio_temporal/README.rst index 08cc2fcd..3cb06b9e 100644 --- a/examples/09_spatio_temporal/README.rst +++ b/examples/09_spatio_temporal/README.rst @@ -6,23 +6,28 @@ like rainfall, air temperature or crop yield. GSTools provides the metric spatio-temporal model for all covariance models by setting ``temporal=True``, which enhances the spatial model dimension with -a time dimension to result in the spatio-temporal dimension and setting a -spatio-temporal anisotropy ratio like this: +a time dimension to result in the spatio-temporal dimension. +Since the model dimension is then higher than the spatial dimension, you can use +the ``spatial_dim`` argument to explicitly set the spatial dimension. +Doing that and setting a spatio-temporal anisotropy ratio looks like this: .. code-block:: python import gstools as gs dim = 3 # spatial dimension st_anis = 0.4 - st_model = gs.Exponential(dim=dim, temporal=True, anis=st_anis) + st_model = gs.Exponential(temporal=True, spatial_dim=dim, anis=st_anis) Since it is given in the name "spatio-temporal", time is always treated as last dimension. -There are three different dimension attributes giving information about (i) the +You could also use ``dim`` to specify the dimension but note that it needs to include +the temporal dimension. + +There are now three different dimension attributes giving information about (i) the model dimension (``dim``), (ii) the field dimension (``field_dim``, including time) and (iii) the spatial dimension (``spatial_dim`` always 1 less than ``field_dim`` for temporal models). Model and field dimension can differ in case of geographic coordinates where the model dimension is 3, but the field or parametric dimension is 2. -If the model is spatio-temporal one with geographic coordinates, the model dimension is 4, +If the model is spatio-temporal with geographic coordinates, the model dimension is 4, the field dimension is 3 and the spatial dimension is 2. In the case above we get: @@ -40,7 +45,7 @@ non-temporal models, without altering the behavior in the time dimension: anis = [0.4, 0.2] # spatial anisotropy in 3D angles = [0.5, 0.4, 0.3] # spatial rotation in 3D - st_model = gs.Exponential(dim=dim, temporal=True, anis=anis+[st_anis], angles=angles) + st_model = gs.Exponential(temporal=True, spatial_dim=dim, anis=anis+[st_anis], angles=angles) In order to generate spatio-temporal position tuples, GSTools provides a convenient function :any:`generate_st_grid`. The output can be used for diff --git a/src/gstools/covmodel/base.py b/src/gstools/covmodel/base.py index b6a533f7..077edaa1 100644 --- a/src/gstools/covmodel/base.py +++ b/src/gstools/covmodel/base.py @@ -54,7 +54,10 @@ class CovModel: Parameters ---------- dim : :class:`int`, optional - dimension of the model. Default: ``3`` + dimension of the model. + Includes the temporal dimension if temporal is true. + To specify only the spatial dimension in that case, use `spatial_dim`. + Default: ``3`` var : :class:`float`, optional variance of the model (the nugget is not included in "this" variance) Default: ``1.0`` @@ -118,6 +121,11 @@ class CovModel: `spatial_dim` will be `field_dim - 1`. The time-dimension is appended, meaning the pos tuple is (x,y,z,...,t). Default: False + spatial_dim : :class:`int`, optional + spatial dimension of the model. + If given, the model dimension will be determined from this spatial dimension + and the possible temporal dimension if temporal is ture. + Default: None var_raw : :class:`float` or :any:`None`, optional raw variance of the model which will be multiplied with :any:`CovModel.var_factor` to result in the actual variance. @@ -149,6 +157,7 @@ def __init__( latlon=False, geo_scale=RADIAN_SCALE, temporal=False, + spatial_dim=None, var_raw=None, hankel_kw=None, **opt_arg, @@ -180,8 +189,10 @@ def __init__( self._geo_scale = abs(float(geo_scale)) # SFT class will be created within dim.setter but needs hankel_kw self.hankel_kw = hankel_kw - # using time increases model dimension - self.dim = dim + int(self.temporal) + # using time increases model dimension given by "spatial_dim" + self.dim = ( + dim if spatial_dim is None else spatial_dim + int(self.temporal) + ) # optional arguments for the variogram-model set_opt_args(self, opt_arg) diff --git a/tests/test_temporal.py b/tests/test_temporal.py index 98893693..c179db1c 100644 --- a/tests/test_temporal.py +++ b/tests/test_temporal.py @@ -58,8 +58,11 @@ def test_latlon2pos(self): ) def test_rotation(self): - mod = gs.Gaussian(dim=3, temporal=True, angles=[1, 2, 3, 4, 5, 6]) + mod = gs.Gaussian( + spatial_dim=3, temporal=True, angles=[1, 2, 3, 4, 5, 6] + ) self.assertTrue(np.allclose(mod.angles, [1, 2, 3, 0, 0, 0])) + self.assertEqual(mod.dim, 4) def test_krige(self): # auto-fitting latlon-temporal model in kriging not possible From 0a3ce91a08d2a6d6165adb4ad15374cca0bcc412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Thu, 15 Jun 2023 10:40:51 +0200 Subject: [PATCH 042/102] minor f-string fixes --- src/gstools/covmodel/tools.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/gstools/covmodel/tools.py b/src/gstools/covmodel/tools.py index 8fcca7a9..dddeb441 100644 --- a/src/gstools/covmodel/tools.py +++ b/src/gstools/covmodel/tools.py @@ -421,9 +421,7 @@ def percentile_scale(model, per=0.9): """ # check the given percentile if not 0.0 < per < 1.0: - raise ValueError( - "percentile needs to be within (0, 1), got: " + str(per) - ) + raise ValueError(f"percentile needs to be within (0, 1), got: {per}") # define a curve, that has its root at the wanted point def curve(x): @@ -537,7 +535,7 @@ def set_dim(model, dim): # check if a fixed dimension should be used if model.fix_dim() is not None and model.fix_dim() != dim: warnings.warn( - model.name + ": using fixed dimension " + str(model.fix_dim()), + f"{model.name}: using fixed dimension {model.fix_dim()}", AttributeWarning, ) dim = model.fix_dim() From 63d49be99372b0624dd9a0d3c3113b8b183926b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Thu, 15 Jun 2023 10:41:07 +0200 Subject: [PATCH 043/102] update changelog --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61aa289f..7b956b3a 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,34 @@ All notable changes to **GSTools** will be documented in this file. +## [Unreleased] - ? - 2023-? + +### Enhancements +- added `temporal` flag to CovModel to explicitly specify spatio-temporal models [#308](https://github.com/GeoStat-Framework/GSTools/pull/308) + - rotation between spatial and temporal dimension will be ignored + - added `spatial_dim` to CovModel to explicitly set spatial dimension for spatio-temporal models + - if not using `spatial_dim`, the provided `dim` needs to include the possible temporal dimension + - `spatial_dim` is always one less than `field_dim` for spatio-temporal models + - also works with `latlon=True` to have a spatio-temporal model with geographic coordinates + - all plotting routines respect this + - the `Field` class now has a `temporal` attribute which forwards the model attribute + - automatic variogram fitting in kriging classes for `temporal=True` and `latlon=True` will raise an error +- added `geo_scale` to CovModel to have a more consistent way to set the units of the model length scale for geographic coordinates [#308](https://github.com/GeoStat-Framework/GSTools/pull/308) + - no need to use `rescale` for this anymore (was rather a hack) + - added `gs.KM_SCALE` which is the same as `gs.EARTH_RADIUS` for kilometer scaling + - added `gs.DEGREE_SCALE` for great circle distance in degrees + - added `gs.RADIAN_SCALE` for great circle distance in radians (default and previous behavior) + - yadrenko variogram respects this and assumes the great circle distances is given in the respective unit + - `vario_estimate` also has `geo_scale` now to control the units of the bins +- `vario_estimate` now forwards additional kwargs to `standard_bins` (`bin_no`, `max_dist`) [#308](https://github.com/GeoStat-Framework/GSTools/pull/308) + +### Changes +- CovModels expect all arguments by keyword now (except `dim`) [#308](https://github.com/GeoStat-Framework/GSTools/pull/308) +- always use f-strings internally [#283](https://github.com/GeoStat-Framework/GSTools/pull/283) + +### Bugfixes +- latex equations were not rendered correctly in docs [#290](https://github.com/GeoStat-Framework/GSTools/pull/290) + ## [1.4.1] - Sassy Sapphire - 2022-11 From edbbe48ddb61f31439ac354e5e5447cba5202256 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Thu, 15 Jun 2023 10:46:12 +0200 Subject: [PATCH 044/102] CovModel: be less strict about key-word-only args (don't want to bother people) --- CHANGELOG.md | 2 +- src/gstools/covmodel/base.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b956b3a..070e0a71 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ All notable changes to **GSTools** will be documented in this file. - `vario_estimate` now forwards additional kwargs to `standard_bins` (`bin_no`, `max_dist`) [#308](https://github.com/GeoStat-Framework/GSTools/pull/308) ### Changes -- CovModels expect all arguments by keyword now (except `dim`) [#308](https://github.com/GeoStat-Framework/GSTools/pull/308) +- CovModels expect special arguments by keyword now [#308](https://github.com/GeoStat-Framework/GSTools/pull/308) - always use f-strings internally [#283](https://github.com/GeoStat-Framework/GSTools/pull/283) ### Bugfixes diff --git a/src/gstools/covmodel/base.py b/src/gstools/covmodel/base.py index 077edaa1..be530075 100644 --- a/src/gstools/covmodel/base.py +++ b/src/gstools/covmodel/base.py @@ -146,12 +146,12 @@ class CovModel: def __init__( self, dim=3, - *, var=1.0, len_scale=1.0, nugget=0.0, anis=1.0, angles=0.0, + *, integral_scale=None, rescale=None, latlon=False, From ba95019296fec2bd5cba060647212bc682984b24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Thu, 15 Jun 2023 17:29:45 +0200 Subject: [PATCH 045/102] variogram: rename bin_center to bin_centers --- src/gstools/variogram/variogram.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/gstools/variogram/variogram.py b/src/gstools/variogram/variogram.py index afb03e70..b1edddf4 100644 --- a/src/gstools/variogram/variogram.py +++ b/src/gstools/variogram/variogram.py @@ -238,7 +238,7 @@ def vario_estimate( Returns ------- - bin_center : (n), :class:`numpy.ndarray` + bin_centers : (n), :class:`numpy.ndarray` The bin centers. gamma : (n) or (d, n), :class:`numpy.ndarray` The estimated variogram values at bin centers. @@ -261,20 +261,17 @@ def vario_estimate( "Geostatistics for environmental scientists.", John Wiley & Sons. (2007) """ - if bin_edges is not None: - bin_edges = np.array(bin_edges, ndmin=1, dtype=np.double, copy=False) - bin_center = (bin_edges[:-1] + bin_edges[1:]) / 2.0 # allow multiple fields at same positions (ndmin=2: first axis -> field ID) # need to convert to ma.array, since list of ma.array is not recognised field = np.ma.array(field, ndmin=2, dtype=np.double, copy=True) masked = np.ma.is_masked(field) or np.any(mask) # catch special case if everything is masked if masked and np.all(mask): - bin_center = np.empty(0) if bin_edges is None else bin_center - estimates = np.zeros_like(bin_center) + bin_centers = np.empty(0) if bin_edges is None else bin_centers + estimates = np.zeros_like(bin_centers) if return_counts: - return bin_center, estimates, np.zeros_like(estimates, dtype=int) - return bin_center, estimates + return bin_centers, estimates, np.zeros_like(estimates, dtype=int) + return bin_centers, estimates if not masked: field = field.filled() # check mesh shape @@ -344,12 +341,15 @@ def vario_estimate( ) field = field[:, sampled_idx] pos = pos[:, sampled_idx] - # create bining if not given + # create bins if bin_edges is None: bin_edges = standard_bins( pos, dim, latlon, geo_scale=geo_scale, **std_bins ) - bin_center = (bin_edges[:-1] + bin_edges[1:]) / 2.0 + else: + bin_edges = np.array(bin_edges, ndmin=1, dtype=np.double, copy=False) + bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2.0 + if latlon: # internally we always use radians bin_edges /= geo_scale @@ -389,7 +389,7 @@ def vario_estimate( if dir_no == 1: estimates, counts = estimates[0], counts[0] est_out = (estimates, counts) - return (bin_center,) + est_out[: 2 if return_counts else 1] + norm_out + return (bin_centers,) + est_out[: 2 if return_counts else 1] + norm_out def vario_estimate_axis( From 87a516e6ee880e7601496008e506ea09f711f9c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Thu, 15 Jun 2023 17:30:44 +0200 Subject: [PATCH 046/102] changelog: minor fix --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 070e0a71..477fc00f 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ All notable changes to **GSTools** will be documented in this file. - `vario_estimate` now forwards additional kwargs to `standard_bins` (`bin_no`, `max_dist`) [#308](https://github.com/GeoStat-Framework/GSTools/pull/308) ### Changes -- CovModels expect special arguments by keyword now [#308](https://github.com/GeoStat-Framework/GSTools/pull/308) +- `CovModel`s expect special arguments by keyword now [#308](https://github.com/GeoStat-Framework/GSTools/pull/308) - always use f-strings internally [#283](https://github.com/GeoStat-Framework/GSTools/pull/283) ### Bugfixes From 639ecdb550186c439d1b47f95d55359af212cd0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Thu, 15 Jun 2023 17:38:41 +0200 Subject: [PATCH 047/102] vario: revert moving code-block --- src/gstools/variogram/variogram.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/gstools/variogram/variogram.py b/src/gstools/variogram/variogram.py index b1edddf4..69746658 100644 --- a/src/gstools/variogram/variogram.py +++ b/src/gstools/variogram/variogram.py @@ -261,6 +261,9 @@ def vario_estimate( "Geostatistics for environmental scientists.", John Wiley & Sons. (2007) """ + if bin_edges is not None: + bin_edges = np.array(bin_edges, ndmin=1, dtype=np.double, copy=False) + bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2.0 # allow multiple fields at same positions (ndmin=2: first axis -> field ID) # need to convert to ma.array, since list of ma.array is not recognised field = np.ma.array(field, ndmin=2, dtype=np.double, copy=True) @@ -346,10 +349,7 @@ def vario_estimate( bin_edges = standard_bins( pos, dim, latlon, geo_scale=geo_scale, **std_bins ) - else: - bin_edges = np.array(bin_edges, ndmin=1, dtype=np.double, copy=False) - bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2.0 - + bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2.0 if latlon: # internally we always use radians bin_edges /= geo_scale From 64d9a14c08007df9c77f56b33a50e01d707d7499 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Thu, 15 Jun 2023 17:41:25 +0200 Subject: [PATCH 048/102] changelog: minor markdown fixes --- CHANGELOG.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 477fc00f..733d75e3 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,16 +5,16 @@ All notable changes to **GSTools** will be documented in this file. ## [Unreleased] - ? - 2023-? ### Enhancements -- added `temporal` flag to CovModel to explicitly specify spatio-temporal models [#308](https://github.com/GeoStat-Framework/GSTools/pull/308) +- added `temporal` flag to `CovModel` to explicitly specify spatio-temporal models [#308](https://github.com/GeoStat-Framework/GSTools/pull/308) - rotation between spatial and temporal dimension will be ignored - - added `spatial_dim` to CovModel to explicitly set spatial dimension for spatio-temporal models + - added `spatial_dim` to `CovModel` to explicitly set spatial dimension for spatio-temporal models - if not using `spatial_dim`, the provided `dim` needs to include the possible temporal dimension - `spatial_dim` is always one less than `field_dim` for spatio-temporal models - also works with `latlon=True` to have a spatio-temporal model with geographic coordinates - all plotting routines respect this - the `Field` class now has a `temporal` attribute which forwards the model attribute - automatic variogram fitting in kriging classes for `temporal=True` and `latlon=True` will raise an error -- added `geo_scale` to CovModel to have a more consistent way to set the units of the model length scale for geographic coordinates [#308](https://github.com/GeoStat-Framework/GSTools/pull/308) +- added `geo_scale` to `CovModel` to have a more consistent way to set the units of the model length scale for geographic coordinates [#308](https://github.com/GeoStat-Framework/GSTools/pull/308) - no need to use `rescale` for this anymore (was rather a hack) - added `gs.KM_SCALE` which is the same as `gs.EARTH_RADIUS` for kilometer scaling - added `gs.DEGREE_SCALE` for great circle distance in degrees @@ -53,7 +53,7 @@ All notable changes to **GSTools** will be documented in this file. - better support for custom generators [#250](https://github.com/GeoStat-Framework/GSTools/pull/250) [#259](https://github.com/GeoStat-Framework/GSTools/pull/259) - add `valid_value_types` class variable to all field classes [#250](https://github.com/GeoStat-Framework/GSTools/pull/250) - PyKrige: fix passed variogram in case of latlon models [#254](https://github.com/GeoStat-Framework/GSTools/pull/254) -- add bounds checks for optional arguments of CovModel when resetting by class attribute [#255](https://github.com/GeoStat-Framework/GSTools/pull/255) +- add bounds checks for optional arguments of `CovModel` when resetting by class attribute [#255](https://github.com/GeoStat-Framework/GSTools/pull/255) - minor coverage improvements [#255](https://github.com/GeoStat-Framework/GSTools/pull/255) - documentation: readability improvements [#257](https://github.com/GeoStat-Framework/GSTools/pull/257) @@ -209,7 +209,7 @@ See: [#197](https://github.com/GeoStat-Framework/GSTools/issues/197) - added new `len_rescaled` attribute to the `CovModel` class, which is the rescaled `len_scale`: `len_rescaled = len_scale / rescale` - new method `default_rescale` to provide default rescale factor (can be overridden) - remove `doctest` calls -- docstring updates in CovModel and derived models +- docstring updates in `CovModel` and derived models - updated all models to use the `cor` routine and make use of the `rescale` argument (See: [#90](https://github.com/GeoStat-Framework/GSTools/issues/90)) - TPL models got a separate base class to not repeat code - added **new models** (See: [#88](https://github.com/GeoStat-Framework/GSTools/issues/88)): @@ -236,7 +236,7 @@ See: [#197](https://github.com/GeoStat-Framework/GSTools/issues/197) #### Arbitrary dimensions ([#112](https://github.com/GeoStat-Framework/GSTools/issues/112)) - allow arbitrary dimensions in all routines (CovModel, Krige, SRF, variogram) - anisotropy and rotation following a generalization of tait-bryan angles -- CovModel provides `isometrize` and `anisometrize` routines to convert points +- `CovModel` provides `isometrize` and `anisometrize` routines to convert points #### New Class for Conditioned Random Fields ([#130](https://github.com/GeoStat-Framework/GSTools/issues/130)) - **THIS BREAKS BACKWARD COMPATIBILITY** @@ -260,7 +260,7 @@ See: [#197](https://github.com/GeoStat-Framework/GSTools/issues/197) ### Changes - drop support for Python 3.5 [#146](https://github.com/GeoStat-Framework/GSTools/pull/146) -- added a finit limit for shape-parameters in some CovModels [#147](https://github.com/GeoStat-Framework/GSTools/pull/147) +- added a finit limit for shape-parameters in some `CovModel`s [#147](https://github.com/GeoStat-Framework/GSTools/pull/147) - drop usage of `pos2xyz` and `xyz2pos` - remove structured option from generators (structured pos need to be converted first) - explicitly assert dim=2,3 when generating vector fields @@ -276,7 +276,7 @@ See: [#197](https://github.com/GeoStat-Framework/GSTools/issues/197) - typo in keyword argument for vario_estimate_structured [#80](https://github.com/GeoStat-Framework/GSTools/issues/80) - isotropic rotation of SRF was not possible [#100](https://github.com/GeoStat-Framework/GSTools/issues/100) - `CovModel.opt_arg` now sorted [#103](https://github.com/GeoStat-Framework/GSTools/issues/103) -- CovModel.fit: check if weights are given as a string (numpy comparison error) [#111](https://github.com/GeoStat-Framework/GSTools/issues/111) +- `CovModel.fit`: check if weights are given as a string (numpy comparison error) [#111](https://github.com/GeoStat-Framework/GSTools/issues/111) - several pylint fixes ([#159](https://github.com/GeoStat-Framework/GSTools/pull/159)) @@ -294,7 +294,7 @@ See: [#197](https://github.com/GeoStat-Framework/GSTools/issues/197) ### Enhancements - different variogram estimator functions can now be used #51 - the TPLGaussian and TPLExponential now have analytical spectra #67 -- added property ``is_isotropic`` to CovModel #67 +- added property `is_isotropic` to `CovModel` #67 - reworked the whole krige sub-module to provide multiple kriging methods #67 - Simple - Ordinary @@ -307,7 +307,7 @@ See: [#197](https://github.com/GeoStat-Framework/GSTools/issues/197) ### Changes - Python versions 2.7 and 3.4 are no longer supported #40 #43 -- CovModel: in 3D the input of anisotropy is now treated slightly different: #67 +- `CovModel`: in 3D the input of anisotropy is now treated slightly different: #67 - single given anisotropy value [e] is converted to [1, e] (it was [e, e] before) - two given length-scales [l_1, l_2] are converted to [l_1, l_2, l_2] (it was [l_1, l_2, l_1] before) @@ -325,7 +325,7 @@ See: [#197](https://github.com/GeoStat-Framework/GSTools/issues/197) ### Bugfixes - define spectral_density instead of spectrum in covariance models since Cov-base derives spectrum. See: [commit 00f2747](https://github.com/GeoStat-Framework/GSTools/commit/00f2747fd0503ff8806f2eebfba36acff813416b) -- better boundaries for CovModel parameters. See: https://github.com/GeoStat-Framework/GSTools/issues/37 +- better boundaries for `CovModel` parameters. See: https://github.com/GeoStat-Framework/GSTools/issues/37 ## [1.1.0] - Reverberating Red - 2019-10-01 @@ -333,23 +333,23 @@ See: [#197](https://github.com/GeoStat-Framework/GSTools/issues/197) ### Enhancements - by using Cython for all the heavy computations, we could achieve quite some speed ups and reduce the memory consumption significantly #16 - parallel computation in Cython is now supported with the help of OpenMP and the performance increase is nearly linear with increasing cores #16 -- new submodule ``krige`` providing simple (known mean) and ordinary (estimated mean) kriging working analogous to the srf class -- interface to pykrige to use the gstools CovModel with the pykrige routines (https://github.com/bsmurphy/PyKrige/issues/124) -- the srf class now provides a ``plot`` and a ``vtk_export`` routine +- new submodule `krige` providing simple (known mean) and ordinary (estimated mean) kriging working analogous to the srf class +- interface to pykrige to use the gstools `CovModel` with the pykrige routines (https://github.com/bsmurphy/PyKrige/issues/124) +- the srf class now provides a `plot` and a `vtk_export` routine - incompressible flow fields can now be generated #14 - new submodule providing several field transformations like: Zinn&Harvey, log-normal, bimodal, ... #13 - Python 3.4 and 3.7 wheel support #19 - field can now be generated directly on meshes from [meshio](https://github.com/nschloe/meshio) and [ogs5py](https://github.com/GeoStat-Framework/ogs5py), see: [commit f4a3439](https://github.com/GeoStat-Framework/GSTools/commit/f4a3439400b81d8d9db81a5f7fbf6435f603cf05) -- the srf and kriging classes now store the last ``pos``, ``mesh_type`` and ``field`` values to keep them accessible, see: [commit 29f7f1b](https://github.com/GeoStat-Framework/GSTools/commit/29f7f1b029866379ce881f44765f72534d757fae) +- the srf and kriging classes now store the last `pos`, `mesh_type` and `field` values to keep them accessible, see: [commit 29f7f1b](https://github.com/GeoStat-Framework/GSTools/commit/29f7f1b029866379ce881f44765f72534d757fae) - tutorials on all important features of GSTools have been written for you guys #20 - a new interface to pyvista is provided to export fields to python vtk representation, which can be used for plotting, exploring and exporting fields #29 ### Changes - the license was changed from GPL to LGPL in order to promote the use of this library #25 - the rotation angles are now interpreted in positive direction (counter clock wise) -- the ``force_moments`` keyword was removed from the SRF call method, it is now in provided as a field transformation #13 +- the `force_moments` keyword was removed from the SRF call method, it is now in provided as a field transformation #13 - drop support of python implementations of the variogram estimators #18 -- the ``variogram_normed`` method was removed from the ``CovModel`` class due to redundance [commit 25b1647](https://github.com/GeoStat-Framework/GSTools/commit/25b164722ac6744ebc7e03f3c0bf1c30be1eba89) +- the `variogram_normed` method was removed from the `CovModel` class due to redundance [commit 25b1647](https://github.com/GeoStat-Framework/GSTools/commit/25b164722ac6744ebc7e03f3c0bf1c30be1eba89) - the position vector of 1D fields does not have to be provided in a list-like object with length 1 [commit a6f5be8](https://github.com/GeoStat-Framework/GSTools/commit/a6f5be8bfd2db1f002e7889ecb8e9a037ea08886) ### Bugfixes @@ -381,7 +381,7 @@ See: [#197](https://github.com/GeoStat-Framework/GSTools/issues/197) ### Changes - release is not downwards compatible with release v0.4.0 -- SRF creation has been adapted for the CovModel +- SRF creation has been adapted for the `CovModel` - a tuple `pos` is now used instead of `x`, `y`, and `z` for the axes - renamed `estimate_unstructured` and `estimate_structured` to `vario_estimate_unstructured` and `vario_estimate_structured` for less ambiguity From e654a6e63c8b1cd5a526a6152b50d6c9a26e1d47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Tue, 6 Jun 2023 16:45:57 +0200 Subject: [PATCH 049/102] Generator: remove 'verbose' argument; make arguments key-word only --- src/gstools/field/generator.py | 33 ++++++++++----------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/src/gstools/field/generator.py b/src/gstools/field/generator.py index a342c4a0..1caf8d43 100644 --- a/src/gstools/field/generator.py +++ b/src/gstools/field/generator.py @@ -130,9 +130,6 @@ class RandMeth(Generator): seed : :class:`int` or :any:`None`, optional The seed of the random number generator. If "None", a random seed is used. Default: :any:`None` - verbose : :class:`bool`, optional - Be chatty during the generation. - Default: :any:`False` sampling : :class:`str`, optional Sampling strategy. Either @@ -175,9 +172,9 @@ class RandMeth(Generator): def __init__( self, model, + *, mode_no=1000, seed=None, - verbose=False, sampling="auto", **kwargs, ): @@ -185,7 +182,6 @@ def __init__( warnings.warn("gstools.RandMeth: **kwargs are ignored") # initialize attributes self._mode_no = int(mode_no) - self._verbose = bool(verbose) # initialize private attributes self._model = None self._seed = None @@ -280,15 +276,12 @@ def update(self, model=None, seed=np.nan): ) # if the user tries to trick us, we beat him! elif model is None and np.isnan(seed): - if ( + if not ( isinstance(self._model, CovModel) and self._z_1 is not None and self._z_2 is not None and self._cov_sample is not None ): - if self.verbose: - print("RandMeth.update: Nothing will be done...") - else: raise ValueError( "gstools.field.generator.RandMeth: " "neither 'model' nor 'seed' given!" @@ -387,15 +380,6 @@ def mode_no(self, mode_no): self._mode_no = int(mode_no) self.reset_seed(self._seed) - @property - def verbose(self): - """:class:`bool`: Verbosity of the generator.""" - return self._verbose - - @verbose.setter - def verbose(self, verbose): - self._verbose = bool(verbose) - @property def value_type(self): """:class:`str`: Type of the field values (scalar, vector).""" @@ -423,9 +407,6 @@ class IncomprRandMeth(RandMeth): seed : :class:`int` or :any:`None`, optional the seed of the random number generator. If "None", a random seed is used. Default: :any:`None` - verbose : :class:`bool`, optional - State if there should be output during the generation. - Default: :any:`False` sampling : :class:`str`, optional Sampling strategy. Either @@ -470,10 +451,10 @@ class IncomprRandMeth(RandMeth): def __init__( self, model, + *, mean_velocity=1.0, mode_no=1000, seed=None, - verbose=False, sampling="auto", **kwargs, ): @@ -481,7 +462,13 @@ def __init__( raise ValueError( "Only 2D and 3D incompressible fields can be generated." ) - super().__init__(model, mode_no, seed, verbose, sampling, **kwargs) + super().__init__( + model=model, + mode_no=mode_no, + seed=seed, + sampling=sampling, + **kwargs, + ) self.mean_u = mean_velocity self._value_type = "vector" From 6cfb74b693ac87dde100205e00c27ca22a5c260b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Tue, 6 Jun 2023 17:05:54 +0200 Subject: [PATCH 050/102] Tests: use kwargs from now on --- tests/test_randmeth.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_randmeth.py b/tests/test_randmeth.py index 1ed6cc1f..7cbb962f 100644 --- a/tests/test_randmeth.py +++ b/tests/test_randmeth.py @@ -26,9 +26,9 @@ def setUp(self): self.y_tuple = np.linspace(-5.0, 5.0, 10) self.z_tuple = np.linspace(-6.0, 8.0, 10) - self.rm_1d = RandMeth(self.cov_model_1d, 100, self.seed) - self.rm_2d = RandMeth(self.cov_model_2d, 100, self.seed) - self.rm_3d = RandMeth(self.cov_model_3d, 100, self.seed) + self.rm_1d = RandMeth(self.cov_model_1d, mode_no=100, seed=self.seed) + self.rm_2d = RandMeth(self.cov_model_2d, mode_no=100, seed=self.seed) + self.rm_3d = RandMeth(self.cov_model_3d, mode_no=100, seed=self.seed) def test_unstruct_1d(self): modes = self.rm_1d((self.x_tuple,)) From ae4ec7a66637491e93b4d6298da15e33a0c69127 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Tue, 6 Jun 2023 17:01:43 +0200 Subject: [PATCH 051/102] transform: add low/high to uniform trans --- src/gstools/transform/array.py | 14 +++++++++++--- src/gstools/transform/field.py | 16 ++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/gstools/transform/array.py b/src/gstools/transform/array.py index 32005f00..30c92bb6 100644 --- a/src/gstools/transform/array.py +++ b/src/gstools/transform/array.py @@ -215,9 +215,9 @@ def array_to_lognormal(field): return np.exp(field) -def array_to_uniform(field, mean=None, var=None): +def array_to_uniform(field, mean=None, var=None, low=0.0, high=1.0): """ - Transform normal distribution to uniform distribution on [0, 1]. + Transform normal distribution to uniform distribution on [low, high]. Parameters ---------- @@ -230,6 +230,12 @@ def array_to_uniform(field, mean=None, var=None): Variance of the given field. If None is given, the variance will be calculated. Default: :any:`None` + low : :class:`float`, optional + Lower bound for the uniform distribution. + Default: 0.0 + high : :class:`float`, optional + Upper bound for the uniform distribution. + Default: 1.0 Returns ------- @@ -239,7 +245,9 @@ def array_to_uniform(field, mean=None, var=None): field = np.asarray(field) mean = np.mean(field) if mean is None else float(mean) var = np.var(field) if var is None else float(var) - return 0.5 * (1 + erf((field - mean) / np.sqrt(2 * var))) + return ( + 0.5 * (1 + erf((field - mean) / np.sqrt(2 * var))) * (high - low) + low + ) def array_to_arcsin(field, mean=None, var=None, a=None, b=None): diff --git a/src/gstools/transform/field.py b/src/gstools/transform/field.py index 81824739..29697a86 100644 --- a/src/gstools/transform/field.py +++ b/src/gstools/transform/field.py @@ -548,6 +548,8 @@ def normal_to_lognormal( def normal_to_uniform( fld, + low=0.0, + high=1.0, field="field", store=True, process=False, @@ -562,6 +564,20 @@ def normal_to_uniform( ---------- fld : :any:`Field` Field class containing a generated field. + low : :class:`float`, optional + Lower bound for the uniform distribution. + Default: 0.0 + high : :class:`float`, optional + Upper bound for the uniform distribution. + Default: 1.0 + field : :class:`str`, optional + Name of field to be transformed. The default is "field". + store : :class:`str` or :class:`bool`, optional + Whether to store field inplace (True/False) or under a given name. + The default is True. + process : :class:`bool`, optional + Whether to process in/out fields with trend, normalizer and mean + of given Field instance. The default is False. keep_mean : :class:`bool`, optional Whether to keep the mean of the field if process=True. The default is True. From 2b755d370c6f8d25fc2c59f98f4ee3a52f32afff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Tue, 6 Jun 2023 17:07:44 +0200 Subject: [PATCH 052/102] uniform: forward low/high --- src/gstools/transform/field.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/gstools/transform/field.py b/src/gstools/transform/field.py index 29697a86..5d204407 100644 --- a/src/gstools/transform/field.py +++ b/src/gstools/transform/field.py @@ -585,7 +585,10 @@ def normal_to_uniform( if not process: _check_for_default_normal(fld) kw = dict( - mean=0.0 if process and not keep_mean else fld.mean, var=fld.model.sill + mean=0.0 if process and not keep_mean else fld.mean, + var=fld.model.sill, + low=low, + high=high, ) return apply_function( fld=fld, From b91a96e183182a98a793482ddc077d76b02daf77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Thu, 15 Jun 2023 21:00:54 +0200 Subject: [PATCH 053/102] finalize changelog for v1.5 --- CHANGELOG.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 733d75e3..633b50b4 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to **GSTools** will be documented in this file. -## [Unreleased] - ? - 2023-? +## [1.5.0] - Nifty Neon - 2023-06 ### Enhancements - added `temporal` flag to `CovModel` to explicitly specify spatio-temporal models [#308](https://github.com/GeoStat-Framework/GSTools/pull/308) @@ -22,10 +22,14 @@ All notable changes to **GSTools** will be documented in this file. - yadrenko variogram respects this and assumes the great circle distances is given in the respective unit - `vario_estimate` also has `geo_scale` now to control the units of the bins - `vario_estimate` now forwards additional kwargs to `standard_bins` (`bin_no`, `max_dist`) [#308](https://github.com/GeoStat-Framework/GSTools/pull/308) +- added `low` and `high` arguments to `uniform` transformation [#310](https://github.com/GeoStat-Framework/GSTools/pull/310) ### Changes - `CovModel`s expect special arguments by keyword now [#308](https://github.com/GeoStat-Framework/GSTools/pull/308) - always use f-strings internally [#283](https://github.com/GeoStat-Framework/GSTools/pull/283) +- removed `verbose` attribute from `RandMeth` classes [#309](https://github.com/GeoStat-Framework/GSTools/pull/309) +- all arguments for `RandMeth` classes key-word-only now except `model` [#309](https://github.com/GeoStat-Framework/GSTools/pull/309) +- rename "package" to "api" in doc structure [#290](https://github.com/GeoStat-Framework/GSTools/pull/290) ### Bugfixes - latex equations were not rendered correctly in docs [#290](https://github.com/GeoStat-Framework/GSTools/pull/290) @@ -400,7 +404,8 @@ See: [#197](https://github.com/GeoStat-Framework/GSTools/issues/197) First release of GSTools. -[Unreleased]: https://github.com/GeoStat-Framework/gstools/compare/v1.4.1...HEAD +[Unreleased]: https://github.com/GeoStat-Framework/gstools/compare/v1.5.0...HEAD +[1.5.0]: https://github.com/GeoStat-Framework/gstools/compare/v1.4.1...v1.5.0 [1.4.1]: https://github.com/GeoStat-Framework/gstools/compare/v1.4.0...v1.4.1 [1.4.0]: https://github.com/GeoStat-Framework/gstools/compare/v1.3.5...v1.4.0 [1.3.5]: https://github.com/GeoStat-Framework/gstools/compare/v1.3.4...v1.3.5 From b40bf5a410bbf7165c43aeb429df5c1ca8f71471 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Thu, 22 Jun 2023 11:32:53 +0200 Subject: [PATCH 054/102] fix cython-lint complains --- src/gstools/field/summator.pyx | 13 +++-- src/gstools/krige/krigesum.pyx | 3 +- src/gstools/variogram/estimator.pyx | 82 ++++++++++++++--------------- 3 files changed, 48 insertions(+), 50 deletions(-) diff --git a/src/gstools/field/summator.pyx b/src/gstools/field/summator.pyx index 87916ab3..398a713b 100644 --- a/src/gstools/field/summator.pyx +++ b/src/gstools/field/summator.pyx @@ -1,12 +1,10 @@ -#cython: language_level=3, boundscheck=False, wraparound=False, cdivision=True +# cython: language_level=3, boundscheck=False, wraparound=False, cdivision=True """ This is the randomization method summator, implemented in cython. """ import numpy as np -cimport cython - from cython.parallel import prange cimport numpy as np @@ -18,7 +16,7 @@ def summate( const double[:] z_1, const double[:] z_2, const double[:, :] pos - ): +): cdef int i, j, d cdef double phase cdef int dim = pos.shape[0] @@ -53,7 +51,7 @@ def summate_incompr( const double[:] z_1, const double[:] z_2, const double[:, :] pos - ): +): cdef int i, j, d cdef double phase cdef double k_2 @@ -76,8 +74,9 @@ def summate_incompr( phase += cov_samples[d, j] * pos[d, i] for d in range(dim): proj[d] = e1[d] - cov_samples[d, j] * cov_samples[0, j] / k_2 - summed_modes[d, i] += proj[d] * (z_1[j] * cos(phase) + z_2[j] * sin(phase)) - + summed_modes[d, i] += ( + proj[d] * (z_1[j] * cos(phase) + z_2[j] * sin(phase)) + ) return np.asarray(summed_modes) diff --git a/src/gstools/krige/krigesum.pyx b/src/gstools/krige/krigesum.pyx index de3a43b1..3b984844 100644 --- a/src/gstools/krige/krigesum.pyx +++ b/src/gstools/krige/krigesum.pyx @@ -1,11 +1,10 @@ -#cython: language_level=3, boundscheck=False, wraparound=False, cdivision=True +# cython: language_level=3, boundscheck=False, wraparound=False, cdivision=True """ This is a summator for the kriging routines """ import numpy as np -cimport cython from cython.parallel import prange cimport numpy as np diff --git a/src/gstools/variogram/estimator.pyx b/src/gstools/variogram/estimator.pyx index 528004fe..7f1a32a2 100644 --- a/src/gstools/variogram/estimator.pyx +++ b/src/gstools/variogram/estimator.pyx @@ -1,4 +1,4 @@ -#cython: language_level=3, boundscheck=False, wraparound=False, cdivision=True +# cython: language_level=3, boundscheck=False, wraparound=False, cdivision=True # distutils: language = c++ """ This is the variogram estimater, implemented in cython. @@ -6,8 +6,6 @@ This is the variogram estimater, implemented in cython. import numpy as np -cimport cython - from cython.parallel import parallel, prange cimport numpy as np @@ -16,20 +14,20 @@ from libc.math cimport M_PI, acos, atan2, cos, fabs, isnan, pow, sin, sqrt cdef inline double dist_euclid( const int dim, - const double[:,:] pos, + const double[:, :] pos, const int i, const int j, ) nogil: cdef int d cdef double dist_squared = 0.0 for d in range(dim): - dist_squared += ((pos[d,i] - pos[d,j]) * (pos[d,i] - pos[d,j])) + dist_squared += ((pos[d, i] - pos[d, j]) * (pos[d, i] - pos[d, j])) return sqrt(dist_squared) cdef inline double dist_haversine( const int dim, - const double[:,:] pos, + const double[:, :] pos, const int i, const int j, ) nogil: @@ -48,7 +46,7 @@ cdef inline double dist_haversine( ctypedef double (*_dist_func)( const int, - const double[:,:], + const double[:, :], const int, const int, ) nogil @@ -56,9 +54,9 @@ ctypedef double (*_dist_func)( cdef inline bint dir_test( const int dim, - const double[:,:] pos, + const double[:, :] pos, const double dist, - const double[:,:] direction, + const double[:, :] direction, const double angles_tol, const double bandwidth, const int i, @@ -74,12 +72,12 @@ cdef inline bint dir_test( # scalar-product calculation for bandwidth projection and angle calculation for k in range(dim): - s_prod += (pos[k,i] - pos[k,j]) * direction[d,k] + s_prod += (pos[k, i] - pos[k, j]) * direction[d, k] # calculate band-distance by projection of point-pair-vec to direction line if bandwidth > 0.0: for k in range(dim): - tmp = (pos[k,i] - pos[k,j]) - s_prod * direction[d,k] + tmp = (pos[k, i] - pos[k, j]) - s_prod * direction[d, k] b_dist += tmp * tmp in_band = sqrt(b_dist) < bandwidth @@ -130,32 +128,31 @@ ctypedef void (*_normalization_func)( ) cdef inline void normalization_matheron_vec( - double[:,:] variogram, - long[:,:] counts, + double[:, :] variogram, + long[:, :] counts, ): - cdef int d, i + cdef int d for d in range(variogram.shape[0]): normalization_matheron(variogram[d, :], counts[d, :]) cdef inline void normalization_cressie_vec( - double[:,:] variogram, - long[:,:] counts, + double[:, :] variogram, + long[:, :] counts, ): - cdef int d, i - cdef long cnt + cdef int d for d in range(variogram.shape[0]): normalization_cressie(variogram[d, :], counts[d, :]) ctypedef void (*_normalization_func_vec)( - double[:,:], - long[:,:], + double[:, :], + long[:, :], ) cdef _estimator_func choose_estimator_func(str estimator_type): cdef _estimator_func estimator_func if estimator_type == 'm': estimator_func = estimator_matheron - else: # estimator_type == 'c' + else: # estimator_type == 'c' estimator_func = estimator_cressie return estimator_func @@ -163,7 +160,7 @@ cdef _normalization_func choose_estimator_normalization(str estimator_type): cdef _normalization_func normalization_func if estimator_type == 'm': normalization_func = normalization_matheron - else: # estimator_type == 'c' + else: # estimator_type == 'c' normalization_func = normalization_cressie return normalization_func @@ -171,16 +168,16 @@ cdef _normalization_func_vec choose_estimator_normalization_vec(str estimator_ty cdef _normalization_func_vec normalization_func_vec if estimator_type == 'm': normalization_func_vec = normalization_matheron_vec - else: # estimator_type == 'c' + else: # estimator_type == 'c' normalization_func_vec = normalization_cressie_vec return normalization_func_vec def directional( - const double[:,:] f, + const double[:, :] f, const double[:] bin_edges, - const double[:,:] pos, - const double[:,:] direction, # should be normed + const double[:, :] pos, + const double[:, :] direction, # should be normed const double angles_tol=M_PI/8.0, const double bandwidth=-1.0, # negative values to turn of bandwidth search const bint separate_dirs=False, # whether the direction bands don't overlap @@ -207,8 +204,8 @@ def directional( cdef int k_max = pos.shape[1] cdef int f_max = f.shape[0] - cdef double[:,:] variogram = np.zeros((d_max, len(bin_edges)-1)) - cdef long[:,:] counts = np.zeros((d_max, len(bin_edges)-1), dtype=long) + cdef double[:, :] variogram = np.zeros((d_max, len(bin_edges)-1)) + cdef long[:, :] counts = np.zeros((d_max, len(bin_edges)-1), dtype=long) cdef int i, j, k, m, d cdef double dist @@ -219,13 +216,15 @@ def directional( if dist < bin_edges[i] or dist >= bin_edges[i+1]: continue # skip if not in current bin for d in range(d_max): - if not dir_test(dim, pos, dist, direction, angles_tol, bandwidth, k, j, d): + if not dir_test( + dim, pos, dist, direction, angles_tol, bandwidth, k, j, d + ): continue # skip if not in current direction for m in range(f_max): # skip no data values - if not (isnan(f[m,k]) or isnan(f[m,j])): + if not (isnan(f[m, k]) or isnan(f[m, j])): counts[d, i] += 1 - variogram[d, i] += estimator_func(f[m,k] - f[m,j]) + variogram[d, i] += estimator_func(f[m, k] - f[m, j]) # once we found a fitting direction # break the search if directions are separated if separate_dirs: @@ -234,10 +233,11 @@ def directional( normalization_func_vec(variogram, counts) return np.asarray(variogram), np.asarray(counts) + def unstructured( - const double[:,:] f, + const double[:, :] f, const double[:] bin_edges, - const double[:,:] pos, + const double[:, :] pos, str estimator_type='m', str distance_type='e', ): @@ -280,15 +280,15 @@ def unstructured( continue # skip if not in current bin for m in range(f_max): # skip no data values - if not (isnan(f[m,k]) or isnan(f[m,j])): + if not (isnan(f[m, k]) or isnan(f[m, j])): counts[i] += 1 - variogram[i] += estimator_func(f[m,k] - f[m,j]) + variogram[i] += estimator_func(f[m, k] - f[m, j]) normalization_func(variogram, counts) return np.asarray(variogram), np.asarray(counts) -def structured(const double[:,:] f, str estimator_type='m'): +def structured(const double[:, :] f, str estimator_type='m'): cdef _estimator_func estimator_func = choose_estimator_func(estimator_type) cdef _normalization_func normalization_func = ( choose_estimator_normalization(estimator_type) @@ -307,15 +307,15 @@ def structured(const double[:,:] f, str estimator_type='m'): for j in range(j_max): for k in prange(1, k_max-i): counts[k] += 1 - variogram[k] += estimator_func(f[i,j] - f[i+k,j]) + variogram[k] += estimator_func(f[i, j] - f[i+k, j]) normalization_func(variogram, counts) return np.asarray(variogram) def ma_structured( - const double[:,:] f, - const bint[:,:] mask, + const double[:, :] f, + const bint[:, :] mask, str estimator_type='m', ): cdef _estimator_func estimator_func = choose_estimator_func(estimator_type) @@ -335,9 +335,9 @@ def ma_structured( for i in range(i_max): for j in range(j_max): for k in prange(1, k_max-i): - if not mask[i,j] and not mask[i+k,j]: + if not mask[i, j] and not mask[i+k, j]: counts[k] += 1 - variogram[k] += estimator_func(f[i,j] - f[i+k,j]) + variogram[k] += estimator_func(f[i, j] - f[i+k, j]) normalization_func(variogram, counts) return np.asarray(variogram) From b9bc3d363ee0f233207afd77e3ea10c9c8b182ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Thu, 22 Jun 2023 11:41:51 +0200 Subject: [PATCH 055/102] add lint opt deps; add cython-lint check in CI --- .github/workflows/main.yml | 11 +++++++---- pyproject.toml | 6 ++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c3bf56e0..a48126fb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -32,20 +32,23 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install black 'pylint<3' 'isort[colors]<6' - pip install -v --editable . + pip install -v --editable .[lint] - name: black check run: | python -m black --check --diff --color . + - name: isort check + run: | + python -m isort --check --diff --color . + - name: pylint check run: | python -m pylint src/gstools/ - - name: isort check + - name: cython-lint check run: | - python -m isort --check --diff --color . + cython-lint src/gstools/ build_wheels: name: wheels for ${{ matrix.cfg.os }} / ${{ matrix.cfg.arch }} diff --git a/pyproject.toml b/pyproject.toml index 3a6c7ec0..39948c19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,6 +72,12 @@ plotting = [ ] rust = ["gstools_core>=0.2.0,<1"] test = ["pytest-cov>=3"] +lint = [ + "black", + "pylint<3", + "isort[colors]<6", + "cython-lint", +] [project.urls] Changelog = "https://github.com/GeoStat-Framework/GSTools/blob/main/CHANGELOG.md" From dd0f2a6a3a60793338b36ed29cd3c2f139fa8e29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Thu, 22 Jun 2023 11:47:02 +0200 Subject: [PATCH 056/102] apply isort to pyx files --- src/gstools/field/summator.pyx | 1 - src/gstools/krige/krigesum.pyx | 2 +- src/gstools/variogram/estimator.pyx | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/gstools/field/summator.pyx b/src/gstools/field/summator.pyx index 398a713b..610dd701 100644 --- a/src/gstools/field/summator.pyx +++ b/src/gstools/field/summator.pyx @@ -4,7 +4,6 @@ This is the randomization method summator, implemented in cython. """ import numpy as np - from cython.parallel import prange cimport numpy as np diff --git a/src/gstools/krige/krigesum.pyx b/src/gstools/krige/krigesum.pyx index 3b984844..2f79d3ad 100644 --- a/src/gstools/krige/krigesum.pyx +++ b/src/gstools/krige/krigesum.pyx @@ -4,8 +4,8 @@ This is a summator for the kriging routines """ import numpy as np - from cython.parallel import prange + cimport numpy as np diff --git a/src/gstools/variogram/estimator.pyx b/src/gstools/variogram/estimator.pyx index 7f1a32a2..611f5efb 100644 --- a/src/gstools/variogram/estimator.pyx +++ b/src/gstools/variogram/estimator.pyx @@ -5,7 +5,6 @@ This is the variogram estimater, implemented in cython. """ import numpy as np - from cython.parallel import parallel, prange cimport numpy as np From e6f3529216208847f99c782ac4dfeaf0c8455970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Fri, 3 Nov 2023 15:45:42 +0100 Subject: [PATCH 057/102] cython: switch to cython >3.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 39948c19..daa4c4bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = [ "setuptools>=64", "setuptools_scm>=7", "oldest-supported-numpy", - "Cython>=0.29.32,<3.0", + "Cython>=3.0", "extension-helpers>=1", ] build-backend = "setuptools.build_meta" From 04adf6548d0e1c3c6b161c1a1fcd1360c6c069c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Fri, 3 Nov 2023 15:50:23 +0100 Subject: [PATCH 058/102] lint: remove caps for lint deps --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index daa4c4bd..cf5048c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,8 +74,8 @@ rust = ["gstools_core>=0.2.0,<1"] test = ["pytest-cov>=3"] lint = [ "black", - "pylint<3", - "isort[colors]<6", + "pylint", + "isort[colors]", "cython-lint", ] From f10ae3522aed31143edbd0048d9e7971b1fafc48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Fri, 3 Nov 2023 15:56:31 +0100 Subject: [PATCH 059/102] python: drop py37 and add 312 --- .github/workflows/main.yml | 4 ++-- pyproject.toml | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a48126fb..e116c483 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -71,7 +71,7 @@ jobs: fetch-depth: '0' - name: Build wheels - uses: pypa/cibuildwheel@v2.11.2 + uses: pypa/cibuildwheel@v2.16.2 env: CIBW_ARCHS: ${{ matrix.cfg.arch }} with: @@ -88,7 +88,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v2 diff --git a/pyproject.toml b/pyproject.toml index cf5048c9..70b7d0a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ requires = [ build-backend = "setuptools.build_meta" [project] -requires-python = ">=3.7" +requires-python = ">=3.8" name = "gstools" description = "GSTools: A geostatistical toolbox." authors = [ @@ -32,11 +32,11 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: Scientific/Engineering", "Topic :: Scientific/Engineering :: GIS", "Topic :: Scientific/Engineering :: Hydrology", @@ -104,11 +104,11 @@ line_length = 79 [tool.black] line-length = 79 target-version = [ - "py37", "py38", "py39", "py310", "py311", + "py312", ] [tool.coverage] @@ -160,8 +160,8 @@ target-version = [ [tool.cibuildwheel] # Switch to using build build-frontend = "build" -# Disable building PyPy wheels on all platforms, 32bit for py3.10/11 and musllinux builds, py3.6 -skip = ["cp36-*", "pp*", "cp31*-win32", "cp31*-manylinux_i686", "*-musllinux_*"] +# Disable building PyPy wheels on all platforms, 32bit for py3.10/11/12, musllinux builds, py3.6/7 +skip = ["cp36-*", "cp37-*", "pp*", "cp31*-win32", "cp31*-manylinux_i686", "*-musllinux_*"] # Run the package tests using `pytest` test-extras = "test" test-command = "pytest -v {package}/tests" From 26efefb4fc38901f1f3bfa549058e88631bbc412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Fri, 3 Nov 2023 16:00:16 +0100 Subject: [PATCH 060/102] update changelog --- CHANGELOG.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 633b50b4..00513d23 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to **GSTools** will be documented in this file. +## [1.5.1] - Nifty Neon - 2023-11 + +### Enhancements + +see [#317](https://github.com/GeoStat-Framework/GSTools/pull/317) + +- added wheels for Python 3.12 +- dropped support for Python 3.7 (EOL) +- linted Cython files with cython-lint +- use Cython 3 to build extensions + + ## [1.5.0] - Nifty Neon - 2023-06 ### Enhancements @@ -404,7 +416,8 @@ See: [#197](https://github.com/GeoStat-Framework/GSTools/issues/197) First release of GSTools. -[Unreleased]: https://github.com/GeoStat-Framework/gstools/compare/v1.5.0...HEAD +[Unreleased]: https://github.com/GeoStat-Framework/gstools/compare/v1.5.1...HEAD +[1.5.1]: https://github.com/GeoStat-Framework/gstools/compare/v1.5.0...v1.5.1 [1.5.0]: https://github.com/GeoStat-Framework/gstools/compare/v1.4.1...v1.5.0 [1.4.1]: https://github.com/GeoStat-Framework/gstools/compare/v1.4.0...v1.4.1 [1.4.0]: https://github.com/GeoStat-Framework/gstools/compare/v1.3.5...v1.4.0 From beb4f4406fe1739c5ecf91822816a00b32f551a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Fri, 3 Nov 2023 16:09:05 +0100 Subject: [PATCH 061/102] rtd config fix --- .readthedocs.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 2ef05a17..7dd17607 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,12 +1,16 @@ version: 2 +build: + os: ubuntu-22.04 + tools: + python: "3.11" + sphinx: configuration: docs/source/conf.py formats: all python: - version: 3.8 install: - method: pip path: . From f5bd2bd6b886d6e568dc6b5d67f743b745ea6ab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lennart=20Sch=C3=BCler?= Date: Wed, 8 Nov 2023 21:00:36 +0200 Subject: [PATCH 062/102] Add a simple example of how to use the Fourier gen --- examples/00_misc/06_fourier.py | 38 ++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 examples/00_misc/06_fourier.py diff --git a/examples/00_misc/06_fourier.py b/examples/00_misc/06_fourier.py new file mode 100644 index 00000000..1099d0d8 --- /dev/null +++ b/examples/00_misc/06_fourier.py @@ -0,0 +1,38 @@ +""" +Generating a Periodic Random Field +---------------------------------- + +In this simple example we are going to learn how to generate periodic spatial +random fields. The Fourier method comes naturally with the property of +periodicity, so we'll use it to create the random field. +""" + +import numpy as np +import gstools as gs + +# We start off by defining the spatial grid. +x = np.linspace(0, 500, 256) +y = np.linspace(0, 500, 128) + +# And by setting up a Gaussian covariance model with a correlation length +# scale which is roughly half the size of the grid. +model = gs.Gaussian(dim=2, var=1, len_scale=200) + +# Next, we hand the cov. model to the spatial random field class +# and set the generator to `Fourier`. The higher the modes_no, the better +# the quality of the generated field, but also the computing time increases. +# The modes_truncation are the cut-off values of the Fourier modes and finally, +# the seed ensures that we generate the same random field each time. +srf = gs.SRF( + model, + generator="Fourier", + modes_no=[16, 8], + modes_truncation=[16, 8], + seed=1681903, +) + +# Now, we can finally calculate the field with the given parameters. +srf((x, y), mesh_type='structured') + +# GSTools has a few simple visualization methods built in. +srf.plot() From ad413d53c1b0fa5e51dbc3360e0b4ad19aec8ae6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lennart=20Sch=C3=BCler?= Date: Fri, 10 Nov 2023 09:39:58 +0200 Subject: [PATCH 063/102] Add exception for Fourier with unstruct grid --- src/gstools/field/srf.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/gstools/field/srf.py b/src/gstools/field/srf.py index 77285a02..4195091a 100644 --- a/src/gstools/field/srf.py +++ b/src/gstools/field/srf.py @@ -143,6 +143,11 @@ def __call__( field : :class:`numpy.ndarray` the SRF """ + if isinstance(self.generator, Fourier) and mesh_type != "structured": + raise ValueError( + f"SRF: Fourier generator only defined for " + 'mesh_type == "structured".' + ) name, save = self.get_store_config(store) # update the model/seed in the generator if any changes were made self.generator.update(self.model, seed) From 3a3b5fe7cbfaab7ad1a69058d31bc37ed465989a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lennart=20Sch=C3=BCler?= Date: Fri, 10 Nov 2023 09:40:57 +0200 Subject: [PATCH 064/102] Black --- src/gstools/field/srf.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/gstools/field/srf.py b/src/gstools/field/srf.py index 4195091a..b845e8d0 100644 --- a/src/gstools/field/srf.py +++ b/src/gstools/field/srf.py @@ -13,7 +13,12 @@ import numpy as np from gstools.field.base import Field -from gstools.field.generator import Generator, IncomprRandMeth, RandMeth, Fourier +from gstools.field.generator import ( + Generator, + IncomprRandMeth, + RandMeth, + Fourier, +) from gstools.field.upscaling import var_coarse_graining, var_no_scaling __all__ = ["SRF"] From ec9644991ca869686ee8cc1b7fd3fa4153debad9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lennart=20Sch=C3=BCler?= Date: Fri, 10 Nov 2023 09:41:10 +0200 Subject: [PATCH 065/102] Remove `period_offset`, should be pre- or post-pro --- src/gstools/field/generator.py | 29 +++++++---------------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/src/gstools/field/generator.py b/src/gstools/field/generator.py index 1caf8d43..5eb1331b 100644 --- a/src/gstools/field/generator.py +++ b/src/gstools/field/generator.py @@ -546,8 +546,6 @@ class Fourier(Generator): Cut-off values of the Fourier modes. period_len : :class:`float` or :class:`list`, optional Period length of the field in each dim as a factor of the domain size. - period_offset : :class:`float` or :class:`list`, optional - The period offset by which the field will be shifted. seed : :class:`int`, optional The seed of the random number generator. If "None", a random seed is used. Default: :any:`None` @@ -592,7 +590,6 @@ def __init__( modes_no, modes_truncation, period_len=None, - period_offset=None, seed=None, verbose=False, **kwargs, @@ -600,8 +597,10 @@ def __init__( if kwargs: warnings.warn("gstools.Fourier: **kwargs are ignored") # initialize attributes - self._modes_truncation = np.array(modes_truncation) - self._modes_no = np.array(modes_no) + self._modes_truncation = self._fill_to_dim( + model.dim, modes_truncation, np.double + ) + self._modes_no = self._fill_to_dim(model.dim, modes_no, int) self._modes = [] [ self._modes.append( @@ -618,10 +617,6 @@ def __init__( self._period_len = self._fill_to_dim( model.dim, period_len, np.double, 1.0 ) - self._period_offset = self._fill_to_dim( - model.dim, period_offset, np.double, 0.0 - ) - self._verbose = bool(verbose) # initialize private attributes self._model = None @@ -653,7 +648,6 @@ def __call__(self, pos, add_nugget=True): the random modes """ pos = np.asarray(pos, dtype=np.double) - pos -= self._period_offset[:, None] domain_size = pos.max(axis=1) - pos.min(axis=1) self._modes = [ self._modes[d] / domain_size[d] * self._period_len[d] @@ -780,10 +774,12 @@ def reset_seed(self, seed=np.nan): self._z_1 = self._rng.random.normal(size=np.prod(self._modes_no)) self._z_2 = self._rng.random.normal(size=np.prod(self._modes_no)) - def _fill_to_dim(self, dim, values, dtype, default_value): + def _fill_to_dim(self, dim, values, dtype, default_value=None): """Fill an array with last element up to len(dim).""" r = values if values is None: + if default_value is None: + raise ValueError(f"Fourier: Value has to be provided") r = default_value r = np.array(r, dtype=dtype) r = np.atleast_1d(r)[:dim] @@ -839,17 +835,6 @@ def period_len(self, period_len): self._model.dim, period_len, np.double, 1.0 ) - @property - def period_offset(self): - """:class:`list`: Period offset of the field in each dim.""" - return self._period_offset - - @period_offset.setter - def period_offset(self, period_offset): - self._period_offset = self._fill_to_dim( - self._model.dim, period_offset, np.double, 0.0 - ) - @property def verbose(self): """:class:`bool`: Verbosity of the generator.""" From d55e52b42c77cf4b79b386ad0e4bddf6a2a29611 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lennart=20Sch=C3=BCler?= Date: Fri, 10 Nov 2023 09:41:39 +0200 Subject: [PATCH 066/102] Add a few first test cases --- tests/test_fouriergen.py | 74 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 tests/test_fouriergen.py diff --git a/tests/test_fouriergen.py b/tests/test_fouriergen.py new file mode 100644 index 00000000..75811a5f --- /dev/null +++ b/tests/test_fouriergen.py @@ -0,0 +1,74 @@ +""" +This is the unittest of the Fourier class. +""" + +import copy +import unittest + +import numpy as np + +import gstools as gs +from gstools.field.generator import Fourier + + +class TestFourier(unittest.TestCase): + def setUp(self): + self.seed = 19900408 + self.cov_model_1d = gs.Gaussian(dim=1, var=0.5, len_scale=10.) + self.cov_model_2d = gs.Gaussian(dim=2, var=2.0, len_scale=30.) + self.cov_model_3d = gs.Gaussian(dim=3, var=2.1, len_scale=21.) + self.x = np.linspace(0, 80, 11) + self.y = np.linspace(0, 30, 31) + self.z = np.linspace(0, 91, 13) + + self.modes_no_1d = 20 + self.trunc_1d = 8 + self.modes_no_2d = [16, 7] + self.trunc_2d = [16, 7] + self.modes_no_3d = [16, 7, 11] + self.trunc_3d = [16, 7, 12] + + self.srf_1d = gs.SRF( + self.cov_model_1d, + generator="Fourier", + modes_no=self.modes_no_1d, + modes_truncation=self.trunc_1d, + seed=self.seed, + ) + self.srf_2d = gs.SRF( + self.cov_model_2d, + generator="Fourier", + modes_no=self.modes_no_2d, + modes_truncation=self.trunc_2d, + seed=self.seed, + ) + self.srf_3d = gs.SRF( + self.cov_model_3d, + generator="Fourier", + modes_no=self.modes_no_3d, + modes_truncation=self.trunc_3d, + seed=self.seed, + ) + + def test_1d(self): + field = self.srf_1d((self.x,), mesh_type="structured") + self.assertAlmostEqual(field[0], 0.9009981010688789) + + def test_2d(self): + field = self.srf_2d((self.x, self.y), mesh_type="structured") + self.assertAlmostEqual(field[0, 0], 1.1085370190533947) + + def test_3d(self): + field = self.srf_3d((self.x, self.y, self.z), mesh_type="structured") + self.assertAlmostEqual(field[0, 0, 0], 1.7648407965681794) + + def test_periodicity(self): + field = self.srf_2d((self.x, self.y), mesh_type="structured") + self.assertAlmostEqual(field[0, len(self.y)//2], field[-1, len(self.y)//2]) + + def test_assertions(self): + # unstructured grids not supported + self.assertRaises(ValueError, self.srf_2d, (self.x, self.y)) + self.assertRaises( + ValueError, self.srf_2d, (self.x, self.y), mesh_type="unstructured" + ) From 362f501f80e2bbe73b117df13ee51827c082c469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lennart=20Sch=C3=BCler?= Date: Fri, 10 Nov 2023 11:43:44 +0200 Subject: [PATCH 067/102] Add another example, including field transf. --- examples/00_misc/06_fourier.py | 4 +-- examples/00_misc/07_fourier_trans.py | 41 ++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 examples/00_misc/07_fourier_trans.py diff --git a/examples/00_misc/06_fourier.py b/examples/00_misc/06_fourier.py index 1099d0d8..cf08148a 100644 --- a/examples/00_misc/06_fourier.py +++ b/examples/00_misc/06_fourier.py @@ -1,6 +1,6 @@ """ -Generating a Periodic Random Field ----------------------------------- +Generating a Simple Periodic Random Field +----------------------------------------- In this simple example we are going to learn how to generate periodic spatial random fields. The Fourier method comes naturally with the property of diff --git a/examples/00_misc/07_fourier_trans.py b/examples/00_misc/07_fourier_trans.py new file mode 100644 index 00000000..aea0cb43 --- /dev/null +++ b/examples/00_misc/07_fourier_trans.py @@ -0,0 +1,41 @@ +""" +Generating a Transformed Periodic Random Field +---------------------------------------------- + +Building on the precious example, we are now going to generate periodic +spatial random fields with a transformation applied, resulting in a level set. +""" + +import numpy as np +import gstools as gs + +# We start off by defining the spatial grid. +x = np.linspace(0, 500, 300) +y = np.linspace(0, 500, 200) + +# Instead of using a Gaussian covariance model, we will use the much rougher +# exponential model and we will introduce an anisotropy by using two different +# length scales in the x- and y-axes +model = gs.Exponential(dim=2, var=2, len_scale=[30, 20]) + +# Very similar as before, setting up the spatial random field +srf = gs.SRF( + model, + generator="Fourier", + modes_no=[30, 20], + modes_truncation=[30, 20], + seed=1681903, +) +# and computing it +srf((x, y), mesh_type='structured') + +# With the field generated, we can now apply transformations +# starting with a discretization of the field into 4 different values +thresholds = np.linspace(np.min(srf.field), np.max(srf.field), 4) +srf.transform("discrete", store="transform_discrete", values=thresholds) +srf.plot("transform_discrete") + +# This is already a nice result, but we want to pronounce the peaks of the +# field. We can do this by applying a log-normal transformation on top +srf.transform("lognormal", field="transform_discrete", store="transform_lognormal") +srf.plot("transform_lognormal") From f92fd638b13e46d96e7c91db370a97db277896bc Mon Sep 17 00:00:00 2001 From: LSchueler Date: Fri, 1 Mar 2024 10:45:22 +0100 Subject: [PATCH 068/102] [WIP] Add variable pre-factors to Fourier gen. --- src/gstools/field/generator.py | 6 ++++-- src/gstools/field/srf.py | 16 +++++++++++++++- src/gstools/field/summator.pyx | 10 ++++++---- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/gstools/field/generator.py b/src/gstools/field/generator.py index cabf4a6d..64e044aa 100644 --- a/src/gstools/field/generator.py +++ b/src/gstools/field/generator.py @@ -635,7 +635,7 @@ def __init__( # set model and seed self.update(model, seed) - def __call__(self, pos, add_nugget=True): + def __call__(self, pos, add_nugget=True, phase_factor=2.*np.pi, spec_factor=1., var_factor=1.,): """Calculate the modes for the Fourier method. This method calls the `summate_*` Cython methods, which are the @@ -674,10 +674,12 @@ def __call__(self, pos, add_nugget=True): self._z_1, self._z_2, pos, + phase_factor, + spec_factor, ) nugget = self.get_nugget(summed_modes.shape) if add_nugget else 0.0 return ( - np.sqrt(2.0 * self.model.var / np.prod(domain_size)) * summed_modes + np.sqrt(var_factor * 2.0 * self.model.var / np.prod(domain_size)) * summed_modes + nugget ) diff --git a/src/gstools/field/srf.py b/src/gstools/field/srf.py index b845e8d0..97b7eea5 100644 --- a/src/gstools/field/srf.py +++ b/src/gstools/field/srf.py @@ -115,6 +115,9 @@ def __call__( mesh_type="unstructured", post_process=True, store=True, + phase_factor=2.*np.pi, + spec_factor=1., + var_factor=1., ): """Generate the spatial random field. @@ -159,7 +162,18 @@ def __call__( # get isometrized positions and the resulting field-shape iso_pos, shape = self.pre_pos(pos, mesh_type) # generate the field - field = np.reshape(self.generator(iso_pos), shape) + try: + field = np.reshape( + self.generator( + iso_pos, + phase_factor=phase_factor, + spec_factor=spec_factor, + var_factor=var_factor + ), + shape, + ) + except TypeError: + field = np.reshape(self.generator(iso_pos, ), shape) # upscaled variance if not np.isscalar(point_volumes) or not np.isclose(point_volumes, 0): scaled_var = self.upscaling_func(self.model, point_volumes) diff --git a/src/gstools/field/summator.pyx b/src/gstools/field/summator.pyx index 04334e56..2db1cf34 100644 --- a/src/gstools/field/summator.pyx +++ b/src/gstools/field/summator.pyx @@ -96,7 +96,6 @@ def summate_incompr( summed_modes[d, i] += ( proj[d] * (z_1[j] * cos(phase) + z_2[j] * sin(phase)) ) - return np.asarray(summed_modes) @@ -105,7 +104,9 @@ def summate_fourier( const double[:, :] modes, const double[:] z_1, const double[:] z_2, - const double[:, :] pos + const double[:, :] pos, + const double phase_factor, + const double spec_factor, ): cdef int i, j, d cdef double phase @@ -122,7 +123,8 @@ def summate_fourier( for d in range(dim): phase += modes[d, j] * pos[d, i] # OpenMP doesn't like *= after +=... seems to be a compiler specific thing - phase = phase * 2. * pi - summed_modes[i] += spectral_density_sqrt[j] * (z_1[j] * cos(phase) + z_2[j] * sin(phase)) + # phase = phase * 2. * pi + phase = phase * phase_factor + summed_modes[i] += spec_factor * spectral_density_sqrt[j] * (z_1[j] * cos(phase) + z_2[j] * sin(phase)) return np.asarray(summed_modes) From aaf84bb2118795a8e6ab71d7cf043508f264fa32 Mon Sep 17 00:00:00 2001 From: LSchueler Date: Tue, 28 May 2024 00:37:31 +0200 Subject: [PATCH 069/102] Fix Fourier meth. eq. and let user generate modes --- src/gstools/field/generator.py | 83 +++++++--------------------------- src/gstools/field/srf.py | 18 +------- src/gstools/field/summator.pyx | 18 ++++---- tests/test_fouriergen.py | 43 +++++++++--------- 4 files changed, 49 insertions(+), 113 deletions(-) mode change 100644 => 100755 src/gstools/field/generator.py mode change 100644 => 100755 src/gstools/field/srf.py mode change 100644 => 100755 src/gstools/field/summator.pyx diff --git a/src/gstools/field/generator.py b/src/gstools/field/generator.py old mode 100644 new mode 100755 index a05f4a25..8bfad69a --- a/src/gstools/field/generator.py +++ b/src/gstools/field/generator.py @@ -32,8 +32,8 @@ else: from gstools.field.summator import ( summate, - summate_incompr, summate_fourier, + summate_incompr, ) __all__ = ["Generator", "RandMeth", "IncomprRandMeth", "Fourier"] @@ -551,8 +551,6 @@ class Fourier(Generator): Number of Fourier modes per dimension. mode_truncation : :class:`list` Cut-off values of the Fourier modes. - period_len : :class:`float` or :class:`list`, optional - Period length of the field in each dim as a factor of the domain size. seed : :class:`int`, optional The seed of the random number generator. If "None", a random seed is used. Default: :any:`None` @@ -594,9 +592,7 @@ class Fourier(Generator): def __init__( self, model, - modes_no, - modes_truncation, - period_len=None, + modes, seed=None, verbose=False, **kwargs, @@ -604,26 +600,12 @@ def __init__( if kwargs: warnings.warn("gstools.Fourier: **kwargs are ignored") # initialize attributes - self._modes_truncation = self._fill_to_dim( - model.dim, modes_truncation, np.double + self._modes = generate_grid(modes) + self._modes_no = [len(m) for m in modes] + self._delta_k = np.array( + [modes[d][1] - modes[d][0] for d in range(model.dim)] ) - self._modes_no = self._fill_to_dim(model.dim, modes_no, int) - self._modes = [] - [ - self._modes.append( - np.linspace( - -self._modes_truncation[d] / 2, - self._modes_truncation[d] / 2, - self._modes_no[d], - endpoint=False, - ).T - ) - for d in range(model.dim) - ] - self._period_len = self._fill_to_dim( - model.dim, period_len, np.double, 1.0 - ) self._verbose = bool(verbose) # initialize private attributes self._model = None @@ -631,12 +613,12 @@ def __init__( self._rng = None self._z_1 = None self._z_2 = None - self._spectral_density_sqrt = None + self._spectrum_factor = None self._value_type = "scalar" # set model and seed self.update(model, seed) - def __call__(self, pos, add_nugget=True, phase_factor=2.*np.pi, spec_factor=1., var_factor=1.,): + def __call__(self, pos, add_nugget=True): """Calculate the modes for the Fourier method. This method calls the `summate_*` Cython methods, which are the @@ -655,34 +637,17 @@ def __call__(self, pos, add_nugget=True, phase_factor=2.*np.pi, spec_factor=1., the random modes """ pos = np.asarray(pos, dtype=np.double) - domain_size = pos.max(axis=1) - pos.min(axis=1) - self._modes = [ - self._modes[d] / domain_size[d] * self._period_len[d] - for d in range(self._model.dim) - ] - self._modes = generate_grid(self._modes) - - # pre calc. the spectral density for all wave numbers - # they are handed over to Cython - k_norm = np.linalg.norm(self._modes, axis=0) - self._spectral_density_sqrt = np.sqrt( - self._model.spectral_density(k_norm) - ) summed_modes = summate_fourier( - self._spectral_density_sqrt, + self._spectrum_factor, self._modes, self._z_1, self._z_2, pos, - phase_factor, - spec_factor, + config.NUM_THREADS, ) nugget = self.get_nugget(summed_modes.shape) if add_nugget else 0.0 - return ( - np.sqrt(var_factor * 2.0 * self.model.var / np.prod(domain_size)) * summed_modes - + nugget - ) + return summed_modes + nugget def get_nugget(self, shape): """ @@ -782,6 +747,12 @@ def reset_seed(self, seed=np.nan): # normal distributed samples for randmeth self._z_1 = self._rng.random.normal(size=np.prod(self._modes_no)) self._z_2 = self._rng.random.normal(size=np.prod(self._modes_no)) + # pre calc. the spectrum for all wave numbers they are handed over to + # Cython, which doesn't have access to the CovModel + k_norm = np.linalg.norm(self._modes, axis=0) + self._spectrum_factor = np.sqrt( + 2.0 * self._model.spectrum(k_norm) * np.prod(self._delta_k) + ) def _fill_to_dim(self, dim, values, dtype, default_value=None): """Fill an array with last element up to len(dim).""" @@ -824,26 +795,6 @@ def model(self): def model(self, model): self.update(model) - @property - def modes_truncation(self): - """:class:`list`: Cut-off values of the Fourier modes.""" - return self._modes_truncation - - @modes_truncation.setter - def modes_truncation(self, modes_truncation): - self._modes_truncation = modes_truncation - - @property - def period_len(self): - """:class:`list`: Period length of the field in each dim.""" - return self._period_len - - @period_len.setter - def period_len(self, period_len): - self._period_len = self._fill_to_dim( - self._model.dim, period_len, np.double, 1.0 - ) - @property def verbose(self): """:class:`bool`: Verbosity of the generator.""" diff --git a/src/gstools/field/srf.py b/src/gstools/field/srf.py old mode 100644 new mode 100755 index aeacc213..5af15aeb --- a/src/gstools/field/srf.py +++ b/src/gstools/field/srf.py @@ -15,10 +15,10 @@ from gstools.field.base import Field from gstools.field.generator import ( + Fourier, Generator, IncomprRandMeth, RandMeth, - Fourier, ) from gstools.field.upscaling import var_coarse_graining, var_no_scaling @@ -116,9 +116,6 @@ def __call__( mesh_type="unstructured", post_process=True, store=True, - phase_factor=2.*np.pi, - spec_factor=1., - var_factor=1., ): """Generate the spatial random field. @@ -163,18 +160,7 @@ def __call__( # get isometrized positions and the resulting field-shape iso_pos, shape = self.pre_pos(pos, mesh_type) # generate the field - try: - field = np.reshape( - self.generator( - iso_pos, - phase_factor=phase_factor, - spec_factor=spec_factor, - var_factor=var_factor - ), - shape, - ) - except TypeError: - field = np.reshape(self.generator(iso_pos, ), shape) + field = np.reshape(self.generator(iso_pos), shape) # upscaled variance if not np.isscalar(point_volumes) or not np.isclose(point_volumes, 0): scaled_var = self.upscaling_func(self.model, point_volumes) diff --git a/src/gstools/field/summator.pyx b/src/gstools/field/summator.pyx old mode 100644 new mode 100755 index 2db1cf34..bfd91c61 --- a/src/gstools/field/summator.pyx +++ b/src/gstools/field/summator.pyx @@ -10,7 +10,7 @@ IF OPENMP: cimport openmp cimport numpy as np -from libc.math cimport pi, cos, sin, sqrt +from libc.math cimport cos, pi, sin, sqrt def set_num_threads(num_threads): @@ -100,13 +100,12 @@ def summate_incompr( def summate_fourier( - const double[:] spectral_density_sqrt, + const double[:] spectrum_factor, const double[:, :] modes, const double[:] z_1, const double[:] z_2, const double[:, :] pos, - const double phase_factor, - const double spec_factor, + num_threads=None, ): cdef int i, j, d cdef double phase @@ -117,14 +116,15 @@ def summate_fourier( cdef double[:] summed_modes = np.zeros(X_len, dtype=float) - for i in prange(X_len, nogil=True): + cdef int num_threads_c = set_num_threads(num_threads) + + for i in prange(X_len, nogil=True, num_threads=num_threads_c): for j in range(N): phase = 0. for d in range(dim): phase += modes[d, j] * pos[d, i] - # OpenMP doesn't like *= after +=... seems to be a compiler specific thing - # phase = phase * 2. * pi - phase = phase * phase_factor - summed_modes[i] += spec_factor * spectral_density_sqrt[j] * (z_1[j] * cos(phase) + z_2[j] * sin(phase)) + summed_modes[i] += ( + spectrum_factor[j] * (z_1[j] * cos(phase) + z_2[j] * sin(phase)) + ) return np.asarray(summed_modes) diff --git a/tests/test_fouriergen.py b/tests/test_fouriergen.py index 75811a5f..78e44052 100644 --- a/tests/test_fouriergen.py +++ b/tests/test_fouriergen.py @@ -14,57 +14,56 @@ class TestFourier(unittest.TestCase): def setUp(self): self.seed = 19900408 - self.cov_model_1d = gs.Gaussian(dim=1, var=0.5, len_scale=10.) - self.cov_model_2d = gs.Gaussian(dim=2, var=2.0, len_scale=30.) - self.cov_model_3d = gs.Gaussian(dim=3, var=2.1, len_scale=21.) - self.x = np.linspace(0, 80, 11) - self.y = np.linspace(0, 30, 31) - self.z = np.linspace(0, 91, 13) + self.cov_model_1d = gs.Gaussian(dim=1, var=0.5, len_scale=10.0) + self.cov_model_2d = gs.Gaussian(dim=2, var=2.0, len_scale=30.0) + self.cov_model_3d = gs.Gaussian(dim=3, var=2.1, len_scale=21.0) + L = [80, 30, 91] + self.x = np.linspace(0, L[0], 11) + self.y = np.linspace(0, L[1], 31) + self.z = np.linspace(0, L[2], 13) - self.modes_no_1d = 20 - self.trunc_1d = 8 - self.modes_no_2d = [16, 7] - self.trunc_2d = [16, 7] - self.modes_no_3d = [16, 7, 11] - self.trunc_3d = [16, 7, 12] + dk = [2 * np.pi / l for l in L] + + self.modes_1d = [np.arange(0, 2, dk[0])] + self.modes_2d = self.modes_1d + [np.arange(0, 2, dk[1])] + self.modes_3d = self.modes_2d + [np.arange(0, 2, dk[2])] self.srf_1d = gs.SRF( self.cov_model_1d, generator="Fourier", - modes_no=self.modes_no_1d, - modes_truncation=self.trunc_1d, + modes=self.modes_1d, seed=self.seed, ) self.srf_2d = gs.SRF( self.cov_model_2d, generator="Fourier", - modes_no=self.modes_no_2d, - modes_truncation=self.trunc_2d, + modes=self.modes_2d, seed=self.seed, ) self.srf_3d = gs.SRF( self.cov_model_3d, generator="Fourier", - modes_no=self.modes_no_3d, - modes_truncation=self.trunc_3d, + modes=self.modes_3d, seed=self.seed, ) def test_1d(self): field = self.srf_1d((self.x,), mesh_type="structured") - self.assertAlmostEqual(field[0], 0.9009981010688789) + self.assertAlmostEqual(field[0], 0.40939882638496783) def test_2d(self): field = self.srf_2d((self.x, self.y), mesh_type="structured") - self.assertAlmostEqual(field[0, 0], 1.1085370190533947) + self.assertAlmostEqual(field[0, 0], 0.8176311251780369) def test_3d(self): field = self.srf_3d((self.x, self.y, self.z), mesh_type="structured") - self.assertAlmostEqual(field[0, 0, 0], 1.7648407965681794) + self.assertAlmostEqual(field[0, 0, 0], -1.2636015063084773) def test_periodicity(self): field = self.srf_2d((self.x, self.y), mesh_type="structured") - self.assertAlmostEqual(field[0, len(self.y)//2], field[-1, len(self.y)//2]) + self.assertAlmostEqual( + field[0, len(self.y) // 2], field[-1, len(self.y) // 2] + ) def test_assertions(self): # unstructured grids not supported From 1a6aae1d9f03f6f3943c265961acf0fd8ae7fddd Mon Sep 17 00:00:00 2001 From: LSchueler Date: Wed, 29 May 2024 00:43:02 +0200 Subject: [PATCH 070/102] Improve user interface of Fourier method Now, the modes can be calculated internally from a rel. cutoff value and the periodicity. --- src/gstools/field/generator.py | 112 +++++++++++++++++++++++++++------ 1 file changed, 93 insertions(+), 19 deletions(-) diff --git a/src/gstools/field/generator.py b/src/gstools/field/generator.py index 8bfad69a..5a485c2d 100755 --- a/src/gstools/field/generator.py +++ b/src/gstools/field/generator.py @@ -19,6 +19,7 @@ from abc import ABC, abstractmethod from copy import deepcopy as dcp +from scipy.optimize import least_squares import numpy as np from gstools import config @@ -547,10 +548,15 @@ class Fourier(Generator): ---------- model : :any:`CovModel` Covariance model - mode_no : :class:`list` - Number of Fourier modes per dimension. - mode_truncation : :class:`list` - Cut-off values of the Fourier modes. + mode_rel_cutoff : :class:`float`, optional + The cutoff value, relative to the spectral density's maximum, e.g. a + value of 0.99 means that the function falls to 1% of its max. value + before it is cut off. + period : :class:`list`, optional + The spatial periodicity of the field, is often the domain size. + modes : :class:`list`, optional + Externally calculated modes, if handed over, `mode_rel_cutoff` and + `period` are ignored. seed : :class:`int`, optional The seed of the random number generator. If "None", a random seed is used. Default: :any:`None` @@ -565,15 +571,14 @@ class Fourier(Generator): ----- The Fourier method is used to generate isotropic spatial random fields characterized by a given covariance model. - The calculation looks like [Hesse2014]_: # TODO check different source + The calculation looks like [Hesse2014]_: .. math:: u\left(x\right)= - \sqrt{2\sigma^{2}}\cdot - \sum_{i=1}^{N}\sqrt{E(k_{i})}\left( - Z_{1,i}\cdot\cos\left(2\pi\left\langle k_{i},x\right\rangle \right)+ - Z_{2,i}\cdot\sin\left(2\pi\left\langle k_{i},x\right\rangle \right) - \right) \sqrt{\Delta k} + \sum_{i=1}^{N}\sqrt{2S(k_{i})\Delta k}\left( + Z_{1,i}\cdot\cos\left(\left\langle k_{i},x\right\rangle \right)+ + Z_{2,i}\cdot\sin\left(\left\langle k_{i},x\right\rangle \right) + \right) where: @@ -592,19 +597,55 @@ class Fourier(Generator): def __init__( self, model, - modes, + mode_rel_cutoff=None, + period=None, + modes=None, seed=None, verbose=False, **kwargs, ): + # TODO update docstrings if kwargs: warnings.warn("gstools.Fourier: **kwargs are ignored") + if ( + mode_rel_cutoff is not None + and period is not None + and modes is not None + ): + warnings.warn( + "gstools.Fourier: mode_rel_cutoff & period are ignored, as " + "modes is provided." + ) + if mode_rel_cutoff is None and period is None and modes is None: + raise ValueError("Fourier: No mode information provided.") + + dim = model.dim + if ( + modes is None + and period is not None + and mode_rel_cutoff is not None + ): + if len(period) != dim: + raise ValueError("Fourier: Dimension mismatch.") + self._mode_rel_cutoff = mode_rel_cutoff + self._period = np.array(period) + self._delta_k = 2.0 * np.pi / self._period + modes_cutoff = self.calc_modes_cutoff(model, self._mode_rel_cutoff) + self._modes_cutoff = self._fill_to_dim(dim, modes_cutoff) + modes = [ + np.arange(0.0, self._modes_cutoff[d], self._delta_k[d]) + for d in range(dim) + ] + elif modes is not None: + self._delta_k = np.array( + [modes[d][1] - modes[d][0] for d in range(dim)] + ) + self._period = 2.0 * np.pi / self._delta_k + + # initialize attributes self._modes = generate_grid(modes) self._modes_no = [len(m) for m in modes] - self._delta_k = np.array( - [modes[d][1] - modes[d][0] for d in range(model.dim)] - ) self._verbose = bool(verbose) # initialize private attributes @@ -710,19 +751,19 @@ def update(self, model=None, seed=np.nan): isinstance(self._model, CovModel) and self._z_1 is not None and self._z_2 is not None - and self._spectral_density_sqrt is not None + and self._spectrum_factor is not None ): if self.verbose: - print("RandMeth.update: Nothing will be done...") + print("Fourier.update: Nothing will be done...") else: raise ValueError( - "gstools.field.generator.RandMeth: " + "gstools.field.generator.Fourier: " "neither 'model' nor 'seed' given!" ) # wrong model type else: raise ValueError( - "gstools.field.generator.RandMeth: 'model' is not an " + "gstools.field.generator.Fourier: 'model' is not an " "instance of 'gstools.CovModel'" ) @@ -754,7 +795,7 @@ def reset_seed(self, seed=np.nan): 2.0 * self._model.spectrum(k_norm) * np.prod(self._delta_k) ) - def _fill_to_dim(self, dim, values, dtype, default_value=None): + def _fill_to_dim(self, dim, values, dtype=float, default_value=None): """Fill an array with last element up to len(dim).""" r = values if values is None: @@ -770,6 +811,39 @@ def _fill_to_dim(self, dim, values, dtype, default_value=None): r = np.pad(r, (0, dim - len(r)), "edge") return r + def calc_modes_cutoff(self, model, mode_rel_cutoff): + """Find the cutoff value so that `mode_rel_cutoff`% of the spectrum is kept. + + This helper function uses a least squares algorithm to determine the + cutoff value so that `mode_rel_cutoff`% of the spectrum is kept. + + Parameters + ---------- + model : :any:`CovModel` + Covariance model + mode_rel_cutoff : :class:`float` + + Returns + ------- + :class:`float` + the cutoff value + """ + norm = model.spectral_density(0) + # the first len_scale is a good enough first guess + try: + len_scale = model.len_scale[0] + except IndexError: + len_scale = model.len_scale + k_cutoff0 = np.sqrt(1.0 / len_scale) + mode_rel_cutoff_r = 1.0 - mode_rel_cutoff + res = least_squares( + lambda k, mode_rel_cutoff_r: model.spectral_density(k) + - mode_rel_cutoff_r * norm, + k_cutoff0, + args=(mode_rel_cutoff_r,), + ) + return res.x[0] + @property def seed(self): """:class:`int`: Seed of the master RNG. From 1c755e4c9568998fa04ef62cbd0488a91dc82294 Mon Sep 17 00:00:00 2001 From: LSchueler Date: Wed, 29 May 2024 00:43:50 +0200 Subject: [PATCH 071/102] Update unittests --- tests/test_fouriergen.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/tests/test_fouriergen.py b/tests/test_fouriergen.py index 78e44052..4d2ce1b6 100644 --- a/tests/test_fouriergen.py +++ b/tests/test_fouriergen.py @@ -22,11 +22,14 @@ def setUp(self): self.y = np.linspace(0, L[1], 31) self.z = np.linspace(0, L[2], 13) + cutoff_rel=0.999 + cutoff_abs = 1 dk = [2 * np.pi / l for l in L] - self.modes_1d = [np.arange(0, 2, dk[0])] - self.modes_2d = self.modes_1d + [np.arange(0, 2, dk[1])] - self.modes_3d = self.modes_2d + [np.arange(0, 2, dk[2])] + self.modes_1d = [np.arange(0, cutoff_abs, dk[0])] + self.modes_2d = self.modes_1d + [np.arange(0, cutoff_abs, dk[1])] + self.modes_3d = self.modes_2d + [np.arange(0, cutoff_abs, dk[2])] + self.srf_1d = gs.SRF( self.cov_model_1d, @@ -43,21 +46,22 @@ def setUp(self): self.srf_3d = gs.SRF( self.cov_model_3d, generator="Fourier", - modes=self.modes_3d, + mode_rel_cutoff=cutoff_rel, + period=L, seed=self.seed, ) def test_1d(self): field = self.srf_1d((self.x,), mesh_type="structured") - self.assertAlmostEqual(field[0], 0.40939882638496783) + self.assertAlmostEqual(field[0], 0.40939877176695477) def test_2d(self): field = self.srf_2d((self.x, self.y), mesh_type="structured") - self.assertAlmostEqual(field[0, 0], 0.8176311251780369) + self.assertAlmostEqual(field[0, 0], 1.6338790313270515) def test_3d(self): field = self.srf_3d((self.x, self.y, self.z), mesh_type="structured") - self.assertAlmostEqual(field[0, 0, 0], -1.2636015063084773) + self.assertAlmostEqual(field[0, 0, 0], 0.2613561098408796) def test_periodicity(self): field = self.srf_2d((self.x, self.y), mesh_type="structured") @@ -71,3 +75,5 @@ def test_assertions(self): self.assertRaises( ValueError, self.srf_2d, (self.x, self.y), mesh_type="unstructured" ) + with self.assertRaises(ValueError): + gs.SRF(self.cov_model_2d, generator="Fourier") From 7c07f6e6475e7717098f1186d725639827617be0 Mon Sep 17 00:00:00 2001 From: LSchueler Date: Wed, 29 May 2024 00:44:02 +0200 Subject: [PATCH 072/102] Update examples --- examples/00_misc/06_fourier.py | 42 ++++++++++++++++++++++------ examples/00_misc/07_fourier_trans.py | 13 +++++---- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/examples/00_misc/06_fourier.py b/examples/00_misc/06_fourier.py index cf08148a..35289e7e 100644 --- a/examples/00_misc/06_fourier.py +++ b/examples/00_misc/06_fourier.py @@ -11,28 +11,52 @@ import gstools as gs # We start off by defining the spatial grid. -x = np.linspace(0, 500, 256) -y = np.linspace(0, 500, 128) +L = np.array((500, 500)) +x = np.linspace(0, L[0], 256) +y = np.linspace(0, L[1], 128) # And by setting up a Gaussian covariance model with a correlation length # scale which is roughly half the size of the grid. model = gs.Gaussian(dim=2, var=1, len_scale=200) # Next, we hand the cov. model to the spatial random field class -# and set the generator to `Fourier`. The higher the modes_no, the better -# the quality of the generated field, but also the computing time increases. -# The modes_truncation are the cut-off values of the Fourier modes and finally, -# the seed ensures that we generate the same random field each time. +# and set the generator to `Fourier`. We will let the class figure out the +# modes internally, by handing over `period` and `mode_rel_cutoff` which is the cutoff +# value of the spectral density, relative to the maximum spectral density at +# the origin. Simply put, we will use `mode_rel_cutoff`% of the spectral +# density for the calculations. The argument `period` is set to the domain +# size. srf = gs.SRF( model, generator="Fourier", - modes_no=[16, 8], - modes_truncation=[16, 8], + mode_rel_cutoff=0.99, + period=L, seed=1681903, ) -# Now, we can finally calculate the field with the given parameters. +# Now, we can calculate the field with the given parameters. srf((x, y), mesh_type='structured') # GSTools has a few simple visualization methods built in. srf.plot() + +# Alternatively, we could calculate the modes ourselves and hand them over to +# GSTools. Therefore, we set the cutoff values to absolut values in Fourier +# space. But always check, if you cover enough of the spectral density to not +# run into numerical problems. +modes_cutoff = [1., 1.] + +# Next, we have to compute the numerical step size in Fourier space. This choice +# influences the periodicity, which we want to set to the domain size by +modes_delta = 2 * np.pi / L + +# Now, we calculate the modes with +modes = [np.arange(0, modes_cutoff[d], modes_delta[d]) for d in 2] + +# And we can create a new instance of the SRF class with our own modes. +srf_modes = gs.SRF( + model, + generator="Fourier", + modes=modes, + seed=494754, +) diff --git a/examples/00_misc/07_fourier_trans.py b/examples/00_misc/07_fourier_trans.py index aea0cb43..0b9bc0ae 100644 --- a/examples/00_misc/07_fourier_trans.py +++ b/examples/00_misc/07_fourier_trans.py @@ -10,23 +10,24 @@ import gstools as gs # We start off by defining the spatial grid. -x = np.linspace(0, 500, 300) -y = np.linspace(0, 500, 200) +L = np.array((500, 500)) +x = np.linspace(0, L[0], 300) +y = np.linspace(0, L[1], 200) # Instead of using a Gaussian covariance model, we will use the much rougher # exponential model and we will introduce an anisotropy by using two different # length scales in the x- and y-axes model = gs.Exponential(dim=2, var=2, len_scale=[30, 20]) -# Very similar as before, setting up the spatial random field +# Same as before, we set up the spatial random field srf = gs.SRF( model, generator="Fourier", - modes_no=[30, 20], - modes_truncation=[30, 20], + mode_rel_cutoff=0.999, + period=L, seed=1681903, ) -# and computing it +# and compute it on our spatial domain srf((x, y), mesh_type='structured') # With the field generated, we can now apply transformations From 3474d0288df045763ce5b7221dbf03ce63b1fc16 Mon Sep 17 00:00:00 2001 From: LSchueler Date: Wed, 29 May 2024 00:44:58 +0200 Subject: [PATCH 073/102] Black --- src/gstools/field/generator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/gstools/field/generator.py b/src/gstools/field/generator.py index 5a485c2d..40ef7ee5 100755 --- a/src/gstools/field/generator.py +++ b/src/gstools/field/generator.py @@ -642,7 +642,6 @@ def __init__( ) self._period = 2.0 * np.pi / self._delta_k - # initialize attributes self._modes = generate_grid(modes) self._modes_no = [len(m) for m in modes] From 23e1b3ace4dcd644d1670369e8b79b69b337a3d0 Mon Sep 17 00:00:00 2001 From: LSchueler Date: Wed, 29 May 2024 00:47:20 +0200 Subject: [PATCH 074/102] Oh damn... also blacken tests --- tests/test_fouriergen.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_fouriergen.py b/tests/test_fouriergen.py index 4d2ce1b6..79ed4297 100644 --- a/tests/test_fouriergen.py +++ b/tests/test_fouriergen.py @@ -22,7 +22,7 @@ def setUp(self): self.y = np.linspace(0, L[1], 31) self.z = np.linspace(0, L[2], 13) - cutoff_rel=0.999 + cutoff_rel = 0.999 cutoff_abs = 1 dk = [2 * np.pi / l for l in L] @@ -30,7 +30,6 @@ def setUp(self): self.modes_2d = self.modes_1d + [np.arange(0, cutoff_abs, dk[1])] self.modes_3d = self.modes_2d + [np.arange(0, cutoff_abs, dk[2])] - self.srf_1d = gs.SRF( self.cov_model_1d, generator="Fourier", From c10d4c7ee4b8da23f27984108b44ad48a2ced0e0 Mon Sep 17 00:00:00 2001 From: LSchueler Date: Wed, 29 May 2024 00:49:08 +0200 Subject: [PATCH 075/102] And of course also black the examples --- examples/00_misc/06_fourier.py | 4 ++-- examples/00_misc/07_fourier_trans.py | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/00_misc/06_fourier.py b/examples/00_misc/06_fourier.py index 35289e7e..9fb04e19 100644 --- a/examples/00_misc/06_fourier.py +++ b/examples/00_misc/06_fourier.py @@ -35,7 +35,7 @@ ) # Now, we can calculate the field with the given parameters. -srf((x, y), mesh_type='structured') +srf((x, y), mesh_type="structured") # GSTools has a few simple visualization methods built in. srf.plot() @@ -44,7 +44,7 @@ # GSTools. Therefore, we set the cutoff values to absolut values in Fourier # space. But always check, if you cover enough of the spectral density to not # run into numerical problems. -modes_cutoff = [1., 1.] +modes_cutoff = [1.0, 1.0] # Next, we have to compute the numerical step size in Fourier space. This choice # influences the periodicity, which we want to set to the domain size by diff --git a/examples/00_misc/07_fourier_trans.py b/examples/00_misc/07_fourier_trans.py index 0b9bc0ae..f084c042 100644 --- a/examples/00_misc/07_fourier_trans.py +++ b/examples/00_misc/07_fourier_trans.py @@ -28,7 +28,7 @@ seed=1681903, ) # and compute it on our spatial domain -srf((x, y), mesh_type='structured') +srf((x, y), mesh_type="structured") # With the field generated, we can now apply transformations # starting with a discretization of the field into 4 different values @@ -38,5 +38,7 @@ # This is already a nice result, but we want to pronounce the peaks of the # field. We can do this by applying a log-normal transformation on top -srf.transform("lognormal", field="transform_discrete", store="transform_lognormal") +srf.transform( + "lognormal", field="transform_discrete", store="transform_lognormal" +) srf.plot("transform_lognormal") From 17013f97155f0164d987774e7f53281c6158d3f9 Mon Sep 17 00:00:00 2001 From: LSchueler Date: Wed, 29 May 2024 01:13:50 +0200 Subject: [PATCH 076/102] Isort --- examples/00_misc/06_fourier.py | 1 + examples/00_misc/07_fourier_trans.py | 1 + src/gstools/field/generator.py | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/00_misc/06_fourier.py b/examples/00_misc/06_fourier.py index 9fb04e19..e68d751a 100644 --- a/examples/00_misc/06_fourier.py +++ b/examples/00_misc/06_fourier.py @@ -8,6 +8,7 @@ """ import numpy as np + import gstools as gs # We start off by defining the spatial grid. diff --git a/examples/00_misc/07_fourier_trans.py b/examples/00_misc/07_fourier_trans.py index f084c042..f1d40be3 100644 --- a/examples/00_misc/07_fourier_trans.py +++ b/examples/00_misc/07_fourier_trans.py @@ -7,6 +7,7 @@ """ import numpy as np + import gstools as gs # We start off by defining the spatial grid. diff --git a/src/gstools/field/generator.py b/src/gstools/field/generator.py index 40ef7ee5..88e1d52b 100755 --- a/src/gstools/field/generator.py +++ b/src/gstools/field/generator.py @@ -19,8 +19,8 @@ from abc import ABC, abstractmethod from copy import deepcopy as dcp -from scipy.optimize import least_squares import numpy as np +from scipy.optimize import least_squares from gstools import config from gstools.covmodel.base import CovModel From a197eac4e1cc2b7b63666d4c96497ceba83f2c91 Mon Sep 17 00:00:00 2001 From: LSchueler Date: Wed, 29 May 2024 01:14:45 +0200 Subject: [PATCH 077/102] Fix test --- tests/test_fouriergen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_fouriergen.py b/tests/test_fouriergen.py index 79ed4297..0a1c3cb9 100644 --- a/tests/test_fouriergen.py +++ b/tests/test_fouriergen.py @@ -60,7 +60,7 @@ def test_2d(self): def test_3d(self): field = self.srf_3d((self.x, self.y, self.z), mesh_type="structured") - self.assertAlmostEqual(field[0, 0, 0], 0.2613561098408796) + self.assertAlmostEqual(field[0, 0, 0], 0.3866689424599251) def test_periodicity(self): field = self.srf_2d((self.x, self.y), mesh_type="structured") From e33bcf6465f00396aec054797688eaf61f557e69 Mon Sep 17 00:00:00 2001 From: LSchueler Date: Wed, 29 May 2024 11:06:16 +0200 Subject: [PATCH 078/102] Fix example --- examples/00_misc/06_fourier.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/00_misc/06_fourier.py b/examples/00_misc/06_fourier.py index e68d751a..5dc1c608 100644 --- a/examples/00_misc/06_fourier.py +++ b/examples/00_misc/06_fourier.py @@ -52,7 +52,7 @@ modes_delta = 2 * np.pi / L # Now, we calculate the modes with -modes = [np.arange(0, modes_cutoff[d], modes_delta[d]) for d in 2] +modes = [np.arange(0, modes_cutoff[d], modes_delta[d]) for d in range(2)] # And we can create a new instance of the SRF class with our own modes. srf_modes = gs.SRF( From 6449f928248fabc7950575984d3a39af9eaeab0e Mon Sep 17 00:00:00 2001 From: LSchueler Date: Wed, 29 May 2024 11:11:38 +0200 Subject: [PATCH 079/102] Delinting --- src/gstools/field/generator.py | 12 ++++++++---- src/gstools/field/srf.py | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/gstools/field/generator.py b/src/gstools/field/generator.py index 88e1d52b..c6ccd844 100755 --- a/src/gstools/field/generator.py +++ b/src/gstools/field/generator.py @@ -30,6 +30,7 @@ if config.USE_RUST: # pragma: no cover # pylint: disable=E0401 from gstools_core import summate, summate_incompr + from gstools.field.summator import summate_fourier else: from gstools.field.summator import ( summate, @@ -604,7 +605,6 @@ def __init__( verbose=False, **kwargs, ): - # TODO update docstrings if kwargs: warnings.warn("gstools.Fourier: **kwargs are ignored") if ( @@ -794,12 +794,14 @@ def reset_seed(self, seed=np.nan): 2.0 * self._model.spectrum(k_norm) * np.prod(self._delta_k) ) - def _fill_to_dim(self, dim, values, dtype=float, default_value=None): + def _fill_to_dim( + self, dim, values, dtype=float, default_value=None + ): # pylint: disable=R6301 """Fill an array with last element up to len(dim).""" r = values if values is None: if default_value is None: - raise ValueError(f"Fourier: Value has to be provided") + raise ValueError("Fourier: Value has to be provided") r = default_value r = np.array(r, dtype=dtype) r = np.atleast_1d(r)[:dim] @@ -810,7 +812,9 @@ def _fill_to_dim(self, dim, values, dtype=float, default_value=None): r = np.pad(r, (0, dim - len(r)), "edge") return r - def calc_modes_cutoff(self, model, mode_rel_cutoff): + def calc_modes_cutoff( + self, model, mode_rel_cutoff + ): # pylint: disable=R6301 """Find the cutoff value so that `mode_rel_cutoff`% of the spectrum is kept. This helper function uses a least squares algorithm to determine the diff --git a/src/gstools/field/srf.py b/src/gstools/field/srf.py index 5af15aeb..c8488b24 100755 --- a/src/gstools/field/srf.py +++ b/src/gstools/field/srf.py @@ -151,7 +151,7 @@ def __call__( """ if isinstance(self.generator, Fourier) and mesh_type != "structured": raise ValueError( - f"SRF: Fourier generator only defined for " + "SRF: Fourier generator only defined for " 'mesh_type == "structured".' ) name, save = self.get_store_config(store) From 15bd8c1e737fd9d8ac9082aac7d6fa539b7907f7 Mon Sep 17 00:00:00 2001 From: LSchueler Date: Wed, 29 May 2024 12:01:00 +0200 Subject: [PATCH 080/102] Isort --- src/gstools/field/generator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gstools/field/generator.py b/src/gstools/field/generator.py index c6ccd844..a1c12c00 100755 --- a/src/gstools/field/generator.py +++ b/src/gstools/field/generator.py @@ -30,6 +30,7 @@ if config.USE_RUST: # pragma: no cover # pylint: disable=E0401 from gstools_core import summate, summate_incompr + from gstools.field.summator import summate_fourier else: from gstools.field.summator import ( From a76d6abfc508e264c67bbf29415522bf995a2243 Mon Sep 17 00:00:00 2001 From: LSchueler Date: Wed, 29 May 2024 18:11:17 +0200 Subject: [PATCH 081/102] Fix formatting problem --- src/gstools/field/summator.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gstools/field/summator.pyx b/src/gstools/field/summator.pyx index bfd91c61..d7a03d7f 100755 --- a/src/gstools/field/summator.pyx +++ b/src/gstools/field/summator.pyx @@ -106,7 +106,7 @@ def summate_fourier( const double[:] z_2, const double[:, :] pos, num_threads=None, - ): +): cdef int i, j, d cdef double phase cdef int dim = pos.shape[0] From 2c05f2fae37c4ce5f1ee7caaec2dcada2a07fbec Mon Sep 17 00:00:00 2001 From: LSchueler Date: Wed, 29 May 2024 18:15:03 +0200 Subject: [PATCH 082/102] Deactivate Cython lint & coverage, both broken atm --- .github/workflows/main.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f8dc74cd..1bd648d8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -48,9 +48,9 @@ jobs: run: | python -m pylint src/gstools/ - - name: cython-lint check - run: | - cython-lint src/gstools/ + #- name: cython-lint check + #run: | + #cython-lint src/gstools/ build_wheels: name: wheels for ${{ matrix.cfg.os }} / ${{ matrix.cfg.arch }} @@ -123,7 +123,7 @@ jobs: run: | pip install "numpy${{ matrix.ver.np }}" "scipy${{ matrix.ver.sp }}" python -m pytest --cov gstools --cov-report term-missing -v tests/ - python -m coveralls --service=github + #python -m coveralls --service=github - name: Build sdist run: | From 456611bd40336b1c2c34b46bca4c06d401485aab Mon Sep 17 00:00:00 2001 From: LSchueler Date: Fri, 31 May 2024 12:29:52 +0200 Subject: [PATCH 083/102] Fix factor --- src/gstools/field/generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gstools/field/generator.py b/src/gstools/field/generator.py index a1c12c00..fea5bc22 100755 --- a/src/gstools/field/generator.py +++ b/src/gstools/field/generator.py @@ -792,7 +792,7 @@ def reset_seed(self, seed=np.nan): # Cython, which doesn't have access to the CovModel k_norm = np.linalg.norm(self._modes, axis=0) self._spectrum_factor = np.sqrt( - 2.0 * self._model.spectrum(k_norm) * np.prod(self._delta_k) + 4.0 * self._model.spectrum(k_norm) * np.prod(self._delta_k) ) def _fill_to_dim( From afad1914f1b16a1afdf3f8b5b51aaa50243845a8 Mon Sep 17 00:00:00 2001 From: LSchueler Date: Thu, 6 Jun 2024 21:46:07 +0200 Subject: [PATCH 084/102] Fix mode generation & variance --- src/gstools/field/generator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/gstools/field/generator.py b/src/gstools/field/generator.py index fea5bc22..c6c74aee 100755 --- a/src/gstools/field/generator.py +++ b/src/gstools/field/generator.py @@ -1,4 +1,4 @@ -""" +"""gene GStools subpackage providing generators for spatial random fields. .. currentmodule:: gstools.field.generator @@ -634,7 +634,7 @@ def __init__( modes_cutoff = self.calc_modes_cutoff(model, self._mode_rel_cutoff) self._modes_cutoff = self._fill_to_dim(dim, modes_cutoff) modes = [ - np.arange(0.0, self._modes_cutoff[d], self._delta_k[d]) + np.arange(-self._modes_cutoff[d], self._modes_cutoff[d], self._delta_k[d]) for d in range(dim) ] elif modes is not None: @@ -792,7 +792,7 @@ def reset_seed(self, seed=np.nan): # Cython, which doesn't have access to the CovModel k_norm = np.linalg.norm(self._modes, axis=0) self._spectrum_factor = np.sqrt( - 4.0 * self._model.spectrum(k_norm) * np.prod(self._delta_k) + self._model.spectrum(k_norm) * np.prod(self._delta_k) ) def _fill_to_dim( From db7444e6577441143c8064fe4bcd731959634446 Mon Sep 17 00:00:00 2001 From: LSchueler Date: Fri, 14 Jun 2024 12:43:32 +0200 Subject: [PATCH 085/102] Fix period. in Fourier gen, update tests, examples --- examples/00_misc/06_fourier.py | 31 +------- examples/00_misc/07_fourier_trans.py | 2 +- src/gstools/field/generator.py | 101 ++++++++------------------- tests/test_fouriergen.py | 65 +++++++++++------ 4 files changed, 77 insertions(+), 122 deletions(-) mode change 100644 => 100755 tests/test_fouriergen.py diff --git a/examples/00_misc/06_fourier.py b/examples/00_misc/06_fourier.py index 5dc1c608..360f4708 100644 --- a/examples/00_misc/06_fourier.py +++ b/examples/00_misc/06_fourier.py @@ -21,16 +21,12 @@ model = gs.Gaussian(dim=2, var=1, len_scale=200) # Next, we hand the cov. model to the spatial random field class -# and set the generator to `Fourier`. We will let the class figure out the -# modes internally, by handing over `period` and `mode_rel_cutoff` which is the cutoff -# value of the spectral density, relative to the maximum spectral density at -# the origin. Simply put, we will use `mode_rel_cutoff`% of the spectral -# density for the calculations. The argument `period` is set to the domain -# size. +# and set the generator to `Fourier`. The `mode_no` argument sets the number of +# Fourier modes per dimension. The argument `period` is set to the domain size. srf = gs.SRF( model, generator="Fourier", - mode_rel_cutoff=0.99, + mode_no=[32, 32], period=L, seed=1681903, ) @@ -40,24 +36,3 @@ # GSTools has a few simple visualization methods built in. srf.plot() - -# Alternatively, we could calculate the modes ourselves and hand them over to -# GSTools. Therefore, we set the cutoff values to absolut values in Fourier -# space. But always check, if you cover enough of the spectral density to not -# run into numerical problems. -modes_cutoff = [1.0, 1.0] - -# Next, we have to compute the numerical step size in Fourier space. This choice -# influences the periodicity, which we want to set to the domain size by -modes_delta = 2 * np.pi / L - -# Now, we calculate the modes with -modes = [np.arange(0, modes_cutoff[d], modes_delta[d]) for d in range(2)] - -# And we can create a new instance of the SRF class with our own modes. -srf_modes = gs.SRF( - model, - generator="Fourier", - modes=modes, - seed=494754, -) diff --git a/examples/00_misc/07_fourier_trans.py b/examples/00_misc/07_fourier_trans.py index f1d40be3..8331f766 100644 --- a/examples/00_misc/07_fourier_trans.py +++ b/examples/00_misc/07_fourier_trans.py @@ -24,7 +24,7 @@ srf = gs.SRF( model, generator="Fourier", - mode_rel_cutoff=0.999, + mode_no=[32, 32], period=L, seed=1681903, ) diff --git a/src/gstools/field/generator.py b/src/gstools/field/generator.py index c6c74aee..712fbece 100755 --- a/src/gstools/field/generator.py +++ b/src/gstools/field/generator.py @@ -599,49 +599,35 @@ class Fourier(Generator): def __init__( self, model, - mode_rel_cutoff=None, - period=None, - modes=None, + mode_no, + period, seed=None, verbose=False, **kwargs, ): if kwargs: warnings.warn("gstools.Fourier: **kwargs are ignored") - if ( - mode_rel_cutoff is not None - and period is not None - and modes is not None - ): - warnings.warn( - "gstools.Fourier: mode_rel_cutoff & period are ignored, as " - "modes is provided." - ) - if mode_rel_cutoff is None and period is None and modes is None: - raise ValueError("Fourier: No mode information provided.") - dim = model.dim - if ( - modes is None - and period is not None - and mode_rel_cutoff is not None - ): - if len(period) != dim: - raise ValueError("Fourier: Dimension mismatch.") - self._mode_rel_cutoff = mode_rel_cutoff - self._period = np.array(period) - self._delta_k = 2.0 * np.pi / self._period - modes_cutoff = self.calc_modes_cutoff(model, self._mode_rel_cutoff) - self._modes_cutoff = self._fill_to_dim(dim, modes_cutoff) - modes = [ - np.arange(-self._modes_cutoff[d], self._modes_cutoff[d], self._delta_k[d]) - for d in range(dim) - ] - elif modes is not None: - self._delta_k = np.array( - [modes[d][1] - modes[d][0] for d in range(dim)] + if len(mode_no) != dim: + raise ValueError( + "Fourier: Dimension mismatch in argument mode_no." + ) + if len(period) != dim: + raise ValueError("Fourier: Dimension mismatch in argument period.") + if (np.asarray([m % 2 for m in mode_no]) != 0).any(): + raise ValueError("Fourier: Odd mode_no not supported.") + + self._period = np.array(period) + self._delta_k = 2.0 * np.pi / self._period + anis = np.insert(model.anis.copy(), 0, 1.0) + modes = [ + np.arange( + -mode_no[d] / 2.0 * self._delta_k[d] / anis[d], + mode_no[d] / 2.0 * self._delta_k[d] / anis[d], + self._delta_k[d], ) - self._period = 2.0 * np.pi / self._delta_k + for d in range(dim) + ] # initialize attributes self._modes = generate_grid(modes) @@ -813,41 +799,6 @@ def _fill_to_dim( r = np.pad(r, (0, dim - len(r)), "edge") return r - def calc_modes_cutoff( - self, model, mode_rel_cutoff - ): # pylint: disable=R6301 - """Find the cutoff value so that `mode_rel_cutoff`% of the spectrum is kept. - - This helper function uses a least squares algorithm to determine the - cutoff value so that `mode_rel_cutoff`% of the spectrum is kept. - - Parameters - ---------- - model : :any:`CovModel` - Covariance model - mode_rel_cutoff : :class:`float` - - Returns - ------- - :class:`float` - the cutoff value - """ - norm = model.spectral_density(0) - # the first len_scale is a good enough first guess - try: - len_scale = model.len_scale[0] - except IndexError: - len_scale = model.len_scale - k_cutoff0 = np.sqrt(1.0 / len_scale) - mode_rel_cutoff_r = 1.0 - mode_rel_cutoff - res = least_squares( - lambda k, mode_rel_cutoff_r: model.spectral_density(k) - - mode_rel_cutoff_r * norm, - k_cutoff0, - args=(mode_rel_cutoff_r,), - ) - return res.x[0] - @property def seed(self): """:class:`int`: Seed of the master RNG. @@ -873,6 +824,16 @@ def model(self): def model(self, model): self.update(model) + @property + def modes(self): + """:class:`numpy.ndarray`: Modes on which the spectrum is calculated.""" + return self._modes + + @property + def period(self): + """:class:`numpy.ndarray`: Period length of the spatial random field.""" + return self._period + @property def verbose(self): """:class:`bool`: Verbosity of the generator.""" diff --git a/tests/test_fouriergen.py b/tests/test_fouriergen.py old mode 100644 new mode 100755 index 0a1c3cb9..23ea7267 --- a/tests/test_fouriergen.py +++ b/tests/test_fouriergen.py @@ -2,13 +2,11 @@ This is the unittest of the Fourier class. """ -import copy import unittest import numpy as np import gstools as gs -from gstools.field.generator import Fourier class TestFourier(unittest.TestCase): @@ -17,56 +15,70 @@ def setUp(self): self.cov_model_1d = gs.Gaussian(dim=1, var=0.5, len_scale=10.0) self.cov_model_2d = gs.Gaussian(dim=2, var=2.0, len_scale=30.0) self.cov_model_3d = gs.Gaussian(dim=3, var=2.1, len_scale=21.0) - L = [80, 30, 91] - self.x = np.linspace(0, L[0], 11) - self.y = np.linspace(0, L[1], 31) - self.z = np.linspace(0, L[2], 13) + self.L = [80, 30, 91] + self.x = np.linspace(0, self.L[0], 11) + self.y = np.linspace(0, self.L[1], 31) + self.z = np.linspace(0, self.L[2], 13) - cutoff_rel = 0.999 - cutoff_abs = 1 - dk = [2 * np.pi / l for l in L] - - self.modes_1d = [np.arange(0, cutoff_abs, dk[0])] - self.modes_2d = self.modes_1d + [np.arange(0, cutoff_abs, dk[1])] - self.modes_3d = self.modes_2d + [np.arange(0, cutoff_abs, dk[2])] + self.mode_no = [12, 6, 14] self.srf_1d = gs.SRF( self.cov_model_1d, generator="Fourier", - modes=self.modes_1d, + mode_no=[self.mode_no[0]], + period=[self.L[0]], seed=self.seed, ) self.srf_2d = gs.SRF( self.cov_model_2d, generator="Fourier", - modes=self.modes_2d, + mode_no=self.mode_no[:2], + period=self.L[:2], seed=self.seed, ) self.srf_3d = gs.SRF( self.cov_model_3d, generator="Fourier", - mode_rel_cutoff=cutoff_rel, - period=L, + mode_no=self.mode_no, + period=self.L, seed=self.seed, ) def test_1d(self): field = self.srf_1d((self.x,), mesh_type="structured") - self.assertAlmostEqual(field[0], 0.40939877176695477) + self.assertAlmostEqual(field[0], 0.6236929351309081) def test_2d(self): field = self.srf_2d((self.x, self.y), mesh_type="structured") - self.assertAlmostEqual(field[0, 0], 1.6338790313270515) + self.assertAlmostEqual(field[0, 0], -0.1431996611581266) def test_3d(self): field = self.srf_3d((self.x, self.y, self.z), mesh_type="structured") - self.assertAlmostEqual(field[0, 0, 0], 0.3866689424599251) + self.assertAlmostEqual(field[0, 0, 0], -1.0433325279452803) + + def test_periodicity_1d(self): + field = self.srf_1d((self.x,), mesh_type="structured") + self.assertAlmostEqual(field[0], field[-1]) - def test_periodicity(self): + def test_periodicity_2d(self): field = self.srf_2d((self.x, self.y), mesh_type="structured") self.assertAlmostEqual( field[0, len(self.y) // 2], field[-1, len(self.y) // 2] ) + self.assertAlmostEqual( + field[len(self.x) // 2, 0], field[len(self.x) // 2, -1] + ) + + def test_periodicity_3d(self): + field = self.srf_3d((self.x, self.y, self.z), mesh_type="structured") + self.assertAlmostEqual( + field[0, len(self.y) // 2, 0], field[-1, len(self.y) // 2, 0] + ) + self.assertAlmostEqual(field[0, 0, 0], field[0, -1, 0]) + self.assertAlmostEqual( + field[len(self.x) // 2, len(self.y) // 2, 0], + field[len(self.x) // 2, len(self.y) // 2, -1], + ) def test_assertions(self): # unstructured grids not supported @@ -74,5 +86,12 @@ def test_assertions(self): self.assertRaises( ValueError, self.srf_2d, (self.x, self.y), mesh_type="unstructured" ) - with self.assertRaises(ValueError): - gs.SRF(self.cov_model_2d, generator="Fourier") + self.assertRaises( + ValueError, + gs.SRF, + self.cov_model_2d, + generator="Fourier", + mode_no=[13, 50], + period=self.L[:2], + seed=self.seed, + ) From 5ac2d99988d0c160ff57d5c2b6a7cee1aa439874 Mon Sep 17 00:00:00 2001 From: LSchueler Date: Fri, 14 Jun 2024 14:03:00 +0200 Subject: [PATCH 086/102] Remove unused import --- src/gstools/field/generator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/gstools/field/generator.py b/src/gstools/field/generator.py index 712fbece..620a2f83 100755 --- a/src/gstools/field/generator.py +++ b/src/gstools/field/generator.py @@ -20,7 +20,6 @@ from copy import deepcopy as dcp import numpy as np -from scipy.optimize import least_squares from gstools import config from gstools.covmodel.base import CovModel From 6e09342a79a2506f49272b4e768f7af2b8acea35 Mon Sep 17 00:00:00 2001 From: LSchueler Date: Tue, 2 Jul 2024 00:41:32 +0200 Subject: [PATCH 087/102] Let's be more inclusive --- src/gstools/field/generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gstools/field/generator.py b/src/gstools/field/generator.py index 620a2f83..d2b0bb07 100755 --- a/src/gstools/field/generator.py +++ b/src/gstools/field/generator.py @@ -279,7 +279,7 @@ def update(self, model=None, seed=np.nan): raise ValueError( "gstools.field.generator.RandMeth: no 'model' given" ) - # if the user tries to trick us, we beat him! + # if the user tries to trick us, we beat them! elif model is None and np.isnan(seed): if not ( isinstance(self._model, CovModel) From 6960bee81c5b509d7187ebaf5526b109ef71f51a Mon Sep 17 00:00:00 2001 From: LSchueler Date: Tue, 2 Jul 2024 00:42:20 +0200 Subject: [PATCH 088/102] Add setters, some cleanup --- src/gstools/field/generator.py | 121 ++++++++++++++++++++++----------- 1 file changed, 82 insertions(+), 39 deletions(-) diff --git a/src/gstools/field/generator.py b/src/gstools/field/generator.py index d2b0bb07..52de8360 100755 --- a/src/gstools/field/generator.py +++ b/src/gstools/field/generator.py @@ -549,15 +549,10 @@ class Fourier(Generator): ---------- model : :any:`CovModel` Covariance model - mode_rel_cutoff : :class:`float`, optional - The cutoff value, relative to the spectral density's maximum, e.g. a - value of 0.99 means that the function falls to 1% of its max. value - before it is cut off. + mode_no : :class:`list` + Number of Fourier modes per dimension. period : :class:`list`, optional The spatial periodicity of the field, is often the domain size. - modes : :class:`list`, optional - Externally calculated modes, if handed over, `mode_rel_cutoff` and - `period` are ignored. seed : :class:`int`, optional The seed of the random number generator. If "None", a random seed is used. Default: :any:`None` @@ -606,34 +601,13 @@ def __init__( ): if kwargs: warnings.warn("gstools.Fourier: **kwargs are ignored") - dim = model.dim - if len(mode_no) != dim: - raise ValueError( - "Fourier: Dimension mismatch in argument mode_no." - ) - if len(period) != dim: - raise ValueError("Fourier: Dimension mismatch in argument period.") - if (np.asarray([m % 2 for m in mode_no]) != 0).any(): - raise ValueError("Fourier: Odd mode_no not supported.") - - self._period = np.array(period) - self._delta_k = 2.0 * np.pi / self._period - anis = np.insert(model.anis.copy(), 0, 1.0) - modes = [ - np.arange( - -mode_no[d] / 2.0 * self._delta_k[d] / anis[d], - mode_no[d] / 2.0 * self._delta_k[d] / anis[d], - self._delta_k[d], - ) - for d in range(dim) - ] - - # initialize attributes - self._modes = generate_grid(modes) - self._modes_no = [len(m) for m in modes] self._verbose = bool(verbose) # initialize private attributes + self._modes = None + self._mode_no = None + self._period = None + self._delta_k = None self._model = None self._seed = None self._rng = None @@ -642,13 +616,13 @@ def __init__( self._spectrum_factor = None self._value_type = "scalar" # set model and seed - self.update(model, seed) + self.update(model, seed, mode_no, period) def __call__(self, pos, add_nugget=True): """Calculate the modes for the Fourier method. - This method calls the `summate_*` Cython methods, which are the - heart of the randomization method. + This method calls the `summate_fourier` Cython method, which is the + heart of the Fourier method. Parameters ---------- @@ -697,7 +671,7 @@ def get_nugget(self, shape): nugget = 0.0 return nugget - def update(self, model=None, seed=np.nan): + def update(self, model=None, seed=np.nan, mode_no=None, period=None): """Update the model and the seed. If model and seed are not different, nothing will be done. @@ -710,7 +684,30 @@ def update(self, model=None, seed=np.nan): the seed of the random number generator. If :any:`None`, a random seed is used. If :any:`numpy.nan`, the actual seed will be kept. Default: :any:`numpy.nan` + mode_no : :class:`list` or :any:`None`, optional + Number of Fourier modes per dimension. + period : :class:`list` or :any:`None, optional + The spatial periodicity of the field, is often the domain size. """ + tmp_model = model if model is not None else self._model + if period is not None: + if len(period) != tmp_model.dim: + raise ValueError( + "Fourier: Dimension mismatch in argument period." + ) + self._period = np.array(period) + self._delta_k = 2.0 * np.pi / self._period + if mode_no is None: + self._set_modes(self._mode_no, tmp_model) + if mode_no is not None: + if len(mode_no) != tmp_model.dim: + raise ValueError( + "Fourier: Dimension mismatch in argument mode_no." + ) + if (np.asarray([m % 2 for m in mode_no]) != 0).any(): + raise ValueError("Fourier: Odd mode_no not supported.") + self._set_modes(mode_no, tmp_model) + # check if a new model is given if isinstance(model, CovModel): if self.model != model: @@ -730,7 +727,13 @@ def update(self, model=None, seed=np.nan): raise ValueError( "gstools.field.generator.RandMeth: no 'model' given" ) - # if the user tries to trick us, we beat him! + # but also update when mode mesh was modified + elif mode_no is not None or period is not None: + if seed is None or not np.isnan(seed): + self.reset_seed(seed) + else: + self.reset_seed(self._seed) + # if the user tries to trick us, we beat them! elif model is None and np.isnan(seed): if ( isinstance(self._model, CovModel) @@ -771,8 +774,8 @@ def reset_seed(self, seed=np.nan): self._seed = seed self._rng = RNG(self._seed) # normal distributed samples for randmeth - self._z_1 = self._rng.random.normal(size=np.prod(self._modes_no)) - self._z_2 = self._rng.random.normal(size=np.prod(self._modes_no)) + self._z_1 = self._rng.random.normal(size=np.prod(self._mode_no)) + self._z_2 = self._rng.random.normal(size=np.prod(self._mode_no)) # pre calc. the spectrum for all wave numbers they are handed over to # Cython, which doesn't have access to the CovModel k_norm = np.linalg.norm(self._modes, axis=0) @@ -798,6 +801,33 @@ def _fill_to_dim( r = np.pad(r, (0, dim - len(r)), "edge") return r + def _set_modes(self, mode_no, model): + """Calculate the mode mesh. + + Parameters + ---------- + mode_no : :class:`list` + Number of Fourier modes per dimension. + model : :any:`CovModel` or :any:`None`, optional + covariance model. Default: :any:`None` + + Notes + ----- + `self._reset_seed` *has* to be called after this method! + """ + anis = np.insert(model.anis.copy(), 0, 1.0) + modes = [ + np.arange( + -mode_no[d] / 2.0 * self._delta_k[d] / anis[d], + mode_no[d] / 2.0 * self._delta_k[d] / anis[d], + self._delta_k[d], + ) + for d in range(model.dim) + ] + # initialize attributes + self._modes = generate_grid(modes) + self._mode_no = [len(m) for m in modes] + @property def seed(self): """:class:`int`: Seed of the master RNG. @@ -828,11 +858,24 @@ def modes(self): """:class:`numpy.ndarray`: Modes on which the spectrum is calculated.""" return self._modes + @property + def mode_no(self): + """:class:`numpy.ndarray`: Number of modes per dimension.""" + return self._mode_no + + @mode_no.setter + def mode_no(self, mode_no): + self.update(mode_no=mode_no) + @property def period(self): """:class:`numpy.ndarray`: Period length of the spatial random field.""" return self._period + @period.setter + def period(self, period): + self.update(period=period) + @property def verbose(self): """:class:`bool`: Verbosity of the generator.""" From d4370da5f16b078535937c728b379b6ea7f15bda Mon Sep 17 00:00:00 2001 From: LSchueler Date: Tue, 2 Jul 2024 00:42:45 +0200 Subject: [PATCH 089/102] Add setter tests --- tests/test_fouriergen.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_fouriergen.py b/tests/test_fouriergen.py index 23ea7267..093b47d5 100755 --- a/tests/test_fouriergen.py +++ b/tests/test_fouriergen.py @@ -80,6 +80,20 @@ def test_periodicity_3d(self): field[len(self.x) // 2, len(self.y) // 2, -1], ) + def test_setters(self): + new_period = [5, 10] + self.srf_2d.generator.period = new_period + np.testing.assert_almost_equal( + self.srf_2d.generator.period, + np.array(new_period), + ) + new_mode_no = [6, 6] + self.srf_2d.generator.mode_no = new_mode_no + np.testing.assert_almost_equal( + self.srf_2d.generator.mode_no, + np.array(new_mode_no), + ) + def test_assertions(self): # unstructured grids not supported self.assertRaises(ValueError, self.srf_2d, (self.x, self.y)) From 6a12024aadaf8f7bfb9894cc5313cf90ca8a30e7 Mon Sep 17 00:00:00 2001 From: LSchueler Date: Tue, 2 Jul 2024 12:30:41 +0200 Subject: [PATCH 090/102] Fix docstring --- src/gstools/field/generator.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/gstools/field/generator.py b/src/gstools/field/generator.py index 52de8360..3ceb0117 100755 --- a/src/gstools/field/generator.py +++ b/src/gstools/field/generator.py @@ -543,7 +543,7 @@ def _create_unit_vector(self, broadcast_shape, axis=0): class Fourier(Generator): - r"""Fourier method for calculating isotropic random fields. + r"""Fourier method for calculating periodic, isotropic random fields. Parameters ---------- @@ -565,9 +565,9 @@ class Fourier(Generator): Notes ----- - The Fourier method is used to generate isotropic - spatial random fields characterized by a given covariance model. - The calculation looks like [Hesse2014]_: + The Fourier method is used to generate periodic isotropic spatial random + fields characterized by a given covariance model. + The calculation looks like: .. math:: u\left(x\right)= @@ -578,16 +578,9 @@ class Fourier(Generator): where: - * :math:`N` : fourier mode number + * :math:`S` : spectrum of the covariance model * :math:`Z_{j,i}` : random samples from a normal distribution - * :math:`k_i` : the equidistant spectral density of the covariance model - - References - ---------- - .. [Hesse2014] Heße, F., Prykhodko, V., Schlüter, S., and Attinger, S., - "Generating random fields with a truncated power-law variogram: - A comparison of several numerical methods", - Environmental Modelling & Software, 55, 32-48., (2014) + * :math:`k_i` : the equidistant Fourier grid """ def __init__( From 38b6c0d3abb6607e42c4725575c4afd7424fc172 Mon Sep 17 00:00:00 2001 From: LSchueler Date: Tue, 2 Jul 2024 14:50:28 +0200 Subject: [PATCH 091/102] Fix docstring --- src/gstools/field/generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gstools/field/generator.py b/src/gstools/field/generator.py index 3ceb0117..0eecea70 100755 --- a/src/gstools/field/generator.py +++ b/src/gstools/field/generator.py @@ -1,4 +1,4 @@ -"""gene +""" GStools subpackage providing generators for spatial random fields. .. currentmodule:: gstools.field.generator From 7397bc64fe9650ab76f9e0cb98f1649bd198a9b8 Mon Sep 17 00:00:00 2001 From: LSchueler Date: Tue, 2 Jul 2024 14:50:45 +0200 Subject: [PATCH 092/102] Remove keyword `verbose` --- src/gstools/field/generator.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/gstools/field/generator.py b/src/gstools/field/generator.py index 0eecea70..f5ff09f7 100755 --- a/src/gstools/field/generator.py +++ b/src/gstools/field/generator.py @@ -556,9 +556,6 @@ class Fourier(Generator): seed : :class:`int`, optional The seed of the random number generator. If "None", a random seed is used. Default: :any:`None` - verbose : :class:`bool`, optional - Be chatty during the generation. - Default: :any:`False` **kwargs Placeholder for keyword-args @@ -589,13 +586,11 @@ def __init__( mode_no, period, seed=None, - verbose=False, **kwargs, ): if kwargs: warnings.warn("gstools.Fourier: **kwargs are ignored") - self._verbose = bool(verbose) # initialize private attributes self._modes = None self._mode_no = None @@ -734,9 +729,6 @@ def update(self, model=None, seed=np.nan, mode_no=None, period=None): and self._z_2 is not None and self._spectrum_factor is not None ): - if self.verbose: - print("Fourier.update: Nothing will be done...") - else: raise ValueError( "gstools.field.generator.Fourier: " "neither 'model' nor 'seed' given!" @@ -869,15 +861,6 @@ def period(self): def period(self, period): self.update(period=period) - @property - def verbose(self): - """:class:`bool`: Verbosity of the generator.""" - return self._verbose - - @verbose.setter - def verbose(self, verbose): - self._verbose = bool(verbose) - @property def value_type(self): """:class:`str`: Type of the field values (scalar, vector).""" From 59952a864b0fdb777086b92e9000cbbfc648b42f Mon Sep 17 00:00:00 2001 From: LSchueler Date: Tue, 2 Jul 2024 16:25:11 +0200 Subject: [PATCH 093/102] Use GSTools-Core for Fourier meth., if USE_RUST --- src/gstools/field/generator.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/gstools/field/generator.py b/src/gstools/field/generator.py index f5ff09f7..b52607a3 100755 --- a/src/gstools/field/generator.py +++ b/src/gstools/field/generator.py @@ -28,9 +28,7 @@ if config.USE_RUST: # pragma: no cover # pylint: disable=E0401 - from gstools_core import summate, summate_incompr - - from gstools.field.summator import summate_fourier + from gstools_core import summate, summate_fourier, summate_incompr else: from gstools.field.summator import ( summate, From fa4dbbf49ab9de4e93824af16d3cdc30b514f02d Mon Sep 17 00:00:00 2001 From: LSchueler Date: Wed, 3 Jul 2024 12:05:26 +0200 Subject: [PATCH 094/102] Turn coveralls back on again --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 71a03a1a..a3031e6a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -124,7 +124,7 @@ jobs: run: | pip install "numpy${{ matrix.ver.np }}" "scipy${{ matrix.ver.sp }}" python -m pytest --cov gstools --cov-report term-missing -v tests/ - #python -m coveralls --service=github + python -m coveralls --service=github - name: Build sdist run: | From c3ec1e28be3a733740036dee81cbcae371f9349f Mon Sep 17 00:00:00 2001 From: LSchueler Date: Thu, 4 Jul 2024 16:14:28 +0200 Subject: [PATCH 095/102] Drop exception for unstruct's, not needed! --- src/gstools/field/srf.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/gstools/field/srf.py b/src/gstools/field/srf.py index c8488b24..c1d5081e 100755 --- a/src/gstools/field/srf.py +++ b/src/gstools/field/srf.py @@ -149,11 +149,6 @@ def __call__( field : :class:`numpy.ndarray` the SRF """ - if isinstance(self.generator, Fourier) and mesh_type != "structured": - raise ValueError( - "SRF: Fourier generator only defined for " - 'mesh_type == "structured".' - ) name, save = self.get_store_config(store) # update the model/seed in the generator if any changes were made self.generator.update(self.model, seed) From f1e7f7caf9b2ce91c7a16aaa4ce16c1687225b46 Mon Sep 17 00:00:00 2001 From: LSchueler Date: Tue, 9 Jul 2024 14:36:45 +0200 Subject: [PATCH 096/102] Improve Fourier's interface --- src/gstools/field/generator.py | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/src/gstools/field/generator.py b/src/gstools/field/generator.py index b52607a3..fe18f21f 100755 --- a/src/gstools/field/generator.py +++ b/src/gstools/field/generator.py @@ -547,10 +547,10 @@ class Fourier(Generator): ---------- model : :any:`CovModel` Covariance model - mode_no : :class:`list` - Number of Fourier modes per dimension. - period : :class:`list`, optional + period : :class:`list` or :class:`float` The spatial periodicity of the field, is often the domain size. + mode_no : :class:`list` or :class:`float`, optional + Number of Fourier modes per dimension. seed : :class:`int`, optional The seed of the random number generator. If "None", a random seed is used. Default: :any:`None` @@ -581,8 +581,8 @@ class Fourier(Generator): def __init__( self, model, - mode_no, period, + mode_no=32, seed=None, **kwargs, ): @@ -591,8 +591,8 @@ def __init__( # initialize private attributes self._modes = None - self._mode_no = None self._period = None + self._mode_no = None self._delta_k = None self._model = None self._seed = None @@ -602,7 +602,7 @@ def __init__( self._spectrum_factor = None self._value_type = "scalar" # set model and seed - self.update(model, seed, mode_no, period) + self.update(model, seed, period, mode_no) def __call__(self, pos, add_nugget=True): """Calculate the modes for the Fourier method. @@ -657,7 +657,7 @@ def get_nugget(self, shape): nugget = 0.0 return nugget - def update(self, model=None, seed=np.nan, mode_no=None, period=None): + def update(self, model=None, seed=np.nan, period=None, mode_no=None): """Update the model and the seed. If model and seed are not different, nothing will be done. @@ -670,26 +670,19 @@ def update(self, model=None, seed=np.nan, mode_no=None, period=None): the seed of the random number generator. If :any:`None`, a random seed is used. If :any:`numpy.nan`, the actual seed will be kept. Default: :any:`numpy.nan` - mode_no : :class:`list` or :any:`None`, optional - Number of Fourier modes per dimension. period : :class:`list` or :any:`None, optional The spatial periodicity of the field, is often the domain size. + mode_no : :class:`list` or :any:`None`, optional + Number of Fourier modes per dimension. """ tmp_model = model if model is not None else self._model if period is not None: - if len(period) != tmp_model.dim: - raise ValueError( - "Fourier: Dimension mismatch in argument period." - ) - self._period = np.array(period) + self._period = self._fill_to_dim(period, tmp_model.dim) self._delta_k = 2.0 * np.pi / self._period if mode_no is None: self._set_modes(self._mode_no, tmp_model) if mode_no is not None: - if len(mode_no) != tmp_model.dim: - raise ValueError( - "Fourier: Dimension mismatch in argument mode_no." - ) + mode_no = self._fill_to_dim(mode_no, tmp_model.dim) if (np.asarray([m % 2 for m in mode_no]) != 0).any(): raise ValueError("Fourier: Odd mode_no not supported.") self._set_modes(mode_no, tmp_model) @@ -767,10 +760,10 @@ def reset_seed(self, seed=np.nan): ) def _fill_to_dim( - self, dim, values, dtype=float, default_value=None + self, values, dim, dtype=float, default_value=None ): # pylint: disable=R6301 """Fill an array with last element up to len(dim).""" - r = values + r = np.atleast_1d(values) if values is None: if default_value is None: raise ValueError("Fourier: Value has to be provided") From 6e709f2cd34562c78c6605d10b594090a4738a96 Mon Sep 17 00:00:00 2001 From: LSchueler Date: Tue, 9 Jul 2024 14:37:31 +0200 Subject: [PATCH 097/102] Fix exception text --- src/gstools/field/generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gstools/field/generator.py b/src/gstools/field/generator.py index fe18f21f..8de1d017 100755 --- a/src/gstools/field/generator.py +++ b/src/gstools/field/generator.py @@ -704,7 +704,7 @@ def update(self, model=None, seed=np.nan, period=None, mode_no=None): self.seed = seed else: raise ValueError( - "gstools.field.generator.RandMeth: no 'model' given" + "gstools.field.generator.Fourier: no 'model' given" ) # but also update when mode mesh was modified elif mode_no is not None or period is not None: From 4fcf7849b3aec5ccf154adef47bbc83047f943d8 Mon Sep 17 00:00:00 2001 From: LSchueler Date: Tue, 9 Jul 2024 15:32:02 +0200 Subject: [PATCH 098/102] Change the way ani. fields are handled by Fourier --- src/gstools/field/generator.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/gstools/field/generator.py b/src/gstools/field/generator.py index 8de1d017..99f4fd46 100755 --- a/src/gstools/field/generator.py +++ b/src/gstools/field/generator.py @@ -676,16 +676,18 @@ def update(self, model=None, seed=np.nan, period=None, mode_no=None): Number of Fourier modes per dimension. """ tmp_model = model if model is not None else self._model + dim = tmp_model if period is not None: - self._period = self._fill_to_dim(period, tmp_model.dim) - self._delta_k = 2.0 * np.pi / self._period + self._period = self._fill_to_dim(period, dim) + anis = np.insert(tmp_model.anis.copy(), 0, 1.0) + self._delta_k = 2.0 * np.pi / self._period * anis if mode_no is None: - self._set_modes(self._mode_no, tmp_model) + self._set_modes(self._mode_no, dim) if mode_no is not None: - mode_no = self._fill_to_dim(mode_no, tmp_model.dim) + mode_no = self._fill_to_dim(mode_no, dim) if (np.asarray([m % 2 for m in mode_no]) != 0).any(): raise ValueError("Fourier: Odd mode_no not supported.") - self._set_modes(mode_no, tmp_model) + self._set_modes(mode_no, dim) # check if a new model is given if isinstance(model, CovModel): @@ -777,28 +779,27 @@ def _fill_to_dim( r = np.pad(r, (0, dim - len(r)), "edge") return r - def _set_modes(self, mode_no, model): + def _set_modes(self, mode_no, dim): """Calculate the mode mesh. Parameters ---------- mode_no : :class:`list` Number of Fourier modes per dimension. - model : :any:`CovModel` or :any:`None`, optional - covariance model. Default: :any:`None` + dim : :class:`int` + dimension of the model. Notes ----- `self._reset_seed` *has* to be called after this method! """ - anis = np.insert(model.anis.copy(), 0, 1.0) modes = [ np.arange( - -mode_no[d] / 2.0 * self._delta_k[d] / anis[d], - mode_no[d] / 2.0 * self._delta_k[d] / anis[d], + -mode_no[d] / 2.0 * self._delta_k[d], + mode_no[d] / 2.0 * self._delta_k[d], self._delta_k[d], ) - for d in range(model.dim) + for d in range(dim) ] # initialize attributes self._modes = generate_grid(modes) From cd1f5ac845ba3995db022f6c0288e1abd653da2d Mon Sep 17 00:00:00 2001 From: LSchueler Date: Tue, 9 Jul 2024 16:03:12 +0200 Subject: [PATCH 099/102] Update readme's, examples --- README.md | 2 +- docs/source/index.rst | 2 +- .../06_fourier.py => 01_random_field/08_fourier.py} | 0 .../09_fourier_trans.py} | 0 examples/01_random_field/README.rst | 6 ++++++ 5 files changed, 8 insertions(+), 2 deletions(-) rename examples/{00_misc/06_fourier.py => 01_random_field/08_fourier.py} (100%) rename examples/{00_misc/07_fourier_trans.py => 01_random_field/09_fourier_trans.py} (100%) diff --git a/README.md b/README.md index 6cb69901..d50ffd9f 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ GeoStatTools provides geostatistical tools for various purposes: -- random field generation +- random field generation, including periodic boundaries - simple, ordinary, universal and external drift kriging - conditioned field generation - incompressible random vector field generation diff --git a/docs/source/index.rst b/docs/source/index.rst index ecad0583..65e5833b 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -24,7 +24,7 @@ Purpose GeoStatTools provides geostatistical tools for various purposes: -- random field generation +- random field generation, including periodic boundaries - simple, ordinary, universal and external drift kriging - conditioned field generation - incompressible random vector field generation diff --git a/examples/00_misc/06_fourier.py b/examples/01_random_field/08_fourier.py similarity index 100% rename from examples/00_misc/06_fourier.py rename to examples/01_random_field/08_fourier.py diff --git a/examples/00_misc/07_fourier_trans.py b/examples/01_random_field/09_fourier_trans.py similarity index 100% rename from examples/00_misc/07_fourier_trans.py rename to examples/01_random_field/09_fourier_trans.py diff --git a/examples/01_random_field/README.rst b/examples/01_random_field/README.rst index 6b226b2f..ab723bae 100644 --- a/examples/01_random_field/README.rst +++ b/examples/01_random_field/README.rst @@ -11,6 +11,12 @@ semi-variogram. This is done by using the so-called randomization method. The spatial random field is represented by a stochastic Fourier integral and its discretised modes are evaluated at random frequencies. +In case you want to generate spatial random fields with periodic boundaries, +you can use the so-called Fourier method. See the corresponding examples for +how to do that. The spatial random field is represented by a stochastic +Fourier integral and its discretised modes are evaluated at equidistant +frequencies. + GSTools supports arbitrary and non-isotropic covariance models. Examples From 29d0ea6c0bc39444a241e4190f679b0760dc986b Mon Sep 17 00:00:00 2001 From: LSchueler Date: Tue, 9 Jul 2024 16:42:10 +0200 Subject: [PATCH 100/102] Fix stupid typo --- src/gstools/field/generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gstools/field/generator.py b/src/gstools/field/generator.py index 99f4fd46..a42b4bbc 100755 --- a/src/gstools/field/generator.py +++ b/src/gstools/field/generator.py @@ -676,7 +676,7 @@ def update(self, model=None, seed=np.nan, period=None, mode_no=None): Number of Fourier modes per dimension. """ tmp_model = model if model is not None else self._model - dim = tmp_model + dim = tmp_model.dim if period is not None: self._period = self._fill_to_dim(period, dim) anis = np.insert(tmp_model.anis.copy(), 0, 1.0) From 784928403eb0952dd7d3097af07c9d8c9a949fad Mon Sep 17 00:00:00 2001 From: LSchueler Date: Thu, 11 Jul 2024 11:56:16 +0200 Subject: [PATCH 101/102] Improve Fourier examples --- examples/01_random_field/08_fourier.py | 28 ++++++++++++-------- examples/01_random_field/09_fourier_trans.py | 26 ++++++++++-------- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/examples/01_random_field/08_fourier.py b/examples/01_random_field/08_fourier.py index 360f4708..e83fc2a8 100644 --- a/examples/01_random_field/08_fourier.py +++ b/examples/01_random_field/08_fourier.py @@ -11,23 +11,29 @@ import gstools as gs -# We start off by defining the spatial grid. -L = np.array((500, 500)) -x = np.linspace(0, L[0], 256) -y = np.linspace(0, L[1], 128) - -# And by setting up a Gaussian covariance model with a correlation length -# scale which is roughly half the size of the grid. +# We start off by defining the spatial grid. For the sake of simplicity, we +# use a square domain. We set the optional argument `endpoint` to `False`, to +# not make the domain in each dimension one grid cell larger than the +# periodicity. +L = 500.0 +x = np.linspace(0, L, 256, endpoint=False) +y = np.linspace(0, L, 128, endpoint=False) + +Now, we create a Gaussian covariance model with a correlation length which is +# roughly half the size of the grid. model = gs.Gaussian(dim=2, var=1, len_scale=200) -# Next, we hand the cov. model to the spatial random field class -# and set the generator to `Fourier`. The `mode_no` argument sets the number of -# Fourier modes per dimension. The argument `period` is set to the domain size. +# Next, we hand the cov. model to the spatial random field class `SRF` +# and set the generator to `"Fourier"`. The argument `period` is set to the +# domain size. If only a single number is given, the same periodicity is +# applied in each dimension, as shown in this example. The `mode_no` argument +# sets the number of Fourier modes. If only an integer is given, that number +# of modes is used for all dimensions. srf = gs.SRF( model, generator="Fourier", - mode_no=[32, 32], period=L, + mode_no=32, seed=1681903, ) diff --git a/examples/01_random_field/09_fourier_trans.py b/examples/01_random_field/09_fourier_trans.py index 8331f766..692cc68c 100644 --- a/examples/01_random_field/09_fourier_trans.py +++ b/examples/01_random_field/09_fourier_trans.py @@ -10,29 +10,33 @@ import gstools as gs -# We start off by defining the spatial grid. -L = np.array((500, 500)) -x = np.linspace(0, L[0], 300) -y = np.linspace(0, L[1], 200) +# We start off by defining the spatial grid. As in the previous example, we do +# not want to include the endpoints. +L = np.array((500, 400)) +x = np.linspace(0, L[0], 300, endpoint=False) +y = np.linspace(0, L[1], 200, endpoint=False) # Instead of using a Gaussian covariance model, we will use the much rougher # exponential model and we will introduce an anisotropy by using two different -# length scales in the x- and y-axes -model = gs.Exponential(dim=2, var=2, len_scale=[30, 20]) +# length scales in the x- and y-directions +model = gs.Exponential(dim=2, var=2, len_scale=[80, 20]) -# Same as before, we set up the spatial random field +# Same as before, we set up the spatial random field. But this time, we will +# use a periodicity which is equal to the domain size in x-direction, but +# half the domain size in y-direction. And we will use different `mode_no` for +# the different dimensions. srf = gs.SRF( model, generator="Fourier", - mode_no=[32, 32], - period=L, + period=[L[0], L[1]/2], + mode_no=[30, 20], seed=1681903, ) # and compute it on our spatial domain srf((x, y), mesh_type="structured") -# With the field generated, we can now apply transformations -# starting with a discretization of the field into 4 different values +# With the field generated, we can now apply transformations starting with a +# discretization of the field into 4 different values thresholds = np.linspace(np.min(srf.field), np.max(srf.field), 4) srf.transform("discrete", store="transform_discrete", values=thresholds) srf.plot("transform_discrete") From c04c924f9f2175a9a91ae29aae9a6e4635509d27 Mon Sep 17 00:00:00 2001 From: LSchueler Date: Mon, 15 Jul 2024 13:38:37 +0200 Subject: [PATCH 102/102] Black example & fix typo --- examples/01_random_field/08_fourier.py | 2 +- examples/01_random_field/09_fourier_trans.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/01_random_field/08_fourier.py b/examples/01_random_field/08_fourier.py index e83fc2a8..8b700ae6 100644 --- a/examples/01_random_field/08_fourier.py +++ b/examples/01_random_field/08_fourier.py @@ -19,7 +19,7 @@ x = np.linspace(0, L, 256, endpoint=False) y = np.linspace(0, L, 128, endpoint=False) -Now, we create a Gaussian covariance model with a correlation length which is +# Now, we create a Gaussian covariance model with a correlation length which is # roughly half the size of the grid. model = gs.Gaussian(dim=2, var=1, len_scale=200) diff --git a/examples/01_random_field/09_fourier_trans.py b/examples/01_random_field/09_fourier_trans.py index 692cc68c..81c303f3 100644 --- a/examples/01_random_field/09_fourier_trans.py +++ b/examples/01_random_field/09_fourier_trans.py @@ -28,7 +28,7 @@ srf = gs.SRF( model, generator="Fourier", - period=[L[0], L[1]/2], + period=[L[0], L[1] / 2], mode_no=[30, 20], seed=1681903, )