Repository: opendatacube/datacube-core

Issue:
Handle correctly `nodata=NaN` case of the product
### Expected behaviour

```python
assert np.isnan(dc.index.products.get_by_name('gm_s2_annual').measurements['SMAD'].nodata)
```

### Actual behaviour

`nodata` attribute is a string containng `"NaN"`.

This happens due to conversion to json from yaml, json doesn't support `nan` or `infinity` floating point values, while yaml does support them just fine. So for products that contain floating point bands and would like default fill value to be `nan`, `nodata` field values will be converted to a string `"NaN"`, the reverse conversion does not happen, but should.



Discussion:

Resolving patch:
diff --git a/datacube/model/__init__.py b/datacube/model/__init__.py
--- a/datacube/model/__init__.py
+++ b/datacube/model/__init__.py
@@ -453,7 +453,14 @@ def measurements(self) -> Mapping[str, Measurement]:
         Dictionary of measurements in this product
         """
         if self._canonical_measurements is None:
-            self._canonical_measurements = OrderedDict((m['name'], Measurement(**m))
+            def fix_nodata(m):
+                nodata = m.get('nodata', None)
+                if isinstance(nodata, str):
+                    m = dict(**m)
+                    m['nodata'] = float(nodata)
+                return m
+
+            self._canonical_measurements = OrderedDict((m['name'], Measurement(**fix_nodata(m)))
                                                        for m in self.definition.get('measurements', []))
         return self._canonical_measurements
 


Test patch:
diff --git a/tests/test_model.py b/tests/test_model.py
--- a/tests/test_model.py
+++ b/tests/test_model.py
@@ -124,6 +124,27 @@ def test_product_dimensions():
     assert product.grid_spec is None
 
 
+def test_product_nodata_nan():
+    # When storing .nan to JSON in DB it becomes a string with value "NaN"
+    # Make sure it is converted back to real NaN
+    product = mk_sample_product('test', measurements=[dict(name='_nan',
+                                                           dtype='float32',
+                                                           nodata='NaN'),
+                                                      dict(name='_inf',
+                                                           dtype='float32',
+                                                           nodata='Infinity'),
+                                                      dict(name='_neg_inf',
+                                                           dtype='float32',
+                                                           nodata='-Infinity'),
+                                                      ])
+    for m in product.measurements.values():
+        assert isinstance(m.nodata, float)
+
+    assert numpy.isnan(product.measurements['_nan'].nodata)
+    assert product.measurements['_inf'].nodata == numpy.inf
+    assert product.measurements['_neg_inf'].nodata == -numpy.inf
+
+
 def test_product_scale_factor():
     product = mk_sample_product('test', measurements=[dict(name='red',
                                                            scale_factor=33,
diff --git a/tests/test_utils_rio.py b/tests/test_utils_rio.py
--- a/tests/test_utils_rio.py
+++ b/tests/test_utils_rio.py
@@ -182,5 +182,9 @@ def test_rio_configure_aws_access(monkeypatch, without_aws_env, dask_client):
     assert 'AWS_REGION' in ee
     assert 'AWS_SESSION_TOKEN' not in ee
 
-    ee = client.submit(get_rio_env, sanitize=False).result()
+    def _activate_and_get(sanitize=True):
+        activate_from_config()
+        return get_rio_env(sanitize=sanitize)
+
+    ee = client.submit(_activate_and_get, sanitize=False).result()
     assert ee == ee_local