From fce7f59485ebfe5fc9aa03adc9041a30c940b31f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Sun, 18 Apr 2021 21:25:32 +0200 Subject: [PATCH 01/12] Field: enable standalone use with given 'dim'; add 'points' method; add 'latlon' property; better doc ref to '__call__' --- gstools/field/base.py | 94 +++++++++++++++++++++++++++++++++++-------- gstools/field/plot.py | 2 +- 2 files changed, 78 insertions(+), 18 deletions(-) diff --git a/gstools/field/base.py b/gstools/field/base.py index 96abf392..40fae75e 100755 --- a/gstools/field/base.py +++ b/gstools/field/base.py @@ -41,7 +41,7 @@ class Field: Parameters ---------- - model : :any:`CovModel` + model : :any:`CovModel`, optional Covariance Model related to the field. value_type : :class:`str`, optional Value type of the field. Either "scalar" or "vector". @@ -56,15 +56,18 @@ class Field: Trend of the denormalized fields. If no normalizer is applied, this behaves equal to 'mean'. The default is None. + dim : :any:`None` or :class:`int`, optional + Dimension of the field if no model is given. """ def __init__( self, - model, + model=None, value_type="scalar", mean=None, normalizer=None, trend=None, + dim=None, ): # initialize attributes self.pos = None @@ -76,6 +79,7 @@ def __init__( self._mean = None self._normalizer = None self._trend = None + self._dim = dim if dim is None else int(dim) # set properties self.model = model self.value_type = value_type @@ -83,13 +87,37 @@ def __init__( self.normalizer = normalizer self.trend = trend - def __call__(self, *args, **kwargs): - """Generate the field.""" + def __call__( + self, pos, field, mesh_type="unstructured", post_process=True + ): + """Generate the field. + + Parameters + ---------- + pos : :class:`list` + the position tuple, containing main direction and transversal + directions + field : :class:`numpy.ndarray` + the field values. + mesh_type : :class:`str` + 'structured' / 'unstructured' + post_process : :class:`bool`, optional + Whether to apply mean, normalizer and trend to the field. + Default: `True` + + Returns + ------- + field : :class:`numpy.ndarray` + the field values. + """ + pos, shape = self.pre_pos(pos, mesh_type) + field = np.array(field, dtype=np.double).reshape(shape) + return self.post_field(field, process=post_process) def structured(self, *args, **kwargs): """Generate a field on a structured mesh. - See :any:`Field.__call__` + See :any:`__call__` """ call = partial(self.__call__, mesh_type="structured") return call(*args, **kwargs) @@ -97,11 +125,27 @@ def structured(self, *args, **kwargs): def unstructured(self, *args, **kwargs): """Generate a field on an unstructured mesh. - See :any:`Field.__call__` + See :any:`__call__` """ call = partial(self.__call__, mesh_type="unstructured") return call(*args, **kwargs) + def points(self, points, **kwargs): + """Generate a field on a point list. + + See :any:`__call__` + + Parameters + ---------- + points : (n, d) :class:`numpy.ndarray` + point list with n points in d dimensions. + **kwargs + Keyword arguments forwarded to field generation call. + """ + points = np.reshape(points, (-1, self.dim)) + call = partial(self.__call__, pos=points.T, mesh_type="unstructured") + return call(**kwargs) + def mesh( self, mesh, points="centroids", direction="all", name="field", **kwargs ): @@ -128,7 +172,7 @@ def mesh( cell_data. If to few names are given, digits will be appended. Default: "field" **kwargs - Keyword arguments forwareded to `Field.__call__`. + Keyword arguments forwarded to field generation call. Notes ----- @@ -138,7 +182,7 @@ def mesh( See: https://github.com/nschloe/meshio See: https://github.com/pyvista/pyvista - See: :any:`Field.__call__` + See: :any:`__call__` """ return generate_on_mesh(self, mesh, points, direction, name, **kwargs) @@ -176,9 +220,11 @@ def pre_pos(self, pos, mesh_type="unstructured"): # prepend dimension if we have a vector field if self.value_type == "vector": shape = (self.dim,) + shape - if self.model.latlon: + if self.latlon: raise ValueError("Field: Vector fields not allowed for latlon") # return isometrized pos tuple and resulting field shape + if self.model is None: + return pos, shape return self.model.isometrize(pos), shape def post_field(self, field, name="field", process=True, save=True): @@ -299,7 +345,7 @@ def plot( if self.value_type == "scalar": r = plot_field(self, field, fig, ax, **kwargs) elif self.value_type == "vector": - if self.model.dim == 2: + if self.dim == 2: r = plot_vec_field(self, field, fig, ax, **kwargs) else: raise NotImplementedError( @@ -317,12 +363,17 @@ def model(self): @model.setter def model(self, model): - if isinstance(model, CovModel): + if model is not None: + if not isinstance(model, CovModel): + raise ValueError( + "Field: 'model' is not an instance of 'gstools.CovModel'" + ) self._model = model + self._dim = None + elif self._dim is None: + raise ValueError("Field: either needs 'model' or 'dim'.") else: - raise ValueError( - "Field: 'model' is not an instance of 'gstools.CovModel'" - ) + self._model = None @property def mean(self): @@ -365,7 +416,12 @@ def value_type(self, value_type): @property def dim(self): """:class:`int`: Dimension of the field.""" - return self.model.field_dim + return self._dim if self.model is None else self.model.field_dim + + @property + 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 name(self): @@ -378,9 +434,13 @@ def _fmt_mean_norm_trend(self): def __repr__(self): """Return String representation.""" - return "{0}(model={1}, value_type='{2}'{3})".format( + if self.model is None: + dim_str = f"dim={self.dim}" + else: + dim_str = f"model={self.model.name}" + return "{0}({1}, value_type='{2}'{3})".format( self.name, - self.model.name, + dim_str, self.value_type, self._fmt_mean_norm_trend(), ) diff --git a/gstools/field/plot.py b/gstools/field/plot.py index 77e604ae..ca3a422b 100644 --- a/gstools/field/plot.py +++ b/gstools/field/plot.py @@ -56,7 +56,7 @@ def plot_field( if fld.dim == 1: return plot_1d(fld.pos, plt_fld, fig, ax, **kwargs) return plot_nd( - fld.pos, plt_fld, fld.mesh_type, fig, ax, fld.model.latlon, **kwargs + fld.pos, plt_fld, fld.mesh_type, fig, ax, fld.latlon, **kwargs ) From e8237964e28aae66dc5f1d9802618e1952e80bed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Sun, 18 Apr 2021 21:54:22 +0200 Subject: [PATCH 02/12] Tests: more tests for Field class --- tests/test_field.py | 57 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 tests/test_field.py diff --git a/tests/test_field.py b/tests/test_field.py new file mode 100644 index 00000000..80b7cfb2 --- /dev/null +++ b/tests/test_field.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +This is the unittest of SRF class. +""" + +import unittest +import numpy as np +import gstools as gs + + +class TestField(unittest.TestCase): + def setUp(self): + self.cov_model = gs.Gaussian(dim=2, var=1.5, len_scale=4.0) + rng = np.random.RandomState(123018) + x = rng.uniform(0.0, 10, 100) + y = rng.uniform(0.0, 10, 100) + self.field = rng.uniform(0.0, 10, 100) + self.pos = np.array([x, y]) + + def test_standalone(self): + fld = gs.field.Field(dim=2) + fld_cov = gs.field.Field(model=self.cov_model) + field1 = fld(self.pos, self.field) + field2 = fld_cov(self.pos, self.field) + self.assertTrue(np.all(np.isclose(field1, field2))) + self.assertTrue(np.all(np.isclose(field1, self.field))) + + def test_points(self): + fld = gs.SRF(self.cov_model) + field1 = fld.unstructured(self.pos) + field2 = fld.points(self.pos.T) + self.assertTrue(np.all(np.isclose(field1, field2))) + + def test_raise(self): + # vector field on latlon + fld = gs.field.Field(gs.Gaussian(latlon=True), value_type="vector") + self.assertRaises(ValueError, fld, [1, 2], [1, 2]) + # no pos tuple present + fld = gs.field.Field(dim=2) + self.assertRaises(ValueError, fld.post_field, [1, 2]) + # wrong model type + with self.assertRaises(ValueError): + gs.field.Field(model=3.1415) + # no model and no dim given + with self.assertRaises(ValueError): + gs.field.Field() + # wrong value type + with self.assertRaises(ValueError): + gs.field.Field(dim=2, value_type="complex") + # wrong mean shape + with self.assertRaises(ValueError): + gs.field.Field(dim=3, mean=[1, 2]) + + +if __name__ == "__main__": + unittest.main() From 325bbcfe2bf721ccb89002243efef5a46fcd2569 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Mon, 31 May 2021 13:08:39 +0200 Subject: [PATCH 03/12] Field: allow None as field values in standalone Field class (will be zeros then) --- gstools/field/base.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/gstools/field/base.py b/gstools/field/base.py index 40fae75e..7acfc329 100755 --- a/gstools/field/base.py +++ b/gstools/field/base.py @@ -88,7 +88,7 @@ def __init__( self.trend = trend def __call__( - self, pos, field, mesh_type="unstructured", post_process=True + self, pos, field=None, mesh_type="unstructured", post_process=True ): """Generate the field. @@ -97,10 +97,10 @@ def __call__( pos : :class:`list` the position tuple, containing main direction and transversal directions - field : :class:`numpy.ndarray` - the field values. - mesh_type : :class:`str` - 'structured' / 'unstructured' + field : :class:`numpy.ndarray` or :any:`None`, optional + the field values. Will be all zeros by default. + mesh_type : :class:`str`, optional + 'structured' / 'unstructured'. Default: 'unstructured' post_process : :class:`bool`, optional Whether to apply mean, normalizer and trend to the field. Default: `True` @@ -111,7 +111,10 @@ def __call__( the field values. """ pos, shape = self.pre_pos(pos, mesh_type) - field = np.array(field, dtype=np.double).reshape(shape) + if field is None: + field = np.zeros(shape, dtype=np.double) + else: + field = np.array(field, dtype=np.double).reshape(shape) return self.post_field(field, process=post_process) def structured(self, *args, **kwargs): From 7b62641934aee19a1249554186938c847cc0a6dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Mon, 31 May 2021 13:09:43 +0200 Subject: [PATCH 04/12] Field: remove points methods to prevent confusion --- gstools/field/base.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/gstools/field/base.py b/gstools/field/base.py index 7acfc329..0c4f6c53 100755 --- a/gstools/field/base.py +++ b/gstools/field/base.py @@ -133,22 +133,6 @@ def unstructured(self, *args, **kwargs): call = partial(self.__call__, mesh_type="unstructured") return call(*args, **kwargs) - def points(self, points, **kwargs): - """Generate a field on a point list. - - See :any:`__call__` - - Parameters - ---------- - points : (n, d) :class:`numpy.ndarray` - point list with n points in d dimensions. - **kwargs - Keyword arguments forwarded to field generation call. - """ - points = np.reshape(points, (-1, self.dim)) - call = partial(self.__call__, pos=points.T, mesh_type="unstructured") - return call(**kwargs) - def mesh( self, mesh, points="centroids", direction="all", name="field", **kwargs ): From d0cefac62d0c9b0d5fd08d29feda0bb1f92ff5c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Mon, 31 May 2021 13:11:58 +0200 Subject: [PATCH 05/12] Field: add example for standalone usage --- examples/00_misc/05_standalone_field.py | 27 +++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 examples/00_misc/05_standalone_field.py diff --git a/examples/00_misc/05_standalone_field.py b/examples/00_misc/05_standalone_field.py new file mode 100644 index 00000000..a14e6f05 --- /dev/null +++ b/examples/00_misc/05_standalone_field.py @@ -0,0 +1,27 @@ +""" +Standalone Field class +---------------------- + +The :any:`Field` class of GSTools can be used to plot arbitrary data in nD. + +In the following example we will produce 10000 random points in 4D with +random values and plot them. +""" +import numpy as np +import gstools as gs + +x1 = np.random.RandomState(19970221).rand(10000) * 100.0 +x2 = np.random.RandomState(20011012).rand(10000) * 100.0 +x3 = np.random.RandomState(20210530).rand(10000) * 100.0 +x4 = np.random.RandomState(20210531).rand(10000) * 100.0 +values = np.random.RandomState(2021).rand(10000) * 100.0 + +############################################################################### +# Only thing needed to instantiate the Field is the dimension. +# +# Afterwards we can call the instance like all other Fields +# (:any:`SRF`, :any:`Krige` or :any:`CondSRF`), but with an additional field. + +plotter = gs.field.Field(dim=4) +plotter(pos=(x1, x2, x3, x4), field=values) +plotter.plot() From ad95b5e661c9038a99a3709ceaf27c09696e4511 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Mon, 31 May 2021 13:18:08 +0200 Subject: [PATCH 06/12] Field: ignore pylint W0222 for differing signature in CondSRF.__call__ --- gstools/field/cond_srf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gstools/field/cond_srf.py b/gstools/field/cond_srf.py index 5e1eb85d..609045c8 100644 --- a/gstools/field/cond_srf.py +++ b/gstools/field/cond_srf.py @@ -9,7 +9,7 @@ .. autosummary:: CondSRF """ -# pylint: disable=C0103, W0231, W0221, E1102 +# pylint: disable=C0103, W0231, W0221, W0222, E1102 import numpy as np from gstools.field.generator import RandMeth from gstools.field.base import Field From aeba3c1b5ad9934fb1af44aaffd4497f32a668fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Mon, 31 May 2021 13:20:37 +0200 Subject: [PATCH 07/12] Field: remove test for points method --- tests/test_field.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/test_field.py b/tests/test_field.py index 80b7cfb2..53e7c336 100644 --- a/tests/test_field.py +++ b/tests/test_field.py @@ -26,12 +26,6 @@ def test_standalone(self): self.assertTrue(np.all(np.isclose(field1, field2))) self.assertTrue(np.all(np.isclose(field1, self.field))) - def test_points(self): - fld = gs.SRF(self.cov_model) - field1 = fld.unstructured(self.pos) - field2 = fld.points(self.pos.T) - self.assertTrue(np.all(np.isclose(field1, field2))) - def test_raise(self): # vector field on latlon fld = gs.field.Field(gs.Gaussian(latlon=True), value_type="vector") From 1ae83ed8d37832d5cf4d07f68358f60a44a96a58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Mon, 31 May 2021 13:37:19 +0200 Subject: [PATCH 08/12] Field.mesh: minor doc update --- gstools/field/base.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/gstools/field/base.py b/gstools/field/base.py index 0c4f6c53..450abc61 100755 --- a/gstools/field/base.py +++ b/gstools/field/base.py @@ -136,12 +136,12 @@ def unstructured(self, *args, **kwargs): def mesh( self, mesh, points="centroids", direction="all", name="field", **kwargs ): - """Generate a field on a given meshio or ogs5py mesh. + """Generate a field on a given meshio, ogs5py or PyVista mesh. Parameters ---------- mesh : meshio.Mesh or ogs5py.MSH or PyVista mesh - The given meshio, ogs5py, or PyVista mesh + The given mesh points : :class:`str`, optional The points to evaluate the field at. Either the "centroids" of the mesh cells @@ -166,10 +166,11 @@ def mesh( This will store the field in the given mesh under the given name, if a meshio or PyVista mesh was given. - See: https://github.com/nschloe/meshio - See: https://github.com/pyvista/pyvista - - See: :any:`__call__` + See: + - meshio: https://github.com/nschloe/meshio + - ogs5py: https://github.com/GeoStat-Framework/ogs5py + - PyVista: https://github.com/pyvista/pyvista + - Called method: :any:`__call__` """ return generate_on_mesh(self, mesh, points, direction, name, **kwargs) From 6c5692a09da0f73a88fceb39204a7d055e4e8582 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Mon, 31 May 2021 13:37:28 +0200 Subject: [PATCH 09/12] update Changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51fa960f..b1e57557 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to **GSTools** will be documented in this file. +## [1.3.1] - Pure Pink - 2021-? + +### Enhancements +- Standalone use of Field class [#166](https://github.com/GeoStat-Framework/GSTools/issues/166) +- add social badges in README [#169](https://github.com/GeoStat-Framework/GSTools/issues/169), [#170](https://github.com/GeoStat-Framework/GSTools/issues/170) + +### Bugfixes +- use `oldest-supported-numpy` to build cython extensions [#165](https://github.com/GeoStat-Framework/GSTools/pull/165) + + ## [1.3.0] - Pure Pink - 2021-04 ### Topics From 68e2c5391bc5c1040dc02c0c34304b2c42ac9f84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Thu, 3 Jun 2021 15:06:14 +0200 Subject: [PATCH 10/12] Field: use single RandomState in example --- examples/00_misc/05_standalone_field.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/examples/00_misc/05_standalone_field.py b/examples/00_misc/05_standalone_field.py index a14e6f05..3d25b26b 100644 --- a/examples/00_misc/05_standalone_field.py +++ b/examples/00_misc/05_standalone_field.py @@ -10,11 +10,12 @@ import numpy as np import gstools as gs -x1 = np.random.RandomState(19970221).rand(10000) * 100.0 -x2 = np.random.RandomState(20011012).rand(10000) * 100.0 -x3 = np.random.RandomState(20210530).rand(10000) * 100.0 -x4 = np.random.RandomState(20210531).rand(10000) * 100.0 -values = np.random.RandomState(2021).rand(10000) * 100.0 +rng = np.random.RandomState(19970221) +x0 = rng.rand(10000) * 100.0 +x1 = rng.rand(10000) * 100.0 +x2 = rng.rand(10000) * 100.0 +x3 = rng.rand(10000) * 100.0 +values = rng.rand(10000) * 100.0 ############################################################################### # Only thing needed to instantiate the Field is the dimension. @@ -23,5 +24,5 @@ # (:any:`SRF`, :any:`Krige` or :any:`CondSRF`), but with an additional field. plotter = gs.field.Field(dim=4) -plotter(pos=(x1, x2, x3, x4), field=values) +plotter(pos=(x0, x1, x2, x3), field=values) plotter.plot() From 986283881924b8f02ae82d269bec566ce9926232 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Thu, 3 Jun 2021 15:08:02 +0200 Subject: [PATCH 11/12] Field: better doc for field in __call__ --- gstools/field/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gstools/field/base.py b/gstools/field/base.py index 450abc61..b255ad1a 100755 --- a/gstools/field/base.py +++ b/gstools/field/base.py @@ -98,7 +98,7 @@ def __call__( the position tuple, containing main direction and transversal directions field : :class:`numpy.ndarray` or :any:`None`, optional - the field values. Will be all zeros by default. + the field values. Will be all zeros if :any:`None` is given. mesh_type : :class:`str`, optional 'structured' / 'unstructured'. Default: 'unstructured' post_process : :class:`bool`, optional From 11db2031548024c70a2b632a360fed5e2cb9c7c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20M=C3=BCller?= Date: Thu, 3 Jun 2021 15:11:04 +0200 Subject: [PATCH 12/12] Field.mesh: better doc for kwargs --- gstools/field/base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gstools/field/base.py b/gstools/field/base.py index b255ad1a..7c83f370 100755 --- a/gstools/field/base.py +++ b/gstools/field/base.py @@ -159,7 +159,7 @@ def mesh( cell_data. If to few names are given, digits will be appended. Default: "field" **kwargs - Keyword arguments forwarded to field generation call. + Keyword arguments forwarded to :any:`__call__`. Notes ----- @@ -170,7 +170,6 @@ def mesh( - meshio: https://github.com/nschloe/meshio - ogs5py: https://github.com/GeoStat-Framework/ogs5py - PyVista: https://github.com/pyvista/pyvista - - Called method: :any:`__call__` """ return generate_on_mesh(self, mesh, points, direction, name, **kwargs)