Skip to content

Commit 70da4be

Browse files
committed
Update and add more tests for blockplotting 2d coords
1 parent b041c50 commit 70da4be

9 files changed

Lines changed: 561 additions & 229 deletions

lib/iris/coords.py

Lines changed: 67 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -143,74 +143,97 @@ def __new__(cls, name_or_coord, minimum, maximum,
143143
'groupby_point, groupby_slice')
144144

145145

146-
def _discontiguity_in_2d_bounds(bds, abs_tol=1e-4):
146+
def _discontiguity_in_2d_bounds(bds, atol=1e-4):
147147
"""
148-
Check bounds of a 2-dimensional coordinate are contiguous
148+
Checks that the bounds of a 2-dimensional coordinate are contiguous.
149+
149150
Args:
150-
bds: Array of bounds of shape (X,Y,4)
151-
abs_tol: tolerance
151+
* bds: (array)
152+
Bounds array of shape (X,Y,4).
153+
* atol: (float)
154+
Absolute tolerance that is used when checking contiguity. Defaults to
155+
1e-4.
152156
153157
Returns:
154-
Bool, if there are no discontinuities
155-
absolute difference along the x axis
156-
absolute difference along the y axis
158+
* all_equal: (boolean)
159+
True if there are no discontiguities.
160+
* diffs_along_x: (array or None)
161+
Either an array of the absolute differences along the x-axis, of the
162+
shape (X,Y-1) or None if the input bounds has the shape (1,Y,4).
163+
* diffs_along_y: (array or None)
164+
Either an array of the absolute differences along the y-axis, of the
165+
shape (X-1,Y) or None if the input bounds has the shape (X,1,4).
157166
158167
"""
159168
# Check bds has the shape (ny, nx, 4)
160-
if not bds.ndim == 3 and bds.shape[2] == 4:
161-
raise ValueError('2D coordinates must have 4 bounds per point '
162-
'for 2D coordinate plotting')
169+
if not (bds.ndim == 3 and bds.shape[-1] == 4):
170+
raise ValueError('Bounds for 2D coordinates must be 3-dimensional and '
171+
'have 4 bounds per point.')
163172

164173
# Check ordering:
165174
# i i+1
166175
# j @0 @1
167176
# j+1 @3 @2
168-
def mod360_diff(x1, x2):
169-
diff = x1 - x2
177+
def mod360_diff(upper_bounds, lower_bounds):
178+
diff = upper_bounds - lower_bounds
170179
diff = (diff + 360.0 + 180.0) % 360.0 - 180.0
171180
return diff
172181

173-
# Compare cell with the cell next to it (i+1)
174-
diffs_along_x = mod360_diff(bds[:, :-1, 1], bds[:, 1:, 0])
175-
# Compare cell with the cell above it (j+1)
176-
diffs_along_y = mod360_diff(bds[:-1, :, 3], bds[1:, :, 0])
182+
def diffs_below_tolerance(diffs_along_axis):
183+
return np.all(np.abs(diffs_along_axis) < atol)
177184

178-
def eq_diffs(x1):
179-
return np.all(np.abs(x1) < abs_tol)
185+
# Compare cell with the cell next to it (i+1)
186+
if bds.shape[0] > 1:
187+
diffs_along_x = np.abs(mod360_diff(bds[:, :-1, 1], bds[:, 1:, 0]))
188+
match_y0_x1 = diffs_below_tolerance(diffs_along_x)
189+
else:
190+
diffs_along_x = None
191+
match_y0_x1 = True
180192

181-
match_y0_x1 = eq_diffs(diffs_along_x)
182-
match_y1_x0 = eq_diffs(diffs_along_y)
193+
# Compare cell with the cell above it (j+1)
194+
if bds.shape[1] > 1:
195+
diffs_along_y = np.abs(mod360_diff(bds[:-1, :, 3], bds[1:, :, 0]))
196+
match_y1_x0 = diffs_below_tolerance(diffs_along_y)
197+
else:
198+
diffs_along_y = None
199+
match_y1_x0 = True
183200

184201
all_eq = match_y0_x1 and match_y1_x0
185202

186-
return all_eq, np.abs(diffs_along_x), np.abs(diffs_along_y)
203+
return all_eq, diffs_along_x, diffs_along_y
187204

188205

189206
def _get_2d_coord_bound_grid(bds):
190207
"""
191-
Function used that takes a bounds array for a 2-D coordinate variable with
192-
4 sides and returns the bounds grid.
208+
Creates a grid using the bounds of a 2D coordinate with 4 sided cells.
193209
194-
Cf standards requires the four vertices of the cell to be traversed
195-
anti-clockwise if the coordinates are defined in a right handed coordinate
196-
system.
210+
Assumes that the four vertices of the cells are in an anti-clockwise order
211+
(bottom-left, bottom-right, top-right, top-left).
197212
198-
selects the zeroth vertex of each cell and then adds the column the first
199-
vertex at the end. For the top row it uses the thirs vertex, with the
200-
second added on to the end.
213+
Selects the zeroth vertex of each cell. A final column is added, which
214+
contains the first vertex of the cells in the final column. A final row
215+
is added, which contains the third vertex of all the cells in the final
216+
row, except for in the final column where it uses the second vertex.
201217
e.g.
202218
# 0-0-0-0-1
203219
# 0-0-0-0-1
204220
# 3-3-3-3-2
205221
206222
207223
Args:
208-
bounds: array of shape (X,Y,4)
224+
* bounds: (array)
225+
Coordinate bounds array of shape (X,Y,4)
209226
210227
Returns:
211-
array of shape (X+1, Y+1)
228+
* grid: (array)
229+
Grid of shape (X+1, Y+1)
212230
213231
"""
232+
# Check bds has the shape (ny, nx, 4)
233+
if not (bds.ndim == 3 and bds.shape[-1] == 4):
234+
raise ValueError('Bounds for 2D coordinates must be 3-dimensional and '
235+
'have 4 bounds per point.')
236+
214237
bds_shape = bds.shape
215238
result = np.zeros((bds_shape[0] + 1, bds_shape[1] + 1))
216239

@@ -1029,7 +1052,7 @@ def cells(self):
10291052
"""
10301053
return _CellIterator(self)
10311054

1032-
def _sanity_check_contiguous(self):
1055+
def _sanity_check_bounds(self):
10331056
if self.ndim == 1:
10341057
if self.nbounds != 2:
10351058
raise ValueError('Invalid operation for {!r}, with {} bounds. '
@@ -1043,9 +1066,9 @@ def _sanity_check_contiguous(self):
10431066
'coordinates with 4 bounds.'.format
10441067
(self.name(), self.nbounds))
10451068
else:
1046-
raise ValueError('Invalid operation for {!r}. Contiguous bounds '
1047-
'are not defined for coordinates with more than '
1048-
'2 dimensions.'.format(self.name()))
1069+
raise ValueError('Invalid operation for {!r}. Not supported for '
1070+
'bounds with more than 2 dimensions.'.format
1071+
(self.name()))
10491072

10501073
def is_contiguous(self, rtol=1e-05, atol=1e-08):
10511074
"""
@@ -1064,28 +1087,25 @@ def is_contiguous(self, rtol=1e-05, atol=1e-08):
10641087
10651088
"""
10661089
if self.has_bounds():
1067-
self._sanity_check_contiguous()
1090+
self._sanity_check_bounds()
10681091
if self.ndim == 1:
10691092
contiguous = np.allclose(self.bounds[1:, 0],
10701093
self.bounds[:-1, 1],
10711094
rtol=rtol, atol=atol)
10721095
elif self.ndim == 2:
1073-
contiguous, _, _ = _discontiguity_in_2d_bounds(self.bounds,
1074-
abs_tol=atol)
1096+
1097+
contiguous, _, _ = _discontiguity_in_2d_bounds(
1098+
self.bounds, atol=atol)
10751099
else:
10761100
contiguous = False
10771101
return contiguous
10781102

10791103
def contiguous_bounds(self):
10801104
"""
10811105
Returns the N+1 bound values for a contiguous bounded 1D coordinate
1082-
of length N.
1083-
1084-
Returns the (N+1, M+1) bound values for a contiguous bounded 2D
1106+
of length N, or the (N+1, M+1) bound values for a contiguous bounded 2D
10851107
coordinate of shape (N, M).
10861108
1087-
Assumes input is contiguous.
1088-
10891109
.. note::
10901110
10911111
If the coordinate does not have bounds, this method will
@@ -1097,7 +1117,10 @@ def contiguous_bounds(self):
10971117
'contiguous bounds.'.format(self.name()))
10981118
bounds = self._guess_bounds()
10991119
else:
1100-
self._sanity_check_contiguous()
1120+
if not self.is_contiguous():
1121+
raise ValueError('Invalid operation. Bounds of coord {!r} '
1122+
'are not contiguous.'.format(self.name()))
1123+
11011124
bounds = self.bounds
11021125

11031126
if self.ndim == 1:

lib/iris/plot.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,20 @@ def get_span(coord):
102102
raise ValueError(msg.format(coord.name()))
103103
if mode == iris.coords.BOUND_MODE and len(span) not in [1, 2]:
104104
raise ValueError('The coordinate {!r} has {} dimensions.'
105-
'Cell-based plotting is only supporting for'
105+
'Cell-based plotting is only supported for'
106106
'coordinates with one or two dimensions.'
107107
.format(coord.name()), len(span))
108108

109+
# Check the coords have the same number of dimensions.
110+
if ndims == 2:
111+
if not (len(spans[0]) == len(spans[1])):
112+
raise ValueError('The coordinate {!r} has {} dimensions and the '
113+
'coordinate {!r} has {} dimensions. Cell-based '
114+
'plotting is only supported for coordinates with '
115+
'the same number of dimensions.'
116+
.format(coords[0].name(), spans[0],
117+
coords[1].name(), spans[1]))
118+
109119
# Check the combination of coordinates spans enough (ndims) data
110120
# dimensions.
111121
total_span = set().union(*spans)
@@ -273,10 +283,11 @@ def _invert_yaxis(v_coord, axes=None):
273283

274284
def _check_contiguity_and_bounds(coord, data, abs_tol=1e-4, transpose=False):
275285
"""
276-
Check that the discontinuous bounds occur where the data is masked.
286+
Checks that any discontiguity in the bounds of the coordinate only occur
287+
where the data is masked.
277288
278-
If discontinuity occurs but data is masked, raise warning
279-
If discontinuity occurs and data is NOT masked, raise error
289+
If discontiguity occurs but data is masked, raise warning
290+
If discontiguity occurs and data is NOT masked, raise error
280291
281292
Args:
282293
coords:
@@ -293,7 +304,7 @@ def _check_contiguity_and_bounds(coord, data, abs_tol=1e-4, transpose=False):
293304
bounds = coord.bounds
294305

295306
both_dirs_contiguous, diffs_along_x, diffs_along_y = \
296-
iris.coords._discontiguity_in_2d_bounds(bounds, abs_tol=abs_tol)
307+
iris.coords._discontiguity_in_2d_bounds(bounds, atol=abs_tol)
297308

298309
if not both_dirs_contiguous:
299310

@@ -333,17 +344,13 @@ def _draw_2d_from_bounds(draw_method_name, cube, *args, **kwargs):
333344
else:
334345
plot_defn = _get_plot_defn(cube, mode, ndims=2)
335346

336-
twodim_contig_atol = kwargs.pop('two_dim_coord_contiguity_atol',
337-
1e-4)
338347
for coord in plot_defn.coords:
339348
if hasattr(coord, 'has_bounds'):
340349
if coord.ndim == 2 and coord.has_bounds():
341350
try:
342-
_check_contiguity_and_bounds(coord, data=cube.data,
343-
abs_tol=twodim_contig_atol)
351+
_check_contiguity_and_bounds(coord, data=cube.data)
344352
except ValueError:
345353
if _check_contiguity_and_bounds(coord, data=cube.data,
346-
abs_tol=twodim_contig_atol,
347354
transpose=True) is True:
348355
plot_defn.transpose = True
349356

@@ -1117,10 +1124,6 @@ def pcolor(cube, *args, **kwargs):
11171124
* axes: the :class:`matplotlib.axes.Axes` to use for drawing.
11181125
Defaults to the current axes if none provided.
11191126
1120-
* two_dim_coord_contiguity_atol: absolute tolerance when checking for
1121-
contiguity between cells in a two dimensional coordinate.
1122-
1123-
11241127
11251128
See :func:`matplotlib.pyplot.pcolor` for details of other valid
11261129
keyword arguments.
@@ -1152,9 +1155,6 @@ def pcolormesh(cube, *args, **kwargs):
11521155
* axes: the :class:`matplotlib.axes.Axes` to use for drawing.
11531156
Defaults to the current axes if none provided.
11541157
1155-
* two_dim_coord_contiguity_atol: absolute tolerance when checking for
1156-
contiguity between cells in a two dimensional coordinate.
1157-
11581158
See :func:`matplotlib.pyplot.pcolormesh` for details of other
11591159
valid keyword arguments.
11601160
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# (C) British Crown Copyright 2017, Met Office
2+
#
3+
# This file is part of Iris.
4+
#
5+
# Iris is free software: you can redistribute it and/or modify it under
6+
# the terms of the GNU Lesser General Public License as published by the
7+
# Free Software Foundation, either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# Iris is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU Lesser General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU Lesser General Public License
16+
# along with Iris. If not, see <http://www.gnu.org/licenses/>.
17+
"""
18+
Test plots with two dimensional coordinates.
19+
20+
"""
21+
22+
from __future__ import (absolute_import, division, print_function)
23+
from six.moves import (filter, input, map, range, zip) # noqa
24+
25+
# import iris tests first so that some things can be initialised before
26+
# importing anything else
27+
import iris.tests as tests
28+
29+
import iris
30+
31+
# Run tests in no graphics mode if matplotlib is not available.
32+
if tests.MPL_AVAILABLE:
33+
import matplotlib.pyplot as plt
34+
import iris.quickplot as qplt
35+
36+
37+
@tests.skip_data
38+
def simple_cube():
39+
path = tests.get_data_path(('NetCDF', 'ORCA2', 'votemper.nc'))
40+
cube = iris.load_cube(path)
41+
return cube[0, 0]
42+
43+
44+
@tests.skip_plot
45+
@tests.skip_data
46+
class Test(tests.GraphicsTest):
47+
def test_2d_coord_bounds(self):
48+
cube = simple_cube()
49+
qplt.pcolormesh(cube)
50+
self.check_graphic()
51+
52+
53+
if __name__ == "__main__":
54+
tests.main()

0 commit comments

Comments
 (0)