Source code for podpac.core.coordinates.coordinates1d

"""
One-Dimensional Coordinates
"""


from __future__ import division, unicode_literals, print_function, absolute_import

import copy

import numpy as np
import traitlets as tl

from podpac.core.utils import ArrayTrait, TupleTrait
from podpac.core.coordinates.utils import make_coord_value, make_coord_delta, make_coord_delta_array
from podpac.core.coordinates.utils import add_coord, divide_delta, lower_precision_time_bounds
from podpac.core.coordinates.utils import Dimension
from podpac.core.coordinates.base_coordinates import BaseCoordinates


[docs]class Coordinates1d(BaseCoordinates): """ Base class for 1-dimensional coordinates. Coordinates1d objects contain values and metadata for a single dimension of coordinates. :class:`podpac.Coordinates` and :class:`StackedCoordinates` use Coordinate1d objects. Parameters ---------- name : str Dimension name, one of 'lat', 'lon', 'time', or 'alt'. coordinates : array, read-only Full array of coordinate values. See Also -------- :class:`ArrayCoordinates1d`, :class:`UniformCoordinates1d` """ name = Dimension(allow_none=True) _properties = tl.Set() @tl.observe("name") def _set_property(self, d): if d["new"] is not None: self._properties.add(d["name"]) def _set_name(self, value): # set name if it is not set already, otherwise check that it matches if "name" not in self._properties and value is not None: self.name = value elif self.name != value: raise ValueError("Dimension mismatch, %s != %s" % (value, self.name)) # ------------------------------------------------------------------------------------------------------------------ # standard methods # ------------------------------------------------------------------------------------------------------------------ def __repr__(self): if self.name is None: name = "%s" % (self.__class__.__name__,) else: name = "%s(%s)" % (self.__class__.__name__, self.name) if self.ndim == 1: desc = "Bounds[%s, %s], N[%d]" % (self.bounds[0], self.bounds[1], self.size) else: desc = "Bounds[%s, %s], N[%s], Shape%s" % (self.bounds[0], self.bounds[1], self.size, self.shape) return "%s: %s" % (name, desc) def _eq_base(self, other): """used by child __eq__ methods for common checks""" if not isinstance(other, Coordinates1d): return False # defined coordinate properties should match for name in self._properties.union(other._properties): if getattr(self, name) != getattr(other, name): return False # shortcuts (not strictly necessary) for name in ["shape", "is_monotonic", "is_descending", "is_uniform"]: if getattr(self, name) != getattr(other, name): return False return True def __len__(self): return self.shape[0] def __contains__(self, item): try: item = make_coord_value(item) except: return False if type(item) != self.dtype: return False return item in self.coordinates # ------------------------------------------------------------------------------------------------------------------ # Properties # ------------------------------------------------------------------------------------------------------------------ @property def dims(self): if self.name is None: raise TypeError("cannot access dims property of unnamed Coordinates1d") return (self.name,) @property def xcoords(self): """:dict: xarray coords""" if self.name is None: raise ValueError("Cannot get xcoords for unnamed Coordinates1d") return {self.name: (self.xdims, self.coordinates)} @property def dtype(self): """:type: Coordinates dtype. ``float`` for numerical coordinates and numpy ``datetime64`` for datetime coordinates. """ raise NotImplementedError @property def deltatype(self): if self.dtype is np.datetime64: return np.timedelta64 else: return self.dtype @property def is_monotonic(self): raise NotImplementedError @property def is_descending(self): raise NotImplementedError @property def is_uniform(self): raise NotImplementedError @property def start(self): raise NotImplementedError @property def stop(self): raise NotImplementedError @property def step(self): raise NotImplementedError @property def bounds(self): """Low and high coordinate bounds.""" raise NotImplementedError @property def properties(self): """:dict: Dictionary of the coordinate properties.""" return {key: getattr(self, key) for key in self._properties} @property def definition(self): """:dict: Serializable 1d coordinates definition.""" return self._get_definition(full=False) @property def full_definition(self): """:dict: Serializable 1d coordinates definition, containing all properties. For internal use.""" return self._get_definition(full=True) def _get_definition(self, full=True): raise NotImplementedError @property def _full_properties(self): return {"name": self.name} # ------------------------------------------------------------------------------------------------------------------ # Methods # ------------------------------------------------------------------------------------------------------------------
[docs] def copy(self): """ Make a deep copy of the 1d Coordinates. Returns ------- :class:`Coordinates1d` Copy of the coordinates. """ raise NotImplementedError
[docs] def simplify(self): """Get the simplified/optimized representation of these coordinates. Returns ------- simplified : Coordinates1d simplified version of the coordinates """ raise NotImplementedError
[docs] def get_area_bounds(self, boundary): """ Get low and high coordinate area bounds. Arguments --------- boundary : float, timedelta, array, None Boundary offsets in this dimension. * For a centered uniform boundary (same for every coordinate), use a single positive float or timedelta offset. This represents the "total segment length" / 2. * For a uniform boundary (segment or polygon same for every coordinate), use an array of float or timedelta offsets * For a fully specified boundary, use an array of boundary arrays (2-D array, N_coords x boundary spec), one per coordinate. The boundary_spec can be a single number, two numbers, or an array of numbers. * For point coordinates, use None. Returns ------- low: float, np.datetime64 low area bound high: float, np.datetime64 high area bound """ # point coordinates if boundary is None: return self.bounds # empty coordinates if self.size == 0: return self.bounds if np.array(boundary).ndim == 0: # shortcut for uniform centered boundary boundary = make_coord_delta(boundary) lo_offset = -boundary hi_offset = boundary elif np.array(boundary).ndim == 1: # uniform boundary polygon boundary = make_coord_delta_array(boundary) lo_offset = min(boundary) hi_offset = max(boundary) else: L, H = self.argbounds lo_offset = min(make_coord_delta_array(boundary[L])) hi_offset = max(make_coord_delta_array(boundary[H])) lo, hi = self.bounds lo = add_coord(lo, lo_offset) hi = add_coord(hi, hi_offset) return lo, hi
def _select_empty(self, return_index): I = [] if return_index: return self[I], I else: return self[I] def _select_full(self, return_index): I = slice(None) if return_index: return self[I], I else: return self[I]
[docs] def select(self, bounds, return_index=False, outer=False): """ Get the coordinate values that are within the given bounds. The default selection returns coordinates that are within the bounds:: In [1]: c = ArrayCoordinates1d([0, 1, 2, 3], name='lat') In [2]: c.select([1.5, 2.5]).coordinates Out[2]: array([2.]) The *outer* selection returns the minimal set of coordinates that contain the bounds:: In [3]: c.select([1.5, 2.5], outer=True).coordinates Out[3]: array([1., 2., 3.]) The *outer* selection also returns a boundary coordinate if a bound is outside this coordinates bounds but *inside* its area bounds:: In [4]: c.select([3.25, 3.35], outer=True).coordinates Out[4]: array([3.0], dtype=float64) In [5]: c.select([10.0, 11.0], outer=True).coordinates Out[5]: array([], dtype=float64) Parameters ---------- bounds : (low, high) or dict Selection bounds. If a dictionary of dim -> (low, high) bounds is supplied, the bounds matching these coordinates will be selected if available, otherwise the full coordinates will be returned. outer : bool, optional If True, do an *outer* selection. Default False. return_index : bool, optional If True, return index for the selection in addition to coordinates. Default False. Returns ------- selection : :class:`Coordinates1d` Coordinates1d object with coordinates within the bounds. I : slice, boolean array index or slice for the selected coordinates (only if return_index=True) """ # empty case if self.dtype is None: return self._select_empty(return_index) if isinstance(bounds, dict): bounds = bounds.get(self.name) if bounds is None: return self._select_full(return_index) bounds = make_coord_value(bounds[0]), make_coord_value(bounds[1]) # check type if not isinstance(bounds[0], self.dtype): raise TypeError( "Input bounds do match the coordinates dtype (%s != %s)" % (type(self.bounds[0]), self.dtype) ) if not isinstance(bounds[1], self.dtype): raise TypeError( "Input bounds do match the coordinates dtype (%s != %s)" % (type(self.bounds[1]), self.dtype) ) my_bounds = self.bounds # If the bounds are of instance datetime64, then the comparison should happen at the lowest precision if self.dtype == np.datetime64: my_bounds, bounds = lower_precision_time_bounds(my_bounds, bounds, outer) # full if my_bounds[0] >= bounds[0] and my_bounds[1] <= bounds[1]: return self._select_full(return_index) # none if my_bounds[0] > bounds[1] or my_bounds[1] < bounds[0]: return self._select_empty(return_index) # partial, implemented in child classes return self._select(bounds, return_index, outer)
def _select(self, bounds, return_index, outer): raise NotImplementedError def _transform(self, transformer): if self.name != "alt": # this assumes that the transformer does not have a spatial transform return self.copy() # transform "alt" coordinates from podpac.core.coordinates.array_coordinates1d import ArrayCoordinates1d _, _, tcoordinates = transformer.transform(np.zeros(self.shape), np.zeros(self.shape), self.coordinates) return ArrayCoordinates1d(tcoordinates, **self.properties)
[docs] def issubset(self, other): """Report whether other coordinates contains these coordinates. Arguments --------- other : Coordinates, Coordinates1d Other coordinates to check Returns ------- issubset : bool True if these coordinates are a subset of the other coordinates. """ from podpac.core.coordinates import Coordinates if isinstance(other, Coordinates): if self.name not in other.dims: return False other = other[self.name] # short-cuts that don't require checking coordinates if self.size == 0: return True if other.size == 0: return False if self.dtype != other.dtype: return False if self.bounds[0] < other.bounds[0] or self.bounds[1] > other.bounds[1]: return False # check actual coordinates using built-in set method issubset # for datetimes, convert to the higher resolution my_coordinates = self.coordinates.ravel() other_coordinates = other.coordinates.ravel() if self.dtype == np.datetime64: if my_coordinates[0].dtype < other_coordinates[0].dtype: my_coordinates = my_coordinates.astype(other_coordinates.dtype) elif other_coordinates[0].dtype < my_coordinates[0].dtype: other_coordinates = other_coordinates.astype(my_coordinates.dtype) return set(my_coordinates).issubset(other_coordinates)