Source code for podpac.core.coordinates.array_coordinates1d

"""
Single-Dimensional Coordinates: Array
"""


from __future__ import division, unicode_literals, print_function, absolute_import

import copy
from collections import OrderedDict

import numpy as np
import traitlets as tl
from collections import OrderedDict

from podpac.core.utils import ArrayTrait
from podpac.core.coordinates.utils import make_coord_array, higher_precision_time_bounds
from podpac.core.coordinates.coordinates1d import Coordinates1d


[docs]class ArrayCoordinates1d(Coordinates1d): """ 1-dimensional array of coordinates. ArrayCoordinates1d is a basic array of 1d coordinates created from an array of coordinate values. Numerical coordinates values are converted to ``float``, and time coordinate values are converted to numpy ``datetime64``. For convenience, podpac automatically converts datetime strings such as ``'2018-01-01'`` to ``datetime64``. The coordinate values must all be of the same type. Parameters ---------- name : str Dimension name, one of 'lat', 'lon', 'time', or 'alt'. coordinates : array, read-only Full array of coordinate values. See Also -------- :class:`Coordinates1d`, :class:`UniformCoordinates1d` """ coordinates = ArrayTrait(read_only=True) _is_monotonic = None _is_descending = None _is_uniform = None _step = None _start = None _stop = None
[docs] def __init__(self, coordinates, name=None, **kwargs): """ Create 1d coordinates from an array. Arguments --------- coordinates : array-like coordinate values. name : str, optional Dimension name, one of 'lat', 'lon', 'time', or 'alt'. """ # validate and set coordinates coordinates = make_coord_array(coordinates) self.set_trait("coordinates", coordinates) self.not_a_trait = coordinates # precalculate once if self.coordinates.size == 0: pass elif self.coordinates.size == 1: self._is_monotonic = True elif self.coordinates.ndim > 1: self._is_monotonic = None self._is_descending = None self._is_uniform = None else: deltas = self.deltas if np.any(deltas <= 0): self._is_monotonic = False self._is_descending = False self._is_uniform = False else: self._is_monotonic = True self._is_descending = self.coordinates[1] < self.coordinates[0] self._is_uniform = np.allclose(deltas, deltas[0]) if self._is_uniform: self._start = self.coordinates[0] self._stop = self.coordinates[-1] self._step = (self._stop - self._start) / (self.coordinates.size - 1) # set common properties super(ArrayCoordinates1d, self).__init__(name=name, **kwargs)
def __eq__(self, other): if not self._eq_base(other): return False if not np.array_equal(self.coordinates, other.coordinates): return False return True # ------------------------------------------------------------------------------------------------------------------ # Alternate Constructors # ------------------------------------------------------------------------------------------------------------------
[docs] @classmethod def from_xarray(cls, x, **kwargs): """ Create 1d Coordinates from named xarray coordinates. Arguments --------- x : xarray.DataArray Nade DataArray of the coordinate values Returns ------- :class:`ArrayCoordinates1d` 1d coordinates """ return cls(x.data, name=x.name, **kwargs).simplify()
[docs] @classmethod def from_definition(cls, d): """ Create 1d coordinates from a coordinates definition. The definition must contain the coordinate values:: c = ArrayCoordinates1d.from_definition({ "values": [0, 1, 2, 3] }) The definition may also contain any of the 1d Coordinates properties:: c = ArrayCoordinates1d.from_definition({ "values": [0, 1, 2, 3], "name": "lat" }) Arguments --------- d : dict 1d coordinates array definition Returns ------- :class:`ArrayCoordinates1d` 1d Coordinates See Also -------- definition """ if "values" not in d: raise ValueError('ArrayCoordinates1d definition requires "values" property') coordinates = d["values"] kwargs = {k: v for k, v in d.items() if k != "values"} return cls(coordinates, **kwargs)
[docs] def copy(self): """ Make a deep copy of the 1d Coordinates array. Returns ------- :class:`ArrayCoordinates1d` Copy of the coordinates. """ return ArrayCoordinates1d(self.coordinates, **self.properties)
[docs] def unique(self, return_index=False): """ Remove duplicate coordinate values from each dimension. Arguments --------- return_index : bool, optional If True, return index for the unique coordinates in addition to the coordinates. Default False. Returns ------- unique : :class:`ArrayCoordinates1d` New ArrayCoordinates1d object with unique, sorted coordinate values. unique_index : list of indices index """ # shortcut, monotonic coordinates are already unique if self.is_monotonic: if return_index: return self.flatten(), np.arange(self.size).tolist() else: return self.flatten() a, I = np.unique(self.coordinates, return_index=True) if return_index: return self.flatten()[I], I else: return self.flatten()[I]
[docs] def simplify(self): """Get the simplified/optimized representation of these coordinates. Returns ------- :class:`ArrayCoordinates1d`, :class:`UniformCoordinates1d` UniformCoordinates1d if the coordinates are uniform, otherwise ArrayCoordinates1d """ from podpac.core.coordinates.uniform_coordinates1d import UniformCoordinates1d if self.is_uniform: return UniformCoordinates1d(self.start, self.stop, self.step, **self.properties) return self
[docs] def flatten(self): """ Get a copy of the coordinates with a flattened array (wraps numpy.flatten). Returns ------- :class:`ArrayCoordinates1d` Flattened coordinates. """ if self.ndim == 1: return self.copy() return ArrayCoordinates1d(self.coordinates.flatten(), **self.properties)
[docs] def reshape(self, newshape): """ Get a copy of the coordinates with a reshaped array (wraps numpy.reshape). Arguments --------- newshape: int, tuple The new shape. Returns ------- :class:`ArrayCoordinates1d` Reshaped coordinates. """ return ArrayCoordinates1d(self.coordinates.reshape(newshape), **self.properties)
# ------------------------------------------------------------------------------------------------------------------ # standard methods, array-like # ------------------------------------------------------------------------------------------------------------------ def __getitem__(self, index): # The following 3 lines are copied by UniformCoordinates1d.__getitem__ if self.ndim == 1 and np.ndim(index) > 1 and np.array(index).dtype == int: index = np.array(index).flatten().tolist() try: return ArrayCoordinates1d(self.coordinates[index], **self.properties) except IndexError as e: # This happens when index is a list, but should be a tuple if isinstance(index, list): return ArrayCoordinates1d(self.coordinates[tuple(index)], **self.properties) raise (e) # ------------------------------------------------------------------------------------------------------------------ # Properties # ------------------------------------------------------------------------------------------------------------------ @property def deltas(self): return (self.coordinates[1:] - self.coordinates[:-1]).astype(float) * np.sign( self.coordinates[1] - self.coordinates[0] ).astype(float) @property def ndim(self): return self.coordinates.ndim @property def size(self): """Number of coordinates.""" return self.coordinates.size @property def shape(self): return self.coordinates.shape @property def dtype(self): """:type: Coordinates dtype. ``float`` for numerical coordinates and numpy ``datetime64`` for datetime coordinates. """ if self.size == 0: return None elif self.coordinates.dtype == float: return float elif np.issubdtype(self.coordinates.dtype, np.datetime64): return np.datetime64 @property def is_monotonic(self): return self._is_monotonic @property def is_descending(self): return self._is_descending @property def is_uniform(self): return self._is_uniform @property def start(self): return self._start @property def stop(self): return self._stop @property def step(self): return self._step @property def bounds(self): """Low and high coordinate bounds.""" if self.size == 0: lo, hi = np.nan, np.nan elif self.is_monotonic: lo, hi = sorted([self.coordinates[0], self.coordinates[-1]]) elif self.dtype is np.datetime64: lo, hi = np.min(self.coordinates), np.max(self.coordinates) else: lo, hi = np.nanmin(self.coordinates), np.nanmax(self.coordinates) return lo, hi @property def argbounds(self): if self.size == 0: raise RuntimeError("Cannot get argbounds for empty coordinates") if not self.is_monotonic: argbounds = np.argmin(self.coordinates), np.argmax(self.coordinates) return np.unravel_index(argbounds[0], self.shape), np.unravel_index(argbounds[1], self.shape) elif not self.is_descending: return 0, -1 else: return -1, 0 def _get_definition(self, full=True): d = OrderedDict() d["values"] = self.coordinates d.update(self._full_properties if full else self.properties) return d # ------------------------------------------------------------------------------------------------------------------ # Methods # ------------------------------------------------------------------------------------------------------------------ def _select(self, bounds, return_index, outer): if self.dtype == np.datetime64: _, bounds = higher_precision_time_bounds(self.bounds, bounds, outer) if not outer: gt = self.coordinates >= bounds[0] lt = self.coordinates <= bounds[1] b = gt & lt elif self.is_monotonic: gt = np.where(self.coordinates >= bounds[0])[0] lt = np.where(self.coordinates <= bounds[1])[0] lo, hi = bounds[0], bounds[1] if self.is_descending: lt, gt = gt, lt lo, hi = hi, lo if self.coordinates[gt[0]] != lo: gt[0] -= 1 if self.coordinates[lt[-1]] != hi: lt[-1] += 1 start = max(0, gt[0]) stop = min(self.size - 1, lt[-1]) b = slice(start, stop + 1) else: try: gt = self.coordinates >= max(self.coordinates[self.coordinates <= bounds[0]]) except ValueError as e: if self.dtype == np.datetime64: gt = ~np.isnat(self.coordinates) else: gt = self.coordinates >= -np.inf try: lt = self.coordinates <= min(self.coordinates[self.coordinates >= bounds[1]]) except ValueError as e: if self.dtype == np.datetime64: lt = ~np.isnat(self.coordinates) else: lt = self.coordinates <= np.inf b = gt & lt if return_index: return self[b], b else: return self[b]