diff --git a/Changelog.rst b/Changelog.rst index dc36568b30..12eb764e9b 100644 --- a/Changelog.rst +++ b/Changelog.rst @@ -3,6 +3,8 @@ version NEXTVERSION **2024-??-??** +* New method: `cf.Field.filled` + (https://github.com/NCAS-CMS/cf-python/issues/811) * New method: `cf.Field.is_discrete_axis` (https://github.com/NCAS-CMS/cf-python/issues/784) * Include the UM version as a field property when reading UM files diff --git a/cf/mixin/propertiesdata.py b/cf/mixin/propertiesdata.py index a756b4aafe..269b9d79ef 100644 --- a/cf/mixin/propertiesdata.py +++ b/cf/mixin/propertiesdata.py @@ -3457,6 +3457,36 @@ def file_locations(self): return set() + @_inplace_enabled(default=False) + def filled(self, fill_value=None, inplace=False): + """Replace masked elements with a fill value. + + .. versionadded:: NEXTVERSION + + :Parameters: + + fill_value: scalar, optional + The fill value. By default the fill returned by + `fill_value` is used, or if this is not set then + the netCDF default fill value for the data type is + used (as defined by `cf.default_netCDF_fillvals`). + + {{inplace: `bool`, optional}} + + :Returns: + + `{{class}}` or `None` + The construct with filled data, or `None` if the + operation was in-place. + + """ + return self._apply_data_oper( + _inplace_enabled_define_and_cleanup(self), + "filled", + fill_value=fill_value, + inplace=inplace, + ) + @_inplace_enabled(default=False) def flatten(self, axes=None, inplace=False): """Flatten axes of the data. diff --git a/cf/mixin/propertiesdatabounds.py b/cf/mixin/propertiesdatabounds.py index 685f177a94..a5581ca478 100644 --- a/cf/mixin/propertiesdatabounds.py +++ b/cf/mixin/propertiesdatabounds.py @@ -2079,6 +2079,41 @@ def file_locations(self): return out + @_inplace_enabled(default=False) + def filled(self, fill_value=None, bounds=True, inplace=False): + """Replace masked elements with a fill value. + + .. versionadded:: NEXTVERSION + + :Parameters: + + fill_value: scalar, optional + The fill value. By default the fill returned by + `fill_value` is used, or if this is not set then + the netCDF default fill value for the data type is + used (as defined by `cf.default_netCDF_fillvals`). + + bounds: `bool`, optional + If False then do not alter any bounds. By default any + bounds are also altered. + + {{inplace: `bool`, optional}} + + :Returns: + + `{{class}}` or `None` + The construct with filled data, or `None` if the + operation was in-place. + + """ + return self._apply_superclass_data_oper( + _inplace_enabled_define_and_cleanup(self), + "filled", + (fill_value,), + bounds=bounds, + inplace=inplace, + ) + @_inplace_enabled(default=False) def flatten(self, axes=None, inplace=False): """Flatten axes of the data. diff --git a/cf/test/test_AuxiliaryCoordinate.py b/cf/test/test_AuxiliaryCoordinate.py index 8bc624ce12..845c4b8c33 100644 --- a/cf/test/test_AuxiliaryCoordinate.py +++ b/cf/test/test_AuxiliaryCoordinate.py @@ -2,7 +2,7 @@ import faulthandler import unittest -import numpy +import numpy as np faulthandler.enable() # to debug seg faults and timeouts @@ -14,7 +14,7 @@ class AuxiliaryCoordinateTest(unittest.TestCase): aux1 = cf.AuxiliaryCoordinate() aux1.standard_name = "latitude" - a = numpy.array( + a = np.array( [ -30, -23.5, @@ -33,7 +33,7 @@ class AuxiliaryCoordinateTest(unittest.TestCase): ) aux1.set_data(cf.Data(a, "degrees_north")) bounds = cf.Bounds() - b = numpy.empty(a.shape + (2,)) + b = np.empty(a.shape + (2,)) b[:, 0] = a - 0.1 b[:, 1] = a + 0.1 bounds.set_data(cf.Data(b)) @@ -97,7 +97,7 @@ def test_AuxiliaryCoordinate_transpose(self): x = self.f.auxiliary_coordinate("longitude").copy() bounds = cf.Bounds( - data=cf.Data(numpy.arange(9 * 10 * 4).reshape(9, 10, 4)) + data=cf.Data(np.arange(9 * 10 * 4).reshape(9, 10, 4)) ) x.set_bounds(bounds) @@ -116,7 +116,7 @@ def test_AuxiliaryCoordinate_squeeze(self): x = self.f.auxiliary_coordinate("longitude").copy() bounds = cf.Bounds( - data=cf.Data(numpy.arange(9 * 10 * 4).reshape(9, 10, 4)) + data=cf.Data(np.arange(9 * 10 * 4).reshape(9, 10, 4)) ) x.set_bounds(bounds) x.insert_dimension(1, inplace=True) @@ -139,21 +139,17 @@ def test_AuxiliaryCoordinate_floor(self): a = aux.array b = aux.bounds.array - self.assertTrue((aux.floor().array == numpy.floor(a)).all()) - self.assertTrue((aux.floor().bounds.array == numpy.floor(b)).all()) - self.assertTrue( - (aux.floor(bounds=False).array == numpy.floor(a)).all() - ) + self.assertTrue((aux.floor().array == np.floor(a)).all()) + self.assertTrue((aux.floor().bounds.array == np.floor(b)).all()) + self.assertTrue((aux.floor(bounds=False).array == np.floor(a)).all()) self.assertTrue((aux.floor(bounds=False).bounds.array == b).all()) aux.del_bounds() - self.assertTrue((aux.floor().array == numpy.floor(a)).all()) - self.assertTrue( - (aux.floor(bounds=False).array == numpy.floor(a)).all() - ) + self.assertTrue((aux.floor().array == np.floor(a)).all()) + self.assertTrue((aux.floor(bounds=False).array == np.floor(a)).all()) self.assertIsNone(aux.floor(inplace=True)) - self.assertTrue((aux.array == numpy.floor(a)).all()) + self.assertTrue((aux.array == np.floor(a)).all()) def test_AuxiliaryCoordinate_ceil(self): aux = self.aux1.copy() @@ -161,17 +157,17 @@ def test_AuxiliaryCoordinate_ceil(self): a = aux.array b = aux.bounds.array - self.assertTrue((aux.ceil().array == numpy.ceil(a)).all()) - self.assertTrue((aux.ceil().bounds.array == numpy.ceil(b)).all()) - self.assertTrue((aux.ceil(bounds=False).array == numpy.ceil(a)).all()) + self.assertTrue((aux.ceil().array == np.ceil(a)).all()) + self.assertTrue((aux.ceil().bounds.array == np.ceil(b)).all()) + self.assertTrue((aux.ceil(bounds=False).array == np.ceil(a)).all()) self.assertTrue((aux.ceil(bounds=False).bounds.array == b).all()) aux.del_bounds() - self.assertTrue((aux.ceil().array == numpy.ceil(a)).all()) - self.assertTrue((aux.ceil(bounds=False).array == numpy.ceil(a)).all()) + self.assertTrue((aux.ceil().array == np.ceil(a)).all()) + self.assertTrue((aux.ceil(bounds=False).array == np.ceil(a)).all()) self.assertIsNone(aux.ceil(inplace=True)) - self.assertTrue((aux.array == numpy.ceil(a)).all()) + self.assertTrue((aux.array == np.ceil(a)).all()) def test_AuxiliaryCoordinate_trunc(self): aux = self.aux1.copy() @@ -179,21 +175,17 @@ def test_AuxiliaryCoordinate_trunc(self): a = aux.array b = aux.bounds.array - self.assertTrue((aux.trunc().array == numpy.trunc(a)).all()) - self.assertTrue((aux.trunc().bounds.array == numpy.trunc(b)).all()) - self.assertTrue( - (aux.trunc(bounds=False).array == numpy.trunc(a)).all() - ) + self.assertTrue((aux.trunc().array == np.trunc(a)).all()) + self.assertTrue((aux.trunc().bounds.array == np.trunc(b)).all()) + self.assertTrue((aux.trunc(bounds=False).array == np.trunc(a)).all()) self.assertTrue((aux.trunc(bounds=False).bounds.array == b).all()) aux.del_bounds() - self.assertTrue((aux.trunc().array == numpy.trunc(a)).all()) - self.assertTrue( - (aux.trunc(bounds=False).array == numpy.trunc(a)).all() - ) + self.assertTrue((aux.trunc().array == np.trunc(a)).all()) + self.assertTrue((aux.trunc(bounds=False).array == np.trunc(a)).all()) self.assertIsNone(aux.trunc(inplace=True)) - self.assertTrue((aux.array == numpy.trunc(a)).all()) + self.assertTrue((aux.array == np.trunc(a)).all()) def test_AuxiliaryCoordinate_rint(self): aux = self.aux1.copy() @@ -204,17 +196,17 @@ def test_AuxiliaryCoordinate_rint(self): x0 = aux.rint() x = x0.array - self.assertTrue((x == numpy.rint(a)).all(), x) - self.assertTrue((aux.rint().bounds.array == numpy.rint(b)).all()) - self.assertTrue((aux.rint(bounds=False).array == numpy.rint(a)).all()) + self.assertTrue((x == np.rint(a)).all(), x) + self.assertTrue((aux.rint().bounds.array == np.rint(b)).all()) + self.assertTrue((aux.rint(bounds=False).array == np.rint(a)).all()) self.assertTrue((aux.rint(bounds=False).bounds.array == b).all()) aux.del_bounds() - self.assertTrue((aux.rint().array == numpy.rint(a)).all()) - self.assertTrue((aux.rint(bounds=False).array == numpy.rint(a)).all()) + self.assertTrue((aux.rint().array == np.rint(a)).all()) + self.assertTrue((aux.rint(bounds=False).array == np.rint(a)).all()) self.assertIsNone(aux.rint(inplace=True)) - self.assertTrue((aux.array == numpy.rint(a)).all()) + self.assertTrue((aux.array == np.rint(a)).all()) def test_AuxiliaryCoordinate_sin_cos_tan(self): aux = self.aux1.copy() @@ -269,18 +261,17 @@ def test_AuxiliaryCoordinate_round(self): aux = self.aux1.copy() self.assertTrue( - (aux.round(decimals).array == numpy.round(a, decimals)).all() + (aux.round(decimals).array == np.round(a, decimals)).all() ) self.assertTrue( ( - aux.round(decimals).bounds.array - == numpy.round(b, decimals) + aux.round(decimals).bounds.array == np.round(b, decimals) ).all() ) self.assertTrue( ( aux.round(decimals, bounds=False).array - == numpy.round(a, decimals) + == np.round(a, decimals) ).all() ) self.assertTrue( @@ -289,17 +280,17 @@ def test_AuxiliaryCoordinate_round(self): aux.del_bounds() self.assertTrue( - (aux.round(decimals).array == numpy.round(a, decimals)).all() + (aux.round(decimals).array == np.round(a, decimals)).all() ) self.assertTrue( ( aux.round(decimals, bounds=False).array - == numpy.round(a, decimals) + == np.round(a, decimals) ).all() ) self.assertIsNone(aux.round(decimals, inplace=True)) - self.assertTrue((aux.array == numpy.round(a, decimals)).all()) + self.assertTrue((aux.array == np.round(a, decimals)).all()) def test_AuxiliaryCoordinate_clip(self): aux = self.aux1.copy() @@ -307,15 +298,13 @@ def test_AuxiliaryCoordinate_clip(self): a = aux.array b = aux.bounds.array + self.assertTrue((aux.clip(-15, 25).array == np.clip(a, -15, 25)).all()) self.assertTrue( - (aux.clip(-15, 25).array == numpy.clip(a, -15, 25)).all() - ) - self.assertTrue( - (aux.clip(-15, 25).bounds.array == numpy.clip(b, -15, 25)).all() + (aux.clip(-15, 25).bounds.array == np.clip(b, -15, 25)).all() ) self.assertTrue( ( - aux.clip(-15, 25, bounds=False).array == numpy.clip(a, -15, 25) + aux.clip(-15, 25, bounds=False).array == np.clip(a, -15, 25) ).all() ) self.assertTrue( @@ -323,17 +312,32 @@ def test_AuxiliaryCoordinate_clip(self): ) aux.del_bounds() - self.assertTrue( - (aux.clip(-15, 25).array == numpy.clip(a, -15, 25)).all() - ) + self.assertTrue((aux.clip(-15, 25).array == np.clip(a, -15, 25)).all()) self.assertTrue( ( - aux.clip(-15, 25, bounds=False).array == numpy.clip(a, -15, 25) + aux.clip(-15, 25, bounds=False).array == np.clip(a, -15, 25) ).all() ) self.assertIsNone(aux.clip(-15, 25, inplace=True)) + def test_AuxiliaryCoordinate_filled(self): + """Test AuxiliaryCoordinate.filled.""" + a = self.aux1.copy() + a.data.where(cf.lt(0), cf.masked, inplace=1) + self.assertEqual(a.data.count_masked(), 6) + self.assertIsNone(a.filled(-999, inplace=True)) + values, counts = np.unique(a, return_counts=True) + self.assertEqual(values[0], -999) + self.assertEqual(counts[0], 6) + + a.bounds.data.where(cf.lt(0), cf.masked, inplace=1) + self.assertEqual(a.bounds.data.count_masked(), 13) + self.assertIsNone(a.filled(-999, inplace=True)) + values, counts = np.unique(a.bounds, return_counts=True) + self.assertEqual(values[0], -999) + self.assertEqual(counts[0], 13) + if __name__ == "__main__": print("Run date:", datetime.datetime.now()) diff --git a/cf/test/test_Field.py b/cf/test/test_Field.py index 13366c52f2..b804bde75a 100644 --- a/cf/test/test_Field.py +++ b/cf/test/test_Field.py @@ -2910,6 +2910,16 @@ def test_Field_is_discrete_axis(self): self.assertTrue(f.is_discrete_axis("cf_role=timeseries_id")) self.assertFalse(f.is_discrete_axis("time")) + def test_Field_filled(self): + """Test Field.filled.""" + f = cf.example_field(0) + f.where(cf.gt(0.1), cf.masked, inplace=1) + self.assertEqual(f.data.count_masked(), 5) + self.assertIsNone(f.filled(-999, inplace=True)) + values, counts = np.unique(f, return_counts=True) + self.assertEqual(values[0], -999) + self.assertEqual(counts[0], 5) + if __name__ == "__main__": print("Run date:", datetime.datetime.now()) diff --git a/docs/source/class/cf.AuxiliaryCoordinate.rst b/docs/source/class/cf.AuxiliaryCoordinate.rst index 3cc8f983d8..cfa9503c68 100644 --- a/docs/source/class/cf.AuxiliaryCoordinate.rst +++ b/docs/source/class/cf.AuxiliaryCoordinate.rst @@ -275,6 +275,7 @@ Data ~cf.AuxiliaryCoordinate.count ~cf.AuxiliaryCoordinate.count_masked ~cf.AuxiliaryCoordinate.fill_value + ~cf.AuxiliaryCoordinate.filled ~cf.AuxiliaryCoordinate.masked_invalid .. autosummary:: diff --git a/docs/source/class/cf.Bounds.rst b/docs/source/class/cf.Bounds.rst index 9f93594073..364076340e 100644 --- a/docs/source/class/cf.Bounds.rst +++ b/docs/source/class/cf.Bounds.rst @@ -191,6 +191,7 @@ Data ~cf.Bounds.count ~cf.Bounds.count_masked ~cf.Bounds.fill_value + ~cf.Bounds.filled ~cf.Bounds.masked_invalid .. autosummary:: diff --git a/docs/source/class/cf.CellMeasure.rst b/docs/source/class/cf.CellMeasure.rst index d3384285b5..1488fce781 100644 --- a/docs/source/class/cf.CellMeasure.rst +++ b/docs/source/class/cf.CellMeasure.rst @@ -214,6 +214,7 @@ Data ~cf.CellMeasure.count ~cf.CellMeasure.count_masked ~cf.CellMeasure.fill_value + ~cf.CellMeasure.filled ~cf.CellMeasure.masked_invalid .. autosummary:: diff --git a/docs/source/class/cf.Count.rst b/docs/source/class/cf.Count.rst index f67d610506..97250f29f1 100644 --- a/docs/source/class/cf.Count.rst +++ b/docs/source/class/cf.Count.rst @@ -185,6 +185,7 @@ Data ~cf.Count.count ~cf.Count.count_masked ~cf.Count.fill_value + ~cf.Count.filled ~cf.Count.masked_invalid .. autosummary:: diff --git a/docs/source/class/cf.DimensionCoordinate.rst b/docs/source/class/cf.DimensionCoordinate.rst index 267ca8934e..300034b4c5 100644 --- a/docs/source/class/cf.DimensionCoordinate.rst +++ b/docs/source/class/cf.DimensionCoordinate.rst @@ -281,6 +281,7 @@ Data ~cf.DimensionCoordinate.count ~cf.DimensionCoordinate.count_masked ~cf.DimensionCoordinate.fill_value + ~cf.DimensionCoordinate.filled ~cf.DimensionCoordinate.masked_invalid .. autosummary:: diff --git a/docs/source/class/cf.DomainAncillary.rst b/docs/source/class/cf.DomainAncillary.rst index 7db4a52c75..c9e8643726 100644 --- a/docs/source/class/cf.DomainAncillary.rst +++ b/docs/source/class/cf.DomainAncillary.rst @@ -260,6 +260,7 @@ Data ~cf.DomainAncillary.count ~cf.DomainAncillary.count_masked ~cf.DomainAncillary.fill_value + ~cf.DomainAncillary.filled ~cf.DomainAncillary.masked_invalid .. autosummary:: diff --git a/docs/source/class/cf.Field.rst b/docs/source/class/cf.Field.rst index 8bbe0f5743..2239992be8 100644 --- a/docs/source/class/cf.Field.rst +++ b/docs/source/class/cf.Field.rst @@ -210,6 +210,7 @@ Data ~cf.Field.count ~cf.Field.count_masked ~cf.Field.fill_value + ~cf.Field.filled ~cf.Field.masked_invalid .. autosummary:: diff --git a/docs/source/class/cf.FieldAncillary.rst b/docs/source/class/cf.FieldAncillary.rst index 6187fa4716..cf585d00a9 100644 --- a/docs/source/class/cf.FieldAncillary.rst +++ b/docs/source/class/cf.FieldAncillary.rst @@ -190,6 +190,7 @@ Data ~cf.FieldAncillary.count ~cf.FieldAncillary.count_masked ~cf.FieldAncillary.fill_value + ~cf.FieldAncillary.filled ~cf.FieldAncillary.masked_invalid .. autosummary:: diff --git a/docs/source/class/cf.Index.rst b/docs/source/class/cf.Index.rst index 8fa4ec05b6..b72adb3d8b 100644 --- a/docs/source/class/cf.Index.rst +++ b/docs/source/class/cf.Index.rst @@ -186,6 +186,7 @@ Data ~cf.Index.count ~cf.Index.count_masked ~cf.Index.fill_value + ~cf.Index.filled ~cf.Index.masked_invalid .. autosummary:: diff --git a/docs/source/class/cf.List.rst b/docs/source/class/cf.List.rst index fd033f946b..2760506db6 100644 --- a/docs/source/class/cf.List.rst +++ b/docs/source/class/cf.List.rst @@ -186,6 +186,7 @@ Data ~cf.List.count ~cf.List.count_masked ~cf.List.fill_value + ~cf.List.filled ~cf.List.masked_invalid .. autosummary::