Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New method: cf.Field.filled #812

Merged
merged 6 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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())
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