Skip to content

Commit 6b4ba73

Browse files
Fixes to _discontiguity_in_bounds (attempt 2) (#4975)
1 parent 3ca669d commit 6b4ba73

File tree

6 files changed

+125
-43
lines changed

6 files changed

+125
-43
lines changed

docs/src/whatsnew/latest.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,14 @@ This document explains the changes made to Iris for this release
7575
:class:`~iris.coords.CellMethod` are printed to be more CF compliant.
7676
(:pull:`5224`)
7777

78+
#. `@stephenworsley`_ fixed the way discontiguities were discovered for 2D coords.
79+
Previously, the only bounds being compared were the bottom right bound in one
80+
cell with the bottom left bound in the cell to its right, and the top left bound
81+
in a cell with the bottom left bound in the cell above it. Now all bounds are
82+
compared with all adjacent bounds from neighbouring cells. This affects
83+
:meth:`~iris.coords.Coord.is_contiguous` and :func:`iris.util.find_discontiguities`
84+
where additional discontiguities may be detected which previously were not.
85+
7886

7987
💣 Incompatible Changes
8088
=======================

lib/iris/coords.py

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1932,11 +1932,12 @@ def _discontiguity_in_bounds(self, rtol=1e-5, atol=1e-8):
19321932
* contiguous: (boolean)
19331933
True if there are no discontiguities.
19341934
* diffs: (array or tuple of arrays)
1935-
The diffs along the bounds of the coordinate. If self is a 2D
1936-
coord of shape (Y, X), a tuple of arrays is returned, where the
1937-
first is an array of differences along the x-axis, of the shape
1938-
(Y, X-1) and the second is an array of differences along the
1939-
y-axis, of the shape (Y-1, X).
1935+
A boolean array or tuple of boolean arrays which are true where
1936+
there are discontiguities between neighbouring bounds. If self is
1937+
a 2D coord of shape (Y, X), a pair of arrays is returned, where
1938+
the first is an array of differences along the x-axis, of the
1939+
shape (Y, X-1) and the second is an array of differences along
1940+
the y-axis, of the shape (Y-1, X).
19401941
19411942
"""
19421943
self._sanity_check_bounds()
@@ -1945,39 +1946,65 @@ def _discontiguity_in_bounds(self, rtol=1e-5, atol=1e-8):
19451946
contiguous = np.allclose(
19461947
self.bounds[1:, 0], self.bounds[:-1, 1], rtol=rtol, atol=atol
19471948
)
1948-
diffs = np.abs(self.bounds[:-1, 1] - self.bounds[1:, 0])
1949+
diffs = ~np.isclose(
1950+
self.bounds[1:, 0], self.bounds[:-1, 1], rtol=rtol, atol=atol
1951+
)
19491952

19501953
elif self.ndim == 2:
19511954

19521955
def mod360_adjust(compare_axis):
19531956
bounds = self.bounds.copy()
19541957

19551958
if compare_axis == "x":
1956-
upper_bounds = bounds[:, :-1, 1]
1957-
lower_bounds = bounds[:, 1:, 0]
1959+
# Extract the pairs of upper bounds and lower bounds which
1960+
# connect along the "x" axis. These connect along indices
1961+
# as shown by the following diagram:
1962+
#
1963+
# 3---2 + 3---2
1964+
# | | | |
1965+
# 0---1 + 0---1
1966+
upper_bounds = np.stack(
1967+
(bounds[:, :-1, 1], bounds[:, :-1, 2])
1968+
)
1969+
lower_bounds = np.stack(
1970+
(bounds[:, 1:, 0], bounds[:, 1:, 3])
1971+
)
19581972
elif compare_axis == "y":
1959-
upper_bounds = bounds[:-1, :, 3]
1960-
lower_bounds = bounds[1:, :, 0]
1973+
# Extract the pairs of upper bounds and lower bounds which
1974+
# connect along the "y" axis. These connect along indices
1975+
# as shown by the following diagram:
1976+
#
1977+
# 3---2
1978+
# | |
1979+
# 0---1
1980+
# + +
1981+
# 3---2
1982+
# | |
1983+
# 0---1
1984+
upper_bounds = np.stack(
1985+
(bounds[:-1, :, 3], bounds[:-1, :, 2])
1986+
)
1987+
lower_bounds = np.stack(
1988+
(bounds[1:, :, 0], bounds[1:, :, 1])
1989+
)
19611990

19621991
if self.name() in ["longitude", "grid_longitude"]:
19631992
# If longitude, adjust for longitude wrapping
19641993
diffs = upper_bounds - lower_bounds
1965-
index = diffs > 180
1994+
index = np.abs(diffs) > 180
19661995
if index.any():
19671996
sign = np.sign(diffs)
19681997
modification = (index.astype(int) * 360) * sign
19691998
upper_bounds -= modification
19701999

1971-
diffs_between_cells = np.abs(upper_bounds - lower_bounds)
1972-
cell_size = lower_bounds - upper_bounds
1973-
diffs_along_axis = diffs_between_cells > (
1974-
atol + rtol * cell_size
2000+
diffs_along_bounds = ~np.isclose(
2001+
upper_bounds, lower_bounds, rtol=rtol, atol=atol
19752002
)
1976-
1977-
points_close_enough = diffs_along_axis <= (
1978-
atol + rtol * cell_size
2003+
diffs_along_axis = np.logical_or(
2004+
diffs_along_bounds[0], diffs_along_bounds[1]
19792005
)
1980-
contiguous_along_axis = np.all(points_close_enough)
2006+
2007+
contiguous_along_axis = ~np.any(diffs_along_axis)
19812008
return diffs_along_axis, contiguous_along_axis
19822009

19832010
diffs_along_x, match_cell_x1 = mod360_adjust(compare_axis="x")

lib/iris/tests/stock/_stock_2d_latlons.py

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,9 @@ def sample_cube(xargs, yargs):
296296
return cube
297297

298298

299-
def make_bounds_discontiguous_at_point(cube, at_iy, at_ix, in_y=False):
299+
def make_bounds_discontiguous_at_point(
300+
cube, at_iy, at_ix, in_y=False, upper=True
301+
):
300302
"""
301303
Meddle with the XY grid bounds of a 2D cube to make the grid discontiguous.
302304
@@ -325,16 +327,22 @@ def adjust_coord(coord):
325327
if not in_y:
326328
# Make a discontinuity "at" (iy, ix), by moving the right-hand edge
327329
# of the cell to the midpoint of the existing left+right bounds.
328-
new_bds_br = 0.5 * (bds_bl + bds_br)
329-
new_bds_tr = 0.5 * (bds_tl + bds_tr)
330-
bds_br, bds_tr = new_bds_br, new_bds_tr
330+
new_bds_b = 0.5 * (bds_bl + bds_br)
331+
new_bds_t = 0.5 * (bds_tl + bds_tr)
332+
if upper:
333+
bds_br, bds_tr = new_bds_b, new_bds_t
334+
else:
335+
bds_bl, bds_tl = new_bds_b, new_bds_t
331336
else:
332337
# Same but in the 'grid y direction' :
333338
# Make a discontinuity "at" (iy, ix), by moving the **top** edge of
334339
# the cell to the midpoint of the existing **top+bottom** bounds.
335-
new_bds_tl = 0.5 * (bds_bl + bds_tl)
336-
new_bds_tr = 0.5 * (bds_br + bds_tr)
337-
bds_tl, bds_tr = new_bds_tl, new_bds_tr
340+
new_bds_l = 0.5 * (bds_bl + bds_tl)
341+
new_bds_r = 0.5 * (bds_br + bds_tr)
342+
if upper:
343+
bds_tl, bds_tr = new_bds_l, new_bds_r
344+
else:
345+
bds_bl, bds_br = new_bds_l, new_bds_r
338346

339347
# Write in the new bounds (all 4 corners).
340348
bds[at_iy, at_ix] = [bds_bl, bds_br, bds_tr, bds_tl]
@@ -355,7 +363,16 @@ def adjust_coord(coord):
355363
msg = "The coordinate {!r} doesn't span a data dimension."
356364
raise ValueError(msg.format(coord.name()))
357365

358-
masked_data = ma.masked_array(cube.data)
359-
masked_data[at_iy, at_ix] = ma.masked
366+
masked_data = ma.masked_array(cube.data)
367+
368+
# Mask all points which would be found discontiguous.
369+
# Note that find_discontiguities finds all instances where a cell is
370+
# discontiguous with a neighbouring cell to its *right* or *above*
371+
# that cell.
372+
masked_data[at_iy, at_ix] = ma.masked
373+
if in_y or not upper:
374+
masked_data[at_iy, at_ix - 1] = ma.masked
375+
if not in_y or not upper:
376+
masked_data[at_iy - 1, at_ix] = ma.masked
360377

361378
cube.data = masked_data

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -708,7 +708,7 @@ def test_1d_discontiguous(self):
708708
coord = DimCoord([10, 20, 40], bounds=[[5, 15], [15, 25], [35, 45]])
709709
contiguous, diffs = coord._discontiguity_in_bounds()
710710
self.assertFalse(contiguous)
711-
self.assertArrayEqual(diffs, np.array([0, 10]))
711+
self.assertArrayEqual(diffs, np.array([False, True]))
712712

713713
def test_1d_one_cell(self):
714714
# Test a 1D coord with a single cell.

lib/iris/tests/unit/util/test_find_discontiguities.py

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,26 +29,55 @@ def setUp(self):
2929
# Set up a 2d lat-lon cube with 2d coordinates that have been
3030
# transformed so they are not in a regular lat-lon grid.
3131
# Then generate a discontiguity at a single lat-lon point.
32-
self.testcube_discontig = full2d_global()
33-
make_bounds_discontiguous_at_point(self.testcube_discontig, 3, 3)
34-
# Repeat that for a discontiguity in the grid 'Y' direction.
35-
self.testcube_discontig_along_y = full2d_global()
32+
# Discontiguities will be caused at the rightmost bounds.
33+
self.testcube_discontig_right = full2d_global()
34+
make_bounds_discontiguous_at_point(self.testcube_discontig_right, 3, 3)
35+
36+
# Repeat for a discontiguity on the leftmost bounds.
37+
self.testcube_discontig_left = full2d_global()
38+
make_bounds_discontiguous_at_point(
39+
self.testcube_discontig_left, 2, 4, upper=False
40+
)
41+
# Repeat for a discontiguity on the topmost bounds.
42+
self.testcube_discontig_top = full2d_global()
3643
make_bounds_discontiguous_at_point(
37-
self.testcube_discontig_along_y, 2, 4, in_y=True
44+
self.testcube_discontig_top, 2, 4, in_y=True
3845
)
3946

40-
def test_find_discontiguities(self):
47+
# Repeat for a discontiguity on the botommost bounds.
48+
self.testcube_discontig_along_bottom = full2d_global()
49+
make_bounds_discontiguous_at_point(
50+
self.testcube_discontig_along_bottom, 2, 4, in_y=True, upper=False
51+
)
52+
53+
def test_find_discontiguities_right(self):
54+
# Check that the mask we generate when making the discontiguity
55+
# matches that generated by find_discontiguities
56+
cube = self.testcube_discontig_right
57+
expected = cube.data.mask
58+
returned = find_discontiguities(cube)
59+
self.assertTrue(np.all(expected == returned))
60+
61+
def test_find_discontiguities_left(self):
62+
# Check that the mask we generate when making the discontiguity
63+
# matches that generated by find_discontiguities
64+
cube = self.testcube_discontig_left
65+
expected = cube.data.mask
66+
returned = find_discontiguities(cube)
67+
self.assertTrue(np.all(expected == returned))
68+
69+
def test_find_discontiguities_top(self):
4170
# Check that the mask we generate when making the discontiguity
4271
# matches that generated by find_discontiguities
43-
cube = self.testcube_discontig
72+
cube = self.testcube_discontig_top
4473
expected = cube.data.mask
4574
returned = find_discontiguities(cube)
4675
self.assertTrue(np.all(expected == returned))
4776

48-
def test_find_discontiguities_in_y(self):
77+
def test_find_discontiguities_bottom(self):
4978
# Check that the mask we generate when making the discontiguity
5079
# matches that generated by find_discontiguities
51-
cube = self.testcube_discontig_along_y
80+
cube = self.testcube_discontig_along_bottom
5281
expected = cube.data.mask
5382
returned = find_discontiguities(cube)
5483
self.assertTrue(np.all(expected == returned))
@@ -61,7 +90,7 @@ def test_find_discontiguities_1d_coord(self):
6190
find_discontiguities(cube)
6291

6392
def test_find_discontiguities_with_atol(self):
64-
cube = self.testcube_discontig
93+
cube = self.testcube_discontig_right
6594
# Choose a very large absolute tolerance which will result in fine
6695
# discontiguities being disregarded
6796
atol = 100
@@ -72,7 +101,7 @@ def test_find_discontiguities_with_atol(self):
72101
self.assertTrue(np.all(expected == returned))
73102

74103
def test_find_discontiguities_with_rtol(self):
75-
cube = self.testcube_discontig
104+
cube = self.testcube_discontig_right
76105
# Choose a very large relative tolerance which will result in fine
77106
# discontiguities being disregarded
78107
rtol = 1000

lib/iris/util.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1820,14 +1820,15 @@ def _meshgrid(*xi, **kwargs):
18201820

18211821
def find_discontiguities(cube, rel_tol=1e-5, abs_tol=1e-8):
18221822
"""
1823-
Searches coord for discontiguities in the bounds array, returned as a
1824-
boolean array (True where discontiguities are present).
1823+
Searches the 'x' and 'y' coord on the cube for discontiguities in the
1824+
bounds array, returned as a boolean array (True for all cells which are
1825+
discontiguous with the cell immediately above them or to their right).
18251826
18261827
Args:
18271828
18281829
* cube (`iris.cube.Cube`):
18291830
The cube to be checked for discontinuities in its 'x' and 'y'
1830-
coordinates.
1831+
coordinates. These coordinates must be 2D.
18311832
18321833
Kwargs:
18331834

0 commit comments

Comments
 (0)