Skip to content

Commit 4088c60

Browse files
authored
Merge pull request #812 from davidhassell/filled
New method: `cf.Field.filled`
2 parents 807f740 + ba5b706 commit 4088c60

15 files changed

+144
-53
lines changed

Changelog.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ version NEXTVERSION
33

44
**2024-??-??**
55

6+
* New method: `cf.Field.filled`
7+
(https://github.com/NCAS-CMS/cf-python/issues/811)
68
* New method: `cf.Field.is_discrete_axis`
79
(https://github.com/NCAS-CMS/cf-python/issues/784)
810
* Include the UM version as a field property when reading UM files

cf/mixin/propertiesdata.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3457,6 +3457,36 @@ def file_locations(self):
34573457

34583458
return set()
34593459

3460+
@_inplace_enabled(default=False)
3461+
def filled(self, fill_value=None, inplace=False):
3462+
"""Replace masked elements with a fill value.
3463+
3464+
.. versionadded:: NEXTVERSION
3465+
3466+
:Parameters:
3467+
3468+
fill_value: scalar, optional
3469+
The fill value. By default the fill returned by
3470+
`fill_value` is used, or if this is not set then
3471+
the netCDF default fill value for the data type is
3472+
used (as defined by `cf.default_netCDF_fillvals`).
3473+
3474+
{{inplace: `bool`, optional}}
3475+
3476+
:Returns:
3477+
3478+
`{{class}}` or `None`
3479+
The construct with filled data, or `None` if the
3480+
operation was in-place.
3481+
3482+
"""
3483+
return self._apply_data_oper(
3484+
_inplace_enabled_define_and_cleanup(self),
3485+
"filled",
3486+
fill_value=fill_value,
3487+
inplace=inplace,
3488+
)
3489+
34603490
@_inplace_enabled(default=False)
34613491
def flatten(self, axes=None, inplace=False):
34623492
"""Flatten axes of the data.

cf/mixin/propertiesdatabounds.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2079,6 +2079,41 @@ def file_locations(self):
20792079

20802080
return out
20812081

2082+
@_inplace_enabled(default=False)
2083+
def filled(self, fill_value=None, bounds=True, inplace=False):
2084+
"""Replace masked elements with a fill value.
2085+
2086+
.. versionadded:: NEXTVERSION
2087+
2088+
:Parameters:
2089+
2090+
fill_value: scalar, optional
2091+
The fill value. By default the fill returned by
2092+
`fill_value` is used, or if this is not set then
2093+
the netCDF default fill value for the data type is
2094+
used (as defined by `cf.default_netCDF_fillvals`).
2095+
2096+
bounds: `bool`, optional
2097+
If False then do not alter any bounds. By default any
2098+
bounds are also altered.
2099+
2100+
{{inplace: `bool`, optional}}
2101+
2102+
:Returns:
2103+
2104+
`{{class}}` or `None`
2105+
The construct with filled data, or `None` if the
2106+
operation was in-place.
2107+
2108+
"""
2109+
return self._apply_superclass_data_oper(
2110+
_inplace_enabled_define_and_cleanup(self),
2111+
"filled",
2112+
(fill_value,),
2113+
bounds=bounds,
2114+
inplace=inplace,
2115+
)
2116+
20822117
@_inplace_enabled(default=False)
20832118
def flatten(self, axes=None, inplace=False):
20842119
"""Flatten axes of the data.

cf/test/test_AuxiliaryCoordinate.py

Lines changed: 57 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import faulthandler
33
import unittest
44

5-
import numpy
5+
import numpy as np
66

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

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

1515
aux1 = cf.AuxiliaryCoordinate()
1616
aux1.standard_name = "latitude"
17-
a = numpy.array(
17+
a = np.array(
1818
[
1919
-30,
2020
-23.5,
@@ -33,7 +33,7 @@ class AuxiliaryCoordinateTest(unittest.TestCase):
3333
)
3434
aux1.set_data(cf.Data(a, "degrees_north"))
3535
bounds = cf.Bounds()
36-
b = numpy.empty(a.shape + (2,))
36+
b = np.empty(a.shape + (2,))
3737
b[:, 0] = a - 0.1
3838
b[:, 1] = a + 0.1
3939
bounds.set_data(cf.Data(b))
@@ -97,7 +97,7 @@ def test_AuxiliaryCoordinate_transpose(self):
9797
x = self.f.auxiliary_coordinate("longitude").copy()
9898

9999
bounds = cf.Bounds(
100-
data=cf.Data(numpy.arange(9 * 10 * 4).reshape(9, 10, 4))
100+
data=cf.Data(np.arange(9 * 10 * 4).reshape(9, 10, 4))
101101
)
102102
x.set_bounds(bounds)
103103

@@ -116,7 +116,7 @@ def test_AuxiliaryCoordinate_squeeze(self):
116116
x = self.f.auxiliary_coordinate("longitude").copy()
117117

118118
bounds = cf.Bounds(
119-
data=cf.Data(numpy.arange(9 * 10 * 4).reshape(9, 10, 4))
119+
data=cf.Data(np.arange(9 * 10 * 4).reshape(9, 10, 4))
120120
)
121121
x.set_bounds(bounds)
122122
x.insert_dimension(1, inplace=True)
@@ -139,61 +139,53 @@ def test_AuxiliaryCoordinate_floor(self):
139139
a = aux.array
140140
b = aux.bounds.array
141141

142-
self.assertTrue((aux.floor().array == numpy.floor(a)).all())
143-
self.assertTrue((aux.floor().bounds.array == numpy.floor(b)).all())
144-
self.assertTrue(
145-
(aux.floor(bounds=False).array == numpy.floor(a)).all()
146-
)
142+
self.assertTrue((aux.floor().array == np.floor(a)).all())
143+
self.assertTrue((aux.floor().bounds.array == np.floor(b)).all())
144+
self.assertTrue((aux.floor(bounds=False).array == np.floor(a)).all())
147145
self.assertTrue((aux.floor(bounds=False).bounds.array == b).all())
148146

149147
aux.del_bounds()
150-
self.assertTrue((aux.floor().array == numpy.floor(a)).all())
151-
self.assertTrue(
152-
(aux.floor(bounds=False).array == numpy.floor(a)).all()
153-
)
148+
self.assertTrue((aux.floor().array == np.floor(a)).all())
149+
self.assertTrue((aux.floor(bounds=False).array == np.floor(a)).all())
154150

155151
self.assertIsNone(aux.floor(inplace=True))
156-
self.assertTrue((aux.array == numpy.floor(a)).all())
152+
self.assertTrue((aux.array == np.floor(a)).all())
157153

158154
def test_AuxiliaryCoordinate_ceil(self):
159155
aux = self.aux1.copy()
160156

161157
a = aux.array
162158
b = aux.bounds.array
163159

164-
self.assertTrue((aux.ceil().array == numpy.ceil(a)).all())
165-
self.assertTrue((aux.ceil().bounds.array == numpy.ceil(b)).all())
166-
self.assertTrue((aux.ceil(bounds=False).array == numpy.ceil(a)).all())
160+
self.assertTrue((aux.ceil().array == np.ceil(a)).all())
161+
self.assertTrue((aux.ceil().bounds.array == np.ceil(b)).all())
162+
self.assertTrue((aux.ceil(bounds=False).array == np.ceil(a)).all())
167163
self.assertTrue((aux.ceil(bounds=False).bounds.array == b).all())
168164

169165
aux.del_bounds()
170-
self.assertTrue((aux.ceil().array == numpy.ceil(a)).all())
171-
self.assertTrue((aux.ceil(bounds=False).array == numpy.ceil(a)).all())
166+
self.assertTrue((aux.ceil().array == np.ceil(a)).all())
167+
self.assertTrue((aux.ceil(bounds=False).array == np.ceil(a)).all())
172168

173169
self.assertIsNone(aux.ceil(inplace=True))
174-
self.assertTrue((aux.array == numpy.ceil(a)).all())
170+
self.assertTrue((aux.array == np.ceil(a)).all())
175171

176172
def test_AuxiliaryCoordinate_trunc(self):
177173
aux = self.aux1.copy()
178174

179175
a = aux.array
180176
b = aux.bounds.array
181177

182-
self.assertTrue((aux.trunc().array == numpy.trunc(a)).all())
183-
self.assertTrue((aux.trunc().bounds.array == numpy.trunc(b)).all())
184-
self.assertTrue(
185-
(aux.trunc(bounds=False).array == numpy.trunc(a)).all()
186-
)
178+
self.assertTrue((aux.trunc().array == np.trunc(a)).all())
179+
self.assertTrue((aux.trunc().bounds.array == np.trunc(b)).all())
180+
self.assertTrue((aux.trunc(bounds=False).array == np.trunc(a)).all())
187181
self.assertTrue((aux.trunc(bounds=False).bounds.array == b).all())
188182

189183
aux.del_bounds()
190-
self.assertTrue((aux.trunc().array == numpy.trunc(a)).all())
191-
self.assertTrue(
192-
(aux.trunc(bounds=False).array == numpy.trunc(a)).all()
193-
)
184+
self.assertTrue((aux.trunc().array == np.trunc(a)).all())
185+
self.assertTrue((aux.trunc(bounds=False).array == np.trunc(a)).all())
194186

195187
self.assertIsNone(aux.trunc(inplace=True))
196-
self.assertTrue((aux.array == numpy.trunc(a)).all())
188+
self.assertTrue((aux.array == np.trunc(a)).all())
197189

198190
def test_AuxiliaryCoordinate_rint(self):
199191
aux = self.aux1.copy()
@@ -204,17 +196,17 @@ def test_AuxiliaryCoordinate_rint(self):
204196
x0 = aux.rint()
205197
x = x0.array
206198

207-
self.assertTrue((x == numpy.rint(a)).all(), x)
208-
self.assertTrue((aux.rint().bounds.array == numpy.rint(b)).all())
209-
self.assertTrue((aux.rint(bounds=False).array == numpy.rint(a)).all())
199+
self.assertTrue((x == np.rint(a)).all(), x)
200+
self.assertTrue((aux.rint().bounds.array == np.rint(b)).all())
201+
self.assertTrue((aux.rint(bounds=False).array == np.rint(a)).all())
210202
self.assertTrue((aux.rint(bounds=False).bounds.array == b).all())
211203

212204
aux.del_bounds()
213-
self.assertTrue((aux.rint().array == numpy.rint(a)).all())
214-
self.assertTrue((aux.rint(bounds=False).array == numpy.rint(a)).all())
205+
self.assertTrue((aux.rint().array == np.rint(a)).all())
206+
self.assertTrue((aux.rint(bounds=False).array == np.rint(a)).all())
215207

216208
self.assertIsNone(aux.rint(inplace=True))
217-
self.assertTrue((aux.array == numpy.rint(a)).all())
209+
self.assertTrue((aux.array == np.rint(a)).all())
218210

219211
def test_AuxiliaryCoordinate_sin_cos_tan(self):
220212
aux = self.aux1.copy()
@@ -269,18 +261,17 @@ def test_AuxiliaryCoordinate_round(self):
269261
aux = self.aux1.copy()
270262

271263
self.assertTrue(
272-
(aux.round(decimals).array == numpy.round(a, decimals)).all()
264+
(aux.round(decimals).array == np.round(a, decimals)).all()
273265
)
274266
self.assertTrue(
275267
(
276-
aux.round(decimals).bounds.array
277-
== numpy.round(b, decimals)
268+
aux.round(decimals).bounds.array == np.round(b, decimals)
278269
).all()
279270
)
280271
self.assertTrue(
281272
(
282273
aux.round(decimals, bounds=False).array
283-
== numpy.round(a, decimals)
274+
== np.round(a, decimals)
284275
).all()
285276
)
286277
self.assertTrue(
@@ -289,51 +280,64 @@ def test_AuxiliaryCoordinate_round(self):
289280

290281
aux.del_bounds()
291282
self.assertTrue(
292-
(aux.round(decimals).array == numpy.round(a, decimals)).all()
283+
(aux.round(decimals).array == np.round(a, decimals)).all()
293284
)
294285
self.assertTrue(
295286
(
296287
aux.round(decimals, bounds=False).array
297-
== numpy.round(a, decimals)
288+
== np.round(a, decimals)
298289
).all()
299290
)
300291

301292
self.assertIsNone(aux.round(decimals, inplace=True))
302-
self.assertTrue((aux.array == numpy.round(a, decimals)).all())
293+
self.assertTrue((aux.array == np.round(a, decimals)).all())
303294

304295
def test_AuxiliaryCoordinate_clip(self):
305296
aux = self.aux1.copy()
306297

307298
a = aux.array
308299
b = aux.bounds.array
309300

301+
self.assertTrue((aux.clip(-15, 25).array == np.clip(a, -15, 25)).all())
310302
self.assertTrue(
311-
(aux.clip(-15, 25).array == numpy.clip(a, -15, 25)).all()
312-
)
313-
self.assertTrue(
314-
(aux.clip(-15, 25).bounds.array == numpy.clip(b, -15, 25)).all()
303+
(aux.clip(-15, 25).bounds.array == np.clip(b, -15, 25)).all()
315304
)
316305
self.assertTrue(
317306
(
318-
aux.clip(-15, 25, bounds=False).array == numpy.clip(a, -15, 25)
307+
aux.clip(-15, 25, bounds=False).array == np.clip(a, -15, 25)
319308
).all()
320309
)
321310
self.assertTrue(
322311
(aux.clip(-15, 25, bounds=False).bounds.array == b).all()
323312
)
324313

325314
aux.del_bounds()
326-
self.assertTrue(
327-
(aux.clip(-15, 25).array == numpy.clip(a, -15, 25)).all()
328-
)
315+
self.assertTrue((aux.clip(-15, 25).array == np.clip(a, -15, 25)).all())
329316
self.assertTrue(
330317
(
331-
aux.clip(-15, 25, bounds=False).array == numpy.clip(a, -15, 25)
318+
aux.clip(-15, 25, bounds=False).array == np.clip(a, -15, 25)
332319
).all()
333320
)
334321

335322
self.assertIsNone(aux.clip(-15, 25, inplace=True))
336323

324+
def test_AuxiliaryCoordinate_filled(self):
325+
"""Test AuxiliaryCoordinate.filled."""
326+
a = self.aux1.copy()
327+
a.data.where(cf.lt(0), cf.masked, inplace=1)
328+
self.assertEqual(a.data.count_masked(), 6)
329+
self.assertIsNone(a.filled(-999, inplace=True))
330+
values, counts = np.unique(a, return_counts=True)
331+
self.assertEqual(values[0], -999)
332+
self.assertEqual(counts[0], 6)
333+
334+
a.bounds.data.where(cf.lt(0), cf.masked, inplace=1)
335+
self.assertEqual(a.bounds.data.count_masked(), 13)
336+
self.assertIsNone(a.filled(-999, inplace=True))
337+
values, counts = np.unique(a.bounds, return_counts=True)
338+
self.assertEqual(values[0], -999)
339+
self.assertEqual(counts[0], 13)
340+
337341

338342
if __name__ == "__main__":
339343
print("Run date:", datetime.datetime.now())

cf/test/test_Field.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2926,6 +2926,16 @@ def test_Field_is_discrete_axis(self):
29262926
self.assertTrue(f.is_discrete_axis("cf_role=timeseries_id"))
29272927
self.assertFalse(f.is_discrete_axis("time"))
29282928

2929+
def test_Field_filled(self):
2930+
"""Test Field.filled."""
2931+
f = cf.example_field(0)
2932+
f.where(cf.gt(0.1), cf.masked, inplace=1)
2933+
self.assertEqual(f.data.count_masked(), 5)
2934+
self.assertIsNone(f.filled(-999, inplace=True))
2935+
values, counts = np.unique(f, return_counts=True)
2936+
self.assertEqual(values[0], -999)
2937+
self.assertEqual(counts[0], 5)
2938+
29292939

29302940
if __name__ == "__main__":
29312941
print("Run date:", datetime.datetime.now())

docs/source/class/cf.AuxiliaryCoordinate.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ Data
275275
~cf.AuxiliaryCoordinate.count
276276
~cf.AuxiliaryCoordinate.count_masked
277277
~cf.AuxiliaryCoordinate.fill_value
278+
~cf.AuxiliaryCoordinate.filled
278279
~cf.AuxiliaryCoordinate.masked_invalid
279280

280281
.. autosummary::

docs/source/class/cf.Bounds.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ Data
191191
~cf.Bounds.count
192192
~cf.Bounds.count_masked
193193
~cf.Bounds.fill_value
194+
~cf.Bounds.filled
194195
~cf.Bounds.masked_invalid
195196

196197
.. autosummary::

docs/source/class/cf.CellMeasure.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ Data
214214
~cf.CellMeasure.count
215215
~cf.CellMeasure.count_masked
216216
~cf.CellMeasure.fill_value
217+
~cf.CellMeasure.filled
217218
~cf.CellMeasure.masked_invalid
218219

219220
.. autosummary::

docs/source/class/cf.Count.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ Data
185185
~cf.Count.count
186186
~cf.Count.count_masked
187187
~cf.Count.fill_value
188+
~cf.Count.filled
188189
~cf.Count.masked_invalid
189190

190191
.. autosummary::

docs/source/class/cf.DimensionCoordinate.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ Data
281281
~cf.DimensionCoordinate.count
282282
~cf.DimensionCoordinate.count_masked
283283
~cf.DimensionCoordinate.fill_value
284+
~cf.DimensionCoordinate.filled
284285
~cf.DimensionCoordinate.masked_invalid
285286

286287
.. autosummary::

0 commit comments

Comments
 (0)