Skip to content

Commit ddc992f

Browse files
Merge branch 'main' into cyclic-sets-domain
2 parents e7088b8 + 393b351 commit ddc992f

File tree

12 files changed

+189
-19
lines changed

12 files changed

+189
-19
lines changed

Changelog.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ version 3.16.2
33

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

6+
* New keyword parameter to `cf.Field.regrids` and `cf.Field.regridc`:
7+
``return_esmpy_regrid_operator``
8+
(https://github.com/NCAS-CMS/cf-python/issues/766)
69
* Allow a halo to be added by `cf.Field.indices` and
710
`cf.Field.subspace`
811
(https://github.com/NCAS-CMS/cf-python/issues/759)
@@ -32,12 +35,20 @@ version 3.16.2
3235
* Fix bug in `cf.aggregate` that sometimes put a null transpose
3336
operation into the Dask graph when one was not needed
3437
(https://github.com/NCAS-CMS/cf-python/issues/754)
38+
* Fix bug in `cf.aggregate` that caused a failure when property values
39+
were `numpy` arrays with two or more elements
40+
(https://github.com/NCAS-CMS/cf-python/issues/764)
41+
* Fix bug in `cf.aggregate` that didn't correctly handle the
42+
"actual_range" CF attribute
43+
(https://github.com/NCAS-CMS/cf-python/issues/764)
3544
* Fix bug whereby `Field.cyclic` is not updated after a
3645
`Field.del_construct` operation
3746
(https://github.com/NCAS-CMS/cf-python/issues/758)
3847
* Fix bug that meant `cyclic()` always returned an empty
3948
set for domains produced by `cf.Field.domain`
4049
(https://github.com/NCAS-CMS/cf-python/issues/762)
50+
* Changed dependency: ``cfunits>=3.3.7``
51+
* Changed dependency: ``netCDF4>=1.6.5``
4152

4253
----
4354

RELEASE.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22
heeding the Versioning Strategy (see
33
https://ncas-cms.github.io/cf-python/releases.html#versioning-strategy).
44

5+
* Set the `NEXTVERSION` version marker across the codebase (added in PRs
6+
to mark the next version where the exact number/name is not yet decided)
7+
by recursively finding all occurences within the `cf` directory and replacing
8+
them with the upcoming version name `X.Y.Z` (replace `X`, `Y` and `Z` with
9+
appropriate numbers), via running this command in `cf-python` repo root
10+
directory (don't run it repo-wide or it will e.g. edit this script!):
11+
12+
```console
13+
$ find cf/ -type f | xargs sed -i 's/NEXTVERSION/X.Y.Z/g'
14+
```
15+
516
* Change the version and date in `cf/__init__.py` (`__version__` and
617
`__date__` variables)
718

cf/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@
167167
)
168168

169169
# Check the version of netCDF4
170-
_minimum_vn = "1.5.4"
170+
_minimum_vn = "1.6.5"
171171
if Version(netCDF4.__version__) < Version(_minimum_vn):
172172
raise RuntimeError(
173173
f"Bad netCDF4 version: cf requires netCDF4>={_minimum_vn}. "
@@ -191,7 +191,7 @@
191191
)
192192

193193
# Check the version of cfunits
194-
_minimum_vn = "3.3.6"
194+
_minimum_vn = "3.3.7"
195195
if Version(cfunits.__version__) < Version(_minimum_vn):
196196
raise RuntimeError(
197197
f"Bad cfunits version: cf requires cfunits>={_minimum_vn}. "

cf/aggregate.py

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4852,20 +4852,36 @@ def _aggregate_2_fields(
48524852
value0 = parent0.get_property(prop, None)
48534853
value1 = parent1.get_property(prop, None)
48544854

4855+
if prop in ("_FillValue", "missing_value"):
4856+
continue
4857+
48554858
if prop in ("valid_min", "valid_max", "valid_range"):
48564859
if not m0.respect_valid:
48574860
parent0.del_property(prop, None)
48584861

48594862
continue
48604863

4861-
if prop in ("_FillValue", "missing_value"):
4864+
if prop == "actual_range":
4865+
try:
4866+
# Try to extend the actual range to encompass both
4867+
# value0 and value1
4868+
actual_range = (
4869+
min(value0[0], value1[0]),
4870+
max(value0[1], value1[1]),
4871+
)
4872+
except (TypeError, IndexError, KeyError):
4873+
# value0 and/or value1 is not set, or is
4874+
# non-CF-compliant.
4875+
parent0.del_property(prop, None)
4876+
else:
4877+
parent0.set_property(prop, actual_range)
4878+
48624879
continue
48634880

48644881
# Still here?
4865-
if isinstance(value0, str) or isinstance(value1, str):
4866-
if value0 == value1:
4867-
continue
4868-
elif parent0._equals(value0, value1):
4882+
if parent0._equals(value0, value1):
4883+
# Both values are equal, so no need to update the
4884+
# property.
48694885
continue
48704886

48714887
if concatenate:
@@ -4876,9 +4892,30 @@ def _aggregate_2_fields(
48764892
)
48774893
else:
48784894
parent0.set_property(prop, f" :AGGREGATED: {value1}")
4879-
else:
4880-
if value0 is not None:
4881-
parent0.del_property(prop)
4895+
elif value0 is not None:
4896+
parent0.del_property(prop)
4897+
4898+
# Check that actual_range is within the bounds of valid_range, and
4899+
# delete it if it isn't.
4900+
actual_range = parent0.get_property("actual_range", None)
4901+
if actual_range is not None:
4902+
valid_range = parent0.get_property("valid_range", None)
4903+
if valid_range is not None:
4904+
try:
4905+
if (
4906+
actual_range[0] < valid_range[0]
4907+
or actual_range[1] > valid_range[1]
4908+
):
4909+
actual_range = parent0.del_property("actual_range", None)
4910+
if actual_range is not None and is_log_level_info(logger):
4911+
logger.info(
4912+
"Deleted 'actual_range' attribute due to being "
4913+
"outside of 'valid_range' attribute limits."
4914+
)
4915+
4916+
except (TypeError, IndexError):
4917+
# valid_range is non-CF-compliant
4918+
pass
48824919

48834920
# Make a note that the parent construct in this _Meta object has
48844921
# already been aggregated

cf/docstring/docstring.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,11 @@
651651
operation and define a halo to be
652652
added to the subspaced axes.
653653
============== ======================================""",
654+
# return_esmpy_regrid_operator
655+
"{{return_esmpy_regrid_operator: `bool`, optional}}": """return_esmpy_regrid_operator: `bool`, optional
656+
If True then do not perform the regridding, rather
657+
return the `esmpy.Regrid` instance that defines the
658+
regridding operation.""",
654659
# ----------------------------------------------------------------
655660
# Method description substitutions (4 levels of indentation)
656661
# ----------------------------------------------------------------

cf/field.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13512,6 +13512,7 @@ def regrids(
1351213512
z=None,
1351313513
ln_z=None,
1351413514
verbose=None,
13515+
return_esmpy_regrid_operator=False,
1351513516
inplace=False,
1351613517
i=False,
1351713518
_compute_field_mass=None,
@@ -13752,6 +13753,10 @@ def regrids(
1375213753

1375313754
{{inplace: `bool`, optional}}
1375413755

13756+
{{return_esmpy_regrid_operator: `bool`, optional}}
13757+
13758+
.. versionadded:: 3.16.2
13759+
1375513760
axis_order: sequence, optional
1375613761
Deprecated at version 3.14.0.
1375713762

@@ -13768,7 +13773,8 @@ def regrids(
1376813773
`Field` or `None` or `RegridOperator`
1376913774
The regridded field construct; or `None` if the
1377013775
operation was in-place; or the regridding operator if
13771-
*return_operator* is True.
13776+
*return_operator* is True; or the `esmpy.Regrid` operator
13777+
object if *return_esmpy_regrid_operator* is True.
1377213778

1377313779
**Examples**
1377413780

@@ -13843,6 +13849,7 @@ def regrids(
1384313849
dst_z=dst_z,
1384413850
z=z,
1384513851
ln_z=ln_z,
13852+
return_esmpy_regrid_operator=return_esmpy_regrid_operator,
1384613853
inplace=inplace,
1384713854
)
1384813855

@@ -13866,6 +13873,7 @@ def regridc(
1386613873
dst_z=None,
1386713874
z=None,
1386813875
ln_z=None,
13876+
return_esmpy_regrid_operator=False,
1386913877
inplace=False,
1387013878
i=False,
1387113879
_compute_field_mass=None,
@@ -14041,6 +14049,10 @@ def regridc(
1404114049

1404214050
{{inplace: `bool`, optional}}
1404314051

14052+
{{return_esmpy_regrid_operator: `bool`, optional}}
14053+
14054+
.. versionadded:: 3.16.2
14055+
1404414056
axis_order: sequence, optional
1404514057
Deprecated at version 3.14.0.
1404614058

@@ -14057,7 +14069,8 @@ def regridc(
1405714069
`Field` or `None` or `RegridOperator`
1405814070
The regridded field construct; or `None` if the
1405914071
operation was in-place; or the regridding operator if
14060-
*return_operator* is True.
14072+
*return_operator* is True; or the `esmpy.Regrid` operator
14073+
object if *return_esmpy_regrid_operator* is True.
1406114074

1406214075
**Examples**
1406314076

@@ -14131,6 +14144,7 @@ def regridc(
1413114144
dst_z=dst_z,
1413214145
z=z,
1413314146
ln_z=ln_z,
14147+
return_esmpy_regrid_operator=return_esmpy_regrid_operator,
1413414148
inplace=inplace,
1413514149
)
1413614150

cf/query.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,6 @@ def __str__(self):
495495
if self.open_lower:
496496
repr_value = "(" + repr_value[1:]
497497

498-
499498
if self.open_upper:
500499
repr_value = repr_value[:-1] + ")"
501500

@@ -1740,15 +1739,15 @@ def wi(
17401739
bound so that value0 is excluded from the
17411740
range. By default the interval is closed
17421741
so that value0 is included.
1743-
1742+
17441743
.. versionadded:: NEXTVERSION
17451744
17461745
open_upper: `bool`, optional
17471746
If True, open the interval at the upper
17481747
bound so that value1 is excluded from the
17491748
range. By default the interval is closed
17501749
so that value1 is included.
1751-
1750+
17521751
.. versionadded:: NEXTVERSION
17531752
17541753
units: `str` or `Units`, optional

cf/test/test_aggregate.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import unittest
55
import warnings
66

7+
import numpy as np
8+
79
faulthandler.enable() # to debug seg faults and timeouts
810

911
import cf
@@ -664,6 +666,77 @@ def test_aggregate_trajectory(self):
664666
)
665667
)
666668

669+
def test_aggregate_actual_range(self):
670+
"""Test aggregation of actual_range"""
671+
f = cf.example_field(0)
672+
f.set_property("actual_range", (5, 10))
673+
f.set_property("valid_range", (0, 15))
674+
f0 = f[:, :2]
675+
f1 = f[:, 2:4]
676+
f2 = f[:, 4:]
677+
678+
g = cf.aggregate([f0, f1, f2])
679+
self.assertEqual(len(g), 1)
680+
self.assertEqual(g[0].get_property("actual_range"), (5, 10))
681+
682+
f1.set_property("actual_range", [2, 13])
683+
g = cf.aggregate([f0, f1, f2])
684+
self.assertEqual(len(g), 1)
685+
self.assertEqual(g[0].get_property("actual_range"), (2, 13))
686+
687+
f1.set_property("actual_range", [-2, 17])
688+
g = cf.aggregate([f0, f1, f2])
689+
self.assertEqual(len(g), 1)
690+
self.assertEqual(g[0].get_property("actual_range"), (-2, 17))
691+
692+
g = cf.aggregate([f0, f1, f2], respect_valid=True)
693+
self.assertEqual(len(g), 1)
694+
self.assertEqual(g[0].get_property("valid_range"), (0, 15))
695+
self.assertFalse(g[0].has_property("actual_range"))
696+
697+
f1.set_property("actual_range", [0, 15])
698+
g = cf.aggregate([f0, f1, f2], respect_valid=True)
699+
self.assertEqual(len(g), 1)
700+
self.assertEqual(g[0].get_property("valid_range"), (0, 15))
701+
self.assertEqual(g[0].get_property("actual_range"), (0, 15))
702+
703+
def test_aggregate_numpy_array_property(self):
704+
"""Test aggregation of numpy array-valued properties"""
705+
a = np.array([5, 10])
706+
f = cf.example_field(0)
707+
f.set_property("array", a)
708+
f0 = f[:, :2]
709+
f1 = f[:, 2:4]
710+
f2 = f[:, 4:]
711+
712+
g = cf.aggregate([f0, f1, f2])
713+
self.assertEqual(len(g), 1)
714+
self.assertTrue((g[0].get_property("array") == a).all())
715+
716+
f1.set_property("array", np.array([-5, 20]))
717+
g = cf.aggregate([f0, f1, f2])
718+
self.assertEqual(len(g), 1)
719+
self.assertEqual(
720+
g[0].get_property("array"),
721+
"[ 5 10] :AGGREGATED: [-5 20] :AGGREGATED: [ 5 10]",
722+
)
723+
724+
f2.set_property("array", np.array([-5, 20]))
725+
g = cf.aggregate([f0, f1, f2])
726+
self.assertEqual(len(g), 1)
727+
self.assertEqual(
728+
g[0].get_property("array"),
729+
"[ 5 10] :AGGREGATED: [-5 20] :AGGREGATED: [-5 20]",
730+
)
731+
732+
f1.set_property("array", np.array([5, 10]))
733+
g = cf.aggregate([f0, f1, f2])
734+
self.assertEqual(len(g), 1)
735+
self.assertEqual(
736+
g[0].get_property("array"),
737+
"[ 5 10] :AGGREGATED: [-5 20]",
738+
)
739+
667740

668741
if __name__ == "__main__":
669742
print("Run date:", datetime.datetime.now())

cf/test/test_regrid.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,25 @@ def test_Field_regrid_weights_file(self):
791791
src.regrids(r, method="linear", weights_file=tmpfile)
792792
)
793793

794+
@unittest.skipUnless(esmpy_imported, "Requires esmpy/ESMF package.")
795+
def test_return_esmpy_regrid_operator(self):
796+
"""esmpy regrid operator returns esmpy.Regrid in regrids and regridc"""
797+
dst = self.dst
798+
src = self.src
799+
800+
opers = src.regrids(
801+
dst, method="conservative", return_esmpy_regrid_operator=True
802+
)
803+
operc = src.regridc(
804+
dst,
805+
axes=["Y", "X"],
806+
method="conservative",
807+
return_esmpy_regrid_operator=True,
808+
)
809+
810+
self.assertIsInstance(opers, esmpy.api.regrid.Regrid)
811+
self.assertIsInstance(operc, esmpy.api.regrid.Regrid)
812+
794813

795814
if __name__ == "__main__":
796815
print("Run date:", datetime.datetime.now())

docs/source/contributing.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ ideas, code, and documentation to the cf library:
121121
* Jonathan Gregory
122122
* Klaus Zimmermann
123123
* Kristian Sebastián
124+
* Mark Rhodes-Smith
124125
* Michael Decker
125126
* Sadie Bartholomew
126127
* Thibault Hallouin

0 commit comments

Comments
 (0)