Skip to content

Commit

Permalink
Merge pull request #166 from GeoStat-Framework/field_update
Browse files Browse the repository at this point in the history
Field: enable standalone use
  • Loading branch information
MuellerSeb authored Jun 3, 2021
2 parents 0025f7d + 11db203 commit 94b006a
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 24 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions examples/00_misc/05_standalone_field.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""
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

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.
#
# 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=(x0, x1, x2, x3), field=values)
plotter.plot()
91 changes: 69 additions & 22 deletions gstools/field/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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".
Expand All @@ -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
Expand All @@ -76,41 +79,69 @@ 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
self.mean = mean
self.normalizer = normalizer
self.trend = trend

def __call__(self, *args, **kwargs):
"""Generate the field."""
def __call__(
self, pos, field=None, 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` or :any:`None`, optional
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
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)
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):
"""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)

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 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
Expand All @@ -128,17 +159,17 @@ 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 :any:`__call__`.
Notes
-----
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:`Field.__call__`
See:
- meshio: https://github.com/nschloe/meshio
- ogs5py: https://github.com/GeoStat-Framework/ogs5py
- PyVista: https://github.com/pyvista/pyvista
"""
return generate_on_mesh(self, mesh, points, direction, name, **kwargs)

Expand Down Expand Up @@ -176,9 +207,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):
Expand Down Expand Up @@ -299,7 +332,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(
Expand All @@ -317,12 +350,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):
Expand Down Expand Up @@ -365,7 +403,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):
Expand All @@ -378,9 +421,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(),
)
2 changes: 1 addition & 1 deletion gstools/field/cond_srf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion gstools/field/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)


Expand Down
51 changes: 51 additions & 0 deletions tests/test_field.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/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_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()

0 comments on commit 94b006a

Please sign in to comment.