Skip to content

Commit ecab3df

Browse files
authored
collapsed preserving masked aux coords (SciTools#6719)
* collapsed preserving masked aux coords * test coverage * add whatsnew entry
1 parent 8ed2570 commit ecab3df

File tree

4 files changed

+184
-1
lines changed

4 files changed

+184
-1
lines changed

docs/src/whatsnew/latest.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ This document explains the changes made to Iris for this release
4848
on the correct dimensions, and merge no longer fails when trying to add
4949
them to a dimension of the wrong length. (:issue:`2076`, :pull:`6688`)
5050

51+
#. `@bjlittle`_ added support for preserving masked auxiliary coordinates when
52+
using :meth:`~iris.cube.Cube.aggregated_by` or :meth:`~iris.cube.Cube.collapsed`.
53+
(:issue:`6473`, :pull:`6706`, :pull:`6719`)
54+
5155

5256
💣 Incompatible Changes
5357
=======================

lib/iris/coords.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2219,7 +2219,7 @@ def serialize(x, axis):
22192219
item = self.core_points()
22202220

22212221
# Determine the array library for stacking
2222-
al = da if _lazy.is_lazy_data(item) else np
2222+
al = da if _lazy.is_lazy_data(item) else ma
22232223

22242224
# Calculate the bounds and points along the right dims
22252225
bounds = al.stack(
@@ -2231,6 +2231,12 @@ def serialize(x, axis):
22312231
)
22322232
points = al.array(bounds.sum(axis=-1) * 0.5, dtype=self.dtype)
22332233

2234+
if ma.isMaskedArray(points) and not np.any(points.mask):
2235+
points = points.data
2236+
2237+
if ma.isMaskedArray(bounds) and not np.any(bounds.mask):
2238+
bounds = bounds.data
2239+
22342240
# Create the new collapsed coordinate.
22352241
coord = self.copy(points=points, bounds=bounds)
22362242
return coord

lib/iris/tests/unit/coords/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ def is_real_data(array):
5656
return isinstance(array, np.ndarray)
5757

5858

59+
def is_masked_data(array):
60+
# Check the array is a masked array.
61+
return ma.isMaskedArray(array)
62+
63+
5964
def arrays_share_data(a1, a2):
6065
# Check whether 2 real arrays with the same content view the same data.
6166
# For an ndarray x, x.base will either be None (if x owns its data) or a
@@ -134,6 +139,12 @@ def assert_is_lazy_array(self, array, msg=None):
134139
msg = f"Array {array} is not a lazy array"
135140
assert is_lazy_data(array), msg
136141

142+
def assert_is_masked_array(self, array, msg=None):
143+
# Check that the arg is a masked array.
144+
if not msg:
145+
msg = f"Array {array} is not a masked array"
146+
assert is_masked_data(array), msg
147+
137148
def assert_equal_real_arrays_and_dtypes(self, a1, a2):
138149
# Check that two arrays are real, equal, and have same dtype.
139150
self.assert_is_real_array(a1)

lib/iris/tests/unit/coords/test_Coord.py

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,46 @@ def test_lazy_points(self):
506506
assert collapsed_coord.has_lazy_bounds()
507507
assert collapsed_coord.has_lazy_points()
508508

509+
def test_numeric_masked_all(self):
510+
coord = AuxCoord(
511+
points=ma.array(
512+
[[1, 2, 4, 5], [4, 5, 7, 8], [7, 8, 10, 11]],
513+
mask=True,
514+
),
515+
)
516+
517+
collapsed_coord = coord.collapsed()
518+
self.assert_is_masked_array(collapsed_coord.points)
519+
self.assert_is_masked_array(collapsed_coord.bounds)
520+
expected = AuxCoord(
521+
ma.array([-1], mask=True), bounds=ma.array([[-1, -1]], mask=[[True, True]])
522+
)
523+
assert collapsed_coord == expected
524+
525+
collapsed_coord = coord.collapsed(dims_to_collapse=1)
526+
self.assert_is_masked_array(collapsed_coord.points)
527+
self.assert_is_masked_array(collapsed_coord.bounds)
528+
expected = AuxCoord(
529+
points=ma.array([-1, -1, -1], mask=[True, True, True]),
530+
bounds=ma.array(
531+
[[-1, -1], [-1, -1], [-1, -1]],
532+
mask=[[True, True], [True, True], [True, True]],
533+
),
534+
)
535+
assert collapsed_coord == expected
536+
537+
collapsed_coord = coord.collapsed(dims_to_collapse=0)
538+
self.assert_is_masked_array(collapsed_coord.points)
539+
self.assert_is_masked_array(collapsed_coord.bounds)
540+
expected = AuxCoord(
541+
points=ma.array([-1, -1, -1, -1], mask=[True, True, True, True]),
542+
bounds=ma.array(
543+
[[-1, -1], [-1, -1], [-1, -1], [-1, -1]],
544+
mask=[[True, True], [True, True], [True, True], [True, True]],
545+
),
546+
)
547+
assert collapsed_coord == expected
548+
509549
def test_numeric_nd(self):
510550
coord = AuxCoord(points=np.array([[1, 2, 4, 5], [4, 5, 7, 8], [7, 8, 10, 11]]))
511551

@@ -530,6 +570,42 @@ def test_numeric_nd(self):
530570
np.array([[1, 7], [2, 8], [4, 10], [5, 11]]),
531571
)
532572

573+
def test_numeric_masked(self):
574+
coord = AuxCoord(
575+
points=ma.array(
576+
[[1, 2, 4, 5], [4, 5, 7, 8], [7, 8, 10, 11]],
577+
mask=[
578+
[True, False, False, True],
579+
[True, True, True, True],
580+
[False, True, True, False],
581+
],
582+
),
583+
)
584+
585+
collapsed_coord = coord.collapsed()
586+
_shared_utils.assert_array_equal(collapsed_coord.points, np.array([6]))
587+
_shared_utils.assert_array_equal(collapsed_coord.bounds, np.array([[2, 11]]))
588+
589+
collapsed_coord = coord.collapsed(dims_to_collapse=1)
590+
self.assert_is_masked_array(collapsed_coord.points)
591+
self.assert_is_masked_array(collapsed_coord.bounds)
592+
expected = AuxCoord(
593+
points=ma.array([3, -1, 9], mask=[False, True, False]),
594+
bounds=ma.array(
595+
[[2, 4], [-1, -1], [7, 11]],
596+
mask=[[False, False], [True, True], [False, False]],
597+
),
598+
)
599+
assert collapsed_coord == expected
600+
601+
collapsed_coord = coord.collapsed(dims_to_collapse=0)
602+
_shared_utils.assert_array_equal(
603+
collapsed_coord.points, np.array([7, 2, 4, 11])
604+
)
605+
_shared_utils.assert_array_equal(
606+
collapsed_coord.bounds, np.array([[7, 7], [2, 2], [4, 4], [11, 11]])
607+
)
608+
533609
def test_numeric_nd_bounds_all(self):
534610
self.setup_test_arrays((3, 4))
535611
coord = AuxCoord(self.pts_real, bounds=self.bds_real)
@@ -538,6 +614,22 @@ def test_numeric_nd_bounds_all(self):
538614
_shared_utils.assert_array_equal(collapsed_coord.points, np.array([55]))
539615
_shared_utils.assert_array_equal(collapsed_coord.bounds, np.array([[-2, 112]]))
540616

617+
def test_numeric_no_masked_bounds_all(self):
618+
self.setup_test_arrays((3, 4), masked=True)
619+
coord = AuxCoord(self.no_masked_pts_real, bounds=self.no_masked_bds_real)
620+
621+
collapsed_coord = coord.collapsed()
622+
_shared_utils.assert_array_equal(collapsed_coord.points, np.array([55]))
623+
_shared_utils.assert_array_equal(collapsed_coord.bounds, np.array([[-2, 112]]))
624+
625+
def test_numeric_masked_bounds_all(self):
626+
self.setup_test_arrays((3, 4), masked=True)
627+
coord = AuxCoord(self.masked_pts_real, bounds=self.masked_bds_real)
628+
629+
collapsed_coord = coord.collapsed()
630+
_shared_utils.assert_array_equal(collapsed_coord.points, np.array([75]))
631+
_shared_utils.assert_array_equal(collapsed_coord.bounds, np.array([[38, 112]]))
632+
541633
def test_numeric_nd_bounds_second(self):
542634
self.setup_test_arrays((3, 4))
543635
coord = AuxCoord(self.pts_real, bounds=self.bds_real)
@@ -547,6 +639,30 @@ def test_numeric_nd_bounds_second(self):
547639
collapsed_coord.bounds, np.array([[-2, 32], [38, 72], [78, 112]])
548640
)
549641

642+
def test_numeric_no_masked_bounds_second(self):
643+
self.setup_test_arrays((3, 4), masked=True)
644+
coord = AuxCoord(self.no_masked_pts_real, bounds=self.no_masked_bds_real)
645+
collapsed_coord = coord.collapsed(dims_to_collapse=1)
646+
_shared_utils.assert_array_equal(collapsed_coord.points, np.array([15, 55, 95]))
647+
_shared_utils.assert_array_equal(
648+
collapsed_coord.bounds, np.array([[-2, 32], [38, 72], [78, 112]])
649+
)
650+
651+
def test_numeric_masked_bounds_second(self):
652+
self.setup_test_arrays((3, 4), masked=True)
653+
coord = AuxCoord(self.masked_pts_real, bounds=self.masked_bds_real)
654+
collapsed_coord = coord.collapsed(dims_to_collapse=1)
655+
self.assert_is_masked_array(collapsed_coord.points)
656+
self.assert_is_masked_array(collapsed_coord.bounds)
657+
expected = AuxCoord(
658+
ma.array([-1, 55, 95], mask=[True, False, False]),
659+
bounds=ma.array(
660+
[[-1, -1], [38, 72], [78, 112]],
661+
mask=[[True, True], [False, False], [False, False]],
662+
),
663+
)
664+
assert collapsed_coord == expected
665+
550666
def test_numeric_nd_bounds_first(self):
551667
self.setup_test_arrays((3, 4))
552668
coord = AuxCoord(self.pts_real, bounds=self.bds_real)
@@ -560,6 +676,28 @@ def test_numeric_nd_bounds_first(self):
560676
np.array([[-2, 82], [8, 92], [18, 102], [28, 112]]),
561677
)
562678

679+
def test_numeric_no_masked_bounds_first(self):
680+
self.setup_test_arrays((3, 4), masked=True)
681+
coord = AuxCoord(self.no_masked_pts_real, bounds=self.no_masked_bds_real)
682+
collapsed_coord = coord.collapsed(dims_to_collapse=0)
683+
_shared_utils.assert_array_equal(
684+
collapsed_coord.points, np.array([40, 50, 60, 70])
685+
)
686+
_shared_utils.assert_array_equal(
687+
collapsed_coord.bounds, np.array([[-2, 82], [8, 92], [18, 102], [28, 112]])
688+
)
689+
690+
def test_numeric_masked_bounds_first(self):
691+
self.setup_test_arrays((3, 4), masked=True)
692+
coord = AuxCoord(self.masked_pts_real, bounds=self.masked_bds_real)
693+
collapsed_coord = coord.collapsed(dims_to_collapse=0)
694+
_shared_utils.assert_array_equal(
695+
collapsed_coord.points, np.array([60, 70, 80, 90])
696+
)
697+
_shared_utils.assert_array_equal(
698+
collapsed_coord.bounds, np.array([[38, 82], [48, 92], [58, 102], [68, 112]])
699+
)
700+
563701
def test_numeric_nd_bounds_last(self):
564702
self.setup_test_arrays((3, 4))
565703
coord = AuxCoord(self.pts_real, bounds=self.bds_real)
@@ -570,6 +708,30 @@ def test_numeric_nd_bounds_last(self):
570708
collapsed_coord.bounds, np.array([[-2, 32], [38, 72], [78, 112]])
571709
)
572710

711+
def test_numeric_no_masked_bounds_last(self):
712+
self.setup_test_arrays((3, 4), masked=True)
713+
coord = AuxCoord(self.no_masked_pts_real, bounds=self.no_masked_bds_real)
714+
collapsed_coord = coord.collapsed(dims_to_collapse=-1)
715+
_shared_utils.assert_array_equal(collapsed_coord.points, np.array([15, 55, 95]))
716+
_shared_utils.assert_array_equal(
717+
collapsed_coord.bounds, np.array([[-2, 32], [38, 72], [78, 112]])
718+
)
719+
720+
def test_numeric_masked_bounds_last(self):
721+
self.setup_test_arrays((3, 4), masked=True)
722+
coord = AuxCoord(self.masked_pts_real, bounds=self.masked_bds_real)
723+
collapsed_coord = coord.collapsed(dims_to_collapse=-1)
724+
self.assert_is_masked_array(collapsed_coord.points)
725+
self.assert_is_masked_array(collapsed_coord.bounds)
726+
expected = AuxCoord(
727+
ma.array([-1, 55, 95], mask=[True, False, False]),
728+
bounds=ma.array(
729+
[[-1, -1], [38, 72], [78, 112]],
730+
mask=[[True, True], [False, False], [False, False]],
731+
),
732+
)
733+
assert collapsed_coord == expected
734+
573735
def test_lazy_nd_bounds_all(self):
574736
self.setup_test_arrays((3, 4))
575737
coord = AuxCoord(self.pts_real, bounds=self.bds_lazy)

0 commit comments

Comments
 (0)