Skip to content

Commit

Permalink
Merge pull request #812 from davidhassell/filled
Browse files Browse the repository at this point in the history
New method: `cf.Field.filled`
  • Loading branch information
davidhassell authored Sep 9, 2024
2 parents 807f740 + ba5b706 commit 4088c60
Show file tree
Hide file tree
Showing 15 changed files with 144 additions and 53 deletions.
2 changes: 2 additions & 0 deletions Changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions cf/mixin/propertiesdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
35 changes: 35 additions & 0 deletions cf/mixin/propertiesdatabounds.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
110 changes: 57 additions & 53 deletions cf/test/test_AuxiliaryCoordinate.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import faulthandler
import unittest

import numpy
import numpy as np

faulthandler.enable() # to debug seg faults and timeouts

Expand All @@ -14,7 +14,7 @@ class AuxiliaryCoordinateTest(unittest.TestCase):

aux1 = cf.AuxiliaryCoordinate()
aux1.standard_name = "latitude"
a = numpy.array(
a = np.array(
[
-30,
-23.5,
Expand All @@ -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))
Expand Down Expand Up @@ -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)

Expand All @@ -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)
Expand All @@ -139,61 +139,53 @@ 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()

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()

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()
Expand All @@ -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()
Expand Down Expand Up @@ -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(
Expand All @@ -289,51 +280,64 @@ 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()

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(
(aux.clip(-15, 25, bounds=False).bounds.array == b).all()
)

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())
Expand Down
10 changes: 10 additions & 0 deletions cf/test/test_Field.py
Original file line number Diff line number Diff line change
Expand Up @@ -2926,6 +2926,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())
Expand Down
1 change: 1 addition & 0 deletions docs/source/class/cf.AuxiliaryCoordinate.rst
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ Data
~cf.AuxiliaryCoordinate.count
~cf.AuxiliaryCoordinate.count_masked
~cf.AuxiliaryCoordinate.fill_value
~cf.AuxiliaryCoordinate.filled
~cf.AuxiliaryCoordinate.masked_invalid

.. autosummary::
Expand Down
1 change: 1 addition & 0 deletions docs/source/class/cf.Bounds.rst
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ Data
~cf.Bounds.count
~cf.Bounds.count_masked
~cf.Bounds.fill_value
~cf.Bounds.filled
~cf.Bounds.masked_invalid

.. autosummary::
Expand Down
1 change: 1 addition & 0 deletions docs/source/class/cf.CellMeasure.rst
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ Data
~cf.CellMeasure.count
~cf.CellMeasure.count_masked
~cf.CellMeasure.fill_value
~cf.CellMeasure.filled
~cf.CellMeasure.masked_invalid

.. autosummary::
Expand Down
1 change: 1 addition & 0 deletions docs/source/class/cf.Count.rst
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ Data
~cf.Count.count
~cf.Count.count_masked
~cf.Count.fill_value
~cf.Count.filled
~cf.Count.masked_invalid

.. autosummary::
Expand Down
1 change: 1 addition & 0 deletions docs/source/class/cf.DimensionCoordinate.rst
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ Data
~cf.DimensionCoordinate.count
~cf.DimensionCoordinate.count_masked
~cf.DimensionCoordinate.fill_value
~cf.DimensionCoordinate.filled
~cf.DimensionCoordinate.masked_invalid

.. autosummary::
Expand Down
Loading

0 comments on commit 4088c60

Please sign in to comment.