diff --git a/lib/iris/coords.py b/lib/iris/coords.py index 6402e73263..5519630c5d 100644 --- a/lib/iris/coords.py +++ b/lib/iris/coords.py @@ -1013,7 +1013,7 @@ def _sanity_check_bounds(self): 'are not defined for coordinates with more than ' '2 dimensions.'.format(self.name())) - def _discontiguity_in_bounds(self, rtol=1e-05, atol=1e-08): + def _discontiguity_in_bounds(self, rtol=1e-5, atol=1e-8): """ Checks that the bounds of the coordinate are contiguous. @@ -1064,9 +1064,14 @@ def mod360_adjust(compare_axis): modification = (index.astype(int) * 360) * sign upper_bounds -= modification - diffs_along_axis = np.abs(upper_bounds - lower_bounds) - contiguous_along_axis = np.allclose(upper_bounds, lower_bounds, - rtol=rtol, atol=atol) + diffs_between_cells = np.abs(upper_bounds - lower_bounds) + cell_size = lower_bounds - upper_bounds + diffs_along_axis = diffs_between_cells > (atol + + rtol * cell_size) + + points_close_enough = diffs_along_axis <= (atol + + rtol * cell_size) + contiguous_along_axis = np.all(points_close_enough) return diffs_along_axis, contiguous_along_axis diffs_along_x, match_cell_x1 = mod360_adjust(compare_axis='x') diff --git a/lib/iris/plot.py b/lib/iris/plot.py index e3193e96a4..88f89cc79a 100644 --- a/lib/iris/plot.py +++ b/lib/iris/plot.py @@ -104,7 +104,7 @@ def get_span(coord): raise ValueError('The coordinate {!r} has {} dimensions.' 'Cell-based plotting is only supported for' 'coordinates with one or two dimensions.' - .format(coord.name()), len(span)) + .format(coord.name(), len(span))) # Check the combination of coordinates spans enough (ndims) data # dimensions. @@ -271,7 +271,7 @@ def _invert_yaxis(v_coord, axes=None): axes.invert_yaxis() -def _check_bounds_contiguity_and_mask(coord, data, atol=None): +def _check_bounds_contiguity_and_mask(coord, data, atol=None, rtol=None): """ Checks that any discontiguities in the bounds of the given coordinate only occur where the data is masked. @@ -297,6 +297,7 @@ def _check_bounds_contiguity_and_mask(coord, data, atol=None): tolerance. """ + kwargs = {} data_is_masked = hasattr(data, 'mask') if data_is_masked: # When checking the location of the discontiguities, we check against @@ -317,15 +318,16 @@ def _check_bounds_contiguity_and_mask(coord, data, atol=None): elif coord.ndim == 2: if atol: - kwargs = {'atol': atol} - else: - kwargs = {} + kwargs['atol'] = atol + if rtol: + kwargs['rtol'] = rtol contiguous, diffs = coord._discontiguity_in_bounds(**kwargs) if not contiguous and data_is_masked: diffs_along_x, diffs_along_y = diffs - # Check along both dimensions. + # Check along both dimensions that any discontiguous + # points are correctly masked. not_masked_at_discontiguity_along_x = np.any( np.logical_and(mask_invert[:, :-1], diffs_along_x)) @@ -341,12 +343,20 @@ def _check_bounds_contiguity_and_mask(coord, data, atol=None): if not data_is_masked: raise ValueError('The bounds of the {} coordinate are not ' 'contiguous. Not able to create a suitable grid' - 'to plot.'.format(coord.name())) + 'to plot. You can use ' + 'iris.util.find_discontiguities() to identify ' + 'discontiguities in your x and y coordinate ' + 'bounds arrays.'.format(coord.name())) if not_masked_at_discontiguity: raise ValueError('The bounds of the {} coordinate are not ' 'contiguous and data is not masked where the ' 'discontiguity occurs. Not able to create a ' - 'suitable grid to plot.'.format( + 'suitable grid to plot. You can use ' + 'iris.util.find_discontiguities() to identify ' + 'discontiguities in your x and y coordinate ' + 'bounds arrays, and then mask them with ' + 'iris.util.mask_cube()' + ''.format( coord.name())) diff --git a/lib/iris/tests/stock/_stock_2d_latlons.py b/lib/iris/tests/stock/_stock_2d_latlons.py index 395a50d8ea..4fb3979a06 100644 --- a/lib/iris/tests/stock/_stock_2d_latlons.py +++ b/lib/iris/tests/stock/_stock_2d_latlons.py @@ -302,17 +302,17 @@ def sample_cube(xargs, yargs): return cube -def make_bounds_discontiguous_at_point(cube, at_iy, at_ix): +def make_bounds_discontiguous_at_point(cube, at_iy, at_ix, in_y=False): """ - Meddle with the XY grid bounds of a cube to make the grid discontiguous. + Meddle with the XY grid bounds of a 2D cube to make the grid discontiguous. Changes the points and bounds of a single gridcell, so that it becomes - discontinuous with the next gridcell to its right. - Also masks the cube data at the given point. + discontinuous with an adjacent gridcell : either the one to its right, or + the one above it (if 'in_y' is True). - The cube must have bounded 2d 'x' and 'y' coordinates. + Also masks the cube data at the given point. - TODO: add a switch to make a discontiguity in the *y*-direction instead ? + The cube must be 2-dimensional and have bounded 2d 'x' and 'y' coordinates. """ x_coord = cube.coord(axis='x') @@ -326,11 +326,21 @@ def adjust_coord(coord): pts, bds = coord.points, coord.bounds # Fetch the 4 bounds (bottom-left, bottom-right, top-right, top-left) bds_bl, bds_br, bds_tr, bds_tl = bds[at_iy, at_ix] - # Make a discontinuity "at" (iy, ix), by moving the right-hand edge of - # the cell to the midpoint of the existing left+right bounds. - new_bds_br = 0.5 * (bds_bl + bds_br) - new_bds_tr = 0.5 * (bds_tl + bds_tr) - bds_br, bds_tr = new_bds_br, new_bds_tr + if not in_y: + # Make a discontinuity "at" (iy, ix), by moving the right-hand edge + # of the cell to the midpoint of the existing left+right bounds. + new_bds_br = 0.5 * (bds_bl + bds_br) + new_bds_tr = 0.5 * (bds_tl + bds_tr) + bds_br, bds_tr = new_bds_br, new_bds_tr + else: + # Same but in the 'grid y direction' : + # Make a discontinuity "at" (iy, ix), by moving the **top** edge of + # the cell to the midpoint of the existing **top+bottom** bounds. + new_bds_tl = 0.5 * (bds_bl + bds_tl) + new_bds_tr = 0.5 * (bds_br + bds_tr) + bds_tl, bds_tr = new_bds_tl, new_bds_tr + + # Write in the new bounds (all 4 corners). bds[at_iy, at_ix] = [bds_bl, bds_br, bds_tr, bds_tl] # Also reset the cell midpoint to the middle of the 4 new corners, # in case having a midpoint outside the corners might cause a problem. @@ -341,10 +351,15 @@ def adjust_coord(coord): adjust_coord(x_coord) adjust_coord(y_coord) - # Also mask the relevant data point. - data = cube.data # N.B. fetch all the data. - if not ma.isMaskedArray(data): - # Promote to masked array, to avoid converting mask to NaN. - data = ma.masked_array(data) - data[at_iy, at_ix] = ma.masked - cube.data = data + + # Check which dimensions are spanned by each coordinate. + for coord in (x_coord, y_coord): + span = set(cube.coord_dims(coord)) + if not span: + msg = 'The coordinate {!r} doesn\'t span a data dimension.' + raise ValueError(msg.format(coord.name())) + + masked_data = ma.masked_array(cube.data) + masked_data[at_iy, at_ix] = ma.masked + + cube.data = masked_data diff --git a/lib/iris/tests/unit/coords/test_Coord.py b/lib/iris/tests/unit/coords/test_Coord.py index 27b468044d..d42448d609 100644 --- a/lib/iris/tests/unit/coords/test_Coord.py +++ b/lib/iris/tests/unit/coords/test_Coord.py @@ -480,7 +480,8 @@ def test_2d_discontiguous_along_x(self): contiguous, diffs = coord._discontiguity_in_bounds() diffs_along_x, diffs_along_y = diffs self.assertFalse(contiguous) - self.assertArrayEqual(diffs_along_x, np.array([2, 2, 2]).reshape(3, 1)) + self.assertArrayEqual(diffs_along_x, + np.array([True, True, True]).reshape(3, 1)) self.assertTrue(not diffs_along_y.any()) def test_2d_discontiguous_along_y(self): @@ -490,7 +491,7 @@ def test_2d_discontiguous_along_y(self): diffs_along_x, diffs_along_y = diffs self.assertFalse(contiguous) self.assertTrue(not diffs_along_x.any()) - self.assertArrayEqual(diffs_along_y, np.array([[2, 2, 2]])) + self.assertArrayEqual(diffs_along_y, np.array([[True, True, True]])) def test_2d_discontiguous_along_x_and_y(self): coord = AuxCoord(np.array([[1, 5], [3, 5]]), @@ -498,8 +499,8 @@ def test_2d_discontiguous_along_x_and_y(self): [[2, 4, 4, 2], [4, 6, 6, 4]]])) contiguous, diffs = coord._discontiguity_in_bounds() diffs_along_x, diffs_along_y = diffs - exp_x_diffs = np.array([2, 0]).reshape(2, 1) - exp_y_diffs = np.array([2, 0]).reshape(1, 2) + exp_x_diffs = np.array([True, False]).reshape(2, 1) + exp_y_diffs = np.array([True, False]).reshape(1, 2) self.assertFalse(contiguous) self.assertArrayEqual(diffs_along_x, exp_x_diffs) self.assertArrayEqual(diffs_along_y, exp_y_diffs) @@ -508,10 +509,11 @@ def test_2d_contiguous_along_x_atol(self): coord = AuxCoord(self.points_3by3[:, ::2], bounds=self.lon_bounds_3by3[:, ::2, :]) # Set a high atol that allows small discontiguities. - contiguous, diffs = coord._discontiguity_in_bounds(atol=2) + contiguous, diffs = coord._discontiguity_in_bounds(atol=5) diffs_along_x, diffs_along_y = diffs self.assertTrue(contiguous) - self.assertArrayEqual(diffs_along_x, np.array([2, 2, 2]).reshape(3, 1)) + self.assertArrayEqual(diffs_along_x, + np.array([False, False, False]).reshape(3, 1)) self.assertTrue(not diffs_along_y.any()) def test_2d_one_cell(self): @@ -571,7 +573,7 @@ def test_2d_discontiguous_mod_360(self): contiguous, diffs = coord._discontiguity_in_bounds() diffs_along_x, diffs_along_y = diffs self.assertFalse(contiguous) - self.assertArrayEqual(diffs_along_x, np.array([[170], [170]])) + self.assertArrayEqual(diffs_along_x, np.array([[True], [True]])) self.assertTrue(not diffs_along_y.any()) def test_2d_contiguous_mod_360_not_longitude(self): @@ -599,7 +601,7 @@ def test_2d_discontiguous_mod_360_not_longitude(self): contiguous, diffs = coord._discontiguity_in_bounds() diffs_along_x, diffs_along_y = diffs self.assertFalse(contiguous) - self.assertArrayEqual(diffs_along_x, np.array([[100], [100]])) + self.assertArrayEqual(diffs_along_x, np.array([[True], [True]])) self.assertTrue(not diffs_along_y.any()) diff --git a/lib/iris/tests/unit/plot/test__check_bounds_contiguity_and_mask.py b/lib/iris/tests/unit/plot/test__check_bounds_contiguity_and_mask.py index 225aa2890e..75c2b5be2f 100644 --- a/lib/iris/tests/unit/plot/test__check_bounds_contiguity_and_mask.py +++ b/lib/iris/tests/unit/plot/test__check_bounds_contiguity_and_mask.py @@ -33,6 +33,9 @@ from iris.tests.stock import (sample_2d_latlons, make_bounds_discontiguous_at_point) +from iris.plot import _check_bounds_contiguity_and_mask + + if tests.MPL_AVAILABLE: import iris.plot as iplt @@ -43,20 +46,21 @@ def test_1d_not_checked(self): # Test a 1D coordinate, which is not checked as atol is not set. coord = DimCoord([1, 3, 5], bounds=[[0, 2], [2, 4], [5, 6]]) data = np.array([278, 300, 282]) - iplt._check_bounds_contiguity_and_mask(coord, data) + # Make sure contiguity checking doesn't throw an error + _check_bounds_contiguity_and_mask(coord, data) def test_1d_contiguous(self): - # Test a 1D coordinate which is contiguous. + # Test that a 1D coordinate which is contiguous does not fail. coord = DimCoord([1, 3, 5], bounds=[[0, 2], [2, 4], [4, 6]]) data = np.array([278, 300, 282]) - iplt._check_bounds_contiguity_and_mask(coord, data, atol=1e-3) + _check_bounds_contiguity_and_mask(coord, data, atol=1e-3) def test_1d_discontigous_masked(self): # Test a 1D coordinate which is discontiguous but masked at # discontiguities. coord = DimCoord([1, 3, 5], bounds=[[0, 2], [2, 4], [5, 6]]) data = ma.array(np.array([278, 300, 282]), mask=[0, 1, 0]) - iplt._check_bounds_contiguity_and_mask(coord, data, atol=1e-3) + _check_bounds_contiguity_and_mask(coord, data, atol=1e-3) def test_1d_discontigous_unmasked(self): # Test a 1D coordinate which is discontiguous and unmasked at @@ -66,13 +70,13 @@ def test_1d_discontigous_unmasked(self): msg = 'coordinate are not contiguous and data is not masked where ' \ 'the discontiguity occurs' with self.assertRaisesRegexp(ValueError, msg): - iplt._check_bounds_contiguity_and_mask(coord, data, atol=1e-3) + _check_bounds_contiguity_and_mask(coord, data, atol=1e-3) def test_2d_contiguous(self): - # Test a 2D coordinate which is contiguous. + # Test that a 2D coordinate which is contiguous does not throw + # an error. cube = sample_2d_latlons() - iplt._check_bounds_contiguity_and_mask(cube.coord('longitude'), - cube.data) + _check_bounds_contiguity_and_mask(cube.coord('longitude'), cube.data) def test_2d_contiguous_atol(self): # Check the atol is passed correctly. @@ -82,18 +86,16 @@ def test_2d_contiguous_atol(self): # Discontiguity returns two objects that are unpacked in # `_check_bounds_contiguity_and_mask`. discontiguity_check.return_value = [True, None] - iplt._check_bounds_contiguity_and_mask(cube.coord('longitude'), - cube.data, - atol=1e-3) + _check_bounds_contiguity_and_mask(cube.coord('longitude'), + cube.data, atol=1e-3) discontiguity_check.assert_called_with(atol=1e-3) def test_2d_discontigous_masked(self): - # Test a 2D coordinate which is discontiguous but masked at - # discontiguities. + # Test that a 2D coordinate which is discontiguous but masked at + # discontiguities doesn't error. cube = sample_2d_latlons() make_bounds_discontiguous_at_point(cube, 3, 4) - iplt._check_bounds_contiguity_and_mask(cube.coord('longitude'), - cube.data) + _check_bounds_contiguity_and_mask(cube.coord('longitude'), cube.data) def test_2d_discontigous_unmasked(self): # Test a 2D coordinate which is discontiguous and unmasked at @@ -103,8 +105,8 @@ def test_2d_discontigous_unmasked(self): msg = 'coordinate are not contiguous' cube.data[3, 4] = ma.nomask with self.assertRaisesRegexp(ValueError, msg): - iplt._check_bounds_contiguity_and_mask(cube.coord('longitude'), - cube.data) + _check_bounds_contiguity_and_mask(cube.coord('longitude'), + cube.data) if __name__ == "__main__": diff --git a/lib/iris/tests/unit/util/test_find_discontiguities.py b/lib/iris/tests/unit/util/test_find_discontiguities.py new file mode 100644 index 0000000000..eeaae1596c --- /dev/null +++ b/lib/iris/tests/unit/util/test_find_discontiguities.py @@ -0,0 +1,99 @@ +# (C) British Crown Copyright 2018, Met Office +# +# This file is part of Iris. +# +# Iris is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Iris is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Iris. If not, see . +"""Test function :func:`iris.util.find_discontiguities""" + +from __future__ import (absolute_import, division, print_function) +from six.moves import (filter, input, map, range, zip) # noqa + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +import numpy as np + +from iris.tests.stock import simple_3d +from iris.tests.stock import sample_2d_latlons +from iris.tests.stock import make_bounds_discontiguous_at_point + +from iris.util import find_discontiguities + + +def full2d_global(): + return sample_2d_latlons(transformed=True) + + +@tests.skip_data +class Test(tests.IrisTest): + def setUp(self): + # Set up a 2d lat-lon cube with 2d coordinates that have been + # transformed so they are not in a regular lat-lon grid. + # Then generate a discontiguity at a single lat-lon point. + self.testcube_discontig = full2d_global() + make_bounds_discontiguous_at_point(self.testcube_discontig, 3, 3) + # Repeat that for a discontiguity in the grid 'Y' direction. + self.testcube_discontig_along_y = full2d_global() + make_bounds_discontiguous_at_point(self.testcube_discontig_along_y, + 2, 4, in_y=True) + + def test_find_discontiguities(self): + # Check that the mask we generate when making the discontiguity + # matches that generated by find_discontiguities + cube = self.testcube_discontig + expected = cube.data.mask + returned = find_discontiguities(cube) + self.assertTrue(np.all(expected == returned)) + + def test_find_discontiguities_in_y(self): + # Check that the mask we generate when making the discontiguity + # matches that generated by find_discontiguities + cube = self.testcube_discontig_along_y + expected = cube.data.mask + returned = find_discontiguities(cube) + self.assertTrue(np.all(expected == returned)) + + def test_find_discontiguities_1d_coord(self): + # Check that an error is raised when we try and use + # find_discontiguities on 1D coordinates: + cube = simple_3d() + with self.assertRaises(NotImplementedError): + find_discontiguities(cube) + + def test_find_discontiguities_with_atol(self): + cube = self.testcube_discontig + # Choose a very large absolute tolerance which will result in fine + # discontiguities being disregarded + atol = 100 + # Construct an array the size of the points array filled with 'False' + # to represent a mask showing no discontiguities + expected = np.zeros(cube.shape, dtype=bool) + returned = find_discontiguities(cube, abs_tol=atol) + self.assertTrue(np.all(expected == returned)) + + def test_find_discontiguities_with_rtol(self): + cube = self.testcube_discontig + # Choose a very large relative tolerance which will result in fine + # discontiguities being disregarded + rtol = 1000 + # Construct an array the size of the points array filled with 'False' + # to represent a mask showing no discontiguities + expected = np.zeros(cube.shape, dtype=bool) + returned = find_discontiguities(cube, rel_tol=rtol) + self.assertTrue(np.all(expected == returned)) + + +if __name__ == '__main__': + tests.main() diff --git a/lib/iris/tests/unit/util/test_mask_cube.py b/lib/iris/tests/unit/util/test_mask_cube.py new file mode 100644 index 0000000000..ce14662174 --- /dev/null +++ b/lib/iris/tests/unit/util/test_mask_cube.py @@ -0,0 +1,61 @@ +# (C) British Crown Copyright 2018, Met Office +# +# This file is part of Iris. +# +# Iris is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Iris is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Iris. If not, see . +"""Test function :func:`iris.util.mask_cube""" + +from __future__ import (absolute_import, division, print_function) +from six.moves import (filter, input, map, range, zip) # noqa + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +import numpy as np +import numpy.ma as ma + +from iris.util import mask_cube +from iris.tests.stock import sample_2d_latlons +from iris.tests.stock import make_bounds_discontiguous_at_point + + +def full2d_global(): + return sample_2d_latlons(transformed=True) + + +@tests.skip_data +class Test(tests.IrisTest): + def setUp(self): + # Set up a 2d cube with a masked discontiguity to test masking + # of 2-dimensional cubes + self.cube_2d = full2d_global() + make_bounds_discontiguous_at_point(self.cube_2d, 3, 3) + + def test_mask_cube_2d(self): + # This tests the masking of a 2d data array + cube = self.cube_2d + discontiguity_array = ma.getmaskarray(cube.data).copy() + expected = cube.copy() + + # Remove mask so that we can pass an unmasked data set to + # mask_discontiguities, and check that it masks the correct point by + # comparing with masked data + cube.data.mask = ma.nomask + returned = mask_cube(cube, discontiguity_array) + self.assertTrue(np.all(expected.data.mask == returned.data.mask)) + + +if __name__ == '__main__': + tests.main() diff --git a/lib/iris/util.py b/lib/iris/util.py index 4f4c42dc4b..a12a5e0102 100644 --- a/lib/iris/util.py +++ b/lib/iris/util.py @@ -33,13 +33,13 @@ import os.path import sys import tempfile -import time import cf_units import numpy as np import numpy.ma as ma import iris +import iris.coords import iris.exceptions @@ -1554,3 +1554,111 @@ def _meshgrid(*xi, **kwargs): if mxii.dtype != xii.dtype: mxi[i] = mxii.astype(xii.dtype) return mxi + + +def find_discontiguities(cube, rel_tol=1e-5, abs_tol=1e-8): + """ + Searches coord for discontiguities in the bounds array, returned as a + boolean array (True where discontiguities are present). + + Args: + + * cube (`iris.cube.Cube`): + The cube to be checked for discontinuities in its 'x' and 'y' + coordinates. + + Kwargs: + + * rel_tol (float): + The relative equality tolerance to apply in coordinate bounds + checking. + + * abs_tol (float): + The absolute value tolerance to apply in coordinate bounds + checking. + + Returns: + + * result (`numpy.ndarray` of bool) : + true/false map of which cells in the cube XY grid have + discontiguities in the coordinate points array. + + This can be used as the input array for + :func:`iris.util.mask_discontiguities`. + + Examples:: + + # Find any unknown discontiguities in your cube's x and y arrays: + discontiguities = iris.util.find_discontiguities(cube) + + # Pass the resultant boolean array to `iris.util.mask_discontiguities` + # with a cube slice; this will use the boolean array to mask + # any discontiguous data points before plotting: + masked_cube_slice = iris.util.mask_discontiguities(cube[0], + discontiguities) + + # Plot the masked cube slice: + iplt.pcolormesh(masked_cube_slice) + + """ + lats_and_lons = ['latitude', 'grid_latitude', + 'longitude', 'grid_longitude'] + spatial_coords = [coord for coord in cube.aux_coords if + coord.name() in lats_and_lons] + dim_err_msg = 'Discontiguity searches are currently only supported for ' \ + '2-dimensional coordinates.' + if len(spatial_coords) != 2: + raise NotImplementedError(dim_err_msg) + + # Check which dimensions are spanned by each coordinate. + for coord in spatial_coords: + if coord.ndim != 2: + raise NotImplementedError(dim_err_msg) + else: + span = set(cube.coord_dims(coord)) + if not span: + msg = 'The coordinate {!r} doesn\'t span a data dimension.' + raise ValueError(msg.format(coord.name())) + + # Check that the 2d coordinate arrays are the same shape as each other + if len(spatial_coords) == 2: + assert spatial_coords[0].points.shape == spatial_coords[1].points.shape + + # Set up unmasked boolean array the same size as the coord points array: + bad_points_boolean = np.zeros(spatial_coords[0].points.shape, dtype=bool) + + for coord in spatial_coords: + _, (diffs_x, diffs_y) = coord._discontiguity_in_bounds(rtol=rel_tol, + atol=abs_tol) + + bad_points_boolean[:, :-1] = np.logical_or(bad_points_boolean[:, :-1], + diffs_x) + # apply mask for y-direction discontiguities: + bad_points_boolean[:-1, :] = np.logical_or(bad_points_boolean[:-1, :], + diffs_y) + return bad_points_boolean + + +def mask_cube(cube, points_to_mask): + """ + Masks any cells in the data array which correspond to cells marked `True` + in the `points_to_mask` array. + + Args: + + * cube (`iris.cube.Cube`): + A 2-dimensional instance of :class:`iris.cube.Cube`. + + * points_to_mask (`numpy.ndarray` of bool): + A 2d boolean array of Truth values representing points to mask in the + x and y arrays of the cube. + + Returns: + + * result (`iris.cube.Cube`): + A cube whose data array is masked at points specified by input array. + + """ + cube.data = ma.masked_array(cube.data) + cube.data[points_to_mask] = ma.masked + return cube