diff --git a/podpac/core/coordinates/coordinates1d.py b/podpac/core/coordinates/coordinates1d.py index 91abbfdea..cecee4176 100644 --- a/podpac/core/coordinates/coordinates1d.py +++ b/podpac/core/coordinates/coordinates1d.py @@ -12,7 +12,7 @@ from podpac.core.utils import ArrayTrait 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 +from podpac.core.coordinates.utils import add_coord, divide_delta, lower_precision_time_bounds from podpac.core.coordinates.utils import Dimension, CoordinateType from podpac.core.coordinates.base_coordinates import BaseCoordinates @@ -350,6 +350,10 @@ def select(self, bounds, return_indices=False, outer=False): index or slice for the selected coordinates (only if return_indices=True) """ + # empty case + if self.dtype is None: + return self._select_empty(return_indices) + if isinstance(bounds, dict): bounds = bounds.get(self.name) if bounds is None: @@ -357,12 +361,28 @@ def select(self, bounds, return_indices=False, outer=False): 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.area_bounds.copy() + + # 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 self.bounds[0] >= bounds[0] and self.bounds[1] <= bounds[1]: + if my_bounds[0] >= bounds[0] and my_bounds[1] <= bounds[1]: return self._select_full(return_indices) # none - if self.area_bounds[0] > bounds[1] or self.area_bounds[1] < bounds[0]: + if my_bounds[0] > bounds[1] or my_bounds[1] < bounds[0]: return self._select_empty(return_indices) # partial, implemented in child classes diff --git a/podpac/core/coordinates/test/test_array_coordinates1d.py b/podpac/core/coordinates/test/test_array_coordinates1d.py index bc9df5093..2892232b4 100644 --- a/podpac/core/coordinates/test/test_array_coordinates1d.py +++ b/podpac/core/coordinates/test/test_array_coordinates1d.py @@ -1039,6 +1039,16 @@ def test_select_time(self): s = c.select({"time": [np.datetime64("2018-01-03"), "2018-02-06"]}) assert_equal(s.coordinates, np.array(["2018-01-03", "2018-01-04"]).astype(np.datetime64)) + def test_select_time_variable_precision(self): + c = ArrayCoordinates1d(["2012-05-19"], name="time") + c2 = ArrayCoordinates1d(["2012-05-19T12:00:00"], name="time") + s = c.select(c2.bounds, outer=True) + s1 = c.select(c2.bounds, outer=False) + s2 = c2.select(c.bounds) + assert s.size == 1 + assert s1.size == 0 + assert s2.size == 1 + def test_select_dtype(self): c = ArrayCoordinates1d([20.0, 40.0, 60.0, 10.0, 90.0, 50.0], name="lat") with pytest.raises(TypeError): diff --git a/podpac/core/coordinates/test/test_uniform_coordinates1d.py b/podpac/core/coordinates/test/test_uniform_coordinates1d.py index 0cc323332..3e4ef61b4 100644 --- a/podpac/core/coordinates/test/test_uniform_coordinates1d.py +++ b/podpac/core/coordinates/test/test_uniform_coordinates1d.py @@ -1180,3 +1180,13 @@ def test_select_outer(self): assert isinstance(s, ArrayCoordinates1d) assert_equal(s.coordinates, []) assert_equal(c.coordinates[I], []) + + def test_select_time_variable_precision(self): + c = UniformCoordinates1d("2012-05-19", "2012-05-20", "1,D", name="time") + c2 = UniformCoordinates1d("2012-05-20T12:00:00", "2012-05-21T12:00:00", "1,D", name="time") + s = c.select(c2.bounds, outer=True) + s1 = c.select(c2.bounds, outer=False) + s2 = c2.select(c.bounds) + assert s.size == 1 + assert s1.size == 0 + assert s2.size == 1 diff --git a/podpac/core/coordinates/uniform_coordinates1d.py b/podpac/core/coordinates/uniform_coordinates1d.py index a4f3fce57..46ee1a36e 100644 --- a/podpac/core/coordinates/uniform_coordinates1d.py +++ b/podpac/core/coordinates/uniform_coordinates1d.py @@ -8,6 +8,7 @@ from collections import OrderedDict from podpac.core.coordinates.utils import make_coord_value, make_coord_delta, add_coord, divide_delta +from podpac.core.coordinates.utils import lower_precision_time_bounds from podpac.core.coordinates.coordinates1d import Coordinates1d from podpac.core.coordinates.array_coordinates1d import ArrayCoordinates1d @@ -384,12 +385,17 @@ def copy(self): def _select(self, bounds, return_indices, outer): # TODO is there an easier way to do this with the new outer flag? + my_bounds = self.bounds.copy() - lo = max(bounds[0], self.bounds[0]) - hi = min(bounds[1], self.bounds[1]) + # 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) - fmin = (lo - self.bounds[0]) / np.abs(self.step) - fmax = (hi - self.bounds[0]) / np.abs(self.step) + lo = max(bounds[0], my_bounds[0]) + hi = min(bounds[1], my_bounds[1]) + + fmin = (lo - my_bounds[0]) / np.abs(self.step) + fmax = (hi - my_bounds[0]) / np.abs(self.step) imin = int(np.ceil(fmin)) imax = int(np.floor(fmax)) diff --git a/podpac/core/coordinates/utils.py b/podpac/core/coordinates/utils.py index 4c3597afb..1c15b665d 100644 --- a/podpac/core/coordinates/utils.py +++ b/podpac/core/coordinates/utils.py @@ -529,3 +529,41 @@ def rem_vunits(crs): if "+vunits" in crs: crs = re.sub(r"\+vunits=[a-z\-]+", "", crs) return crs + + +def lower_precision_time_bounds(my_bounds, other_bounds, outer): + """ + When given two bounds of np.datetime64, this function will convert both bounds to the lower-precision (in terms of + time unit) numpy datetime4 object if outer==True, otherwise only my_bounds will be converted. + + Parameters + ----------- + my_bounds : List(np.datetime64) + The bounds of the native coordinates of the dataset + other_bounds : List(np.datetime64) + The bounds used for the selection + outer : bool + When the other_bounds are higher precision than the input_bounds, only convert these IF outer=True + + Returns + -------- + my_bounds : List(np.datetime64) + The bounds of the native coordinates of the dataset at the new precision + other_bounds : List(np.datetime64) + The bounds used for the selection at the new precision, if outer == True, otherwise return original coordinates + """ + if not isinstance(other_bounds[0], np.datetime64) or not isinstance(other_bounds[1], np.datetime64): + raise TypeError("Input bounds should be of type np.datetime64 when selecting data from:", str(my_bounds)) + + if not isinstance(my_bounds[0], np.datetime64) or not isinstance(my_bounds[1], np.datetime64): + raise TypeError("Native bounds should be of type np.datetime64 when selecting data using:", str(other_bounds)) + + mine = np.timedelta64(1, my_bounds[0].dtype.name.replace("datetime64", "")[1:-1]) + your = np.timedelta64(1, other_bounds[0].dtype.name.replace("datetime64", "")[1:-1]) + + if mine > your and outer: + other_bounds = [b.astype(my_bounds[0].dtype) for b in other_bounds] + else: + my_bounds = [b.astype(other_bounds[0].dtype) for b in my_bounds] + + return my_bounds, other_bounds