Source code for gstools.field.base

# -*- coding: utf-8 -*-
"""
GStools subpackage providing a base class for spatial fields.

.. currentmodule:: gstools.field.base

The following classes are provided

.. autosummary::
   Field
"""
# pylint: disable=C0103, C0415
from functools import partial
import numpy as np
from gstools.covmodel.base import CovModel
from gstools.tools.geometric import format_struct_pos_dim, generate_grid
from gstools.field.tools import (
    generate_on_mesh,
    to_vtk_helper,
    fmt_mean_norm_trend,
)
from gstools.normalizer.tools import apply_mean_norm_trend, _check_normalizer

__all__ = ["Field"]

VALUE_TYPES = ["scalar", "vector"]
""":class:`list` of :class:`str`: valid field value types."""


def _set_mean_trend(value, dim):
    if callable(value) or value is None:
        return value
    value = np.array(value, dtype=np.double).ravel()
    if value.size > 1 and value.size != dim:  # vector mean
        raise ValueError(f"Mean/Trend: Wrong size ({value})")
    return value if value.size > 1 else value.item()


[docs]class Field: """A base class for random fields, kriging fields, etc. Parameters ---------- model : :any:`CovModel`, optional Covariance Model related to the field. value_type : :class:`str`, optional Value type of the field. Either "scalar" or "vector". The default is "scalar". mean : :any:`None` or :class:`float` or :any:`callable`, optional Mean of the field if wanted. Could also be a callable. The default is None. normalizer : :any:`None` or :any:`Normalizer`, optional Normalizer to be applied to the field. The default is None. trend : :any:`None` or :class:`float` or :any:`callable`, optional 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=None, value_type="scalar", mean=None, normalizer=None, trend=None, dim=None, ): # initialize attributes self.pos = None self.mesh_type = None self.field = None # initialize private attributes self._model = None self._value_type = None 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
[docs] 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)
[docs] def structured(self, *args, **kwargs): """Generate a field on a structured mesh. See :any:`__call__` """ call = partial(self.__call__, mesh_type="structured") return call(*args, **kwargs)
[docs] def unstructured(self, *args, **kwargs): """Generate a field on an unstructured mesh. See :any:`__call__` """ call = partial(self.__call__, mesh_type="unstructured") return call(*args, **kwargs)
[docs] def mesh( self, mesh, points="centroids", direction="all", name="field", **kwargs ): """Generate a field on a given meshio, ogs5py or PyVista mesh. Parameters ---------- mesh : meshio.Mesh or ogs5py.MSH or PyVista mesh The given mesh points : :class:`str`, optional The points to evaluate the field at. Either the "centroids" of the mesh cells (calculated as mean of the cell vertices) or the "points" of the given mesh. Default: "centroids" direction : :class:`str` or :class:`list`, optional Here you can state which direction should be choosen for lower dimension. For example, if you got a 2D mesh in xz direction, you have to pass "xz". By default, all directions are used. One can also pass a list of indices. Default: "all" name : :class:`str` or :class:`list` of :class:`str`, optional Name(s) to store the field(s) in the given mesh as point_data or cell_data. If to few names are given, digits will be appended. Default: "field" **kwargs 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: - 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)
[docs] def pre_pos(self, pos, mesh_type="unstructured"): """ Preprocessing positions and mesh_type. Parameters ---------- pos : :any:`iterable` the position tuple, containing main direction and transversal directions mesh_type : :class:`str`, optional 'structured' / 'unstructured' Default: `"unstructured"` Returns ------- iso_pos : (d, n), :class:`numpy.ndarray` the isometrized position tuple shape : :class:`tuple` Shape of the resulting field. """ # save mesh-type self.mesh_type = mesh_type # save pos tuple if mesh_type != "unstructured": pos, shape = format_struct_pos_dim(pos, self.dim) self.pos = pos pos = generate_grid(pos) else: pos = np.array(pos, dtype=np.double).reshape(self.dim, -1) self.pos = pos shape = np.shape(pos[0]) # prepend dimension if we have a vector field if self.value_type == "vector": shape = (self.dim,) + shape 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
[docs] def post_field(self, field, name="field", process=True, save=True): """ Postprocessing field values. Parameters ---------- field : :class:`numpy.ndarray` Field values. name : :class:`str`, optional Name. to store the field. The default is "field". process : :class:`bool`, optional Whether to process field to apply mean, normalizer and trend. The default is True. save : :class:`bool`, optional Whether to store the field under the given name. The default is True. Returns ------- field : :class:`numpy.ndarray` Processed field values. """ if process: if self.pos is None: raise ValueError("post_field: no 'pos' tuple set for field.") field = apply_mean_norm_trend( pos=self.pos, field=field, mesh_type=self.mesh_type, value_type=self.value_type, mean=self.mean, normalizer=self.normalizer, trend=self.trend, check_shape=False, stacked=False, ) if save: setattr(self, name, field) return field
[docs] def to_pyvista( self, field_select="field", fieldname="field" ): # pragma: no cover """Create a VTK/PyVista grid of the stored field. Parameters ---------- field_select : :class:`str`, optional Field that should be stored. Can be: "field", "raw_field", "krige_field", "err_field" or "krige_var". Default: "field" fieldname : :class:`str`, optional Name of the field in the VTK file. Default: "field" """ grid = to_vtk_helper( self, filename=None, field_select=field_select, fieldname=fieldname ) return grid
[docs] def vtk_export( self, filename, field_select="field", fieldname="field" ): # pragma: no cover """Export the stored field to vtk. Parameters ---------- filename : :class:`str` Filename of the file to be saved, including the path. Note that an ending (.vtr or .vtu) will be added to the name. field_select : :class:`str`, optional Field that should be stored. Can be: "field", "raw_field", "krige_field", "err_field" or "krige_var". Default: "field" fieldname : :class:`str`, optional Name of the field in the VTK file. Default: "field" """ if not isinstance(filename, str): raise TypeError("Please use a string filename.") return to_vtk_helper( self, filename=filename, field_select=field_select, fieldname=fieldname, )
[docs] def plot( self, field="field", fig=None, ax=None, **kwargs ): # pragma: no cover """ Plot the spatial random field. Parameters ---------- field : :class:`str`, optional Field that should be plotted. Default: "field" fig : :class:`Figure` or :any:`None` Figure to plot the axes on. If `None`, a new one will be created. Default: `None` ax : :class:`Axes` or :any:`None` Axes to plot on. If `None`, a new one will be added to the figure. Default: `None` **kwargs Forwarded to the plotting routine. """ # just import if needed; matplotlib is not required by setup from gstools.field.plot import plot_field, plot_vec_field if self.value_type is None: raise ValueError( "Field value type not set! " "Specify 'scalar' or 'vector' before plotting." ) if self.value_type == "scalar": r = plot_field(self, field, fig, ax, **kwargs) elif self.value_type == "vector": if self.dim == 2: r = plot_vec_field(self, field, fig, ax, **kwargs) else: raise NotImplementedError( "Streamflow plotting only supported for 2d case." ) else: raise ValueError(f"Unknown field value type: {self.value_type}") return r
@property def model(self): """:any:`CovModel`: The covariance model of the field.""" return self._model @model.setter def model(self, model): 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: self._model = None @property def mean(self): """:class:`float` or :any:`callable`: The mean of the field.""" return self._mean @mean.setter def mean(self, mean): self._mean = _set_mean_trend(mean, self.dim) @property def normalizer(self): """:any:`Normalizer`: Normalizer of the field.""" return self._normalizer @normalizer.setter def normalizer(self, normalizer): self._normalizer = _check_normalizer(normalizer) @property def trend(self): """:class:`float` or :any:`callable`: The trend of the field.""" return self._trend @trend.setter def trend(self, trend): self._trend = _set_mean_trend(trend, self.dim) @property def value_type(self): """:class:`str`: Type of the field values (scalar, vector).""" return self._value_type @value_type.setter def value_type(self, value_type): if value_type not in VALUE_TYPES: raise ValueError(f"Field: value type not in {VALUE_TYPES}") self._value_type = value_type @property def dim(self): """:class:`int`: Dimension of the field.""" 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): """:class:`str`: The name of the class.""" return self.__class__.__name__ def _fmt_mean_norm_trend(self): # fmt_mean_norm_trend for all child classes return fmt_mean_norm_trend(self) def __repr__(self): """Return String representation.""" 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, dim_str, self.value_type, self._fmt_mean_norm_trend(), )