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