"""
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)