Repository: opendatacube/datacube-core

Issue:
Solar day computation is more expensive than necessary
Given a dataset `solar_day(dataset)` function recomputes `lon/lat` bounding box instead of reading it from metadata.

https://github.com/opendatacube/datacube-core/blob/6fc3852abca1952877ac0c715345575d393a895f/datacube/api/query.py#L309-L315

Line `311` is actually quite expensive, and the information it seeks to compute eventually, is already available under `dataset.metadata.lon`.  Too bad we do not guarantee that dataset's metadata has `lon` property, on the other hand we do not guarantee that `extent` is not `None` either, and there are no checks for that.

I suggest we re-write `solar_day` function to check presence of `dataset.metadata.lon` property and use that when computing "solar day", it should raise `ValueError` if supplied dataset doesn't have spatial bounds defined.




Discussion:

Resolving patch:
diff --git a/datacube/api/query.py b/datacube/api/query.py
--- a/datacube/api/query.py
+++ b/datacube/api/query.py
@@ -21,13 +21,10 @@
 import datetime
 import collections
 import warnings
-import calendar
-import re
 import pandas
 
 from dateutil import tz
 from pandas import to_datetime as pandas_to_datetime
-from pypeg2 import word, attr, List, maybe_some, parse as peg_parse
 import numpy as np
 
 from ..compat import string_types, integer_types
@@ -268,13 +265,13 @@ def _to_datetime(t):
 
     return pandas_to_datetime(t, utc=True, infer_datetime_format=True).to_pydatetime()
 
+
 def _time_to_search_dims(time_range):
     with warnings.catch_warnings():
         warnings.simplefilter("ignore", UserWarning)
 
         tr_start, tr_end = time_range, time_range
 
-        #pylint: disable=bad-whitespace
         if hasattr(time_range, '__iter__') and not isinstance(time_range, str):
             l = list(time_range)
             tr_start, tr_end = l[0], l[-1]
@@ -285,13 +282,12 @@ def _time_to_search_dims(time_range):
         if hasattr(tr_start, 'isoformat'):
             tr_start = tr_start.isoformat()
         if hasattr(tr_end, 'isoformat'):
-            tr_end   = tr_end.isoformat()
+            tr_end = tr_end.isoformat()
 
         start = _to_datetime(tr_start)
-        end   = _to_datetime(pandas.Period(tr_end)
-                             .end_time
-                             .to_pydatetime()
-                            )
+        end = _to_datetime(pandas.Period(tr_end)
+                           .end_time
+                           .to_pydatetime())
 
         tr = Range(start, end)
         if start == end:
@@ -299,6 +295,7 @@ def _time_to_search_dims(time_range):
 
         return tr
 
+
 def _convert_to_solar_time(utc, longitude):
     seconds_per_degree = 240
     offset_seconds = int(longitude * seconds_per_degree)
@@ -306,10 +303,16 @@ def _convert_to_solar_time(utc, longitude):
     return utc + offset
 
 
-def solar_day(dataset):
+def solar_day(dataset, longitude=None):
     utc = dataset.center_time
-    bb = dataset.extent.to_crs(geometry.CRS('WGS84')).boundingbox
-    assert bb.left < bb.right  # TODO: Handle dateline?
-    longitude = (bb.left + bb.right) * 0.5
+
+    if longitude is None:
+        m = dataset.metadata
+        if hasattr(m, 'lon'):
+            lon = m.lon
+            longitude = (lon.begin + lon.end)*0.5
+        else:
+            raise ValueError('Cannot compute solar_day: dataset is missing spatial info')
+
     solar_time = _convert_to_solar_time(utc, longitude)
     return np.datetime64(solar_time.date(), 'D')


Test patch:
diff --git a/tests/api/test_query.py b/tests/api/test_query.py
--- a/tests/api/test_query.py
+++ b/tests/api/test_query.py
@@ -16,11 +16,14 @@
 
 import datetime
 import pandas
+import numpy as np
+from types import SimpleNamespace
 
 import pytest
 
-from datacube.api.query import Query, _datetime_to_timestamp, query_group_by
+from datacube.api.query import Query, _datetime_to_timestamp, query_group_by, solar_day
 from datacube.model import Range
+from datacube.utils import parse_time
 
 
 def test_datetime_to_timestamp():
@@ -146,3 +149,20 @@ def test_time_handling(time_param, expected):
     query = Query(time=time_param)
     assert 'time' in query.search_terms
     assert query.search_terms['time'] == expected
+
+
+def test_solar_day():
+    _s = SimpleNamespace
+    ds = _s(center_time=parse_time('1987-05-22 23:07:44.2270250Z'),
+            metadata=_s(lon=Range(begin=150.415,
+                                  end=152.975)))
+
+    assert solar_day(ds) == np.datetime64('1987-05-23', 'D')
+    assert solar_day(ds, longitude=0) == np.datetime64('1987-05-22', 'D')
+
+    ds.metadata = _s()
+
+    with pytest.raises(ValueError) as e:
+        solar_day(ds)
+
+    assert 'Cannot compute solar_day: dataset is missing spatial info' in str(e.value)