Skip to content

Commit a5f91ab

Browse files
Merge pull request #763 from sadielbartholomew/del-construct-cyclic-consideration
Override `del_construct` to update cyclic axes set when due
2 parents 7d78eac + 4f271e5 commit a5f91ab

File tree

4 files changed

+174
-0
lines changed

4 files changed

+174
-0
lines changed

Changelog.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ version 3.16.2
2727
* Fix bug in `cf.aggregate` that sometimes put a null transpose
2828
operation into the Dask graph when one was not needed
2929
(https://github.com/NCAS-CMS/cf-python/issues/754)
30+
* Fix bug whereby `Field.cyclic` is not updated after a
31+
`Field.del_construct` operation
32+
(https://github.com/NCAS-CMS/cf-python/issues/758)
3033

3134
----
3235

cf/mixin/fielddomain.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2408,6 +2408,109 @@ def set_construct(
24082408
# Return the construct key
24092409
return out
24102410

2411+
def del_construct(self, *identity, default=ValueError(), **filter_kwargs):
2412+
"""Remove a metadata construct.
2413+
2414+
If a domain axis construct is selected for removal then it
2415+
can't be spanned by any data arrays of the field nor metadata
2416+
constructs, nor be referenced by any cell method
2417+
constructs. However, a domain ancillary construct may be
2418+
removed even if it is referenced by coordinate reference
2419+
construct.
2420+
2421+
.. versionadded:: NEXTVERSION
2422+
2423+
.. seealso:: `get_construct`, `constructs`, `has_construct`,
2424+
`set_construct`
2425+
2426+
:Parameters:
2427+
2428+
identity:
2429+
Select the unique construct that has the identity,
2430+
defined by its `!identities` method, that matches the
2431+
given values.
2432+
2433+
Additionally, the values are matched against construct
2434+
identifiers, with or without the ``'key%'`` prefix.
2435+
2436+
{{value match}}
2437+
2438+
{{displayed identity}}
2439+
2440+
default: optional
2441+
Return the value of the *default* parameter if the
2442+
data axes have not been set.
2443+
2444+
{{default Exception}}
2445+
2446+
{{filter_kwargs: optional}}
2447+
2448+
.. versionadded:: NEXTVERSION
2449+
2450+
:Returns:
2451+
2452+
The removed metadata construct.
2453+
2454+
**Examples**
2455+
2456+
>>> f = {{package}}.example_field(0)
2457+
>>> print(f)
2458+
Field: specific_humidity (ncvar%q)
2459+
----------------------------------
2460+
Data : specific_humidity(latitude(5), longitude(8)) 1
2461+
Cell methods : area: mean
2462+
Dimension coords: latitude(5) = [-75.0, ..., 75.0] degrees_north
2463+
: longitude(8) = [22.5, ..., 337.5] degrees_east
2464+
: time(1) = [2019-01-01 00:00:00]
2465+
>>> f.del_construct('time')
2466+
<{{repr}}DimensionCoordinate: time(1) days since 2018-12-01 >
2467+
>>> f.del_construct('time')
2468+
Traceback (most recent call last):
2469+
...
2470+
ValueError: Can't find unique construct to remove
2471+
>>> f.del_construct('time', default='No time')
2472+
'No time'
2473+
>>> f.del_construct('dimensioncoordinate1')
2474+
<{{repr}}DimensionCoordinate: longitude(8) degrees_east>
2475+
>>> print(f)
2476+
Field: specific_humidity (ncvar%q)
2477+
----------------------------------
2478+
Data : specific_humidity(latitude(5), ncdim%lon(8)) 1
2479+
Cell methods : area: mean
2480+
Dimension coords: latitude(5) = [-75.0, ..., 75.0] degrees_north
2481+
2482+
"""
2483+
# Need to re-define to overload this method since cfdm doesn't
2484+
# have the concept of cyclic axes, so have to update the
2485+
# register of cyclic axes when we delete a construct in cf.
2486+
2487+
# Get the relevant key first because it will be lost upon deletion
2488+
key = self.construct_key(*identity, default=None, **filter_kwargs)
2489+
cyclic_axes = self._cyclic
2490+
2491+
deld_construct = super().del_construct(
2492+
*identity, default=None, **filter_kwargs
2493+
)
2494+
if deld_construct is None:
2495+
if default is None:
2496+
return
2497+
2498+
return self._default(
2499+
default, "Can't find unique construct to remove"
2500+
)
2501+
2502+
# If the construct deleted was a cyclic axes, remove it from the set
2503+
# of stored cyclic axes, to sync that. This is safe now, since given
2504+
# the block above we can be sure the deletion was successful.
2505+
if key in cyclic_axes:
2506+
# Never change value of _cyclic attribute in-place. Only copy now
2507+
# when the copy is known to be required.
2508+
cyclic_axes = cyclic_axes.copy()
2509+
cyclic_axes.remove(key)
2510+
self._cyclic = cyclic_axes
2511+
2512+
return deld_construct
2513+
24112514
def set_coordinate_reference(
24122515
self, coordinate_reference, key=None, parent=None, strict=True
24132516
):

cf/test/test_Domain.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,43 @@ def test_Domain_create_regular(self):
391391
np.allclose(latitude_specific.array - y_points_specific, 0)
392392
)
393393

394+
def test_Domain_del_construct(self):
395+
"""Test the `del_construct` Domain method."""
396+
# Test a domain without cyclic axes. These are equivalent tests to
397+
# those in the cfdm test suite, to check behaviour is the same in cf.
398+
d = self.d.copy()
399+
400+
self.assertIsInstance(
401+
d.del_construct("dimensioncoordinate1"), cf.DimensionCoordinate
402+
)
403+
self.assertIsInstance(
404+
d.del_construct("auxiliarycoordinate1"), cf.AuxiliaryCoordinate
405+
)
406+
with self.assertRaises(ValueError):
407+
d.del_construct("auxiliarycoordinate1")
408+
409+
self.assertIsNone(
410+
d.del_construct("auxiliarycoordinate1", default=None)
411+
)
412+
413+
self.assertIsInstance(d.del_construct("measure:area"), cf.CellMeasure)
414+
415+
# NOTE: this test will fail presently because of a bug which means
416+
# that Field.domain doesn't inherit the cyclic() axes of the
417+
# corresponding Field (see Issue #762) which will be fixed shortly.
418+
#
419+
# Test a domain with cyclic axes, to ensure the cyclic() set is
420+
# updated accordingly if a cyclic axes is the one removed.
421+
e = cf.example_field(2).domain # this has a cyclic axes 'domainaxis2'
422+
# To delete a cyclic axes, must first delete this dimension coordinate
423+
# because 'domainaxis2' spans it.
424+
self.assertIsInstance(
425+
e.del_construct("dimensioncoordinate2"), cf.DimensionCoordinate
426+
)
427+
self.assertEqual(e.cyclic(), set(("domainaxis2",)))
428+
self.assertIsInstance(e.del_construct("domainaxis2"), cf.DomainAxis)
429+
self.assertEqual(e.cyclic(), set())
430+
394431

395432
if __name__ == "__main__":
396433
print("Run date:", datetime.datetime.now())

cf/test/test_Field.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2572,6 +2572,37 @@ def test_Field_set_construct_conform(self):
25722572
cm2 = f.cell_method("method:maximum")
25732573
self.assertEqual(cm2.get_axes(), ("T",))
25742574

2575+
def test_Field_del_construct(self):
2576+
"""Test the `del_construct` Field method."""
2577+
# Test a field without cyclic axes. These are equivalent tests to those
2578+
# in the cfdm test suite, to check the behaviour is the same in cf.
2579+
f = self.f1.copy()
2580+
2581+
self.assertIsInstance(
2582+
f.del_construct("auxiliarycoordinate1"), cf.AuxiliaryCoordinate
2583+
)
2584+
2585+
with self.assertRaises(ValueError):
2586+
f.del_construct("auxiliarycoordinate1")
2587+
2588+
self.assertIsNone(
2589+
f.del_construct("auxiliarycoordinate1", default=None)
2590+
)
2591+
2592+
self.assertIsInstance(f.del_construct("measure:area"), cf.CellMeasure)
2593+
2594+
# Test a field with cyclic axes, to ensure the cyclic() set is
2595+
# updated accordingly if a cyclic axes is the one removed.
2596+
g = cf.example_field(2) # this has a cyclic axes 'domainaxis2'
2597+
# To delete a cyclic axes, must first delete this dimension coordinate
2598+
# because 'domainaxis2' spans it.
2599+
self.assertIsInstance(
2600+
g.del_construct("dimensioncoordinate2"), cf.DimensionCoordinate
2601+
)
2602+
self.assertEqual(g.cyclic(), set(("domainaxis2",)))
2603+
self.assertIsInstance(g.del_construct("domainaxis2"), cf.DomainAxis)
2604+
self.assertEqual(g.cyclic(), set())
2605+
25752606
def test_Field_persist(self):
25762607
"""Test the `persist` Field method."""
25772608
f = cf.example_field(0)

0 commit comments

Comments
 (0)