Source code for landlab.field.graph_field

"""Define collections of fields that are attached to a *Landlab*
:class:`~landlab.graph.graph.Graph`.
"""
import numpy as np
import xarray as xr

from .errors import FieldError, GroupError


def reshape_for_storage(array, field_size=None):
    """Reshape an array to be stored as a field.

    For reshaping rules, see :func:`~.shape_for_storage`.

    Parameters
    ----------
    array : numpy.ndarray
        The array to be stored.
    field_size : int, optional
        The size of the field.

    Returns
    -------
    ndarray
        The (possibly) reshaped array.

    Examples
    --------
    >>> import numpy as np
    >>> from landlab.field.graph_field import reshape_for_storage

    The shape will be such that the first dimension in the field size.

    >>> data = np.arange(6)
    >>> reshape_for_storage(data, 3)
    array([[0, 1],
           [2, 3],
           [4, 5]])
    >>> reshape_for_storage(data, 2)
    array([[0, 1, 2],
           [3, 4, 5]])
    >>> reshape_for_storage(data, 6)
    array([0, 1, 2, 3, 4, 5])

    If the array is already the correct shape, just return that array.

    >>> data = np.arange(6).reshape((2, 3))
    >>> reshape_for_storage(data, 2) is data
    True

    :meta private:
    """
    shape = shape_for_storage(array, field_size)
    if shape == array.shape or array.ndim == 0:
        return array
    else:
        return array.reshape(shape)


def shape_for_storage(array, field_size=None):
    """Return the shape an array will be stored as.

    :meta private:

    Parameters
    ----------
    array : numpy.ndarray
        The array to be stored.
    field_size : int, optional
        The size of the field.

    Returns
    -------
    tuple of int
        The shape the array will be stored as.

    Examples
    --------
    >>> import numpy as np
    >>> from landlab.field.graph_field import shape_for_storage

    The shape will be such that the first dimension in the field size.

    >>> data = np.arange(6)
    >>> shape_for_storage(data, 3) == (3, 2)
    True
    >>> shape_for_storage(data, 2) == (2, 3)
    True
    >>> shape_for_storage(data, 6) == (6, )
    True

    If a field size is not given, the array will be stored as a
    flattened array.

    >>> shape_for_storage(data) == (6, )
    True
    >>> data = np.arange(6).reshape((3, 2))
    >>> shape_for_storage(data) == (6, )
    True

    For field sizes of 1, the array is always flattened.

    >>> shape_for_storage(data, 1) == (1, 6)
    True

    For scalar arrays, the field size must be 1.

    >>> data = np.array(1.)
    >>> shape_for_storage(data) == (1, )
    True
    >>> shape_for_storage(data, field_size=1) == (1, )
    True

    If the array cannot be shaped into a storage shape, a ``ValueError``
    is raised.

    >>> data = np.array(1.)
    >>> shape_for_storage(data, field_size=4) # DOCTEST: +IGNORE_EXCEPTION_DETAIL
    Traceback (most recent call last):
    ...
    ValueError: unable to reshape array to field size
    >>> data = np.arange(6.)
    >>> shape_for_storage(data, field_size=4) # DOCTEST: +IGNORE_EXCEPTION_DETAIL
    Traceback (most recent call last):
    ...
    ValueError: unable to reshape array to field size
    """
    if field_size is None:
        field_size = array.size

    if array.size % field_size != 0:
        raise ValueError(
            "unable to reshape array to field size ({} != {})".format(
                array.size, field_size
            )
        )

    if field_size == array.size:
        shape = (array.size,)
    else:
        shape = (field_size, array.size // field_size)

    return shape


class FieldDataset(dict):
    """Wrap an xarray.Dataset as a landlab field.

    This is a light wrapping of xarray.Dataset. The main differences
    are that a `FieldDataset` can be created with a size but not
    allocate any memory for data arrays until an array is actually
    needed. The setitem method is also overriden so that when arrays
    are added they are stored reshaped in the landlab style. That
    is, shaped as `(n_elements, values_per_element)`.

    :meta private:

    Examples
    --------
    >>> from landlab.field.graph_field import FieldDataset

    >>> ds = FieldDataset("node")
    >>> ds.size is None
    True
    >>> ds.set_value("air_temperature", [1.0, 1.0, 1.0, 1.0])
    >>> ds["air_temperature"]
    array([ 1.,  1.,  1.,  1.])
    >>> ds.size
    4
    >>> ds.set_value("air_temperature", [1.0, 1.0, 1.0])
    Traceback (most recent call last):
    ValueError: unable to reshape array to field size (3 != 4)

    >>> ds = FieldDataset("node", fixed_size=False)
    >>> ds.size is None
    True
    >>> ds.set_value("air_temperature", [1.0, 1.0, 1.0, 1.0])
    >>> ds.size
    4
    >>> ds.set_value("air_temperature", [0.0, 0.0])
    >>> ds.size
    2
    >>> ds["air_temperature"]
    array([ 0.,  0.])
    """

    def __init__(self, name, size=None, fixed_size=True):
        """Create a container to hold a collection of *Landlab* fields.

        Parameters
        ----------
        name : str
            The name of the group that identifies the dataset of fields.
        size : int, optional
            The size of fields that will be held within the dataset.
            If not provided, the size will be set later.
        fixed_size : bool, optional
            The size of fields held within the dataset cannot change.
        """
        self._name = name
        self._size = None
        self._fixed_size = bool(fixed_size)
        self._ds = xr.Dataset()
        self._units = {}

        self.size = size

        super().__init__()

    @property
    def size(self):
        """Size of the field dataset as number of elements.

        Examples
        --------
        >>> from landlab.field.graph_field import FieldDataset

        >>> ds = FieldDataset("grid", size=1)
        >>> ds.set_value("air_temperature", [1.0, 1.0, 1.0])
        >>> ds.set_value("ground_temperature", [0.0, 0.0])
        >>> ds["ground_temperature"]
        array([[ 0.,  0.]])

        >>> ds = FieldDataset("grid", size=1)
        >>> ds.set_value("air_temperature", 0.1)
        >>> ds["air_temperature"]
        array(0.1)
        """
        return self._size

    @size.setter
    def size(self, size):
        if self._size != size:
            if self._size is not None and self.fixed_size:
                raise ValueError(
                    "size has already been set ({size}) and fixed_size is True".format(
                        size=self._size
                    )
                )
            elif not isinstance(size, int) or size < 0:
                raise ValueError(f"size must be a positive integer or None ({size})")
            self._size = size

    @property
    def fixed_size(self):
        """Flag that indicates if arrays added to the dataset must be of a fixed size.

        Examples
        --------
        >>> from landlab.field.graph_field import FieldDataset

        >>> ds = FieldDataset("node", fixed_size=False)
        >>> ds.set_value("air_temperature", [1.0, 1.0, 1.0, 1.0])
        >>> ds.set_value("air_temperature", [0.0, 0.0])
        >>> ds["air_temperature"]
        array([ 0.,  0.])

        >>> ds.fixed_size = True
        >>> ds.size
        2
        >>> ds.set_value("air_temperature", [1.0, 1.0, 1.0])
        Traceback (most recent call last):
        ValueError: unable to reshape array to field size (4 != 2)
        """
        return self._fixed_size

    @fixed_size.setter
    def fixed_size(self, fixed_size):
        self._fixed_size = bool(fixed_size)
        if self._fixed_size:
            self.size = self._ds.dims[self._name]

    @property
    def units(self):
        """Return units for each of the fields of the dataset."""
        return self._units

    def set_units(self, name, new_units):
        """Assign units to a field.

        Parameters
        ----------
        name : str
            The name of the field.
        new_units : str
            The new units of the field.
        """
        self._units[name] = self._ds[name].attrs["units"] = new_units

    @property
    def dataset(self):
        """Return the dataset as an :class:`xarray.Dataset`."""
        return self._ds

    def keys(self):
        """Return the name of the fields held in the dataset."""
        return list(self._ds.variables)

    def set_value(self, name, value_array, attrs=None):
        """Assign values to a field.

        Parameters
        ----------
        name : str
            Name of the field in the dataset.
        value_array : numpy.ndarray
            Array of values to assign to the field.
        attrs : dict, optional
            A `dict` holding attributes to save with the field.
        """
        attrs = attrs or {}
        attrs.setdefault("units", "?")

        value_array = np.asarray(value_array)

        if name in self._ds and self._ds[name].values is value_array:
            self._ds[name].values.shape = shape_for_storage(value_array, self.size)
            return

        if self.fixed_size:
            value_array = reshape_for_storage(value_array, self.size)
        else:
            value_array = reshape_for_storage(value_array, None)

        if value_array.ndim > 0:
            self.size = value_array.shape[0]
        else:
            self.size = value_array.size

        if value_array.ndim == 0:
            dims = ()
        elif value_array.ndim == 1:
            dims = (self._name,)
        else:
            dims = (self._name, name + "_per_" + self._name)

        if name in self._ds:
            self._ds = self._ds.drop_vars(name)

        self._ds.update({name: xr.DataArray(value_array, dims=dims, attrs=attrs)})
        self._units[name] = attrs["units"]

    def pop(self, name):
        """Remove a field from the dataset.

        Parameters
        ----------
        name : str
            The name of the field to remove.

        Returns
        -------
        numpy.ndarray
            The values of the field that was removed.
        """
        array = self._ds[name].values
        self._ds = self._ds.drop_vars(name)
        return array

    def __getitem__(self, name):
        """Get a field's values by name."""
        if isinstance(name, str):
            try:
                return self._ds[name].values
            except KeyError as exc:
                raise FieldError(name) from exc
        else:
            raise TypeError("field name not a string")

    def __setitem__(self, name, value_array):
        """Assign values to a field by name."""
        self.set_value(name, value_array)

    def __contains__(self, name):
        """Return if the dataset contains a named field.

        Parameters
        ----------
        name : str
            A field name.

        Returns
        -------
        bool
            `True` if the dataset contains the field, otherwise `False`.
        """
        return name in self._ds

    def __str__(self):
        """Return a human-readable string of the dataset."""
        return str(self._ds)

    def __repr__(self):
        """Return a string representation of the dataset."""
        return "FieldDataset({name}, size={size}, fixed_size={fixed_size})".format(
            name=repr(self._name),
            size=repr(self.size),
            fixed_size=repr(self.fixed_size),
        )

    def __len__(self):
        """Return the number of fields held in the dataset."""
        return len(self._ds.variables)

    def __iter__(self):
        """Iterate through the fields in the dataset."""
        return iter(self._ds.variables)


[docs]class GraphFields: """Collection of grouped data-fields. The :class:`GraphFields` class holds a set of data fields that are separated into *groups*. A typical use for this class would be to define the groups as being locations on a grid where the values are defined. For instance, the groups could be *node*, *cell*, *link*, and *face*. Examples -------- Create two groups of data fields defined at *node* and *cell*. Each set can have a different number of values. >>> from landlab.field import GraphFields >>> fields = GraphFields() >>> fields.new_field_location("node", 12) >>> fields.new_field_location("cell", 2) Create some new value arrays for each of the data fields. >>> fields.ones("node") array([ 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]) >>> fields.zeros("cell") array([ 0., 0.]) Create new value arrays and add them to the data fields. Because the data fields are in different groups (node and cell), they can have the same name. >>> fields.add_ones("topographic__elevation", at="node") array([ 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]) >>> fields.at_node["topographic__elevation"] array([ 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]) >>> fields.add_ones("topographic__elevation", at="cell") array([ 1., 1.]) >>> fields.at_cell["topographic__elevation"] array([ 1., 1.]) Each group acts as a :class:`dict` so, for instance, to get the variables names in a group use the :meth:`keys` method, >>> list(fields.at_cell.keys()) ['topographic__elevation'] If the size of the new field location is ``None``, the field will be unsized. This means that fields added to this location can be of any size. >>> fields = GraphFields() >>> fields.new_field_location("grid", None) >>> fields.at_grid["g"] = 9.81 >>> fields.at_grid["g"] array(9.81) >>> fields.at_grid["w"] = (3., 4.) >>> fields.at_grid["w"] array([ 3., 4.]) The dimensions of groups can also be specified when the object is instantiated. In this case, group sizes are specified as a dictionary with keys being group names and values group sizes. >>> fields = GraphFields({"node": 6, "grid": None}) >>> fields.at_grid["g"] = 9.81 >>> fields.at_node["x"] = [0, 1, 2, 3, 4, 5] >>> fields.at_grid["g"] array(9.81) >>> fields.at_node["x"] array([0, 1, 2, 3, 4, 5]) """
[docs] def __init__(self, *args, **kwds): """Create a new collection of field groups.""" try: dims = args[0] except IndexError: dims = {} self._groups = set() for loc in dims: self.new_field_location(loc, dims[loc]) self.default_group = kwds.get("default_group", None)
[docs] def __getitem__(self, name): """Get the collection of fields from the named group.""" try: return getattr(self, "at_" + name) except AttributeError as exc: raise GroupError(name) from exc
@property def default_group(self): """Return the name of the group into which fields are put by default.""" return self._default_group @default_group.setter def default_group(self, loc): if self.has_group(loc) or loc is None: self._default_group = loc else: groups = ", ".join([repr(name) for name in sorted(self._groups)]) raise ValueError(f"{loc!r}: Group does not exists. Not one of {groups}.")
[docs] def new_field_location(self, loc, size=None): """Add a new quantity to a field. Create an empty group into which new fields can be added. The new group is created but no memory allocated yet. The dictionary of the new group can be through a new *at_* attribute of the class instance. Parameters ---------- loc: str Name of the new group to add to the field. size: int, optional Number of elements in the new quantity. If not provided, the size is set to be the size of the first field added to the group. Raises ------ ValueError If the field already contains the group. Examples -------- Create a collection of fields and add two groups, *node* and *cell*, to it. >>> from landlab.field import GraphFields >>> fields = GraphFields() >>> fields.new_field_location("node", 12) >>> fields.new_field_location("cell", 2) The group names in the collection are retrieved with the *groups* attribute as a :class:`set`. >>> names = list(fields.groups) >>> names.sort() >>> names ['cell', 'node'] Access the new (empty) groups with the *at_* attributes. >>> fields.at_cell FieldDataset('cell', size=2, fixed_size=True) >>> fields.at_node FieldDataset('node', size=12, fixed_size=True) >>> fields.new_field_location("core_node") >>> fields.at_core_node.size is None True >>> fields.at_core_node["air__temperature"] = [0, 1] >>> fields.at_core_node.size 2 :meta landlab: field-add """ dataset_name = "at_" + loc if loc not in self._groups: setattr( self, dataset_name, FieldDataset(loc, size, fixed_size=size is not None) ) self._groups.add(loc) else: raise ValueError(f"{loc!r} location already exists")
@property def groups(self): """List of group names. Returns ------- set Names of field groupings. """ return self._groups
[docs] def has_group(self, name): """Check if a group exists. Parameters ---------- name: str Name of the group. Returns ------- bool ``True`` if the field contains *group*, otherwise ``False``. Examples -------- Check if the field has the groups named *node* or *cell*. >>> from landlab.field import GraphFields >>> fields = GraphFields() >>> fields.new_field_location('node', 12) >>> fields.has_group('node') True >>> fields.has_group('cell') False :meta landlab: info-field """ return name in self._groups
[docs] def has_field(self, *args, **kwds): """has_field(field, at=None) Check if a field is in a group. Parameters ---------- field: str Name of the field. at: str, optional Name of the group. Returns ------- bool ``True`` if the group contains the field, otherwise ``False``. Examples -------- Check if the field named ``topographic__elevation`` is contained in a group. >>> from landlab.field import GraphFields >>> fields = GraphFields() >>> fields.new_field_location("node", 12) >>> _ = fields.add_ones("topographic__elevation", at="node") >>> fields.has_field("topographic__elevation", at="node") True >>> fields.has_field("topographic__elevation", at="cell") False >>> fields = GraphFields() >>> fields.new_field_location("node", 12) >>> _ = fields.add_ones("topographic__elevation", at="node") >>> fields.has_field("node", "topographic__elevation") True >>> fields.has_field("cell", "topographic__elevation") False :meta landlab: info-field """ if len(args) == 2: group, field = args elif len(args) == 1: group, field = kwds.pop("at", self.default_group), args[0] else: raise ValueError("number of arguments must be 1 or 2") if group is None: raise ValueError("no group provided") try: return field in self[group] except KeyError: return False
[docs] def keys(self, group): """Return the field names in a group. Parameters ---------- group : str Group name. Returns ------- list Names of fields held in the given group. Examples -------- >>> from landlab.field import GraphFields >>> fields = GraphFields() >>> fields.new_field_location("node", 4) >>> list(fields.keys("node")) [] >>> _ = fields.add_empty("topographic__elevation", at="node") >>> list(fields.keys("node")) ['topographic__elevation'] :meta landlab: info-field """ return self[group].keys()
[docs] def size(self, group): """Return the size of the arrays stored in a group. Parameters ---------- group : str Group name. Returns ------- int Array size. Examples -------- >>> from landlab.field import GraphFields >>> fields = GraphFields() >>> fields.new_field_location("node", 4) >>> fields.size("node") 4 :meta landlab: info-grid, info-field """ return self[group].size
[docs] def field_values(self, *args, **kwds): """field_values(field, at=None) Return the values of a field. Given a *group* and a *field*, return a reference to the associated data array. Parameters ---------- field: str Name of the field within *group*. at: str, optional Name of the group. Returns ------- array The values of the field. Raises ------ landlab.field.errors.GroupError If *group* does not exist landlab.field.errors.FieldError If *field* does not exist Examples -------- Create a group of fields called *node*. >>> from landlab.field import GraphFields >>> fields = GraphFields() >>> fields.new_field_location("node", 4) Add a field, initialized to ones, called *topographic__elevation* to the *node* group. The *field_values* method returns a reference to the field's data. >>> _ = fields.add_ones("topographic__elevation", at="node") >>> fields.field_values("topographic__elevation", at="node") array([ 1., 1., 1., 1.]) Raise FieldError if *field* does not exist in *group*. >>> fields.field_values("planet_surface__temperature", at="node") ... # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): FieldError: planet_surface__temperature If *group* does not exists, raise :class:`~landlab.field.errors.GroupError`. >>> fields.field_values("topographic__elevation", at="cell") ... # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): GroupError: cell :meta landlab: field-io """ if len(args) == 2: group, field = args elif len(args) == 1: group, field = kwds.pop("at", self.default_group), args[0] else: raise ValueError("number of arguments must be 1 or 2") if group is None: raise ValueError("no group provided") try: fields = self[group] except KeyError as exc: groups = ", ".join([repr(g) for g in sorted(self._groups)]) raise GroupError(f"{group!r}: Not one of {groups}.") from exc try: return fields[field] except KeyError as exc: raise FieldError(f"{field!r}") from exc
[docs] def return_array_or_field_values(self, *args, **kwds): """return_array_or_field_values(field, at=None) Return field given a field name, or array of with the correct shape. Given a *group* and a *field*, return a reference to the associated data array. *field* is either a string that is a field in the group or an array of the correct size. This function is meant to serve like the :class:`~landlab.utils.decorators.use_field_name_or_array` decorator for bound functions. Parameters ---------- field: str or array Name of the field withing *group*. at: str, optional Name of the group. Returns ------- numpy.ndarray The values of the field. Raises ------ landlab.field.errors.GroupError If *group* does not exist landlab.field.errors.FieldError If *field* does not exist Examples -------- Create a group of fields called *node*. >>> import numpy as np >>> from landlab.field import GraphFields >>> fields = GraphFields() >>> fields.new_field_location('node', 4) Add a field, initialized to ones, called *topographic__elevation* to the *node* group. The *field_values* method returns a reference to the field's data. >>> _ = fields.add_ones("topographic__elevation", at="node") >>> fields.field_values("topographic__elevation", at="node") array([ 1., 1., 1., 1.]) Alternatively, if the second argument is an array, its size is checked and returned if correct. >>> vals = np.array([4., 5., 7., 3.]) >>> fields.return_array_or_field_values(vals, at="node") array([ 4., 5., 7., 3.]) Raise FieldError if *field* does not exist in *group*. >>> fields.return_array_or_field_values("surface__temperature", at="node") ... # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): FieldError: surface__temperature If *group* does not exists, Raise GroupError. >>> fields.return_array_or_field_values("topographic__elevation", at="cell") ... # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): GroupError: cell And if the array of values provided is incorrect, raise a :class:`ValueError`. >>> vals = np.array([3., 2., 1.]) >>> fields.return_array_or_field_values(vals, at="node") ... # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ValueError: Array has incorrect size. :meta landlab: field-io """ if len(args) == 2: group, field = args elif len(args) == 1: group, field = kwds.pop("at", self.default_group), args[0] else: raise ValueError("number of arguments must be 1 or 2") if group is None: raise ValueError("no group provided") if isinstance(field, str): vals = self.field_values(group, field) else: vals = np.asarray(field) if vals.size != self[group].size: raise ValueError( f"Incorrect array size. The array size of {vals.size} does not " "match that of the group, {group!r}, which has a size of " f"{self[group].size}." ) return vals
[docs] def field_units(self, *args, **kwds): """field_units(field, at=None) Get units for a field. Returns the unit string associated with the data array in *group* and *field*. Parameters ---------- field: str Name of the field withing *group*. at: str, optional Name of the group. Returns ------- str The units of the field. Raises ------ KeyError If either *field* or *group* does not exist. :meta landlab: info-field """ if len(args) == 2: group, field = args elif len(args) == 1: group, field = kwds.pop("at", self.default_group), args[0] else: raise ValueError("number of arguments must be 1 or 2") if group is None: raise ValueError("no group provided") return self[group]._ds[field].attrs["units"]
[docs] def empty(self, *args, **kwds): """Uninitialized array whose size is that of the field. Return a new array of the data field size, without initializing entries. Keyword arguments are the same as that for the equivalent *numpy* function. Parameters ---------- group : str Name of the group. See Also -------- numpy.empty : See for a description of optional keywords. GraphFields.ones : Equivalent method that initializes the data to 1. GraphFields.zeros : Equivalent method that initializes the data to 0. Examples -------- >>> from landlab.field import GraphFields >>> field = GraphFields() >>> field.new_field_location("node", 4) >>> field.empty("node") # doctest: +SKIP array([ 2.31584178e+077, -2.68156175e+154, 9.88131292e-324, ... 2.78134232e-309]) # Uninitialized memory Note that a new field is *not* added to the collection of fields. >>> list(field.keys("node")) [] :meta landlab: field-add """ if len(args) == 0: group = kwds.pop("at", kwds.pop("centering", "node")) else: group = args[0] if group == "grid": raise ValueError( "ones is not supported for at='grid', if you " "want to create a field at the grid, use\n" "grid.at_grid['value_name']=value\n" "instead.\nAlternatively, if you want ones" "of the shape stored at_grid, use np.array(1)." ) size = getattr(self, f"at_{group}").size if size is None: raise ValueError("{group!r}: Group is not yet sized.") return np.empty(size, **kwds)
[docs] def ones(self, *args, **kwds): """Array, initialized to 1, whose size is that of the field. Return a new array of the data field size, filled with ones. Keyword arguments are the same as that for the equivalent *numpy* function. Parameters ---------- group : str Name of the group. See Also -------- numpy.ones : See for a description of optional keywords. GraphFields.empty : Equivalent method that does not initialize the new array. GraphFields.zeros : Equivalent method that initializes the data to 0. Examples -------- >>> from landlab.field import GraphFields >>> field = GraphFields() >>> field.new_field_location("node", 4) >>> field.ones("node") array([ 1., 1., 1., 1.]) >>> field.ones("node", dtype=int) array([1, 1, 1, 1]) Note that a new field is *not* added to the collection of fields. >>> list(field.keys("node")) [] :meta landlab: field-add """ allocated = self.empty(*args, **kwds) allocated.fill(1) return allocated
[docs] def zeros(self, *args, **kwds): """Array, initialized to 0, whose size is that of the field. Parameters ---------- group : str Name of the group. Return a new array of the data field size, filled with zeros. Keyword arguments are the same as that for the equivalent *numpy* function. This method is not valid for the group *grid*. See Also -------- numpy.zeros : See for a description of optional keywords. GraphFields.empty : Equivalent method that does not initialize the new array. GraphFields.ones : Equivalent method that initializes the data to 1. Examples -------- >>> from landlab.field import GraphFields >>> field = GraphFields() >>> field.new_field_location("node", 4) >>> field.zeros("node") array([ 0., 0., 0., 0.]) Note that a new field is *not* added to the collection of fields. >>> list(field.keys("node")) [] :meta landlab: field-add """ allocated = self.empty(*args, **kwds) allocated.fill(0) return allocated
[docs] def add_field(self, *args, **kwds): """add_field(name, value_array, at='node', units='-', copy=False, clobber=False) Add an array of values to the field. Add an array of data values to a collection of fields and associate it with the key, *name*. Use the *copy* keyword to, optionally, add a copy of the provided array. In the case of adding to the collection *grid*, the added field is a *numpy* scalar rather than a *numpy* array. Parameters ---------- name : str Name of the new field to add. value_array : numpy.array Array of values to add to the field. at : str, optional Grid location to store values. If not given, values are assumed to be on `node`. units : str, optional Optionally specify the units of the field. copy : bool, optional If True, add a *copy* of the array to the field. Otherwise save add a reference to the array. clobber : bool, optional Raise an exception if adding to an already existing field. Returns ------- numpy.ndarray The data array added to the field. Depending on the *copy* keyword, this could be a copy of *value_array* or *value_array* itself. Raises ------ ValueError If *value_array* has a size different from the field. Examples -------- >>> import numpy as np >>> from landlab.field import GraphFields >>> field = GraphFields() >>> field.new_field_location("node", 4) >>> values = np.ones(4, dtype=int) >>> field.add_field("topographic__elevation", values, at="node") array([1, 1, 1, 1]) A new field is added to the collection of fields. The saved value array is the same as the one initially created. >>> field.at_node["topographic__elevation"] is values True If you want to save a copy of the array, use the *copy* keyword. In addition, adding values to an existing field will remove the reference to the previously saved array. The *clobber=False* keyword changes this behavior to raise an exception in such a case. >>> field.add_field( ... "topographic__elevation", values, at="node", copy=True, clobber=True ... ) array([1, 1, 1, 1]) >>> field.at_node["topographic__elevation"] is values False >>> field.add_field( ... "topographic__elevation", values, at="node", clobber=False ... ) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): FieldError: topographic__elevation :meta landlab: field-add """ if len(args) == 3: at, name, value_array = args elif len(args) == 2: at, name, value_array = (kwds.pop("at", None), args[0], args[1]) else: raise ValueError("number of arguments must be 2 or 3") units = kwds.get("units", "?") copy = kwds.get("copy", False) clobber = kwds.get("clobber", False) value_array = np.asarray(value_array) at = at or self.default_group if at is None: raise ValueError("no group specified") attrs = {"long_name": name, "units": units} if copy: value_array = value_array.copy() ds = getattr(self, "at_" + at) if not clobber and name in ds: raise FieldError( f"Unable to add the field, {name!r}, to the group, {at!r}, because a field " "with that name already exists in that group. " "Use `clobber=True` to replace the existing field. " f"For example, grid.add_field({name!r}, at={at!r}, clobber=True)" ) dims = (at,) if value_array.ndim > 1: dims += (name + "_per_" + at,) value_array = value_array.reshape((value_array.shape[0], -1)) ds.set_value(name, value_array, attrs=attrs) # ds[name] = value_array return ds[name]
[docs] def delete_field(self, loc, name): """Erases an existing field. Parameters ---------- loc : str Name of the group. name: str Name of the field. Raises ------ KeyError If the named field does not exist. :meta landlab: field-add """ try: ds = getattr(self, "at_" + loc) except AttributeError as exc: raise KeyError(loc) from exc ds._ds = ds._ds.drop_vars(name)
[docs] def add_empty(self, *args, **kwds): """add_empty(name, at='node', units='-', clobber=False) Create and add an uninitialized array of values to the field. Create a new array of the data field size, without initializing entries, and add it to the field as *name*. The *units* keyword gives the units of the new fields as a string. Remaining keyword arguments are the same as that for the equivalent *numpy* function. This method is not valid for the group *grid*. Parameters ---------- name : str Name of the new field to add. at : str, optional Grid location to store values. If not given, values are assumed to be on `node`. units : str, optional Optionally specify the units of the field. clobber : bool, optional Raise an exception if adding to an already existing field. Returns ------- numpy.ndarray A reference to the newly-created array. See Also -------- numpy.empty : See for a description of optional keywords. GraphFields.empty : Equivalent method that does not initialize the new array. GraphFields.zeros : Equivalent method that initializes the data to 0. :meta landlab: field-add """ if len(args) == 2: loc, name = args elif len(args) == 1: loc, name = kwds.pop("at"), args[0] else: raise ValueError("number of arguments must be 1 or 2") units = kwds.pop("units", "?") copy = kwds.pop("copy", False) clobber = kwds.pop("clobber", False) return self.add_field( name, self.empty(at=loc, **kwds), at=loc, units=units, copy=copy, clobber=clobber, )
[docs] def add_ones(self, *args, **kwds): """add_ones(name, at='node', units='-', clobber=False) Create and add an array of values, initialized to 1, to the field. Create a new array of the data field size, filled with ones, and add it to the field as *name*. The *units* keyword gives the units of the new fields as a string. Remaining keyword arguments are the same as that for the equivalent *numpy* function. This method is not valid for the group *grid*. Parameters ---------- name : str Name of the new field to add. at : str, optional Grid location to store values. If not given, values are assumed to be on `node`. units : str, optional Optionally specify the units of the field. clobber : bool, optional Raise an exception if adding to an already existing field. Returns ------- numpy.ndarray A reference to the newly-created array. See Also -------- numpy.ones : See for a description of optional keywords. GraphFields.add_empty : Equivalent method that does not initialize the new array. GraphFields.add_zeros : Equivalent method that initializes the data to 0. Examples -------- Add a new, named field to a collection of fields. >>> from landlab.field import GraphFields >>> field = GraphFields() >>> field.new_field_location('node', 4) >>> field.add_ones("topographic__elevation", at="node") array([ 1., 1., 1., 1.]) >>> list(field.keys('node')) ['topographic__elevation'] >>> field['node']['topographic__elevation'] array([ 1., 1., 1., 1.]) >>> field.at_node['topographic__elevation'] array([ 1., 1., 1., 1.]) :meta landlab: field-add """ data = self.add_empty(*args, **kwds) data.fill(1) return data
[docs] def add_zeros(self, *args, **kwds): """add_zeros(name, at='node', units='-', clobber=False) Create and add an array of values, initialized to 0, to the field. Create a new array of the data field size, filled with zeros, and add it to the field as *name*. The *units* keyword gives the units of the new fields as a string. Remaining keyword arguments are the same as that for the equivalent *numpy* function. :meta landlab: field-add Parameters ---------- name : str Name of the new field to add. at : str, optional Grid location to store values. If not given, values are assumed to be on `node`. units : str, optional Optionally specify the units of the field. clobber : bool, optional Raise an exception if adding to an already existing field. Returns ------- array : A reference to the newly-created array. See also -------- numpy.zeros : See for a description of optional keywords. GraphFields.add_empty : Equivalent method that does not initialize the new array. GraphFields.add_ones : Equivalent method that initializes the data to 1. :meta landlab: field-add """ data = self.add_empty(*args, **kwds) data.fill(0) return data
[docs] def add_full(self, *args, **kwds): """Create and add an array of values, fill with `fill_value`. Parameters ---------- name : str Name of the new field to add. fill_value : scalar Fill value. at : str, optional Grid location to store values. If not given, values are assumed to be on `node`. units : str, optional Optionally specify the units of the field. copy : bool, optional If True, add a *copy* of the array to the field. Otherwise save add a reference to the array. clobber : bool, optional Raise an exception if adding to an already existing field. Returns ------- numpy.ndarray A reference to the newly-created array. :meta landlab: field-add """ if len(args) == 3: at, name, fill_value = args elif len(args) == 2: at = kwds.pop("at", "node") name, fill_value = args else: raise ValueError("number of arguments must be 2 or 3") data = self.add_empty(name, at=at, **kwds) data.fill(fill_value) return data