From ddff1e7b4a980f5027014d459d48ff39b9ac6a29 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Thu, 14 Jun 2018 08:16:02 +0100 Subject: [PATCH 01/62] minor requirements tidy --- requirements/all.txt | 2 +- requirements/gen_conda_requirements.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/all.txt b/requirements/all.txt index bf9ba34e4e..657be9ce19 100644 --- a/requirements/all.txt +++ b/requirements/all.txt @@ -5,7 +5,7 @@ #conda: esmpy>=7.0 (only python=2) #gdal : under review -- not tested at present mo_pack -nc-time-axis # conda: nc_time_axis +nc-time-axis pandas stratify #conda: python-stratify pyugrid diff --git a/requirements/gen_conda_requirements.py b/requirements/gen_conda_requirements.py index a6a5fd84aa..e7ad2654d5 100644 --- a/requirements/gen_conda_requirements.py +++ b/requirements/gen_conda_requirements.py @@ -28,7 +28,7 @@ def read_conda_reqs(fname, options): with open(fname, 'r') as fh: for line in fh: line = line.strip() - if '#conda:' in line: + if CONDA_PATTERN in line: line_start = line.index(CONDA_PATTERN) + len(CONDA_PATTERN) line = line[line_start:].strip() if 'only python=2' in line: From a0d18011b8b54f592fa6d417838aa0d79392f88f Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Thu, 2 Aug 2018 16:22:07 +0100 Subject: [PATCH 02/62] Pin Dask for avoid 0.18.2 bug with masked arrays. --- requirements/core.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/core.txt b/requirements/core.txt index c28cdb04a1..997c91c665 100644 --- a/requirements/core.txt +++ b/requirements/core.txt @@ -6,7 +6,7 @@ cartopy cf_units>=2 cftime -dask[array]>=0.17.1 #conda: dask>=0.17.1 +dask[array]==0.18.1 #conda: dask==0.18.1 matplotlib>=2 netcdf4 numpy>=1.14 From c89f43e8a741b7126df2e92809062cca62eec146 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Fri, 3 Aug 2018 11:23:13 +0100 Subject: [PATCH 03/62] Tiny fix for dask, Python3 only? --- lib/iris/analysis/geometry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/iris/analysis/geometry.py b/lib/iris/analysis/geometry.py index a415e10091..cca5d836ec 100644 --- a/lib/iris/analysis/geometry.py +++ b/lib/iris/analysis/geometry.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2010 - 2015, Met Office +# (C) British Crown Copyright 2010 - 2018, Met Office # # This file is part of Iris. # @@ -223,7 +223,7 @@ def geometry_area_weights(cube, geometry, normalize=False): else: slices.append(slice(None)) - weights[slices] = subweights + weights[tuple(slices)] = subweights # Fix for the limitation of iris.analysis.MEAN weights handling. # Broadcast the array to the full shape of the cube From a6de324d7240a3f9bca5226fe92526865f832c71 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Mon, 13 Aug 2018 16:46:34 +0100 Subject: [PATCH 04/62] Disable doctests for Python 2. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b92ecdbfb2..bd4c64b6f1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -128,7 +128,7 @@ script: - INSTALL_DIR=$(pwd) - > - if [[ $TEST_TARGET == 'doctest' ]]; then + if [[ $TEST_TARGET == 'doctest' && ${TRAVIS_PYTHON_VERSION} == 3* ]]; then MPL_RC_DIR=$HOME/.config/matplotlib; mkdir -p $MPL_RC_DIR; echo 'backend : agg' > $MPL_RC_DIR/matplotlibrc; From 6e32e14263df7167f0fe5287d917754c88be2c8b Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Mon, 13 Aug 2018 16:46:56 +0100 Subject: [PATCH 05/62] Ignore warnings and update array printouts. --- .../src/userguide/interpolation_and_regridding.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/iris/src/userguide/interpolation_and_regridding.rst b/docs/iris/src/userguide/interpolation_and_regridding.rst index 4f2b06de44..e3cd622541 100644 --- a/docs/iris/src/userguide/interpolation_and_regridding.rst +++ b/docs/iris/src/userguide/interpolation_and_regridding.rst @@ -5,6 +5,8 @@ import numpy as np import iris + import warnings + warnings.simplefilter('ignore') ================================= Cube interpolation and regridding @@ -137,10 +139,9 @@ This cube has a "hybrid-height" vertical coordinate system, meaning that the ver coordinate is unevenly spaced in altitude: >>> print(column.coord('altitude').points) - [418.6983642578125 434.57049560546875 456.79278564453125 485.3664855957031 - 520.2932739257812 561.5751953125 609.2144775390625 663.214111328125 - 723.5769653320312 790.306640625 863.4072265625 942.88232421875 - 1028.737060546875 1120.9764404296875 1219.6051025390625] + [ 418.69836 434.5705 456.7928 485.3665 520.2933 561.5752 + 609.2145 663.2141 723.57697 790.30664 863.4072 942.8823 + 1028.737 1120.9764 1219.6051 ] We could regularise the vertical coordinate by defining 10 equally spaced altitude sample points between 400 and 1250 and interpolating our vertical coordinate onto @@ -184,9 +185,8 @@ For example, to mask values that lie beyond the range of the original data: >>> scheme = iris.analysis.Linear(extrapolation_mode='mask') >>> new_column = column.interpolate(sample_points, scheme) >>> print(new_column.coord('altitude').points) - [nan 494.44451904296875 588.888916015625 683.333251953125 777.77783203125 - 872.2222290039062 966.666748046875 1061.111083984375 1155.555419921875 - nan] + [ nan 494.44452 588.8889 683.33325 777.77783 872.2222 + 966.66675 1061.1111 1155.5554 nan] .. _caching_an_interpolator: From d69222a795e147462d2f0044c946622703d4769f Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Mon, 13 Aug 2018 17:53:04 +0100 Subject: [PATCH 06/62] Better way to disable Python2 doctests. --- .travis.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index bd4c64b6f1..ac5d341307 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,6 +31,15 @@ install: export IRIS_TEST_DATA_REF="2f3a6bcf25f81bd152b3d66223394074c9069a96"; export IRIS_TEST_DATA_SUFFIX=$(echo "${IRIS_TEST_DATA_REF}" | sed "s/^v//"); + # Cut short doctest phase under Python 2 : now only supports Python 3 + # SEE : https://github.com/SciTools/iris/pull/3134 + # ------------ + - > + if [[ $TEST_TARGET == 'doctest' && ${TRAVIS_PYTHON_VERSION} != 3* ]]; then + echo "DOCTEST phase only valid in Python 3 : ABORTING during 'install'." + exit 0 + fi + # Install miniconda # ----------------- - > @@ -128,7 +137,7 @@ script: - INSTALL_DIR=$(pwd) - > - if [[ $TEST_TARGET == 'doctest' && ${TRAVIS_PYTHON_VERSION} == 3* ]]; then + if [[ $TEST_TARGET == 'doctest' ]]; then MPL_RC_DIR=$HOME/.config/matplotlib; mkdir -p $MPL_RC_DIR; echo 'backend : agg' > $MPL_RC_DIR/matplotlibrc; From ff7ec92cd1294d188bebbe034b1e6b22c57e0ca6 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Mon, 23 Jul 2018 16:10:40 +0100 Subject: [PATCH 07/62] Add gridcell_angles and rotate_grid_vectors to iris.analysis.cartography, with tests: INCOMPLETE WIP --- lib/iris/analysis/_grid_angles.py | 261 ++++++++++++++++++ lib/iris/analysis/cartography.py | 3 + .../cartography/test_gridcell_angles.py | 171 ++++++++++++ 3 files changed, 435 insertions(+) create mode 100644 lib/iris/analysis/_grid_angles.py create mode 100644 lib/iris/tests/unit/analysis/cartography/test_gridcell_angles.py diff --git a/lib/iris/analysis/_grid_angles.py b/lib/iris/analysis/_grid_angles.py new file mode 100644 index 0000000000..6f7dcf15db --- /dev/null +++ b/lib/iris/analysis/_grid_angles.py @@ -0,0 +1,261 @@ +import numpy as np + +import iris + + +def _3d_xyz_from_latlon(lon, lat): + """ + Return locations of (lon, lat) in 3D space. + + Args: + + * lon, lat: (arrays in degrees) + + Returns: + + x, y, z : (array, dtype=float64) + cartesian coordinates on a unit sphere. + + """ + lon1 = np.deg2rad(lon).astype(np.float64) + lat1 = np.deg2rad(lat).astype(np.float64) + + old_way = True + if old_way: + x3 = np.empty((3,) + lon.shape, dtype=float) + x3[0] = np.cos(lat1) * np.cos(lon1) + x3[1] = np.cos(lat1) * np.sin(lon1) + x3[2] = np.sin(lat1) + result = x3 + else: + x = np.cos(lat1) * np.cos(lon1) + y = np.cos(lat1) * np.sin(lon1) + z = np.sin(lat1) + + result = np.concatenate([array[np.newaxis] for array in (x, y, z)]) + + return result + + +def _latlon_from_xyz(xyz): + """ + Return arrays of lons+lats angles from xyz locations. + + Args: + + * xyz: (array) + positions array, of dims (3, ), where index 0 maps x/y/z. + + Returns: + + lonlat : (array) + spherical angles, of dims (2, ), in radians. + Dim 0 maps longitude, latitude. + + """ + lons = np.arctan2(xyz[1], xyz[0]) + axial_radii = np.sqrt(xyz[0] * xyz[0] + xyz[1] * xyz[1]) + lats = np.arctan2(xyz[2], axial_radii) + return np.array([lons, lats]) + + +def _angle(p, q, r): + """ + Return angle (in _radians_) of grid wrt local east. Anticlockwise +ve, as usual. + {P, Q, R} are consecutve points in the same row, eg {v(i,j),f(i,j),v(i+1,j)}, or {T(i-1,j),T(i,j),T(i+1,j)} + Calculate dot product of PR with lambda_hat at Q. This gives us cos(required angle). + Disciminate between +/- angles by comparing latitudes of P and R. + p, q, r, are all 2-element arrays [lon, lat] of angles in degrees. + + """ + q1 = np.deg2rad(q) + + pr = _3d_xyz_from_latlon(r[0], r[1]) - _3d_xyz_from_latlon(p[0], p[1]) + pr_norm = np.sqrt(np.sum(pr**2, axis=0)) + pr_top = pr[1] * np.cos(q1[0]) - pr[0] * np.sin(q1[0]) + + index = np.where(pr_norm == 0) + pr_norm[index] = 1 + + cosine = np.maximum(np.minimum(pr_top / pr_norm, 1), -1) + cosine[index] = 0 + + psi = np.arccos(cosine) * np.sign(r[1] - p[1]) + psi[index] = np.nan + + return psi + + +def gridcell_angles(x, y=None): + """ + Calculate gridcell orientation angles. + + The inputs (x [,y]) can be any of the folliwing : + + * x (:class:`~iris.cube.Cube`): + a grid cube with 2D longitude and latitude coordinates. + + * x, y (:class:`~iris.coords.Coord`): + longitude and latitude coordinates. + + * x, y (2-dimensional arrays of same shape (ny, nx)): + longitude and latitude cell center locations, in degrees. + + * x, y (3-dimensional arrays of same shape (ny, nx, 4)): + longitude and latitude cell bounds, in degrees. + The last index maps cell corners anticlockwise from bottom-left. + + Returns: + + angles : (2-dimensional cube) + + Cube of angles of grid-x vector from true Eastward direction for + each gridcell, in radians. + It also has longitude and latitude coordinates. If coordinates + were input the output has identical ones : If the input was 2d + arrays, the output coords have no bounds; or, if the input was 3d + arrays, the output coords have bounds and centrepoints which are + the average of the 4 bounds. + + """ + if hasattr(x, 'core_data'): + # N.B. only "true" lats + longs will do : Cannot handle rotated ! + x, y = x.coord('longitude'), x.coord('latitude') + + # Now should have either 2 coords or 2 arrays. + if not hasattr(x, 'shape') and hasattr(y, 'shape'): + msg = ('Inputs (x,y) must have array shape property.' + 'Got type(x)={} and type(y)={}.') + raise ValueError(msg.format(type(x), type(y))) + + x_coord, y_coord = None, None + if isinstance(x, iris.coords.Coord) and isinstance(y, iris.coords.Coord): + x_coord, y_coord = x.copy(), y.copy() + x_coord.convert_units('degrees') + y_coord.convert_units('degrees') + if x_coord.ndim != 2 or y_coord.ndim != 2: + msg = ('Coordinate inputs must have 2-dimensional shape. ', + 'Got x-shape of {} and y-shape of {}.') + raise ValueError(msg.format(x_coord.shape, y_coord.shape)) + if x_coord.shape != y_coord.shape: + msg = ('Coordinate inputs must have same shape. ', + 'Got x-shape of {} and y-shape of {}.') + raise ValueError(msg.format(x_coord.shape, y_coord.shape)) +# NOTE: would like to check that dims are in correct order, but can't do that +# if there is no cube. +# TODO: **document** -- another input format requirement +# x_dims, y_dims = (cube.coord_dims(co) for co in (x_coord, y_coord)) +# if x_dims != (0, 1) or y_dims != (0, 1): +# msg = ('Coordinate inputs must map to cube dimensions (0, 1). ', +# 'Got x-dims of {} and y-dims of {}.') +# raise ValueError(msg.format(x_dims, y_dims)) + if x_coord.has_bounds() and y_coord.has_bounds(): + x, y = x_coord.bounds, y_coord.bounds + else: + x, y = x_coord.points, y_coord.points + + elif isinstance(x, iris.coords.Coord) or isinstance(y, iris.coords.Coord): + is_and_not = ('x', 'y') + if isinstance(y, iris.coords.Coord): + is_and_not = reversed(is_and_not) + msg = 'Input {!r} is a Coordinate, but {!r} is not.' + raise ValueError(*is_and_not) + + # Now have either 2 points arrays or 2 bounds arrays. + # Construct (lhs, mid, rhs) where these represent 3 adjacent points with + # increasing longitudes. + if x.ndim == 2: + # PROBLEM: we can't use this if data is not full-longitudes, + # i.e. rhs of array must connect to lhs (aka 'circular' coordinate). + # But we have no means of checking that ? + + # Use previous + subsequent points along longitude-axis as references. + # NOTE: we also have no way to check that dim #2 really is the 'X' dim. + mid = np.array([x, y]) + lhs = np.roll(mid, 1, 2) + rhs = np.roll(mid, -1, 2) + if not x_coord: + # Create coords for result cube : with no bounds. + y_coord = iris.coords.AuxCoord(x, standard_name='latitude', + units='degrees') + x_coord = iris.coords.AuxCoord(y, standard_name='longitude', + units='degrees') + else: + # Get lhs and rhs locations by averaging top+bottom each side. + # NOTE: so with bounds, we *don't* need full circular longitudes. + xyz = _3d_xyz_from_latlon(x, y) + lhs_xyz = 0.5 * (xyz[..., 0] + xyz[..., 3]) + rhs_xyz = 0.5 * (xyz[..., 1] + xyz[..., 2]) + if not x_coord: + # Create bounded coords for result cube. + # Use average lhs+rhs points in 3d to get 'mid' points, as coords + # with no points are not allowed. + mid_xyz = 0.5 * (lhs_xyz + rhs_xyz) + mid_latlons = _latlon_from_xyz(mid_xyz) + # Create coords with given bounds, and averaged centrepoints. + x_coord = iris.coords.AuxCoord( + points=mid_latlons[0], bounds=x, + standard_name='longitude', units='degrees') + y_coord = iris.coords.AuxCoord( + points=mid_latlons[1], bounds=y, + standard_name='latitude', units='degrees') + # Convert lhs and rhs points back to latlon form -- IN DEGREES ! + lhs = np.rad2deg(_latlon_from_xyz(lhs_xyz)) + rhs = np.rad2deg(_latlon_from_xyz(rhs_xyz)) + # mid is coord.points, whether input or made up. + mid = np.array([x_coord.points, y_coord.points]) + + # Do the angle calcs, and return as a suitable cube. + angles = _angle(lhs, mid, rhs) + result = iris.cube.Cube(angles, + long_name='gridcell_angle_from_true_east', + units='radians') + result.add_aux_coord(x_coord, (0, 1)) + result.add_aux_coord(y_coord, (0, 1)) + return result + + +def true_vectors_from_grid_vectors(u_cube, v_cube, grid_angles_cube=None): + """ + Rotate distance vectors from grid-oriented to true-latlon-oriented. + + Args: + + * u_cube, v_cube : (cube) + Cubes of grid-u and grid-v vector components. + Units should be differentials of true-distance, e.g. 'm/s'. + + Optional args: + + * grid_angles_cube : (cube) + gridcell orientation angles. + Units must be angular, i.e. can be converted to 'radians'. + If not provided, grid angles are estimated from 'u_cube' using the + :func:`gridcell_angles` method. + + Returns: + + true_u, true_v : (cube) + Cubes of true-north oriented vector components. + Units are same as inputs. + + """ + u_out, v_out = (cube.copy() for cube in (u_cube, v_cube)) + if not grid_angles_cube: + grid_angles_cube = gridcell_angles(u_cube) + gridangles = grid_angles_cube.copy() + gridangles.convert_units('radians') + uu, vv, aa = (cube.data for cube in (u_out, v_out, gridangles)) + mags = np.sqrt(uu*uu + vv*vv) + angs = np.arctan2(vv, uu) + aa + uu, vv = mags * np.cos(angs), mags * np.sin(angs) + + # Promote all to masked arrays, and also apply mask at bad (NaN) angles. + mask = np.isnan(aa) + for cube in (u_out, v_out, aa): + if hasattr(cube.data, 'mask'): + mask |= cube.data.mask + u_out.data = np.ma.masked_array(uu, mask=mask) + v_out.data = np.ma.masked_array(vv, mask=mask) + + return u_out, v_out diff --git a/lib/iris/analysis/cartography.py b/lib/iris/analysis/cartography.py index 4b68dcc949..1c55d48fd3 100644 --- a/lib/iris/analysis/cartography.py +++ b/lib/iris/analysis/cartography.py @@ -37,6 +37,9 @@ import iris.coord_systems import iris.exceptions from iris.util import _meshgrid +from ._grid_angles import ( + gridcell_angles, + true_vectors_from_grid_vectors as rotate_grid_vectors) # This value is used as a fall-back if the cube does not define the earth diff --git a/lib/iris/tests/unit/analysis/cartography/test_gridcell_angles.py b/lib/iris/tests/unit/analysis/cartography/test_gridcell_angles.py new file mode 100644 index 0000000000..b3a10a783f --- /dev/null +++ b/lib/iris/tests/unit/analysis/cartography/test_gridcell_angles.py @@ -0,0 +1,171 @@ +# (C) British Crown Copyright 2015 - 2016, 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 . +""" +Unit tests for the function +:func:`iris.analysis.cartography.gridcell_angles`. + +""" +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 + +import cartopy.crs as ccrs +from iris.cube import Cube +from iris.coords import DimCoord, AuxCoord +import iris.coord_systems +from iris.analysis.cartography import unrotate_pole + +from iris.analysis.cartography import (gridcell_angles, + rotate_grid_vectors) + + +def _rotated_grid_sample(pole_lat=15, pole_lon=-180, + lon_bounds=np.linspace(-30, 30, 8, endpoint=True), + lat_bounds=np.linspace(-30, 30, 8, endpoint=True)): + # Calculate *true* lat_bounds+lon_bounds for the rotated grid. + lon_bounds = np.array(lon_bounds, dtype=float) + lat_bounds = np.array(lat_bounds, dtype=float) + # Construct centrepoints. + lons = 0.5 * (lon_bounds[:-1] + lon_bounds[1:]) + lats = 0.5 * (lat_bounds[:-1] + lat_bounds[1:]) + # Convert all to full 2d arrays. + lon_bounds, lat_bounds = np.meshgrid(lon_bounds, lat_bounds) + lons, lats = np.meshgrid(lons, lats) + # Calculate true lats+lons for all points. + lons_true_bds, lats_true_bds = unrotate_pole(lon_bounds, lat_bounds, + pole_lon, pole_lat) + lons_true, lats_true = unrotate_pole(lons, lats, pole_lon, pole_lat) + # Make the 'unified' bounds into contiguous (ny, nx, 4) arrays. + def expand_unified_bds(bds): + ny, nx = bds.shape + bds_4 = np.zeros((ny - 1, nx - 1, 4)) + bds_4[:, :, 0] = bds[:-1, :-1] + bds_4[:, :, 1] = bds[:-1, 1:] + bds_4[:, :, 2] = bds[1:, 1:] + bds_4[:, :, 3] = bds[1:, :-1] + return bds_4 + + lon_true_bds4, lat_true_bds4 = (expand_unified_bds(bds) + for bds in (lons_true_bds, lats_true_bds)) + # Make these into a 2d-latlon grid for a cube + cube = Cube(np.zeros(lon_true_bds4.shape[:-1])) + co_x = AuxCoord(lons_true, bounds=lon_true_bds4, + standard_name='longitude', units='degrees') + co_y = AuxCoord(lats_true, bounds=lat_true_bds4, + standard_name='latitude', units='degrees') + cube.add_aux_coord(co_x, (0, 1)) + cube.add_aux_coord(co_y, (0, 1)) + return cube + + +class TestGridcellAngles(tests.IrisTest): + def test_values(self): + # Construct a rotated-pole grid and check angle calculation. + testcube = _rotated_grid_sample() + angles_cube = gridcell_angles(testcube) + angles_cube.convert_units('radians') + + # testing phase... + print(np.rad2deg(angles_cube.data)) + + import matplotlib.pyplot as plt + plt.switch_backend('tkagg') + ax = plt.axes(projection=ccrs.Orthographic(central_longitude=0.0, + central_latitude=90.0,)) + ax.coastlines() + ax.gridlines() + for i_bnd in range(4): + color = ['black', 'red', 'blue', 'magenta'][i_bnd] + plt.plot(testcube.coord('longitude').bounds[..., i_bnd], + testcube.coord('latitude').bounds[..., i_bnd], + '+', markersize=10., markeredgewidth=2., + markerfacecolor=color, markeredgecolor=color, + transform=ccrs.PlateCarree()) + + + # Show plain 0,1 + 1,0 vectors unrotated at the given points. + pts_shape = testcube.coord('longitude').shape + u0 = np.ones(pts_shape) + v0 = np.zeros(pts_shape) + u1 = v0.copy() + v1 = u0.copy() + +# u0_cube, u1_cube, v0_cube, v1_cube = [testcube.copy(data=aa) +# for aa in (u0, v0, u1, v1)] +# u0r_cube, v0r_cube = rotate_grid_vectors( +# u0_cube, v0_cube, grid_angles_cube=angles_cube) + + scale = 4.0e-6 + plt.quiver(testcube.coord('longitude').points, + testcube.coord('latitude').points, + u0, v0, color='blue', linewidth=0.5, scale_units='xy', scale=scale, + transform=ccrs.PlateCarree()) + plt.quiver(testcube.coord('longitude').points, + testcube.coord('latitude').points, + u1, v1, color='red', linewidth=0.5, scale_units='xy', scale=scale, + transform=ccrs.PlateCarree()) + +# plt.quiver(testcube.coord('longitude').points, +# testcube.coord('latitude').points, +# u0r_cube.data, v0r_cube.data, +# color='red', +# transform=ccrs.PlateCarree()) + + # Also draw small lines pointing at the correct angle. + x0s = testcube.coord('longitude').points + y0s = testcube.coord('latitude').points + ny, nx = x0s.shape + size_degrees = 5.0 + angles = angles_cube.copy() + angles.convert_units('radians') + angles = angles.data + lats = testcube.coord('latitude').copy() + lats.convert_units('radians') + lats = lats.points + dys = size_degrees * np.sin(angles) / np.cos(-lats) + dxs = size_degrees * np.cos(angles) + x1s = x0s + dxs + y1s = y0s + dys + for iy in range(ny): + for ix in range(nx): + plt.plot([x0s[iy, ix], x1s[iy, ix]], + [y0s[iy, ix], y1s[iy, ix]], + 'o-', markersize=4., markeredgewidth=0., + color='green', + transform=ccrs.PlateCarree()) + + + + ax.set_global() + plt.show() + + + self.assertArrayAllClose( + angles_cube.data, + [[100.0, 100.0, 100.0], + [100.0, 100.0, 100.0], + [100.0, 100.0, 100.0]]) + + +if __name__ == "__main__": + tests.main() From 393489eb6f827a4675fb9126bc2d066254b28371 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Wed, 25 Jul 2018 20:22:48 +0100 Subject: [PATCH 08/62] Roughly working, snapshotted with complex test plot code, to be reduced. --- lib/iris/analysis/_grid_angles.py | 136 ++++++-- lib/iris/analysis/cartography.py | 2 +- .../cartography/test_gridcell_angles.py | 323 +++++++++++++++--- 3 files changed, 375 insertions(+), 86 deletions(-) diff --git a/lib/iris/analysis/_grid_angles.py b/lib/iris/analysis/_grid_angles.py index 6f7dcf15db..f5fa94a256 100644 --- a/lib/iris/analysis/_grid_angles.py +++ b/lib/iris/analysis/_grid_angles.py @@ -13,26 +13,18 @@ def _3d_xyz_from_latlon(lon, lat): Returns: - x, y, z : (array, dtype=float64) - cartesian coordinates on a unit sphere. + xyz : (array, dtype=float64) + cartesian coordinates on a unit sphere. Dimension 0 maps x,y,z. """ lon1 = np.deg2rad(lon).astype(np.float64) lat1 = np.deg2rad(lat).astype(np.float64) - old_way = True - if old_way: - x3 = np.empty((3,) + lon.shape, dtype=float) - x3[0] = np.cos(lat1) * np.cos(lon1) - x3[1] = np.cos(lat1) * np.sin(lon1) - x3[2] = np.sin(lat1) - result = x3 - else: - x = np.cos(lat1) * np.cos(lon1) - y = np.cos(lat1) * np.sin(lon1) - z = np.sin(lat1) + x = np.cos(lat1) * np.cos(lon1) + y = np.cos(lat1) * np.sin(lon1) + z = np.sin(lat1) - result = np.concatenate([array[np.newaxis] for array in (x, y, z)]) + result = np.concatenate([array[np.newaxis] for array in (x, y, z)]) return result @@ -61,35 +53,82 @@ def _latlon_from_xyz(xyz): def _angle(p, q, r): """ - Return angle (in _radians_) of grid wrt local east. Anticlockwise +ve, as usual. - {P, Q, R} are consecutve points in the same row, eg {v(i,j),f(i,j),v(i+1,j)}, or {T(i-1,j),T(i,j),T(i+1,j)} - Calculate dot product of PR with lambda_hat at Q. This gives us cos(required angle). - Disciminate between +/- angles by comparing latitudes of P and R. - p, q, r, are all 2-element arrays [lon, lat] of angles in degrees. + Return angle (in _radians_) of grid wrt local east. + Anticlockwise +ve, as usual. + {P, Q, R} are consecutive points in the same row, + eg {v(i,j),f(i,j),v(i+1,j)}, or {T(i-1,j),T(i,j),T(i+1,j)} + Calculate dot product of PR with lambda_hat at Q. + This gives us cos(required angle). + Disciminate between +/- angles by comparing latitudes of P and R. + p, q, r, are all 2-element arrays [lon, lat] of angles in degrees. """ - q1 = np.deg2rad(q) +# old_style = True + old_style = False + if old_style: + mid_lons = np.deg2rad(q[0]) - pr = _3d_xyz_from_latlon(r[0], r[1]) - _3d_xyz_from_latlon(p[0], p[1]) - pr_norm = np.sqrt(np.sum(pr**2, axis=0)) - pr_top = pr[1] * np.cos(q1[0]) - pr[0] * np.sin(q1[0]) + pr = _3d_xyz_from_latlon(r[0], r[1]) - _3d_xyz_from_latlon(p[0], p[1]) + pr_norm = np.sqrt(np.sum(pr**2, axis=0)) + pr_top = pr[1] * np.cos(mid_lons) - pr[0] * np.sin(mid_lons) - index = np.where(pr_norm == 0) - pr_norm[index] = 1 + index = pr_norm == 0 + pr_norm[index] = 1 - cosine = np.maximum(np.minimum(pr_top / pr_norm, 1), -1) - cosine[index] = 0 + cosine = np.maximum(np.minimum(pr_top / pr_norm, 1), -1) + cosine[index] = 0 - psi = np.arccos(cosine) * np.sign(r[1] - p[1]) - psi[index] = np.nan + psi = np.arccos(cosine) * np.sign(r[1] - p[1]) + psi[index] = np.nan + else: + # Calculate unit vectors. + midpt_lons, midpt_lats = q[0], q[1] + lmb_r, phi_r = (np.deg2rad(arr) for arr in (midpt_lons, midpt_lats)) + phi_hatvec_x = -np.sin(phi_r) * np.cos(lmb_r) + phi_hatvec_y = -np.sin(phi_r) * np.sin(lmb_r) + phi_hatvec_z = np.cos(phi_r) + shape_xyz = (1,) + midpt_lons.shape + phi_hatvec = np.concatenate([arr.reshape(shape_xyz) + for arr in (phi_hatvec_x, + phi_hatvec_y, + phi_hatvec_z)]) + lmb_hatvec_z = np.zeros(midpt_lons.shape) + lmb_hatvec_y = np.cos(lmb_r) + lmb_hatvec_x = -np.sin(lmb_r) + lmb_hatvec = np.concatenate([arr.reshape(shape_xyz) + for arr in (lmb_hatvec_x, + lmb_hatvec_y, + lmb_hatvec_z)]) + + pr = _3d_xyz_from_latlon(r[0], r[1]) - _3d_xyz_from_latlon(p[0], p[1]) + + # Dot products to form true-northward / true-eastward projections. + pr_cmpt_e = np.sum(pr * lmb_hatvec, axis=0) + pr_cmpt_n = np.sum(pr * phi_hatvec, axis=0) + psi = np.arctan2(pr_cmpt_n, pr_cmpt_e) + + # TEMPORARY CHECKS: + # ensure that the two unit vectors are perpendicular. + dotprod = np.sum(phi_hatvec * lmb_hatvec, axis=0) + assert np.allclose(dotprod, 0.0) + # ensure that the vector components carry the original magnitude. + mag_orig = np.sum(pr * pr) + mag_rot = np.sum(pr_cmpt_e * pr_cmpt_e) + np.sum(pr_cmpt_n * pr_cmpt_n) + rtol = 1.e-3 + check = np.allclose(mag_rot, mag_orig, rtol=rtol) + if not check: + print (mag_rot, mag_orig) + assert np.allclose(mag_rot, mag_orig, rtol=rtol) return psi -def gridcell_angles(x, y=None): +def gridcell_angles(x, y=None, cell_angle_boundpoints='mid-lhs, mid-rhs'): """ Calculate gridcell orientation angles. + Args: + The inputs (x [,y]) can be any of the folliwing : * x (:class:`~iris.cube.Cube`): @@ -105,6 +144,16 @@ def gridcell_angles(x, y=None): longitude and latitude cell bounds, in degrees. The last index maps cell corners anticlockwise from bottom-left. + Optional Args: + + * cell_angle_boundpoints (string): + Controls which gridcell bounds locations are used to calculate angles, + if the inputs are bounds or bounded coordinates. + Valid values are 'lower-left, lower-right', which takes the angle from + the lower left to the lower right corner, and 'mid-lhs, mid-rhs' which + takes an angles between the average of the left-hand and right-hand + pairs of corners. The default is 'mid-lhs, mid-rhs'. + Returns: angles : (2-dimensional cube) @@ -184,8 +233,20 @@ def gridcell_angles(x, y=None): # Get lhs and rhs locations by averaging top+bottom each side. # NOTE: so with bounds, we *don't* need full circular longitudes. xyz = _3d_xyz_from_latlon(x, y) - lhs_xyz = 0.5 * (xyz[..., 0] + xyz[..., 3]) - rhs_xyz = 0.5 * (xyz[..., 1] + xyz[..., 2]) + angle_boundpoints_vals = {'mid-lhs, mid-rhs': '03_to_12', + 'lower-left, lower-right': '0_to_1'} + bounds_pos = angle_boundpoints_vals.get(cell_angle_boundpoints) + if bounds_pos == '0_to_1': + lhs_xyz = xyz[..., 0] + rhs_xyz = xyz[..., 1] + elif bounds_pos == '03_to_12': + lhs_xyz = 0.5 * (xyz[..., 0] + xyz[..., 3]) + rhs_xyz = 0.5 * (xyz[..., 1] + xyz[..., 2]) + else: + msg = ('unrecognised cell_angle_boundpoints of "{}", ' + 'must be one of {}') + raise ValueError(msg.format(cell_angle_boundpoints, + list(angle_boundpoints_vals.keys()))) if not x_coord: # Create bounded coords for result cube. # Use average lhs+rhs points in 3d to get 'mid' points, as coords @@ -215,7 +276,9 @@ def gridcell_angles(x, y=None): return result -def true_vectors_from_grid_vectors(u_cube, v_cube, grid_angles_cube=None): +def true_vectors_from_grid_vectors(u_cube, v_cube, + grid_angles_cube=None, + grid_angles_kwargs=None): """ Rotate distance vectors from grid-oriented to true-latlon-oriented. @@ -233,6 +296,10 @@ def true_vectors_from_grid_vectors(u_cube, v_cube, grid_angles_cube=None): If not provided, grid angles are estimated from 'u_cube' using the :func:`gridcell_angles` method. + * grid_angles_kwargs : (dict or None) + Additional keyword args to be passed to the :func:`gridcell_angles` + method, if it is used. + Returns: true_u, true_v : (cube) @@ -242,7 +309,8 @@ def true_vectors_from_grid_vectors(u_cube, v_cube, grid_angles_cube=None): """ u_out, v_out = (cube.copy() for cube in (u_cube, v_cube)) if not grid_angles_cube: - grid_angles_cube = gridcell_angles(u_cube) + grid_angles_kwargs = grid_angles_kwargs or {} + grid_angles_cube = gridcell_angles(u_cube, **grid_angles_kwargs) gridangles = grid_angles_cube.copy() gridangles.convert_units('radians') uu, vv, aa = (cube.data for cube in (u_out, v_out, gridangles)) diff --git a/lib/iris/analysis/cartography.py b/lib/iris/analysis/cartography.py index 1c55d48fd3..16c99c83a8 100644 --- a/lib/iris/analysis/cartography.py +++ b/lib/iris/analysis/cartography.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2010 - 2017, Met Office +# (C) British Crown Copyright 2010 - 2018, Met Office # # This file is part of Iris. # diff --git a/lib/iris/tests/unit/analysis/cartography/test_gridcell_angles.py b/lib/iris/tests/unit/analysis/cartography/test_gridcell_angles.py index b3a10a783f..a73988f668 100644 --- a/lib/iris/tests/unit/analysis/cartography/test_gridcell_angles.py +++ b/lib/iris/tests/unit/analysis/cartography/test_gridcell_angles.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2015 - 2016, Met Office +# (C) British Crown Copyright 2018, Met Office # # This file is part of Iris. # @@ -38,10 +38,13 @@ from iris.analysis.cartography import (gridcell_angles, rotate_grid_vectors) +import matplotlib.pyplot as plt +from orca_utils.plot_testing.blockplot_from_bounds import blockplot_2dll + def _rotated_grid_sample(pole_lat=15, pole_lon=-180, - lon_bounds=np.linspace(-30, 30, 8, endpoint=True), - lat_bounds=np.linspace(-30, 30, 8, endpoint=True)): + lon_bounds=np.linspace(-30, 30, 6, endpoint=True), + lat_bounds=np.linspace(-30, 30, 6, endpoint=True)): # Calculate *true* lat_bounds+lon_bounds for the rotated grid. lon_bounds = np.array(lon_bounds, dtype=float) lat_bounds = np.array(lat_bounds, dtype=float) @@ -79,19 +82,151 @@ def expand_unified_bds(bds): class TestGridcellAngles(tests.IrisTest): + def _singlecell_30deg_cube(self, x0=90., y0=0., dx=20., dy=10.): + x_pts = np.array([[x0]]) + y_pts = np.array([[y0]]) + x_bds = x0 + dx * np.array([[[-1., 1, 0.5, -1.5]]]) +# self.assertArrayAllClose(x_bds, np.array([[[70., 110, 100, 60]]])) + y_bds = y0 + dy * np.array([[[-1., 1, 3, 1]]]) +# self.assertArrayAllClose(y_bds, np.array([[[-10., 10, 30, 10]]])) + co_x = AuxCoord(points=x_pts, bounds=x_bds, + standard_name='longitude', units='degrees') + co_y = AuxCoord(points=y_pts, bounds=y_bds, + standard_name='latitude', units='degrees') + cube = Cube(np.zeros((1, 1))) + cube.add_aux_coord(co_x, (0, 1)) + cube.add_aux_coord(co_y, (0, 1)) + return cube + + def _singlecell_diamond_cube(self, x0=90., y0=0., dy=10., dx_eq=None): + if dx_eq is None: + dx_eq = dy + x_pts = np.array([[x0]]) + y_pts = np.array([[y0]]) + dx = dx_eq / np.cos(np.deg2rad(y0)) + x_bds = np.array([[[x0, x0 + dx, x0, x0 - dx]]]) + y_bds = np.array([[[y0 - dy, y0, y0 + dy, y0]]]) + co_x = AuxCoord(points=x_pts, bounds=x_bds, + standard_name='longitude', units='degrees') + co_y = AuxCoord(points=y_pts, bounds=y_bds, + standard_name='latitude', units='degrees') + cube = Cube(np.zeros((1, 1))) + cube.add_aux_coord(co_x, (0, 1)) + cube.add_aux_coord(co_y, (0, 1)) + return cube + + def test_single_cell_equatorial(self): + plt.switch_backend('tkagg') + plt.figure(figsize=(10,10)) + ax = plt.axes(projection=ccrs.Mercator()) +# ax = plt.axes(projection=ccrs.NorthPolarStereo()) +# ax = plt.axes(projection=ccrs.Orthographic(central_longitude=90., +# central_latitude=30.)) + + lon0 = 90.0 + dy = 2.0 + y_0, y_n, ny = -80, 80, 9 + angles = [] + for lat in np.linspace(y_0, y_n, ny): + cube = self._singlecell_diamond_cube(x0=lon0, y0=lat, dy=dy) + angles_cube = gridcell_angles(cube, +# cell_angle_boundpoints='mid-lhs, mid-rhs') + cell_angle_boundpoints='lower-left, lower-right') + tmp_cube = angles_cube.copy() + tmp_cube.convert_units('degrees') + print('') + print(lat) + co_x, co_y = (cube.coord(axis=ax) for ax in ('x', 'y')) + print() + print(' at : {}, {}'.format(co_x.points[0, 0], co_y.points[0, 0])) + print(' x-bds:') + print(co_x.bounds) + print(' y-bds:') + print(co_y.bounds) + angle = tmp_cube.data[0, 0] + angles.append(angle) + print(angle) + blockplot_2dll(cube) + + ax.coastlines() + ax.set_global() + + # Plot constant NEly (45deg) arrows. + xx = np.array([lon0] * ny) + yy = np.linspace(y_0, y_n, ny) - dy + uu = np.array([1.0] * ny) + plt.quiver(xx, yy, + uu, np.cos(np.deg2rad(yy)), + zorder=2, color='red', + scale_units='xy', + transform=ccrs.PlateCarree()) + + # Also plot returned angles. + angles_arr_rad = np.deg2rad(angles) + u_arr = uu * np.cos(angles_arr_rad) + v_arr = uu * np.sin(angles_arr_rad) * np.cos(np.deg2rad(yy)) + + plt.quiver(xx, yy, + u_arr, + v_arr, + zorder=2, color='magenta', + scale_units='xy', + width=0.005, + scale=0.2e-6, +# width=0.5, + transform=ccrs.PlateCarree()) + + plt.show() + + def test_values(self): # Construct a rotated-pole grid and check angle calculation. testcube = _rotated_grid_sample() - angles_cube = gridcell_angles(testcube) + + cell_angle_boundpoints = 'mid-lhs, mid-rhs' +# cell_angle_boundpoints = 'lower-left, lower-right' +# cell_angle_boundpoints = 'garble' + angles_cube = gridcell_angles( + testcube, + cell_angle_boundpoints=cell_angle_boundpoints) angles_cube.convert_units('radians') # testing phase... print(np.rad2deg(angles_cube.data)) - + import matplotlib.pyplot as plt plt.switch_backend('tkagg') - ax = plt.axes(projection=ccrs.Orthographic(central_longitude=0.0, - central_latitude=90.0,)) + +# plot_map = 'north_polar_stereographic' +# plot_map = 'plate_carree' +# plot_map = 'mercator' + plot_map = 'north_polar_orthographic' + if plot_map == 'plate_carree': + scale = 0.1 + map_proj = ccrs.PlateCarree() + elif plot_map == 'mercator': + scale = 3.0e-6 + map_proj = ccrs.Mercator() + map_proj._threshold *= 0.01 + elif plot_map == 'north_polar_orthographic': + scale = 3.0e-6 + map_proj = ccrs.Orthographic(central_longitude=0.0, + central_latitude=90.0,) + map_proj._threshold *= 0.01 + elif plot_map == 'north_polar_stereographic': + scale = 3.0e-6 + map_proj = ccrs.NorthPolarStereo() + else: + assert 0 + + ax = plt.axes(projection=map_proj) + data_proj = ccrs.PlateCarree() + + deg_scale = 10.0 + +# angles = 'uv' + angles = 'xy' + ax.coastlines() ax.gridlines() for i_bnd in range(4): @@ -100,71 +235,157 @@ def test_values(self): testcube.coord('latitude').bounds[..., i_bnd], '+', markersize=10., markeredgewidth=2., markerfacecolor=color, markeredgecolor=color, - transform=ccrs.PlateCarree()) + transform=data_proj) - # Show plain 0,1 + 1,0 vectors unrotated at the given points. + # Show plain 0,1 + 1,0 (PlateCarree) vectors unrotated at the given points. pts_shape = testcube.coord('longitude').shape + ny, nx = pts_shape u0 = np.ones(pts_shape) v0 = np.zeros(pts_shape) u1 = v0.copy() v1 = u0.copy() -# u0_cube, u1_cube, v0_cube, v1_cube = [testcube.copy(data=aa) -# for aa in (u0, v0, u1, v1)] -# u0r_cube, v0r_cube = rotate_grid_vectors( -# u0_cube, v0_cube, grid_angles_cube=angles_cube) + x0s = testcube.coord('longitude').points + y0s = testcube.coord('latitude').points + yscale = np.cos(np.deg2rad(y0s)) + plt.quiver(x0s, y0s, u0, v0 * yscale, + color='blue', width=0.005, + headwidth=2., # headlength=1.0, headaxislength=0.7, + angles=angles, + scale_units='xy', scale=scale, + transform=data_proj) + plt.quiver(x0s, y0s, u1, v1 * yscale, + color='red', width=0.005, + headwidth=2., # headlength=1.0, headaxislength=0.7, + angles=angles, + scale_units='xy', scale=scale, + transform=data_proj) - scale = 4.0e-6 - plt.quiver(testcube.coord('longitude').points, - testcube.coord('latitude').points, - u0, v0, color='blue', linewidth=0.5, scale_units='xy', scale=scale, - transform=ccrs.PlateCarree()) - plt.quiver(testcube.coord('longitude').points, - testcube.coord('latitude').points, - u1, v1, color='red', linewidth=0.5, scale_units='xy', scale=scale, - transform=ccrs.PlateCarree()) + # Add 45deg arrows (NEly), still on a PlateCarree map. + plt.quiver(x0s, y0s, v1, v1 * yscale, + color='green', width=0.005, + headwidth=2., # headlength=1.0, headaxislength=0.7, + angles=angles, + scale_units='xy', scale=scale, + transform=data_proj) -# plt.quiver(testcube.coord('longitude').points, -# testcube.coord('latitude').points, -# u0r_cube.data, v0r_cube.data, -# color='red', -# transform=ccrs.PlateCarree()) - # Also draw small lines pointing at the correct angle. - x0s = testcube.coord('longitude').points - y0s = testcube.coord('latitude').points - ny, nx = x0s.shape - size_degrees = 5.0 - angles = angles_cube.copy() - angles.convert_units('radians') - angles = angles.data - lats = testcube.coord('latitude').copy() - lats.convert_units('radians') - lats = lats.points - dys = size_degrees * np.sin(angles) / np.cos(-lats) - dxs = size_degrees * np.cos(angles) - x1s = x0s + dxs - y1s = y0s + dys + + # + # Repeat the above plotting short lines INSTEAD of quiver. + # + u0d = x0s + deg_scale * u0 + v0d = y0s + deg_scale * v0 + u1d = x0s + deg_scale * u1 + v1d = y0s + deg_scale * v1 + u2d = x0s + deg_scale * u0 + v2d = y0s + deg_scale * v1 for iy in range(ny): for ix in range(nx): - plt.plot([x0s[iy, ix], x1s[iy, ix]], - [y0s[iy, ix], y1s[iy, ix]], - 'o-', markersize=4., markeredgewidth=0., - color='green', - transform=ccrs.PlateCarree()) + plt.plot([x0s[iy, ix], u0d[iy, ix]], + [y0s[iy, ix], v0d[iy, ix]], + ':', color='blue', linewidth=0.5, + transform=data_proj) + plt.plot([x0s[iy, ix], u1d[iy, ix]], + [y0s[iy, ix], v1d[iy, ix]], + ':', color='red', linewidth=0.5, + transform=data_proj) + plt.plot([x0s[iy, ix], u2d[iy, ix]], + [y0s[iy, ix], v2d[iy, ix]], + ':', color='green', linewidth=0.5, + transform=data_proj) + + + # Overplot BL-BR and BL-TL lines from the cell bounds. + co_lon, co_lat = [testcube.coord(name).copy() + for name in ('longitude', 'latitude')] + for co in (co_lon, co_lat): + co.convert_units('degrees') + lon_bds, lat_bds = [co.bounds for co in (co_lon, co_lat)] +# ny, nx = lon_bds.shape[:-1] + for iy in range(ny): + for ix in range(nx): + x0, y0 = lon_bds[iy, ix, 0], lat_bds[iy, ix, 0] + x1, y1 = lon_bds[iy, ix, 1], lat_bds[iy, ix, 1] + x2, y2 = lon_bds[iy, ix, 3], lat_bds[iy, ix, 3] + plt.plot([x0, x1], [y0, y1], 'x-', + color='orange', + transform=data_proj) + plt.plot([x0, x2], [y0, y2], 'x-', + color='orange', linestyle='--', + transform=data_proj) + + # Plot U0, rotated by cell angles, also at cell bottom-lefts. + u0_cube, u1_cube, v0_cube, v1_cube = [testcube.copy(data=aa) + for aa in (u0, v0, u1, v1)] + u0r_cube, v0r_cube = rotate_grid_vectors( + u0_cube, v0_cube, grid_angles_cube=angles_cube) + u0r, v0r = [cube.data for cube in (u0r_cube, v0r_cube)] + + xbl, ybl = lon_bds[..., 0], lat_bds[..., 0] + # + # Replace quiver here with delta-based lineplot + # + urd = xbl + deg_scale * u0r + vrd = ybl + deg_scale * v0r * yscale + for iy in range(ny): + for ix in range(nx): + plt.plot([xbl[iy, ix], urd[iy, ix]], + [ybl[iy, ix], vrd[iy, ix]], + ':', color='magenta', linewidth=2.5, + transform=data_proj) + # Show this is the SAME as lineplot + plt.quiver(xbl, ybl, u0r, v0r * yscale, + color='magenta', width=0.01, + headwidth=1.2, # headlength=1.0, headaxislength=0.7, + angles=angles, + scale_units='xy', scale=scale, + transform=data_proj) + + plt.suptitle('angles from "{}"'.format(cell_angle_boundpoints)) + +# # Also draw small lines pointing at the correct (TRUE, not ) angle. +# ny, nx = x0s.shape +# size_degrees = 1.0 +# angles = angles_cube.copy() +# angles.convert_units('radians') +# angles = angles.data +# lats = testcube.coord('latitude').copy() +# lats.convert_units('radians') +# lats = lats.points +# dxs = size_degrees * u0.copy() #* np.cos(angles) +# dys = size_degrees * u0.copy() # / np.sqrt(np.cos(lats)) +# x1s = x0s + dxs +# y1s = y0s + dys +## for iy in range(ny): +## for ix in range(nx): +## plt.plot([x0s[iy, ix], x1s[iy, ix]], +## [y0s[iy, ix], y1s[iy, ix]], +## 'o-', markersize=4., markeredgewidth=0., +## color='green', # scale_units='xy', scale=scale, +## transform=data_proj) +# plt.quiver(x0s, y0s, dxs, dys, +# color='green', linewidth=0.2, +# angles=angles, +# scale_units='xy', scale=scale * 0.6, +# transform=data_proj) ax.set_global() - plt.show() +# plt.show() + angles_cube.convert_units('degrees') self.assertArrayAllClose( angles_cube.data, - [[100.0, 100.0, 100.0], - [100.0, 100.0, 100.0], - [100.0, 100.0, 100.0]]) + [[33.421, 17.928, 0., -17.928, -33.421], + [41.981, 24.069, 0., -24.069, -41.981], + [56.624, 37.809, 0., -37.809, -56.624], + [79.940, 74.227, 0., -74.227, -79.940], + [107.313, 126.361, -180., -126.361, -107.313]], + atol=0.002) if __name__ == "__main__": From d8bb331ca2774f34f538b9746e4cdc43e935dac8 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Thu, 26 Jul 2018 16:14:04 +0100 Subject: [PATCH 09/62] Small improvements. --- lib/iris/analysis/_grid_angles.py | 79 ++++++++++++++++--- .../cartography/test_gridcell_angles.py | 2 +- 2 files changed, 71 insertions(+), 10 deletions(-) diff --git a/lib/iris/analysis/_grid_angles.py b/lib/iris/analysis/_grid_angles.py index f5fa94a256..1746834a65 100644 --- a/lib/iris/analysis/_grid_angles.py +++ b/lib/iris/analysis/_grid_angles.py @@ -1,3 +1,27 @@ +# (C) British Crown Copyright 2010 - 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 . +""" +Code to implement vector rotation by angles, and inferring gridcell angles +from coordinate points and bounds. + +""" +from __future__ import (absolute_import, division, print_function) +from six.moves import (filter, input, map, range, zip) # noqa + import numpy as np import iris @@ -125,23 +149,41 @@ def _angle(p, q, r): def gridcell_angles(x, y=None, cell_angle_boundpoints='mid-lhs, mid-rhs'): """ - Calculate gridcell orientation angles. + Calculate gridcell orientations for an arbitrary 2-dimensional grid. + + The input grid is defined by two 2-dimensional coordinate arrays with the + same dimensions (ny, nx), specifying the geolocations of a 2D mesh. + + Input values may be coordinate points (ny, nx) or bounds (ny, nx, 4). + However, if points, the edges in the X direction are assumed to be + connected by wraparound. + + Input can be either two arrays, two coordinates, or a single cube + containing two suitable coordinates identified with the 'x' and'y' axes. Args: The inputs (x [,y]) can be any of the folliwing : * x (:class:`~iris.cube.Cube`): - a grid cube with 2D longitude and latitude coordinates. + a grid cube with 2D X and Y coordinates, identified by 'axis'. + The coordinates must be 2-dimensional with the same shape. + The two dimensions represent grid dimensions in the order Y, then X. * x, y (:class:`~iris.coords.Coord`): - longitude and latitude coordinates. + X and Y coordinates, specifying grid locations on the globe. + The coordinates must be 2-dimensional with the same shape. + The two dimensions represent grid dimensions in the order Y, then X. + If there is no coordinate system, they are assumed to be true + longitudes and latitudes. Units must convertible to 'degrees'. * x, y (2-dimensional arrays of same shape (ny, nx)): longitude and latitude cell center locations, in degrees. + The two dimensions represent grid dimensions in the order Y, then X. * x, y (3-dimensional arrays of same shape (ny, nx, 4)): longitude and latitude cell bounds, in degrees. + The first two dimensions are grid dimensions in the order Y, then X. The last index maps cell corners anticlockwise from bottom-left. Optional Args: @@ -167,9 +209,11 @@ def gridcell_angles(x, y=None, cell_angle_boundpoints='mid-lhs, mid-rhs'): the average of the 4 bounds. """ - if hasattr(x, 'core_data'): - # N.B. only "true" lats + longs will do : Cannot handle rotated ! - x, y = x.coord('longitude'), x.coord('latitude') + cube = None + if hasattr(x, 'add_aux_coord'): + # Passed a cube : extract 'x' and ;'y' axis coordinates. + cube = x # Save for later checking. + x, y = cube.coord(axis='x'), cube.coord(axis='y') # Now should have either 2 coords or 2 arrays. if not hasattr(x, 'shape') and hasattr(y, 'shape'): @@ -178,7 +222,7 @@ def gridcell_angles(x, y=None, cell_angle_boundpoints='mid-lhs, mid-rhs'): raise ValueError(msg.format(type(x), type(y))) x_coord, y_coord = None, None - if isinstance(x, iris.coords.Coord) and isinstance(y, iris.coords.Coord): + if hasattr(x, 'bounds') and hasattr(y, 'bounds'): x_coord, y_coord = x.copy(), y.copy() x_coord.convert_units('degrees') y_coord.convert_units('degrees') @@ -203,9 +247,10 @@ def gridcell_angles(x, y=None, cell_angle_boundpoints='mid-lhs, mid-rhs'): else: x, y = x_coord.points, y_coord.points - elif isinstance(x, iris.coords.Coord) or isinstance(y, iris.coords.Coord): + elif hasattr(x, 'bounds') or hasattr(y, 'bounds'): + # One was a Coord, and the other not ? is_and_not = ('x', 'y') - if isinstance(y, iris.coords.Coord): + if hasattr(y, 'bounds'): is_and_not = reversed(is_and_not) msg = 'Input {!r} is a Coordinate, but {!r} is not.' raise ValueError(*is_and_not) @@ -282,6 +327,18 @@ def true_vectors_from_grid_vectors(u_cube, v_cube, """ Rotate distance vectors from grid-oriented to true-latlon-oriented. + .. Note:: + + This operation overlaps somewhat in function with + :func:`iris.analysis.cartography.rotate_winds`. + However, that routine only rotates vectors according to transformations + between coordinate systems. + This function, by contrast, can rotate vectors by arbitrary angles. + Most commonly, the angles are estimated solely from grid sampling + points, using :func:`gridcell_angles` : This allows operation on + complex meshes defined by two-dimensional coordinates, such as most + ocean grids. + Args: * u_cube, v_cube : (cube) @@ -306,6 +363,10 @@ def true_vectors_from_grid_vectors(u_cube, v_cube, Cubes of true-north oriented vector components. Units are same as inputs. + .. Note:: + + Vector magnitudes will always be the same as the inputs. + """ u_out, v_out = (cube.copy() for cube in (u_cube, v_cube)) if not grid_angles_cube: diff --git a/lib/iris/tests/unit/analysis/cartography/test_gridcell_angles.py b/lib/iris/tests/unit/analysis/cartography/test_gridcell_angles.py index a73988f668..de00d2bc76 100644 --- a/lib/iris/tests/unit/analysis/cartography/test_gridcell_angles.py +++ b/lib/iris/tests/unit/analysis/cartography/test_gridcell_angles.py @@ -374,7 +374,7 @@ def test_values(self): ax.set_global() -# plt.show() + plt.show() angles_cube.convert_units('degrees') From c902ce3460279588e4d7cf0d9523372479f9d1d2 Mon Sep 17 00:00:00 2001 From: lbdreyer Date: Fri, 20 Jul 2018 10:19:20 +0100 Subject: [PATCH 10/62] Support plotting 2D bounded coords --- lib/iris/coords.py | 129 ++++++++++-- lib/iris/plot.py | 160 ++++++++++++--- lib/iris/tests/stock.py | 217 ++++++++++++--------- lib/iris/tests/unit/plot/test_2d_coords.py | 158 +++++++++++++++ 4 files changed, 529 insertions(+), 135 deletions(-) create mode 100644 lib/iris/tests/unit/plot/test_2d_coords.py diff --git a/lib/iris/coords.py b/lib/iris/coords.py index e4a8eb35f2..57ae723205 100644 --- a/lib/iris/coords.py +++ b/lib/iris/coords.py @@ -143,6 +143,85 @@ def __new__(cls, name_or_coord, minimum, maximum, 'groupby_point, groupby_slice') +def _discontinuity_in_2d_bounds(bds, abs_tol=1e-4): + """ + Check bounds of a 2-dimensional coordinate are contiguous + Args: + bds: Array of bounds of shape (X,Y,4) + abs_tol: tolerance + + Returns: + Bool, if there are no discontinuities + absolute difference along the x axis + absolute difference along the y axis + + """ + # Check form is (ny, nx, 4) + if not bds.ndim == 3 and bds.shape[2] == 4: + raise ValueError('2D coordinates must have 4 bounds per point ' + 'for 2D coordinate plotting') + + # Check ordering: + # i i+1 + # j @0 @1 + # j+1 @3 @2 + def mod360_diff(x1, x2): + diff = x1 - x2 + diff = (diff + 360.0 + 180.0) % 360.0 - 180.0 + return diff + + # Compare cell with the cell next to it (i+1) + diffs_along_x = mod360_diff(bds[:, :-1, 1], bds[:, 1:, 0]) + # Compare cell with the cell above it (j+1) + diffs_along_y = mod360_diff(bds[:-1, :, 3], bds[1:, :, 0]) + + def eq_diffs(x1): + return np.all(np.abs(x1) < abs_tol) + + match_y0_x1 = eq_diffs(diffs_along_x) + match_y1_x0 = eq_diffs(diffs_along_y) + + all_eq = match_y0_x1 and match_y1_x0 + + return all_eq, np.abs(diffs_along_x), np.abs(diffs_along_y) + + +def _get_2d_coord_bound_grid(bds): + """ + Function used that takes a bounds array for a 2-D coordinate variable with + 4 sides and returns the bounds grid. + + Cf standards requires the four vertices of the cell to be traversed + anti-clockwise if the coordinates are defined in a right handed coordinate + system. + + selects the zeroth vertex of each cell and then adds the column the first + vertex at the end. For the top row it uses the thirs vertex, with the + second added on to the end. + e.g. + # 0-0-0-0-1 + # 0-0-0-0-1 + # 3-3-3-3-2 + + + Args: + bounds: array of shape (X,Y,4) + + Returns: + array of shape (X+1, Y+1) + + """ + bds_shape = bds.shape + result = np.zeros((bds_shape[0] + 1, bds_shape[1] + 1)) + + result[:-1, :-1] = bds[:, :, 0] + result[:-1, -1] = bds[:, -1, 1] + result[-1, :-1] = bds[-1, :, 3] + result[-1, -1] = bds[-1, -1, 2] + + return result + + class Cell(collections.namedtuple('Cell', ['point', 'bound'])): """ An immutable representation of a single cell of a coordinate, including the @@ -951,15 +1030,22 @@ def cells(self): return _CellIterator(self) def _sanity_check_contiguous(self): - if self.ndim != 1: - raise iris.exceptions.CoordinateMultiDimError( - 'Invalid operation for {!r}. Contiguous bounds are not defined' - ' for multi-dimensional coordinates.'.format(self.name())) - if self.nbounds != 2: - raise ValueError( - 'Invalid operation for {!r}, with {} bounds. Contiguous bounds' - ' are only defined for coordinates with 2 bounds.'.format( - self.name(), self.nbounds)) + if self.ndim == 1: + if self.nbounds != 2: + raise ValueError('Invalid operation for {!r}, with {} bounds. ' + 'Contiguous bounds are only defined for 1D ' + 'coordinates with 2 bounds.'.format + (self.name(), self.nbounds)) + elif self.ndim == 2: + if self.nbounds != 4: + raise ValueError('Invalid operation for {!r}, with {} bounds. ' + 'Contiguous bounds are only defined for 2D ' + 'coordinates with 4 bounds.'.format + (self.name(), self.nbounds)) + else: + raise ValueError('Invalid operation for {!r}. Contiguous bounds ' + 'are not defined for coordinates with more than ' + '2 dimensions.'.format(self.name())) def is_contiguous(self, rtol=1e-05, atol=1e-08): """ @@ -979,19 +1065,29 @@ def is_contiguous(self, rtol=1e-05, atol=1e-08): """ if self.has_bounds(): self._sanity_check_contiguous() - return np.allclose(self.bounds[1:, 0], self.bounds[:-1, 1], - rtol=rtol, atol=atol) + if self.ndim == 1: + return np.allclose(self.bounds[1:, 0], self.bounds[:-1, 1], + rtol=rtol, atol=atol) + elif self.ndim == 2: + allclose, _, _ = _discontinuity_in_2d_bounds(self.bounds, + abs_tol=atol) + return allclose else: return False def contiguous_bounds(self): """ - Returns the N+1 bound values for a contiguous bounded coordinate + Returns the N+1 bound values for a contiguous bounded 1D coordinate of length N. + Returns the (N+1, M+1) bound values for a contiguous bounded 2D + coordinate of shape (N, M). + + Assumes input is contiguous. + .. note:: - If the coordinate is does not have bounds, this method will + If the coordinate does not have bounds, this method will return bounds positioned halfway between the coordinate's points. """ @@ -1003,8 +1099,11 @@ def contiguous_bounds(self): self._sanity_check_contiguous() bounds = self.bounds - c_bounds = np.resize(bounds[:, 0], bounds.shape[0] + 1) - c_bounds[-1] = bounds[-1, 1] + if self.ndim == 1: + c_bounds = np.resize(bounds[:, 0], bounds.shape[0] + 1) + c_bounds[-1] = bounds[-1, 1] + elif self.ndim == 2: + c_bounds = _get_2d_coord_bound_grid(bounds) return c_bounds def is_monotonic(self): diff --git a/lib/iris/plot.py b/lib/iris/plot.py index 64ce54c04c..5e00d5f713 100644 --- a/lib/iris/plot.py +++ b/lib/iris/plot.py @@ -31,16 +31,17 @@ import cartopy.crs as ccrs import cartopy.mpl.geoaxes from cartopy.geodesic import Geodesic +import cftime import matplotlib.axes import matplotlib.collections as mpl_collections import matplotlib.dates as mpl_dates import matplotlib.pyplot as plt from matplotlib.offsetbox import AnchoredText import matplotlib.ticker as mpl_ticker -import cftime import matplotlib.transforms as mpl_transforms import numpy as np import numpy.ma as ma +import warnings import iris.cube import iris.analysis.cartography as cartography @@ -51,7 +52,6 @@ import iris.palette from iris.util import _meshgrid - # Cynthia Brewer citation text. BREWER_CITE = 'Colours based on ColorBrewer.org' @@ -65,7 +65,7 @@ def names(coords): if isinstance(coord, int): result.append('dim={}'.format(coord)) else: - result.append(coord.name()) + result.append(cube.coord.name()) return ', '.join(result) def as_coord(coord): @@ -95,15 +95,17 @@ def get_span(coord): else: span = set(cube.coord_dims(coord)) return span + spans = list(map(get_span, coords)) for span, coord in zip(spans, coords): if not span: msg = 'The coordinate {!r} doesn\'t span a data dimension.' raise ValueError(msg.format(coord.name())) - if mode == iris.coords.BOUND_MODE and len(span) != 1: - raise ValueError('The coordinate {!r} is multi-dimensional and' - ' cannot be used in a cell-based plot.' - .format(coord.name())) + if mode == iris.coords.BOUND_MODE and len(span) not in [1, 2]: + raise ValueError('The coordinate {!r} has {} dimensions.' + 'Cell-based plotting is only supporting for' + 'coordinates with one or two dimensions.' + .format(coord.name()), len(span)) # Check the combination of coordinates spans enough (ndims) data # dimensions. @@ -125,11 +127,13 @@ def get_span(coord): # Note the use of `reversed` to convert from the X-then-Y # convention of the end-user API to the V-then-U convention used by # the plotting routines. + coords = names(coords) + plot_coords = list(reversed(coords)) return PlotDefn(plot_coords, transpose) -def _valid_bound_coord(coord): +def _valid_bound_dim_coord(coord): result = None if coord and coord.ndim == 1 and coord.nbounds: result = coord @@ -154,7 +158,7 @@ def _get_plot_defn(cube, mode, ndims=2): # When appropriate, restrict to 1D with bounds. if mode == iris.coords.BOUND_MODE: - coords = list(map(_valid_bound_coord, coords)) + coords = list(map(_valid_bound_dim_coord, coords)) def guess_axis(coord): axis = None @@ -172,6 +176,19 @@ def guess_axis(coord): aux_coords.sort(key=lambda coord: coord._as_defn()) coords[dim] = aux_coords[0] + # If plotting a 2 dimensional plot, check for 2d coordinates + if ndims == 2: + missing_dims = [dim for dim, coord in enumerate(coords) if + coord is None] + if missing_dims: + # Note that this only picks up coordinates that span the dims + two_dim_coords = cube.coords(dimensions=missing_dims) + two_dim_coords = [coord for coord in two_dim_coords + if coord.ndim == 2] + if len(two_dim_coords) >= 2: + two_dim_coords.sort(key=lambda coord: coord._as_defn()) + coords = two_dim_coords[:2] + if mode == iris.coords.POINT_MODE: # Allow multi-dimensional aux_coords to override the dim_coords # along the Z axis. This results in a preference for using the @@ -195,6 +212,7 @@ def sort_key(coord): return (order.get(axis, 0), coords.index(coord), coord and coord.name()) + sorted_coords = sorted(coords, key=sort_key) transpose = (sorted_coords != coords) @@ -256,6 +274,61 @@ def _invert_yaxis(v_coord, axes=None): axes.invert_yaxis() +def _check_contiguity_and_bounds(coord, data, abs_tol=1e-4, transpose=False): + """ + Check that the discontinuous bounds occur where the data is masked. + + If discontinuity occurs but data is masked, raise warning + If discontinuity occurs and data is NOT masked, raise error + + Args: + coords: + Array of the bounds of a 2D coord, of shape (X,Y,4) + data: + data of the the cube we are plotting + abs_tol: + tolerance when checking the contiguity + + """ + if transpose: + bounds = coord.bounds.T + else: + bounds = coord.bounds + + both_dirs_contiguous, diffs_along_x, diffs_along_y = \ + iris.coords._discontinuity_in_2d_bounds(bounds, abs_tol=abs_tol) + + if not both_dirs_contiguous: + + # True where data exists + mask_invert = np.logical_not(data.mask) + + # Where a discontinuity occurs the grid will not be created correctly. + # This does not matter if the data is masked as this is not plotted. + # So for places where data exists (opposite of the mask) AND a + # diff exists. If any exist, raise a warning + not_masked_at_discontinuity_along_x = np.any( + np.logical_and(mask_invert[:, :-1], diffs_along_x)) + + not_masked_at_discontinuity_along_y = np.any( + np.logical_and(mask_invert[:-1, ], diffs_along_y)) + + # If discontinuity occurs but not masked, any grid will be created + # incorrectly, so raise a warning + if not_masked_at_discontinuity_along_x or \ + not_masked_at_discontinuity_along_y: + 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 mesh to give to' + ' Matplotlib'.format(coord.name())) + else: + msg = 'The bounds of the {} coordinate are not contiguous. ' \ + 'However, data is masked where the discontiguity occurs ' \ + 'so plotting anyway.'.format(coord.name()) + warnings.warn(msg) + + def _draw_2d_from_bounds(draw_method_name, cube, *args, **kwargs): # NB. In the interests of clarity we use "u" and "v" to refer to the # horizontal and vertical axes on the matplotlib plot. @@ -263,10 +336,25 @@ def _draw_2d_from_bounds(draw_method_name, cube, *args, **kwargs): # Get & remove the coords entry from kwargs. coords = kwargs.pop('coords', None) if coords is not None: - plot_defn = _get_plot_defn_custom_coords_picked(cube, coords, mode) + plot_defn = _get_plot_defn_custom_coords_picked(cube, coords, mode, + ndims=2) else: plot_defn = _get_plot_defn(cube, mode, ndims=2) + two_dim_contig_atol = kwargs.pop('two_dim_coord_contiguity_atol', + 1e-4) + for coord in plot_defn.coords: + if coord.ndim == 2 and coord.has_bounds(): + try: + _check_contiguity_and_bounds(coord, data=cube.data, + abs_tol=two_dim_contig_atol) + except ValueError: + if _check_contiguity_and_bounds(coord, data=cube.data, + abs_tol=two_dim_contig_atol, + transpose=True)\ + is True: + plot_defn.transpose = True + if _can_draw_map(plot_defn.coords): result = _map_common(draw_method_name, None, iris.coords.BOUND_MODE, cube, plot_defn, *args, **kwargs) @@ -286,7 +374,7 @@ def _draw_2d_from_bounds(draw_method_name, cube, *args, **kwargs): string_axes = {} for coord, axis_name, data_dim in zip([u_coord, v_coord], - ['xaxis', 'yaxis'], + ['plotxaxis', 'yaxis'], [1, 0]): if coord is None: values = np.arange(data.shape[data_dim] + 1) @@ -309,7 +397,16 @@ def _draw_2d_from_bounds(draw_method_name, cube, *args, **kwargs): plot_arrays.append(values) u, v = plot_arrays - u, v = _broadcast_2d(u, v) + + # If the data is tranposed, 2D coordinates will also need to be + # tranposed. + if u.ndim == v.ndim == 2 and plot_defn.transpose is True: + u = u.T + v = v.T + + if u.ndim == v.ndim == 1: + u, v = _broadcast_2d(u, v) + axes = kwargs.pop('axes', None) draw_method = getattr(axes if axes else plt, draw_method_name) result = draw_method(u, v, data, *args, **kwargs) @@ -434,8 +531,8 @@ def _fixup_dates(coord, values): raise IrisError(msg) r = [nc_time_axis.CalendarDateTime( - cftime.datetime(*date), coord.units.calendar) - for date in dates] + cftime.datetime(*date), coord.units.calendar) + for date in dates] values = np.empty(len(r), dtype=object) values[:] = r return values @@ -675,8 +772,8 @@ def _ensure_cartopy_axes_and_determine_kwargs(x_coord, y_coord, kwargs): axes = kwargs.get('axes') if axes is None: if (isinstance(cs, iris.coord_systems.RotatedGeogCS) and - x_coord.points.max() > 180 and x_coord.points.max() < 360 and - x_coord.points.min() > 0): + x_coord.points.max() > 180 and x_coord.points.max() < 360 and + x_coord.points.min() > 0): # The RotatedGeogCS has 0 - 360 extent, different from the # assumptions made by Cartopy: rebase longitudes for the map axes # to set the datum longitude to the International Date Line. @@ -721,17 +818,22 @@ def _map_common(draw_method_name, arg_func, mode, cube, plot_defn, else: raise ValueError("Expected 1D or 2D XY coords") else: - try: - x, y = _meshgrid(x_coord.contiguous_bounds(), - y_coord.contiguous_bounds()) - # Exception translation. - except iris.exceptions.CoordinateMultiDimError: - raise ValueError("Could not get XY grid from bounds. " - "X or Y coordinate not 1D.") - except ValueError: - raise ValueError("Could not get XY grid from bounds. " - "X or Y coordinate doesn't have 2 bounds " - "per point.") + if not x_coord.ndim == y_coord.ndim == 2: + try: + x, y = _meshgrid(x_coord.contiguous_bounds(), + y_coord.contiguous_bounds()) + # Exception translation. + except iris.exceptions.CoordinateMultiDimError: + raise ValueError("Expected two 1D coords. Could not get XY" + " grid from bounds. X or Y coordinate not" + " 1D.") + except ValueError: + raise ValueError("Could not get XY grid from bounds. " + "X or Y coordinate doesn't have 2 bounds " + "per point.") + else: + x = x_coord.contiguous_bounds() + y = y_coord.contiguous_bounds() # Obtain the data array. data = cube.data @@ -1045,6 +1147,9 @@ def pcolormesh(cube, *args, **kwargs): * axes: the :class:`matplotlib.axes.Axes` to use for drawing. Defaults to the current axes if none provided. + * two_dim_coord_contiguity_atol: absolute tolerance when checking for + contiguity between cells in a two dimensional coordinate. + See :func:`matplotlib.pyplot.pcolormesh` for details of other valid keyword arguments. @@ -1073,6 +1178,7 @@ def points(cube, *args, **kwargs): keyword arguments. """ + def _scatter_args(u, v, data, *args, **kwargs): return ((u, v) + args, kwargs) diff --git a/lib/iris/tests/stock.py b/lib/iris/tests/stock.py index 25df8eda33..1b55510d2b 100644 --- a/lib/iris/tests/stock.py +++ b/lib/iris/tests/stock.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2010 - 2017, Met Office +# (C) British Crown Copyright 2010 - 2018, Met Office # # This file is part of Iris. # @@ -46,12 +46,12 @@ def lat_lon_cube(): """ cube = Cube(np.arange(12, dtype=np.int32).reshape((3, 4))) cs = GeogCS(6371229) - coord = iris.coords.DimCoord(points=np.array([-1, 0, 1], dtype=np.int32), + coord = DimCoord(points=np.array([-1, 0, 1], dtype=np.int32), standard_name='latitude', units='degrees', coord_system=cs) cube.add_dim_coord(coord, 0) - coord = iris.coords.DimCoord(points=np.array([-1, 0, 1, 2], dtype=np.int32), + coord = DimCoord(points=np.array([-1, 0, 1, 2], dtype=np.int32), standard_name='longitude', units='degrees', coord_system=cs) @@ -99,7 +99,7 @@ def simple_1d(with_bounds=True): cube.units = '1' points = np.arange(11, dtype=np.int32) + 1 bounds = np.column_stack([np.arange(11, dtype=np.int32), np.arange(11, dtype=np.int32) + 1]) - coord = iris.coords.DimCoord(points, long_name='foo', units='1', bounds=bounds) + coord = DimCoord(points, long_name='foo', units='1', bounds=bounds) cube.add_dim_coord(coord, 0) return cube @@ -124,12 +124,15 @@ def simple_2d(with_bounds=True): cube = Cube(np.arange(12, dtype=np.int32).reshape((3, 4))) cube.long_name = 'thingness' cube.units = '1' - y_points = np.array([ 2.5, 7.5, 12.5]) + y_points = np.array([2.5, 7.5, 12.5]) y_bounds = np.array([[0, 5], [5, 10], [10, 15]], dtype=np.int32) - y_coord = iris.coords.DimCoord(y_points, long_name='bar', units='1', bounds=y_bounds if with_bounds else None) + y_coord = DimCoord(y_points, long_name='bar', units='1', + bounds=y_bounds if with_bounds else None) x_points = np.array([ -7.5, 7.5, 22.5, 37.5]) - x_bounds = np.array([[-15, 0], [0, 15], [15, 30], [30, 45]], dtype=np.int32) - x_coord = iris.coords.DimCoord(x_points, long_name='foo', units='1', bounds=x_bounds if with_bounds else None) + x_bounds = np.array([[-15, 0], [0, 15], [15, 30], [30, 45]], + dtype=np.int32) + x_coord = DimCoord(x_points, long_name='foo', units='1', + bounds=x_bounds if with_bounds else None) cube.add_dim_coord(y_coord, 0) cube.add_dim_coord(x_coord, 1) @@ -191,9 +194,8 @@ def simple_3d_w_multidim_coords(with_bounds=True): [[5, 15], [15, 20], [20, 35], [35, 50]], [[10, 20], [20, 25], [25, 40], [40, 60]]], dtype=np.int32) - y_coord = iris.coords.AuxCoord(points=y_points, long_name='bar', - units='1', - bounds=y_bounds if with_bounds else None) + y_coord = AuxCoord(points=y_points, long_name='bar', units='1', + bounds=y_bounds if with_bounds else None) x_points = np.array([[-7.5, 7.5, 22.5, 37.5], [-12.5, 4., 26.5, 47.5], [2.5, 14., 36.5, 44.]]) @@ -201,12 +203,10 @@ def simple_3d_w_multidim_coords(with_bounds=True): [[-25, 0], [0, 8], [8, 45], [45, 50]], [[-5, 10], [10, 18], [18, 55], [18, 70]]], dtype=np.int32) - x_coord = iris.coords.AuxCoord(points=x_points, long_name='foo', - units='1', - bounds=x_bounds if with_bounds else None) - wibble_coord = iris.coords.DimCoord(np.array([10., 30.], - dtype=np.float32), - long_name='wibble', units='1') + x_coord = AuxCoord(points=x_points, long_name='foo', units='1', + bounds=x_bounds if with_bounds else None) + wibble_coord = DimCoord(np.array([10., 30.], dtype=np.float32), + long_name='wibble', units='1') cube.add_dim_coord(wibble_coord, [0]) cube.add_aux_coord(y_coord, [1, 2]) @@ -238,13 +238,13 @@ def simple_3d(): cube = Cube(np.arange(24, dtype=np.int32).reshape((2, 3, 4))) cube.long_name = 'thingness' cube.units = '1' - wibble_coord = iris.coords.DimCoord(np.array([10., 30.], + wibble_coord = DimCoord(np.array([10., 30.], dtype=np.float32), long_name='wibble', units='1') - lon = iris.coords.DimCoord([-180, -90, 0, 90], + lon = DimCoord([-180, -90, 0, 90], standard_name='longitude', units='degrees', circular=True) - lat = iris.coords.DimCoord([90, 0, -90], + lat = DimCoord([90, 0, -90], standard_name='latitude', units='degrees') cube.add_dim_coord(wibble_coord, [0]) cube.add_dim_coord(lat, [1]) @@ -294,39 +294,46 @@ def track_1d(duplicate_x=False): array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) """ - cube = Cube(np.arange(11, dtype=np.int32), standard_name='air_temperature', units='K') - bounds = np.column_stack([np.arange(11, dtype=np.int32), np.arange(11, dtype=np.int32) + 1]) + cube = Cube(np.arange(11, dtype=np.int32), standard_name='air_temperature', + units='K') + bounds = np.column_stack([np.arange(11, dtype=np.int32), + np.arange(11, dtype=np.int32) + 1]) pts = bounds[:, 1] - coord = iris.coords.AuxCoord(pts, 'projection_x_coordinate', units='1', bounds=bounds) + coord = AuxCoord(pts, 'projection_x_coordinate', units='1', bounds=bounds) cube.add_aux_coord(coord, [0]) if duplicate_x: - coord = iris.coords.AuxCoord(pts, 'projection_x_coordinate', units='1', bounds=bounds) + coord = AuxCoord(pts, 'projection_x_coordinate', units='1', + bounds=bounds) cube.add_aux_coord(coord, [0]) - coord = iris.coords.AuxCoord(pts * 2, 'projection_y_coordinate', units='1', bounds=bounds * 2) + coord = AuxCoord(pts * 2, 'projection_y_coordinate', units='1', + bounds=bounds * 2) cube.add_aux_coord(coord, 0) return cube def simple_2d_w_multidim_and_scalars(): data = np.arange(50, dtype=np.int32).reshape((5, 10)) - cube = iris.cube.Cube(data, long_name='test 2d dimensional cube', units='meters') + cube = iris.cube.Cube(data, long_name='test 2d dimensional cube', + units='meters') # DimCoords - dim1 = iris.coords.DimCoord(np.arange(5, dtype=np.float32) * 5.1 + 3.0, long_name='dim1', units='meters') - dim2 = iris.coords.DimCoord(np.arange(10, dtype=np.int32), long_name='dim2', units='meters', - bounds=np.arange(20, dtype=np.int32).reshape(10, 2)) + dim1 = DimCoord(np.arange(5, dtype=np.float32) * 5.1 + 3.0, + long_name='dim1', units='meters') + dim2 = DimCoord(np.arange(10, dtype=np.int32), + long_name='dim2', units='meters', + bounds=np.arange(20, dtype=np.int32).reshape(10, 2)) # Scalars - an_other = iris.coords.AuxCoord(3.0, long_name='an_other', units='meters') - yet_an_other = iris.coords.DimCoord(23.3, standard_name='air_temperature', - long_name='custom long name', - var_name='custom_var_name', - units='K') + an_other = AuxCoord(3.0, long_name='an_other', units='meters') + yet_an_other = DimCoord(23.3, standard_name='air_temperature', + long_name='custom long name', + var_name='custom_var_name', units='K') # Multidim - my_multi_dim_coord = iris.coords.AuxCoord(np.arange(50, dtype=np.int32).reshape(5, 10), - long_name='my_multi_dim_coord', units='1', - bounds=np.arange(200, dtype=np.int32).reshape(5, 10, 4)) + my_multi_dim_coord = AuxCoord(np.arange(50, dtype=np.int32).reshape(5, 10), + long_name='my_multi_dim_coord', units='1', + bounds=np.arange(200, dtype=np.int32). + reshape(5, 10, 4)) cube.add_dim_coord(dim1, 0) cube.add_dim_coord(dim2, 1) @@ -361,18 +368,21 @@ def hybrid_height(): """ data = np.arange(12, dtype='i8').reshape((3, 4)) - orography = icoords.AuxCoord([10, 25, 50, 5], standard_name='surface_altitude', units='m') - model_level = icoords.AuxCoord([2, 1, 0], standard_name='model_level_number') - level_height = icoords.DimCoord([100, 50, 10], long_name='level_height', - units='m', attributes={'positive': 'up'}, - bounds=[[150, 75], [75, 20], [20, 0]]) - sigma = icoords.AuxCoord([0.8, 0.9, 0.95], long_name='sigma', - bounds=[[0.7, 0.85], [0.85, 0.97], [0.97, 1.0]]) - hybrid_height = iris.aux_factory.HybridHeightFactory(level_height, sigma, orography) + orography = AuxCoord([10, 25, 50, 5], standard_name='surface_altitude', + units='m') + model_level = AuxCoord([2, 1, 0], standard_name='model_level_number') + level_height = DimCoord([100, 50, 10], long_name='level_height', + units='m', attributes={'positive': 'up'}, + bounds=[[150, 75], [75, 20], [20, 0]]) + sigma = AuxCoord([0.8, 0.9, 0.95], long_name='sigma', + bounds=[[0.7, 0.85], [0.85, 0.97], [0.97, 1.0]]) + hybrid_height = iris.aux_factory.HybridHeightFactory(level_height, + sigma, orography) cube = iris.cube.Cube(data, standard_name='air_temperature', units='K', dim_coords_and_dims=[(level_height, 0)], - aux_coords_and_dims=[(orography, 1), (model_level, 0), (sigma, 0)], + aux_coords_and_dims=[(orography, 1), + (model_level, 0), (sigma, 0)], aux_factories=[hybrid_height]) return cube @@ -381,25 +391,25 @@ def simple_4d_with_hybrid_height(): cube = iris.cube.Cube(np.arange(3*4*5*6, dtype='i8').reshape(3, 4, 5, 6), "air_temperature", units="K") - cube.add_dim_coord(iris.coords.DimCoord(np.arange(3, dtype='i8'), "time", - units="hours since epoch"), 0) - cube.add_dim_coord(iris.coords.DimCoord(np.arange(4, dtype='i8')+10, - "model_level_number", units="1"), 1) - cube.add_dim_coord(iris.coords.DimCoord(np.arange(5, dtype='i8')+20, - "grid_latitude", - units="degrees"), 2) - cube.add_dim_coord(iris.coords.DimCoord(np.arange(6, dtype='i8')+30, - "grid_longitude", - units="degrees"), 3) - - cube.add_aux_coord(iris.coords.AuxCoord(np.arange(4, dtype='i8')+40, - long_name="level_height", - units="m"), 1) - cube.add_aux_coord(iris.coords.AuxCoord(np.arange(4, dtype='i8')+50, - long_name="sigma", units="1"), 1) - cube.add_aux_coord(iris.coords.AuxCoord(np.arange(5*6, dtype='i8').reshape(5, 6)+100, - long_name="surface_altitude", - units="m"), [2, 3]) + cube.add_dim_coord(DimCoord(np.arange(3, dtype='i8'), "time", + units="hours since epoch"), 0) + cube.add_dim_coord(DimCoord(np.arange(4, dtype='i8')+10, + "model_level_number", units="1"), 1) + cube.add_dim_coord(DimCoord(np.arange(5, dtype='i8')+20, + "grid_latitude", + units="degrees"), 2) + cube.add_dim_coord(DimCoord(np.arange(6, dtype='i8')+30, + "grid_longitude", + units="degrees"), 3) + + cube.add_aux_coord(AuxCoord(np.arange(4, dtype='i8')+40, + long_name="level_height", + units="m"), 1) + cube.add_aux_coord(AuxCoord(np.arange(4, dtype='i8')+50, + long_name="sigma", units="1"), 1) + cube.add_aux_coord(AuxCoord(np.arange(5*6, dtype='i8').reshape(5, 6)+100, + long_name="surface_altitude", + units="m"), [2, 3]) cube.add_aux_factory(iris.aux_factory.HybridHeightFactory( delta=cube.coord("level_height"), @@ -449,7 +459,8 @@ def realistic_4d(): Returns a realistic 4d cube. >>> print(repr(realistic_4d())) - + """ # the stock arrays were created in Iris 0.8 with: @@ -477,7 +488,8 @@ def realistic_4d(): if not os.path.isfile(data_path): raise IOError('Test data is not available at {}.'.format(data_path)) r = np.load(data_path) - # sort the arrays based on the order they were originally given. The names given are of the form 'arr_1' or 'arr_10' + # sort the arrays based on the order they were originally given. + # The names given are of the form 'arr_1' or 'arr_10' _, arrays = zip(*sorted(six.iteritems(r), key=lambda item: int(item[0][4:]))) lat_pts, lat_bnds, lon_pts, lon_bnds, level_height_pts, \ @@ -486,25 +498,37 @@ def realistic_4d(): ll_cs = RotatedGeogCS(37.5, 177.5, ellipsoid=GeogCS(6371229.0)) - lat = icoords.DimCoord(lat_pts, standard_name='grid_latitude', units='degrees', - bounds=lat_bnds, coord_system=ll_cs) - lon = icoords.DimCoord(lon_pts, standard_name='grid_longitude', units='degrees', - bounds=lon_bnds, coord_system=ll_cs) + lat = icoords.DimCoord(lat_pts, standard_name='grid_latitude', + units='degrees', bounds=lat_bnds, + coord_system=ll_cs) + lon = icoords.DimCoord(lon_pts, standard_name='grid_longitude', + units='degrees', bounds=lon_bnds, + coord_system=ll_cs) level_height = icoords.DimCoord(level_height_pts, long_name='level_height', units='m', bounds=level_height_bnds, attributes={'positive': 'up'}) - model_level = icoords.DimCoord(model_level_pts, standard_name='model_level_number', + model_level = icoords.DimCoord(model_level_pts, + standard_name='model_level_number', units='1', attributes={'positive': 'up'}) - sigma = icoords.AuxCoord(sigma_pts, long_name='sigma', units='1', bounds=sigma_bnds) - orography = icoords.AuxCoord(orography, standard_name='surface_altitude', units='m') - time = icoords.DimCoord(time_pts, standard_name='time', units='hours since 1970-01-01 00:00:00') - forecast_period = icoords.DimCoord(forecast_period_pts, standard_name='forecast_period', units='hours') + sigma = icoords.AuxCoord(sigma_pts, long_name='sigma', units='1', + bounds=sigma_bnds) + orography = icoords.AuxCoord(orography, standard_name='surface_altitude', + units='m') + time = icoords.DimCoord(time_pts, standard_name='time', + units='hours since 1970-01-01 00:00:00') + forecast_period = icoords.DimCoord(forecast_period_pts, + standard_name='forecast_period', + units='hours') - hybrid_height = iris.aux_factory.HybridHeightFactory(level_height, sigma, orography) + hybrid_height = iris.aux_factory.HybridHeightFactory(level_height, + sigma, orography) - cube = iris.cube.Cube(data, standard_name='air_potential_temperature', units='K', - dim_coords_and_dims=[(time, 0), (model_level, 1), (lat, 2), (lon, 3)], - aux_coords_and_dims=[(orography, (2, 3)), (level_height, 1), (sigma, 1), + cube = iris.cube.Cube(data, standard_name='air_potential_temperature', + units='K', + dim_coords_and_dims=[(time, 0), (model_level, 1), + (lat, 2), (lon, 3)], + aux_coords_and_dims=[(orography, (2, 3)), + (level_height, 1), (sigma, 1), (forecast_period, None)], attributes={'source': 'Iris test case'}, aux_factories=[hybrid_height]) @@ -516,7 +540,8 @@ def realistic_4d_no_derived(): Returns a realistic 4d cube without hybrid height >>> print(repr(realistic_4d())) - + """ cube = realistic_4d() @@ -534,24 +559,30 @@ def realistic_4d_w_missing_data(): data_archive = np.load(data_path) data = ma.masked_array(data_archive['arr_0'], mask=data_archive['arr_1']) - # sort the arrays based on the order they were originally given. The names given are of the form 'arr_1' or 'arr_10' + # sort the arrays based on the order they were originally given. + # The names given are of the form 'arr_1' or 'arr_10' ll_cs = GeogCS(6371229) - lat = iris.coords.DimCoord(np.arange(20, dtype=np.float32), standard_name='grid_latitude', - units='degrees', coord_system=ll_cs) - lon = iris.coords.DimCoord(np.arange(20, dtype=np.float32), standard_name='grid_longitude', - units='degrees', coord_system=ll_cs) - time = iris.coords.DimCoord([1000., 1003., 1006.], standard_name='time', - units='hours since 1970-01-01 00:00:00') - forecast_period = iris.coords.DimCoord([0.0, 3.0, 6.0], standard_name='forecast_period', units='hours') - pressure = iris.coords.DimCoord(np.array([ 800., 900., 1000.], dtype=np.float32), - long_name='pressure', units='hPa') + lat = DimCoord(np.arange(20, dtype=np.float32), + standard_name='grid_latitude', + units='degrees', coord_system=ll_cs) + lon = DimCoord(np.arange(20, dtype=np.float32), + standard_name='grid_longitude', + units='degrees', coord_system=ll_cs) + time = DimCoord([1000., 1003., 1006.], standard_name='time', + units='hours since 1970-01-01 00:00:00') + forecast_period = DimCoord([0.0, 3.0, 6.0], + standard_name='forecast_period', + units='hours') + pressure = DimCoord(np.array([800., 900., 1000.], dtype=np.float32), + long_name='pressure', units='hPa') cube = iris.cube.Cube(data, long_name='missing data test data', units='K', - dim_coords_and_dims=[(time, 0), (pressure, 1), (lat, 2), (lon, 3)], + dim_coords_and_dims=[(time, 0), (pressure, 1), + (lat, 2), (lon, 3)], aux_coords_and_dims=[(forecast_period, 0)], - attributes={'source':'Iris test case'}) + attributes={'source': 'Iris test case'}) return cube diff --git a/lib/iris/tests/unit/plot/test_2d_coords.py b/lib/iris/tests/unit/plot/test_2d_coords.py new file mode 100644 index 0000000000..3b8ca91652 --- /dev/null +++ b/lib/iris/tests/unit/plot/test_2d_coords.py @@ -0,0 +1,158 @@ +# (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 . +"""Unit tests for handling and plotting of 2-dimensional coordinates""" + +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 iris.coords as coords + +import numpy.ma as ma + +from iris.tests.stock import simple_2d_w_multidim_coords as cube_2dcoords +from iris.tests.stock import simple_3d_w_multidim_coords as cube3d_2dcoords +from iris.tests.stock import lat_lon_cube + +from orca_utils.plot_testing import testdata_2d_coords + +if tests.MPL_AVAILABLE: + import iris.plot as iplt + + +class Test_2d_coords_plot_defn_bound_mode(tests.IrisTest): + def setUp(self): + self.multidim_cube = cube_2dcoords() + self.overspan_cube = cube3d_2dcoords() + + # latlon_2d is a cube with 2d coords, 4 bounds per point, + # discontiguities in the bounds but masked data at the discontiguities. + self.latlon_2d = testdata_2d_coords.full2d_global() + testdata_2d_coords.make_bounds_discontiguous_at_point\ + (self.latlon_2d, 2, 2) + + # Take a latlon cube with 1D coords, broadcast the coords into 2D + # ones, then add ONE of them back into the cube in place of original: + single_dims = lat_lon_cube() + lon = single_dims.coord('longitude') + lat = single_dims.coord('latitude') + big_lon, big_lat = testdata_2d_coords.grid_coords_2d_from_1d(lon, lat) + mixed_dims = single_dims.copy() + mixed_dims.remove_coord(lon) + # TODO Fix this coord addition: + # When adding an aux_coord, the function '_check_multidim_metadata' + # throws an error as it requires coord.shape to be (1, ) instead of + # (3, 4) or whatever. + mixed_dims.add_aux_coord(big_lon) + + # mixed_dims is now a cube with 2 1D dim coords and an additional + # 2D aux coord. + self.mixed_dims = mixed_dims + + self.mode = coords.BOUND_MODE + + def test_2d_coords_identified(self): + # Test that 2d coords are identified in the plot definition without + # having to be specified in the args without any errors. + cube = self.multidim_cube + defn = iplt._get_plot_defn(cube, mode=self.mode) + self.assertEqual([coord.name() for coord in defn.coords], + ['bar', 'foo']) + + def test_2d_coords_custom_picked(self): + # Test that 2d coords which are specified in the args will be + # accepted without any errors. + cube = self.multidim_cube + defn = iplt._get_plot_defn_custom_coords_picked(cube, ('foo', 'bar'), + self.mode) + self.assertEqual([coord.name() for coord in defn.coords], + ['bar', 'foo']) + + def test_2d_coords_as_integers(self): + # Test that if you pass in 2d coords as args in the form of integers, + # they will still be correctly identified without any errors. + cube = self.multidim_cube + defn = iplt._get_plot_defn_custom_coords_picked(cube, (0, 1), + self.mode) + self.assertEqual([coord for coord in defn.coords], + [1, 0]) + + def test_total_span_check(self): + # Test that an error is raised if a user tries to plot a 2d coord + # against a different coord, making total number of dimensions 3. + cube = self.overspan_cube + with self.assertRaises(ValueError): + iplt._get_plot_defn_custom_coords_picked(cube, ('wibble', 'foo'), + self.mode) + + def test_2dcoord_with_1dcoord(self): + # TODO Generate a cube with one 2d coord and one 1d coord + # TODO Try and plot them against each other + # TODO Find out where I can put a catch for this (if necessary) + cube = self.mixed_dims + with self.assertRaises(ValueError): + iplt._get_plot_defn_custom_coords_picked(cube, + ('latitude', 'longitude'), + self.mode) + + def test_map_common_not_enough_bounds(self): + # Test that a lat-lon cube with 2d coords and 2 bounds per point + # throws an error in contiguity checks. + cube = self.multidim_cube + cube.coord('foo').rename('longitude') + cube.coord('bar').rename('latitude') + with self.assertRaises(ValueError): + plot_defn = iplt._get_plot_defn(cube, self.mode) + iplt._map_common('pcolor', None, self.mode, cube, plot_defn) + + def test_map_common_2d(self): + # Test that a cube with 2d coords can be plotted as a map. + cube = self.latlon_2d + # Get necessary variables from _get_plot_defn to check that the test + # case will be accepted by _map_common. + plot_defn = iplt._get_plot_defn(cube, self.mode) + result = iplt._map_common('pcolor', None, self.mode, cube, plot_defn) + self.assertTrue(result) + + def test_discontiguous_masked(self): + # Test that a contiguity check will raise a warning (not an error) for + # discontiguous bounds but appropriately masked data. + cube = self.latlon_2d + coord = cube.coord('longitude') + msg = 'The bounds of the longitude coordinate are not contiguous. ' \ + 'However, data is masked where the discontiguity occurs so ' \ + 'plotting anyway.' + with self.assertWarnsRegexp(msg): + iplt._check_contiguity_and_bounds(coord, cube.data) + + def test_discontiguous_unmasked(self): + # Check that an error occurs when the contiguity check finds + # discontiguous bounds but unmasked data. + cube = self.latlon_2d + cube.data.mask = ma.nomask + coord = cube.coord('longitude') + with self.assertRaises(ValueError): + iplt._check_contiguity_and_bounds(coord, cube.data) + + def test_draw_2d_from_bounds(self): + # Test this function will not raise an error even with our most + # awkward but supported cube. + cube = self.latlon_2d + result = iplt._draw_2d_from_bounds('pcolormesh', cube) + self.assertTrue(result) From 723d45b1fe40afabcae54ff1d2868805e77da58a Mon Sep 17 00:00:00 2001 From: Corinne Bosley Date: Tue, 31 Jul 2018 09:44:38 +0100 Subject: [PATCH 11/62] some words for docs, WIP probably --- docs/iris/src/whatsnew/2.2.rst | 26 ++++++++++++++++++++++++++ lib/iris/plot.py | 15 +++++++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 docs/iris/src/whatsnew/2.2.rst diff --git a/docs/iris/src/whatsnew/2.2.rst b/docs/iris/src/whatsnew/2.2.rst new file mode 100644 index 0000000000..4165ebf43d --- /dev/null +++ b/docs/iris/src/whatsnew/2.2.rst @@ -0,0 +1,26 @@ +What's New in Iris 2.2.0 +************************ + +:Release: 2.2.0a0 +:Date: + + +This document explains the new/changed features of Iris in the alpha release + of version 2.2.0 +(:doc:`View all changes `). + + +Iris 2.2.0 Features +=================== +.. _showcase: + +.. admonition:: 2-Dimensional Coordinate Plotting + + The iris plot functions :func:`~iris.plot.pcolor` and + :func:`~iris.plot.pcolormesh` now accommodate the plotting of 2-dimensional + coordinates as well as 1-dimensional coordinates. + + To enable this feature, each coordinate passed in for plotting will be + automatically checked for contiguity. Coordinate bounds must either be + contiguous, or the cube's data must be masked at the discontiguities in + order to avoid plotting errors. diff --git a/lib/iris/plot.py b/lib/iris/plot.py index 5e00d5f713..a464f5393b 100644 --- a/lib/iris/plot.py +++ b/lib/iris/plot.py @@ -1107,7 +1107,10 @@ def outline(cube, coords=None, color='k', linewidth=None, axes=None): def pcolor(cube, *args, **kwargs): """ - Draws a pseudocolor plot based on the given Cube. + Draws a pseudocolor plot based on the given 2-dimensional Cube. + + The cube must have either two 1-dimensional coordinates or two + 2-dimensional coordinates with contiguous bounds to plot against each other. Kwargs: @@ -1121,6 +1124,11 @@ def pcolor(cube, *args, **kwargs): * axes: the :class:`matplotlib.axes.Axes` to use for drawing. Defaults to the current axes if none provided. + * two_dim_coord_contiguity_atol: absolute tolerance when checking for + contiguity between cells in a two dimensional coordinate. + + + See :func:`matplotlib.pyplot.pcolor` for details of other valid keyword arguments. @@ -1133,7 +1141,10 @@ def pcolor(cube, *args, **kwargs): def pcolormesh(cube, *args, **kwargs): """ - Draws a pseudocolor plot based on the given Cube. + Draws a pseudocolor plot based on the given 2-dimensional Cube. + + The cube must have either two 1-dimensional coordinates or two + 2-dimensional coordinates with contiguous bounds to plot against each other. Kwargs: From 0b8baf55437f2dd7882cddc1c96e93e415ac1a23 Mon Sep 17 00:00:00 2001 From: lbdreyer Date: Mon, 30 Jul 2018 18:54:27 +0100 Subject: [PATCH 12/62] Handle custom coords correctly --- lib/iris/coords.py | 15 ++++++++------- lib/iris/plot.py | 33 +++++++++++++-------------------- 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/lib/iris/coords.py b/lib/iris/coords.py index 57ae723205..8f1e48fd18 100644 --- a/lib/iris/coords.py +++ b/lib/iris/coords.py @@ -143,7 +143,7 @@ def __new__(cls, name_or_coord, minimum, maximum, 'groupby_point, groupby_slice') -def _discontinuity_in_2d_bounds(bds, abs_tol=1e-4): +def _discontiguity_in_2d_bounds(bds, abs_tol=1e-4): """ Check bounds of a 2-dimensional coordinate are contiguous Args: @@ -156,7 +156,7 @@ def _discontinuity_in_2d_bounds(bds, abs_tol=1e-4): absolute difference along the y axis """ - # Check form is (ny, nx, 4) + # Check bds has the shape (ny, nx, 4) if not bds.ndim == 3 and bds.shape[2] == 4: raise ValueError('2D coordinates must have 4 bounds per point ' 'for 2D coordinate plotting') @@ -1066,14 +1066,15 @@ def is_contiguous(self, rtol=1e-05, atol=1e-08): if self.has_bounds(): self._sanity_check_contiguous() if self.ndim == 1: - return np.allclose(self.bounds[1:, 0], self.bounds[:-1, 1], - rtol=rtol, atol=atol) + contiguous = np.allclose(self.bounds[1:, 0], + self.bounds[:-1, 1], + rtol=rtol, atol=atol) elif self.ndim == 2: - allclose, _, _ = _discontinuity_in_2d_bounds(self.bounds, + contiguous, _, _ = _discontiguity_in_2d_bounds(self.bounds, abs_tol=atol) - return allclose else: - return False + contiguous = False + return contiguous def contiguous_bounds(self): """ diff --git a/lib/iris/plot.py b/lib/iris/plot.py index a464f5393b..6ede9e1735 100644 --- a/lib/iris/plot.py +++ b/lib/iris/plot.py @@ -41,7 +41,6 @@ import matplotlib.transforms as mpl_transforms import numpy as np import numpy.ma as ma -import warnings import iris.cube import iris.analysis.cartography as cartography @@ -65,7 +64,7 @@ def names(coords): if isinstance(coord, int): result.append('dim={}'.format(coord)) else: - result.append(cube.coord.name()) + result.append(coord.name()) return ', '.join(result) def as_coord(coord): @@ -127,8 +126,6 @@ def get_span(coord): # Note the use of `reversed` to convert from the X-then-Y # convention of the end-user API to the V-then-U convention used by # the plotting routines. - coords = names(coords) - plot_coords = list(reversed(coords)) return PlotDefn(plot_coords, transpose) @@ -296,7 +293,7 @@ def _check_contiguity_and_bounds(coord, data, abs_tol=1e-4, transpose=False): bounds = coord.bounds both_dirs_contiguous, diffs_along_x, diffs_along_y = \ - iris.coords._discontinuity_in_2d_bounds(bounds, abs_tol=abs_tol) + iris.coords._discontiguity_in_2d_bounds(bounds, abs_tol=abs_tol) if not both_dirs_contiguous: @@ -322,11 +319,6 @@ def _check_contiguity_and_bounds(coord, data, abs_tol=1e-4, transpose=False): ' discontiguity occurs. Not able to create a' ' suitable mesh to give to' ' Matplotlib'.format(coord.name())) - else: - msg = 'The bounds of the {} coordinate are not contiguous. ' \ - 'However, data is masked where the discontiguity occurs ' \ - 'so plotting anyway.'.format(coord.name()) - warnings.warn(msg) def _draw_2d_from_bounds(draw_method_name, cube, *args, **kwargs): @@ -344,16 +336,17 @@ def _draw_2d_from_bounds(draw_method_name, cube, *args, **kwargs): two_dim_contig_atol = kwargs.pop('two_dim_coord_contiguity_atol', 1e-4) for coord in plot_defn.coords: - if coord.ndim == 2 and coord.has_bounds(): - try: - _check_contiguity_and_bounds(coord, data=cube.data, - abs_tol=two_dim_contig_atol) - except ValueError: - if _check_contiguity_and_bounds(coord, data=cube.data, - abs_tol=two_dim_contig_atol, - transpose=True)\ + if hasattr(coord, 'has_bounds'): + if coord.ndim == 2 and coord.has_bounds(): + try: + _check_contiguity_and_bounds(coord, data=cube.data, + abs_tol=two_dim_contig_atol) + except ValueError: + if _check_contiguity_and_bounds( + coord, data=cube.data, + abs_tol=two_dim_contig_atol, transpose=True) \ is True: - plot_defn.transpose = True + plot_defn.transpose = True if _can_draw_map(plot_defn.coords): result = _map_common(draw_method_name, None, iris.coords.BOUND_MODE, @@ -374,7 +367,7 @@ def _draw_2d_from_bounds(draw_method_name, cube, *args, **kwargs): string_axes = {} for coord, axis_name, data_dim in zip([u_coord, v_coord], - ['plotxaxis', 'yaxis'], + ['xaxis', 'yaxis'], [1, 0]): if coord is None: values = np.arange(data.shape[data_dim] + 1) From 759e357134d980c314a5da45e53d861cfbb6f2bc Mon Sep 17 00:00:00 2001 From: Corinne Bosley Date: Tue, 31 Jul 2018 11:15:12 +0100 Subject: [PATCH 13/62] suggested tweaks and corrections --- lib/iris/plot.py | 21 ++++++++++----------- lib/iris/tests/unit/plot/test_2d_coords.py | 9 ++++----- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/lib/iris/plot.py b/lib/iris/plot.py index 6ede9e1735..d81354fe4c 100644 --- a/lib/iris/plot.py +++ b/lib/iris/plot.py @@ -288,9 +288,7 @@ def _check_contiguity_and_bounds(coord, data, abs_tol=1e-4, transpose=False): """ if transpose: - bounds = coord.bounds.T - else: - bounds = coord.bounds + data = data.T both_dirs_contiguous, diffs_along_x, diffs_along_y = \ iris.coords._discontiguity_in_2d_bounds(bounds, abs_tol=abs_tol) @@ -333,19 +331,18 @@ def _draw_2d_from_bounds(draw_method_name, cube, *args, **kwargs): else: plot_defn = _get_plot_defn(cube, mode, ndims=2) - two_dim_contig_atol = kwargs.pop('two_dim_coord_contiguity_atol', + twodim_contig_atol = kwargs.pop('two_dim_coord_contiguity_atol', 1e-4) for coord in plot_defn.coords: if hasattr(coord, 'has_bounds'): if coord.ndim == 2 and coord.has_bounds(): try: _check_contiguity_and_bounds(coord, data=cube.data, - abs_tol=two_dim_contig_atol) + abs_tol=twodim_contig_atol) except ValueError: - if _check_contiguity_and_bounds( - coord, data=cube.data, - abs_tol=two_dim_contig_atol, transpose=True) \ - is True: + if _check_contiguity_and_bounds(coord, data=cube.data, + abs_tol=twodim_contig_atol, + transpose=True) is True: plot_defn.transpose = True if _can_draw_map(plot_defn.coords): @@ -1103,7 +1100,8 @@ def pcolor(cube, *args, **kwargs): Draws a pseudocolor plot based on the given 2-dimensional Cube. The cube must have either two 1-dimensional coordinates or two - 2-dimensional coordinates with contiguous bounds to plot against each other. + 2-dimensional coordinates with contiguous bounds to plot against each + other. Kwargs: @@ -1137,7 +1135,8 @@ def pcolormesh(cube, *args, **kwargs): Draws a pseudocolor plot based on the given 2-dimensional Cube. The cube must have either two 1-dimensional coordinates or two - 2-dimensional coordinates with contiguous bounds to plot against each other. + 2-dimensional coordinates with contiguous bounds to plot against each + other. Kwargs: diff --git a/lib/iris/tests/unit/plot/test_2d_coords.py b/lib/iris/tests/unit/plot/test_2d_coords.py index 3b8ca91652..ce96157bf4 100644 --- a/lib/iris/tests/unit/plot/test_2d_coords.py +++ b/lib/iris/tests/unit/plot/test_2d_coords.py @@ -30,7 +30,7 @@ from iris.tests.stock import simple_3d_w_multidim_coords as cube3d_2dcoords from iris.tests.stock import lat_lon_cube -from orca_utils.plot_testing import testdata_2d_coords +from orca_utils.plot_testing import testdata_2d_coords as testdata if tests.MPL_AVAILABLE: import iris.plot as iplt @@ -43,16 +43,15 @@ def setUp(self): # latlon_2d is a cube with 2d coords, 4 bounds per point, # discontiguities in the bounds but masked data at the discontiguities. - self.latlon_2d = testdata_2d_coords.full2d_global() - testdata_2d_coords.make_bounds_discontiguous_at_point\ - (self.latlon_2d, 2, 2) + self.latlon_2d = testdata.full2d_global() + testdata.make_bounds_discontiguous_at_point(self.latlon_2d, 2, 2) # Take a latlon cube with 1D coords, broadcast the coords into 2D # ones, then add ONE of them back into the cube in place of original: single_dims = lat_lon_cube() lon = single_dims.coord('longitude') lat = single_dims.coord('latitude') - big_lon, big_lat = testdata_2d_coords.grid_coords_2d_from_1d(lon, lat) + big_lon, big_lat = testdata.grid_coords_2d_from_1d(lon, lat) mixed_dims = single_dims.copy() mixed_dims.remove_coord(lon) # TODO Fix this coord addition: From ade18ad70cbd9fba6fd21802136e5456b143f49d Mon Sep 17 00:00:00 2001 From: lbdreyer Date: Tue, 31 Jul 2018 11:24:30 +0100 Subject: [PATCH 14/62] Update docs to included 2.2 versions (#3110) --- docs/iris/src/_templates/index.html | 2 +- docs/iris/src/whatsnew/index.rst | 1 + lib/iris/__init__.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/iris/src/_templates/index.html b/docs/iris/src/_templates/index.html index 0c4b5d958f..31acded447 100644 --- a/docs/iris/src/_templates/index.html +++ b/docs/iris/src/_templates/index.html @@ -134,7 +134,7 @@ extra information on specific technical issues

  • -
  • diff --git a/docs/iris/src/whatsnew/index.rst b/docs/iris/src/whatsnew/index.rst index 104c5074ca..c3a34303f0 100644 --- a/docs/iris/src/whatsnew/index.rst +++ b/docs/iris/src/whatsnew/index.rst @@ -9,6 +9,7 @@ Iris versions. .. toctree:: :maxdepth: 2 + 2.2.rst 2.1.rst 2.0.rst 1.13.rst diff --git a/lib/iris/__init__.py b/lib/iris/__init__.py index 2e7eba5892..882b52aaea 100644 --- a/lib/iris/__init__.py +++ b/lib/iris/__init__.py @@ -121,7 +121,7 @@ def callback(cube, field, filename): # Iris revision. -__version__ = '2.2.0dev0' +__version__ = '2.2.0a0' # Restrict the names imported when using "from iris import *" __all__ = ['load', 'load_cube', 'load_cubes', 'load_raw', From 91d69fa660d7fd1b340e5a2dbdad7ce69082dc97 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Tue, 31 Jul 2018 11:36:09 +0100 Subject: [PATCH 15/62] Provide a test skipper for 2d coords WIP. (#3099) --- lib/iris/tests/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/iris/tests/__init__.py b/lib/iris/tests/__init__.py index 8c64dc15b1..710a3fbac6 100644 --- a/lib/iris/tests/__init__.py +++ b/lib/iris/tests/__init__.py @@ -1196,6 +1196,12 @@ class MyPlotTests(test.GraphicsTest): 'Test(s) require "python-stratify", which is not available.') +SKIP_2D_TESTS = True +skip_2d = unittest.skipIf( + SKIP_2D_TESTS, + 'Test(s) broken by WIP on 2d coords support -- temporarily disabled.') + + def no_warnings(func): """ Provides a decorator to ensure that there are no warnings raised From b6b305f5a17a5a8d658e71835b0a91aba2bdf36f Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Mon, 30 Jul 2018 14:43:17 +0100 Subject: [PATCH 16/62] First working quiver+streamplot. --- lib/iris/plot.py | 133 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/lib/iris/plot.py b/lib/iris/plot.py index d81354fe4c..ac8da822cc 100644 --- a/lib/iris/plot.py +++ b/lib/iris/plot.py @@ -1189,6 +1189,139 @@ def _scatter_args(u, v, data, *args, **kwargs): *args, **kwargs) +def _vector_component_args(x_points, y_points, u_data, *args, **kwargs): + """ + Callback from _draw_2d_from_points for 'quiver' and 'streamlines'. + + Returns arguments (x, y, u, v), to be passed to the underlying matplotlib + call. + + "u_data" will always be "u_cube.data". + The matching "v_cube.data" component is stored in kwargs['_v_data']. + + """ + v_data = kwargs.pop('_v_data') + + # Rescale u+v values for plot distortion. + crs = kwargs.get('transform', None) + if crs: + if not isinstance(crs, (ccrs.PlateCarree, ccrs.RotatedPole)): + msg = ('Can only plot vectors provided in a lat-lon ' + 'projection, i.e. "cartopy.crs.PlateCarree" or ' + '"cartopy.crs.RotatedPole". This ' + "cubes coordinate system is {}.") + raise ValueError(msg.format(crs)) + # Given the above check, the Y points must be latitudes. + # We therefore **assume** they are in degrees : I'm not sure this + # is wise, but all the rest of this plot code does that, e.g. in + # _map_common. + # TODO: investigate degree units assumptions, here + elsewhere. + + # Implement a latitude scaling, but preserve the given magnitudes. + v_data = v_data.copy() + mags = np.sqrt(u_data * u_data + v_data * v_data) + v_data *= np.cos(np.deg2rad(y_points)) + scales = mags / np.sqrt(u_data * u_data + v_data * v_data) + u_data *= scales + v_data *= scales + + return ((x_points, y_points, u_data, v_data), kwargs) + + +def quiver(u_cube, v_cube, *args, **kwargs): + """ + Draws an arrow plot from two vector component cubes. + + Args: + + * u_cube, v_cube : (:class:`~iris.cube.Cube`) + u and v vector components. Must have same shape and units. + If the cubes have geographic coordinates, the values are treated as + true distance differentials, e.g. windspeeds, and *not* map coordinate + vectors. The components are aligned with the North and East of the + cube coordinate system. + + .. Note: + + At present, if u_cube and v_cube have geographic coordinates, then they + must be in a lat-lon coordinate system, though it may be a rotated one. + To transform wind values between coordinate systems, use + :func:`iris.analysis.cartography.rotate_vectors`. + To transform coordinate grid points, you will need to create + 2-dimensional arrays of x and y values. These can be transformed with + :meth:`cartopy.crs.CRS.transform_points`. + + Kwargs: + + * coords: (list of :class:`~iris.coords.Coord` or string) + Coordinates or coordinate names. Use the given coordinates as the axes + for the plot. The order of the given coordinates indicates which axis + to use for each, where the first element is the horizontal + axis of the plot and the second element is the vertical axis + of the plot. + + * axes: the :class:`matplotlib.axes.Axes` to use for drawing. + Defaults to the current axes if none provided. + + See :func:`matplotlib.pyplot.quiver` for details of other valid + keyword arguments. + + """ + # + # TODO: check u + v cubes for compatibility. + # + kwargs['_v_data'] = v_cube.data + return _draw_2d_from_points('quiver', _vector_component_args, u_cube, + *args, **kwargs) + + +def streamplot(u_cube, v_cube, *args, **kwargs): + """ + Draws a streamline plot from two vector component cubes. + + Args: + + * u_cube, v_cube : (:class:`~iris.cube.Cube`) + u and v vector components. Must have same shape and units. + If the cubes have geographic coordinates, the values are treated as + true distance differentials, e.g. windspeeds, and *not* map coordinate + vectors. The components are aligned with the North and East of the + cube coordinate system. + + .. Note: + + At present, if u_cube and v_cube have geographic coordinates, then they + must be in a lat-lon coordinate system, though it may be a rotated one. + To transform wind values between coordinate systems, use + :func:`iris.analysis.cartography.rotate_vectors`. + To transform coordinate grid points, you will need to create + 2-dimensional arrays of x and y values. These can be transformed with + :meth:`cartopy.crs.CRS.transform_points`. + + Kwargs: + + * coords: (list of :class:`~iris.coords.Coord` or string) + Coordinates or coordinate names. Use the given coordinates as the axes + for the plot. The order of the given coordinates indicates which axis + to use for each, where the first element is the horizontal + axis of the plot and the second element is the vertical axis + of the plot. + + * axes: the :class:`matplotlib.axes.Axes` to use for drawing. + Defaults to the current axes if none provided. + + See :func:`matplotlib.pyplot.quiver` for details of other valid + keyword arguments. + + """ + # + # TODO: check u + v cubes for compatibility. + # + kwargs['_v_data'] = v_cube.data + return _draw_2d_from_points('streamplot', _vector_component_args, u_cube, + *args, **kwargs) + + def plot(*args, **kwargs): """ Draws a line plot based on the given cube(s) or coordinate(s). From aee7f561d9e6dcc0cf5a8829d2f6db4bc8c17b25 Mon Sep 17 00:00:00 2001 From: lbdreyer Date: Tue, 31 Jul 2018 14:05:40 +0100 Subject: [PATCH 17/62] Change version number (#3118) --- docs/iris/src/_templates/layout.html | 2 +- docs/iris/src/whatsnew/2.2.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/iris/src/_templates/layout.html b/docs/iris/src/_templates/layout.html index 53ae5105cb..8ecc35bade 100644 --- a/docs/iris/src/_templates/layout.html +++ b/docs/iris/src/_templates/layout.html @@ -37,7 +37,7 @@

    - Iris v2.1 + Iris v2.2

    A powerful, format-agnostic, community-driven Python library for analysing and diff --git a/docs/iris/src/whatsnew/2.2.rst b/docs/iris/src/whatsnew/2.2.rst index 4165ebf43d..97eba3dee6 100644 --- a/docs/iris/src/whatsnew/2.2.rst +++ b/docs/iris/src/whatsnew/2.2.rst @@ -6,7 +6,7 @@ What's New in Iris 2.2.0 This document explains the new/changed features of Iris in the alpha release - of version 2.2.0 +of version 2.2.0 (:doc:`View all changes `). From 297becf2ddb6b74128f700cffd65876cdeac002b Mon Sep 17 00:00:00 2001 From: Corinne Bosley Date: Tue, 31 Jul 2018 14:22:55 +0100 Subject: [PATCH 18/62] re-added bounds definition for contiguity check, removed unnecessary test --- lib/iris/plot.py | 2 + lib/iris/tests/unit/plot/test_2d_coords.py | 72 +++++++++++----------- 2 files changed, 38 insertions(+), 36 deletions(-) diff --git a/lib/iris/plot.py b/lib/iris/plot.py index ac8da822cc..8f11c8d116 100644 --- a/lib/iris/plot.py +++ b/lib/iris/plot.py @@ -290,6 +290,8 @@ def _check_contiguity_and_bounds(coord, data, abs_tol=1e-4, transpose=False): if transpose: data = data.T + bounds = coord.bounds + both_dirs_contiguous, diffs_along_x, diffs_along_y = \ iris.coords._discontiguity_in_2d_bounds(bounds, abs_tol=abs_tol) diff --git a/lib/iris/tests/unit/plot/test_2d_coords.py b/lib/iris/tests/unit/plot/test_2d_coords.py index ce96157bf4..bb27341baa 100644 --- a/lib/iris/tests/unit/plot/test_2d_coords.py +++ b/lib/iris/tests/unit/plot/test_2d_coords.py @@ -46,23 +46,23 @@ def setUp(self): self.latlon_2d = testdata.full2d_global() testdata.make_bounds_discontiguous_at_point(self.latlon_2d, 2, 2) - # Take a latlon cube with 1D coords, broadcast the coords into 2D - # ones, then add ONE of them back into the cube in place of original: - single_dims = lat_lon_cube() - lon = single_dims.coord('longitude') - lat = single_dims.coord('latitude') - big_lon, big_lat = testdata.grid_coords_2d_from_1d(lon, lat) - mixed_dims = single_dims.copy() - mixed_dims.remove_coord(lon) - # TODO Fix this coord addition: - # When adding an aux_coord, the function '_check_multidim_metadata' - # throws an error as it requires coord.shape to be (1, ) instead of - # (3, 4) or whatever. - mixed_dims.add_aux_coord(big_lon) - - # mixed_dims is now a cube with 2 1D dim coords and an additional - # 2D aux coord. - self.mixed_dims = mixed_dims + # # Take a latlon cube with 1D coords, broadcast the coords into 2D + # # ones, then add ONE of them back into the cube in place of original: + # single_dims = lat_lon_cube() + # lon = single_dims.coord('longitude') + # lat = single_dims.coord('latitude') + # big_lon, big_lat = testdata.grid_coords_2d_from_1d(lon, lat) + # mixed_dims = single_dims.copy() + # mixed_dims.remove_coord(lon) + # # TODO Fix this coord addition: + # # When adding an aux_coord, the function '_check_multidim_metadata' + # # throws an error as it requires coord.shape to be (1, ) instead of + # # (3, 4) or whatever. + # mixed_dims.add_aux_coord(big_lon) + # + # # mixed_dims is now a cube with 2 1D dim coords and an additional + # # 2D aux coord. + # self.mixed_dims = mixed_dims self.mode = coords.BOUND_MODE @@ -100,15 +100,15 @@ def test_total_span_check(self): iplt._get_plot_defn_custom_coords_picked(cube, ('wibble', 'foo'), self.mode) - def test_2dcoord_with_1dcoord(self): - # TODO Generate a cube with one 2d coord and one 1d coord - # TODO Try and plot them against each other - # TODO Find out where I can put a catch for this (if necessary) - cube = self.mixed_dims - with self.assertRaises(ValueError): - iplt._get_plot_defn_custom_coords_picked(cube, - ('latitude', 'longitude'), - self.mode) + # def test_2dcoord_with_1dcoord(self): + # # TODO Generate a cube with one 2d coord and one 1d coord + # # TODO Try and plot them against each other + # # TODO Find out where I can put a catch for this (if necessary) + # cube = self.mixed_dims + # with self.assertRaises(ValueError): + # iplt._get_plot_defn_custom_coords_picked(cube, + # ('latitude', 'longitude'), + # self.mode) def test_map_common_not_enough_bounds(self): # Test that a lat-lon cube with 2d coords and 2 bounds per point @@ -129,16 +129,16 @@ def test_map_common_2d(self): result = iplt._map_common('pcolor', None, self.mode, cube, plot_defn) self.assertTrue(result) - def test_discontiguous_masked(self): - # Test that a contiguity check will raise a warning (not an error) for - # discontiguous bounds but appropriately masked data. - cube = self.latlon_2d - coord = cube.coord('longitude') - msg = 'The bounds of the longitude coordinate are not contiguous. ' \ - 'However, data is masked where the discontiguity occurs so ' \ - 'plotting anyway.' - with self.assertWarnsRegexp(msg): - iplt._check_contiguity_and_bounds(coord, cube.data) + # def test_discontiguous_masked(self): + # # Test that a contiguity check will raise a warning (not an error) for + # # discontiguous bounds but appropriately masked data. + # cube = self.latlon_2d + # coord = cube.coord('longitude') + # msg = 'The bounds of the longitude coordinate are not contiguous. ' \ + # 'However, data is masked where the discontiguity occurs so ' \ + # 'plotting anyway.' + # with self.assertWarnsRegexp(msg): + # iplt._check_contiguity_and_bounds(coord, cube.data) def test_discontiguous_unmasked(self): # Check that an error occurs when the contiguity check finds From a281805785e212f2f23cdb8a1ecc372978feca39 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Thu, 2 Aug 2018 12:32:50 +0100 Subject: [PATCH 19/62] Ensure Sphinx autodocs for grid_angles routines. --- lib/iris/analysis/cartography.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/iris/analysis/cartography.py b/lib/iris/analysis/cartography.py index 16c99c83a8..0c19b84815 100644 --- a/lib/iris/analysis/cartography.py +++ b/lib/iris/analysis/cartography.py @@ -42,6 +42,23 @@ true_vectors_from_grid_vectors as rotate_grid_vectors) +# List of contents to control Sphinx autodocs. +# Unfortunately essential to get docs for the grid_angles functions. +__all__ = [ + 'area_weights', + 'cosine_latitude_weights', + 'get_xy_contiguous_bounded_grids', + 'get_xy_grids', + 'gridcell_angles', + 'project', + 'rotate_grid_vectors', + 'rotate_pole', + 'rotate_winds', + 'unrotate_pole', + 'wrap_lons', + 'DistanceDifferential', + 'PartialDifferential'] + # This value is used as a fall-back if the cube does not define the earth DEFAULT_SPHERICAL_EARTH_RADIUS = 6367470 # TODO: This should not be necessary, as CF is always in meters From e024f5019702013c228844da410af8bc014f30ea Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Thu, 2 Aug 2018 12:34:54 +0100 Subject: [PATCH 20/62] Codestyle fixes. --- lib/iris/analysis/cartography.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/iris/analysis/cartography.py b/lib/iris/analysis/cartography.py index 0c19b84815..dadcdedc86 100644 --- a/lib/iris/analysis/cartography.py +++ b/lib/iris/analysis/cartography.py @@ -352,7 +352,7 @@ def _quadrant_area(radian_lat_bounds, radian_lon_bounds, radius_of_earth): def area_weights(cube, normalize=False): - """ + r""" Returns an array of area weights, with the same dimensions as the cube. This is a 2D lat/lon area weights array, repeated over the non lat/lon @@ -460,7 +460,7 @@ def area_weights(cube, normalize=False): def cosine_latitude_weights(cube): - """ + r""" Returns an array of latitude weights, with the same dimensions as the cube. The weights are the cosine of latitude. @@ -970,7 +970,7 @@ def _transform_distance_vectors_tolerance_mask(src_crs, x, y, tgt_crs, def rotate_winds(u_cube, v_cube, target_cs): - """ + r""" Transform wind vectors to a different coordinate system. The input cubes contain U and V components parallel to the local X and Y From e3e82e458f68ebbc008f43eb5ce91bced038d445 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Thu, 2 Aug 2018 12:53:08 +0100 Subject: [PATCH 21/62] Whatsnew entries for 2d vector support. --- docs/iris/src/whatsnew/2.2.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/iris/src/whatsnew/2.2.rst b/docs/iris/src/whatsnew/2.2.rst index 97eba3dee6..4edc305025 100644 --- a/docs/iris/src/whatsnew/2.2.rst +++ b/docs/iris/src/whatsnew/2.2.rst @@ -24,3 +24,13 @@ Iris 2.2.0 Features automatically checked for contiguity. Coordinate bounds must either be contiguous, or the cube's data must be masked at the discontiguities in order to avoid plotting errors. + + The iris plot functions :func:`iris.plot.quiver` and + :func:`iris.plot.streamplot` have been added, and these also work with + 2-dimensional plot coordinates. + +.. admonition:: 2-Dimensional Grid Vectors + + The iris functions :func:`iris.analysis.cartography.gridcell_angles` and + :func:`iris.analysis.cartography.rotate_grid_vectors` have been added, + allowing you to convert gridcell-oriented vectors to true-North/East ones. From b041c50ca371dacbbfc14489b35aa9a9dca44b50 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Tue, 14 Aug 2018 15:55:29 +0100 Subject: [PATCH 22/62] Pin Dask to avoid 0.18.2 bug with masked arrays. (#3127) * Put orca_util routines in subpackage of iris.test.stock, to get existing iris.tests.unit.plot.test_2d_coords working. * Disable broken misused testcode in test_gridcell_angles. * Ditch test_gridcell_angles, none of it is functional. * Further style fixes. * Skip tests using iris-test-data, for Travis TEST_MINIMAL phases. * Codestyle fix (though this code obsolete anyway). * Fix unused imports. * Made-up test cube replaces use of iris-test-data. * Renamed keyword; improved docstring. * Change 'co' to 'coord' for clarity. * Review changes. --- lib/iris/analysis/_grid_angles.py | 6 +- lib/iris/coords.py | 2 +- lib/iris/plot.py | 2 +- .../tests/{stock.py => stock/__init__.py} | 3 +- lib/iris/tests/stock/_stock_2d_latlons.py | 346 ++++++++++++++++ lib/iris/tests/test_coding_standards.py | 2 +- .../cartography/test_gridcell_angles.py | 392 ------------------ lib/iris/tests/unit/plot/test_2d_coords.py | 46 +- 8 files changed, 382 insertions(+), 417 deletions(-) rename lib/iris/tests/{stock.py => stock/__init__.py} (99%) create mode 100644 lib/iris/tests/stock/_stock_2d_latlons.py delete mode 100644 lib/iris/tests/unit/analysis/cartography/test_gridcell_angles.py diff --git a/lib/iris/analysis/_grid_angles.py b/lib/iris/analysis/_grid_angles.py index 1746834a65..ffe6d51c7f 100644 --- a/lib/iris/analysis/_grid_angles.py +++ b/lib/iris/analysis/_grid_angles.py @@ -92,7 +92,7 @@ def _angle(p, q, r): if old_style: mid_lons = np.deg2rad(q[0]) - pr = _3d_xyz_from_latlon(r[0], r[1]) - _3d_xyz_from_latlon(p[0], p[1]) + pr = _3d_xyz_from_latlon(r[0], r[1]) - _3d_xyz_from_latlon(p[0], p[1]) pr_norm = np.sqrt(np.sum(pr**2, axis=0)) pr_top = pr[1] * np.cos(mid_lons) - pr[0] * np.sin(mid_lons) @@ -124,7 +124,7 @@ def _angle(p, q, r): lmb_hatvec_y, lmb_hatvec_z)]) - pr = _3d_xyz_from_latlon(r[0], r[1]) - _3d_xyz_from_latlon(p[0], p[1]) + pr = _3d_xyz_from_latlon(r[0], r[1]) - _3d_xyz_from_latlon(p[0], p[1]) # Dot products to form true-northward / true-eastward projections. pr_cmpt_e = np.sum(pr * lmb_hatvec, axis=0) @@ -141,7 +141,7 @@ def _angle(p, q, r): rtol = 1.e-3 check = np.allclose(mag_rot, mag_orig, rtol=rtol) if not check: - print (mag_rot, mag_orig) + print(mag_rot, mag_orig) assert np.allclose(mag_rot, mag_orig, rtol=rtol) return psi diff --git a/lib/iris/coords.py b/lib/iris/coords.py index 8f1e48fd18..ff2f170a2f 100644 --- a/lib/iris/coords.py +++ b/lib/iris/coords.py @@ -1071,7 +1071,7 @@ def is_contiguous(self, rtol=1e-05, atol=1e-08): rtol=rtol, atol=atol) elif self.ndim == 2: contiguous, _, _ = _discontiguity_in_2d_bounds(self.bounds, - abs_tol=atol) + abs_tol=atol) else: contiguous = False return contiguous diff --git a/lib/iris/plot.py b/lib/iris/plot.py index 8f11c8d116..7bf8ea07f3 100644 --- a/lib/iris/plot.py +++ b/lib/iris/plot.py @@ -334,7 +334,7 @@ def _draw_2d_from_bounds(draw_method_name, cube, *args, **kwargs): plot_defn = _get_plot_defn(cube, mode, ndims=2) twodim_contig_atol = kwargs.pop('two_dim_coord_contiguity_atol', - 1e-4) + 1e-4) for coord in plot_defn.coords: if hasattr(coord, 'has_bounds'): if coord.ndim == 2 and coord.has_bounds(): diff --git a/lib/iris/tests/stock.py b/lib/iris/tests/stock/__init__.py similarity index 99% rename from lib/iris/tests/stock.py rename to lib/iris/tests/stock/__init__.py index 1b55510d2b..5965d5a208 100644 --- a/lib/iris/tests/stock.py +++ b/lib/iris/tests/stock/__init__.py @@ -36,7 +36,8 @@ from iris.coords import DimCoord, AuxCoord import iris.tests as tests from iris.coord_systems import GeogCS, RotatedGeogCS - +from ._stock_2d_latlons import (sample_2d_latlons, + make_bounds_discontiguous_at_point) def lat_lon_cube(): """ diff --git a/lib/iris/tests/stock/_stock_2d_latlons.py b/lib/iris/tests/stock/_stock_2d_latlons.py new file mode 100644 index 0000000000..75b62f31ba --- /dev/null +++ b/lib/iris/tests/stock/_stock_2d_latlons.py @@ -0,0 +1,346 @@ +# (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 . +""" +Extra stock routines for making and manipulating cubes with 2d coordinates, +to mimic ocean grid data. + +""" +from __future__ import (absolute_import, division, print_function) +from six.moves import (filter, input, map, range, zip) # noqa + +import numpy as np +import numpy.ma as ma + +from iris.analysis.cartography import unrotate_pole +from iris.cube import Cube +from iris.coords import AuxCoord, DimCoord +from iris.coord_systems import RotatedGeogCS + + +def expand_1d_x_and_y_bounds_to_2d_xy(x_bounds_1d, y_bounds_1d): + """ + Convert bounds for separate 1-D X and Y coords into bounds for the + equivalent 2D coordinates. + + The output arrays have 4 points per cell, for 4 'corners' of a gridcell, + in the usual anticlockwise order + (bottom-left, bottom-right, top-right, top-left). + + If 1-dimensional X and Y coords have shapes nx and ny, then their bounds + have shapes (nx, 2) and (ny, 2). + The equivalent 2d coordinates would have values which are a "meshgrid" of + the original 1-D points, both having the shape (ny, ny). + The outputs are 2d bounds arrays suitable for such 2d coordinates. + + Args: + + * x_bounds_1d, y_bounds_1d : (array) + Coordinate bounds arrays, with shapes (nx, 2) and (ny, 2). + + Result: + + * x_bounds_2d, y_bounds_2d : (array) + Expanded 2d bounds arrays, both of shape (ny, nx, 4). + + """ + shapes = [bds.shape for bds in (x_bounds_1d, y_bounds_1d)] + for shape in shapes: + if len(shape) != 2 or shape[1] != 2: + msg = ('One-dimensional bounds arrays must have shapes (ny, 2) ' + 'and (nx, 2). Got {} and {}.') + raise ValueError(msg.format(*shapes)) + + # Construct output arrays, which are both (ny, nx, 4). + nx, ny = [shape[0] for shape in shapes] + bds_2d_x = np.zeros((ny, nx, 4)) + bds_2d_y = bds_2d_x.copy() + + # Expand x bounds to 2D array (ny, nx, 4) : the same over all 'Y'. + # Bottom left+right corners are the original 1-D x bounds. + bds_2d_x[:, :, 0] = x_bounds_1d[:, 0].reshape((1, nx)) + bds_2d_x[:, :, 1] = x_bounds_1d[:, 1].reshape((1, nx)) + # Top left+right corners are the same as bottom left+right. + bds_2d_x[:, :, 2] = bds_2d_x[:, :, 1].copy() + bds_2d_x[:, :, 3] = bds_2d_x[:, :, 0].copy() + + # Expand y bounds to 2D array (ny, nx, 4) : the same over all 'X'. + # Left-hand lower+upper corners are the original 1-D y bounds. + bds_2d_y[:, :, 0] = y_bounds_1d[:, 0].reshape((ny, 1)) + bds_2d_y[:, :, 3] = y_bounds_1d[:, 1].reshape((ny, 1)) + # Right-hand lower+upper corners are the same as the left-hand ones. + bds_2d_y[:, :, 1] = bds_2d_y[:, :, 0].copy() + bds_2d_y[:, :, 2] = bds_2d_y[:, :, 3].copy() + + return bds_2d_x, bds_2d_y + + +def grid_coords_2d_from_1d(x_coord_1d, y_coord_1d): + """ + Calculate a pair of 2d X+Y coordinates from 1d ones, in a "meshgrid" style. + If the inputs are bounded, the outputs have 4 points per bounds in the + usual way, i.e. points 0,1,2,3 are the gridcell corners anticlockwise from + the bottom left. + + """ + for coord in (x_coord_1d, y_coord_1d): + if coord.ndim != 1: + msg = ('Input coords must be one-dimensional. ' + 'Coordinate "{}" has shape {}.') + raise ValueError(msg.format(coord.name(), coord.shape)) + + # Calculate centre-points as a mesh of the 2 inputs. + pts_2d_x, pts_2d_y = np.meshgrid(x_coord_1d.points, y_coord_1d.points) + if not x_coord_1d.has_bounds() or not y_coord_1d.has_bounds(): + bds_2d_x = None + bds_2d_y = None + else: + bds_2d_x, bds_2d_y = expand_1d_x_and_y_bounds_to_2d_xy( + x_coord_1d.bounds, y_coord_1d.bounds) + + # Make two new coords + return them. + result = [] + for pts, bds, name in zip((pts_2d_x, pts_2d_y), + (bds_2d_x, bds_2d_y), + ('longitude', 'latitude')): + coord = AuxCoord(pts, bounds=bds, standard_name=name, units='degrees') + result.append(coord) + + return result + + +def sample_2d_latlons(regional=False, rotated=False, transformed=False): + """ + Construct small 2d cubes with 2d X and Y coordinates. + + This makes cubes with 'expanded' coordinates (4 bounds per cell), analagous + to ORCA data. + The coordinates are always geographical, so either it has a coord system + or they are "true" lats + lons. + ( At present, they are always latitudes and longitudes, but maybe in a + rotated system. ) + The results always have fully contiguous bounds. + + Kwargs: + * regional (bool): + If False (default), results cover the whole globe, and there is + implicit connectivity between rhs + lhs of the array. + If True, coverage is regional and edges do not connect. + * rotated (bool): + If False, X and Y coordinates are true-latitudes and longitudes, with + an implicit coordinate system (i.e. None). + If True, the X and Y coordinates are lats+lons in a selected + rotated-latlon coordinate system. + * transformed (bool): + Build coords from rotated coords as for 'rotated', but then replace + their values with the equivalent "true" lats + lons, and no + coord-system (defaults to true-latlon). + In this case, the X and Y coords are no longer 'meshgrid' style, + i.e. the points + bounds values vary in *both* dimensions. + + .. note:: + + 'transformed' is an alternative to 'rotated' : when 'transformed' is + set, then 'rotated' has no effect. + + .. Some sample results printouts :: + + >>> print(sample_2d_latlons()) + test_data / (unknown) (-- : 5; -- : 6) + Auxiliary coordinates: + latitude x x + longitude x x + >>> + >>> print(sample_2d_latlons().coord(axis='x')[0, :2]) + AuxCoord(array([ 37.5 , 93.75]), + bounds=array([[ 0. , 65.625, 65.625, 0. ], + [ 65.625, 121.875, 121.875, 65.625]]), + standard_name='longitude', units=Unit('degrees')) + >>> print(np.round(sample_2d_latlons().coord(axis='x').points, 3)) + [[ 37.5 93.75 150. 206.25 262.5 318.75] + [ 37.5 93.75 150. 206.25 262.5 318.75] + [ 37.5 93.75 150. 206.25 262.5 318.75] + [ 37.5 93.75 150. 206.25 262.5 318.75] + [ 37.5 93.75 150. 206.25 262.5 318.75]] + >>> print(np.round(sample_2d_latlons().coord(axis='y').points, 3)) + [[-85. -85. -85. -85. -85. -85. ] + [-47.5 -47.5 -47.5 -47.5 -47.5 -47.5] + [-10. -10. -10. -10. -10. -10. ] + [ 27.5 27.5 27.5 27.5 27.5 27.5] + [ 65. 65. 65. 65. 65. 65. ]] + + + >>> print(np.round( + sample_2d_latlons(rotated=True).coord(axis='x').points, 3)) + [[ 37.5 93.75 150. 206.25 262.5 318.75] + [ 37.5 93.75 150. 206.25 262.5 318.75] + [ 37.5 93.75 150. 206.25 262.5 318.75] + [ 37.5 93.75 150. 206.25 262.5 318.75] + [ 37.5 93.75 150. 206.25 262.5 318.75]] + >>> print(sample_2d_latlons(rotated=True).coord(axis='y').coord_system) + RotatedGeogCS(75.0, 120.0) + + + >>> print( + sample_2d_latlons(transformed=True).coord(axis='y').coord_system) + None + >>> print(np.round( + sample_2d_latlons(transformed=True).coord(axis='x').points, 3)) + [[ -50.718 -40.983 -46.74 -71.938 -79.293 -70.146] + [ -29.867 17.606 77.936 157.145 -141.037 -93.172] + [ -23.139 31.007 87.699 148.322 -154.639 -100.505] + [ -16.054 41.218 92.761 143.837 -164.738 -108.105] + [ 10.86 61.78 100.236 137.285 175.511 -135.446]] + >>> print(np.round( + sample_2d_latlons(transformed=True).coord(axis='y').points, 3)) + [[-70.796 -74.52 -79.048 -79.26 -74.839 -70.96 ] + [-34.99 -46.352 -59.721 -60.34 -47.305 -35.499] + [ 1.976 -10.626 -22.859 -23.349 -11.595 1.37 ] + [ 38.914 25.531 14.312 13.893 24.585 38.215] + [ 74.197 60.258 51.325 51.016 59.446 73.268]] + >>> + + """ + def sample_cube(xargs, yargs): + # Make a test cube with given latitude + longitude coordinates. + # xargs/yargs are args for np.linspace (start, stop, N), to make the X + # and Y coordinate points. + x0, x1, nx = xargs + y0, y1, ny = yargs + # Data has cycling values, staggered a bit in successive rows. + data = np.zeros((ny, nx)) + data.flat[:] = np.arange(ny * nx) % (nx + 2) + # Build a 2d cube with longitude + latitude coordinates. + cube = Cube(data, long_name='test_data') + x_pts = np.linspace(x0, x1, nx, endpoint=True) + y_pts = np.linspace(y0, y1, ny, endpoint=True) + co_x = DimCoord(x_pts, standard_name='longitude', units='degrees') + co_y = DimCoord(y_pts, standard_name='latitude', units='degrees') + cube.add_dim_coord(co_y, 0) + cube.add_dim_coord(co_x, 1) + return cube + + if regional: + # Extract small region. + cube = sample_cube(xargs=(150., 243.75, 6), yargs=(-10., 40., 5)) + + # Make contiguous bounds. + for ax in ('x', 'y'): + cube.coord(axis=ax).guess_bounds() + else: + # Global data, but drastically reduced resolution. + cube = sample_cube(xargs=(37.5, 318.75, 6), yargs=(-85., 65., 5)) + + # Patch bounds to ensure it is still contiguous + global. + for name in ('longitude', 'latitude'): + coord = cube.coord(name) + coord.guess_bounds() + bds = coord.bounds.copy() + # Make bounds global, by fixing lowest and uppermost values. + if name == 'longitude': + bds[0, 0] = 0.0 + bds[-1, 1] = 360.0 + else: + bds[0, 0] = -90.0 + bds[-1, 1] = 90.0 + coord.bounds = bds + + # Get 1d coordinate points + bounds + calculate 2d equivalents. + co_1d_x, co_1d_y = [cube.coord(axis=ax).copy() for ax in ('x', 'y')] + co_2d_x, co_2d_y = grid_coords_2d_from_1d(co_1d_x, co_1d_y) + + # Remove the old grid coords. + for coord in (co_1d_x, co_1d_y): + cube.remove_coord(coord) + + # Add the new grid coords. + for coord in (co_2d_x, co_2d_y): + cube.add_aux_coord(coord, (0, 1)) + + if transformed or rotated: + # Take the lats + lons as being in a rotated coord system. + pole_lat, pole_lon = 75.0, 120.0 + if transformed: + # Reproject coordinate values from rotated to true lat-lons. + co_x, co_y = [cube.coord(axis=ax) for ax in ('x', 'y')] + # Unrotate points. + lons, lats = co_x.points, co_y.points + lons, lats = unrotate_pole(lons, lats, pole_lon, pole_lat) + co_x.points, co_y.points = lons, lats + # Unrotate bounds. + lons, lats = co_x.bounds, co_y.bounds + # Note: save the shape, flatten + then re-apply the shape, because + # "unrotate_pole" uses "cartopy.crs.CRS.transform_points", which + # only works on arrays of 1 or 2 dimensions. + shape = lons.shape + lons, lats = unrotate_pole(lons.flatten(), lats.flatten(), + pole_lon, pole_lat) + co_x.bounds, co_y.bounds = lons.reshape(shape), lats.reshape(shape) + else: + # "Just" rotate operation : add a coord-system to each coord. + cs = RotatedGeogCS(pole_lat, pole_lon) + for coord in cube.coords(): + coord.coord_system = cs + + return cube + + +def make_bounds_discontiguous_at_point(cube, at_iy, at_ix): + """ + Meddle with the XY grid bounds of a 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. + + The cube must have bounded 2d 'x' and 'y' coordinates. + + TODO: add a switch to make a discontiguity in the *y*-direction instead ? + + """ + x_coord = cube.coord(axis='x') + y_coord = cube.coord(axis='y') + assert x_coord.shape == y_coord.shape + assert (coord.bounds.ndim == 3 and coord.shape[-1] == 4 + for coord in (x_coord, y_coord)) + + # For both X and Y coord, move points + bounds to create a discontinuity. + 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 + 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. + new_pt = 0.25 * sum([bds_bl, bds_br, bds_tr, bds_tl]) + pts[at_iy, at_ix] = new_pt + # Write back the coord points+bounds (can only assign whole arrays). + coord.points, coord.bounds = pts, bds + + 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 diff --git a/lib/iris/tests/test_coding_standards.py b/lib/iris/tests/test_coding_standards.py index 39c73e8285..c24ac46cb3 100644 --- a/lib/iris/tests/test_coding_standards.py +++ b/lib/iris/tests/test_coding_standards.py @@ -93,7 +93,7 @@ class StandardReportWithExclusions(pep8.StandardReport): '*/iris/io/format_picker.py', '*/iris/tests/__init__.py', '*/iris/tests/pp.py', - '*/iris/tests/stock.py', + '*/iris/tests/stock/__init__.py', '*/iris/tests/system_test.py', '*/iris/tests/test_analysis.py', '*/iris/tests/test_analysis_calculus.py', diff --git a/lib/iris/tests/unit/analysis/cartography/test_gridcell_angles.py b/lib/iris/tests/unit/analysis/cartography/test_gridcell_angles.py deleted file mode 100644 index de00d2bc76..0000000000 --- a/lib/iris/tests/unit/analysis/cartography/test_gridcell_angles.py +++ /dev/null @@ -1,392 +0,0 @@ -# (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 . -""" -Unit tests for the function -:func:`iris.analysis.cartography.gridcell_angles`. - -""" -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 - -import cartopy.crs as ccrs -from iris.cube import Cube -from iris.coords import DimCoord, AuxCoord -import iris.coord_systems -from iris.analysis.cartography import unrotate_pole - -from iris.analysis.cartography import (gridcell_angles, - rotate_grid_vectors) - -import matplotlib.pyplot as plt -from orca_utils.plot_testing.blockplot_from_bounds import blockplot_2dll - - -def _rotated_grid_sample(pole_lat=15, pole_lon=-180, - lon_bounds=np.linspace(-30, 30, 6, endpoint=True), - lat_bounds=np.linspace(-30, 30, 6, endpoint=True)): - # Calculate *true* lat_bounds+lon_bounds for the rotated grid. - lon_bounds = np.array(lon_bounds, dtype=float) - lat_bounds = np.array(lat_bounds, dtype=float) - # Construct centrepoints. - lons = 0.5 * (lon_bounds[:-1] + lon_bounds[1:]) - lats = 0.5 * (lat_bounds[:-1] + lat_bounds[1:]) - # Convert all to full 2d arrays. - lon_bounds, lat_bounds = np.meshgrid(lon_bounds, lat_bounds) - lons, lats = np.meshgrid(lons, lats) - # Calculate true lats+lons for all points. - lons_true_bds, lats_true_bds = unrotate_pole(lon_bounds, lat_bounds, - pole_lon, pole_lat) - lons_true, lats_true = unrotate_pole(lons, lats, pole_lon, pole_lat) - # Make the 'unified' bounds into contiguous (ny, nx, 4) arrays. - def expand_unified_bds(bds): - ny, nx = bds.shape - bds_4 = np.zeros((ny - 1, nx - 1, 4)) - bds_4[:, :, 0] = bds[:-1, :-1] - bds_4[:, :, 1] = bds[:-1, 1:] - bds_4[:, :, 2] = bds[1:, 1:] - bds_4[:, :, 3] = bds[1:, :-1] - return bds_4 - - lon_true_bds4, lat_true_bds4 = (expand_unified_bds(bds) - for bds in (lons_true_bds, lats_true_bds)) - # Make these into a 2d-latlon grid for a cube - cube = Cube(np.zeros(lon_true_bds4.shape[:-1])) - co_x = AuxCoord(lons_true, bounds=lon_true_bds4, - standard_name='longitude', units='degrees') - co_y = AuxCoord(lats_true, bounds=lat_true_bds4, - standard_name='latitude', units='degrees') - cube.add_aux_coord(co_x, (0, 1)) - cube.add_aux_coord(co_y, (0, 1)) - return cube - - -class TestGridcellAngles(tests.IrisTest): - def _singlecell_30deg_cube(self, x0=90., y0=0., dx=20., dy=10.): - x_pts = np.array([[x0]]) - y_pts = np.array([[y0]]) - x_bds = x0 + dx * np.array([[[-1., 1, 0.5, -1.5]]]) -# self.assertArrayAllClose(x_bds, np.array([[[70., 110, 100, 60]]])) - y_bds = y0 + dy * np.array([[[-1., 1, 3, 1]]]) -# self.assertArrayAllClose(y_bds, np.array([[[-10., 10, 30, 10]]])) - co_x = AuxCoord(points=x_pts, bounds=x_bds, - standard_name='longitude', units='degrees') - co_y = AuxCoord(points=y_pts, bounds=y_bds, - standard_name='latitude', units='degrees') - cube = Cube(np.zeros((1, 1))) - cube.add_aux_coord(co_x, (0, 1)) - cube.add_aux_coord(co_y, (0, 1)) - return cube - - def _singlecell_diamond_cube(self, x0=90., y0=0., dy=10., dx_eq=None): - if dx_eq is None: - dx_eq = dy - x_pts = np.array([[x0]]) - y_pts = np.array([[y0]]) - dx = dx_eq / np.cos(np.deg2rad(y0)) - x_bds = np.array([[[x0, x0 + dx, x0, x0 - dx]]]) - y_bds = np.array([[[y0 - dy, y0, y0 + dy, y0]]]) - co_x = AuxCoord(points=x_pts, bounds=x_bds, - standard_name='longitude', units='degrees') - co_y = AuxCoord(points=y_pts, bounds=y_bds, - standard_name='latitude', units='degrees') - cube = Cube(np.zeros((1, 1))) - cube.add_aux_coord(co_x, (0, 1)) - cube.add_aux_coord(co_y, (0, 1)) - return cube - - def test_single_cell_equatorial(self): - plt.switch_backend('tkagg') - plt.figure(figsize=(10,10)) - ax = plt.axes(projection=ccrs.Mercator()) -# ax = plt.axes(projection=ccrs.NorthPolarStereo()) -# ax = plt.axes(projection=ccrs.Orthographic(central_longitude=90., -# central_latitude=30.)) - - lon0 = 90.0 - dy = 2.0 - y_0, y_n, ny = -80, 80, 9 - angles = [] - for lat in np.linspace(y_0, y_n, ny): - cube = self._singlecell_diamond_cube(x0=lon0, y0=lat, dy=dy) - angles_cube = gridcell_angles(cube, -# cell_angle_boundpoints='mid-lhs, mid-rhs') - cell_angle_boundpoints='lower-left, lower-right') - tmp_cube = angles_cube.copy() - tmp_cube.convert_units('degrees') - print('') - print(lat) - co_x, co_y = (cube.coord(axis=ax) for ax in ('x', 'y')) - print() - print(' at : {}, {}'.format(co_x.points[0, 0], co_y.points[0, 0])) - print(' x-bds:') - print(co_x.bounds) - print(' y-bds:') - print(co_y.bounds) - angle = tmp_cube.data[0, 0] - angles.append(angle) - print(angle) - blockplot_2dll(cube) - - ax.coastlines() - ax.set_global() - - # Plot constant NEly (45deg) arrows. - xx = np.array([lon0] * ny) - yy = np.linspace(y_0, y_n, ny) - dy - uu = np.array([1.0] * ny) - plt.quiver(xx, yy, - uu, np.cos(np.deg2rad(yy)), - zorder=2, color='red', - scale_units='xy', - transform=ccrs.PlateCarree()) - - # Also plot returned angles. - angles_arr_rad = np.deg2rad(angles) - u_arr = uu * np.cos(angles_arr_rad) - v_arr = uu * np.sin(angles_arr_rad) * np.cos(np.deg2rad(yy)) - - plt.quiver(xx, yy, - u_arr, - v_arr, - zorder=2, color='magenta', - scale_units='xy', - width=0.005, - scale=0.2e-6, -# width=0.5, - transform=ccrs.PlateCarree()) - - plt.show() - - - def test_values(self): - # Construct a rotated-pole grid and check angle calculation. - testcube = _rotated_grid_sample() - - cell_angle_boundpoints = 'mid-lhs, mid-rhs' -# cell_angle_boundpoints = 'lower-left, lower-right' -# cell_angle_boundpoints = 'garble' - angles_cube = gridcell_angles( - testcube, - cell_angle_boundpoints=cell_angle_boundpoints) - angles_cube.convert_units('radians') - - # testing phase... - print(np.rad2deg(angles_cube.data)) - - import matplotlib.pyplot as plt - plt.switch_backend('tkagg') - -# plot_map = 'north_polar_stereographic' -# plot_map = 'plate_carree' -# plot_map = 'mercator' - plot_map = 'north_polar_orthographic' - if plot_map == 'plate_carree': - scale = 0.1 - map_proj = ccrs.PlateCarree() - elif plot_map == 'mercator': - scale = 3.0e-6 - map_proj = ccrs.Mercator() - map_proj._threshold *= 0.01 - elif plot_map == 'north_polar_orthographic': - scale = 3.0e-6 - map_proj = ccrs.Orthographic(central_longitude=0.0, - central_latitude=90.0,) - map_proj._threshold *= 0.01 - elif plot_map == 'north_polar_stereographic': - scale = 3.0e-6 - map_proj = ccrs.NorthPolarStereo() - else: - assert 0 - - ax = plt.axes(projection=map_proj) - data_proj = ccrs.PlateCarree() - - deg_scale = 10.0 - -# angles = 'uv' - angles = 'xy' - - ax.coastlines() - ax.gridlines() - for i_bnd in range(4): - color = ['black', 'red', 'blue', 'magenta'][i_bnd] - plt.plot(testcube.coord('longitude').bounds[..., i_bnd], - testcube.coord('latitude').bounds[..., i_bnd], - '+', markersize=10., markeredgewidth=2., - markerfacecolor=color, markeredgecolor=color, - transform=data_proj) - - - # Show plain 0,1 + 1,0 (PlateCarree) vectors unrotated at the given points. - pts_shape = testcube.coord('longitude').shape - ny, nx = pts_shape - u0 = np.ones(pts_shape) - v0 = np.zeros(pts_shape) - u1 = v0.copy() - v1 = u0.copy() - - x0s = testcube.coord('longitude').points - y0s = testcube.coord('latitude').points - yscale = np.cos(np.deg2rad(y0s)) - plt.quiver(x0s, y0s, u0, v0 * yscale, - color='blue', width=0.005, - headwidth=2., # headlength=1.0, headaxislength=0.7, - angles=angles, - scale_units='xy', scale=scale, - transform=data_proj) - plt.quiver(x0s, y0s, u1, v1 * yscale, - color='red', width=0.005, - headwidth=2., # headlength=1.0, headaxislength=0.7, - angles=angles, - scale_units='xy', scale=scale, - transform=data_proj) - - # Add 45deg arrows (NEly), still on a PlateCarree map. - plt.quiver(x0s, y0s, v1, v1 * yscale, - color='green', width=0.005, - headwidth=2., # headlength=1.0, headaxislength=0.7, - angles=angles, - scale_units='xy', scale=scale, - transform=data_proj) - - - - # - # Repeat the above plotting short lines INSTEAD of quiver. - # - u0d = x0s + deg_scale * u0 - v0d = y0s + deg_scale * v0 - u1d = x0s + deg_scale * u1 - v1d = y0s + deg_scale * v1 - u2d = x0s + deg_scale * u0 - v2d = y0s + deg_scale * v1 - for iy in range(ny): - for ix in range(nx): - plt.plot([x0s[iy, ix], u0d[iy, ix]], - [y0s[iy, ix], v0d[iy, ix]], - ':', color='blue', linewidth=0.5, - transform=data_proj) - plt.plot([x0s[iy, ix], u1d[iy, ix]], - [y0s[iy, ix], v1d[iy, ix]], - ':', color='red', linewidth=0.5, - transform=data_proj) - plt.plot([x0s[iy, ix], u2d[iy, ix]], - [y0s[iy, ix], v2d[iy, ix]], - ':', color='green', linewidth=0.5, - transform=data_proj) - - - # Overplot BL-BR and BL-TL lines from the cell bounds. - co_lon, co_lat = [testcube.coord(name).copy() - for name in ('longitude', 'latitude')] - for co in (co_lon, co_lat): - co.convert_units('degrees') - lon_bds, lat_bds = [co.bounds for co in (co_lon, co_lat)] -# ny, nx = lon_bds.shape[:-1] - for iy in range(ny): - for ix in range(nx): - x0, y0 = lon_bds[iy, ix, 0], lat_bds[iy, ix, 0] - x1, y1 = lon_bds[iy, ix, 1], lat_bds[iy, ix, 1] - x2, y2 = lon_bds[iy, ix, 3], lat_bds[iy, ix, 3] - plt.plot([x0, x1], [y0, y1], 'x-', - color='orange', - transform=data_proj) - plt.plot([x0, x2], [y0, y2], 'x-', - color='orange', linestyle='--', - transform=data_proj) - - # Plot U0, rotated by cell angles, also at cell bottom-lefts. - u0_cube, u1_cube, v0_cube, v1_cube = [testcube.copy(data=aa) - for aa in (u0, v0, u1, v1)] - u0r_cube, v0r_cube = rotate_grid_vectors( - u0_cube, v0_cube, grid_angles_cube=angles_cube) - u0r, v0r = [cube.data for cube in (u0r_cube, v0r_cube)] - - xbl, ybl = lon_bds[..., 0], lat_bds[..., 0] - # - # Replace quiver here with delta-based lineplot - # - urd = xbl + deg_scale * u0r - vrd = ybl + deg_scale * v0r * yscale - for iy in range(ny): - for ix in range(nx): - plt.plot([xbl[iy, ix], urd[iy, ix]], - [ybl[iy, ix], vrd[iy, ix]], - ':', color='magenta', linewidth=2.5, - transform=data_proj) - # Show this is the SAME as lineplot - plt.quiver(xbl, ybl, u0r, v0r * yscale, - color='magenta', width=0.01, - headwidth=1.2, # headlength=1.0, headaxislength=0.7, - angles=angles, - scale_units='xy', scale=scale, - transform=data_proj) - - plt.suptitle('angles from "{}"'.format(cell_angle_boundpoints)) - -# # Also draw small lines pointing at the correct (TRUE, not ) angle. -# ny, nx = x0s.shape -# size_degrees = 1.0 -# angles = angles_cube.copy() -# angles.convert_units('radians') -# angles = angles.data -# lats = testcube.coord('latitude').copy() -# lats.convert_units('radians') -# lats = lats.points -# dxs = size_degrees * u0.copy() #* np.cos(angles) -# dys = size_degrees * u0.copy() # / np.sqrt(np.cos(lats)) -# x1s = x0s + dxs -# y1s = y0s + dys -## for iy in range(ny): -## for ix in range(nx): -## plt.plot([x0s[iy, ix], x1s[iy, ix]], -## [y0s[iy, ix], y1s[iy, ix]], -## 'o-', markersize=4., markeredgewidth=0., -## color='green', # scale_units='xy', scale=scale, -## transform=data_proj) -# plt.quiver(x0s, y0s, dxs, dys, -# color='green', linewidth=0.2, -# angles=angles, -# scale_units='xy', scale=scale * 0.6, -# transform=data_proj) - - - - ax.set_global() - plt.show() - - angles_cube.convert_units('degrees') - - self.assertArrayAllClose( - angles_cube.data, - [[33.421, 17.928, 0., -17.928, -33.421], - [41.981, 24.069, 0., -24.069, -41.981], - [56.624, 37.809, 0., -37.809, -56.624], - [79.940, 74.227, 0., -74.227, -79.940], - [107.313, 126.361, -180., -126.361, -107.313]], - atol=0.002) - - -if __name__ == "__main__": - tests.main() diff --git a/lib/iris/tests/unit/plot/test_2d_coords.py b/lib/iris/tests/unit/plot/test_2d_coords.py index bb27341baa..7397921861 100644 --- a/lib/iris/tests/unit/plot/test_2d_coords.py +++ b/lib/iris/tests/unit/plot/test_2d_coords.py @@ -22,20 +22,25 @@ # Import iris.tests first so that some things can be initialised before # importing anything else. import iris.tests as tests -import iris.coords as coords import numpy.ma as ma +import iris.coords as coords from iris.tests.stock import simple_2d_w_multidim_coords as cube_2dcoords from iris.tests.stock import simple_3d_w_multidim_coords as cube3d_2dcoords -from iris.tests.stock import lat_lon_cube +from iris.tests.stock import sample_2d_latlons +from iris.tests.stock import make_bounds_discontiguous_at_point -from orca_utils.plot_testing import testdata_2d_coords as testdata if tests.MPL_AVAILABLE: import iris.plot as iplt +def full2d_global(): + return sample_2d_latlons(transformed=True) + + +@tests.skip_data class Test_2d_coords_plot_defn_bound_mode(tests.IrisTest): def setUp(self): self.multidim_cube = cube_2dcoords() @@ -43,8 +48,8 @@ def setUp(self): # latlon_2d is a cube with 2d coords, 4 bounds per point, # discontiguities in the bounds but masked data at the discontiguities. - self.latlon_2d = testdata.full2d_global() - testdata.make_bounds_discontiguous_at_point(self.latlon_2d, 2, 2) + self.latlon_2d = full2d_global() + make_bounds_discontiguous_at_point(self.latlon_2d, 2, 2) # # Take a latlon cube with 1D coords, broadcast the coords into 2D # # ones, then add ONE of them back into the cube in place of original: @@ -106,9 +111,10 @@ def test_total_span_check(self): # # TODO Find out where I can put a catch for this (if necessary) # cube = self.mixed_dims # with self.assertRaises(ValueError): - # iplt._get_plot_defn_custom_coords_picked(cube, - # ('latitude', 'longitude'), - # self.mode) + # iplt._get_plot_defn_custom_coords_picked( + # cube, + # ('latitude', 'longitude'), + # self.mode) def test_map_common_not_enough_bounds(self): # Test that a lat-lon cube with 2d coords and 2 bounds per point @@ -129,16 +135,16 @@ def test_map_common_2d(self): result = iplt._map_common('pcolor', None, self.mode, cube, plot_defn) self.assertTrue(result) - # def test_discontiguous_masked(self): - # # Test that a contiguity check will raise a warning (not an error) for - # # discontiguous bounds but appropriately masked data. - # cube = self.latlon_2d - # coord = cube.coord('longitude') - # msg = 'The bounds of the longitude coordinate are not contiguous. ' \ - # 'However, data is masked where the discontiguity occurs so ' \ - # 'plotting anyway.' - # with self.assertWarnsRegexp(msg): - # iplt._check_contiguity_and_bounds(coord, cube.data) +# def test_discontiguous_masked(self): +# # Test that a contiguity check will raise a warning (not an error) for +# # discontiguous bounds but appropriately masked data. +# cube = self.latlon_2d +# coord = cube.coord('longitude') +# msg = 'The bounds of the longitude coordinate are not contiguous. ' \ +# 'However, data is masked where the discontiguity occurs so ' \ +# 'plotting anyway.' +# with self.assertWarnsRegexp(msg): +# iplt._check_contiguity_and_bounds(coord, cube.data) def test_discontiguous_unmasked(self): # Check that an error occurs when the contiguity check finds @@ -155,3 +161,7 @@ def test_draw_2d_from_bounds(self): cube = self.latlon_2d result = iplt._draw_2d_from_bounds('pcolormesh', cube) self.assertTrue(result) + + +if __name__ == '__main__': + tests.main() From 7a9cb5c6555f735c48d8d6232b50cf4d6cbbc748 Mon Sep 17 00:00:00 2001 From: hdyson Date: Tue, 28 Aug 2018 12:34:05 +0100 Subject: [PATCH 23/62] Update pp save rules to handle time means (#3132) --- lib/iris/fileformats/pp_save_rules.py | 9 +- lib/iris/tests/test_cube_to_pp.py | 6 +- .../tests/unit/fileformats/pp/test_save.py | 140 +++++++++++++++++- 3 files changed, 150 insertions(+), 5 deletions(-) diff --git a/lib/iris/fileformats/pp_save_rules.py b/lib/iris/fileformats/pp_save_rules.py index 84b4980647..5323311061 100644 --- a/lib/iris/fileformats/pp_save_rules.py +++ b/lib/iris/fileformats/pp_save_rules.py @@ -174,7 +174,14 @@ def _general_time_rules(cube, pp): if (time_coord is not None and time_coord.has_bounds() and clim_season_coord is None and - (fp_coord is not None or frt_coord is not None) and + cm_time_mean is not None): + pp.lbtim.ib = 2 + pp.t1 = time_coord.units.num2date(time_coord.bounds[0, 0]) + pp.t2 = time_coord.units.num2date(time_coord.bounds[0, 1]) + + if (time_coord is not None and + time_coord.has_bounds() and + clim_season_coord is None and cm_time_mean is not None and cm_time_mean.intervals != () and cm_time_mean.intervals[0].endswith('hour')): diff --git a/lib/iris/tests/test_cube_to_pp.py b/lib/iris/tests/test_cube_to_pp.py index fa78f95166..46045fb55d 100644 --- a/lib/iris/tests/test_cube_to_pp.py +++ b/lib/iris/tests/test_cube_to_pp.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2010 - 2017, Met Office +# (C) British Crown Copyright 2010 - 2018, Met Office # # This file is part of Iris. # @@ -294,8 +294,8 @@ def test_lbvc(self): self.assertEqual(field.lbvc, lbvc) self.assertEqual(field.lblev, lblev) self.assertEqual(field.blev, blev) - - + + def fields_from_cube(cubes): """ Return an iterator of PP fields generated from saving the given cube(s) diff --git a/lib/iris/tests/unit/fileformats/pp/test_save.py b/lib/iris/tests/unit/fileformats/pp/test_save.py index 0784132915..081e01fcf9 100644 --- a/lib/iris/tests/unit/fileformats/pp/test_save.py +++ b/lib/iris/tests/unit/fileformats/pp/test_save.py @@ -19,6 +19,9 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa +import cftime +import cf_units + # Import iris.tests first so that some things can be initialised before # importing anything else. import iris.tests as tests @@ -26,7 +29,7 @@ from iris.coords import DimCoord, CellMethod from iris.fileformats._ff_cross_references import STASH_TRANS import iris.fileformats.pp as pp -from iris.fileformats.pp_save_rules import _lbproc_rules +from iris.fileformats.pp_save_rules import _lbproc_rules, verify from iris.tests import mock import iris.tests.stock as stock @@ -198,5 +201,140 @@ def test_maximum(self): self.assertEqual(lbproc, 8192) +class TestTimeMean(tests.IrisTest): + ''' + Tests that time mean cell method is converted to pp appropriately. + + Pattern is pairs of tests - one with time mean method, and one without, to + show divergent behaviour. + + ''' + def test_t1_time_mean(self): + cube = _get_single_time_cube(set_time_mean=True) + tc = cube.coord(axis='t') + expected = tc.units.num2date(0) + + with mock.patch('iris.fileformats.pp.PPField3', + autospec=True) as pp_field: + verify(cube, pp_field) + actual = pp_field.t1 + + self.assertEqual(expected, actual) + + def test_t1_no_time_mean(self): + cube = _get_single_time_cube() + tc = cube.coord(axis='t') + expected = tc.units.num2date(15) + + with mock.patch('iris.fileformats.pp.PPField3', + autospec=True) as pp_field: + verify(cube, pp_field) + actual = pp_field.t1 + + self.assertEqual(expected, actual) + + def test_t2_time_mean(self): + cube = _get_single_time_cube(set_time_mean=True) + tc = cube.coord(axis='t') + expected = tc.units.num2date(30) + + with mock.patch('iris.fileformats.pp.PPField3', + autospec=True) as pp_field: + verify(cube, pp_field) + actual = pp_field.t2 + + self.assertEqual(expected, actual) + + def test_t2_no_time_mean(self): + cube = _get_single_time_cube(set_time_mean=False) + expected = cftime.datetime(0, 0, 0) + + with mock.patch('iris.fileformats.pp.PPField3', + autospec=True) as pp_field: + verify(cube, pp_field) + actual = pp_field.t2 + self.assertEqual(expected, actual) + + def test_lbft_no_forecast_time(self): + # Different pattern here: checking that lbft hasn't been changed from + # the default value. + cube = _get_single_time_cube() + mock_lbft = mock.sentinel.lbft + + with mock.patch('iris.fileformats.pp.PPField3', + autospec=True) as pp_field: + pp_field.lbft = mock_lbft + verify(cube, pp_field) + actual = pp_field.lbft + + assert(mock_lbft is actual) + + def test_lbtim_no_time_mean(self): + cube = _get_single_time_cube() + expected_ib = 0 + expected_ic = 2 # 360 day calendar + + with mock.patch('iris.fileformats.pp.PPField3', + autospec=True) as pp_field: + verify(cube, pp_field) + actual_ib = pp_field.lbtim.ib + actual_ic = pp_field.lbtim.ic + + self.assertEqual(expected_ib, actual_ib) + self.assertEqual(expected_ic, actual_ic) + + def test_lbtim_time_mean(self): + cube = _get_single_time_cube(set_time_mean=True) + expected_ib = 2 # Time mean + expected_ic = 2 # 360 day calendar + + with mock.patch('iris.fileformats.pp.PPField3', + autospec=True) as pp_field: + verify(cube, pp_field) + actual_ib = pp_field.lbtim.ib + actual_ic = pp_field.lbtim.ic + + self.assertEqual(expected_ib, actual_ib) + self.assertEqual(expected_ic, actual_ic) + + def test_lbproc_no_time_mean(self): + cube = _get_single_time_cube() + expected = 0 + + with mock.patch('iris.fileformats.pp.PPField3', + autospec=True) as pp_field: + verify(cube, pp_field) + actual = pp_field.lbproc + + self.assertEqual(expected, actual) + + def test_lbproc_time_mean(self): + cube = _get_single_time_cube(set_time_mean=True) + expected = 128 + + with mock.patch('iris.fileformats.pp.PPField3', + autospec=True) as pp_field: + verify(cube, pp_field) + actual = pp_field.lbproc + + self.assertEqual(expected, actual) + + +def _get_single_time_cube(set_time_mean=False): + cube = stock.realistic_3d()[0:1, :, :] + cube.remove_coord('time') + cube.remove_coord('forecast_period') + tc = DimCoord( + points=[15, ], + standard_name='time', + units=cf_units.Unit('days since epoch', calendar='360_day'), + bounds=[[0, 30], ], + ) + cube.add_dim_coord(tc, 0) + if set_time_mean: + cube.cell_methods = (CellMethod("mean", coords='time'), ) + return cube + + if __name__ == "__main__": tests.main() From 5de040fc60c2cad69ce8e8c9cc3d7bd4c1569bfb Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Wed, 29 Aug 2018 10:35:06 +0100 Subject: [PATCH 24/62] Vector plots 2 (#3120) * Small improvements; first sensible tests. * Enhanced testing; better checking and crs awareness in grid_angles routine. * Remove crud from test_gridcell_angles. * Use degree units for everything in _grid_angles. * Make assertArrayAllClose print details when it fails. * Rework and extend testing for gridcell_angles. * Fix assertArrayAllClose; remove debug code from test_gridcell_angles. * Remove obsolete assignments. * Remove obsolete code. * Small comment improvements. * Attempt to clarify docstrings of low-level routines. * More tests, and some functional fixes. * Codestyle fixes. * Review changes + fixes. * Avoid using sample data. --- lib/iris/analysis/_grid_angles.py | 281 +++++++++------ lib/iris/analysis/cartography.py | 5 +- lib/iris/tests/__init__.py | 18 +- lib/iris/tests/stock/_stock_2d_latlons.py | 16 +- .../cartography/test_gridcell_angles.py | 327 ++++++++++++++++++ 5 files changed, 526 insertions(+), 121 deletions(-) create mode 100644 lib/iris/tests/unit/analysis/cartography/test_gridcell_angles.py diff --git a/lib/iris/analysis/_grid_angles.py b/lib/iris/analysis/_grid_angles.py index ffe6d51c7f..90876840af 100644 --- a/lib/iris/analysis/_grid_angles.py +++ b/lib/iris/analysis/_grid_angles.py @@ -24,6 +24,7 @@ import numpy as np +import cartopy.crs as ccrs import iris @@ -33,12 +34,16 @@ def _3d_xyz_from_latlon(lon, lat): Args: - * lon, lat: (arrays in degrees) + * lon, lat: (float array) + Arrays of longitudes and latitudes, in degrees. + Both the same shape. Returns: - xyz : (array, dtype=float64) - cartesian coordinates on a unit sphere. Dimension 0 maps x,y,z. + * xyz : (array, dtype=float64) + Cartesian coordinates on a unit sphere. + Shape is (3, ). + The x / y / z coordinates are in xyz[0 / 1 / 2]. """ lon1 = np.deg2rad(lon).astype(np.float64) @@ -60,91 +65,87 @@ def _latlon_from_xyz(xyz): Args: * xyz: (array) - positions array, of dims (3, ), where index 0 maps x/y/z. + Array of 3-D cartesian coordinates. + Shape (3, ). + x / y / z values are in xyz[0 / 1 / 2], Returns: - lonlat : (array) - spherical angles, of dims (2, ), in radians. - Dim 0 maps longitude, latitude. + * lonlat : (array) + longitude and latitude position angles, in degrees. + Shape (2, ). + The longitudes / latitudes are in lonlat[0 / 1]. """ - lons = np.arctan2(xyz[1], xyz[0]) - axial_radii = np.sqrt(xyz[0] * xyz[0] + xyz[1] * xyz[1]) - lats = np.arctan2(xyz[2], axial_radii) + lons = np.rad2deg(np.arctan2(xyz[1], xyz[0])) + radii = np.sqrt(np.sum(xyz * xyz, axis=0)) + lats = np.rad2deg(np.arcsin(xyz[2] / radii)) return np.array([lons, lats]) def _angle(p, q, r): """ - Return angle (in _radians_) of grid wrt local east. - Anticlockwise +ve, as usual. - {P, Q, R} are consecutive points in the same row, - eg {v(i,j),f(i,j),v(i+1,j)}, or {T(i-1,j),T(i,j),T(i+1,j)} - Calculate dot product of PR with lambda_hat at Q. - This gives us cos(required angle). - Disciminate between +/- angles by comparing latitudes of P and R. - p, q, r, are all 2-element arrays [lon, lat] of angles in degrees. + Estimate grid-angles to true-Eastward direction from positions in the same + grid row, but at increasing column (grid-Eastward) positions. + + {P, Q, R} are locations of consecutive points in the same grid row. + These could be successive points in a single grid, + e.g. {T(i-1,j), T(i,j), T(i+1,j)} + or a mixture of U/V and T gridpoints if row positions are aligned, + e.g. {v(i,j), f(i,j), v(i+1,j)}. + + Method: + + Calculate dot product of a unit-vector parallel to P-->R, unit-scaled, + with the unit eastward (true longitude) vector at Q. + This value is cos(required angle). + Discriminate between +/- angles by comparing latitudes of P and R. + Return NaN where any P-->R are zero. + + .. NOTE:: + + This method assumes that the vector PR is parallel to the surface + at the longitude of Q, as it uses the length of PR as the basis for + the cosine ratio. + That is only exact when Q is at the same longitude as the midpoint + of PR, and this typically causes errors which grow with increasing + gridcell angle. + However, we retain this method because it reproduces the "standard" + gridcell-orientation-angle arrays found in files output by the CICE + model, which presumably uses an equivalent calculation. + + Args: + + * p, q, r : (float array) + Arrays of angles, in degrees. + All the same shape. + Shape is (2, ). + Longitudes / latitudes are in array[0 / 1]. + + Returns: + + * angle : (float array) + Grid angles relative to true-East, in degrees. + Positive when grid-East is anticlockwise from true-East. + Shape is same as . """ -# old_style = True - old_style = False - if old_style: - mid_lons = np.deg2rad(q[0]) + mid_lons = np.deg2rad(q[0]) - pr = _3d_xyz_from_latlon(r[0], r[1]) - _3d_xyz_from_latlon(p[0], p[1]) - pr_norm = np.sqrt(np.sum(pr**2, axis=0)) - pr_top = pr[1] * np.cos(mid_lons) - pr[0] * np.sin(mid_lons) + pr = _3d_xyz_from_latlon(r[0], r[1]) - _3d_xyz_from_latlon(p[0], p[1]) + pr_norm = np.sqrt(np.sum(pr**2, axis=0)) + pr_top = pr[1] * np.cos(mid_lons) - pr[0] * np.sin(mid_lons) - index = pr_norm == 0 - pr_norm[index] = 1 + index = pr_norm == 0 + pr_norm[index] = 1 - cosine = np.maximum(np.minimum(pr_top / pr_norm, 1), -1) - cosine[index] = 0 + cosine = np.maximum(np.minimum(pr_top / pr_norm, 1), -1) + cosine[index] = 0 - psi = np.arccos(cosine) * np.sign(r[1] - p[1]) - psi[index] = np.nan - else: - # Calculate unit vectors. - midpt_lons, midpt_lats = q[0], q[1] - lmb_r, phi_r = (np.deg2rad(arr) for arr in (midpt_lons, midpt_lats)) - phi_hatvec_x = -np.sin(phi_r) * np.cos(lmb_r) - phi_hatvec_y = -np.sin(phi_r) * np.sin(lmb_r) - phi_hatvec_z = np.cos(phi_r) - shape_xyz = (1,) + midpt_lons.shape - phi_hatvec = np.concatenate([arr.reshape(shape_xyz) - for arr in (phi_hatvec_x, - phi_hatvec_y, - phi_hatvec_z)]) - lmb_hatvec_z = np.zeros(midpt_lons.shape) - lmb_hatvec_y = np.cos(lmb_r) - lmb_hatvec_x = -np.sin(lmb_r) - lmb_hatvec = np.concatenate([arr.reshape(shape_xyz) - for arr in (lmb_hatvec_x, - lmb_hatvec_y, - lmb_hatvec_z)]) - - pr = _3d_xyz_from_latlon(r[0], r[1]) - _3d_xyz_from_latlon(p[0], p[1]) - - # Dot products to form true-northward / true-eastward projections. - pr_cmpt_e = np.sum(pr * lmb_hatvec, axis=0) - pr_cmpt_n = np.sum(pr * phi_hatvec, axis=0) - psi = np.arctan2(pr_cmpt_n, pr_cmpt_e) - - # TEMPORARY CHECKS: - # ensure that the two unit vectors are perpendicular. - dotprod = np.sum(phi_hatvec * lmb_hatvec, axis=0) - assert np.allclose(dotprod, 0.0) - # ensure that the vector components carry the original magnitude. - mag_orig = np.sum(pr * pr) - mag_rot = np.sum(pr_cmpt_e * pr_cmpt_e) + np.sum(pr_cmpt_n * pr_cmpt_n) - rtol = 1.e-3 - check = np.allclose(mag_rot, mag_orig, rtol=rtol) - if not check: - print(mag_rot, mag_orig) - assert np.allclose(mag_rot, mag_orig, rtol=rtol) - - return psi + psi = np.arccos(cosine) * np.sign(r[1] - p[1]) + psi[index] = np.nan + + return np.rad2deg(psi) def gridcell_angles(x, y=None, cell_angle_boundpoints='mid-lhs, mid-rhs'): @@ -201,12 +202,18 @@ def gridcell_angles(x, y=None, cell_angle_boundpoints='mid-lhs, mid-rhs'): angles : (2-dimensional cube) Cube of angles of grid-x vector from true Eastward direction for - each gridcell, in radians. - It also has longitude and latitude coordinates. If coordinates - were input the output has identical ones : If the input was 2d - arrays, the output coords have no bounds; or, if the input was 3d - arrays, the output coords have bounds and centrepoints which are - the average of the 4 bounds. + each gridcell, in degrees. + It also has "true" longitude and latitude coordinates, with no + coordinate system. + When the input has coords, then the output ones are identical if + the inputs are true-latlons, otherwise they are transformed + true-latlon versions. + When the input has bounded coords, then the output coords have + matching bounds and centrepoints (possibly transformed). + When the input is 2d arrays, or has unbounded coords, then the + output coords have matching points and no bounds. + When the input is 3d arrays, then the output coords have matching + bounds, and the centrepoints are an average of the 4 boundpoints. """ cube = None @@ -216,55 +223,105 @@ def gridcell_angles(x, y=None, cell_angle_boundpoints='mid-lhs, mid-rhs'): x, y = cube.coord(axis='x'), cube.coord(axis='y') # Now should have either 2 coords or 2 arrays. - if not hasattr(x, 'shape') and hasattr(y, 'shape'): + if not hasattr(x, 'shape') or not hasattr(y, 'shape'): msg = ('Inputs (x,y) must have array shape property.' 'Got type(x)={} and type(y)={}.') raise ValueError(msg.format(type(x), type(y))) x_coord, y_coord = None, None if hasattr(x, 'bounds') and hasattr(y, 'bounds'): + # x and y are Coords. x_coord, y_coord = x.copy(), y.copy() - x_coord.convert_units('degrees') - y_coord.convert_units('degrees') + + # They must be angles : convert into degrees + for coord in (x_coord, y_coord): + if not coord.units.is_convertible('degrees'): + msg = ('Input X and Y coordinates must have angular ' + 'units. Got units of "{!s}" and "{!s}".') + raise ValueError(msg.format(x_coord.units, y_coord.units)) + coord.convert_units('degrees') + if x_coord.ndim != 2 or y_coord.ndim != 2: - msg = ('Coordinate inputs must have 2-dimensional shape. ', + msg = ('Coordinate inputs must have 2-dimensional shape. ' 'Got x-shape of {} and y-shape of {}.') raise ValueError(msg.format(x_coord.shape, y_coord.shape)) if x_coord.shape != y_coord.shape: - msg = ('Coordinate inputs must have same shape. ', + msg = ('Coordinate inputs must have same shape. ' 'Got x-shape of {} and y-shape of {}.') raise ValueError(msg.format(x_coord.shape, y_coord.shape)) -# NOTE: would like to check that dims are in correct order, but can't do that -# if there is no cube. -# TODO: **document** -- another input format requirement -# x_dims, y_dims = (cube.coord_dims(co) for co in (x_coord, y_coord)) -# if x_dims != (0, 1) or y_dims != (0, 1): -# msg = ('Coordinate inputs must map to cube dimensions (0, 1). ', -# 'Got x-dims of {} and y-dims of {}.') -# raise ValueError(msg.format(x_dims, y_dims)) + if cube: + x_dims, y_dims = (cube.coord_dims(co) for co in (x, y)) + if x_dims != y_dims: + msg = ('X and Y coordinates must have the same cube ' + 'dimensions. Got x-dims = {} and y-dims = {}.') + raise ValueError(msg.format(x_dims, y_dims)) + cs = x_coord.coord_system + if y_coord.coord_system != cs: + msg = ('Coordinate inputs must have same coordinate system. ' + 'Got x of {} and y of {}.') + raise ValueError(msg.format(cs, y_coord.coord_system)) + + # Base calculation on bounds if we have them, or points as a fallback. if x_coord.has_bounds() and y_coord.has_bounds(): x, y = x_coord.bounds, y_coord.bounds else: x, y = x_coord.points, y_coord.points + # Make sure these arrays are ordinary lats+lons, in degrees. + if cs is not None: + # Transform points into true lats + lons. + crs_src = cs.as_cartopy_crs() + crs_pc = ccrs.PlateCarree() + + def transform_xy_arrays(x, y): + # Note: flatten, as transform_points is limited to 2D arrays. + shape = x.shape + x, y = (arr.flatten() for arr in (x, y)) + pts = crs_pc.transform_points(crs_src, x, y) + x = pts[..., 0].reshape(shape) + y = pts[..., 1].reshape(shape) + return x, y + + # Transform the main reference points into standard lats+lons. + x, y = transform_xy_arrays(x, y) + + # Likewise replace the original coordinates with transformed ones, + # because the calculation also needs the centrepoint values. + xpts, ypts = (coord.points for coord in (x_coord, y_coord)) + xbds, ybds = (coord.bounds for coord in (x_coord, y_coord)) + xpts, ypts = transform_xy_arrays(xpts, ypts) + xbds, ybds = transform_xy_arrays(xbds, ybds) + x_coord = iris.coords.AuxCoord( + points=xpts, bounds=xbds, + standard_name='longitude', units='degrees') + y_coord = iris.coords.AuxCoord( + points=ypts, bounds=ybds, + standard_name='latitude', units='degrees') + elif hasattr(x, 'bounds') or hasattr(y, 'bounds'): # One was a Coord, and the other not ? is_and_not = ('x', 'y') if hasattr(y, 'bounds'): is_and_not = reversed(is_and_not) msg = 'Input {!r} is a Coordinate, but {!r} is not.' - raise ValueError(*is_and_not) + raise ValueError(msg.format(*is_and_not)) - # Now have either 2 points arrays or 2 bounds arrays. - # Construct (lhs, mid, rhs) where these represent 3 adjacent points with - # increasing longitudes. + # Now have either 2 points arrays (ny, nx) or 2 bounds arrays (ny, nx, 4). + # Construct (lhs, mid, rhs) where these represent 3 points at increasing + # grid-x indices (columns). + # Also make suitable X and Y coordinates for the result cube. if x.ndim == 2: - # PROBLEM: we can't use this if data is not full-longitudes, - # i.e. rhs of array must connect to lhs (aka 'circular' coordinate). - # But we have no means of checking that ? + # Data is points arrays. + # Use previous + subsequent points along grid-x-axis as references. + + # PROBLEM: we assume that the rhs connects to the lhs, so we should + # really only use this if data is full-longitudes (as a 'circular' + # coordinate). + # This is mentioned in the docstring, but we have no general means of + # checking it. - # Use previous + subsequent points along longitude-axis as references. - # NOTE: we also have no way to check that dim #2 really is the 'X' dim. + # NOTE: we take the 2d grid as presented, so the second dimension is + # the 'X' dim. Again, that is implicit + can't be checked. mid = np.array([x, y]) lhs = np.roll(mid, 1, 2) rhs = np.roll(mid, -1, 2) @@ -275,9 +332,11 @@ def gridcell_angles(x, y=None, cell_angle_boundpoints='mid-lhs, mid-rhs'): x_coord = iris.coords.AuxCoord(y, standard_name='longitude', units='degrees') else: - # Get lhs and rhs locations by averaging top+bottom each side. + # Data is bounds arrays. + # Use gridcell corners at different grid-x positions as references. # NOTE: so with bounds, we *don't* need full circular longitudes. xyz = _3d_xyz_from_latlon(x, y) + # Support two different choices of reference points locations. angle_boundpoints_vals = {'mid-lhs, mid-rhs': '03_to_12', 'lower-left, lower-right': '0_to_1'} bounds_pos = angle_boundpoints_vals.get(cell_angle_boundpoints) @@ -294,8 +353,8 @@ def gridcell_angles(x, y=None, cell_angle_boundpoints='mid-lhs, mid-rhs'): list(angle_boundpoints_vals.keys()))) if not x_coord: # Create bounded coords for result cube. - # Use average lhs+rhs points in 3d to get 'mid' points, as coords - # with no points are not allowed. + # Use average of lhs+rhs points in 3d to get 'mid' points, + # as coords without points are not allowed. mid_xyz = 0.5 * (lhs_xyz + rhs_xyz) mid_latlons = _latlon_from_xyz(mid_xyz) # Create coords with given bounds, and averaged centrepoints. @@ -305,28 +364,30 @@ def gridcell_angles(x, y=None, cell_angle_boundpoints='mid-lhs, mid-rhs'): y_coord = iris.coords.AuxCoord( points=mid_latlons[1], bounds=y, standard_name='latitude', units='degrees') + # Convert lhs and rhs points back to latlon form -- IN DEGREES ! - lhs = np.rad2deg(_latlon_from_xyz(lhs_xyz)) - rhs = np.rad2deg(_latlon_from_xyz(rhs_xyz)) - # mid is coord.points, whether input or made up. + lhs = _latlon_from_xyz(lhs_xyz) + rhs = _latlon_from_xyz(rhs_xyz) + # 'mid' is coord.points, whether from input or just made up. mid = np.array([x_coord.points, y_coord.points]) # Do the angle calcs, and return as a suitable cube. angles = _angle(lhs, mid, rhs) result = iris.cube.Cube(angles, long_name='gridcell_angle_from_true_east', - units='radians') + units='degrees') result.add_aux_coord(x_coord, (0, 1)) result.add_aux_coord(y_coord, (0, 1)) return result -def true_vectors_from_grid_vectors(u_cube, v_cube, - grid_angles_cube=None, - grid_angles_kwargs=None): +def rotate_grid_vectors(u_cube, v_cube, grid_angles_cube=None, + grid_angles_kwargs=None): """ Rotate distance vectors from grid-oriented to true-latlon-oriented. + Can also rotate by arbitrary angles, if they are passed in. + .. Note:: This operation overlaps somewhat in function with diff --git a/lib/iris/analysis/cartography.py b/lib/iris/analysis/cartography.py index dadcdedc86..a778f5f846 100644 --- a/lib/iris/analysis/cartography.py +++ b/lib/iris/analysis/cartography.py @@ -37,10 +37,7 @@ import iris.coord_systems import iris.exceptions from iris.util import _meshgrid -from ._grid_angles import ( - gridcell_angles, - true_vectors_from_grid_vectors as rotate_grid_vectors) - +from ._grid_angles import gridcell_angles, rotate_grid_vectors # List of contents to control Sphinx autodocs. # Unfortunately essential to get docs for the grid_angles functions. diff --git a/lib/iris/tests/__init__.py b/lib/iris/tests/__init__.py index 710a3fbac6..7550fce1ca 100644 --- a/lib/iris/tests/__init__.py +++ b/lib/iris/tests/__init__.py @@ -663,7 +663,23 @@ def assertArrayAllClose(self, a, b, rtol=1.0e-7, atol=0.0, **kwargs): For full details see underlying routine numpy.testing.assert_allclose. """ - np.testing.assert_allclose(a, b, rtol=rtol, atol=atol, **kwargs) + ok = np.allclose(a, b, atol=atol, rtol=rtol, equal_nan=True) + if not ok: + # Calculate errors above a pointwise tolerance : The method is + # taken from "numpy.core.numeric.isclose". + errors = (np.abs(a-b) - atol + rtol * np.abs(b)) + worst_inds = np.unravel_index(np.argmax(errors.flat), errors.shape) + # Build a more useful message than from np.testing.assert_allclose. + msg = ('\nARRAY CHECK FAILED "assertArrayAllClose" :' + '\n with shapes={} {}, atol={}, rtol={}' + '\n worst at element {} : a={} b={}' + '\n absolute error ~{:.3g}, equivalent to rtol ~{:.3e}') + aval, bval = a[worst_inds], b[worst_inds] + absdiff = np.abs(aval - bval) + equiv_rtol = absdiff / bval + raise AssertionError(msg.format( + a.shape, b.shape, atol, rtol, worst_inds, aval, bval, absdiff, + equiv_rtol)) @contextlib.contextmanager def temp_filename(self, suffix=''): diff --git a/lib/iris/tests/stock/_stock_2d_latlons.py b/lib/iris/tests/stock/_stock_2d_latlons.py index 75b62f31ba..395a50d8ea 100644 --- a/lib/iris/tests/stock/_stock_2d_latlons.py +++ b/lib/iris/tests/stock/_stock_2d_latlons.py @@ -233,18 +233,19 @@ def sample_cube(xargs, yargs): cube.add_dim_coord(co_x, 1) return cube + # Start by making a "normal" cube with separate 1-D X and Y coords. if regional: - # Extract small region. + # Make a small regional cube. cube = sample_cube(xargs=(150., 243.75, 6), yargs=(-10., 40., 5)) - # Make contiguous bounds. + # Add contiguous bounds. for ax in ('x', 'y'): cube.coord(axis=ax).guess_bounds() else: - # Global data, but drastically reduced resolution. + # Global data, but at a drastically reduced resolution. cube = sample_cube(xargs=(37.5, 318.75, 6), yargs=(-85., 65., 5)) - # Patch bounds to ensure it is still contiguous + global. + # Make contiguous bounds and adjust outer edges to ensure it is global. for name in ('longitude', 'latitude'): coord = cube.coord(name) coord.guess_bounds() @@ -258,8 +259,11 @@ def sample_cube(xargs, yargs): bds[-1, 1] = 90.0 coord.bounds = bds - # Get 1d coordinate points + bounds + calculate 2d equivalents. + # Now convert the 1-d coords to 2-d equivalents. + # Get original 1-d coords. co_1d_x, co_1d_y = [cube.coord(axis=ax).copy() for ax in ('x', 'y')] + + # Calculate 2-d equivalents. co_2d_x, co_2d_y = grid_coords_2d_from_1d(co_1d_x, co_1d_y) # Remove the old grid coords. @@ -271,7 +275,7 @@ def sample_cube(xargs, yargs): cube.add_aux_coord(coord, (0, 1)) if transformed or rotated: - # Take the lats + lons as being in a rotated coord system. + # Put the cube locations into a rotated coord system. pole_lat, pole_lon = 75.0, 120.0 if transformed: # Reproject coordinate values from rotated to true lat-lons. diff --git a/lib/iris/tests/unit/analysis/cartography/test_gridcell_angles.py b/lib/iris/tests/unit/analysis/cartography/test_gridcell_angles.py new file mode 100644 index 0000000000..4a1344e83d --- /dev/null +++ b/lib/iris/tests/unit/analysis/cartography/test_gridcell_angles.py @@ -0,0 +1,327 @@ +# (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 . +""" +Unit tests for the function +:func:`iris.analysis.cartography.gridcell_angles`. + +""" +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 cf_units import Unit +from iris.cube import Cube +from iris.coords import AuxCoord + +from iris.analysis.cartography import gridcell_angles +from iris.tests.stock import sample_2d_latlons, lat_lon_cube + + +def _2d_multicells_testcube(cellsize_degrees=1.0): + """ + Create a test cube with a grid of X and Y points, where each gridcell + is independent (disjoint), arranged at an angle == the x-coord point. + + """ + # Setup np.linspace arguments to make the coordinate points. + x0, x1, nx = -164, 164, 9 + y0, y1, ny = -75, 75, 7 + + lats = np.linspace(y0, y1, ny, endpoint=True) + lons_angles = np.linspace(x0, x1, nx, endpoint=True) + x_pts_2d, y_pts_2d = np.meshgrid(lons_angles, lats) + + # Make gridcells rectangles surrounding these centrepoints, but also + # tilted at various angles (= same as x-point lons, as that's easy). + + # Calculate centrepoint lons+lats : in radians, and shape (ny, nx, 1). + xangs, yangs = np.deg2rad(x_pts_2d), np.deg2rad(y_pts_2d) + xangs, yangs = [arr[..., None] for arr in (xangs, yangs)] + # Program which corners are up+down on each gridcell axis. + dx_corners = [[[-1, 1, 1, -1]]] + dy_corners = [[[-1, -1, 1, 1]]] + # Calculate the relative offsets in x+y at the 4 corners. + x_ofs_2d = cellsize_degrees * np.cos(xangs) * dx_corners + x_ofs_2d -= cellsize_degrees * np.sin(xangs) * dy_corners + y_ofs_2d = cellsize_degrees * np.cos(xangs) * dy_corners + y_ofs_2d += cellsize_degrees * np.sin(xangs) * dx_corners + # Apply a latitude stretch to make correct angles on the globe. + y_ofs_2d *= np.cos(yangs) + # Make bounds arrays by adding the corner offsets to the centrepoints. + x_bds_2d = x_pts_2d[..., None] + x_ofs_2d + y_bds_2d = y_pts_2d[..., None] + y_ofs_2d + + # Create a cube with these points + bounds in its 'X' and 'Y' coords. + co_x = AuxCoord(points=x_pts_2d, bounds=x_bds_2d, + standard_name='longitude', units='degrees') + co_y = AuxCoord(points=y_pts_2d, bounds=y_bds_2d, + standard_name='latitude', units='degrees') + cube = Cube(np.zeros((ny, nx))) + cube.add_aux_coord(co_x, (0, 1)) + cube.add_aux_coord(co_y, (0, 1)) + return cube + + +class TestGridcellAngles(tests.IrisTest): + def setUp(self): + # Make a small "normal" contiguous-bounded cube to test on. + # This one is regional. + self.standard_regional_cube = sample_2d_latlons( + regional=True, transformed=True) + # Record the standard correct angle answers. + result_cube = gridcell_angles(self.standard_regional_cube) + result_cube.convert_units('degrees') + self.standard_result_cube = result_cube + self.standard_small_cube_results = result_cube.data + + def _check_multiple_orientations_and_latitudes(self, + method='mid-lhs, mid-rhs', + atol_degrees=0.005, + cellsize_degrees=1.0): + + cube = _2d_multicells_testcube(cellsize_degrees=cellsize_degrees) + + # Calculate gridcell angles at each point. + angles_cube = gridcell_angles(cube, cell_angle_boundpoints=method) + + # Check that the results are a close match to the original intended + # gridcell orientation angles. + # NOTE: neither the above gridcell construction nor the calculation + # itself are exact : Errors scale as the square of gridcell sizes. + angles_cube.convert_units('degrees') + angles_calculated = angles_cube.data + + # Note: the gridcell angles **should** just match the longitudes at + # each point + angles_expected = cube.coord('longitude').points + + # Wrap both into standard range for comparison. + angles_calculated = (angles_calculated + 360.) % 360. + angles_expected = (angles_expected + 360.) % 360. + + # Assert (toleranced) equality, and return results. + self.assertArrayAllClose(angles_calculated, angles_expected, + atol=atol_degrees) + + return angles_calculated, angles_expected + + def test_various_orientations_and_locations(self): + self._check_multiple_orientations_and_latitudes() + + def test_result_form(self): + # Check properties of the result cube *other than* the data values. + test_cube = self.standard_regional_cube + result_cube = self.standard_result_cube + self.assertEqual(result_cube.long_name, + 'gridcell_angle_from_true_east') + self.assertEqual(result_cube.units, Unit('degrees')) + self.assertEqual(len(result_cube.coords()), 2) + self.assertEqual(result_cube.coord(axis='x'), + test_cube.coord(axis='x')) + self.assertEqual(result_cube.coord(axis='y'), + test_cube.coord(axis='y')) + + def test_bottom_edge_method(self): + # Get results with the "other" calculation method + check to tolerance. + # A smallish cellsize should yield similar results in both cases. + r1, _ = self._check_multiple_orientations_and_latitudes() + r2, _ = self._check_multiple_orientations_and_latitudes( + method='lower-left, lower-right', + cellsize_degrees=0.1, atol_degrees=0.1) + + # Not *exactly* the same : this checks we tested the 'other' method ! + self.assertFalse(np.allclose(r1, r2)) + # Note: results are a bit different in places. This is acceptable. + self.assertArrayAllClose(r1, r2, atol=0.1) + + def test_bounded_coord_args(self): + # Check that passing the coords gives the same result as the cube. + co_x, co_y = (self.standard_regional_cube.coord(axis=ax) + for ax in ('x', 'y')) + result = gridcell_angles(co_x, co_y) + self.assertArrayAllClose(result.data, + self.standard_small_cube_results) + + def test_coords_radians_args(self): + # Check it still works with coords converted to radians. + co_x, co_y = (self.standard_regional_cube.coord(axis=ax) + for ax in ('x', 'y')) + for coord in (co_x, co_y): + coord.convert_units('radians') + result = gridcell_angles(co_x, co_y) + self.assertArrayAllClose(result.data, + self.standard_small_cube_results) + + def test_bounds_array_args(self): + # Check we can calculate from bounds values alone. + co_x, co_y = (self.standard_regional_cube.coord(axis=ax) + for ax in ('x', 'y')) + # Results drawn from coord bounds should be nearly the same, + # but not exactly, because of the different 'midpoint' values. + result = gridcell_angles(co_x.bounds, co_y.bounds) + self.assertArrayAllClose(result.data, + self.standard_small_cube_results, atol=0.1) + + def test_unbounded_regional_coord_args(self): + # Remove the coord bounds to check points-based calculation. + co_x, co_y = (self.standard_regional_cube.coord(axis=ax) + for ax in ('x', 'y')) + for coord in (co_x, co_y): + coord.bounds = None + result = gridcell_angles(co_x, co_y) + # Note: in this case, we can expect the leftmost and rightmost columns + # to be rubbish, because the data is not global. + # But the rest should match okay. + self.assertArrayAllClose(result.data[:, 1:-1], + self.standard_small_cube_results[:, 1:-1]) + + def test_points_array_args(self): + # Check we can calculate from points arrays alone (no coords). + co_x, co_y = (self.standard_regional_cube.coord(axis=ax) + for ax in ('x', 'y')) + # As previous, the leftmost and rightmost columns are not good. + result = gridcell_angles(co_x.points, co_y.points) + self.assertArrayAllClose(result.data[:, 1:-1], + self.standard_small_cube_results[:, 1:-1]) + + def test_unbounded_global(self): + # For a contiguous global grid, a result based on points, i.e. with the + # bounds removed, should be a reasonable match for the 'ideal' one + # based on the bounds. + + # Make a global cube + calculate ideal bounds-based results. + global_cube = sample_2d_latlons(transformed=True) + result_cube = gridcell_angles(global_cube) + result_cube.convert_units('degrees') + global_cube_results = result_cube.data + + # Check a points-based calculation on the same basic grid. + co_x, co_y = (global_cube.coord(axis=ax) + for ax in ('x', 'y')) + for coord in (co_x, co_y): + coord.bounds = None + result = gridcell_angles(co_x, co_y) + # In this case, the match is actually rather poor (!). + self.assertArrayAllClose(result.data, + global_cube_results, + atol=7.5) + # Leaving off first + last columns again gives a decent result. + self.assertArrayAllClose(result.data[:, 1:-1], + global_cube_results[:, 1:-1]) + + # NOTE: although this looks just as bad as 'test_points_array_args', + # maximum errors there in the end columns are actually > 100 degrees ! + + def test_nonlatlon_coord_system(self): + # Check with points specified in an unexpected coord system. + cube = sample_2d_latlons(regional=True, rotated=True) + result = gridcell_angles(cube) + self.assertArrayAllClose(result.data, + self.standard_small_cube_results) + # Check that the result has transformed (true-latlon) coordinates. + self.assertEqual(len(result.coords()), 2) + x_coord = result.coord(axis='x') + y_coord = result.coord(axis='y') + self.assertEqual(x_coord.shape, cube.shape) + self.assertEqual(y_coord.shape, cube.shape) + self.assertIsNotNone(cube.coord_system) + self.assertIsNone(x_coord.coord_system) + self.assertIsNone(y_coord.coord_system) + + def test_fail_coords_bad_units(self): + # Check error with bad coords units. + co_x, co_y = (self.standard_regional_cube.coord(axis=ax) + for ax in ('x', 'y')) + co_y.units = 'm' + with self.assertRaisesRegexp(ValueError, 'must have angular units'): + gridcell_angles(co_x, co_y) + + def test_fail_nonarraylike(self): + # Check error with bad args. + co_x, co_y = 1, 2 + with self.assertRaisesRegexp(ValueError, + 'must have array shape property'): + gridcell_angles(co_x, co_y) + + def test_fail_non2d_coords(self): + # Check error with bad args. + cube = lat_lon_cube() + with self.assertRaisesRegexp(ValueError, + 'inputs must have 2-dimensional shape'): + gridcell_angles(cube) + + def test_fail_different_shapes(self): + # Check error with mismatched shapes. + co_x, co_y = (self.standard_regional_cube.coord(axis=ax) + for ax in ('x', 'y')) + co_y = co_y[1:] + with self.assertRaisesRegexp(ValueError, 'must have same shape'): + gridcell_angles(co_x, co_y) + + def test_fail_different_coord_system(self): + # Check error with mismatched coord systems. + cube = sample_2d_latlons(regional=True, rotated=True) + cube.coord(axis='x').coord_system = None + with self.assertRaisesRegexp(ValueError, + 'must have same coordinate system'): + gridcell_angles(cube) + + def test_fail_cube_dims(self): + # Check error with mismatched cube dims. + cube = self.standard_regional_cube + # Make 5x6 into 5x5. + cube = cube[:, :-1] + co_x = cube.coord(axis='x') + pts, bds = co_x.points, co_x.bounds + co_new_x = co_x.copy(points=pts.transpose((1, 0)), + bounds=bds.transpose((1, 0, 2))) + cube.remove_coord(co_x) + cube.add_aux_coord(co_new_x, (1, 0)) + with self.assertRaisesRegexp(ValueError, + 'must have the same cube dimensions'): + gridcell_angles(cube) + + def test_fail_coord_noncoord(self): + # Check that passing a coord + an array gives an error. + co_x, co_y = (self.standard_regional_cube.coord(axis=ax) + for ax in ('x', 'y')) + with self.assertRaisesRegexp(ValueError, + 'is a Coordinate, but .* is not'): + gridcell_angles(co_x, co_y.bounds) + + def test_fail_noncoord_coord(self): + # Check that passing an array + a coord gives an error. + co_x, co_y = (self.standard_regional_cube.coord(axis=ax) + for ax in ('x', 'y')) + with self.assertRaisesRegexp(ValueError, + 'is a Coordinate, but .* is not'): + gridcell_angles(co_x.points, co_y) + + def test_fail_bad_method(self): + with self.assertRaisesRegexp(ValueError, + 'unrecognised cell_angle_boundpoints'): + self._check_multiple_orientations_and_latitudes( + method='something_unknown') + + +if __name__ == "__main__": + tests.main() From 2b6b7870e346d3eb2fecd89ae1d5d742d6e42338 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Fri, 31 Aug 2018 16:28:59 +0100 Subject: [PATCH 25/62] Temporary pin to avoid problems caused by Proj4 v5.1. (#3150) --- requirements/core.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements/core.txt b/requirements/core.txt index 997c91c665..e9abec5732 100644 --- a/requirements/core.txt +++ b/requirements/core.txt @@ -4,6 +4,7 @@ # Without these, iris won't even import. cartopy +#conda: proj4<5 cf_units>=2 cftime dask[array]==0.18.1 #conda: dask==0.18.1 From cb647615968b0bb07013543745205330af90210a Mon Sep 17 00:00:00 2001 From: lbdreyer Date: Fri, 31 Aug 2018 16:35:22 +0100 Subject: [PATCH 26/62] Extending ORCA blockplotting (w/ tests) (#3135) --- lib/iris/coords.py | 210 +++++++------ lib/iris/plot.py | 145 +++++---- lib/iris/tests/unit/coords/test_Coord.py | 293 ++++++++++++++++++ lib/iris/tests/unit/plot/test_2d_coords.py | 167 ---------- .../test__check_bounds_contiguity_and_mask.py | 96 ++++++ .../tests/unit/plot/test__get_plot_defn.py | 10 +- ...est__get_plot_defn_custom_coords_picked.py | 93 ++++++ 7 files changed, 694 insertions(+), 320 deletions(-) delete mode 100644 lib/iris/tests/unit/plot/test_2d_coords.py create mode 100644 lib/iris/tests/unit/plot/test__check_bounds_contiguity_and_mask.py create mode 100644 lib/iris/tests/unit/plot/test__get_plot_defn_custom_coords_picked.py diff --git a/lib/iris/coords.py b/lib/iris/coords.py index ff2f170a2f..42882fbc2c 100644 --- a/lib/iris/coords.py +++ b/lib/iris/coords.py @@ -143,81 +143,43 @@ def __new__(cls, name_or_coord, minimum, maximum, 'groupby_point, groupby_slice') -def _discontiguity_in_2d_bounds(bds, abs_tol=1e-4): +def _get_2d_coord_bound_grid(bounds): """ - Check bounds of a 2-dimensional coordinate are contiguous - Args: - bds: Array of bounds of shape (X,Y,4) - abs_tol: tolerance - - Returns: - Bool, if there are no discontinuities - absolute difference along the x axis - absolute difference along the y axis - - """ - # Check bds has the shape (ny, nx, 4) - if not bds.ndim == 3 and bds.shape[2] == 4: - raise ValueError('2D coordinates must have 4 bounds per point ' - 'for 2D coordinate plotting') - - # Check ordering: - # i i+1 - # j @0 @1 - # j+1 @3 @2 - def mod360_diff(x1, x2): - diff = x1 - x2 - diff = (diff + 360.0 + 180.0) % 360.0 - 180.0 - return diff - - # Compare cell with the cell next to it (i+1) - diffs_along_x = mod360_diff(bds[:, :-1, 1], bds[:, 1:, 0]) - # Compare cell with the cell above it (j+1) - diffs_along_y = mod360_diff(bds[:-1, :, 3], bds[1:, :, 0]) - - def eq_diffs(x1): - return np.all(np.abs(x1) < abs_tol) - - match_y0_x1 = eq_diffs(diffs_along_x) - match_y1_x0 = eq_diffs(diffs_along_y) + Creates a grid using the bounds of a 2D coordinate with 4 sided cells. - all_eq = match_y0_x1 and match_y1_x0 + Assumes that the four vertices of the cells are in an anti-clockwise order + (bottom-left, bottom-right, top-right, top-left). - return all_eq, np.abs(diffs_along_x), np.abs(diffs_along_y) - - -def _get_2d_coord_bound_grid(bds): - """ - Function used that takes a bounds array for a 2-D coordinate variable with - 4 sides and returns the bounds grid. - - Cf standards requires the four vertices of the cell to be traversed - anti-clockwise if the coordinates are defined in a right handed coordinate - system. - - selects the zeroth vertex of each cell and then adds the column the first - vertex at the end. For the top row it uses the thirs vertex, with the - second added on to the end. + Selects the zeroth vertex of each cell. A final column is added, which + contains the first vertex of the cells in the final column. A final row + is added, which contains the third vertex of all the cells in the final + row, except for in the final column where it uses the second vertex. e.g. # 0-0-0-0-1 # 0-0-0-0-1 # 3-3-3-3-2 - Args: - bounds: array of shape (X,Y,4) + * bounds: (array) + Coordinate bounds array of shape (Y, X, 4) Returns: - array of shape (X+1, Y+1) + * grid: (array) + Grid of shape (Y+1, X+1) """ - bds_shape = bds.shape - result = np.zeros((bds_shape[0] + 1, bds_shape[1] + 1)) + # Check bds has the shape (ny, nx, 4) + if not (bounds.ndim == 3 and bounds.shape[-1] == 4): + raise ValueError('Bounds for 2D coordinates must be 3-dimensional and ' + 'have 4 bounds per point.') + + bounds_shape = bounds.shape + result = np.zeros((bounds_shape[0] + 1, bounds_shape[1] + 1)) - result[:-1, :-1] = bds[:, :, 0] - result[:-1, -1] = bds[:, -1, 1] - result[-1, :-1] = bds[-1, :, 3] - result[-1, -1] = bds[-1, -1, 2] + result[:-1, :-1] = bounds[:, :, 0] + result[:-1, -1] = bounds[:, -1, 1] + result[-1, :-1] = bounds[-1, :, 3] + result[-1, -1] = bounds[-1, -1, 2] return result @@ -1029,29 +991,101 @@ def cells(self): """ return _CellIterator(self) - def _sanity_check_contiguous(self): + def _sanity_check_bounds(self): if self.ndim == 1: if self.nbounds != 2: - raise ValueError('Invalid operation for {!r}, with {} bounds. ' - 'Contiguous bounds are only defined for 1D ' - 'coordinates with 2 bounds.'.format - (self.name(), self.nbounds)) + raise ValueError('Invalid operation for {!r}, with {} ' + 'bound(s). Contiguous bounds are only ' + 'defined for 1D coordinates with 2 ' + 'bounds.'.format(self.name(), self.nbounds)) elif self.ndim == 2: if self.nbounds != 4: - raise ValueError('Invalid operation for {!r}, with {} bounds. ' - 'Contiguous bounds are only defined for 2D ' - 'coordinates with 4 bounds.'.format - (self.name(), self.nbounds)) + raise ValueError('Invalid operation for {!r}, with {} ' + 'bound(s). Contiguous bounds are only ' + 'defined for 2D coordinates with 4 ' + 'bounds.'.format(self.name(), self.nbounds)) else: raise ValueError('Invalid operation for {!r}. Contiguous bounds ' 'are not defined for coordinates with more than ' '2 dimensions.'.format(self.name())) + def _discontiguity_in_bounds(self, rtol=1e-05, atol=1e-08): + """ + Checks that the bounds of the coordinate are contiguous. + + Kwargs: + * rtol: (float) + Relative tolerance that is used when checking contiguity. Defaults + to 1e-5. + * atol: (float) + Absolute tolerance that is used when checking contiguity. Defaults + to 1e-8. + + Returns: + * contiguous: (boolean) + True if there are no discontiguities. + * diffs: (array or tuple of arrays) + The diffs along the bounds of the coordinate. If self is a 2D + coord of shape (Y, X), a tuple of arrays is returned, where the + first is an array of differences along the x-axis, of the shape + (Y, X-1) and the second is an array of differences along the + y-axis, of the shape (Y-1, X). + + """ + self._sanity_check_bounds() + + if self.ndim == 1: + contiguous = np.allclose(self.bounds[1:, 0], + self.bounds[:-1, 1], + rtol=rtol, atol=atol) + diffs = np.abs(self.bounds[:-1, 1] - self.bounds[1:, 0]) + + elif self.ndim == 2: + def mod360_adjust(compare_axis): + bounds = self.bounds.copy() + + if compare_axis == 'x': + upper_bounds = bounds[:, :-1, 1] + lower_bounds = bounds[:, 1:, 0] + elif compare_axis == 'y': + upper_bounds = bounds[:-1, :, 3] + lower_bounds = bounds[1:, :, 0] + + if self.name() in ['longitude', 'grid_longitude']: + # If longitude, adjust for longitude wrapping + diffs = upper_bounds - lower_bounds + index = diffs > 180 + if index.any(): + sign = np.sign(diffs) + 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) + return diffs_along_axis, contiguous_along_axis + + diffs_along_x, match_cell_x1 = mod360_adjust(compare_axis='x') + diffs_along_y, match_cell_y1 = mod360_adjust(compare_axis='y') + + contiguous = match_cell_x1 and match_cell_y1 + diffs = (diffs_along_x, diffs_along_y) + + return contiguous, diffs + def is_contiguous(self, rtol=1e-05, atol=1e-08): """ Return True if, and only if, this Coord is bounded with contiguous bounds to within the specified relative and absolute tolerances. + 1D coords are contiguous if the upper bound of a cell aligns, + within a tolerance, to the lower bound of the next cell along. + + 2D coords, with 4 bounds, are contiguous if the lower right corner of + each cell aligns with the lower left corner of the cell to the right of + it, and the upper left corner of each cell aligns with the lower left + corner of the cell above it. + Args: * rtol: @@ -1064,14 +1098,7 @@ def is_contiguous(self, rtol=1e-05, atol=1e-08): """ if self.has_bounds(): - self._sanity_check_contiguous() - if self.ndim == 1: - contiguous = np.allclose(self.bounds[1:, 0], - self.bounds[:-1, 1], - rtol=rtol, atol=atol) - elif self.ndim == 2: - contiguous, _, _ = _discontiguity_in_2d_bounds(self.bounds, - abs_tol=atol) + contiguous, _ = self._discontiguity_in_bounds(rtol=rtol, atol=atol) else: contiguous = False return contiguous @@ -1079,25 +1106,34 @@ def is_contiguous(self, rtol=1e-05, atol=1e-08): def contiguous_bounds(self): """ Returns the N+1 bound values for a contiguous bounded 1D coordinate - of length N. - - Returns the (N+1, M+1) bound values for a contiguous bounded 2D + of length N, or the (N+1, M+1) bound values for a contiguous bounded 2D coordinate of shape (N, M). - Assumes input is contiguous. + Only 1D or 2D coordinates are supported. .. note:: - If the coordinate does not have bounds, this method will + If the coordinate has bounds, this method assumes they are + contiguous. + + If the coordinate is 1D and does not have bounds, this method will return bounds positioned halfway between the coordinate's points. + If the coordinate is 2D and does not have bounds, an error will be + raised. + """ if not self.has_bounds(): - warnings.warn('Coordinate {!r} is not bounded, guessing ' - 'contiguous bounds.'.format(self.name())) - bounds = self._guess_bounds() + if self.ndim == 1: + warnings.warn('Coordinate {!r} is not bounded, guessing ' + 'contiguous bounds.'.format(self.name())) + bounds = self._guess_bounds() + elif self.ndim == 2: + raise ValueError('2D coordinate {!r} is not bounded. Guessing ' + 'bounds of 2D coords is not currently ' + 'supported.'.format(self.name())) else: - self._sanity_check_contiguous() + self._sanity_check_bounds() bounds = self.bounds if self.ndim == 1: diff --git a/lib/iris/plot.py b/lib/iris/plot.py index 7bf8ea07f3..0b9b7c54be 100644 --- a/lib/iris/plot.py +++ b/lib/iris/plot.py @@ -102,7 +102,7 @@ def get_span(coord): raise ValueError(msg.format(coord.name())) if mode == iris.coords.BOUND_MODE and len(span) not in [1, 2]: raise ValueError('The coordinate {!r} has {} dimensions.' - 'Cell-based plotting is only supporting for' + 'Cell-based plotting is only supported for' 'coordinates with one or two dimensions.' .format(coord.name()), len(span)) @@ -271,54 +271,82 @@ def _invert_yaxis(v_coord, axes=None): axes.invert_yaxis() -def _check_contiguity_and_bounds(coord, data, abs_tol=1e-4, transpose=False): +def _check_bounds_contiguity_and_mask(coord, data, atol=None): """ - Check that the discontinuous bounds occur where the data is masked. + Checks that any discontiguities in the bounds of the given coordinate only + occur where the data is masked. - If discontinuity occurs but data is masked, raise warning - If discontinuity occurs and data is NOT masked, raise error + Where a discontinuity occurs the grid created for plotting will not be + correct. This does not matter if the data is masked in that location as + this is not plotted. + + If a discontiguity occurs where the data is *not* masked, an error is + raised. Args: - coords: - Array of the bounds of a 2D coord, of shape (X,Y,4) - data: - data of the the cube we are plotting - abs_tol: - tolerance when checking the contiguity + coord: (iris.coord.Coord) + Coordinate the bounds of which will be checked for contiguity + data: (array) + Data of the the cube we are plotting + atol: + Absolute tolerance when checking the contiguity. Defaults to None. + If an absolute tolerance is not set, 1D coords are not checked (so + as to not introduce a breaking change without a major release) but + 2D coords are always checked, by calling + :meth:`iris.coords.Coord._discontiguity_in_bounds` with its default + tolerance. """ - if transpose: - data = data.T - - bounds = coord.bounds - - both_dirs_contiguous, diffs_along_x, diffs_along_y = \ - iris.coords._discontiguity_in_2d_bounds(bounds, abs_tol=abs_tol) - - if not both_dirs_contiguous: - - # True where data exists + data_is_masked = hasattr(data, 'mask') + if data_is_masked: + # When checking the location of the discontiguities, we check against + # the opposite of the mask, which is True where data exists. mask_invert = np.logical_not(data.mask) - # Where a discontinuity occurs the grid will not be created correctly. - # This does not matter if the data is masked as this is not plotted. - # So for places where data exists (opposite of the mask) AND a - # diff exists. If any exist, raise a warning - not_masked_at_discontinuity_along_x = np.any( - np.logical_and(mask_invert[:, :-1], diffs_along_x)) + if coord.ndim == 1: + # 1D coords are only checked if an absolute tolerance is set, to avoid + # introducing a breaking change. + if atol: + contiguous, diffs = coord._discontiguity_in_bounds(atol=atol) - not_masked_at_discontinuity_along_y = np.any( - np.logical_and(mask_invert[:-1, ], diffs_along_y)) + if not contiguous and data_is_masked: + not_masked_at_discontiguity = np.any( + np.logical_and(mask_invert[:-1], diffs)) + else: + return - # If discontinuity occurs but not masked, any grid will be created - # incorrectly, so raise a warning - if not_masked_at_discontinuity_along_x or \ - not_masked_at_discontinuity_along_y: - 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 mesh to give to' - ' Matplotlib'.format(coord.name())) + elif coord.ndim == 2: + if atol: + args = {'atol': atol} + else: + args = {} + contiguous, diffs = coord._discontiguity_in_bounds(*args) + diffs_along_x, diffs_along_y = diffs + + if not contiguous and data_is_masked: + # Check along both dimensions. + not_masked_at_discontiguity_along_x = np.any( + np.logical_and(mask_invert[:, :-1], diffs_along_x)) + + not_masked_at_discontiguity_along_y = np.any( + np.logical_and(mask_invert[:-1, ], diffs_along_y)) + + not_masked_at_discontiguity = not_masked_at_discontiguity_along_x \ + or not_masked_at_discontiguity_along_y + + # If any discontiguity occurs where the data is not masked the grid will be + # created incorrectly, so raise an error. + if not contiguous: + 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())) + 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( + coord.name())) def _draw_2d_from_bounds(draw_method_name, cube, *args, **kwargs): @@ -333,19 +361,12 @@ def _draw_2d_from_bounds(draw_method_name, cube, *args, **kwargs): else: plot_defn = _get_plot_defn(cube, mode, ndims=2) - twodim_contig_atol = kwargs.pop('two_dim_coord_contiguity_atol', - 1e-4) + contig_tol = kwargs.pop('contiguity_tolerance', None) + for coord in plot_defn.coords: - if hasattr(coord, 'has_bounds'): - if coord.ndim == 2 and coord.has_bounds(): - try: - _check_contiguity_and_bounds(coord, data=cube.data, - abs_tol=twodim_contig_atol) - except ValueError: - if _check_contiguity_and_bounds(coord, data=cube.data, - abs_tol=twodim_contig_atol, - transpose=True) is True: - plot_defn.transpose = True + if hasattr(coord, 'has_bounds') and coord.has_bounds(): + _check_bounds_contiguity_and_mask(coord, data=cube.data, + atol=contig_tol) if _can_draw_map(plot_defn.coords): result = _map_common(draw_method_name, None, iris.coords.BOUND_MODE, @@ -390,11 +411,10 @@ def _draw_2d_from_bounds(draw_method_name, cube, *args, **kwargs): u, v = plot_arrays - # If the data is tranposed, 2D coordinates will also need to be - # tranposed. - if u.ndim == v.ndim == 2 and plot_defn.transpose is True: - u = u.T - v = v.T + # If the data is transposed, 2D coordinates will also need to be + # transposed. + if plot_defn.transpose is True: + u, v = [coord.T if coord.ndim == 2 else coord for coord in [u, v]] if u.ndim == v.ndim == 1: u, v = _broadcast_2d(u, v) @@ -1102,8 +1122,7 @@ def pcolor(cube, *args, **kwargs): Draws a pseudocolor plot based on the given 2-dimensional Cube. The cube must have either two 1-dimensional coordinates or two - 2-dimensional coordinates with contiguous bounds to plot against each - other. + 2-dimensional coordinates with contiguous bounds to plot the cube against. Kwargs: @@ -1117,10 +1136,8 @@ def pcolor(cube, *args, **kwargs): * axes: the :class:`matplotlib.axes.Axes` to use for drawing. Defaults to the current axes if none provided. - * two_dim_coord_contiguity_atol: absolute tolerance when checking for - contiguity between cells in a two dimensional coordinate. - - + * contiguity_tolerance: The absolute tolerance used when checking for + contiguity between the bounds of the cells. Defaults to None. See :func:`matplotlib.pyplot.pcolor` for details of other valid keyword arguments. @@ -1152,8 +1169,8 @@ def pcolormesh(cube, *args, **kwargs): * axes: the :class:`matplotlib.axes.Axes` to use for drawing. Defaults to the current axes if none provided. - * two_dim_coord_contiguity_atol: absolute tolerance when checking for - contiguity between cells in a two dimensional coordinate. + * contiguity_tolerance: The absolute tolerance used when checking for + contiguity between the bounds of the cells. Defaults to None. See :func:`matplotlib.pyplot.pcolormesh` for details of other valid keyword arguments. diff --git a/lib/iris/tests/unit/coords/test_Coord.py b/lib/iris/tests/unit/coords/test_Coord.py index b43df9447a..58fc3e1f9e 100644 --- a/lib/iris/tests/unit/coords/test_Coord.py +++ b/lib/iris/tests/unit/coords/test_Coord.py @@ -24,6 +24,8 @@ import iris.tests as tests import collections +import mock +import warnings import numpy as np @@ -355,6 +357,297 @@ def test_different_array_attrs_incompatible(self): self.assertFalse(self.test_coord.is_compatible(self.other_coord)) +class Test_contiguous_bounds(tests.IrisTest): + def test_1d_coord_no_bounds_warning(self): + coord = DimCoord([0, 1, 2], standard_name='latitude') + msg = "Coordinate 'latitude' is not bounded, guessing contiguous " \ + "bounds." + with warnings.catch_warnings(): + # Cause all warnings to raise Exceptions + warnings.simplefilter("error") + with self.assertRaisesRegexp(Warning, msg): + coord.contiguous_bounds() + + def test_2d_coord_no_bounds_error(self): + coord = AuxCoord(np.array([[0, 0], [5, 5]]), standard_name='latitude') + emsg = 'Guessing bounds of 2D coords is not currently supported' + with self.assertRaisesRegexp(ValueError, emsg): + coord.contiguous_bounds() + + def test__sanity_check_bounds_call(self): + coord = DimCoord([5, 15, 25], bounds=[[0, 10], [10, 20], [20, 30]]) + with mock.patch('iris.coords.Coord._sanity_check_bounds' + ) as bounds_check: + coord.contiguous_bounds() + bounds_check.assert_called_once() + + def test_1d_coord(self): + coord = DimCoord([2, 4, 6], standard_name='latitude', + bounds=[[1, 3], [3, 5], [5, 7]]) + expected = np.array([1, 3, 5, 7]) + result = coord.contiguous_bounds() + self.assertArrayEqual(result, expected) + + def test_1d_coord_discontiguous(self): + coord = DimCoord([2, 4, 6], standard_name='latitude', + bounds=[[1, 3], [4, 5], [5, 7]]) + expected = np.array([1, 4, 5, 7]) + result = coord.contiguous_bounds() + self.assertArrayEqual(result, expected) + + def test_2d_lon_bounds(self): + coord = AuxCoord(np.array([[1, 3], [1, 3]]), + bounds=np.array([[[0, 2, 2, 0], [2, 4, 4, 2]], + [[0, 2, 2, 0], [2, 4, 4, 2]]])) + expected = np.array([[0, 2, 4], [0, 2, 4], [0, 2, 4]]) + result = coord.contiguous_bounds() + self.assertArrayEqual(result, expected) + + def test_2d_lat_bounds(self): + coord = AuxCoord(np.array([[1, 1], [3, 3]]), + bounds=np.array([[[0, 0, 2, 2], [0, 0, 2, 2]], + [[2, 2, 4, 4], [2, 2, 4, 4]]])) + expected = np.array([[0, 0, 0], [2, 2, 2], [4, 4, 4]]) + result = coord.contiguous_bounds() + self.assertArrayEqual(result, expected) + + +class Test_is_contiguous(tests.IrisTest): + def test_no_bounds(self): + coord = DimCoord([1, 3]) + result = coord.is_contiguous() + self.assertFalse(result) + + def test__discontiguity_in_bounds_call(self): + # Check that :meth:`iris.coords.Coord._discontiguity_in_bounds` is + # called. + coord = DimCoord([1, 3], bounds=[[0, 2], [2, 4]]) + with mock.patch('iris.coords.Coord._discontiguity_in_bounds' + ) as discontiguity_check: + # Discontiguity returns two objects that are unpacked in + # `coord.is_contiguous`. + discontiguity_check.return_value = [None, None] + coord.is_contiguous(rtol=1e-1, atol=1e-3) + discontiguity_check.assert_called_with(rtol=1e-1, atol=1e-3) + + +class Test__discontiguity_in_bounds(tests.IrisTest): + def setUp(self): + self.points_3by3 = np.array([[1, 2, 3], + [1, 2, 3], + [1, 2, 3]]) + self.lon_bounds_3by3 = np.array( + [[[0, 2, 2, 0], [2, 4, 4, 2], [4, 6, 6, 4]], + [[0, 2, 2, 0], [2, 4, 4, 2], [4, 6, 6, 4]], + [[0, 2, 2, 0], [2, 4, 4, 2], [4, 6, 6, 4]]]) + self.lat_bounds_3by3 = np.array( + [[[0, 0, 2, 2], [0, 0, 2, 2], [0, 0, 2, 2]], + [[2, 2, 4, 4], [2, 2, 4, 4], [2, 2, 4, 4]], + [[4, 4, 6, 6], [4, 4, 6, 6], [4, 4, 6, 6]]]) + + def test_1d_contiguous(self): + coord = DimCoord([-20, 0, 20], + bounds=[[-30, -10], [-10, 10], [10, 30]]) + contiguous, diffs = coord._discontiguity_in_bounds() + self.assertTrue(contiguous) + self.assertArrayEqual(diffs, np.zeros(2)) + + def test_1d_discontiguous(self): + coord = DimCoord([10, 20, 40], + bounds=[[5, 15], [15, 25], [35, 45]]) + contiguous, diffs = coord._discontiguity_in_bounds() + self.assertFalse(contiguous) + self.assertArrayEqual(diffs, np.array([0, 10])) + + def test_1d_one_cell(self): + # Test a 1D coord with a single cell. + coord = DimCoord(20, bounds=[[10, 30]]) + contiguous, diffs = coord._discontiguity_in_bounds() + self.assertTrue(contiguous) + self.assertArrayEqual(diffs, np.array([])) + + def test_2d_contiguous_both_dirs(self): + coord = AuxCoord(self.points_3by3, bounds=self.lon_bounds_3by3) + contiguous, diffs = coord._discontiguity_in_bounds() + diffs_along_x, diffs_along_y = diffs + self.assertTrue(contiguous) + self.assertTrue(not diffs_along_x.any()) + self.assertTrue(not diffs_along_y.any()) + + def test_2d_discontiguous_along_x(self): + coord = AuxCoord(self.points_3by3[:, ::2], + bounds=self.lon_bounds_3by3[:, ::2, :]) + 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.assertTrue(not diffs_along_y.any()) + + def test_2d_discontiguous_along_y(self): + coord = AuxCoord(self.points_3by3[::2, :], + bounds=self.lat_bounds_3by3[::2, :, :]) + contiguous, diffs = coord._discontiguity_in_bounds() + 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]])) + + def test_2d_discontiguous_along_x_and_y(self): + coord = AuxCoord(np.array([[1, 5], [3, 5]]), + bounds=np.array([[[0, 2, 2, 0], [4, 6, 6, 4]], + [[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) + self.assertFalse(contiguous) + self.assertArrayEqual(diffs_along_x, exp_x_diffs) + self.assertArrayEqual(diffs_along_y, exp_y_diffs) + + def test_2d_one_cell(self): + # Test a 2D coord with a single cell, where the coord has shape (1, 1). + coord = AuxCoord(self.points_3by3[:1, :1], + bounds=self.lon_bounds_3by3[:1, :1, :]) + contiguous, diffs = coord._discontiguity_in_bounds() + diffs_along_x, diffs_along_y = diffs + expected_diffs = np.array([], dtype=np.int64) + self.assertTrue(contiguous) + self.assertArrayEqual(diffs_along_x, expected_diffs.reshape(1, 0)) + self.assertArrayEqual(diffs_along_y, expected_diffs.reshape(0, 1)) + + def test_2d_one_cell_along_x(self): + # Test a 2D coord with a single cell along the x axis, where the coord + # has shape (2, 1). + coord = AuxCoord(self.points_3by3[:, :1], + bounds=self.lat_bounds_3by3[:, :1, :]) + contiguous, diffs = coord._discontiguity_in_bounds() + diffs_along_x, diffs_along_y = diffs + self.assertTrue(contiguous) + self.assertTrue(not diffs_along_x.any()) + self.assertArrayEqual(diffs_along_y, np.array([0, 0]).reshape(2, 1)) + + def test_2d_one_cell_along_y(self): + # Test a 2D coord with a single cell along the y axis, where the coord + # has shape (1, 2). + coord = AuxCoord(self.points_3by3[:1, :], + bounds=self.lon_bounds_3by3[:1, :, :]) + contiguous, diffs = coord._discontiguity_in_bounds() + diffs_along_x, diffs_along_y = diffs + self.assertTrue(contiguous) + self.assertTrue(not diffs_along_x.any()) + self.assertTrue(not diffs_along_y.any()) + + def test_2d_contiguous_mod_360(self): + # Test that longitude coordinates are adjusted by the 360 modulus when + # calculating the discontiguities in contiguous bounds. + coord = AuxCoord( + [[175, -175], [175, -175]], standard_name='longitude', + bounds=np.array([[[170, 180, 180, 170], [-180, -170, -170, -180]], + [[170, 180, 180, 170], [-180, -170, -170, -180]]]) + ) + contiguous, diffs = coord._discontiguity_in_bounds() + diffs_along_x, diffs_along_y = diffs + self.assertTrue(contiguous) + self.assertTrue(not diffs_along_x.any()) + self.assertTrue(not diffs_along_y.any()) + + def test_2d_discontiguous_mod_360(self): + # Test that longitude coordinates are adjusted by the 360 modulus when + # calculating the discontiguities in contiguous bounds. + coord = AuxCoord( + [[175, -175], [175, -175]], standard_name='longitude', + bounds=np.array([[[170, 180, 180, 170], [10, 20, 20, 10]], + [[170, 180, 180, 170], [10, 20, 20, 10]]])) + 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.assertTrue(not diffs_along_y.any()) + + def test_2d_contiguous_mod_360_not_longitude(self): + # Test that non-longitude coordinates are not adjusted by the 360 + # modulus when calculating the discontiguities in contiguous bounds. + coord = AuxCoord( + [[-150, 350], [-150, 350]], standard_name='height', + bounds=np.array([[[-400, 100, 100, -400], [100, 600, 600, 100]], + [[-400, 100, 100, -400], [100, 600, 600, 100]]]) + ) + contiguous, diffs = coord._discontiguity_in_bounds() + diffs_along_x, diffs_along_y = diffs + self.assertTrue(contiguous) + self.assertTrue(not diffs_along_x.any()) + self.assertTrue(not diffs_along_y.any()) + + def test_2d_discontiguous_mod_360_not_longitude(self): + # Test that non-longitude coordinates are not adjusted by the 360 + # modulus when calculating the discontiguities in discontiguous bounds. + coord = AuxCoord( + [[-150, 350], [-150, 350]], standard_name='height', + bounds=np.array([[[-400, 100, 100, -400], [200, 600, 600, 200]], + [[-400, 100, 100, -400], [200, 600, 600, 200]]]) + ) + 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.assertTrue(not diffs_along_y.any()) + + +class Test__sanity_check_bounds(tests.IrisTest): + def test_coord_1d_2_bounds(self): + # Check that a 1d coord with 2 bounds does not raise an error. + coord = iris.coords.DimCoord([0, 1], standard_name='latitude', + bounds=[[0, 1], [1, 2]]) + coord._sanity_check_bounds() + + def test_coord_1d_no_bounds(self): + coord = iris.coords.DimCoord([0, 1], standard_name='latitude') + emsg = "Contiguous bounds are only defined for 1D coordinates with " \ + "2 bounds." + with self.assertRaisesRegexp(ValueError, emsg): + coord._sanity_check_bounds() + + def test_coord_1d_1_bounds(self): + coord = iris.coords.DimCoord([0, 1], standard_name='latitude', + bounds=np.array([[0], [1]])) + emsg = "Contiguous bounds are only defined for 1D coordinates with " \ + "2 bounds." + with self.assertRaisesRegexp(ValueError, emsg): + coord._sanity_check_bounds() + + def test_coord_2d_4_bounds(self): + coord = iris.coords.AuxCoord( + [[0, 0], [1, 1]], standard_name='latitude', + bounds=np.array([[[0, 0, 1, 1], [0, 0, 1, 1]], + [[1, 1, 2, 2], [1, 1, 2, 2]]])) + coord._sanity_check_bounds() + + def test_coord_2d_no_bounds(self): + coord = iris.coords.AuxCoord([[0, 0], [1, 1]], + standard_name='latitude') + emsg = "Contiguous bounds are only defined for 2D coordinates with " \ + "4 bounds." + with self.assertRaisesRegexp(ValueError, emsg): + coord._sanity_check_bounds() + + def test_coord_2d_2_bounds(self): + coord = iris.coords.AuxCoord( + [[0, 0], [1, 1]], standard_name='latitude', + bounds=np.array([[[0, 1], [0, 1]], [[1, 2], [1, 2]]])) + emsg = "Contiguous bounds are only defined for 2D coordinates with " \ + "4 bounds." + with self.assertRaisesRegexp(ValueError, emsg): + coord._sanity_check_bounds() + + def test_coord_3d(self): + coord = iris.coords.AuxCoord(np.zeros((2, 2, 2)), + standard_name='height') + emsg = "Contiguous bounds are not defined for coordinates with more " \ + "than 2 dimensions." + with self.assertRaisesRegexp(ValueError, emsg): + coord._sanity_check_bounds() + + class Test_convert_units(tests.IrisTest): def test_convert_unknown_units(self): coord = iris.coords.AuxCoord(1, units='unknown') diff --git a/lib/iris/tests/unit/plot/test_2d_coords.py b/lib/iris/tests/unit/plot/test_2d_coords.py deleted file mode 100644 index 7397921861..0000000000 --- a/lib/iris/tests/unit/plot/test_2d_coords.py +++ /dev/null @@ -1,167 +0,0 @@ -# (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 . -"""Unit tests for handling and plotting of 2-dimensional coordinates""" - -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.ma as ma - -import iris.coords as coords -from iris.tests.stock import simple_2d_w_multidim_coords as cube_2dcoords -from iris.tests.stock import simple_3d_w_multidim_coords as cube3d_2dcoords -from iris.tests.stock import sample_2d_latlons -from iris.tests.stock import make_bounds_discontiguous_at_point - - -if tests.MPL_AVAILABLE: - import iris.plot as iplt - - -def full2d_global(): - return sample_2d_latlons(transformed=True) - - -@tests.skip_data -class Test_2d_coords_plot_defn_bound_mode(tests.IrisTest): - def setUp(self): - self.multidim_cube = cube_2dcoords() - self.overspan_cube = cube3d_2dcoords() - - # latlon_2d is a cube with 2d coords, 4 bounds per point, - # discontiguities in the bounds but masked data at the discontiguities. - self.latlon_2d = full2d_global() - make_bounds_discontiguous_at_point(self.latlon_2d, 2, 2) - - # # Take a latlon cube with 1D coords, broadcast the coords into 2D - # # ones, then add ONE of them back into the cube in place of original: - # single_dims = lat_lon_cube() - # lon = single_dims.coord('longitude') - # lat = single_dims.coord('latitude') - # big_lon, big_lat = testdata.grid_coords_2d_from_1d(lon, lat) - # mixed_dims = single_dims.copy() - # mixed_dims.remove_coord(lon) - # # TODO Fix this coord addition: - # # When adding an aux_coord, the function '_check_multidim_metadata' - # # throws an error as it requires coord.shape to be (1, ) instead of - # # (3, 4) or whatever. - # mixed_dims.add_aux_coord(big_lon) - # - # # mixed_dims is now a cube with 2 1D dim coords and an additional - # # 2D aux coord. - # self.mixed_dims = mixed_dims - - self.mode = coords.BOUND_MODE - - def test_2d_coords_identified(self): - # Test that 2d coords are identified in the plot definition without - # having to be specified in the args without any errors. - cube = self.multidim_cube - defn = iplt._get_plot_defn(cube, mode=self.mode) - self.assertEqual([coord.name() for coord in defn.coords], - ['bar', 'foo']) - - def test_2d_coords_custom_picked(self): - # Test that 2d coords which are specified in the args will be - # accepted without any errors. - cube = self.multidim_cube - defn = iplt._get_plot_defn_custom_coords_picked(cube, ('foo', 'bar'), - self.mode) - self.assertEqual([coord.name() for coord in defn.coords], - ['bar', 'foo']) - - def test_2d_coords_as_integers(self): - # Test that if you pass in 2d coords as args in the form of integers, - # they will still be correctly identified without any errors. - cube = self.multidim_cube - defn = iplt._get_plot_defn_custom_coords_picked(cube, (0, 1), - self.mode) - self.assertEqual([coord for coord in defn.coords], - [1, 0]) - - def test_total_span_check(self): - # Test that an error is raised if a user tries to plot a 2d coord - # against a different coord, making total number of dimensions 3. - cube = self.overspan_cube - with self.assertRaises(ValueError): - iplt._get_plot_defn_custom_coords_picked(cube, ('wibble', 'foo'), - self.mode) - - # def test_2dcoord_with_1dcoord(self): - # # TODO Generate a cube with one 2d coord and one 1d coord - # # TODO Try and plot them against each other - # # TODO Find out where I can put a catch for this (if necessary) - # cube = self.mixed_dims - # with self.assertRaises(ValueError): - # iplt._get_plot_defn_custom_coords_picked( - # cube, - # ('latitude', 'longitude'), - # self.mode) - - def test_map_common_not_enough_bounds(self): - # Test that a lat-lon cube with 2d coords and 2 bounds per point - # throws an error in contiguity checks. - cube = self.multidim_cube - cube.coord('foo').rename('longitude') - cube.coord('bar').rename('latitude') - with self.assertRaises(ValueError): - plot_defn = iplt._get_plot_defn(cube, self.mode) - iplt._map_common('pcolor', None, self.mode, cube, plot_defn) - - def test_map_common_2d(self): - # Test that a cube with 2d coords can be plotted as a map. - cube = self.latlon_2d - # Get necessary variables from _get_plot_defn to check that the test - # case will be accepted by _map_common. - plot_defn = iplt._get_plot_defn(cube, self.mode) - result = iplt._map_common('pcolor', None, self.mode, cube, plot_defn) - self.assertTrue(result) - -# def test_discontiguous_masked(self): -# # Test that a contiguity check will raise a warning (not an error) for -# # discontiguous bounds but appropriately masked data. -# cube = self.latlon_2d -# coord = cube.coord('longitude') -# msg = 'The bounds of the longitude coordinate are not contiguous. ' \ -# 'However, data is masked where the discontiguity occurs so ' \ -# 'plotting anyway.' -# with self.assertWarnsRegexp(msg): -# iplt._check_contiguity_and_bounds(coord, cube.data) - - def test_discontiguous_unmasked(self): - # Check that an error occurs when the contiguity check finds - # discontiguous bounds but unmasked data. - cube = self.latlon_2d - cube.data.mask = ma.nomask - coord = cube.coord('longitude') - with self.assertRaises(ValueError): - iplt._check_contiguity_and_bounds(coord, cube.data) - - def test_draw_2d_from_bounds(self): - # Test this function will not raise an error even with our most - # awkward but supported cube. - cube = self.latlon_2d - result = iplt._draw_2d_from_bounds('pcolormesh', cube) - self.assertTrue(result) - - -if __name__ == '__main__': - tests.main() 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 new file mode 100644 index 0000000000..b067b3f387 --- /dev/null +++ b/lib/iris/tests/unit/plot/test__check_bounds_contiguity_and_mask.py @@ -0,0 +1,96 @@ +# (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 . +"""Unit tests for the `iris.plot._check_bounds_contiguity_and_mask` +function.""" + +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.coords import DimCoord +from iris.tests.stock import (sample_2d_latlons, + make_bounds_discontiguous_at_point) + +if tests.MPL_AVAILABLE: + import iris.plot as iplt + + +@tests.skip_plot +class Test_check_bounds_contiguity_and_mask(tests.IrisTest): + 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) + + def test_1d_contiguous(self): + # Test a 1D coordinate which is contiguous. + 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) + + 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) + + def test_1d_discontigous_unmasked(self): + # Test a 1D coordinate which is discontiguous and unmasked at + # discontiguities. + coord = DimCoord([1, 3, 5], bounds=[[0, 2], [2, 4], [5, 6]]) + data = ma.array(np.array([278, 300, 282]), mask=[1, 0, 0]) + 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) + + def test_2d_contiguous(self): + # Test a 2D coordinate which is contiguous. + cube = sample_2d_latlons() + iplt._check_bounds_contiguity_and_mask(cube.coord('longitude'), + cube.data) + + def test_2d_discontigous_masked(self): + # Test a 2D coordinate which is discontiguous but masked at + # discontiguities. + cube = sample_2d_latlons() + make_bounds_discontiguous_at_point(cube, 3, 4) + iplt._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 + # discontiguities. + cube = sample_2d_latlons() + make_bounds_discontiguous_at_point(cube, 3, 4) + 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) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/plot/test__get_plot_defn.py b/lib/iris/tests/unit/plot/test__get_plot_defn.py index 6dfb6f9f11..2947933c7d 100644 --- a/lib/iris/tests/unit/plot/test__get_plot_defn.py +++ b/lib/iris/tests/unit/plot/test__get_plot_defn.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2017, Met Office +# (C) British Crown Copyright 2017 - 2018, Met Office # # This file is part of Iris. # @@ -24,7 +24,7 @@ import iris.tests as tests import iris.coords -from iris.tests.stock import simple_2d +from iris.tests.stock import simple_2d, simple_2d_w_multidim_coords if tests.MPL_AVAILABLE: import iris.plot as iplt @@ -45,6 +45,12 @@ def test_axis_order_yx(self): self.assertEqual([coord.name() for coord in defn.coords], ['foo', 'bar']) + def test_2d_coords(self): + cube = simple_2d_w_multidim_coords() + defn = iplt._get_plot_defn(cube, iris.coords.BOUND_MODE) + self.assertEqual([coord.name() for coord in defn.coords], + ['bar', 'foo']) + if __name__ == "__main__": tests.main() diff --git a/lib/iris/tests/unit/plot/test__get_plot_defn_custom_coords_picked.py b/lib/iris/tests/unit/plot/test__get_plot_defn_custom_coords_picked.py new file mode 100644 index 0000000000..d0ce55a389 --- /dev/null +++ b/lib/iris/tests/unit/plot/test__get_plot_defn_custom_coords_picked.py @@ -0,0 +1,93 @@ +# (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 . +"""Unit tests for the `iris.plot._get_plot_defn_custom_coords_picked` +function.""" + + +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 + +from iris.coords import BOUND_MODE, POINT_MODE +from iris.tests.stock import (simple_2d, simple_2d_w_multidim_coords, + hybrid_height) + + +if tests.MPL_AVAILABLE: + import iris.plot as iplt + + +@tests.skip_plot +class Test_get_plot_defn_custom_coords_picked(tests.IrisTest): + def test_1d_coords(self): + cube = simple_2d() + defn = iplt._get_plot_defn_custom_coords_picked(cube, ('foo', 'bar'), + POINT_MODE) + self.assertEqual([coord.name() for coord in defn.coords], + ['bar', 'foo']) + self.assertFalse(defn.transpose) + + def test_1d_coords_swapped(self): + cube = simple_2d() + defn = iplt._get_plot_defn_custom_coords_picked(cube, ('bar', 'foo'), + POINT_MODE) + self.assertEqual([coord.name() for coord in defn.coords], + ['foo', 'bar']) + self.assertTrue(defn.transpose) + + def test_1d_coords_as_integers(self): + cube = simple_2d() + defn = iplt._get_plot_defn_custom_coords_picked(cube, (1, 0), + POINT_MODE) + self.assertEqual([coord for coord in defn.coords], [0, 1]) + self.assertFalse(defn.transpose) + + def test_1d_coords_as_integers_swapped(self): + cube = simple_2d() + defn = iplt._get_plot_defn_custom_coords_picked(cube, (0, 1), + POINT_MODE) + self.assertEqual([coord for coord in defn.coords], [1, 0]) + self.assertTrue(defn.transpose) + + def test_2d_coords(self): + cube = simple_2d_w_multidim_coords() + defn = iplt._get_plot_defn_custom_coords_picked(cube, ('foo', 'bar'), + BOUND_MODE) + self.assertEqual([coord.name() for coord in defn.coords], + ['bar', 'foo']) + self.assertFalse(defn.transpose) + + def test_2d_coords_as_integers(self): + cube = simple_2d_w_multidim_coords() + defn = iplt._get_plot_defn_custom_coords_picked(cube, (0, 1), + BOUND_MODE) + self.assertEqual([coord for coord in defn.coords], [1, 0]) + self.assertTrue(defn.transpose) + + def test_span_check(self): + cube = hybrid_height() + emsg = 'don\'t span the 2 data dimensions' + with self.assertRaisesRegexp(ValueError, emsg): + iplt._get_plot_defn_custom_coords_picked( + cube, ('sigma', 'level_height'), POINT_MODE) + + +if __name__ == "__main__": + tests.main() From 7455bb1aa2a12080e46501317ae7b54a89946d61 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Mon, 3 Sep 2018 12:47:45 +0100 Subject: [PATCH 27/62] Tests for rotate_grid_vectors (#3148) * Tests for rotate_grid_vectors. * Small fix to assertArrayAllClose for values near 0. * Small tweaks. * Fix test method. * Fix 1 test for 'equal_nans' no longer the default. * Review changes. --- lib/iris/tests/__init__.py | 37 +++-- .../cartography/test_rotate_grid_vectors.py | 148 ++++++++++++++++++ .../regrid/test_RectilinearRegridder.py | 3 +- 3 files changed, 173 insertions(+), 15 deletions(-) create mode 100644 lib/iris/tests/unit/analysis/cartography/test_rotate_grid_vectors.py diff --git a/lib/iris/tests/__init__.py b/lib/iris/tests/__init__.py index 7550fce1ca..b7806b455b 100644 --- a/lib/iris/tests/__init__.py +++ b/lib/iris/tests/__init__.py @@ -642,7 +642,7 @@ def assertMaskedArrayAlmostEqual(self, a, b, decimal=6, strict=False): self._assertMaskedArray(np.testing.assert_array_almost_equal, a, b, strict, decimal=decimal) - def assertArrayAllClose(self, a, b, rtol=1.0e-7, atol=0.0, **kwargs): + def assertArrayAllClose(self, a, b, rtol=1.0e-7, atol=1.0e-8, **kwargs): """ Check arrays are equal, within given relative + absolute tolerances. @@ -660,26 +660,35 @@ def assertArrayAllClose(self, a, b, rtol=1.0e-7, atol=0.0, **kwargs): Performs pointwise toleranced comparison, and raises an assertion if the two are not equal 'near enough'. - For full details see underlying routine numpy.testing.assert_allclose. + For full details see underlying routine numpy.allclose. """ - ok = np.allclose(a, b, atol=atol, rtol=rtol, equal_nan=True) + # Handle the 'err_msg' kwarg, which is the only API difference + # between np.allclose and np.testing_assert_allclose. + msg = kwargs.pop('err_msg', None) + ok = np.allclose(a, b, rtol=rtol, atol=atol, **kwargs) if not ok: # Calculate errors above a pointwise tolerance : The method is # taken from "numpy.core.numeric.isclose". + a, b = np.broadcast_arrays(a, b) errors = (np.abs(a-b) - atol + rtol * np.abs(b)) worst_inds = np.unravel_index(np.argmax(errors.flat), errors.shape) - # Build a more useful message than from np.testing.assert_allclose. - msg = ('\nARRAY CHECK FAILED "assertArrayAllClose" :' - '\n with shapes={} {}, atol={}, rtol={}' - '\n worst at element {} : a={} b={}' - '\n absolute error ~{:.3g}, equivalent to rtol ~{:.3e}') - aval, bval = a[worst_inds], b[worst_inds] - absdiff = np.abs(aval - bval) - equiv_rtol = absdiff / bval - raise AssertionError(msg.format( - a.shape, b.shape, atol, rtol, worst_inds, aval, bval, absdiff, - equiv_rtol)) + + if msg is None: + # Build a more useful message than np.testing.assert_allclose. + msg = ( + '\nARRAY CHECK FAILED "assertArrayAllClose" :' + '\n with shapes={} {}, atol={}, rtol={}' + '\n worst at element {} : a={} b={}' + '\n absolute error ~{:.3g}, equivalent to rtol ~{:.3e}') + aval, bval = a[worst_inds], b[worst_inds] + absdiff = np.abs(aval - bval) + equiv_rtol = absdiff / bval + msg = msg.format( + a.shape, b.shape, atol, rtol, worst_inds, aval, bval, + absdiff, equiv_rtol) + + raise AssertionError(msg) @contextlib.contextmanager def temp_filename(self, suffix=''): diff --git a/lib/iris/tests/unit/analysis/cartography/test_rotate_grid_vectors.py b/lib/iris/tests/unit/analysis/cartography/test_rotate_grid_vectors.py new file mode 100644 index 0000000000..90ec37e162 --- /dev/null +++ b/lib/iris/tests/unit/analysis/cartography/test_rotate_grid_vectors.py @@ -0,0 +1,148 @@ +# (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 . +""" +Unit tests for the function +:func:`iris.analysis.cartography.rotate_grid_vectors`. + +""" +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 + +from mock import Mock, call as mock_call +import numpy as np + +from iris.cube import Cube +from iris.tests.stock import sample_2d_latlons + +from iris.analysis.cartography import rotate_grid_vectors + + +class TestRotateGridVectors(tests.IrisTest): + def _check_angles_calculation(self, angles_in_degrees=True, + nan_angles_mask=None): + # Check basic maths on a 2d latlon grid. + u_cube = sample_2d_latlons(regional=True, transformed=True) + u_cube.units = 'ms-1' + u_cube.rename('dx') + u_cube.data[...] = 0 + v_cube = u_cube.copy() + v_cube.name('dy') + + # Define 6 different vectors, repeated in each data row. + in_vu = np.array([(0, 1), (2, -1), (-1, -1), (-3, 1), (2, 0), (0, 0)]) + in_angs = np.rad2deg(np.arctan2(in_vu[..., 0], in_vu[..., 1])) + in_mags = np.sqrt(np.sum(in_vu * in_vu, axis=1)) + v_cube.data[...] = in_vu[..., 0] + u_cube.data[...] = in_vu[..., 1] + + # Define 5 different test rotation angles, one for each data row. + rotation_angles = np.array([0., -45., 135, -140., 90.]) + ang_cube_data = np.broadcast_to(rotation_angles[:, None], + u_cube.shape) + ang_cube = u_cube.copy() + if angles_in_degrees: + ang_cube.units = 'degrees' + else: + ang_cube.units = 'radians' + ang_cube_data = np.deg2rad(ang_cube_data) + ang_cube.data[:] = ang_cube_data + + if nan_angles_mask is not None: + ang_cube.data[nan_angles_mask] = np.nan + + # Rotate all vectors by all the given angles. + result = rotate_grid_vectors(u_cube, v_cube, ang_cube) + out_u, out_v = [cube.data for cube in result] + + # Check that vector magnitudes were unchanged. + out_mags = np.sqrt(out_u * out_u + out_v * out_v) + expect_mags = in_mags[None, :] + self.assertArrayAllClose(out_mags, expect_mags) + + # Check that vector angles are all as expected. + out_angs = np.rad2deg(np.arctan2(out_v, out_u)) + expect_angs = in_angs[None, :] + rotation_angles[:, None] + ang_diffs = out_angs - expect_angs + # Fix for null vectors, and +/-360 differences. + ang_diffs[np.abs(out_mags) < 0.001] = 0.0 + ang_diffs = ang_diffs % 360.0 + # Check that any differences are very small. + self.assertArrayAllClose(ang_diffs, 0.0) + + # Check that results are always masked arrays, masked at NaN angles. + self.assertTrue(np.ma.isMaskedArray(out_u)) + self.assertTrue(np.ma.isMaskedArray(out_v)) + if nan_angles_mask is not None: + self.assertArrayEqual(out_u.mask, nan_angles_mask) + self.assertArrayEqual(out_v.mask, nan_angles_mask) + + def test_angles_calculation(self): + self._check_angles_calculation() + + def test_angles_in_radians(self): + self._check_angles_calculation(angles_in_degrees=False) + + def test_angles_from_grid(self): + # Check it will gets angles from 'u_cube', and pass any kwargs on to + # the angles routine. + u_cube = sample_2d_latlons(regional=True, transformed=True) + u_cube = u_cube[:2, :3] + u_cube.units = 'ms-1' + u_cube.rename('dx') + u_cube.data[...] = 1.0 + v_cube = u_cube.copy() + v_cube.name('dy') + v_cube.data[...] = 0.0 + + # Setup a fake angles result from the inner call to 'gridcell_angles'. + angles_result_data = np.array([[0.0, 90.0, 180.0], + [-180.0, -90.0, 270.0]]) + angles_result_cube = Cube(angles_result_data, units='degrees') + angles_kwargs = {'this': 2} + angles_call_patch = self.patch( + 'iris.analysis._grid_angles.gridcell_angles', + Mock(return_value=angles_result_cube)) + + # Call the routine. + result = rotate_grid_vectors(u_cube, v_cube, + grid_angles_kwargs=angles_kwargs) + + self.assertEqual(angles_call_patch.call_args_list, + [mock_call(u_cube, this=2)]) + + out_u, out_v = [cube.data for cube in result] + # Records what results should be for the various n*90deg rotations. + expect_u = np.array([[1.0, 0.0, -1.0], + [-1.0, 0.0, 0.0]]) + expect_v = np.array([[0.0, 1.0, 0.0], + [0.0, -1.0, -1.0]]) + # Check results are as expected. + self.assertArrayAllClose(out_u, expect_u) + self.assertArrayAllClose(out_v, expect_v) + + def test_nan_vectors(self): + bad_angle_points = np.zeros((5, 6), dtype=bool) + bad_angle_points[2, 3] = True + self._check_angles_calculation(nan_angles_mask=bad_angle_points) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/analysis/regrid/test_RectilinearRegridder.py b/lib/iris/tests/unit/analysis/regrid/test_RectilinearRegridder.py index ca86e5cdd7..6035630aae 100644 --- a/lib/iris/tests/unit/analysis/regrid/test_RectilinearRegridder.py +++ b/lib/iris/tests/unit/analysis/regrid/test_RectilinearRegridder.py @@ -86,7 +86,8 @@ def assert_values(self, values): result = regrid(self.data, self.x_dim, self.y_dim, self.x, self.y, np.array([xs]), np.array([ys])) - self.assertArrayAllClose(result, expecteds, rtol=1e-04) + self.assertArrayAllClose(result, expecteds, rtol=1e-04, + equal_nan=True) # Check that transposing the input data results in the same values ndim = self.data.ndim From 42a90a260d217f59080825e0900e1f1c9c1cc366 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Mon, 3 Sep 2018 14:17:00 +0100 Subject: [PATCH 28/62] Remove 'skip_2d' test decorator. (#3156) --- lib/iris/tests/__init__.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/iris/tests/__init__.py b/lib/iris/tests/__init__.py index b7806b455b..84d4614c0e 100644 --- a/lib/iris/tests/__init__.py +++ b/lib/iris/tests/__init__.py @@ -1221,12 +1221,6 @@ class MyPlotTests(test.GraphicsTest): 'Test(s) require "python-stratify", which is not available.') -SKIP_2D_TESTS = True -skip_2d = unittest.skipIf( - SKIP_2D_TESTS, - 'Test(s) broken by WIP on 2d coords support -- temporarily disabled.') - - def no_warnings(func): """ Provides a decorator to ensure that there are no warnings raised From 2d6c6a4a66ca6ae9d7cf910e53bbcd197ac590f4 Mon Sep 17 00:00:00 2001 From: lbdreyer Date: Mon, 3 Sep 2018 14:52:55 +0100 Subject: [PATCH 29/62] Add integration graphics tests for pcolormesh with 2d coords (#3149) --- .../integration/plot/test_plot_2d_coords.py | 67 ++ lib/iris/tests/results/imagerepo.json | 1036 +++++++++-------- 2 files changed, 588 insertions(+), 515 deletions(-) create mode 100644 lib/iris/tests/integration/plot/test_plot_2d_coords.py diff --git a/lib/iris/tests/integration/plot/test_plot_2d_coords.py b/lib/iris/tests/integration/plot/test_plot_2d_coords.py new file mode 100644 index 0000000000..5b63b10959 --- /dev/null +++ b/lib/iris/tests/integration/plot/test_plot_2d_coords.py @@ -0,0 +1,67 @@ +# (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 plots with two dimensional coordinates. + +""" + +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 cartopy.crs as ccrs +import matplotlib.pyplot as plt + +import iris + +# Run tests in no graphics mode if matplotlib is not available. +if tests.MPL_AVAILABLE: + import iris.quickplot as qplt + + +@tests.skip_data +def simple_cube_w_2d_coords(): + path = tests.get_data_path(('NetCDF', 'ORCA2', 'votemper.nc')) + cube = iris.load_cube(path) + return cube + + +@tests.skip_plot +@tests.skip_data +class Test(tests.GraphicsTest): + def test_2d_coord_bounds_platecarree(self): + # To avoid a problem with Cartopy smearing the data where the + # longitude wraps, we set the central_longitude + cube = simple_cube_w_2d_coords()[0, 0] + ax = plt.axes(projection=ccrs.PlateCarree(central_longitude=180)) + qplt.pcolormesh(cube) + ax.coastlines(color='red') + self.check_graphic() + + def test_2d_coord_bounds_northpolarstereo(self): + cube = simple_cube_w_2d_coords()[0, 0] + ax = plt.axes(projection=ccrs.NorthPolarStereo()) + qplt.pcolormesh(cube) + ax.coastlines(color='red') + self.check_graphic() + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/results/imagerepo.json b/lib/iris/tests/results/imagerepo.json index 51d37daf3c..e7614cae19 100644 --- a/lib/iris/tests/results/imagerepo.json +++ b/lib/iris/tests/results/imagerepo.json @@ -1,946 +1,952 @@ { "example_tests.test_COP_1d_plot.TestCOP1DPlot.test_COP_1d_plot.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/baff589936602d8ec977334ae4dac9b61a6dc4d99532c86cc2913e36c4cc0f61.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/baff589936602d8ec977334ae4dac9b61a6dc4d99532c86cc2913e36c4cc0f61.png", "https://scitools.github.io/test-iris-imagehash/images/v4/aefec91c3601249cc9b3336dc4c8cdb31a64c6d997b3c0eccb5932d285e42f33.png" - ], + ], "example_tests.test_COP_maps.TestCOPMaps.test_cop_maps.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea9138db95668524913e6ac168997e85957e917e876396b96a81b5ce3c496935.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea9138db95668524913e6ac168997e85957e917e876396b96a81b5ce3c496935.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea9130db95668524913c6ac178995b0d956e917ec76396b96a853dcf94696935.png" - ], + ], "example_tests.test_SOI_filtering.TestSOIFiltering.test_soi_filtering.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fac460b9c17b78723e05a5a9954edaf062332799954e9ca5c63b9a52d24e5a95.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/fa8460b9c17b78723e05a5a9954edaf062333799954e9ca5c63b9a52d24e4a9d.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fac460b9c17b78723e05a5a9954edaf062332799954e9ca5c63b9a52d24e5a95.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa8460b9c17b78723e05a5a9954edaf062333799954e9ca5c63b9a52d24e4a9d.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa167295c5e0696a3c17a58c9568da536233da19994cdab487739b4b9b444eb5.png" - ], + ], "example_tests.test_TEC.TestTEC.test_TEC.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e1a561b69b1a9a42846e9a49c7596e3cce6c907b3a83c17e1b8239b3e4f33bc4.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/e1a561b69b1a9e43846e9a49c7596e2cce6c907b3a83c16e1b9231b3e4f33b8c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e1a561b69b1a9a42846e9a49c7596e3cce6c907b3a83c17e1b8239b3e4f33bc4.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e1a561b69b1a9e43846e9a49c7596e2cce6c907b3a83c16e1b9231b3e4f33b8c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e5a761b69a589a4bc46f9e48c65c6631ce61d1ce3982c13739b33193c0ee3f8c.png" - ], + ], "example_tests.test_anomaly_log_colouring.TestAnomalyLogColouring.test_anomaly_log_colouring.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ec4464e185a39f93931e9b1e91696d2949dde6e63e26a47a5ad391938d9a5a0c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ec4464e185a39f93931e9b1e91696d2949dde6e63e26a47a5ad391938d9a5a0c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ecc164e78e979b19b3789b0885a564a56cc2c65e3ec69469db1bdb9a853c1e24.png" - ], + ], "example_tests.test_atlantic_profiles.TestAtlanticProfiles.test_atlantic_profiles.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/9f8260536bd28e1320739437b5f437b0a51d66f4cc5d08fcd00fdb1c93fcb21c.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/9f8260536bd28e1320739437b5f437b0a51d66f4cc7c09f4d00fdb1c93fcb21c.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/9f8a60536bd28e1320739437b5f437b0a53d66f4cc5c08f4d00fdb1c93fcb21c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/9f8260536bd28e1320739437b5f437b0a51d66f4cc5d08fcd00fdb1c93fcb21c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/9f8260536bd28e1320739437b5f437b0a51d66f4cc7c09f4d00fdb1c93fcb21c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/9f8a60536bd28e1320739437b5f437b0a53d66f4cc5c08f4d00fdb1c93fcb21c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/9fc060f462a08f07203ebc77a1f36707e61f4e38d8f7d08a910197fc877cec58.png" - ], + ], "example_tests.test_atlantic_profiles.TestAtlanticProfiles.test_atlantic_profiles.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/a6eaa57e6e81ddf999311ba3b3775e20845d5889c199673b4e22a4675e8ca11c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a6eaa57e6e81ddf999311ba3b3775e20845d5889c199673b4e22a4675e8ca11c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/eeea64dd6ea8cd99991f1322b3761e06845718d89995b3131f32a4765ec2a1cd.png" - ], + ], "example_tests.test_coriolis_plot.TestCoriolisPlot.test_coriolis_plot.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e78665de9a699659e55e9965886979966986c5e63e98c19e3a256679e1981a24.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e78665de9a699659e55e9965886979966986c5e63e98c19e3a256679e1981a24.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e68665de9a699659c1fe99a5896965966996c46e3e19c1da3a652669c51e1a26.png" - ], + ], "example_tests.test_cross_section.TestCrossSection.test_cross_section.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea95317b9562e4d1649f5a05856e4ca4da52947e4ea5f13f1b499d42f13b1b41.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea95317b9562e4d1649f5a05856e4ca4da52947e4ea5f13f1b499d42f13b1b41.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea91b17b9562e4d1609f5a05856e4ca45a52957e5ea5f13b1bca9dc0b17b1ac1.png" - ], + ], "example_tests.test_cross_section.TestCrossSection.test_cross_section.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea9521fb956a394069921e93f07f4aad856cc47e4e95857a1ea5da3591ba1b81.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea9521fb956a394069921e93f07f4aad856cc47e4e95857a1ea5da3591ba1b81.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea9521fb956a394068931e9be07e4aa5856cc47e4a91957a1ba55bb5b17a3b81.png" - ], + ], "example_tests.test_custom_aggregation.TestCustomAggregation.test_custom_aggregation.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fe816e81917e907eb43e873f85677ac190f0703c6a95811f1ac33ce1a57a6f18.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fe816e81917e907eb43e873f85677ac190f0703c6a95811f1ac33ce1a57a6f18.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fe816e81817e907eb43e873f85637ac198d8703c6a94811f1ac73ee1a57a6f90.png" - ], + ], "example_tests.test_custom_file_loading.TestCustomFileLoading.test_custom_file_loading.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/faa0cbf1845e34be913787416edcc8bc3bc81f9b63332662a4ed30cdc1b2cd21.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/fba0cbf1845e34be912787416edcc8bc3b881f9b62332762a5ad32cdc1b2cd21.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/faa0cbf1845e34be913787416edcc8bc3bc81f9b63332662a4ed30cdc1b2cd21.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fba0cbf1845e34be912787416edcc8bc3b881f9b62332762a5ad32cdc1b2cd21.png", "https://scitools.github.io/test-iris-imagehash/images/v4/faa1cb47845e34bc912797436cccc8343f11359b73523746c48c72d9d9b34da5.png" - ], + ], "example_tests.test_deriving_phenomena.TestDerivingPhenomena.test_deriving_phenomena.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/b9993986866952e6c9464639c4766bd9c669916e7b99c1663f99768990763e81.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/b99139de866952e6c946c639c47e6bd18769d16e7a9981662e813699d0763e89.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/b9993986866952e6c9464639c4766bd9c669916e7b99c1663f99768990763e81.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/b99139de866952e6c946c639c47e6bd18769d16e7a9981662e813699d0763e89.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ec97681793689768943c97e8926669d186e8c33f6c99c32e6b936c83d33e2c98.png" - ], + ], "example_tests.test_global_map.TestGlobalMap.test_global_map.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa9979468566857ef07e3e8978566b91cb0179883c89946686a96b9d83766f81.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa9979468566857ef07e3e8978566b91cb0179883c89946686a96b9d83766f81.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa997b958466846ed13e87467a997a898d66d17e2cc9906684696f99d3162f81.png" - ], + ], "example_tests.test_hovmoller.TestGlobalMap.test_hovmoller.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/bab430b4ce4bce43c5becf89c54b1a63c543c56e1e64907e3bb469b490de1ac1.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bab430b4ce4bce43c5becf89c54b1a63c543c56e1e64907e3bb469b490de1ac1.png", "https://scitools.github.io/test-iris-imagehash/images/v4/eeb46cb4934b934bc07e974bc14b38949943c0fe3e94c17f6ea46cb4c07b3f00.png" - ], + ], "example_tests.test_inset_plot.TestInsetPlot.test_inset_plot.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ebff6992f50096a5b245dac4f6559496b49248dbc95dcb699529912dcf244a54.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/e9ff6992b50096a5b245dac4f64594b6b49248dbc95dcb699529952dcf244a56.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ebff6992f50096a5b245dac4f6559496b49248dbc95dcb699529912dcf244a54.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e9ff6992b50096a5b245dac4f64594b6b49248dbc95dcb699529952dcf244a56.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ebff6992b50096ad9267dac4d64094b294924cdbc95d4b699d29952dcda46e94.png" - ], + ], "example_tests.test_lagged_ensemble.TestLaggedEnsemble.test_lagged_ensemble.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/bbbb31e1c44e64e4b0459b5bb1716ecac464f496ce34618eb1079b39b193ce25.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bbbb31e1c44e64e4b0459b5bb1716ecac464f496ce34618eb1079b39b193ce25.png", "https://scitools.github.io/test-iris-imagehash/images/v4/bbbb31b1c44e64e4b1579b5b917133cecc61f146c414668eb1119b1bb197ce34.png" - ], + ], "example_tests.test_lagged_ensemble.TestLaggedEnsemble.test_lagged_ensemble.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/abfef958fd462c993a07d87960464b81d1009687c139d3b594e9cf87c6b89687.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/abfef958fd462c993a07d87960464b81d1009687c139d3b594e9cf87c6b89687.png", "https://scitools.github.io/test-iris-imagehash/images/v4/aafec5e9e5e03e099a07e0f86542db879438261ec3b13ce78d8dc65a92d83d89.png" - ], + ], "example_tests.test_lineplot_with_legend.TestLineplotWithLegend.test_lineplot_with_legend.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/eae942526540b869961f8da694589da69543cc9af1014afbc3fd596b84fe19a7.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/eae942146540b869961f8de694589da69543cc9af1014afbc3fd596b84fe19a7.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/eae942526540b869961f8da694589da69543cc9af1014afbc3fd596b84fe19a7.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/eae942146540b869961f8de694589da69543cc9af1014afbc3fd596b84fe19a7.png", "https://scitools.github.io/test-iris-imagehash/images/v4/eafd9e12a5a061e9925ec716de489e9685078ec981b229e70ddb79219cc3768d.png" - ], + ], "example_tests.test_orca_projection.TestOrcaProjection.test_orca_projection.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fb11731a94cea4ee64b35e91d1d2304e9e5ac7397b20e1fe12852487e666ce46.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fb11731a94cea4ee64b35e91d1d2304e9e5ac7397b20e1fe12852487e666ce46.png", "https://scitools.github.io/test-iris-imagehash/images/v4/bb11721a87cce5e4cce79e81d19b3b5e1e1cd3783168e07835853485e65e2e1e.png" - ], + ], "example_tests.test_orca_projection.TestOrcaProjection.test_orca_projection.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e5a665a69a599659e5db1865c2653b869996cce63e99e19a1a912639e7181e65.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e5a665a69a599659e5db1865c2653b869996cce63e99e19a1a912639e7181e65.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e58661969e799659c1f719a6c867359a1996c0773649c09c3e612679c07b3f66.png" - ], + ], "example_tests.test_orca_projection.TestOrcaProjection.test_orca_projection.2": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/f2c464ce9e399332e1b74ce1cc79338c6586e5b33b31b37a66c9664cc06e1a64.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/f2c464ce9e399332e1b74ce1cc79338c6586e5b33b31b37a66c9664cc06e1a64.png", "https://scitools.github.io/test-iris-imagehash/images/v4/a58660ce9e739b31c93d1cc9c8df33863383e33b3f11c03f2664366cc8ee3cc1.png" - ], + ], "example_tests.test_orca_projection.TestOrcaProjection.test_orca_projection.3": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa817a83846ea46ce539c93391de32cc86cf87a33fa168721cdb3e896e374b04.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa817a83846ea46ce539c93391de32cc86cf87a33fa168721cdb3e896e374b04.png", "https://scitools.github.io/test-iris-imagehash/images/v4/be817a87845ea56cec79817a919e338436a5c1e73fa16c736c4a3e816a1e6b1c.png" - ], + ], "example_tests.test_polar_stereo.TestPolarStereo.test_polar_stereo.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e168317a92d36d89c5bb9e94c55e6f0c9a93c15a6ec584763b21716791de3a81.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e168317a92d36d89c5bb9e94c55e6f0c9a93c15a6ec584763b21716791de3a81.png", "https://scitools.github.io/test-iris-imagehash/images/v4/b9e16079971e9e93c8ce0f84c31e3b929f92c0ff3ca1c17e39e03961c07e3f80.png" - ], + ], "example_tests.test_polynomial_fit.TestPolynomialFit.test_polynomial_fit.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/abff4a9df26435886520c97f12414695c4b69d23934bc86adc969237d68ccc6f.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/aaff4a9df26435886520c97f12414695c4b69d23934bc86adc969a17d69ccc6f.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/abff4a9df26435886520c97f12414695c4b69d23934bc86adc969237d68ccc6f.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/aaff4a9df26435886520c97f12414695c4b69d23934bc86adc969a17d69ccc6f.png", "https://scitools.github.io/test-iris-imagehash/images/v4/aeffcb34d244348be5a2c96c3a4fc6d0c4b69f2d87294ccb9f1a125684cd7c11.png" - ], + ], "example_tests.test_projections_and_annotations.TestProjectionsAndAnnotations.test_projections_and_annotations.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa854f19851a30e4cc76cd0bb179325ca7c665b0c938cb4b4e719e9cb727b5c0.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/fac54f19851a30e4cc76cd0bb179325cb78665b0c938cb4b4e719e9c9727b5c0.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/fa854e19851a30e4cc76cd0bb179325cb7c664b0c938cb4bce739e9c37a3b5c0.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/fa854e19851a30e4cc76cd0bb179325cb78665b1c938c94bce739e9c3727b5c0.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa854f19851a30e4cc76cd0bb179325ca7c665b0c938cb4b4e719e9cb727b5c0.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fac54f19851a30e4cc76cd0bb179325cb78665b0c938cb4b4e719e9c9727b5c0.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa854e19851a30e4cc76cd0bb179325cb7c664b0c938cb4bce739e9c37a3b5c0.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa854e19851a30e4cc76cd0bb179325cb78665b1c938c94bce739e9c3727b5c0.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa854f19851a30e4cc76cd0bb0f932dca7c665b1c92ccb4b4ed19e9c3721b5c8.png" - ], + ], "example_tests.test_projections_and_annotations.TestProjectionsAndAnnotations.test_projections_and_annotations.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e385699d9c3896627243318fcdad5a7dc6dba492e9b69964936dc21974b18592.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/e385699d9c3896727243318f8dad5a7dc65ba492b93699649b6dc25b64938592.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/e385699d9c3896627243318fcdad5a7dc6dba492b93699649b6dc25964938592.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/e3856b999c3896727243318f8dad5a75965ba492f9b69964db4cc65b64918592.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/e3856b999c3896727243318f8dad5a75865ba492e9b69964db6cc65b74918592.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e385699d9c3896627243318fcdad5a7dc6dba492e9b69964936dc21974b18592.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e385699d9c3896727243318f8dad5a7dc65ba492b93699649b6dc25b64938592.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e385699d9c3896627243318fcdad5a7dc6dba492b93699649b6dc25964938592.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e3856b999c3896727243318f8dad5a75965ba492f9b69964db4cc65b64918592.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e3856b999c3896727243318f8dad5a75865ba492e9b69964db6cc65b74918592.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e3856d999c389662734731afcdad5a7384daa592b1b69b64d26dc29974b18590.png" - ], + ], "example_tests.test_rotated_pole_mapping.TestRotatedPoleMapping.test_rotated_pole_mapping.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa15615e97a193adc15e1e81c4fa3eb49d30817e3e05c17e7ba59927817e1e01.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa15615e97a193adc15e1e81c4fa3eb49d30817e3e05c17e7ba59927817e1e01.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ee46607e97a19781c0df1f81d0bb3e241f20c16f3fc0c1fe39263d33d06f3e80.png" - ], + ], "example_tests.test_rotated_pole_mapping.TestRotatedPoleMapping.test_rotated_pole_mapping.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ba056717c3e099e9b90f8e81c4da589499b696763e45e56b3b893929c17b7e01.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ba056717c3e099e9b90f8e81c4da589499b696763e45e56b3b893929c17b7e01.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea57685f95a886a1c0de9da090be3e2697e1c0ff3f00c17e6b266c17c07f3f00.png" - ], + ], "example_tests.test_rotated_pole_mapping.TestRotatedPoleMapping.test_rotated_pole_mapping.2": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ba1e605ec7a191a1b85e9e81c4da58909996b37e3a65e16f7b817939e57a1e01.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/ba1e605ec7a193a1b85e9e81c4da58909996b3763a65e16f7b816939ed7a1e01.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ba1e605ec7a191a1b85e9e81c4da58909996b37e3a65e16f7b817939e57a1e01.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ba1e605ec7a193a1b85e9e81c4da58909996b3763a65e16f7b816939ed7a1e01.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e85a697e97a18681c6da9f8190bf3e263624c1ef3b48c17a2b223c47c0ff3f81.png" - ], + ], "example_tests.test_rotated_pole_mapping.TestRotatedPoleMapping.test_rotated_pole_mapping.3": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa8172d0847ecd2bc913939c36846c714933799cc3cc8727e67639f939996a58.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa8172d0847ecd2bc913939c36846c714933799cc3cc8727e67639f939996a58.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa8172c6857ecd38cb3392ce36c564311931d85ec64e9787719a39993c316e66.png" - ], + ], "example_tests.test_wind_speed.TestWindSpeed.test_wind_speed.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/bcf924fb9306930ce12ccf97c73236b28ecec4cd3e29847b18e639e6c14f1a09.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bcf924fb9306930ce12ccf97c73236b28ecec4cd3e29847b18e639e6c14f1a09.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e9e960e996169306c1ee9e96c29e36739e13c07d3d61c07f39a139a1c07f3f01.png" - ], + ], "example_tests.test_wind_speed.TestWindSpeed.test_wind_speed.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/bcf924fb9306930ce12ccf97c73236b28ecec4cc3e29847b38e639e6c14f1a09.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bcf924fb9306930ce12ccf97c73236b28ecec4cc3e29847b38e639e6c14f1a09.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e9e960e996169306c1ee9e86c29e36739e13c07d3d61c07f39a139a1c17f3f01.png" - ], + ], "iris.tests.experimental.test_animate.IntegrationTest.test_cube_animation.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fe81957ac17e6a85817e6a85857e942a3e81957a7e81917a7a81d95ec17e2ca1.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fe81957ac17e6a85817e6a85857e942a3e81957a7e81917a7a81d95ec17e2ca1.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fe81c17e817e3e81817e7e81857e7e817e81c07e7e81c17e7a81817e817e8c2a.png" - ], + ], "iris.tests.experimental.test_animate.IntegrationTest.test_cube_animation.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/be81c17ec17e7e81c17e3e81c57ea55a3e80c17e3e81c1fe7a81c285c95f2c03.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/be81c17ec17e7e81c17e3e81c57ea55a3e80c17e3e81c1fe7a81c285c95f2c03.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fe81857e817e6a85817e7a81857e7e817e81957a7e81817e7a81817e817e843e.png" - ], + ], "iris.tests.experimental.test_animate.IntegrationTest.test_cube_animation.2": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea857a81957a857e957ec17e817e6a816a853e817a853e816e818d3a862ad3fe.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea857a81957a857e957ec17e817e6a816a853e817a853e816e818d3a862ad3fe.png", "https://scitools.github.io/test-iris-imagehash/images/v4/be81857ec17e7a81c17e7e81857e3e803e81817a3e81c17e7a81c17ec97e2c2b.png" - ], + ], + "iris.tests.integration.plot.test_plot_2d_coords.Test.test_2d_coord_bounds_northpolarstereo.0": [ + "https://scitools.github.io/test-iris-imagehash/images/v4/e59661969e699659c0f719a6c967339a1992c07f3649c09c3f612669c07b3f66.png" + ], + "iris.tests.integration.plot.test_plot_2d_coords.Test.test_2d_coord_bounds_platecarree.0": [ + "https://scitools.github.io/test-iris-imagehash/images/v4/ee816299954a1da699b6915ec25b6e419729c42c3f84bd9fe6d262d1d1dac076.png" + ], "iris.tests.test_analysis.TestProject.test_cartopy_projection.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/9e1952c9c165b4fc668a9d47c1461d7a60fb2e853eb426bd62fd229c9f04c16d.png" - ], + ], "iris.tests.test_mapping.TestBasic.test_contourf.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e85a69cc96ad92e193c9963385929e1cc3819acde6d965ce6e666b30386e65b1.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e85a69cc96ad92e193c9963385929e1cc3819acde6d965ce6e666b30386e65b1.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e97a346c9685cb899685c9c39695c79396ec634969ce2c74697a3864697b3c8c.png" - ], + ], "iris.tests.test_mapping.TestBasic.test_pcolor.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e95a69c896a592e59bc99e3384929636c32d98cde6d964ce7e666332386465b1.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e95a69c896a592e59bc99e3384929636c32d98cde6d964ce7e666332386465b1.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e97a347c96858b8d9685c9c39696c393966c634969ce3c64697a3864697b3c9c.png" - ], + ], "iris.tests.test_mapping.TestBasic.test_unmappable.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/eaa5684eb54a947ad09eb731c521978dc2fb1cc0e4966ce26e2c6b2d3a6e691a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/eaa5684eb54a947ad09eb731c521978dc2fb1cc0e4966ce26e2c6b2d3a6e691a.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea853e48957ac1df957ac8be852bc1b1944e7a9878e03f4c6a253e6c7a912dc2.png" - ], + ], "iris.tests.test_mapping.TestBoundedCube.test_grid.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa81917e857e6e81857e7a857a81917a7a81857e857e7e81857e7a817a81852e.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e857e7a81857e7a817a81817e7a81857e857e7a81857e7a817a81857e.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa81917e857e6e81857e7a857a81917a7a81857e857e7e81857e7a817a81852e.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e857e7a81857e7a817a81817e7a81857e857e7a81857e7a817a81857e.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e857e7a81857e7a817a81857a7a81857e857e7a85857e7a817a81857a.png" - ], + ], "iris.tests.test_mapping.TestBoundedCube.test_pcolormesh.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa81e535857e92ca8ec23d21b13ce15e7a811ea5c47e1a5ac17b652d3b05e4f2.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa81e535857e92ca8ec23d21b13ce15e7a811ea5c47e1a5ac17b652d3b05e4f2.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa81c17a857e1ea5857e634a7a81cd257e8584da857e3b29817e68f47a81c791.png" - ], + ], "iris.tests.test_mapping.TestLimitedAreaCube.test_grid.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/bf80e2b1c17f1d0ac4f7c8d739a637202749699b6bb3ce3666e4b048944d9d89.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/bf80e2f1c17f1d0ac457c8d619a637213749699b6bb34e3666e4b04e944d9d89.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bf80e2b1c17f1d0ac4f7c8d739a637202749699b6bb3ce3666e4b048944d9d89.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bf80e2f1c17f1d0ac457c8d619a637213749699b6bb34e3666e4b04e944d9d89.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea05392995bac6d691ce3f21666569d86a96c6360ee195cb91e8ce54953b313b.png" - ], + ], "iris.tests.test_mapping.TestLimitedAreaCube.test_outline.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e857e3e80857e7a817a817a817a81817f7a81857e857e857e857e7a81.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e857e7e21857e7a817a817a857a81857a7a81857a857e857a857e7a84.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e857e3e80857e7a817a817a817a81817f7a81857e857e857e857e7a81.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e857e7e21857e7a817a817a857a81857a7a81857a857e857a857e7a84.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa1585e885e87a1785fa7a177a177e807a1585e85fa0857a85e86817857f6a16.png" - ], + ], "iris.tests.test_mapping.TestLimitedAreaCube.test_pcolormesh.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/bf81e6b1c17e1d4884bfc8df39a43720374969db69b34e26c4e4b0ca904f9d89.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bf81e6b1c17e1d4884bfc8df39a43720374969db69b34e26c4e4b0ca904f9d89.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea57396995a8c6d691ea3f25664569d86b16c63686ed958991ea4a549531393b.png" - ], + ], "iris.tests.test_mapping.TestLimitedAreaCube.test_scatter.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea053d2e916ac2d9c4d894346b24f3477acf68ad39329ed8c696e136c1ab9a71.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/ea053d2e916ac2d9c4d895346b2473477acf68ad39329ed8c69ee126c1ab9a71.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea053d2e916ac2d9c4d894346b24f3477acf68ad39329ed8c696e136c1ab9a71.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea053d2e916ac2d9c4d895346b2473477acf68ad39329ed8c69ee126c1ab9a71.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea05bd2e916ac2d984983d346b2473477acf69ad3d3296d8c696e126c1ab1e71.png" - ], + ], "iris.tests.test_mapping.TestLowLevel.test_keywords.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/be21a71bc1de58e43a31871f7e856470c1fa9b8c7b81647384665b9ed1b998c1.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/be21a71bc1de58e43a31871f7e856470c1fa9b8c7b81647384665b9ed1b998c1.png", "https://scitools.github.io/test-iris-imagehash/images/v4/be21a71bc1de58e43a63a71b3e016061c1fe9b8c3e01a473847e5b94d1fb9ac3.png" - ], + ], "iris.tests.test_mapping.TestLowLevel.test_keywords.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea811831957fe3cea68c6ce0d9f29b9b6a816463953e61cc917f1ae36ac09d38.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea811831957fe3cea68c6ce0d9f29b9b6a816463953e61cc917f1ae36ac09d38.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa819097857e6560957e7bcc7a819c316e81951e857e62c281fe79a17aa19637.png" - ], + ], "iris.tests.test_mapping.TestLowLevel.test_params.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ee819cb7913b63c8846e64737bb1999c6ec52633953a69c8916f6c636e92911c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ee819cb7913b63c8846e64737bb1999c6ec52633953a69c8916f6c636e92911c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa8190be857e6739917a7bc47a8594337bb1911c857e6ec3913279007e819637.png" - ], + ], "iris.tests.test_mapping.TestLowLevel.test_params.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/be21a71bc1de58e43a31871f7e856470c1fa9b8c7b81647384665b9ed1b998c1.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/be21a71bc1de58e43a31871f7e856470c1fa9b8c7b81647384665b9ed1b998c1.png", "https://scitools.github.io/test-iris-imagehash/images/v4/be21a71bc1de58e43a63a71b3e016061c1fe9b8c3e01a473847e5b94d1fb9ac3.png" - ], + ], "iris.tests.test_mapping.TestLowLevel.test_params.2": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea811831957ae3cea68c6ce0c9f39b9b6a816473953e63cc917f1ae36ac09d38.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea811831957ae3cea68c6ce0c9f39b9b6a816473953e63cc917f1ae36ac09d38.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa81909f857e6520957e5bcc7a8194716e31851e857e6ac281fe3f817a81963f.png" - ], + ], "iris.tests.test_mapping.TestLowLevel.test_simple.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/eae0943295154bcc844e6c314fb093ce7bc7c4b3a4307bc4916f3f316ed2b4ce.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/eae0943295154bcc844e6c314fb093ce7bc7c4b3a4307bc4916f3f316ed2b4ce.png", "https://scitools.github.io/test-iris-imagehash/images/v4/faa0e55c855fdce7857a1ab16a85a50c3ea1e55e856658a5c11837096e8fe17a.png" - ], + ], "iris.tests.test_mapping.TestMappingSubRegion.test_simple.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/bd913e01d07ee07e926e87876f8196c1e0d36967393c1f181e2c3cb8b0f960d7.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bd913e01d07ee07e926e87876f8196c1e0d36967393c1f181e2c3cb8b0f960d7.png", "https://scitools.github.io/test-iris-imagehash/images/v4/b9913d90c66eca6ec66ec2f3689195b6cf5b2f00392cb3496695621d34db6c92.png" - ], + ], "iris.tests.test_mapping.TestUnmappable.test_simple.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fe818d6ac17e5a958d7ab12b9d677615986e666dc4f20dea7281d98833889b22.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fe818d6ac17e5a958d7ab12b9d677615986e666dc4f20dea7281d98833889b22.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa81b54a817eca35817ec701857e3e64943e7bb41b846f996e817e006ee1b19b.png" - ], + ], "iris.tests.test_plot.Test1dPlotMultiArgs.test_coord.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/83fec2ff7c00a56de9023b52e4143da5d16d7ecad1b76f2094c963929c6471c8.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/83fec2ff7c00a56de9023b52e4143da5d16d7ecad1b76f2094c963929c6471c8.png", "https://scitools.github.io/test-iris-imagehash/images/v4/8bfec2d77e01a5a5ed013b4ac4521c94817d4e6d91ff63349c6d61991e3278cc.png" - ], + ], "iris.tests.test_plot.Test1dPlotMultiArgs.test_coord_coord.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/87ff95776a01e1f67801cc36f4075b81c5437668c1167c88d2676d39d6867b68.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/87ff95776a01e1f67801cc36f4075b81c5437668c1167c88d2676d39d6867b68.png", "https://scitools.github.io/test-iris-imagehash/images/v4/8fff941e7e01e1c2f801c878a41e5b0d85cf36e1837e2d9992c62f21769e6a4d.png" - ], + ], "iris.tests.test_plot.Test1dPlotMultiArgs.test_coord_coord_map.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fbe0623dc9879d91b41e4b449b6579e78798a49b7872d2644b8c919b39306e6c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fbe0623dc9879d91b41e4b449b6579e78798a49b7872d2644b8c919b39306e6c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/bbe0c21ccd179dc3b05e4b689b0771b48698961b7962da446e8ca5bb36716c6e.png" - ], + ], "iris.tests.test_plot.Test1dPlotMultiArgs.test_coord_cube.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8ff897066b41f076f81dce1fb007da79c50633e9c40626b8d1066df9d6067969.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8ff897066b41f076f81dce1fb007da79c50633e9c40626b8d1066df9d6067969.png", "https://scitools.github.io/test-iris-imagehash/images/v4/8ff897066a01f0f2f818ee1eb007ca41853e3b81c57e36a991fe2ca9725e29ed.png" - ], + ], "iris.tests.test_plot.Test1dPlotMultiArgs.test_cube.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8ffac1547a0792546c179db7f1254f6d945b7392841678e895017e3e91c17a0f.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8ffac1547a0792546c179db7f1254f6d945b7392841678e895017e3e91c17a0f.png", "https://scitools.github.io/test-iris-imagehash/images/v4/8ff8c1fa7a05b4ea6c059d2ff1494e4b90f26304846d78d1872a6cfc938b2e3e.png" - ], + ], "iris.tests.test_plot.Test1dPlotMultiArgs.test_cube_coord.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/83fec1ff7e0098757103a71ce4506dc3d11e7b20d2477ec094857db895217f6a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/83fec1ff7e0098757103a71ce4506dc3d11e7b20d2477ec094857db895217f6a.png", "https://scitools.github.io/test-iris-imagehash/images/v4/8fffc1dc7e019c70f001b70ee4386de1814e7938837b6a7f84d07c9f15b02f21.png" - ], + ], "iris.tests.test_plot.Test1dPlotMultiArgs.test_cube_cube.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8ff8c2d73a09b4a76c099d26f14b0e5ad0d643b0d42763e9d51378f895867c39.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/8fe8c0173a19b4066d599946f35f0ed5d0b74729d40369d8953678e897877879.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8ff8c2d73a09b4a76c099d26f14b0e5ad0d643b0d42763e9d51378f895867c39.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8fe8c0173a19b4066d599946f35f0ed5d0b74729d40369d8953678e897877879.png", "https://scitools.github.io/test-iris-imagehash/images/v4/8ff8c0567a01b096e4019daff10b464bd4da6391943678e5879f7e3103e67f1c.png" - ], + ], "iris.tests.test_plot.Test1dQuickplotPlotMultiArgs.test_coord.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/83fec2777e04256f68023352f6d61da5c109dec8d19bcf089cc9d99a9c85d999.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/83fec2777e06256f68023352f6d61da5c009decad19bcf089cc9d99a9c85d989.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/83fec2777e04256f68023352f6d61da5c109dec8d19bcf089cc9d99a9c85d999.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/83fec2777e06256f68023352f6d61da5c009decad19bcf089cc9d99a9c85d989.png", "https://scitools.github.io/test-iris-imagehash/images/v4/83fec2777e002427e801bb4ae65a1c94813dcec999db4bbc9ccd79991f3238cc.png" - ], + ], "iris.tests.test_plot.Test1dQuickplotPlotMultiArgs.test_coord_coord.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/83fe9dd77f00e1d73000cc1df707db8184427ef8d1367c88d2667d39d0866b68.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/83fe9d977f41e1d73000cc1df707d98184427ef8d1367c88d2667d39d0866b68.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/83fe9dd77f00e1d73000cc1df707db8184427ef8d1367c88d2667d39d0866b68.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/83fe9d977f41e1d73000cc1df707d98184427ef8d1367c88d2667d39d0866b68.png", "https://scitools.github.io/test-iris-imagehash/images/v4/83ff9d9f7e01e1c2b001c8f8f63e1b1d81cf36e1837e259982ce2f215c9a626c.png" - ], + ], "iris.tests.test_plot.Test1dQuickplotPlotMultiArgs.test_coord_coord_map.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fbe0623dc9879d91b41e4b449b6579e78798a49b7872d2644b8c919b39306e6c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fbe0623dc9879d91b41e4b449b6579e78798a49b7872d2644b8c919b39306e6c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/bbe0c21ccd179dc3b05e4b689b0771b48698961b7962da446e8ca5bb36716c6e.png" - ], + ], "iris.tests.test_plot.Test1dQuickplotPlotMultiArgs.test_coord_cube.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/87ffb5867f0060d4301f6d9fb007d899c50699e9c8668e78d8678d69de069969.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/87ffb5867f0060d4301f6d9fb007d899c50699e9c8668e78d8678d69de069969.png", "https://scitools.github.io/test-iris-imagehash/images/v4/87ffb79e7f0060d8303fcd1eb007d801c52699e18d769e2199e60ce1da5629ed.png" - ], + ], "iris.tests.test_plot.Test1dQuickplotPlotMultiArgs.test_cube.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/83ffc1dc7e00b0dc66179d95f127cfc9d44959ba846658e891075a3e99415a2f.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/83ffc1dc7e00b0dc66179d95f127cfc9d44959ba846658e891075a3e99415a2f.png", "https://scitools.github.io/test-iris-imagehash/images/v4/a3ffc1d87e00b49964179d28f16bce4b98724b268c6d58e1972e4874998b2e7e.png" - ], + ], "iris.tests.test_plot.Test1dQuickplotPlotMultiArgs.test_cube_coord.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/83fec1ff7f90987720029f1ef458cd43811cdb60d647de609485ddb899215f62.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/83fec1ff7f94987720009f1ef458cd43810cdb60d647de609485ddb89921df62.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/83fec1ff7f90987720029f1ef458cd43811cdb60d647de609485ddb899215f62.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/83fec1ff7f94987720009f1ef458cd43810cdb60d647de609485ddb89921df62.png", "https://scitools.github.io/test-iris-imagehash/images/v4/a3ffc1de7e009c7030019786f438cde3810fd97c93734a778ce07c9f99b02731.png" - ], + ], "iris.tests.test_plot.Test1dQuickplotPlotMultiArgs.test_cube_cube.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/83ffc8967e0098a6241f9d26e34b8e42f4d20bb4942759e9941f78f8d7867a39.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/83f9c8967e009da6245f9946e25f9ed6f0940f29f40749d8853678e8d7857879.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/83ffc8967e0098a6241f9d26e34b8e42f4d20bb4942759e9941f78f8d7867a39.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/83f9c8967e009da6245f9946e25f9ed6f0940f29f40749d8853678e8d7857879.png", "https://scitools.github.io/test-iris-imagehash/images/v4/83ffc9d67e00909624079daef160cf4bd45a439184367ae5979f7e3119e6261c.png" - ], + ], "iris.tests.test_plot.Test1dQuickplotScatter.test_coord_coord.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/a3fac1947c99184e62669ca7f65bc96ab81d97b7e248199cc7913662d94ac5a1.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/a3fac1947c99184e62669ca7f65bc96ab81d97b7c248399cc7917662d84ac5a1.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/a3fac1b47c99184e62669ca7f65bc96ab81d97b7e248199cc7913662d84acda0.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a3fac1947c99184e62669ca7f65bc96ab81d97b7e248199cc7913662d94ac5a1.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a3fac1947c99184e62669ca7f65bc96ab81d97b7c248399cc7917662d84ac5a1.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a3fac1b47c99184e62669ca7f65bc96ab81d97b7e248199cc7913662d84acda0.png", "https://scitools.github.io/test-iris-imagehash/images/v4/b2ecc1a8b9994a16e666b5e3ce151969a5fb4ed49909653990d46b9bfc097684.png" - ], + ], "iris.tests.test_plot.Test1dQuickplotScatter.test_coord_coord_map.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/bea07c99c15eb16e9891ce50c742394a3ced6cb13390f1cc73c29f1b2d0ecd66.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bea07c99c15eb16e9891ce50c742394a3ced6cb13390f1cc73c29f1b2d0ecd66.png", "https://scitools.github.io/test-iris-imagehash/images/v4/bea06899c14eb16e9895ce46c74a396a74ed64b13390b3c61b439f1b4d2ccde6.png" - ], + ], "iris.tests.test_plot.Test1dQuickplotScatter.test_coord_cube.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ae7f1f07f3e0e0f0211b9e066e074d83926ed8f8cd3792dad1964db0d80e9b09.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/ae7f1f07f3e0e0f0311b9e066e074d839266d8e8cd379adad1964db0d80e9b09.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ae7f1f07f3e0e0f0211b9e066e074d83926ed8f8cd3792dad1964db0d80e9b09.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ae7f1f07f3e0e0f0311b9e066e074d839266d8e8cd379adad1964db0d80e9b09.png", "https://scitools.github.io/test-iris-imagehash/images/v4/be852fc1e078c83eb30e3607672149c098d95c5b9e4636f2c1fc299d999f7e03.png" - ], + ], "iris.tests.test_plot.Test1dQuickplotScatter.test_cube_coord.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/a5f896d99a67b94c621deda3f69392cccd246db39018989ec4836de9ed249292.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/a5f896d99a66b94c621deda3f69392cccd646db3901898dec4836de9cd249292.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/a5f896d99a67b94c621ceda3f6d392cccd246db3901898dec4836de9cd249292.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a5f896d99a67b94c621deda3f69392cccd246db39018989ec4836de9ed249292.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a5f896d99a66b94c621deda3f69392cccd646db3901898dec4836de9cd249292.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a5f896d99a67b94c621ceda3f6d392cccd246db3901898dec4836de9cd249292.png", "https://scitools.github.io/test-iris-imagehash/images/v4/edfa96cb9a256b4f65466d9892d9c865693a1a9c94b39ed8484b35ad9a864c32.png" - ], + ], "iris.tests.test_plot.Test1dQuickplotScatter.test_cube_cube.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/a4fb19b3db04c6cd6307b98678601c738c39d71cf3866186d8616e69bd191b9e.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a4fb19b3db04c6cd6307b98678601c738c39d71cf3866186d8616e69bd191b9e.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e8b33c129649c78de3a773e578650c728e92279be12de1edc4f246b2939c3b01.png" - ], + ], "iris.tests.test_plot.Test1dScatter.test_coord_coord.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/bbfac39d9899384a6f6694a7b613cb489c95b7b7c24a399cc5913262d84acda0.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bbfac39d9899384a6f6694a7b613cb489c95b7b7c24a399cc5913262d84acda0.png", "https://scitools.github.io/test-iris-imagehash/images/v4/b2ecc12999994e16e666b5e3ce171969a5fb4ed49909e53990c44b9b7c09f684.png" - ], + ], "iris.tests.test_plot.Test1dScatter.test_coord_coord_map.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/bea07c99c15eb16e9891ce50c742394a3ced6cb13390f1cc73c29f1b2d0ecd66.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bea07c99c15eb16e9891ce50c742394a3ced6cb13390f1cc73c29f1b2d0ecd66.png", "https://scitools.github.io/test-iris-imagehash/images/v4/bea06899c14eb16e9895ce46c74a396a74ed64b13390b3c61b439f1b4d2ccde6.png" - ], + ], "iris.tests.test_plot.Test1dScatter.test_coord_cube.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/af7e1f0ff1e1e0f0d918960e6c076d8bd266d868c537365a90966db0de0e1b09.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/ae7e1f0ff1e1e0f0d918960e6c076d83d266d868c537365ad0966db0de4e1b09.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/af7e1f0ff1e1e0f0d918960e6c076d8bd266d868c537365a90966db0de0e1b09.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ae7e1f0ff1e1e0f0d918960e6c076d83d266d868c537365ad0966db0de4e1b09.png", "https://scitools.github.io/test-iris-imagehash/images/v4/be812fc1c078c03e930e3627672369c1d8d85c5b96463662e1fc699d9b9f7e03.png" - ], + ], "iris.tests.test_plot.Test1dScatter.test_cube_coord.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/edf896d79a67b94c651ced23d29392cccd646d33901912fcc4836d69ed249292.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/edf896d79a67b94c651ced23d29392cccd646d33901912fcc4836d69ed249292.png", "https://scitools.github.io/test-iris-imagehash/images/v4/edda96cb9a256b4765c26d9892dbc665693a1a9494b796c86c4b37ad92864c32.png" - ], + ], "iris.tests.test_plot.Test1dScatter.test_cube_cube.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/acf939339a16c64de306318638673c738c19d71cf3866186d8636e69bd191b9e.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/acf939339a16c64de306318638673c738c19d71cf3866186d8636e69bd191b9e.png", "https://scitools.github.io/test-iris-imagehash/images/v4/edb23c529649c78de38773e538650c729e92279be12de1edc4f246b2139c3b01.png" - ], + ], "iris.tests.test_plot.TestAttributePositive.test_1d_positive_down.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/87fef8117980c7c160078f1ffc049e7e90159a7a95419a7e910dcf1ece19ce3a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/87fef8117980c7c160078f1ffc049e7e90159a7a95419a7e910dcf1ece19ce3a.png", "https://scitools.github.io/test-iris-imagehash/images/v4/a7fe781b708487c360079e3bb4789869816bdb64c76b4a3cce7b4e749a6130c5.png" - ], + ], "iris.tests.test_plot.TestAttributePositive.test_1d_positive_up.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/87ff85d47800bd9f660779d0863f49c9947f4e1e9141de38d700da28ce1d9a2b.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/87ff85d47a00bc9f660779d8863f49c9907f4e1e9141de38d708da28ce1d9a0b.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/87ff85d47800bd9f660779d0863f49c9947f4e1e9141de38d700da28ce1d9a2b.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/87ff85d47a00bc9f660779d8863f49c9907f4e1e9141de38d708da28ce1d9a0b.png", "https://scitools.github.io/test-iris-imagehash/images/v4/a7ff958b7a00b09c661761c9907fcb0d9163ce7895289a618f381bffccf97200.png" - ], + ], "iris.tests.test_plot.TestAttributePositive.test_2d_positive_down.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fb946ba684e194fb901b3a0587641ad03b1ae7674e64c15a5b99c767c47e3a98.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fb946ba684e194fb901b3a0587641ad03b1ae7674e64c15a5b99c767c47e3a98.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fb946ba484e194dbc01f3665c0e4399a3f0fc2653f90c99e3f613e64c81e3f81.png" - ], + ], "iris.tests.test_plot.TestAttributePositive.test_2d_positive_up.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ee176c7f93e093a0c50f9383815e6e156859e17e6e15e17a9be08e2d851a9b83.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ee176c7f93e093a0c50f9383815e6e156859e17e6e15e17a9be08e2d851a9b83.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ebc06be1941e941ec07f941f907f6fa0950fc07e6f80c07f6b806be1c07f3f80.png" - ], + ], "iris.tests.test_plot.TestContour.test_tx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/cff8a55f7a15b55a7817854ad007a5e8c04f3ce8c04f3e2ac4706ab295b37a96.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/cff8a55f7a15b55a7817854ad007a5e8c04f3ce8c04f3e2ac4706ab295b37a96.png", "https://scitools.github.io/test-iris-imagehash/images/v4/eaece0173d17951fbd03974a914964e8c04a72e8c1531ee1cc746bb293973ecd.png" - ], + ], "iris.tests.test_plot.TestContour.test_ty.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8bfc815e78018597fc019b65b425d121955e7eda854b7d6a80db7eb481b72b61.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8bfc815e78018597fc019b65b425d121955e7eda854b7d6a80db7eb481b72b61.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ebfa8553fc01b15ab4044a269546caa5956b7e9bc0b97f2cc2d62d360b363b49.png" - ], + ], "iris.tests.test_plot.TestContour.test_tz.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8bfe81ff780185fff800955ad4027e00d517d400855f7e0085ff7e8085ff6aed.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/8bfe81ff780085fff800855fd4027e00d517d400855f7e0085ff7e8085ff6aed.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8bfe81ff780185fff800955ad4027e00d517d400855f7e0085ff7e8085ff6aed.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8bfe81ff780085fff800855fd4027e00d517d400855f7e0085ff7e8085ff6aed.png", "https://scitools.github.io/test-iris-imagehash/images/v4/8bfe817ffc00855ef0007e81d4027e80815fd56a03ff7a8085ff3aa883ff6aa5.png" - ], + ], "iris.tests.test_plot.TestContour.test_yx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa56c3cc34e891b1c9a91c36c5a170e3c71b3e5993a784e492c49b4ecec76393.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa56c3cc34e891b1c9a91c36c5a170e3c71b3e5993a784e492c49b4ecec76393.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e85e36cb95b199999765cd3694b06478c7396329958434c2cecb6c6d69ce1b92.png" - ], + ], "iris.tests.test_plot.TestContour.test_zx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8bfe857f7a01a56afa05854ad015bd00d015d50a90577e80857f7ea0857f7abf.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8bfe857f7a01a56afa05854ad015bd00d015d50a90577e80857f7ea0857f7abf.png", "https://scitools.github.io/test-iris-imagehash/images/v4/affe815ffc008554f8007e01d0027e808557d5ea815f7ea0817f2fea817d2aff.png" - ], + ], "iris.tests.test_plot.TestContour.test_zy.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8bff81ff7a0195fcf8019578d4027e00d550d402857c7e0185fe7a8385fe6aaf.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8bff81ff7a0195fcf8019578d4027e00d550d402857c7e0185fe7a8385fe6aaf.png", "https://scitools.github.io/test-iris-imagehash/images/v4/abff857ff8018578f8017a80d4027e00855ec42a81fe7a8185fe6a8f85fe6ab7.png" - ], + ], "iris.tests.test_plot.TestContourf.test_tx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/faa562ed68569d52857abd12953a8f12951f64e0d30f3ac96a4d6a696ee06a32.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/faa562ed68569d52857abd12953a8f12951f64e0d30f3ac96a4d6a696ee06a32.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea857a81957ac57e957a857a957a958ac5723b0d6ac56b833e856e606a923e90.png" - ], + ], "iris.tests.test_plot.TestContourf.test_ty.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/eaa5e03f957a4f80954a9e41e16e9c60970fb5b24ada634e6e93692d4ba562d8.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/eaa5e03f957a4f80954a9e41e16e9c60970fb5b24ada634e6e93692d4ba562d8.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea851f00957ac0f7957ac07f957a628d815e7b126ab13e816a953ae46a859ed3.png" - ], + ], "iris.tests.test_plot.TestContourf.test_tz.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e954a7a81857e957e857efc00857e7e007a85c02a7e859f287a85c1fe.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e954a7a81857e957e857efc00857e7e007a85c02a7e859f287a85c1fe.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e857e7a81857e7a81857e7a81857e7a806a85857a7a85857e7a85817e.png" - ], + ], "iris.tests.test_plot.TestContourf.test_yx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e95a6938b6b5969193901a4fc1e594a7c69999cbce33639879526e72330e65e4.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e95a6938b6b5969193901a4fc1e594a7c69999cbce33639879526e72330e65e4.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e97a3c7e968597b19685c9c696a7c79491c16e59691a387f6978396e68683184.png" - ], + ], "iris.tests.test_plot.TestContourf.test_zx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa85857ec45a7a81857e854a857ee56a917ec56a3a85c56a3a85c4ea7a8112fe.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa85857ec45a7a81857e854a857ee56a917ec56a3a85c56a3a85c4ea7a8112fe.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa81817e857e7a81857a7a81957a6e81917a6caa3a85c57a3a8585fa6a8591fe.png" - ], + ], "iris.tests.test_plot.TestContourf.test_zy.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa81817ec40a7a81857e957e857ef40a857ef60b7a81c40a7b81e60f7a814aff.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa81817ec40a7a81857e957e857ef40a857ef60b7a81c40a7b81e60f7a814aff.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa81817e857e7a81857e7a81817a7e81817a668f7a91857e7a81857e7a85817e.png" - ], + ], "iris.tests.test_plot.TestHybridHeight.test_bounds.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/eab5313f954a7b9260f39789c5ec4cd084d0c4e45aa1c5fe3a04797bb13b3b06.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/eab5313f954a7b9260f39789c5ec4cd084d0c4e45aa1c5fe3a04797bb13b3b06.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ee856aa5957a955ac0bf954bc17e3b819548c07f3e81c07e2ec46ea4c07f3e84.png" - ], + ], "iris.tests.test_plot.TestHybridHeight.test_bounds.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/be853f80854ac17ec0bdc2f5c17a0d09cc1fc07f5ab5e1fe3f409d7a38743e00.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/be853f80854ac17ec0bdc2f5c17a0d09cc1fc07f5ab5e1fe3f409d7a38743e00.png", "https://scitools.github.io/test-iris-imagehash/images/v4/bf813e85c07ec57ec17e9073c07e3f81856ec17a3f80c0fe3e813f84c2733e80.png" - ], + ], "iris.tests.test_plot.TestHybridHeight.test_bounds.2": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/eab5313f954a7b9260f39789c5ec4cd084d0c4e45aa1c5fe3a04797bb13b3b06.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/eab5313f954a7b9260f39789c5ec4cd084d0c4e45aa1c5fe3a04797bb13b3b06.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ee856aa5957a955ac0bf954bc17e3b819548c07f3e81c07e2ec46ea4c07f3e84.png" - ], + ], "iris.tests.test_plot.TestHybridHeight.test_orography.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa17291f95e895e8645e7a95c17a6eece4b4e1333b01c07e1bb13909914b9ec1.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/fa17291f95e895e8645e7a91c17a6ee464f4e1333b01c17e1bb1390d914b9ec1.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa17291f95e895e8645e7a95c17a6eece4b4e1333b01c07e1bb13909914b9ec1.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa17291f95e895e8645e7a91c17a6ee464f4e1333b01c17e1bb1390d914b9ec1.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa817a91957a857ac4fe268cc07f6e846e05d9373b81d17b1b6a1b41c4fa2cc4.png" - ], + ], "iris.tests.test_plot.TestHybridHeight.test_orography.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/bb07314fc4e0c6b4c31e9ee1847939a1c116c15e7b94e57e1ea9391de16e1ac3.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/bb07314fc6e1c6b4c31e9ee1846939a1c116c15e7b14e17e1ea9393de16e1ac3.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bb07314fc4e0c6b4c31e9ee1847939a1c116c15e7b94e57e1ea9391de16e1ac3.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bb07314fc6e1c6b4c31e9ee1846939a1c116c15e7b14e17e1ea9393de16e1ac3.png", "https://scitools.github.io/test-iris-imagehash/images/v4/af0b690f96f0d2d4c25e94a194ad3da19a52c25e3f02c07f3fa52d03c16a3fcb.png" - ], + ], "iris.tests.test_plot.TestHybridHeight.test_points.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea953bfb956ac4f4649f1a05c56e6ca45a53945e6ea5c13f1b498542c13f1b41.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea953bfb956ac4f4649f1a05c56e6ca45a53945e6ea5c13f1b498542c13f1b41.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fe857b91917a847ec0bd3f01c47e6ca43b11915a3ea4db3b1b4a84c4c03f3fc1.png" - ], + ], "iris.tests.test_plot.TestHybridHeight.test_points.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/be813fc0c15ac13dc1bfc27dc17e1d93c51fc43f1ea1c17a3ec138e4b1721a81.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/be813fc0c15ac13dc1bfc27dc17e1d93c51fc43f1ea1c17a3ec138e4b1721a81.png", "https://scitools.github.io/test-iris-imagehash/images/v4/be813a81c17ec57ec17e952ac07f3f808556c17e3f80c07f3e813f80c27e3f81.png" - ], + ], "iris.tests.test_plot.TestHybridHeight.test_points.2": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea9561ef956a7b92609b922dc16e6ec6845ac47e5aa5c57e5ec04861957b1b81.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea9561ef956a7b92609b922dc16e6ec6845ac47e5aa5c57e5ec04861957b1b81.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fe856a85957a955ac03f956ac17f3f809552c07f3e81c07e3e806e85c07e3f84.png" - ], + ], "iris.tests.test_plot.TestHybridHeight.test_points.3": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea953bfb956ac4f4649f1a05c56e6ca45a53945e6ea5c13f1b498542c13f1b41.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea953bfb956ac4f4649f1a05c56e6ca45a53945e6ea5c13f1b498542c13f1b41.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fe857b91917a847ec0bd3f01c47e6ca43b11915a3ea4db3b1b4a84c4c03f3fc1.png" - ], + ], "iris.tests.test_plot.TestHybridHeight.test_points.4": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/baf5347ecf0ac3f1c1f68f83850b1f83cc11c0fc7ad0c17a1be138e4b07e1a0d.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/baf5347ecf0ac3f1c1f68f83850b1f83cc11c0fc7ad0c17a1be138e4b07e1a0d.png", "https://scitools.github.io/test-iris-imagehash/images/v4/b878387e978ec2f0c0f09f83878f3f81c070c0fe78d0c1763fa13856d03e3f0f.png" - ], + ], "iris.tests.test_plot.TestMissingCS.test_missing_cs.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fac16ee0953b911bc15e9648e56ec4e691be7bcc7a8184733ea16a90c17e930d.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fac16ee0953b911bc15e9648e56ec4e691be7bcc7a8184733ea16a90c17e930d.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa816ac1857e853cc17f957ac15f3e849486c8f43e81c13b3f813e91c07e3f46.png" - ], + ], "iris.tests.test_plot.TestMissingCoord.test_no_u.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fe816a95c17fb51e953e9485857a1f409552856a1f81c17e5ab94e15c0ff5a85.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fe816a95c17fb51e953e9485857a1f409552856a1f81c17e5ab94e15c0ff5a85.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea856a95955a954ac17f954ac07e3f48951ec07e3e81c0ff7ea16a81c0bf3e81.png" - ], + ], "iris.tests.test_plot.TestMissingCoord.test_no_u.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/be0e695ac3f096b5943fd2a185fc1e8590e594ee1e05c17a4f403d0fe1fe4b42.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/be0e695ac3f096b5943fd2a185fc1e8590e594ee1e05c17a4f403d0fe1fe4b42.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea956ab5954a954ac17f954a817e3f40950ac07f3e81c0ff7a856aa1c0ff3f80.png" - ], + ], "iris.tests.test_plot.TestMissingCoord.test_no_v.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa8562b6c0773d09956a955a857a1d88845ec57e3f81c07e4ae56b21d0ff5a85.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa8562b6c0773d09956a955a857a1d88845ec57e3f81c07e4ae56b21d0ff5a85.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa816a85957a857ac17e954ac17e1fa2950bc07e3e81c07f3e807a85c17f3f81.png" - ], + ], "iris.tests.test_plot.TestMissingCoord.test_no_v.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa9562d4c7c43d0bb57b97e0857a3f1995d284763a05c17a7b856a2dc0f45a84.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa9562d4c7c43d0bb57b97e0857a3f1995d284763a05c17a7b856a2dc0f45a84.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa856a85957a857ac17e954ac17e9d02954ac07e3e81c07f3e857a85c2fd3f80.png" - ], + ], "iris.tests.test_plot.TestMissingCoord.test_none.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa8562b6c0763d09b54a955a857a3f88845ec57a3e85c07e6a616b25d0ff7a81.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa8562b6c0763d09b54a955a857a3f88845ec57a3e85c07e6a616b25d0ff7a81.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa816a85957a857ac17e954ac17e3fa29506c07e3e81c07f3e807a84c1ff3f81.png" - ], + ], "iris.tests.test_plot.TestMissingCoord.test_none.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa8562f6c0773d09b54a955a857a3f81955ac47e3e85c17e7aa16a25c0765aa1.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa8562f6c0773d09b54a955a857a3f81955ac47e3e85c17e7aa16a25c0765aa1.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa856a85957a957ac17e954ac17a1f06954ac07e3e81c07f3e817a85c0ff3f80.png" - ], + ], "iris.tests.test_plot.TestPcolor.test_tx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e85e67c9c7e1391e97a596b03a3696a13c4f63066318695ec5c9695e6c49c6a5.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e85e67c9c7e1391e97a596b03a3696a13c4f63066318695ec5c9695e6c49c6a5.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea817a81957e857e957e953e957e857e857e6aa06a816ac16a017a816a9585fa.png" - ], + ], "iris.tests.test_plot.TestPcolor.test_ty.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea74c707958b3878958b38f8c7236a557a542c7868d54b877875978abc789722.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/ea74c707958b387895ab38f8c7236a557a542c7868d54b05787197eab478972a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea74c707958b3878958b38f8c7236a557a542c7868d54b877875978abc789722.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea74c707958b387895ab38f8c7236a557a542c7868d54b05787197eab478972a.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea953f83954ac2bc956ac07e956a3509c0de61796ab57a816a856ad16ab590fb.png" - ], + ], "iris.tests.test_plot.TestPcolor.test_tz.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e874978b978b6875978b6875978b7854950b78506855787468747ea2687597aa.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e874978b978b6875978b6875978b7854950b78506855787468747ea2687597aa.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e857a7a84857a7a85857e7a813a2f7a817a85857a7a85857a7a85857a.png" - ], + ], "iris.tests.test_plot.TestPcolor.test_yx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e95e696994b196b793b19a1ec3c191c5c6e596191e4e693269336c36391a6e3a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e95e696994b196b793b19a1ec3c191c5c6e596191e4e693269336c36391a6e3a.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e97a387e968596319697c3c19284a62c93a560c36933393a6c7e793b6c6b31cd.png" - ], + ], "iris.tests.test_plot.TestPcolor.test_zx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e85e87a197a1695a97a1695a97a17d5a97a17906785a7816685a7e86685ad687.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e85e87a197a1695a97a1695a97a17d5a97a17906785a7816685a7e86685ad687.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e857a7e01857e7a81857e7a81e8177a816a8585fa7a85857e7a81857e.png" - ], + ], "iris.tests.test_plot.TestPcolor.test_zy.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/af42c0bdd0ad2f52d0bd3f42d0bd7f02d0bd7f003d527f002f427ea82f42d6a8.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/af42c0bdd0ad2f52d0ad2b52d0bd7f02d0bd7f002d527f002f527e0d2f52d4ad.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/af42c0bdd0ad2f52d0bd3f42d0bd7f02d0bd7f003d527f002f427ea82f42d6a8.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/af42c0bdd0ad2f52d0ad2b52d0bd7f02d0bd7f002d527f002f527e0d2f52d4ad.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e857e7e80857e7a81857e7a812d557a817a85857e7a81857e7a80857e.png" - ], + ], "iris.tests.test_plot.TestPcolorNoBounds.test_tx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa1594f3858a670c94e37b1cccb13e736a1d8cf17a1f94e2c119938e9463678c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa1594f3858a670c94e37b1cccb13e736a1d8cf17a1f94e2c119938e9463678c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea858782957a703f957a3878957a7a65957a6bc06ae76f806ad50fd06a859c50.png" - ], + ], "iris.tests.test_plot.TestPcolorNoBounds.test_ty.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ad5e94a5c3b0c3f096a5695a96a53c0f711b3c0f7d1b97b46943c3e0cc416b5a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ad5e94a5c3b0c3f096a5695a96a53c0f711b3c0f7d1b97b46943c3e0cc416b5a.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea85857a857e7e81957a7a81957a6a85857acac6c1fb6aa67a81956e6a81b506.png" - ], + ], "iris.tests.test_plot.TestPcolorNoBounds.test_tz.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/a95e3c1f96a096a5d6a5eb40c3f0ebe0c1c0c3f07c0b3e3e96a13c1e6d5b694a.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/a95e381f96a096a5d6a5eb40c3f0ebf0c1e0c3f07c0a3e3e96a13c1e6d5b694a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a95e3c1f96a096a5d6a5eb40c3f0ebe0c1c0c3f07c0b3e3e96a13c1e6d5b694a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a95e381f96a096a5d6a5eb40c3f0ebf0c1e0c3f07c0a3e3e96a13c1e6d5b694a.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa817e81857e857a857e7a81857e6a85817b81e63a913e857e81c17e7a81956e.png" - ], + ], "iris.tests.test_plot.TestPcolorNoBounds.test_yx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/bc7a1c32d3c366cdc585c39986cdc79ec792e3a6960d584939793c3438743873.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bc7a1c32d3c366cdc585c39986cdc79ec792e3a6960d584939793c3438743873.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e96ac78796953c4c9685383996c538e69692637263696b49693ac796693ac71b.png" - ], + ], "iris.tests.test_plot.TestPcolorNoBounds.test_zx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea1f781f95e085e885e0954295e195ea95a085e87a153e7f95e06a1778557a1f.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea1f781f95e085e885e0954295e195ea95a085e87a153e7f95e06a1778557a1f.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa817a81857e857e857e7a81857e6a81c17f95786aa77a807e81c17c7e819558.png" - ], + ], "iris.tests.test_plot.TestPcolorNoBounds.test_zy.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ba173a1795e895e8c5e8f400c1f8c1f895a8c5e87a077a5ec5e83e173e177e02.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ba173a1795e895e8c5e8f400c1f8c1f895a8c5e87a077a5ec5e83e173e177e02.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa817a80857e857e857e7a81817e3e81817e857f6aa07a857e80c17f7e80c15f.png" - ], + ], "iris.tests.test_plot.TestPcolormesh.test_tx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e85e67c9c7e1391e97a596b03a3696a13c4fe3026318695ec5c9695e6c49c6a5.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e85e67c9c7e1391e97a596b03a3696a13c4fe3026318695ec5c9695e6c49c6a5.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea817a81957e857e957e953e957e857e857e6aa06a816ac16a017a816a9585fa.png" - ], + ], "iris.tests.test_plot.TestPcolormesh.test_ty.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea74c707958b3878958b38f8c7236a557a542c7868d54b877875978abc789722.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/ea74c707958b387895ab38f8c7236a557a542c7868d54b05787197eabc789722.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea74c707958b3878958b38f8c7236a557a542c7868d54b877875978abc789722.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea74c707958b387895ab38f8c7236a557a542c7868d54b05787197eabc789722.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea953f83954ac2fc956ac07e956a3509c0de61796ab57a816a854ad16ab590fb.png" - ], + ], "iris.tests.test_plot.TestPcolormesh.test_tz.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e874978b978b6875978b6875978b7854950b78506855787468747ea2687597aa.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e874978b978b6875978b6875978b7854950b78506855787468747ea2687597aa.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e857a7a84857a7a85857e7a813a2f7a817a85857a7a85857a7a85857a.png" - ], + ], "iris.tests.test_plot.TestPcolormesh.test_yx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e95e696994b196b593b19a1ec3c591c5c6e596191e4e693269336c36391a6e3a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e95e696994b196b593b19a1ec3c591c5c6e596191e4e693269336c36391a6e3a.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e9693878969696139296c38f9bcc3474692169cb6c7339393c6cc387c78796cc.png" - ], + ], "iris.tests.test_plot.TestPcolormesh.test_zx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e85e87a197a1695a97a16d5a97a17d5a97a17806785a7816685a7e86685ad687.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e85e87a197a1695a97a16d5a97a17d5a97a17806785a7816685a7e86685ad687.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e857e6e05857e7a81857e7a81a0577a816a8585fa7a85857e7a81857e.png" - ], + ], "iris.tests.test_plot.TestPcolormesh.test_zy.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/af42c0bdd0ad2f52d0bd3f42d0bd7f02d0bd7f002d527f002f427fa82f42d6a8.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/af4280bdd0ad2f52d0ad2b52d0bd7f02d0bd7f002d527f002f527f0d2f52d4ad.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/af42c0bdd0ad2f52d0bd3f42d0bd7f02d0bd7f002d527f002f427fa82f42d6a8.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/af4280bdd0ad2f52d0ad2b52d0bd7f02d0bd7f002d527f002f527f0d2f52d4ad.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e857e7e80857e7a81857e7a8129577a817a85857e7a81857e7a80857e.png" - ], + ], "iris.tests.test_plot.TestPcolormeshNoBounds.test_tx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa1594f3858a670c94e37b1cccb13e736a1d84f17a1d94e2c11d938e9463678e.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/fa1594f3858a670c94e37b1cccb13e736a1d8cf17a1d94e2c11993ae9463678c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa1594f3858a670c94e37b1cccb13e736a1d84f17a1d94e2c11d938e9463678e.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa1594f3858a670c94e37b1cccb13e736a1d8cf17a1d94e2c11993ae9463678c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea858782957a703f957a3878957a7a65957e6bc06ae56f806ad50fd06a859c50.png" - ], + ], "iris.tests.test_plot.TestPcolormeshNoBounds.test_ty.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ad5e94a5c3b0c3f096a1695a96a53c1f711b3c0f791b97b46943c3e06c436b5a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ad5e94a5c3b0c3f096a1695a96a53c1f711b3c0f791b97b46943c3e06c436b5a.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea85857a857e7e81957a7a81957a6a85857acae6c1fb6aa47a81956e6a81b506.png" - ], + ], "iris.tests.test_plot.TestPcolormeshNoBounds.test_tz.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/a95e3c1f96a096a5d6a56b40c3f06be2c1c0c3f07c0b3ebe96a13c1e6d5b694a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a95e3c1f96a096a5d6a56b40c3f06be2c1c0c3f07c0b3ebe96a13c1e6d5b694a.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa813e81857e857a857e7a81857e6a85817b00e63eb93e857e81c17e7a81956e.png" - ], + ], "iris.tests.test_plot.TestPcolormeshNoBounds.test_yx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/bc7a1c32d3c366cdc785c39986cdc78ec792e7a6960d584939793c3438703873.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bc7a1c32d3c366cdc785c39986cdc78ec792e7a6960d584939793c3438703873.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e129c7169ed638ec9ed6387196c761c665396724612dcf0d693896929ed698c9.png" - ], + ], "iris.tests.test_plot.TestPcolormeshNoBounds.test_zx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea1f781f95e085e895e0fd4295e095ea95a085e87a153e7e95e06a1778157a17.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea1f781f95e085e895e0fd4295e095ea95a085e87a153e7e95e06a1778157a17.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa817a85857a857e857e7a81857e7a81817f95506aaf7a807e81c17c7a81957a.png" - ], + ], "iris.tests.test_plot.TestPcolormeshNoBounds.test_zy.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ba176a1795e895e8c5e87c00c1f8c1f894a8c5e87a077adec5e83e173e177a06.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ba176a1795e895e8c5e87c00c1f8c1f894a8c5e87a077adec5e83e173e177a06.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa817a80857a857e857e7a81857e3e81817e2fc56aa07a857e80c17f7e80c17f.png" - ], + ], "iris.tests.test_plot.TestPlot.test_t.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/83fe955f6a05e5137305d9c4f443127195187e9cd5467fa3d4917b68fc007a1a.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/8ffe95027e05e7007305d9c4a447127f853f069f814f2fa7d4d12b6cfc007e5a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/83fe955f6a05e5137305d9c4f443127195187e9cd5467fa3d4917b68fc007a1a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8ffe95027e05e7007305d9c4a447127f853f069f814f2fa7d4d12b6cfc007e5a.png", "https://scitools.github.io/test-iris-imagehash/images/v4/8ffe9c1a7e05e718f305d9d2e46312718138049e824e2fa783db2bed76b4fe00.png" - ], + ], "iris.tests.test_plot.TestPlot.test_t_dates.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/abffd5ae2a15cdb6b10178d7d4082e57d7290906f685814277b1dc88724cfd26.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/abffd5ae2a15c9b6a10178d7d4082c57d7290906f6c58942f7b1dc88724cfd26.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/abffd4a02a01cc84f10078d7d4082c77d73909ded6ef816273bd9c98725cdd26.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/abffd5ae2a15cdb6b10178d7d4082e57d7290906f685814277b1dc88724cfd26.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/abffd5ae2a15c9b6a10178d7d4082c57d7290906f6c58942f7b1dc88724cfd26.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/abffd4a02a01cc84f10078d7d4082c77d73909ded6ef816273bd9c98725cdd26.png", "https://scitools.github.io/test-iris-imagehash/images/v4/87fc9d8a7e054d83f5067bc1c1423471927ba73c8d9f864e09a1a7b358c8276f.png" - ], + ], "iris.tests.test_plot.TestPlot.test_x.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8ffe95297e87c74a6a059158f89c3d6ed0536597c0387836d0f87866d0697097.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8ffe95297e87c74a6a059158f89c3d6ed0536597c0387836d0f87866d0697097.png", "https://scitools.github.io/test-iris-imagehash/images/v4/8bfe956b7c01c2f26300929dfc1e3c6690736f91817e3b0c84be6be5d1603ed1.png" - ], + ], "iris.tests.test_plot.TestPlot.test_y.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8fe896266f068d873b83cb71e435725cd07c607ad07e70fcd0007a7881fe7ab8.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/8fe896066f068d873b83cb71e435725cd07c607ad07c70fcd0007af881fe7bb8.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/8fe896366f0f8d93398bcb71e435f24ed074646ed07670acf010726d81f2798c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8fe896266f068d873b83cb71e435725cd07c607ad07e70fcd0007a7881fe7ab8.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8fe896066f068d873b83cb71e435725cd07c607ad07c70fcd0007af881fe7bb8.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8fe896366f0f8d93398bcb71e435f24ed074646ed07670acf010726d81f2798c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/aff8946c7a14c99fb193d263e42432d8d00c2d27944a3f8dc5223ef703ff6b90.png" - ], + ], "iris.tests.test_plot.TestPlot.test_z.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8ffac1547a0792546c179db7f1254f6d945b7392841678e895017e3e91c17a0f.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8ffac1547a0792546c179db7f1254f6d945b7392841678e895017e3e91c17a0f.png", "https://scitools.github.io/test-iris-imagehash/images/v4/8ff8c1fa7a05b4ea6c059d2ff1494e4b90f26304846d78d1872a6cfc938b2e3e.png" - ], + ], "iris.tests.test_plot.TestPlotCitation.test.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/abf895067a1d9506f811783585437abd85426ab995067af9f00687f96afe87c8.png" - ], + ], "iris.tests.test_plot.TestPlotCitation.test_axes.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/abf895067a1d9506f811783585437abd85426ab995067af9f00687f96afe87c8.png" - ], + ], "iris.tests.test_plot.TestPlotCitation.test_figure.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/abf895067a1d9506f811783585437abd85426ab995067af9f00687f96afe87c8.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_non_cube_coordinate.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e857e7e81857e7a81857e7a81857e7a818576c02a7e95856a7e81c17a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e857e7e81857e7a81857e7a81857e7a818576c02a7e95856a7e81c17a.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e857e3e85857e7a81857e7a81857e7a817e81780b7a81c56a7a81857e.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_tx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fe8142f5c17ebd2cc16eb548954a9542916a347a915e60bd4afd68793f916296.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fe8142f5c17ebd2cc16eb548954a9542916a347a915e60bd4afd68793f916296.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea853f10956ac5e1957a854e957a203e955e6aa76ae17aa16a856aaf6ab19e12.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_tx.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa8542b7b503b548857abd08857abd09945eed6b91d968c161b972d76aa462b5.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/fa8542b7b503b548857abd08857abd09945eed6a91d96ac163b972d36aa462b5.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa8542b7b503b548857abd08857abd09945eed6b91d968c161b972d76aa462b5.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa8542b7b503b548857abd08857abd09945eed6a91d96ac163b972d36aa462b5.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea853a85857a857a957a857a957ad05a857b3e946a606b917a816f647a853af4.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_tx.2": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8bf88f457a03b5307e16b561f007b53ed067217ac1786afec0f570bf8178681a.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/8bf98f057a03b5307e16b561f007b53ad067217ac1786afec0f570bf8178685a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8bf88f457a03b5307e16b561f007b53ed067217ac1786afec0f570bf8178681a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8bf98f057a03b5307e16b561f007b53ad067217ac1786afec0f570bf8178685a.png", "https://scitools.github.io/test-iris-imagehash/images/v4/eafdcec9bc219530b696a56694c2852a95656b7b81986acdc0e516adad186eda.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_tx.3": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8ffe8f367e05952afe05a50b980ded4bd05d69c2c1fb71c1c06272f4d0a06af4.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8ffe8f367e05952afe05a50b980ded4bd05d69c2c1fb71c1c06272f4d0a06af4.png", "https://scitools.github.io/test-iris-imagehash/images/v4/aff24ab7bd05952fbd0f950f914fcd48c47860f3e1b9329094266e345a850f6c.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_tx.4": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/aa953d0f85fab50fd0f2956a7a1785fafa176877d00f68f1d02c60f2f008d0f0.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/ebeaa5419e94b5019e97950d685395bee05361fad05560fad01570fef001dabe.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/ebeaa5419e95b5419e97950d6853953ee053617ad05560fad01570fef001dabe.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/aa953d0f85fab50fd0f2956a7a1785fafa176877d00f68f1d02c60f2f008d0f0.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ebeaa5419e94b5019e97950d685395bee05361fad05560fad01570fef001dabe.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ebeaa5419e95b5419e97950d6853953ee053617ad05560fad01570fef001dabe.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ebfaa56f96a1856cd681a56ee8162d52e8467e12c50c7e8095ad7e0095ad03ff.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_tx.5": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ebfaaf439e87b5019687b5019687b56ac05561fae07103fe6079687a607178f8.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/ebfa2d4b968795059e87970f6854697ae055697ac08561fad041d7aef001d6ae.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ebfaaf439e87b5019687b5019687b56ac05561fae07103fe6079687a607178f8.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ebfa2d4b968795059e87970f6854697ae055697ac08561fad041d7aef001d6ae.png", "https://scitools.github.io/test-iris-imagehash/images/v4/eb7a3e0c978187a4950190bc6856687a607e687bc0fcc1e394acfc0197fc2bfb.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_x.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/aeb8b5095a87cd60386592d9ec97ad6dd23ca4f6d0797827f0096216c1f878e6.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/aeb8b5095a87cd60386592d9ec97ad6dd23ca4f6d0797827f0096216c1f878e6.png", "https://scitools.github.io/test-iris-imagehash/images/v4/affa950ddb13c03634359ad8a4c80f26911f26f3c06e0ff3f4007b4285fd6e72.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_y.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8fea97194f07c9c830d79169ce16269f91097af6c47861f6d0796076d0797a16.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/8fee970b4f07c9c930d79129ce16269f91097af6c4f861f4d0786076d0797a16.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/afea97094f07c9c870d79129ce16269f91096af6c4f861f6c07960f6d0797a16.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8fea97194f07c9c830d79169ce16269f91097af6c47861f6d0796076d0797a16.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8fee970b4f07c9c930d79129ce16269f91097af6c4f861f4d0786076d0797a16.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/afea97094f07c9c870d79129ce16269f91096af6c4f861f6c07960f6d0797a16.png", "https://scitools.github.io/test-iris-imagehash/images/v4/afee9632de05c9d9f180d168c454a53e931b3e84956a3b8c85d94ce703ff7284.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_yx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea85603f956a9741951e9d83c1fa8d2fd0a55af0d25f345ae5f062c72d68612d.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea85603f956a9741951e9d83c1fa8d2fd0a55af0d25f345ae5f062c72d68612d.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea853f00957ac07c957ac0be951a69f3c47c7a5f3a6127816b953e646b813761.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_yx.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e85a69cc96ad92e193c9963385929e1cc3819acde6d965ce6e666b30386e65b1.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e85a69cc96ad92e193c9963385929e1cc3819acde6d965ce6e666b30386e65b1.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e97a346c9685cb899685c9c39695c79396ec634969ce2c74697a3864697b3c8c.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_yx.2": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8ffcc65767039740bc069d9ad00b8dadd03f52f181dd347a847a62ff81e8626c.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/8ffcc65777039740bc069d9ad00b8dadd03d52f181dd707a847a62ff81e8626c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8ffcc65767039740bc069d9ad00b8dadd03f52f181dd347a847a62ff81e8626c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8ffcc65777039740bc069d9ad00b8dadd03d52f181dd707a847a62ff81e8626c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ebffca44f502b36498309c9b940999add1bb62bba784374acc5a6a246acc6b65.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_yx.3": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea5649c434ac92e5d9c9361b95b39c38c3835a5ec6d966ced34c633099ace5a5.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea5649c434ac92e5d9c9361b95b39c38c3835a5ec6d966ced34c633099ace5a5.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e85a6b6c96a597a591c9949b94b61b69c7926b5bccce66646b3869b831a52c26.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_yx.4": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ad2f6d2dd2d09295c3c0c7d13c1bc6d23d2c696de0e53c3ac393daf6d205c2c4.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/ad2f6d2fd2d09295c3c0c7d13c1bc6d23d2c696ce0e53c3ac393dbf6d205c2c0.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/ad2f6d2f92d09295c3d0c7d13c1bc6d23d2c696cf0e53c3ac2b3d9f6d201c2c4.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/e85e3e2f97a1c19996a1c8f26c1e360f684a3c2c6913dca497b9d38097a903ff.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ad2f6d2dd2d09295c3c0c7d13c1bc6d23d2c696de0e53c3ac393daf6d205c2c4.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ad2f6d2fd2d09295c3c0c7d13c1bc6d23d2c696ce0e53c3ac393dbf6d205c2c0.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ad2f6d2f92d09295c3d0c7d13c1bc6d23d2c696cf0e53c3ac2b3d9f6d201c2c4.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e85e3e2f97a1c19996a1c8f26c1e360f684a3c2c6913dca497b9d38097a903ff.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e85e3e3f96a1c3e197a169f1785e3b0e68523e1c398bc58687b1d86096e1039f.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_yx.5": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e9686d8c9696924797879e3b86929e58696d69cc6869659379626133398d9ccd.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/e961658f961e92469e1e1c7966f36cd86165618c70e166b39b9698719e1e9ec8.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e9686d8c9696924797879e3b86929e58696d69cc6869659379626133398d9ccd.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e961658f961e92469e1e1c7966f36cd86165618c70e166b39b9698719e1e9ec8.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e1a530e29e5ecf199a5acd8f64f1326161a530e265999cd29e52cf199a5e6669.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_zx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/bf803f00c05fc4bfc07ec15dc05fd8bbc07cc96c333a32113bd02dd27ced3ec0.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bf803f00c05fc4bfc07ec15dc05fd8bbc07cc96c333a32113bd02dd27ced3ec0.png", "https://scitools.github.io/test-iris-imagehash/images/v4/be813ea0c17ec55ac17ed23dc07e295ac57e3b653f803f813e816e853e81b542.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_zx.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea95956a95626993941a6a2d956e6ed6845a6e65c4bec7b64a9594686ea19578.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea95956a95626993941a6a2d956e6ed6845a6e65c4bec7b64a9594686ea19578.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea85856e857e4893957a7aa1956a7b81954b3b817a856fd46a85846e6e85857e.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_zx.2": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8fe82f047c018c83bc01bc5af01fd1bcd15a327c847860fdc57a69beb0be68bd.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/8fe82f047c018c83bc01bc5af01fd1bcd15a32fd847860fdc57269beb0be689d.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8fe82f047c018c83bc01bc5af01fd1bcd15a327c847860fdc57a69beb0be68bd.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8fe82f047c018c83bc01bc5af01fd1bcd15a32fd847860fdc57269beb0be689d.png", "https://scitools.github.io/test-iris-imagehash/images/v4/8bedcf25bc03a4929c103a5bf03fdbbc81cb364d86e46da70f86899b3a0f6cc0.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_zx.3": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/cee8953a7a15856978579696d03d672cc49a6e5a842d3d2cc0b66bd1c2ea39f1.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/cee8953a7a15856978579696d03d672cc49a6e5a842d3d2cc0b66bd1c2ea39f1.png", "https://scitools.github.io/test-iris-imagehash/images/v4/aee1f93a63168569b852d697913d632485ca2e43952d3bbcc2b66bd1426b3c71.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_zx.4": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ee953f0591ea3f07914a95fa7e07d1fa68156a15d07c6a3dd038c0fef000d0fa.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/ae953f0591ea3f07914a95fa7e07d1fa68156a15d07c6a7dd068c0fef000d0fa.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ee953f0591ea3f07914a95fa7e07d1fa68156a15d07c6a3dd038c0fef000d0fa.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ae953f0591ea3f07914a95fa7e07d1fa68156a15d07c6a7dd068c0fef000d0fa.png", "https://scitools.github.io/test-iris-imagehash/images/v4/bec11ab5c1be857ac13e7ae53c422d423e017a85b542fc00c1fefe0091fe03ff.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_zx.5": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e87a973d96a56953968769439685a54ae05117eae0511fba60513bba69717aba.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/e87a952d96a56953968769439685a54ae85197eae0511fba60513bba69717aba.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e87a973d96a56953968769439685a54ae05117eae0511fba60513bba69717aba.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e87a952d96a56953968769439685a54ae85197eae0511fba60513bba69717aba.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e85a96ac97a16c5897a1791e95a53b0b913c6953687c4ec3685cc6c36e7c87c3.png" - ], + ], "iris.tests.test_plot.TestPlotDimAndAuxCoordsKwarg.test_coord_names.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/f9789b388786678686966c9093879ce592c79bc94d19929b6939cf66316c672c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/f9789b388786678686966c9093879ce592c79bc94d19929b6939cf66316c672c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/b87830b0c786cf269ec766c99399cce998d3b3166f2530d3658c692d30ec6735.png" - ], + ], "iris.tests.test_plot.TestPlotDimAndAuxCoordsKwarg.test_coord_names.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e9a53a59961ec5a62c691a587b9662e1c0e1e53e9e0e9b873ec15a7161bc642f.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e9a53a59961ec5a62c691a587b9662e1c0e1e53e9e0e9b873ec15a7161bc642f.png", "https://scitools.github.io/test-iris-imagehash/images/v4/b8a53b59c71ac5a6b8791c1867876b63d9e0e65c96199d871cc23339633664ce.png" - ], + ], "iris.tests.test_plot.TestPlotDimAndAuxCoordsKwarg.test_coords.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/f9789b388786678686966c9093879ce592c79bc94d19929b6939cf66316c672c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/f9789b388786678686966c9093879ce592c79bc94d19929b6939cf66316c672c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/b87830b0c786cf269ec766c99399cce998d3b3166f2530d3658c692d30ec6735.png" - ], + ], "iris.tests.test_plot.TestPlotDimAndAuxCoordsKwarg.test_coords.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e9a53a59961ec5a62c691a587b9662e1c0e1e53e9e0e9b873ec15a7161bc642f.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e9a53a59961ec5a62c691a587b9662e1c0e1e53e9e0e9b873ec15a7161bc642f.png", "https://scitools.github.io/test-iris-imagehash/images/v4/b8a53b59c71ac5a6b8791c1867876b63d9e0e65c96199d871cc23339633664ce.png" - ], + ], "iris.tests.test_plot.TestPlotDimAndAuxCoordsKwarg.test_default.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/f9789b388786678686966c9093879ce592c79bc94d19929b6939cf66316c672c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/f9789b388786678686966c9093879ce592c79bc94d19929b6939cf66316c672c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/b87830b0c786cf269ec766c99399cce998d3b3166f2530d3658c692d30ec6735.png" - ], + ], "iris.tests.test_plot.TestPlotDimAndAuxCoordsKwarg.test_yx_order.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa81948e857e4971907ea72e95fa66b2952e4ead6d429b527ac7a5286e981836.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa81948e857e4971907ea72e95fa66b2952e4ead6d429b527ac7a5286e981836.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa85978e837e68f094d3673089626ad792073985659a9b1a7a15b52869f19f56.png" - ], + ], "iris.tests.test_plot.TestPlotDimAndAuxCoordsKwarg.test_yx_order.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea159694856a6b5096afa53a36941da1e4f5c369cd1ae6d69b6a1c80625af2f6.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea159694856a6b5096afa53a36941da1e4f5c369cd1ae6d69b6a1c80625af2f6.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea95969c874a63d39ca3ad2a231cdbc9c4973631cd6336c633182cbc61c3d3f2.png" - ], + ], "iris.tests.test_plot.TestPlotOtherCoordSystems.test_plot_tmerc.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e63399cd99cd64b29999335965369b262649c98c9b3966c6998d3319ccd69333.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e63399cd99cd64b29999335965369b262649c98c9b3966c6998d3319ccd69333.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e665326d999ecc9b3319b3246666cce69b496cccccc9669923193336666699a6.png" - ], + ], "iris.tests.test_plot.TestQuickplotPlot.test_t.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/83ffb5d67fd4e5962211d9c6a443da77d5389c8ed346d923d011d968dc00da48.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/82ffb5d67fdde5962211d9c6a441da77d5389c8cd346d927d011d968dc00da48.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/82fabd867fd5e5822201d9c6a4539a77953d8cbf834f99e7d051996cdc00da48.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/83ffb5d67fd4e5962211d9c6a443da77d5389c8ed346d923d011d968dc00da48.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/82ffb5d67fdde5962211d9c6a441da77d5389c8cd346d927d011d968dc00da48.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/82fabd867fd5e5822201d9c6a4539a77953d8cbf834f99e7d051996cdc00da48.png", "https://scitools.github.io/test-iris-imagehash/images/v4/83ffb59a7f00e59a2205d9d6e4619a74d9388c8e884e8da799d30b6dddb47e00.png" - ], + ], "iris.tests.test_plot.TestQuickplotPlot.test_t_dates.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/a3ffd5ae7f51efb6200378d7d4082c17d7280906d6e58962db31d800da6cdd26.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/a3ffd4ae7f55efbe200178d7d4082c17d7280906d6e58962df319800da6cdd26.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/a3ffd4827f51ef94200078d7c4082c57d739095ed6ed8962db759808da6cdd26.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a3ffd5ae7f51efb6200378d7d4082c17d7280906d6e58962db31d800da6cdd26.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a3ffd4ae7f55efbe200178d7d4082c17d7280906d6e58962df319800da6cdd26.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a3ffd4827f51ef94200078d7c4082c57d739095ed6ed8962db759808da6cdd26.png", "https://scitools.github.io/test-iris-imagehash/images/v4/82fd958a7e006f9ba0077bc5c9462c759873dd3c8d8f826699a187b358c82f67.png" - ], + ], "iris.tests.test_plot.TestQuickplotPlot.test_x.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/83ffb5097e84c54a621799d8601d9966d213cd67c039d876d078d866d869d8f7.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/83ffbd097e84c54a621799d8601d9966d253cc27c039d876d078d866d869d8f7.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/83ffb5097e84c54a621799d8601d9966d213cd67c039d876d078d866d869d8f7.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/83ffbd097e84c54a621799d8601d9966d253cc27c039d876d078d866d869d8f7.png", "https://scitools.github.io/test-iris-imagehash/images/v4/82ff950b7f81c0d6620199bcfc5e986695734da1816e1b2c85be2b65d96276d1.png" - ], + ], "iris.tests.test_plot.TestQuickplotPlot.test_y.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/a7ffb6067f008d87339bc973e435d86ef034c87ad07c586cd001da69897e5838.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/a7ffb6067f008d87339bc973e435d86ef034c87ad07cd86cd001da68897e58a8.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/a7efb6367f008d97338fc973e435d86ef030c86ed070d86cd030d86d89f0d82c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a7ffb6067f008d87339bc973e435d86ef034c87ad07c586cd001da69897e5838.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a7ffb6067f008d87339bc973e435d86ef034c87ad07cd86cd001da68897e58a8.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a7efb6367f008d97338fc973e435d86ef030c86ed070d86cd030d86d89f0d82c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/a2fbb46e7f10c99f2013d863e46498dcd06c0d2798421fa5dd221e7789ff6f10.png" - ], + ], "iris.tests.test_plot.TestQuickplotPlot.test_z.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/83ffc1dc7e00b0dc66179d95f127cfc9d44959ba846658e891075a3e99415a2f.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/83ffc1dc7e00b0dc66179d95f127cfc9d44959ba846658e891075a3e99415a2f.png", "https://scitools.github.io/test-iris-imagehash/images/v4/a3ffc1d87e00b49964179d28f16bce4b98724b268c6d58e1972e4874998b2e7e.png" - ], + ], "iris.tests.test_plot.TestSimple.test_bounds.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa9562fcc7c0b50bb53b9f8085727a157a95c0f67a85e07e0be08069e07d9fa0.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa9562fcc7c0b50bb53b9f8085727a157a95c0f67a85e07e0be08069e07d9fa0.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea856a85954a957ac17e954ac17a9c3e956ac07e3e81c07f3e857aa5c2753f80.png" - ], + ], "iris.tests.test_plot.TestSimple.test_points.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa8562b7c2763d09956a955a855a1d88d45ec57a3f81c07e6ae16b21c0ff7a81.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa8562b7c2763d09956a955a855a1d88d45ec57a3f81c07e6ae16b21c0ff7a81.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea856a85957a957ac17e954ac17e1ca2954ac07e3e81c07f3e807a85c1ff3f81.png" - ], + ], "iris.tests.test_plot.TestSymbols.test_cloud_cover.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e95a330c96a5ccf2695a330c96a5ccf2695a330c96b5ccf3694a330c96b5ccf3.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e95a330c96a5ccf2695a330c96a5ccf2695a330c96b5ccf3694a330c96b5ccf3.png", "https://scitools.github.io/test-iris-imagehash/images/v4/eb52916494ad6e1b6b5291e494ad6e1b6b5291e494ad6e1b6b5291e494ad6e1b.png" - ], + ], "iris.tests.test_quickplot.TestLabels.test_alignment.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa95350f952ad2f0c1f66ac1c55a4af4e550a52b3e05905e1e419e6f937e3b21.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/fa95350f952ad3f0c1f66a81e55a4af4e550a52b3e05905e1e419e6f937e1b21.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa95350f952ad2f0c1f66ac1c55a4af4e550a52b3e05905e1e419e6f937e3b21.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa95350f952ad3f0c1f66a81e55a4af4e550a52b3e05905e1e419e6f937e1b21.png", "https://scitools.github.io/test-iris-imagehash/images/v4/be8137f4954ac03fc0ff3e81d03f496a6d00b4af3ea0c07f6fa232c0db7f2d00.png" - ], + ], "iris.tests.test_quickplot.TestLabels.test_contour.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/a3fd956a7a01a5ee321fc96666919b6ec15fdca593600d2586785a259dfa5a01.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/a3fd956a7a01a5ee3217c9e66691996ec15fdca593680d2586785a259dfa5a01.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a3fd956a7a01a5ee321fc96666919b6ec15fdca593600d2586785a259dfa5a01.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a3fd956a7a01a5ee3217c9e66691996ec15fdca593680d2586785a259dfa5a01.png", "https://scitools.github.io/test-iris-imagehash/images/v4/a7fd95da7a01654a3217c962e4819a56c96f3c8593624da584da3b658db662db.png" - ], + ], "iris.tests.test_quickplot.TestLabels.test_contour.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/faa12bc1954ef43fc0bf9f02854a4ee48548c17a5ab5c17e7a0d7875a17e3a81.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/faa12bc1954ef43fc0bf9f02854a4ee48548c17a5ab5c17e7a0d7875a17e3a81.png", "https://scitools.github.io/test-iris-imagehash/images/v4/bf802f85c17fc17fc07eb42ac07f3f929130c07e3f80c07f7aa02e85c07f3e81.png" - ], + ], "iris.tests.test_quickplot.TestLabels.test_contourf.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fe812f88957a955a857a9257c17f7aa5c03dc0bf5a85c07e7f402d40a57a3f01.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fe812f88957a955a857a9257c17f7aa5c03dc0bf5a85c07e7f402d40a57a3f01.png", "https://scitools.github.io/test-iris-imagehash/images/v4/be816a95957a957ac0fe1e8bc07f7f806e01c07f3f80c07f3fa23f00c07f3d00.png" - ], + ], "iris.tests.test_quickplot.TestLabels.test_contourf.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/faa12bc1954ef43fc0bf9f02854a4ee48548c17a5ab5c17e7a0d7875a17e3a81.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/faa12bc1954ef43fc0bf9f02854a4ee48548c17a5ab5c17e7a0d7875a17e3a81.png", "https://scitools.github.io/test-iris-imagehash/images/v4/bf802f85c17fc17fc07eb42ac07f3f929130c07e3f80c07f7aa02e85c07f3e81.png" - ], + ], "iris.tests.test_quickplot.TestLabels.test_contourf.2": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa852f81955ac532c0bf9e89c57edae69357e13f4ea0c05a3f8561a4935a3e01.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa852f81955ac532c0bf9e89c57edae69357e13f4ea0c05a3f8561a4935a3e01.png", "https://scitools.github.io/test-iris-imagehash/images/v4/be816a95907ae508c17e955ac07f3fa0945bc07f3f80c07f3aa36f01c0ff3f80.png" - ], + ], "iris.tests.test_quickplot.TestLabels.test_contourf_nameless.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/faa52ec1955ac536c0bf9e09c57edae69357e13f4e80c0da2f81618493da3f01.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/faa52ec1955ac536c0bf9e09c57edae69357e13f4e80c0da2f81618493da3f01.png", "https://scitools.github.io/test-iris-imagehash/images/v4/be816af5907ee508c17e955ac03f3f809419c07f3f80c07f3a8b6f81c0ff3f80.png" - ], + ], "iris.tests.test_quickplot.TestLabels.test_map.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea5e618434ac36e5c1c9369b95b39c38c3a39a4fcee19a6e9b64cb609925cd25.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea5e618434ac36e5c1c9369b95b39c38c3a39a4fcee19a6e9b64cb609925cd25.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e85a636c86a597a593c9b49b94b79969c396c95bccc69a64db30d9b039a52c26.png" - ], + ], "iris.tests.test_quickplot.TestLabels.test_map.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea5e618434ac36e5c1c9369b95b39c38c3a39a4ecef19a6e9b64cb609925cd25.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea5e618434ac36e5c1c9369b95b39c38c3a39a4ecef19a6e9b64cb609925cd25.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e85a636c86a597a593c9b49b94b79969c396c95bccc69a64db30d9b039a52c26.png" - ], + ], "iris.tests.test_quickplot.TestLabels.test_pcolor.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/bb423d4e94a5c6b9c15adaadc1fb6a469c8de43a3e07904e5f016b57984e1ea1.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bb423d4e94a5c6b9c15adaadc1fb6a469c8de43a3e07904e5f016b57984e1ea1.png", "https://scitools.github.io/test-iris-imagehash/images/v4/eea16affc05ab500956e974ac53f3d80925ac03f2f81c07e3fa12da1c2fe3f80.png" - ], + ], "iris.tests.test_quickplot.TestLabels.test_pcolormesh.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/bb433d4e94a4c6b9c15adaadc1fb6a469c8de43a3e07904e5f016b57984e1ea1.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bb433d4e94a4c6b9c15adaadc1fb6a469c8de43a3e07904e5f016b57984e1ea1.png", "https://scitools.github.io/test-iris-imagehash/images/v4/eea16affc05ab500956e974ac53f3d80925ac03f3f81c07e3fa12da1c27e3f80.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_non_cube_coordinate.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa816a85857a955ae17e957ec57e7a81855fc17e3a81c57e1a813a85c57a1a05.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa816a85857a955ae17e957ec57e7a81855fc17e3a81c57e1a813a85c57a1a05.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fe816a85857a957ac07f957ac07f3e80956ac07f3e80c07f3e813e85c07e3f80.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_tx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa856a95e15ab51a953e9485857a1f409552857e1fc1c07e5abd4a35e07f4aa5.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa856a95e15ab51a953e9485857a1f409552857e1fc1c07e5abd4a35e07f4aa5.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea856a95955a956ac17f950ac07e3f48951ac07f3f81c0ff3ea16aa1c0be3e81.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_tx.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa8562b7c2763d09956a955a855a1d88d45ec57a3f81c07e6ae16b21c0ff7a81.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa8562b7c2763d09956a955a855a1d88d45ec57a3f81c07e6ae16b21c0ff7a81.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea856a85957a957ac17e954ac17e1ca2954ac07e3e81c07f3e807a85c1ff3f81.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_tx.2": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8aff878b7f00953062179561f087953ad167997a80784a7fc1e5d86d9978485f.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/8aff878b7f80953860179561f087953ad167997a80784a7fc1e5d86d9978485b.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8aff878b7f00953062179561f087953ad167997a80784a7fc1e5d86d9978485f.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8aff878b7f80953860179561f087953ad167997a80784a7fc1e5d86d9978485b.png", "https://scitools.github.io/test-iris-imagehash/images/v4/eafdc6c9f720953030968d6795d28d6a95674b7b81304aedc9e51cad8d186c9a.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_tx.3": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/82ff8db67f94952e76159d6bb01dcd629059c962c1fbd9c1c062da74d820ca74.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/82be8db67f95952e761d9d6bb01dcd628059c962c1fbd9e1c072da64d060ca74.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/82fe8db67f95952e76159d6bb01dcd629059c962c1fbd9e1c072da64d020ca74.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/82ff8db67f94952e76159d6bb01dcd629059c962c1fbd9c1c062da74d820ca74.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/82be8db67f95952e761d9d6bb01dcd628059c962c1fbd9e1c072da64d060ca74.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/82fe8db67f95952e76159d6bb01dcd629059c962c1fbd9e1c072da64d020ca74.png", "https://scitools.github.io/test-iris-imagehash/images/v4/a2ff6a967f00952eb40d9d0f900fcd62c47069f3d1f93a909c266e34d8a56f68.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_tx.4": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/aa97b70ff5f0970f20b2956a6a17957af805da71d06f5a75d02cd870d800d8f2.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/e1faa549de9497090697971d60539f3ef171c87ac075487ad025d87ed801da3e.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/aa97b70ff5f0970f20b2956a6a17957af805da71d06f5a75d02cd870d800d8f2.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e1faa549de9497090697971d60539f3ef171c87ac075487ad025d87ed801da3e.png", "https://scitools.github.io/test-iris-imagehash/images/v4/eadab54fd7a1856d90819d6df8169962e946d862802ed8809ded7e809d2d03ff.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_tx.5": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e8faad47f784bd0596859d03969f9962c05dc96ee07189fe6870c862687178f8.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/a8fa2d4797859585b6959d07605f896ee051697ad061d9fad0619aaed801deae.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e8faad47f784bd0596859d03969f9962c05dc96ee07189fe6870c862687178f8.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a8fa2d4797859585b6959d07605f896ee051697ad061d9fad0619aaed801deae.png", "https://scitools.github.io/test-iris-imagehash/images/v4/aa5b3c0c978187a4b60199bc605f6976687e6873d07c99e390acdc0391fc2f7b.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_x.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/a6ffb5097e84cde2224598d1649f8d6cd2388c76d0799867d009da76c9f8d866.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/a6bfb5097f84cde2224599d1649f8d6cd2388c76d0799867d009da76c1f8d866.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a6ffb5097e84cde2224598d1649f8d6cd2388c76d0799867d009da76c9f8d866.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a6bfb5097f84cde2224599d1649f8d6cd2388c76d0799867d009da76c1f8d866.png", "https://scitools.github.io/test-iris-imagehash/images/v4/a6fbb50cfbd0c036203598dce4c88d26d32f8cf3886e1df3dc047b4289ec6e72.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_y.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/a7ff978b7f00c9c830d7992166179e969509d866c478d964d079c876d869da26.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/a7ff97837f00c9c830d79921661f9e9695099876c478d964c079c876d879da26.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a7ff978b7f00c9c830d7992166179e969509d866c478d964d079c876d869da26.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a7ff97837f00c9c830d79921661f9e9695099876c478d964c079c876d879da26.png", "https://scitools.github.io/test-iris-imagehash/images/v4/a2ffb6127f0dc9993085d960c6748d3e9b121ca49d6a1b048df34ce789ff7205.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_yx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea856a9ff16eb740954a9e05855a19a3c0fbc13e1ea5c07d5ad0cb58e45e3c35.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea856a9ff16eb740954a9e05855a19a3c0fbc13e1ea5c07d5ad0cb58e45e3c35.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea856a95957a957ac07e954ac17e3e87950bc07f3ea4c27d3e833ac1c1e03f80.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_yx.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e5a565b69e1a9a42917e1a19c17b3a619e59c47b3a25c53e3b8430e5c57a3e85.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e5a565b69e1a9a42917e1a19c17b3a619e59c47b3a25c53e3b8430e5c57a3e85.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e5a761a79a589e58c07d1e48c07c3f819e41c07f3d84c17e3fa62585c0fe3f83.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_yx.2": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/afffe6d67700958636179d92e019992dd039daf5817d987a807a48e499684a6d.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/aeffe6d67780958636179d92e019892dd139daf5815d987a807a48e699684a6d.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/afffe6d67700958636179d92e019992dd039daf5817d987a807a48e499684a6d.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/aeffe6d67780958636179d92e019892dd139daf5815d987a807a48e699684a6d.png", "https://scitools.github.io/test-iris-imagehash/images/v4/eaff6ad4f74ab16490109c9b942999add1b74bb785a41d4acd526a254acc6365.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_yx.3": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea5e618434ac36e5c1c9369b95b39c38c3a39a4fcee19a6e9b64cb609925cd25.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea5e618434ac36e5c1c9369b95b39c38c3a39a4fcee19a6e9b64cb609925cd25.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e85a636c86a597a593c9b49b94b79969c396c95bccc69a64db30d9b039a52c26.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_yx.4": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ad2f6d2fd2d09295c2d1c3d33c1bc2d67d2c696ce0653c3ac2b1d976da05c2c4.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/ad2f6d2fd2d09295c2d1c3d33c1bc2d27d2c696ce0e53c3ad2b1d976da01c2c4.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/e85e3e2f97a1c19996a1c8f26d1e3a0f684a3c2c6913dc2497b9db8095e502ff.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ad2f6d2fd2d09295c2d1c3d33c1bc2d67d2c696ce0653c3ac2b1d976da05c2c4.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ad2f6d2fd2d09295c2d1c3d33c1bc2d27d2c696ce0e53c3ad2b1d976da01c2c4.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e85e3e2f97a1c19996a1c8f26d1e3a0f684a3c2c6913dc2497b9db8095e502ff.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e85e3c1f97a1c3e197a1c9f37c5e390668521e0c390bdd8685b1d86096e5279f.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_yx.5": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e968658e969692c797879e3b86929e58696d49cd6869c9a37962c923990d9c6d.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/e9e1658e961e92569e9e3c7966d36c586165698c70e1ce739b3698619e1e984c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e968658e969692c797879e3b86929e58696d49cd6869c9a37962c923990d9c6d.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e9e1658e961e92569e9e3c7966d36c586165698c70e1ce739b3698619e1e984c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e1a530e29e5ecf199a5acd8f64f1326161a538e665a198d29e52cb1d9a5e6669.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_zx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/bf813f80c156c05dc0fec29dc17f1a6dd05fc0ff1aa1c57e3b243b20375a1e81.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bf813f80c156c05dc0fec29dc17f1a6dd05fc0ff1aa1c57e3b243b20375a1e81.png", "https://scitools.github.io/test-iris-imagehash/images/v4/be816a81d17ec57ac07e952ac07f3aa0955ec17e3f80c07f3f803f80c0bf3f81.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_zx.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea95629d956a996069939e9bc07f7aad856cc47e5e81857a1e254a35c1be1b81.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea95629d956a996069939e9bc07f7aad856cc47e5e81857a1e254a35c1be1b81.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa816a85957a957ac03f957ac07f3ba1954ac07e3e81c07f3ea47a85c07e3e80.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_zx.2": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/87ed2f867f008d8220179852f01fd9bed1789a6c847cc877c46ac972987ec8fd.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/87ed2f067f008d8220179852f01fd9bed1789a6c847cc877c468c9f6987ec8fd.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/87ed2f067f008d8220179c52f01fd9bed1789a6c847cc877c560c976987ec8fd.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/87ed2f867f008d8220179852f01fd9bed1789a6c847cc877c46ac972987ec8fd.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/87ed2f067f008d8220179852f01fd9bed1789a6c847cc877c468c9f6987ec8fd.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/87ed2f067f008d8220179c52f01fd9bed1789a6c847cc877c560c976987ec8fd.png", "https://scitools.github.io/test-iris-imagehash/images/v4/a3eded05fe11a492b000985af07fdbb4d1e3366d8c644da79fa68993180f6ec1.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_zx.3": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/a2f9b5ba7600a56962df9e96f01dc926c498cc46847f9d6cd0244bf19a6b19f1.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/a2f9b5ba7600856962df9e96f01dcd26c498cc46847f9d6cd0244bf19a6b1975.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a2f9b5ba7600a56962df9e96f01dc926c498cc46847f9d6cd0244bf19a6b19f1.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a2f9b5ba7600856962df9e96f01dcd26c498cc46847f9d6cd0244bf19a6b1975.png", "https://scitools.github.io/test-iris-imagehash/images/v4/aef9f93a770085e9205fd696d13c4b2485ca1a43952f1934daa66bd1ca6b3c71.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_zx.4": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ae953f87d5e82d86801f91ee6e1591fe7e117876c07d6877d068d878d800d07a.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/ae953f87d5e82d87801b91ee6e1599fe7e117874c07d6877d068d878d800d07a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ae953f87d5e82d86801f91ee6e1591fe7e117876c07d6877d068d878d800d07a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ae953f87d5e82d87801b91ee6e1599fe7e117874c07d6877d068d878d800d07a.png", "https://scitools.github.io/test-iris-imagehash/images/v4/bec1329dc5be85dac01d58d73e419d423e41daa59822dc00c5fefe0091fe03ff.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_zx.5": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e87a952d96856943969f694696858d4ee0519d6ee07f9b6a78619b2a79711a2a.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/e87a952d96856943969f694696858d4ae0519d6ee07f996a78719b2a79711a3a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e87a952d96856943969f694696858d4ee0519d6ee07f9b6a78619b2a79711a2a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e87a952d96856943969f694696858d4ae0519d6ee07f996a78719b2a79711a3a.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e85e96ac97a168d897a5791695a19927913c3953687ecce3687c86e3487cc6c3.png" - ], + ], "iris.tests.test_quickplot.TestTimeReferenceUnitsLabels.test_not_reference_time_units.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/82faa1977fdf89976200ddf6e000d9e7f75f9866d560dae4dc00d966dc005e20.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/82b8a1977fdf89876200dde6e000d9e7f77f9866d560dfe4dc00d966fc005e20.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/82f8a1977fdf89876200ddf6e000d9e7f77f9866d560dee4dc00d966dc005e20.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/82f8a1977fdf89876200dde6e000d9e7f77f9866d560dfe4dc00dd64dc005e20.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/82faa1977fdf89976200ddf6e000d9e7f75f9866d560dae4dc00d966dc005e20.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/82b8a1977fdf89876200dde6e000d9e7f77f9866d560dfe4dc00d966fc005e20.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/82f8a1977fdf89876200ddf6e000d9e7f77f9866d560dee4dc00d966dc005e20.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/82f8a1977fdf89876200dde6e000d9e7f77f9866d560dfe4dc00dd64dc005e20.png", "https://scitools.github.io/test-iris-imagehash/images/v4/82faa19e7f51898c6001dd86845fd9a2dd7f996281ee19f389ef03ffdc007e00.png" - ], + ], "iris.tests.test_quickplot.TestTimeReferenceUnitsLabels.test_reference_time_units.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/82fe81987fd777ffe0002addd4002805dda8de65dde9d4625bfddc209841de20.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/82fe81987fdf77ffe0002a9dd4002805ddaade65d9a9d5625bfddc209841de20.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/82fe81987fdf77ffe0002addd4002805dd28df67d9a9d4625bfddc209841de20.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/82fe81987fd777ffe0002addd4002805dda8de65dde9d4625bfddc209841de20.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/82fe81987fdf77ffe0002a9dd4002805ddaade65d9a9d5625bfddc209841de20.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/82fe81987fdf77ffe0002addd4002805dd28df67d9a9d4625bfddc209841de20.png", "https://scitools.github.io/test-iris-imagehash/images/v4/82fa80997f547799a0037a00d52f0956ddaf9f7e98a1816e09f5d8260bfffe00.png" ] } \ No newline at end of file From f51ae8405e1e0c7e8a68bf05ddfbd4da9091cc62 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Tue, 4 Sep 2018 17:27:15 +0100 Subject: [PATCH 30/62] Vector plots 4 (#3158) --- docs/iris/src/whatsnew/2.2.rst | 5 +- lib/iris/plot.py | 55 +- .../integration/plot/test_vector_plots.py | 180 +++ lib/iris/tests/results/imagerepo.json | 1049 +++++++++-------- 4 files changed, 718 insertions(+), 571 deletions(-) create mode 100644 lib/iris/tests/integration/plot/test_vector_plots.py diff --git a/docs/iris/src/whatsnew/2.2.rst b/docs/iris/src/whatsnew/2.2.rst index 4edc305025..7b069f455e 100644 --- a/docs/iris/src/whatsnew/2.2.rst +++ b/docs/iris/src/whatsnew/2.2.rst @@ -25,9 +25,8 @@ Iris 2.2.0 Features contiguous, or the cube's data must be masked at the discontiguities in order to avoid plotting errors. - The iris plot functions :func:`iris.plot.quiver` and - :func:`iris.plot.streamplot` have been added, and these also work with - 2-dimensional plot coordinates. + The iris plot functions :func:`iris.plot.quiver` has been added, and this + also works with 2-dimensional plot coordinates. .. admonition:: 2-Dimensional Grid Vectors diff --git a/lib/iris/plot.py b/lib/iris/plot.py index 0b9b7c54be..f0dfccd8c5 100644 --- a/lib/iris/plot.py +++ b/lib/iris/plot.py @@ -1226,9 +1226,9 @@ def _vector_component_args(x_points, y_points, u_data, *args, **kwargs): if crs: if not isinstance(crs, (ccrs.PlateCarree, ccrs.RotatedPole)): msg = ('Can only plot vectors provided in a lat-lon ' - 'projection, i.e. "cartopy.crs.PlateCarree" or ' - '"cartopy.crs.RotatedPole". This ' - "cubes coordinate system is {}.") + 'projection, i.e. equivalent to "cartopy.crs.PlateCarree" ' + 'or "cartopy.crs.RotatedPole". This ' + "cube coordinate system translates as Cartopy {}.") raise ValueError(msg.format(crs)) # Given the above check, the Y points must be latitudes. # We therefore **assume** they are in degrees : I'm not sure this @@ -1237,7 +1237,7 @@ def _vector_component_args(x_points, y_points, u_data, *args, **kwargs): # TODO: investigate degree units assumptions, here + elsewhere. # Implement a latitude scaling, but preserve the given magnitudes. - v_data = v_data.copy() + u_data, v_data = [arr.copy() for arr in (u_data, v_data)] mags = np.sqrt(u_data * u_data + v_data * v_data) v_data *= np.cos(np.deg2rad(y_points)) scales = mags / np.sqrt(u_data * u_data + v_data * v_data) @@ -1294,53 +1294,6 @@ def quiver(u_cube, v_cube, *args, **kwargs): *args, **kwargs) -def streamplot(u_cube, v_cube, *args, **kwargs): - """ - Draws a streamline plot from two vector component cubes. - - Args: - - * u_cube, v_cube : (:class:`~iris.cube.Cube`) - u and v vector components. Must have same shape and units. - If the cubes have geographic coordinates, the values are treated as - true distance differentials, e.g. windspeeds, and *not* map coordinate - vectors. The components are aligned with the North and East of the - cube coordinate system. - - .. Note: - - At present, if u_cube and v_cube have geographic coordinates, then they - must be in a lat-lon coordinate system, though it may be a rotated one. - To transform wind values between coordinate systems, use - :func:`iris.analysis.cartography.rotate_vectors`. - To transform coordinate grid points, you will need to create - 2-dimensional arrays of x and y values. These can be transformed with - :meth:`cartopy.crs.CRS.transform_points`. - - Kwargs: - - * coords: (list of :class:`~iris.coords.Coord` or string) - Coordinates or coordinate names. Use the given coordinates as the axes - for the plot. The order of the given coordinates indicates which axis - to use for each, where the first element is the horizontal - axis of the plot and the second element is the vertical axis - of the plot. - - * axes: the :class:`matplotlib.axes.Axes` to use for drawing. - Defaults to the current axes if none provided. - - See :func:`matplotlib.pyplot.quiver` for details of other valid - keyword arguments. - - """ - # - # TODO: check u + v cubes for compatibility. - # - kwargs['_v_data'] = v_cube.data - return _draw_2d_from_points('streamplot', _vector_component_args, u_cube, - *args, **kwargs) - - def plot(*args, **kwargs): """ Draws a line plot based on the given cube(s) or coordinate(s). diff --git a/lib/iris/tests/integration/plot/test_vector_plots.py b/lib/iris/tests/integration/plot/test_vector_plots.py new file mode 100644 index 0000000000..00d9d48e8d --- /dev/null +++ b/lib/iris/tests/integration/plot/test_vector_plots.py @@ -0,0 +1,180 @@ +# (C) British Crown Copyright 2014 - 2016, 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 some key usages of :func:`iris.plot.quiver`. + +""" + +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 cartopy.crs as ccrs +from iris.coords import AuxCoord, DimCoord +from iris.coord_systems import Mercator +from iris.cube import Cube +from iris.tests.stock import sample_2d_latlons + +# Run tests in no graphics mode if matplotlib is not available. +if tests.MPL_AVAILABLE: + import matplotlib.pyplot as plt + from iris.plot import quiver + + +@tests.skip_plot +class MixinVectorPlotCases(object): + """ + Test examples mixin, used by separate quiver + streamplot classes. + + NOTE: at present for quiver only, as streamplot does not support arbitrary + coordinates. + + """ + + def plot(self, plotname, *args, **kwargs): + plot_function = self.plot_function_to_test() + plot_function(*args, **kwargs) + plt.suptitle(plotname) + + @staticmethod + def _nonlatlon_xyuv(): + # Create common x, y, u, v arrays for quiver/streamplot testing. + x = np.array([0., 2, 3, 5]) + y = np.array([0., 2.5, 4]) + uv = np.array([[(0., 0), (0, 1), (0, -1), (2, 1)], + [(-1, 0), (-1, -1), (-1, 1), (-2, 1)], + [(1., 0), (1, -1), (1, 1), (-2, 2)]]) + uv = np.array(uv) + u, v = uv[..., 0], uv[..., 1] + return x, y, u, v + + @staticmethod + def _nonlatlon_uv_cubes(x, y, u, v): + # Create u and v test cubes from x, y, u, v arrays. + coord_cls = DimCoord if x.ndim == 1 else AuxCoord + x_coord = coord_cls(x, long_name='x') + y_coord = coord_cls(y, long_name='y') + u_cube = Cube(u, long_name='u', units='ms-1') + if x.ndim == 1: + u_cube.add_dim_coord(y_coord, 0) + u_cube.add_dim_coord(x_coord, 1) + else: + u_cube.add_aux_coord(y_coord, (0, 1)) + u_cube.add_aux_coord(x_coord, (0, 1)) + v_cube = u_cube.copy() + v_cube.rename('v') + v_cube.data = v + return u_cube, v_cube + + def test_non_latlon_1d_coords(self): + # Plot against simple 1D x and y coords. + x, y, u, v = self._nonlatlon_xyuv() + u_cube, v_cube = self._nonlatlon_uv_cubes(x, y, u, v) + self.plot('nonlatlon, 1-d coords', u_cube, v_cube) + plt.xlim(x.min() - 1, x.max() + 2) + plt.ylim(y.min() - 1, y.max() + 2) + self.check_graphic() + + def test_non_latlon_2d_coords(self): + # Plot against expanded 2D x and y coords. + x, y, u, v = self._nonlatlon_xyuv() + x, y = np.meshgrid(x, y) + u_cube, v_cube = self._nonlatlon_uv_cubes(x, y, u, v) + # Call plot : N.B. default gives wrong coords order. + self.plot('nonlatlon_2d', u_cube, v_cube, coords=('x', 'y')) + plt.xlim(x.min() - 1, x.max() + 2) + plt.ylim(y.min() - 1, y.max() + 2) + self.check_graphic() + + @staticmethod + def _latlon_uv_cubes(grid_cube): + # Make a sample grid into u and v data for quiver/streamplot testing. + u_cube = grid_cube.copy() + u_cube.rename('dx') + u_cube.units = 'ms-1' + v_cube = u_cube.copy() + v_cube.rename('dy') + ny, nx = u_cube.shape + nn = nx * ny + angles = np.arange(nn).reshape((ny, nx)) + angles = (angles * 360.0 / 5.5) % 360. + scale = np.arange(nn) % 5 + scale = (scale + 4) / 4 + scale = scale.reshape((ny, nx)) + u_cube.data = scale * np.cos(np.deg2rad(angles)) + v_cube.data = scale * np.sin(np.deg2rad(angles)) + return u_cube, v_cube + + def test_2d_plain_latlon(self): + # Test 2d vector plotting with implicit (PlateCarree) coord system. + u_cube, v_cube = self._latlon_uv_cubes(sample_2d_latlons()) + ax = plt.axes(projection=ccrs.PlateCarree(central_longitude=180)) + self.plot('latlon_2d', u_cube, v_cube, + coords=('longitude', 'latitude')) + ax.coastlines(color='red') + ax.set_global() + self.check_graphic() + + def test_2d_plain_latlon_on_polar_map(self): + # Test 2d vector plotting onto a different projection. + u_cube, v_cube = self._latlon_uv_cubes(sample_2d_latlons()) + ax = plt.axes(projection=ccrs.NorthPolarStereo()) + self.plot('latlon_2d_polar', u_cube, v_cube, + coords=('longitude', 'latitude')) + ax.coastlines(color='red') + self.check_graphic() + + def test_2d_rotated_latlon(self): + # Test plotting vectors in a rotated latlon coord system. + u_cube, v_cube = self._latlon_uv_cubes( + sample_2d_latlons(rotated=True)) + ax = plt.axes(projection=ccrs.PlateCarree(central_longitude=180)) + self.plot('2d_rotated', u_cube, v_cube, + coords=('longitude', 'latitude')) + ax.coastlines(color='red') + ax.set_global() + self.check_graphic() + + def test_fail_unsupported_coord_system(self): + # Test plotting vectors in a rotated latlon coord system. + u_cube, v_cube = self._latlon_uv_cubes(sample_2d_latlons()) + patch_coord_system = Mercator() + for cube in u_cube, v_cube: + for coord in cube.coords(): + coord.coord_system = patch_coord_system + re_msg = ('Can only plot .* lat-lon projection, .* ' + 'This .* translates as Cartopy.*Mercator') + with self.assertRaisesRegexp(ValueError, re_msg): + self.plot('2d_rotated', u_cube, v_cube, + coords=('longitude', 'latitude')) + + +class TestQuiver(MixinVectorPlotCases, tests.GraphicsTest): + def setUp(self): + super(TestQuiver, self).setUp() + + def plot_function_to_test(self): + return quiver + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/results/imagerepo.json b/lib/iris/tests/results/imagerepo.json index e7614cae19..ef9bf542de 100644 --- a/lib/iris/tests/results/imagerepo.json +++ b/lib/iris/tests/results/imagerepo.json @@ -1,952 +1,967 @@ { "example_tests.test_COP_1d_plot.TestCOP1DPlot.test_COP_1d_plot.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/baff589936602d8ec977334ae4dac9b61a6dc4d99532c86cc2913e36c4cc0f61.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/baff589936602d8ec977334ae4dac9b61a6dc4d99532c86cc2913e36c4cc0f61.png", "https://scitools.github.io/test-iris-imagehash/images/v4/aefec91c3601249cc9b3336dc4c8cdb31a64c6d997b3c0eccb5932d285e42f33.png" - ], + ], "example_tests.test_COP_maps.TestCOPMaps.test_cop_maps.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea9138db95668524913e6ac168997e85957e917e876396b96a81b5ce3c496935.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea9138db95668524913e6ac168997e85957e917e876396b96a81b5ce3c496935.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea9130db95668524913c6ac178995b0d956e917ec76396b96a853dcf94696935.png" - ], + ], "example_tests.test_SOI_filtering.TestSOIFiltering.test_soi_filtering.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fac460b9c17b78723e05a5a9954edaf062332799954e9ca5c63b9a52d24e5a95.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/fa8460b9c17b78723e05a5a9954edaf062333799954e9ca5c63b9a52d24e4a9d.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fac460b9c17b78723e05a5a9954edaf062332799954e9ca5c63b9a52d24e5a95.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa8460b9c17b78723e05a5a9954edaf062333799954e9ca5c63b9a52d24e4a9d.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa167295c5e0696a3c17a58c9568da536233da19994cdab487739b4b9b444eb5.png" - ], + ], "example_tests.test_TEC.TestTEC.test_TEC.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e1a561b69b1a9a42846e9a49c7596e3cce6c907b3a83c17e1b8239b3e4f33bc4.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/e1a561b69b1a9e43846e9a49c7596e2cce6c907b3a83c16e1b9231b3e4f33b8c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e1a561b69b1a9a42846e9a49c7596e3cce6c907b3a83c17e1b8239b3e4f33bc4.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e1a561b69b1a9e43846e9a49c7596e2cce6c907b3a83c16e1b9231b3e4f33b8c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e5a761b69a589a4bc46f9e48c65c6631ce61d1ce3982c13739b33193c0ee3f8c.png" - ], + ], "example_tests.test_anomaly_log_colouring.TestAnomalyLogColouring.test_anomaly_log_colouring.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ec4464e185a39f93931e9b1e91696d2949dde6e63e26a47a5ad391938d9a5a0c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ec4464e185a39f93931e9b1e91696d2949dde6e63e26a47a5ad391938d9a5a0c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ecc164e78e979b19b3789b0885a564a56cc2c65e3ec69469db1bdb9a853c1e24.png" - ], + ], "example_tests.test_atlantic_profiles.TestAtlanticProfiles.test_atlantic_profiles.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/9f8260536bd28e1320739437b5f437b0a51d66f4cc5d08fcd00fdb1c93fcb21c.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/9f8260536bd28e1320739437b5f437b0a51d66f4cc7c09f4d00fdb1c93fcb21c.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/9f8a60536bd28e1320739437b5f437b0a53d66f4cc5c08f4d00fdb1c93fcb21c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/9f8260536bd28e1320739437b5f437b0a51d66f4cc5d08fcd00fdb1c93fcb21c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/9f8260536bd28e1320739437b5f437b0a51d66f4cc7c09f4d00fdb1c93fcb21c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/9f8a60536bd28e1320739437b5f437b0a53d66f4cc5c08f4d00fdb1c93fcb21c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/9fc060f462a08f07203ebc77a1f36707e61f4e38d8f7d08a910197fc877cec58.png" - ], + ], "example_tests.test_atlantic_profiles.TestAtlanticProfiles.test_atlantic_profiles.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/a6eaa57e6e81ddf999311ba3b3775e20845d5889c199673b4e22a4675e8ca11c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a6eaa57e6e81ddf999311ba3b3775e20845d5889c199673b4e22a4675e8ca11c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/eeea64dd6ea8cd99991f1322b3761e06845718d89995b3131f32a4765ec2a1cd.png" - ], + ], "example_tests.test_coriolis_plot.TestCoriolisPlot.test_coriolis_plot.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e78665de9a699659e55e9965886979966986c5e63e98c19e3a256679e1981a24.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e78665de9a699659e55e9965886979966986c5e63e98c19e3a256679e1981a24.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e68665de9a699659c1fe99a5896965966996c46e3e19c1da3a652669c51e1a26.png" - ], + ], "example_tests.test_cross_section.TestCrossSection.test_cross_section.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea95317b9562e4d1649f5a05856e4ca4da52947e4ea5f13f1b499d42f13b1b41.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea95317b9562e4d1649f5a05856e4ca4da52947e4ea5f13f1b499d42f13b1b41.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea91b17b9562e4d1609f5a05856e4ca45a52957e5ea5f13b1bca9dc0b17b1ac1.png" - ], + ], "example_tests.test_cross_section.TestCrossSection.test_cross_section.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea9521fb956a394069921e93f07f4aad856cc47e4e95857a1ea5da3591ba1b81.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea9521fb956a394069921e93f07f4aad856cc47e4e95857a1ea5da3591ba1b81.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea9521fb956a394068931e9be07e4aa5856cc47e4a91957a1ba55bb5b17a3b81.png" - ], + ], "example_tests.test_custom_aggregation.TestCustomAggregation.test_custom_aggregation.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fe816e81917e907eb43e873f85677ac190f0703c6a95811f1ac33ce1a57a6f18.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fe816e81917e907eb43e873f85677ac190f0703c6a95811f1ac33ce1a57a6f18.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fe816e81817e907eb43e873f85637ac198d8703c6a94811f1ac73ee1a57a6f90.png" - ], + ], "example_tests.test_custom_file_loading.TestCustomFileLoading.test_custom_file_loading.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/faa0cbf1845e34be913787416edcc8bc3bc81f9b63332662a4ed30cdc1b2cd21.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/fba0cbf1845e34be912787416edcc8bc3b881f9b62332762a5ad32cdc1b2cd21.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/faa0cbf1845e34be913787416edcc8bc3bc81f9b63332662a4ed30cdc1b2cd21.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fba0cbf1845e34be912787416edcc8bc3b881f9b62332762a5ad32cdc1b2cd21.png", "https://scitools.github.io/test-iris-imagehash/images/v4/faa1cb47845e34bc912797436cccc8343f11359b73523746c48c72d9d9b34da5.png" - ], + ], "example_tests.test_deriving_phenomena.TestDerivingPhenomena.test_deriving_phenomena.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/b9993986866952e6c9464639c4766bd9c669916e7b99c1663f99768990763e81.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/b99139de866952e6c946c639c47e6bd18769d16e7a9981662e813699d0763e89.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/b9993986866952e6c9464639c4766bd9c669916e7b99c1663f99768990763e81.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/b99139de866952e6c946c639c47e6bd18769d16e7a9981662e813699d0763e89.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ec97681793689768943c97e8926669d186e8c33f6c99c32e6b936c83d33e2c98.png" - ], + ], "example_tests.test_global_map.TestGlobalMap.test_global_map.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa9979468566857ef07e3e8978566b91cb0179883c89946686a96b9d83766f81.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa9979468566857ef07e3e8978566b91cb0179883c89946686a96b9d83766f81.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa997b958466846ed13e87467a997a898d66d17e2cc9906684696f99d3162f81.png" - ], + ], "example_tests.test_hovmoller.TestGlobalMap.test_hovmoller.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/bab430b4ce4bce43c5becf89c54b1a63c543c56e1e64907e3bb469b490de1ac1.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bab430b4ce4bce43c5becf89c54b1a63c543c56e1e64907e3bb469b490de1ac1.png", "https://scitools.github.io/test-iris-imagehash/images/v4/eeb46cb4934b934bc07e974bc14b38949943c0fe3e94c17f6ea46cb4c07b3f00.png" - ], + ], "example_tests.test_inset_plot.TestInsetPlot.test_inset_plot.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ebff6992f50096a5b245dac4f6559496b49248dbc95dcb699529912dcf244a54.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/e9ff6992b50096a5b245dac4f64594b6b49248dbc95dcb699529952dcf244a56.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ebff6992f50096a5b245dac4f6559496b49248dbc95dcb699529912dcf244a54.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e9ff6992b50096a5b245dac4f64594b6b49248dbc95dcb699529952dcf244a56.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ebff6992b50096ad9267dac4d64094b294924cdbc95d4b699d29952dcda46e94.png" - ], + ], "example_tests.test_lagged_ensemble.TestLaggedEnsemble.test_lagged_ensemble.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/bbbb31e1c44e64e4b0459b5bb1716ecac464f496ce34618eb1079b39b193ce25.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bbbb31e1c44e64e4b0459b5bb1716ecac464f496ce34618eb1079b39b193ce25.png", "https://scitools.github.io/test-iris-imagehash/images/v4/bbbb31b1c44e64e4b1579b5b917133cecc61f146c414668eb1119b1bb197ce34.png" - ], + ], "example_tests.test_lagged_ensemble.TestLaggedEnsemble.test_lagged_ensemble.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/abfef958fd462c993a07d87960464b81d1009687c139d3b594e9cf87c6b89687.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/abfef958fd462c993a07d87960464b81d1009687c139d3b594e9cf87c6b89687.png", "https://scitools.github.io/test-iris-imagehash/images/v4/aafec5e9e5e03e099a07e0f86542db879438261ec3b13ce78d8dc65a92d83d89.png" - ], + ], "example_tests.test_lineplot_with_legend.TestLineplotWithLegend.test_lineplot_with_legend.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/eae942526540b869961f8da694589da69543cc9af1014afbc3fd596b84fe19a7.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/eae942146540b869961f8de694589da69543cc9af1014afbc3fd596b84fe19a7.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/eae942526540b869961f8da694589da69543cc9af1014afbc3fd596b84fe19a7.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/eae942146540b869961f8de694589da69543cc9af1014afbc3fd596b84fe19a7.png", "https://scitools.github.io/test-iris-imagehash/images/v4/eafd9e12a5a061e9925ec716de489e9685078ec981b229e70ddb79219cc3768d.png" - ], + ], "example_tests.test_orca_projection.TestOrcaProjection.test_orca_projection.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fb11731a94cea4ee64b35e91d1d2304e9e5ac7397b20e1fe12852487e666ce46.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fb11731a94cea4ee64b35e91d1d2304e9e5ac7397b20e1fe12852487e666ce46.png", "https://scitools.github.io/test-iris-imagehash/images/v4/bb11721a87cce5e4cce79e81d19b3b5e1e1cd3783168e07835853485e65e2e1e.png" - ], + ], "example_tests.test_orca_projection.TestOrcaProjection.test_orca_projection.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e5a665a69a599659e5db1865c2653b869996cce63e99e19a1a912639e7181e65.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e5a665a69a599659e5db1865c2653b869996cce63e99e19a1a912639e7181e65.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e58661969e799659c1f719a6c867359a1996c0773649c09c3e612679c07b3f66.png" - ], + ], "example_tests.test_orca_projection.TestOrcaProjection.test_orca_projection.2": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/f2c464ce9e399332e1b74ce1cc79338c6586e5b33b31b37a66c9664cc06e1a64.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/f2c464ce9e399332e1b74ce1cc79338c6586e5b33b31b37a66c9664cc06e1a64.png", "https://scitools.github.io/test-iris-imagehash/images/v4/a58660ce9e739b31c93d1cc9c8df33863383e33b3f11c03f2664366cc8ee3cc1.png" - ], + ], "example_tests.test_orca_projection.TestOrcaProjection.test_orca_projection.3": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa817a83846ea46ce539c93391de32cc86cf87a33fa168721cdb3e896e374b04.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa817a83846ea46ce539c93391de32cc86cf87a33fa168721cdb3e896e374b04.png", "https://scitools.github.io/test-iris-imagehash/images/v4/be817a87845ea56cec79817a919e338436a5c1e73fa16c736c4a3e816a1e6b1c.png" - ], + ], "example_tests.test_polar_stereo.TestPolarStereo.test_polar_stereo.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e168317a92d36d89c5bb9e94c55e6f0c9a93c15a6ec584763b21716791de3a81.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e168317a92d36d89c5bb9e94c55e6f0c9a93c15a6ec584763b21716791de3a81.png", "https://scitools.github.io/test-iris-imagehash/images/v4/b9e16079971e9e93c8ce0f84c31e3b929f92c0ff3ca1c17e39e03961c07e3f80.png" - ], + ], "example_tests.test_polynomial_fit.TestPolynomialFit.test_polynomial_fit.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/abff4a9df26435886520c97f12414695c4b69d23934bc86adc969237d68ccc6f.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/aaff4a9df26435886520c97f12414695c4b69d23934bc86adc969a17d69ccc6f.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/abff4a9df26435886520c97f12414695c4b69d23934bc86adc969237d68ccc6f.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/aaff4a9df26435886520c97f12414695c4b69d23934bc86adc969a17d69ccc6f.png", "https://scitools.github.io/test-iris-imagehash/images/v4/aeffcb34d244348be5a2c96c3a4fc6d0c4b69f2d87294ccb9f1a125684cd7c11.png" - ], + ], "example_tests.test_projections_and_annotations.TestProjectionsAndAnnotations.test_projections_and_annotations.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa854f19851a30e4cc76cd0bb179325ca7c665b0c938cb4b4e719e9cb727b5c0.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/fac54f19851a30e4cc76cd0bb179325cb78665b0c938cb4b4e719e9c9727b5c0.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/fa854e19851a30e4cc76cd0bb179325cb7c664b0c938cb4bce739e9c37a3b5c0.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/fa854e19851a30e4cc76cd0bb179325cb78665b1c938c94bce739e9c3727b5c0.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa854f19851a30e4cc76cd0bb179325ca7c665b0c938cb4b4e719e9cb727b5c0.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fac54f19851a30e4cc76cd0bb179325cb78665b0c938cb4b4e719e9c9727b5c0.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa854e19851a30e4cc76cd0bb179325cb7c664b0c938cb4bce739e9c37a3b5c0.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa854e19851a30e4cc76cd0bb179325cb78665b1c938c94bce739e9c3727b5c0.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa854f19851a30e4cc76cd0bb0f932dca7c665b1c92ccb4b4ed19e9c3721b5c8.png" - ], + ], "example_tests.test_projections_and_annotations.TestProjectionsAndAnnotations.test_projections_and_annotations.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e385699d9c3896627243318fcdad5a7dc6dba492e9b69964936dc21974b18592.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/e385699d9c3896727243318f8dad5a7dc65ba492b93699649b6dc25b64938592.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/e385699d9c3896627243318fcdad5a7dc6dba492b93699649b6dc25964938592.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/e3856b999c3896727243318f8dad5a75965ba492f9b69964db4cc65b64918592.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/e3856b999c3896727243318f8dad5a75865ba492e9b69964db6cc65b74918592.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e385699d9c3896627243318fcdad5a7dc6dba492e9b69964936dc21974b18592.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e385699d9c3896727243318f8dad5a7dc65ba492b93699649b6dc25b64938592.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e385699d9c3896627243318fcdad5a7dc6dba492b93699649b6dc25964938592.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e3856b999c3896727243318f8dad5a75965ba492f9b69964db4cc65b64918592.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e3856b999c3896727243318f8dad5a75865ba492e9b69964db6cc65b74918592.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e3856d999c389662734731afcdad5a7384daa592b1b69b64d26dc29974b18590.png" - ], + ], "example_tests.test_rotated_pole_mapping.TestRotatedPoleMapping.test_rotated_pole_mapping.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa15615e97a193adc15e1e81c4fa3eb49d30817e3e05c17e7ba59927817e1e01.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa15615e97a193adc15e1e81c4fa3eb49d30817e3e05c17e7ba59927817e1e01.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ee46607e97a19781c0df1f81d0bb3e241f20c16f3fc0c1fe39263d33d06f3e80.png" - ], + ], "example_tests.test_rotated_pole_mapping.TestRotatedPoleMapping.test_rotated_pole_mapping.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ba056717c3e099e9b90f8e81c4da589499b696763e45e56b3b893929c17b7e01.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ba056717c3e099e9b90f8e81c4da589499b696763e45e56b3b893929c17b7e01.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea57685f95a886a1c0de9da090be3e2697e1c0ff3f00c17e6b266c17c07f3f00.png" - ], + ], "example_tests.test_rotated_pole_mapping.TestRotatedPoleMapping.test_rotated_pole_mapping.2": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ba1e605ec7a191a1b85e9e81c4da58909996b37e3a65e16f7b817939e57a1e01.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/ba1e605ec7a193a1b85e9e81c4da58909996b3763a65e16f7b816939ed7a1e01.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ba1e605ec7a191a1b85e9e81c4da58909996b37e3a65e16f7b817939e57a1e01.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ba1e605ec7a193a1b85e9e81c4da58909996b3763a65e16f7b816939ed7a1e01.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e85a697e97a18681c6da9f8190bf3e263624c1ef3b48c17a2b223c47c0ff3f81.png" - ], + ], "example_tests.test_rotated_pole_mapping.TestRotatedPoleMapping.test_rotated_pole_mapping.3": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa8172d0847ecd2bc913939c36846c714933799cc3cc8727e67639f939996a58.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa8172d0847ecd2bc913939c36846c714933799cc3cc8727e67639f939996a58.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa8172c6857ecd38cb3392ce36c564311931d85ec64e9787719a39993c316e66.png" - ], + ], "example_tests.test_wind_speed.TestWindSpeed.test_wind_speed.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/bcf924fb9306930ce12ccf97c73236b28ecec4cd3e29847b18e639e6c14f1a09.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bcf924fb9306930ce12ccf97c73236b28ecec4cd3e29847b18e639e6c14f1a09.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e9e960e996169306c1ee9e96c29e36739e13c07d3d61c07f39a139a1c07f3f01.png" - ], + ], "example_tests.test_wind_speed.TestWindSpeed.test_wind_speed.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/bcf924fb9306930ce12ccf97c73236b28ecec4cc3e29847b38e639e6c14f1a09.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bcf924fb9306930ce12ccf97c73236b28ecec4cc3e29847b38e639e6c14f1a09.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e9e960e996169306c1ee9e86c29e36739e13c07d3d61c07f39a139a1c17f3f01.png" - ], + ], "iris.tests.experimental.test_animate.IntegrationTest.test_cube_animation.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fe81957ac17e6a85817e6a85857e942a3e81957a7e81917a7a81d95ec17e2ca1.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fe81957ac17e6a85817e6a85857e942a3e81957a7e81917a7a81d95ec17e2ca1.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fe81c17e817e3e81817e7e81857e7e817e81c07e7e81c17e7a81817e817e8c2a.png" - ], + ], "iris.tests.experimental.test_animate.IntegrationTest.test_cube_animation.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/be81c17ec17e7e81c17e3e81c57ea55a3e80c17e3e81c1fe7a81c285c95f2c03.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/be81c17ec17e7e81c17e3e81c57ea55a3e80c17e3e81c1fe7a81c285c95f2c03.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fe81857e817e6a85817e7a81857e7e817e81957a7e81817e7a81817e817e843e.png" - ], + ], "iris.tests.experimental.test_animate.IntegrationTest.test_cube_animation.2": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea857a81957a857e957ec17e817e6a816a853e817a853e816e818d3a862ad3fe.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea857a81957a857e957ec17e817e6a816a853e817a853e816e818d3a862ad3fe.png", "https://scitools.github.io/test-iris-imagehash/images/v4/be81857ec17e7a81c17e7e81857e3e803e81817a3e81c17e7a81c17ec97e2c2b.png" - ], + ], "iris.tests.integration.plot.test_plot_2d_coords.Test.test_2d_coord_bounds_northpolarstereo.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/e59661969e699659c0f719a6c967339a1992c07f3649c09c3f612669c07b3f66.png" - ], + ], "iris.tests.integration.plot.test_plot_2d_coords.Test.test_2d_coord_bounds_platecarree.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/ee816299954a1da699b6915ec25b6e419729c42c3f84bd9fe6d262d1d1dac076.png" - ], + ], + "iris.tests.integration.plot.test_vector_plots.TestQuiver.test_2d_plain_latlon.0": [ + "https://scitools.github.io/test-iris-imagehash/images/v4/fb8d4f21c472b27e919d2e216f216b3178e69c7e961ab39a84696c616d245b94.png" + ], + "iris.tests.integration.plot.test_vector_plots.TestQuiver.test_2d_plain_latlon_on_polar_map.0": [ + "https://scitools.github.io/test-iris-imagehash/images/v4/e66c6619999933666666c6d99999336663646d9999c1332667b60cf964d8672c.png" + ], + "iris.tests.integration.plot.test_vector_plots.TestQuiver.test_2d_rotated_latlon.0": [ + "https://scitools.github.io/test-iris-imagehash/images/v4/eba925a5c476d25a95a56b876f3826246a449c6b96a3731ab13f6c656a5cb48a.png" + ], + "iris.tests.integration.plot.test_vector_plots.TestQuiver.test_non_latlon_1d_coords.0": [ + "https://scitools.github.io/test-iris-imagehash/images/v4/a7ac24947259f3493697632df45926b6e126c4f392593b4937266f26ccf032d8.png" + ], + "iris.tests.integration.plot.test_vector_plots.TestQuiver.test_non_latlon_2d_coords.0": [ + "https://scitools.github.io/test-iris-imagehash/images/v4/afac26367251d3493617632df45c26a6e126c6f392593b4937266f26ccf232d0.png" + ], "iris.tests.test_analysis.TestProject.test_cartopy_projection.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/9e1952c9c165b4fc668a9d47c1461d7a60fb2e853eb426bd62fd229c9f04c16d.png" - ], + ], "iris.tests.test_mapping.TestBasic.test_contourf.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e85a69cc96ad92e193c9963385929e1cc3819acde6d965ce6e666b30386e65b1.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e85a69cc96ad92e193c9963385929e1cc3819acde6d965ce6e666b30386e65b1.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e97a346c9685cb899685c9c39695c79396ec634969ce2c74697a3864697b3c8c.png" - ], + ], "iris.tests.test_mapping.TestBasic.test_pcolor.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e95a69c896a592e59bc99e3384929636c32d98cde6d964ce7e666332386465b1.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e95a69c896a592e59bc99e3384929636c32d98cde6d964ce7e666332386465b1.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e97a347c96858b8d9685c9c39696c393966c634969ce3c64697a3864697b3c9c.png" - ], + ], "iris.tests.test_mapping.TestBasic.test_unmappable.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/eaa5684eb54a947ad09eb731c521978dc2fb1cc0e4966ce26e2c6b2d3a6e691a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/eaa5684eb54a947ad09eb731c521978dc2fb1cc0e4966ce26e2c6b2d3a6e691a.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea853e48957ac1df957ac8be852bc1b1944e7a9878e03f4c6a253e6c7a912dc2.png" - ], + ], "iris.tests.test_mapping.TestBoundedCube.test_grid.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa81917e857e6e81857e7a857a81917a7a81857e857e7e81857e7a817a81852e.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e857e7a81857e7a817a81817e7a81857e857e7a81857e7a817a81857e.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa81917e857e6e81857e7a857a81917a7a81857e857e7e81857e7a817a81852e.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e857e7a81857e7a817a81817e7a81857e857e7a81857e7a817a81857e.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e857e7a81857e7a817a81857a7a81857e857e7a85857e7a817a81857a.png" - ], + ], "iris.tests.test_mapping.TestBoundedCube.test_pcolormesh.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa81e535857e92ca8ec23d21b13ce15e7a811ea5c47e1a5ac17b652d3b05e4f2.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa81e535857e92ca8ec23d21b13ce15e7a811ea5c47e1a5ac17b652d3b05e4f2.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa81c17a857e1ea5857e634a7a81cd257e8584da857e3b29817e68f47a81c791.png" - ], + ], "iris.tests.test_mapping.TestLimitedAreaCube.test_grid.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/bf80e2b1c17f1d0ac4f7c8d739a637202749699b6bb3ce3666e4b048944d9d89.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/bf80e2f1c17f1d0ac457c8d619a637213749699b6bb34e3666e4b04e944d9d89.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bf80e2b1c17f1d0ac4f7c8d739a637202749699b6bb3ce3666e4b048944d9d89.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bf80e2f1c17f1d0ac457c8d619a637213749699b6bb34e3666e4b04e944d9d89.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea05392995bac6d691ce3f21666569d86a96c6360ee195cb91e8ce54953b313b.png" - ], + ], "iris.tests.test_mapping.TestLimitedAreaCube.test_outline.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e857e3e80857e7a817a817a817a81817f7a81857e857e857e857e7a81.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e857e7e21857e7a817a817a857a81857a7a81857a857e857a857e7a84.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e857e3e80857e7a817a817a817a81817f7a81857e857e857e857e7a81.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e857e7e21857e7a817a817a857a81857a7a81857a857e857a857e7a84.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa1585e885e87a1785fa7a177a177e807a1585e85fa0857a85e86817857f6a16.png" - ], + ], "iris.tests.test_mapping.TestLimitedAreaCube.test_pcolormesh.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/bf81e6b1c17e1d4884bfc8df39a43720374969db69b34e26c4e4b0ca904f9d89.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bf81e6b1c17e1d4884bfc8df39a43720374969db69b34e26c4e4b0ca904f9d89.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea57396995a8c6d691ea3f25664569d86b16c63686ed958991ea4a549531393b.png" - ], + ], "iris.tests.test_mapping.TestLimitedAreaCube.test_scatter.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea053d2e916ac2d9c4d894346b24f3477acf68ad39329ed8c696e136c1ab9a71.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/ea053d2e916ac2d9c4d895346b2473477acf68ad39329ed8c69ee126c1ab9a71.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea053d2e916ac2d9c4d894346b24f3477acf68ad39329ed8c696e136c1ab9a71.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea053d2e916ac2d9c4d895346b2473477acf68ad39329ed8c69ee126c1ab9a71.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea05bd2e916ac2d984983d346b2473477acf69ad3d3296d8c696e126c1ab1e71.png" - ], + ], "iris.tests.test_mapping.TestLowLevel.test_keywords.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/be21a71bc1de58e43a31871f7e856470c1fa9b8c7b81647384665b9ed1b998c1.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/be21a71bc1de58e43a31871f7e856470c1fa9b8c7b81647384665b9ed1b998c1.png", "https://scitools.github.io/test-iris-imagehash/images/v4/be21a71bc1de58e43a63a71b3e016061c1fe9b8c3e01a473847e5b94d1fb9ac3.png" - ], + ], "iris.tests.test_mapping.TestLowLevel.test_keywords.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea811831957fe3cea68c6ce0d9f29b9b6a816463953e61cc917f1ae36ac09d38.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea811831957fe3cea68c6ce0d9f29b9b6a816463953e61cc917f1ae36ac09d38.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa819097857e6560957e7bcc7a819c316e81951e857e62c281fe79a17aa19637.png" - ], + ], "iris.tests.test_mapping.TestLowLevel.test_params.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ee819cb7913b63c8846e64737bb1999c6ec52633953a69c8916f6c636e92911c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ee819cb7913b63c8846e64737bb1999c6ec52633953a69c8916f6c636e92911c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa8190be857e6739917a7bc47a8594337bb1911c857e6ec3913279007e819637.png" - ], + ], "iris.tests.test_mapping.TestLowLevel.test_params.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/be21a71bc1de58e43a31871f7e856470c1fa9b8c7b81647384665b9ed1b998c1.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/be21a71bc1de58e43a31871f7e856470c1fa9b8c7b81647384665b9ed1b998c1.png", "https://scitools.github.io/test-iris-imagehash/images/v4/be21a71bc1de58e43a63a71b3e016061c1fe9b8c3e01a473847e5b94d1fb9ac3.png" - ], + ], "iris.tests.test_mapping.TestLowLevel.test_params.2": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea811831957ae3cea68c6ce0c9f39b9b6a816473953e63cc917f1ae36ac09d38.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea811831957ae3cea68c6ce0c9f39b9b6a816473953e63cc917f1ae36ac09d38.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa81909f857e6520957e5bcc7a8194716e31851e857e6ac281fe3f817a81963f.png" - ], + ], "iris.tests.test_mapping.TestLowLevel.test_simple.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/eae0943295154bcc844e6c314fb093ce7bc7c4b3a4307bc4916f3f316ed2b4ce.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/eae0943295154bcc844e6c314fb093ce7bc7c4b3a4307bc4916f3f316ed2b4ce.png", "https://scitools.github.io/test-iris-imagehash/images/v4/faa0e55c855fdce7857a1ab16a85a50c3ea1e55e856658a5c11837096e8fe17a.png" - ], + ], "iris.tests.test_mapping.TestMappingSubRegion.test_simple.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/bd913e01d07ee07e926e87876f8196c1e0d36967393c1f181e2c3cb8b0f960d7.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bd913e01d07ee07e926e87876f8196c1e0d36967393c1f181e2c3cb8b0f960d7.png", "https://scitools.github.io/test-iris-imagehash/images/v4/b9913d90c66eca6ec66ec2f3689195b6cf5b2f00392cb3496695621d34db6c92.png" - ], + ], "iris.tests.test_mapping.TestUnmappable.test_simple.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fe818d6ac17e5a958d7ab12b9d677615986e666dc4f20dea7281d98833889b22.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fe818d6ac17e5a958d7ab12b9d677615986e666dc4f20dea7281d98833889b22.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa81b54a817eca35817ec701857e3e64943e7bb41b846f996e817e006ee1b19b.png" - ], + ], "iris.tests.test_plot.Test1dPlotMultiArgs.test_coord.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/83fec2ff7c00a56de9023b52e4143da5d16d7ecad1b76f2094c963929c6471c8.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/83fec2ff7c00a56de9023b52e4143da5d16d7ecad1b76f2094c963929c6471c8.png", "https://scitools.github.io/test-iris-imagehash/images/v4/8bfec2d77e01a5a5ed013b4ac4521c94817d4e6d91ff63349c6d61991e3278cc.png" - ], + ], "iris.tests.test_plot.Test1dPlotMultiArgs.test_coord_coord.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/87ff95776a01e1f67801cc36f4075b81c5437668c1167c88d2676d39d6867b68.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/87ff95776a01e1f67801cc36f4075b81c5437668c1167c88d2676d39d6867b68.png", "https://scitools.github.io/test-iris-imagehash/images/v4/8fff941e7e01e1c2f801c878a41e5b0d85cf36e1837e2d9992c62f21769e6a4d.png" - ], + ], "iris.tests.test_plot.Test1dPlotMultiArgs.test_coord_coord_map.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fbe0623dc9879d91b41e4b449b6579e78798a49b7872d2644b8c919b39306e6c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fbe0623dc9879d91b41e4b449b6579e78798a49b7872d2644b8c919b39306e6c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/bbe0c21ccd179dc3b05e4b689b0771b48698961b7962da446e8ca5bb36716c6e.png" - ], + ], "iris.tests.test_plot.Test1dPlotMultiArgs.test_coord_cube.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8ff897066b41f076f81dce1fb007da79c50633e9c40626b8d1066df9d6067969.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8ff897066b41f076f81dce1fb007da79c50633e9c40626b8d1066df9d6067969.png", "https://scitools.github.io/test-iris-imagehash/images/v4/8ff897066a01f0f2f818ee1eb007ca41853e3b81c57e36a991fe2ca9725e29ed.png" - ], + ], "iris.tests.test_plot.Test1dPlotMultiArgs.test_cube.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8ffac1547a0792546c179db7f1254f6d945b7392841678e895017e3e91c17a0f.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8ffac1547a0792546c179db7f1254f6d945b7392841678e895017e3e91c17a0f.png", "https://scitools.github.io/test-iris-imagehash/images/v4/8ff8c1fa7a05b4ea6c059d2ff1494e4b90f26304846d78d1872a6cfc938b2e3e.png" - ], + ], "iris.tests.test_plot.Test1dPlotMultiArgs.test_cube_coord.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/83fec1ff7e0098757103a71ce4506dc3d11e7b20d2477ec094857db895217f6a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/83fec1ff7e0098757103a71ce4506dc3d11e7b20d2477ec094857db895217f6a.png", "https://scitools.github.io/test-iris-imagehash/images/v4/8fffc1dc7e019c70f001b70ee4386de1814e7938837b6a7f84d07c9f15b02f21.png" - ], + ], "iris.tests.test_plot.Test1dPlotMultiArgs.test_cube_cube.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8ff8c2d73a09b4a76c099d26f14b0e5ad0d643b0d42763e9d51378f895867c39.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/8fe8c0173a19b4066d599946f35f0ed5d0b74729d40369d8953678e897877879.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8ff8c2d73a09b4a76c099d26f14b0e5ad0d643b0d42763e9d51378f895867c39.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8fe8c0173a19b4066d599946f35f0ed5d0b74729d40369d8953678e897877879.png", "https://scitools.github.io/test-iris-imagehash/images/v4/8ff8c0567a01b096e4019daff10b464bd4da6391943678e5879f7e3103e67f1c.png" - ], + ], "iris.tests.test_plot.Test1dQuickplotPlotMultiArgs.test_coord.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/83fec2777e04256f68023352f6d61da5c109dec8d19bcf089cc9d99a9c85d999.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/83fec2777e06256f68023352f6d61da5c009decad19bcf089cc9d99a9c85d989.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/83fec2777e04256f68023352f6d61da5c109dec8d19bcf089cc9d99a9c85d999.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/83fec2777e06256f68023352f6d61da5c009decad19bcf089cc9d99a9c85d989.png", "https://scitools.github.io/test-iris-imagehash/images/v4/83fec2777e002427e801bb4ae65a1c94813dcec999db4bbc9ccd79991f3238cc.png" - ], + ], "iris.tests.test_plot.Test1dQuickplotPlotMultiArgs.test_coord_coord.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/83fe9dd77f00e1d73000cc1df707db8184427ef8d1367c88d2667d39d0866b68.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/83fe9d977f41e1d73000cc1df707d98184427ef8d1367c88d2667d39d0866b68.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/83fe9dd77f00e1d73000cc1df707db8184427ef8d1367c88d2667d39d0866b68.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/83fe9d977f41e1d73000cc1df707d98184427ef8d1367c88d2667d39d0866b68.png", "https://scitools.github.io/test-iris-imagehash/images/v4/83ff9d9f7e01e1c2b001c8f8f63e1b1d81cf36e1837e259982ce2f215c9a626c.png" - ], + ], "iris.tests.test_plot.Test1dQuickplotPlotMultiArgs.test_coord_coord_map.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fbe0623dc9879d91b41e4b449b6579e78798a49b7872d2644b8c919b39306e6c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fbe0623dc9879d91b41e4b449b6579e78798a49b7872d2644b8c919b39306e6c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/bbe0c21ccd179dc3b05e4b689b0771b48698961b7962da446e8ca5bb36716c6e.png" - ], + ], "iris.tests.test_plot.Test1dQuickplotPlotMultiArgs.test_coord_cube.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/87ffb5867f0060d4301f6d9fb007d899c50699e9c8668e78d8678d69de069969.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/87ffb5867f0060d4301f6d9fb007d899c50699e9c8668e78d8678d69de069969.png", "https://scitools.github.io/test-iris-imagehash/images/v4/87ffb79e7f0060d8303fcd1eb007d801c52699e18d769e2199e60ce1da5629ed.png" - ], + ], "iris.tests.test_plot.Test1dQuickplotPlotMultiArgs.test_cube.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/83ffc1dc7e00b0dc66179d95f127cfc9d44959ba846658e891075a3e99415a2f.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/83ffc1dc7e00b0dc66179d95f127cfc9d44959ba846658e891075a3e99415a2f.png", "https://scitools.github.io/test-iris-imagehash/images/v4/a3ffc1d87e00b49964179d28f16bce4b98724b268c6d58e1972e4874998b2e7e.png" - ], + ], "iris.tests.test_plot.Test1dQuickplotPlotMultiArgs.test_cube_coord.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/83fec1ff7f90987720029f1ef458cd43811cdb60d647de609485ddb899215f62.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/83fec1ff7f94987720009f1ef458cd43810cdb60d647de609485ddb89921df62.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/83fec1ff7f90987720029f1ef458cd43811cdb60d647de609485ddb899215f62.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/83fec1ff7f94987720009f1ef458cd43810cdb60d647de609485ddb89921df62.png", "https://scitools.github.io/test-iris-imagehash/images/v4/a3ffc1de7e009c7030019786f438cde3810fd97c93734a778ce07c9f99b02731.png" - ], + ], "iris.tests.test_plot.Test1dQuickplotPlotMultiArgs.test_cube_cube.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/83ffc8967e0098a6241f9d26e34b8e42f4d20bb4942759e9941f78f8d7867a39.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/83f9c8967e009da6245f9946e25f9ed6f0940f29f40749d8853678e8d7857879.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/83ffc8967e0098a6241f9d26e34b8e42f4d20bb4942759e9941f78f8d7867a39.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/83f9c8967e009da6245f9946e25f9ed6f0940f29f40749d8853678e8d7857879.png", "https://scitools.github.io/test-iris-imagehash/images/v4/83ffc9d67e00909624079daef160cf4bd45a439184367ae5979f7e3119e6261c.png" - ], + ], "iris.tests.test_plot.Test1dQuickplotScatter.test_coord_coord.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/a3fac1947c99184e62669ca7f65bc96ab81d97b7e248199cc7913662d94ac5a1.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/a3fac1947c99184e62669ca7f65bc96ab81d97b7c248399cc7917662d84ac5a1.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/a3fac1b47c99184e62669ca7f65bc96ab81d97b7e248199cc7913662d84acda0.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a3fac1947c99184e62669ca7f65bc96ab81d97b7e248199cc7913662d94ac5a1.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a3fac1947c99184e62669ca7f65bc96ab81d97b7c248399cc7917662d84ac5a1.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a3fac1b47c99184e62669ca7f65bc96ab81d97b7e248199cc7913662d84acda0.png", "https://scitools.github.io/test-iris-imagehash/images/v4/b2ecc1a8b9994a16e666b5e3ce151969a5fb4ed49909653990d46b9bfc097684.png" - ], + ], "iris.tests.test_plot.Test1dQuickplotScatter.test_coord_coord_map.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/bea07c99c15eb16e9891ce50c742394a3ced6cb13390f1cc73c29f1b2d0ecd66.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bea07c99c15eb16e9891ce50c742394a3ced6cb13390f1cc73c29f1b2d0ecd66.png", "https://scitools.github.io/test-iris-imagehash/images/v4/bea06899c14eb16e9895ce46c74a396a74ed64b13390b3c61b439f1b4d2ccde6.png" - ], + ], "iris.tests.test_plot.Test1dQuickplotScatter.test_coord_cube.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ae7f1f07f3e0e0f0211b9e066e074d83926ed8f8cd3792dad1964db0d80e9b09.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/ae7f1f07f3e0e0f0311b9e066e074d839266d8e8cd379adad1964db0d80e9b09.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ae7f1f07f3e0e0f0211b9e066e074d83926ed8f8cd3792dad1964db0d80e9b09.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ae7f1f07f3e0e0f0311b9e066e074d839266d8e8cd379adad1964db0d80e9b09.png", "https://scitools.github.io/test-iris-imagehash/images/v4/be852fc1e078c83eb30e3607672149c098d95c5b9e4636f2c1fc299d999f7e03.png" - ], + ], "iris.tests.test_plot.Test1dQuickplotScatter.test_cube_coord.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/a5f896d99a67b94c621deda3f69392cccd246db39018989ec4836de9ed249292.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/a5f896d99a66b94c621deda3f69392cccd646db3901898dec4836de9cd249292.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/a5f896d99a67b94c621ceda3f6d392cccd246db3901898dec4836de9cd249292.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a5f896d99a67b94c621deda3f69392cccd246db39018989ec4836de9ed249292.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a5f896d99a66b94c621deda3f69392cccd646db3901898dec4836de9cd249292.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a5f896d99a67b94c621ceda3f6d392cccd246db3901898dec4836de9cd249292.png", "https://scitools.github.io/test-iris-imagehash/images/v4/edfa96cb9a256b4f65466d9892d9c865693a1a9c94b39ed8484b35ad9a864c32.png" - ], + ], "iris.tests.test_plot.Test1dQuickplotScatter.test_cube_cube.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/a4fb19b3db04c6cd6307b98678601c738c39d71cf3866186d8616e69bd191b9e.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a4fb19b3db04c6cd6307b98678601c738c39d71cf3866186d8616e69bd191b9e.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e8b33c129649c78de3a773e578650c728e92279be12de1edc4f246b2939c3b01.png" - ], + ], "iris.tests.test_plot.Test1dScatter.test_coord_coord.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/bbfac39d9899384a6f6694a7b613cb489c95b7b7c24a399cc5913262d84acda0.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bbfac39d9899384a6f6694a7b613cb489c95b7b7c24a399cc5913262d84acda0.png", "https://scitools.github.io/test-iris-imagehash/images/v4/b2ecc12999994e16e666b5e3ce171969a5fb4ed49909e53990c44b9b7c09f684.png" - ], + ], "iris.tests.test_plot.Test1dScatter.test_coord_coord_map.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/bea07c99c15eb16e9891ce50c742394a3ced6cb13390f1cc73c29f1b2d0ecd66.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bea07c99c15eb16e9891ce50c742394a3ced6cb13390f1cc73c29f1b2d0ecd66.png", "https://scitools.github.io/test-iris-imagehash/images/v4/bea06899c14eb16e9895ce46c74a396a74ed64b13390b3c61b439f1b4d2ccde6.png" - ], + ], "iris.tests.test_plot.Test1dScatter.test_coord_cube.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/af7e1f0ff1e1e0f0d918960e6c076d8bd266d868c537365a90966db0de0e1b09.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/ae7e1f0ff1e1e0f0d918960e6c076d83d266d868c537365ad0966db0de4e1b09.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/af7e1f0ff1e1e0f0d918960e6c076d8bd266d868c537365a90966db0de0e1b09.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ae7e1f0ff1e1e0f0d918960e6c076d83d266d868c537365ad0966db0de4e1b09.png", "https://scitools.github.io/test-iris-imagehash/images/v4/be812fc1c078c03e930e3627672369c1d8d85c5b96463662e1fc699d9b9f7e03.png" - ], + ], "iris.tests.test_plot.Test1dScatter.test_cube_coord.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/edf896d79a67b94c651ced23d29392cccd646d33901912fcc4836d69ed249292.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/edf896d79a67b94c651ced23d29392cccd646d33901912fcc4836d69ed249292.png", "https://scitools.github.io/test-iris-imagehash/images/v4/edda96cb9a256b4765c26d9892dbc665693a1a9494b796c86c4b37ad92864c32.png" - ], + ], "iris.tests.test_plot.Test1dScatter.test_cube_cube.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/acf939339a16c64de306318638673c738c19d71cf3866186d8636e69bd191b9e.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/acf939339a16c64de306318638673c738c19d71cf3866186d8636e69bd191b9e.png", "https://scitools.github.io/test-iris-imagehash/images/v4/edb23c529649c78de38773e538650c729e92279be12de1edc4f246b2139c3b01.png" - ], + ], "iris.tests.test_plot.TestAttributePositive.test_1d_positive_down.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/87fef8117980c7c160078f1ffc049e7e90159a7a95419a7e910dcf1ece19ce3a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/87fef8117980c7c160078f1ffc049e7e90159a7a95419a7e910dcf1ece19ce3a.png", "https://scitools.github.io/test-iris-imagehash/images/v4/a7fe781b708487c360079e3bb4789869816bdb64c76b4a3cce7b4e749a6130c5.png" - ], + ], "iris.tests.test_plot.TestAttributePositive.test_1d_positive_up.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/87ff85d47800bd9f660779d0863f49c9947f4e1e9141de38d700da28ce1d9a2b.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/87ff85d47a00bc9f660779d8863f49c9907f4e1e9141de38d708da28ce1d9a0b.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/87ff85d47800bd9f660779d0863f49c9947f4e1e9141de38d700da28ce1d9a2b.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/87ff85d47a00bc9f660779d8863f49c9907f4e1e9141de38d708da28ce1d9a0b.png", "https://scitools.github.io/test-iris-imagehash/images/v4/a7ff958b7a00b09c661761c9907fcb0d9163ce7895289a618f381bffccf97200.png" - ], + ], "iris.tests.test_plot.TestAttributePositive.test_2d_positive_down.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fb946ba684e194fb901b3a0587641ad03b1ae7674e64c15a5b99c767c47e3a98.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fb946ba684e194fb901b3a0587641ad03b1ae7674e64c15a5b99c767c47e3a98.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fb946ba484e194dbc01f3665c0e4399a3f0fc2653f90c99e3f613e64c81e3f81.png" - ], + ], "iris.tests.test_plot.TestAttributePositive.test_2d_positive_up.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ee176c7f93e093a0c50f9383815e6e156859e17e6e15e17a9be08e2d851a9b83.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ee176c7f93e093a0c50f9383815e6e156859e17e6e15e17a9be08e2d851a9b83.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ebc06be1941e941ec07f941f907f6fa0950fc07e6f80c07f6b806be1c07f3f80.png" - ], + ], "iris.tests.test_plot.TestContour.test_tx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/cff8a55f7a15b55a7817854ad007a5e8c04f3ce8c04f3e2ac4706ab295b37a96.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/cff8a55f7a15b55a7817854ad007a5e8c04f3ce8c04f3e2ac4706ab295b37a96.png", "https://scitools.github.io/test-iris-imagehash/images/v4/eaece0173d17951fbd03974a914964e8c04a72e8c1531ee1cc746bb293973ecd.png" - ], + ], "iris.tests.test_plot.TestContour.test_ty.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8bfc815e78018597fc019b65b425d121955e7eda854b7d6a80db7eb481b72b61.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8bfc815e78018597fc019b65b425d121955e7eda854b7d6a80db7eb481b72b61.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ebfa8553fc01b15ab4044a269546caa5956b7e9bc0b97f2cc2d62d360b363b49.png" - ], + ], "iris.tests.test_plot.TestContour.test_tz.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8bfe81ff780185fff800955ad4027e00d517d400855f7e0085ff7e8085ff6aed.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/8bfe81ff780085fff800855fd4027e00d517d400855f7e0085ff7e8085ff6aed.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8bfe81ff780185fff800955ad4027e00d517d400855f7e0085ff7e8085ff6aed.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8bfe81ff780085fff800855fd4027e00d517d400855f7e0085ff7e8085ff6aed.png", "https://scitools.github.io/test-iris-imagehash/images/v4/8bfe817ffc00855ef0007e81d4027e80815fd56a03ff7a8085ff3aa883ff6aa5.png" - ], + ], "iris.tests.test_plot.TestContour.test_yx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa56c3cc34e891b1c9a91c36c5a170e3c71b3e5993a784e492c49b4ecec76393.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa56c3cc34e891b1c9a91c36c5a170e3c71b3e5993a784e492c49b4ecec76393.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e85e36cb95b199999765cd3694b06478c7396329958434c2cecb6c6d69ce1b92.png" - ], + ], "iris.tests.test_plot.TestContour.test_zx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8bfe857f7a01a56afa05854ad015bd00d015d50a90577e80857f7ea0857f7abf.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8bfe857f7a01a56afa05854ad015bd00d015d50a90577e80857f7ea0857f7abf.png", "https://scitools.github.io/test-iris-imagehash/images/v4/affe815ffc008554f8007e01d0027e808557d5ea815f7ea0817f2fea817d2aff.png" - ], + ], "iris.tests.test_plot.TestContour.test_zy.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8bff81ff7a0195fcf8019578d4027e00d550d402857c7e0185fe7a8385fe6aaf.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8bff81ff7a0195fcf8019578d4027e00d550d402857c7e0185fe7a8385fe6aaf.png", "https://scitools.github.io/test-iris-imagehash/images/v4/abff857ff8018578f8017a80d4027e00855ec42a81fe7a8185fe6a8f85fe6ab7.png" - ], + ], "iris.tests.test_plot.TestContourf.test_tx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/faa562ed68569d52857abd12953a8f12951f64e0d30f3ac96a4d6a696ee06a32.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/faa562ed68569d52857abd12953a8f12951f64e0d30f3ac96a4d6a696ee06a32.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea857a81957ac57e957a857a957a958ac5723b0d6ac56b833e856e606a923e90.png" - ], + ], "iris.tests.test_plot.TestContourf.test_ty.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/eaa5e03f957a4f80954a9e41e16e9c60970fb5b24ada634e6e93692d4ba562d8.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/eaa5e03f957a4f80954a9e41e16e9c60970fb5b24ada634e6e93692d4ba562d8.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea851f00957ac0f7957ac07f957a628d815e7b126ab13e816a953ae46a859ed3.png" - ], + ], "iris.tests.test_plot.TestContourf.test_tz.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e954a7a81857e957e857efc00857e7e007a85c02a7e859f287a85c1fe.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e954a7a81857e957e857efc00857e7e007a85c02a7e859f287a85c1fe.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e857e7a81857e7a81857e7a81857e7a806a85857a7a85857e7a85817e.png" - ], + ], "iris.tests.test_plot.TestContourf.test_yx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e95a6938b6b5969193901a4fc1e594a7c69999cbce33639879526e72330e65e4.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e95a6938b6b5969193901a4fc1e594a7c69999cbce33639879526e72330e65e4.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e97a3c7e968597b19685c9c696a7c79491c16e59691a387f6978396e68683184.png" - ], + ], "iris.tests.test_plot.TestContourf.test_zx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa85857ec45a7a81857e854a857ee56a917ec56a3a85c56a3a85c4ea7a8112fe.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa85857ec45a7a81857e854a857ee56a917ec56a3a85c56a3a85c4ea7a8112fe.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa81817e857e7a81857a7a81957a6e81917a6caa3a85c57a3a8585fa6a8591fe.png" - ], + ], "iris.tests.test_plot.TestContourf.test_zy.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa81817ec40a7a81857e957e857ef40a857ef60b7a81c40a7b81e60f7a814aff.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa81817ec40a7a81857e957e857ef40a857ef60b7a81c40a7b81e60f7a814aff.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa81817e857e7a81857e7a81817a7e81817a668f7a91857e7a81857e7a85817e.png" - ], + ], "iris.tests.test_plot.TestHybridHeight.test_bounds.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/eab5313f954a7b9260f39789c5ec4cd084d0c4e45aa1c5fe3a04797bb13b3b06.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/eab5313f954a7b9260f39789c5ec4cd084d0c4e45aa1c5fe3a04797bb13b3b06.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ee856aa5957a955ac0bf954bc17e3b819548c07f3e81c07e2ec46ea4c07f3e84.png" - ], + ], "iris.tests.test_plot.TestHybridHeight.test_bounds.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/be853f80854ac17ec0bdc2f5c17a0d09cc1fc07f5ab5e1fe3f409d7a38743e00.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/be853f80854ac17ec0bdc2f5c17a0d09cc1fc07f5ab5e1fe3f409d7a38743e00.png", "https://scitools.github.io/test-iris-imagehash/images/v4/bf813e85c07ec57ec17e9073c07e3f81856ec17a3f80c0fe3e813f84c2733e80.png" - ], + ], "iris.tests.test_plot.TestHybridHeight.test_bounds.2": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/eab5313f954a7b9260f39789c5ec4cd084d0c4e45aa1c5fe3a04797bb13b3b06.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/eab5313f954a7b9260f39789c5ec4cd084d0c4e45aa1c5fe3a04797bb13b3b06.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ee856aa5957a955ac0bf954bc17e3b819548c07f3e81c07e2ec46ea4c07f3e84.png" - ], + ], "iris.tests.test_plot.TestHybridHeight.test_orography.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa17291f95e895e8645e7a95c17a6eece4b4e1333b01c07e1bb13909914b9ec1.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/fa17291f95e895e8645e7a91c17a6ee464f4e1333b01c17e1bb1390d914b9ec1.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa17291f95e895e8645e7a95c17a6eece4b4e1333b01c07e1bb13909914b9ec1.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa17291f95e895e8645e7a91c17a6ee464f4e1333b01c17e1bb1390d914b9ec1.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa817a91957a857ac4fe268cc07f6e846e05d9373b81d17b1b6a1b41c4fa2cc4.png" - ], + ], "iris.tests.test_plot.TestHybridHeight.test_orography.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/bb07314fc4e0c6b4c31e9ee1847939a1c116c15e7b94e57e1ea9391de16e1ac3.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/bb07314fc6e1c6b4c31e9ee1846939a1c116c15e7b14e17e1ea9393de16e1ac3.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bb07314fc4e0c6b4c31e9ee1847939a1c116c15e7b94e57e1ea9391de16e1ac3.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bb07314fc6e1c6b4c31e9ee1846939a1c116c15e7b14e17e1ea9393de16e1ac3.png", "https://scitools.github.io/test-iris-imagehash/images/v4/af0b690f96f0d2d4c25e94a194ad3da19a52c25e3f02c07f3fa52d03c16a3fcb.png" - ], + ], "iris.tests.test_plot.TestHybridHeight.test_points.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea953bfb956ac4f4649f1a05c56e6ca45a53945e6ea5c13f1b498542c13f1b41.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea953bfb956ac4f4649f1a05c56e6ca45a53945e6ea5c13f1b498542c13f1b41.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fe857b91917a847ec0bd3f01c47e6ca43b11915a3ea4db3b1b4a84c4c03f3fc1.png" - ], + ], "iris.tests.test_plot.TestHybridHeight.test_points.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/be813fc0c15ac13dc1bfc27dc17e1d93c51fc43f1ea1c17a3ec138e4b1721a81.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/be813fc0c15ac13dc1bfc27dc17e1d93c51fc43f1ea1c17a3ec138e4b1721a81.png", "https://scitools.github.io/test-iris-imagehash/images/v4/be813a81c17ec57ec17e952ac07f3f808556c17e3f80c07f3e813f80c27e3f81.png" - ], + ], "iris.tests.test_plot.TestHybridHeight.test_points.2": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea9561ef956a7b92609b922dc16e6ec6845ac47e5aa5c57e5ec04861957b1b81.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea9561ef956a7b92609b922dc16e6ec6845ac47e5aa5c57e5ec04861957b1b81.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fe856a85957a955ac03f956ac17f3f809552c07f3e81c07e3e806e85c07e3f84.png" - ], + ], "iris.tests.test_plot.TestHybridHeight.test_points.3": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea953bfb956ac4f4649f1a05c56e6ca45a53945e6ea5c13f1b498542c13f1b41.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea953bfb956ac4f4649f1a05c56e6ca45a53945e6ea5c13f1b498542c13f1b41.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fe857b91917a847ec0bd3f01c47e6ca43b11915a3ea4db3b1b4a84c4c03f3fc1.png" - ], + ], "iris.tests.test_plot.TestHybridHeight.test_points.4": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/baf5347ecf0ac3f1c1f68f83850b1f83cc11c0fc7ad0c17a1be138e4b07e1a0d.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/baf5347ecf0ac3f1c1f68f83850b1f83cc11c0fc7ad0c17a1be138e4b07e1a0d.png", "https://scitools.github.io/test-iris-imagehash/images/v4/b878387e978ec2f0c0f09f83878f3f81c070c0fe78d0c1763fa13856d03e3f0f.png" - ], + ], "iris.tests.test_plot.TestMissingCS.test_missing_cs.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fac16ee0953b911bc15e9648e56ec4e691be7bcc7a8184733ea16a90c17e930d.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fac16ee0953b911bc15e9648e56ec4e691be7bcc7a8184733ea16a90c17e930d.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa816ac1857e853cc17f957ac15f3e849486c8f43e81c13b3f813e91c07e3f46.png" - ], + ], "iris.tests.test_plot.TestMissingCoord.test_no_u.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fe816a95c17fb51e953e9485857a1f409552856a1f81c17e5ab94e15c0ff5a85.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fe816a95c17fb51e953e9485857a1f409552856a1f81c17e5ab94e15c0ff5a85.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea856a95955a954ac17f954ac07e3f48951ec07e3e81c0ff7ea16a81c0bf3e81.png" - ], + ], "iris.tests.test_plot.TestMissingCoord.test_no_u.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/be0e695ac3f096b5943fd2a185fc1e8590e594ee1e05c17a4f403d0fe1fe4b42.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/be0e695ac3f096b5943fd2a185fc1e8590e594ee1e05c17a4f403d0fe1fe4b42.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea956ab5954a954ac17f954a817e3f40950ac07f3e81c0ff7a856aa1c0ff3f80.png" - ], + ], "iris.tests.test_plot.TestMissingCoord.test_no_v.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa8562b6c0773d09956a955a857a1d88845ec57e3f81c07e4ae56b21d0ff5a85.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa8562b6c0773d09956a955a857a1d88845ec57e3f81c07e4ae56b21d0ff5a85.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa816a85957a857ac17e954ac17e1fa2950bc07e3e81c07f3e807a85c17f3f81.png" - ], + ], "iris.tests.test_plot.TestMissingCoord.test_no_v.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa9562d4c7c43d0bb57b97e0857a3f1995d284763a05c17a7b856a2dc0f45a84.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa9562d4c7c43d0bb57b97e0857a3f1995d284763a05c17a7b856a2dc0f45a84.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa856a85957a857ac17e954ac17e9d02954ac07e3e81c07f3e857a85c2fd3f80.png" - ], + ], "iris.tests.test_plot.TestMissingCoord.test_none.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa8562b6c0763d09b54a955a857a3f88845ec57a3e85c07e6a616b25d0ff7a81.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa8562b6c0763d09b54a955a857a3f88845ec57a3e85c07e6a616b25d0ff7a81.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa816a85957a857ac17e954ac17e3fa29506c07e3e81c07f3e807a84c1ff3f81.png" - ], + ], "iris.tests.test_plot.TestMissingCoord.test_none.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa8562f6c0773d09b54a955a857a3f81955ac47e3e85c17e7aa16a25c0765aa1.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa8562f6c0773d09b54a955a857a3f81955ac47e3e85c17e7aa16a25c0765aa1.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa856a85957a957ac17e954ac17a1f06954ac07e3e81c07f3e817a85c0ff3f80.png" - ], + ], "iris.tests.test_plot.TestPcolor.test_tx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e85e67c9c7e1391e97a596b03a3696a13c4f63066318695ec5c9695e6c49c6a5.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e85e67c9c7e1391e97a596b03a3696a13c4f63066318695ec5c9695e6c49c6a5.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea817a81957e857e957e953e957e857e857e6aa06a816ac16a017a816a9585fa.png" - ], + ], "iris.tests.test_plot.TestPcolor.test_ty.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea74c707958b3878958b38f8c7236a557a542c7868d54b877875978abc789722.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/ea74c707958b387895ab38f8c7236a557a542c7868d54b05787197eab478972a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea74c707958b3878958b38f8c7236a557a542c7868d54b877875978abc789722.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea74c707958b387895ab38f8c7236a557a542c7868d54b05787197eab478972a.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea953f83954ac2bc956ac07e956a3509c0de61796ab57a816a856ad16ab590fb.png" - ], + ], "iris.tests.test_plot.TestPcolor.test_tz.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e874978b978b6875978b6875978b7854950b78506855787468747ea2687597aa.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e874978b978b6875978b6875978b7854950b78506855787468747ea2687597aa.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e857a7a84857a7a85857e7a813a2f7a817a85857a7a85857a7a85857a.png" - ], + ], "iris.tests.test_plot.TestPcolor.test_yx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e95e696994b196b793b19a1ec3c191c5c6e596191e4e693269336c36391a6e3a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e95e696994b196b793b19a1ec3c191c5c6e596191e4e693269336c36391a6e3a.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e97a387e968596319697c3c19284a62c93a560c36933393a6c7e793b6c6b31cd.png" - ], + ], "iris.tests.test_plot.TestPcolor.test_zx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e85e87a197a1695a97a1695a97a17d5a97a17906785a7816685a7e86685ad687.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e85e87a197a1695a97a1695a97a17d5a97a17906785a7816685a7e86685ad687.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e857a7e01857e7a81857e7a81e8177a816a8585fa7a85857e7a81857e.png" - ], + ], "iris.tests.test_plot.TestPcolor.test_zy.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/af42c0bdd0ad2f52d0bd3f42d0bd7f02d0bd7f003d527f002f427ea82f42d6a8.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/af42c0bdd0ad2f52d0ad2b52d0bd7f02d0bd7f002d527f002f527e0d2f52d4ad.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/af42c0bdd0ad2f52d0bd3f42d0bd7f02d0bd7f003d527f002f427ea82f42d6a8.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/af42c0bdd0ad2f52d0ad2b52d0bd7f02d0bd7f002d527f002f527e0d2f52d4ad.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e857e7e80857e7a81857e7a812d557a817a85857e7a81857e7a80857e.png" - ], + ], "iris.tests.test_plot.TestPcolorNoBounds.test_tx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa1594f3858a670c94e37b1cccb13e736a1d8cf17a1f94e2c119938e9463678c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa1594f3858a670c94e37b1cccb13e736a1d8cf17a1f94e2c119938e9463678c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea858782957a703f957a3878957a7a65957a6bc06ae76f806ad50fd06a859c50.png" - ], + ], "iris.tests.test_plot.TestPcolorNoBounds.test_ty.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ad5e94a5c3b0c3f096a5695a96a53c0f711b3c0f7d1b97b46943c3e0cc416b5a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ad5e94a5c3b0c3f096a5695a96a53c0f711b3c0f7d1b97b46943c3e0cc416b5a.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea85857a857e7e81957a7a81957a6a85857acac6c1fb6aa67a81956e6a81b506.png" - ], + ], "iris.tests.test_plot.TestPcolorNoBounds.test_tz.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/a95e3c1f96a096a5d6a5eb40c3f0ebe0c1c0c3f07c0b3e3e96a13c1e6d5b694a.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/a95e381f96a096a5d6a5eb40c3f0ebf0c1e0c3f07c0a3e3e96a13c1e6d5b694a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a95e3c1f96a096a5d6a5eb40c3f0ebe0c1c0c3f07c0b3e3e96a13c1e6d5b694a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a95e381f96a096a5d6a5eb40c3f0ebf0c1e0c3f07c0a3e3e96a13c1e6d5b694a.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa817e81857e857a857e7a81857e6a85817b81e63a913e857e81c17e7a81956e.png" - ], + ], "iris.tests.test_plot.TestPcolorNoBounds.test_yx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/bc7a1c32d3c366cdc585c39986cdc79ec792e3a6960d584939793c3438743873.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bc7a1c32d3c366cdc585c39986cdc79ec792e3a6960d584939793c3438743873.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e96ac78796953c4c9685383996c538e69692637263696b49693ac796693ac71b.png" - ], + ], "iris.tests.test_plot.TestPcolorNoBounds.test_zx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea1f781f95e085e885e0954295e195ea95a085e87a153e7f95e06a1778557a1f.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea1f781f95e085e885e0954295e195ea95a085e87a153e7f95e06a1778557a1f.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa817a81857e857e857e7a81857e6a81c17f95786aa77a807e81c17c7e819558.png" - ], + ], "iris.tests.test_plot.TestPcolorNoBounds.test_zy.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ba173a1795e895e8c5e8f400c1f8c1f895a8c5e87a077a5ec5e83e173e177e02.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ba173a1795e895e8c5e8f400c1f8c1f895a8c5e87a077a5ec5e83e173e177e02.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa817a80857e857e857e7a81817e3e81817e857f6aa07a857e80c17f7e80c15f.png" - ], + ], "iris.tests.test_plot.TestPcolormesh.test_tx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e85e67c9c7e1391e97a596b03a3696a13c4fe3026318695ec5c9695e6c49c6a5.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e85e67c9c7e1391e97a596b03a3696a13c4fe3026318695ec5c9695e6c49c6a5.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea817a81957e857e957e953e957e857e857e6aa06a816ac16a017a816a9585fa.png" - ], + ], "iris.tests.test_plot.TestPcolormesh.test_ty.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea74c707958b3878958b38f8c7236a557a542c7868d54b877875978abc789722.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/ea74c707958b387895ab38f8c7236a557a542c7868d54b05787197eabc789722.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea74c707958b3878958b38f8c7236a557a542c7868d54b877875978abc789722.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea74c707958b387895ab38f8c7236a557a542c7868d54b05787197eabc789722.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea953f83954ac2fc956ac07e956a3509c0de61796ab57a816a854ad16ab590fb.png" - ], + ], "iris.tests.test_plot.TestPcolormesh.test_tz.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e874978b978b6875978b6875978b7854950b78506855787468747ea2687597aa.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e874978b978b6875978b6875978b7854950b78506855787468747ea2687597aa.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e857a7a84857a7a85857e7a813a2f7a817a85857a7a85857a7a85857a.png" - ], + ], "iris.tests.test_plot.TestPcolormesh.test_yx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e95e696994b196b593b19a1ec3c591c5c6e596191e4e693269336c36391a6e3a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e95e696994b196b593b19a1ec3c591c5c6e596191e4e693269336c36391a6e3a.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e9693878969696139296c38f9bcc3474692169cb6c7339393c6cc387c78796cc.png" - ], + ], "iris.tests.test_plot.TestPcolormesh.test_zx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e85e87a197a1695a97a16d5a97a17d5a97a17806785a7816685a7e86685ad687.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e85e87a197a1695a97a16d5a97a17d5a97a17806785a7816685a7e86685ad687.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e857e6e05857e7a81857e7a81a0577a816a8585fa7a85857e7a81857e.png" - ], + ], "iris.tests.test_plot.TestPcolormesh.test_zy.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/af42c0bdd0ad2f52d0bd3f42d0bd7f02d0bd7f002d527f002f427fa82f42d6a8.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/af4280bdd0ad2f52d0ad2b52d0bd7f02d0bd7f002d527f002f527f0d2f52d4ad.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/af42c0bdd0ad2f52d0bd3f42d0bd7f02d0bd7f002d527f002f427fa82f42d6a8.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/af4280bdd0ad2f52d0ad2b52d0bd7f02d0bd7f002d527f002f527f0d2f52d4ad.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e857e7e80857e7a81857e7a8129577a817a85857e7a81857e7a80857e.png" - ], + ], "iris.tests.test_plot.TestPcolormeshNoBounds.test_tx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa1594f3858a670c94e37b1cccb13e736a1d84f17a1d94e2c11d938e9463678e.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/fa1594f3858a670c94e37b1cccb13e736a1d8cf17a1d94e2c11993ae9463678c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa1594f3858a670c94e37b1cccb13e736a1d84f17a1d94e2c11d938e9463678e.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa1594f3858a670c94e37b1cccb13e736a1d8cf17a1d94e2c11993ae9463678c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea858782957a703f957a3878957a7a65957e6bc06ae56f806ad50fd06a859c50.png" - ], + ], "iris.tests.test_plot.TestPcolormeshNoBounds.test_ty.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ad5e94a5c3b0c3f096a1695a96a53c1f711b3c0f791b97b46943c3e06c436b5a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ad5e94a5c3b0c3f096a1695a96a53c1f711b3c0f791b97b46943c3e06c436b5a.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea85857a857e7e81957a7a81957a6a85857acae6c1fb6aa47a81956e6a81b506.png" - ], + ], "iris.tests.test_plot.TestPcolormeshNoBounds.test_tz.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/a95e3c1f96a096a5d6a56b40c3f06be2c1c0c3f07c0b3ebe96a13c1e6d5b694a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a95e3c1f96a096a5d6a56b40c3f06be2c1c0c3f07c0b3ebe96a13c1e6d5b694a.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa813e81857e857a857e7a81857e6a85817b00e63eb93e857e81c17e7a81956e.png" - ], + ], "iris.tests.test_plot.TestPcolormeshNoBounds.test_yx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/bc7a1c32d3c366cdc785c39986cdc78ec792e7a6960d584939793c3438703873.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bc7a1c32d3c366cdc785c39986cdc78ec792e7a6960d584939793c3438703873.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e129c7169ed638ec9ed6387196c761c665396724612dcf0d693896929ed698c9.png" - ], + ], "iris.tests.test_plot.TestPcolormeshNoBounds.test_zx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea1f781f95e085e895e0fd4295e095ea95a085e87a153e7e95e06a1778157a17.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea1f781f95e085e895e0fd4295e095ea95a085e87a153e7e95e06a1778157a17.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa817a85857a857e857e7a81857e7a81817f95506aaf7a807e81c17c7a81957a.png" - ], + ], "iris.tests.test_plot.TestPcolormeshNoBounds.test_zy.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ba176a1795e895e8c5e87c00c1f8c1f894a8c5e87a077adec5e83e173e177a06.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ba176a1795e895e8c5e87c00c1f8c1f894a8c5e87a077adec5e83e173e177a06.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa817a80857a857e857e7a81857e3e81817e2fc56aa07a857e80c17f7e80c17f.png" - ], + ], "iris.tests.test_plot.TestPlot.test_t.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/83fe955f6a05e5137305d9c4f443127195187e9cd5467fa3d4917b68fc007a1a.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/8ffe95027e05e7007305d9c4a447127f853f069f814f2fa7d4d12b6cfc007e5a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/83fe955f6a05e5137305d9c4f443127195187e9cd5467fa3d4917b68fc007a1a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8ffe95027e05e7007305d9c4a447127f853f069f814f2fa7d4d12b6cfc007e5a.png", "https://scitools.github.io/test-iris-imagehash/images/v4/8ffe9c1a7e05e718f305d9d2e46312718138049e824e2fa783db2bed76b4fe00.png" - ], + ], "iris.tests.test_plot.TestPlot.test_t_dates.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/abffd5ae2a15cdb6b10178d7d4082e57d7290906f685814277b1dc88724cfd26.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/abffd5ae2a15c9b6a10178d7d4082c57d7290906f6c58942f7b1dc88724cfd26.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/abffd4a02a01cc84f10078d7d4082c77d73909ded6ef816273bd9c98725cdd26.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/abffd5ae2a15cdb6b10178d7d4082e57d7290906f685814277b1dc88724cfd26.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/abffd5ae2a15c9b6a10178d7d4082c57d7290906f6c58942f7b1dc88724cfd26.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/abffd4a02a01cc84f10078d7d4082c77d73909ded6ef816273bd9c98725cdd26.png", "https://scitools.github.io/test-iris-imagehash/images/v4/87fc9d8a7e054d83f5067bc1c1423471927ba73c8d9f864e09a1a7b358c8276f.png" - ], + ], "iris.tests.test_plot.TestPlot.test_x.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8ffe95297e87c74a6a059158f89c3d6ed0536597c0387836d0f87866d0697097.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8ffe95297e87c74a6a059158f89c3d6ed0536597c0387836d0f87866d0697097.png", "https://scitools.github.io/test-iris-imagehash/images/v4/8bfe956b7c01c2f26300929dfc1e3c6690736f91817e3b0c84be6be5d1603ed1.png" - ], + ], "iris.tests.test_plot.TestPlot.test_y.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8fe896266f068d873b83cb71e435725cd07c607ad07e70fcd0007a7881fe7ab8.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/8fe896066f068d873b83cb71e435725cd07c607ad07c70fcd0007af881fe7bb8.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/8fe896366f0f8d93398bcb71e435f24ed074646ed07670acf010726d81f2798c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8fe896266f068d873b83cb71e435725cd07c607ad07e70fcd0007a7881fe7ab8.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8fe896066f068d873b83cb71e435725cd07c607ad07c70fcd0007af881fe7bb8.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8fe896366f0f8d93398bcb71e435f24ed074646ed07670acf010726d81f2798c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/aff8946c7a14c99fb193d263e42432d8d00c2d27944a3f8dc5223ef703ff6b90.png" - ], + ], "iris.tests.test_plot.TestPlot.test_z.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8ffac1547a0792546c179db7f1254f6d945b7392841678e895017e3e91c17a0f.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8ffac1547a0792546c179db7f1254f6d945b7392841678e895017e3e91c17a0f.png", "https://scitools.github.io/test-iris-imagehash/images/v4/8ff8c1fa7a05b4ea6c059d2ff1494e4b90f26304846d78d1872a6cfc938b2e3e.png" - ], + ], "iris.tests.test_plot.TestPlotCitation.test.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/abf895067a1d9506f811783585437abd85426ab995067af9f00687f96afe87c8.png" - ], + ], "iris.tests.test_plot.TestPlotCitation.test_axes.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/abf895067a1d9506f811783585437abd85426ab995067af9f00687f96afe87c8.png" - ], + ], "iris.tests.test_plot.TestPlotCitation.test_figure.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/abf895067a1d9506f811783585437abd85426ab995067af9f00687f96afe87c8.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_non_cube_coordinate.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e857e7e81857e7a81857e7a81857e7a818576c02a7e95856a7e81c17a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e857e7e81857e7a81857e7a81857e7a818576c02a7e95856a7e81c17a.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa81857e857e3e85857e7a81857e7a81857e7a817e81780b7a81c56a7a81857e.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_tx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fe8142f5c17ebd2cc16eb548954a9542916a347a915e60bd4afd68793f916296.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fe8142f5c17ebd2cc16eb548954a9542916a347a915e60bd4afd68793f916296.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea853f10956ac5e1957a854e957a203e955e6aa76ae17aa16a856aaf6ab19e12.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_tx.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa8542b7b503b548857abd08857abd09945eed6b91d968c161b972d76aa462b5.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/fa8542b7b503b548857abd08857abd09945eed6a91d96ac163b972d36aa462b5.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa8542b7b503b548857abd08857abd09945eed6b91d968c161b972d76aa462b5.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa8542b7b503b548857abd08857abd09945eed6a91d96ac163b972d36aa462b5.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea853a85857a857a957a857a957ad05a857b3e946a606b917a816f647a853af4.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_tx.2": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8bf88f457a03b5307e16b561f007b53ed067217ac1786afec0f570bf8178681a.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/8bf98f057a03b5307e16b561f007b53ad067217ac1786afec0f570bf8178685a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8bf88f457a03b5307e16b561f007b53ed067217ac1786afec0f570bf8178681a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8bf98f057a03b5307e16b561f007b53ad067217ac1786afec0f570bf8178685a.png", "https://scitools.github.io/test-iris-imagehash/images/v4/eafdcec9bc219530b696a56694c2852a95656b7b81986acdc0e516adad186eda.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_tx.3": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8ffe8f367e05952afe05a50b980ded4bd05d69c2c1fb71c1c06272f4d0a06af4.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8ffe8f367e05952afe05a50b980ded4bd05d69c2c1fb71c1c06272f4d0a06af4.png", "https://scitools.github.io/test-iris-imagehash/images/v4/aff24ab7bd05952fbd0f950f914fcd48c47860f3e1b9329094266e345a850f6c.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_tx.4": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/aa953d0f85fab50fd0f2956a7a1785fafa176877d00f68f1d02c60f2f008d0f0.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/ebeaa5419e94b5019e97950d685395bee05361fad05560fad01570fef001dabe.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/ebeaa5419e95b5419e97950d6853953ee053617ad05560fad01570fef001dabe.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/aa953d0f85fab50fd0f2956a7a1785fafa176877d00f68f1d02c60f2f008d0f0.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ebeaa5419e94b5019e97950d685395bee05361fad05560fad01570fef001dabe.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ebeaa5419e95b5419e97950d6853953ee053617ad05560fad01570fef001dabe.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ebfaa56f96a1856cd681a56ee8162d52e8467e12c50c7e8095ad7e0095ad03ff.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_tx.5": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ebfaaf439e87b5019687b5019687b56ac05561fae07103fe6079687a607178f8.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/ebfa2d4b968795059e87970f6854697ae055697ac08561fad041d7aef001d6ae.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ebfaaf439e87b5019687b5019687b56ac05561fae07103fe6079687a607178f8.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ebfa2d4b968795059e87970f6854697ae055697ac08561fad041d7aef001d6ae.png", "https://scitools.github.io/test-iris-imagehash/images/v4/eb7a3e0c978187a4950190bc6856687a607e687bc0fcc1e394acfc0197fc2bfb.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_x.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/aeb8b5095a87cd60386592d9ec97ad6dd23ca4f6d0797827f0096216c1f878e6.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/aeb8b5095a87cd60386592d9ec97ad6dd23ca4f6d0797827f0096216c1f878e6.png", "https://scitools.github.io/test-iris-imagehash/images/v4/affa950ddb13c03634359ad8a4c80f26911f26f3c06e0ff3f4007b4285fd6e72.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_y.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8fea97194f07c9c830d79169ce16269f91097af6c47861f6d0796076d0797a16.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/8fee970b4f07c9c930d79129ce16269f91097af6c4f861f4d0786076d0797a16.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/afea97094f07c9c870d79129ce16269f91096af6c4f861f6c07960f6d0797a16.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8fea97194f07c9c830d79169ce16269f91097af6c47861f6d0796076d0797a16.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8fee970b4f07c9c930d79129ce16269f91097af6c4f861f4d0786076d0797a16.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/afea97094f07c9c870d79129ce16269f91096af6c4f861f6c07960f6d0797a16.png", "https://scitools.github.io/test-iris-imagehash/images/v4/afee9632de05c9d9f180d168c454a53e931b3e84956a3b8c85d94ce703ff7284.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_yx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea85603f956a9741951e9d83c1fa8d2fd0a55af0d25f345ae5f062c72d68612d.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea85603f956a9741951e9d83c1fa8d2fd0a55af0d25f345ae5f062c72d68612d.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea853f00957ac07c957ac0be951a69f3c47c7a5f3a6127816b953e646b813761.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_yx.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e85a69cc96ad92e193c9963385929e1cc3819acde6d965ce6e666b30386e65b1.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e85a69cc96ad92e193c9963385929e1cc3819acde6d965ce6e666b30386e65b1.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e97a346c9685cb899685c9c39695c79396ec634969ce2c74697a3864697b3c8c.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_yx.2": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8ffcc65767039740bc069d9ad00b8dadd03f52f181dd347a847a62ff81e8626c.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/8ffcc65777039740bc069d9ad00b8dadd03d52f181dd707a847a62ff81e8626c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8ffcc65767039740bc069d9ad00b8dadd03f52f181dd347a847a62ff81e8626c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8ffcc65777039740bc069d9ad00b8dadd03d52f181dd707a847a62ff81e8626c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ebffca44f502b36498309c9b940999add1bb62bba784374acc5a6a246acc6b65.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_yx.3": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea5649c434ac92e5d9c9361b95b39c38c3835a5ec6d966ced34c633099ace5a5.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea5649c434ac92e5d9c9361b95b39c38c3835a5ec6d966ced34c633099ace5a5.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e85a6b6c96a597a591c9949b94b61b69c7926b5bccce66646b3869b831a52c26.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_yx.4": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ad2f6d2dd2d09295c3c0c7d13c1bc6d23d2c696de0e53c3ac393daf6d205c2c4.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/ad2f6d2fd2d09295c3c0c7d13c1bc6d23d2c696ce0e53c3ac393dbf6d205c2c0.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/ad2f6d2f92d09295c3d0c7d13c1bc6d23d2c696cf0e53c3ac2b3d9f6d201c2c4.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/e85e3e2f97a1c19996a1c8f26c1e360f684a3c2c6913dca497b9d38097a903ff.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ad2f6d2dd2d09295c3c0c7d13c1bc6d23d2c696de0e53c3ac393daf6d205c2c4.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ad2f6d2fd2d09295c3c0c7d13c1bc6d23d2c696ce0e53c3ac393dbf6d205c2c0.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ad2f6d2f92d09295c3d0c7d13c1bc6d23d2c696cf0e53c3ac2b3d9f6d201c2c4.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e85e3e2f97a1c19996a1c8f26c1e360f684a3c2c6913dca497b9d38097a903ff.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e85e3e3f96a1c3e197a169f1785e3b0e68523e1c398bc58687b1d86096e1039f.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_yx.5": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e9686d8c9696924797879e3b86929e58696d69cc6869659379626133398d9ccd.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/e961658f961e92469e1e1c7966f36cd86165618c70e166b39b9698719e1e9ec8.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e9686d8c9696924797879e3b86929e58696d69cc6869659379626133398d9ccd.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e961658f961e92469e1e1c7966f36cd86165618c70e166b39b9698719e1e9ec8.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e1a530e29e5ecf199a5acd8f64f1326161a530e265999cd29e52cf199a5e6669.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_zx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/bf803f00c05fc4bfc07ec15dc05fd8bbc07cc96c333a32113bd02dd27ced3ec0.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bf803f00c05fc4bfc07ec15dc05fd8bbc07cc96c333a32113bd02dd27ced3ec0.png", "https://scitools.github.io/test-iris-imagehash/images/v4/be813ea0c17ec55ac17ed23dc07e295ac57e3b653f803f813e816e853e81b542.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_zx.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea95956a95626993941a6a2d956e6ed6845a6e65c4bec7b64a9594686ea19578.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea95956a95626993941a6a2d956e6ed6845a6e65c4bec7b64a9594686ea19578.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea85856e857e4893957a7aa1956a7b81954b3b817a856fd46a85846e6e85857e.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_zx.2": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8fe82f047c018c83bc01bc5af01fd1bcd15a327c847860fdc57a69beb0be68bd.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/8fe82f047c018c83bc01bc5af01fd1bcd15a32fd847860fdc57269beb0be689d.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8fe82f047c018c83bc01bc5af01fd1bcd15a327c847860fdc57a69beb0be68bd.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8fe82f047c018c83bc01bc5af01fd1bcd15a32fd847860fdc57269beb0be689d.png", "https://scitools.github.io/test-iris-imagehash/images/v4/8bedcf25bc03a4929c103a5bf03fdbbc81cb364d86e46da70f86899b3a0f6cc0.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_zx.3": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/cee8953a7a15856978579696d03d672cc49a6e5a842d3d2cc0b66bd1c2ea39f1.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/cee8953a7a15856978579696d03d672cc49a6e5a842d3d2cc0b66bd1c2ea39f1.png", "https://scitools.github.io/test-iris-imagehash/images/v4/aee1f93a63168569b852d697913d632485ca2e43952d3bbcc2b66bd1426b3c71.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_zx.4": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ee953f0591ea3f07914a95fa7e07d1fa68156a15d07c6a3dd038c0fef000d0fa.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/ae953f0591ea3f07914a95fa7e07d1fa68156a15d07c6a7dd068c0fef000d0fa.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ee953f0591ea3f07914a95fa7e07d1fa68156a15d07c6a3dd038c0fef000d0fa.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ae953f0591ea3f07914a95fa7e07d1fa68156a15d07c6a7dd068c0fef000d0fa.png", "https://scitools.github.io/test-iris-imagehash/images/v4/bec11ab5c1be857ac13e7ae53c422d423e017a85b542fc00c1fefe0091fe03ff.png" - ], + ], "iris.tests.test_plot.TestPlotCoordinatesGiven.test_zx.5": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e87a973d96a56953968769439685a54ae05117eae0511fba60513bba69717aba.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/e87a952d96a56953968769439685a54ae85197eae0511fba60513bba69717aba.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e87a973d96a56953968769439685a54ae05117eae0511fba60513bba69717aba.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e87a952d96a56953968769439685a54ae85197eae0511fba60513bba69717aba.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e85a96ac97a16c5897a1791e95a53b0b913c6953687c4ec3685cc6c36e7c87c3.png" - ], + ], "iris.tests.test_plot.TestPlotDimAndAuxCoordsKwarg.test_coord_names.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/f9789b388786678686966c9093879ce592c79bc94d19929b6939cf66316c672c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/f9789b388786678686966c9093879ce592c79bc94d19929b6939cf66316c672c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/b87830b0c786cf269ec766c99399cce998d3b3166f2530d3658c692d30ec6735.png" - ], + ], "iris.tests.test_plot.TestPlotDimAndAuxCoordsKwarg.test_coord_names.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e9a53a59961ec5a62c691a587b9662e1c0e1e53e9e0e9b873ec15a7161bc642f.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e9a53a59961ec5a62c691a587b9662e1c0e1e53e9e0e9b873ec15a7161bc642f.png", "https://scitools.github.io/test-iris-imagehash/images/v4/b8a53b59c71ac5a6b8791c1867876b63d9e0e65c96199d871cc23339633664ce.png" - ], + ], "iris.tests.test_plot.TestPlotDimAndAuxCoordsKwarg.test_coords.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/f9789b388786678686966c9093879ce592c79bc94d19929b6939cf66316c672c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/f9789b388786678686966c9093879ce592c79bc94d19929b6939cf66316c672c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/b87830b0c786cf269ec766c99399cce998d3b3166f2530d3658c692d30ec6735.png" - ], + ], "iris.tests.test_plot.TestPlotDimAndAuxCoordsKwarg.test_coords.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e9a53a59961ec5a62c691a587b9662e1c0e1e53e9e0e9b873ec15a7161bc642f.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e9a53a59961ec5a62c691a587b9662e1c0e1e53e9e0e9b873ec15a7161bc642f.png", "https://scitools.github.io/test-iris-imagehash/images/v4/b8a53b59c71ac5a6b8791c1867876b63d9e0e65c96199d871cc23339633664ce.png" - ], + ], "iris.tests.test_plot.TestPlotDimAndAuxCoordsKwarg.test_default.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/f9789b388786678686966c9093879ce592c79bc94d19929b6939cf66316c672c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/f9789b388786678686966c9093879ce592c79bc94d19929b6939cf66316c672c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/b87830b0c786cf269ec766c99399cce998d3b3166f2530d3658c692d30ec6735.png" - ], + ], "iris.tests.test_plot.TestPlotDimAndAuxCoordsKwarg.test_yx_order.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa81948e857e4971907ea72e95fa66b2952e4ead6d429b527ac7a5286e981836.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa81948e857e4971907ea72e95fa66b2952e4ead6d429b527ac7a5286e981836.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa85978e837e68f094d3673089626ad792073985659a9b1a7a15b52869f19f56.png" - ], + ], "iris.tests.test_plot.TestPlotDimAndAuxCoordsKwarg.test_yx_order.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea159694856a6b5096afa53a36941da1e4f5c369cd1ae6d69b6a1c80625af2f6.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea159694856a6b5096afa53a36941da1e4f5c369cd1ae6d69b6a1c80625af2f6.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea95969c874a63d39ca3ad2a231cdbc9c4973631cd6336c633182cbc61c3d3f2.png" - ], + ], "iris.tests.test_plot.TestPlotOtherCoordSystems.test_plot_tmerc.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e63399cd99cd64b29999335965369b262649c98c9b3966c6998d3319ccd69333.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e63399cd99cd64b29999335965369b262649c98c9b3966c6998d3319ccd69333.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e665326d999ecc9b3319b3246666cce69b496cccccc9669923193336666699a6.png" - ], + ], "iris.tests.test_plot.TestQuickplotPlot.test_t.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/83ffb5d67fd4e5962211d9c6a443da77d5389c8ed346d923d011d968dc00da48.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/82ffb5d67fdde5962211d9c6a441da77d5389c8cd346d927d011d968dc00da48.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/82fabd867fd5e5822201d9c6a4539a77953d8cbf834f99e7d051996cdc00da48.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/83ffb5d67fd4e5962211d9c6a443da77d5389c8ed346d923d011d968dc00da48.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/82ffb5d67fdde5962211d9c6a441da77d5389c8cd346d927d011d968dc00da48.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/82fabd867fd5e5822201d9c6a4539a77953d8cbf834f99e7d051996cdc00da48.png", "https://scitools.github.io/test-iris-imagehash/images/v4/83ffb59a7f00e59a2205d9d6e4619a74d9388c8e884e8da799d30b6dddb47e00.png" - ], + ], "iris.tests.test_plot.TestQuickplotPlot.test_t_dates.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/a3ffd5ae7f51efb6200378d7d4082c17d7280906d6e58962db31d800da6cdd26.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/a3ffd4ae7f55efbe200178d7d4082c17d7280906d6e58962df319800da6cdd26.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/a3ffd4827f51ef94200078d7c4082c57d739095ed6ed8962db759808da6cdd26.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a3ffd5ae7f51efb6200378d7d4082c17d7280906d6e58962db31d800da6cdd26.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a3ffd4ae7f55efbe200178d7d4082c17d7280906d6e58962df319800da6cdd26.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a3ffd4827f51ef94200078d7c4082c57d739095ed6ed8962db759808da6cdd26.png", "https://scitools.github.io/test-iris-imagehash/images/v4/82fd958a7e006f9ba0077bc5c9462c759873dd3c8d8f826699a187b358c82f67.png" - ], + ], "iris.tests.test_plot.TestQuickplotPlot.test_x.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/83ffb5097e84c54a621799d8601d9966d213cd67c039d876d078d866d869d8f7.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/83ffbd097e84c54a621799d8601d9966d253cc27c039d876d078d866d869d8f7.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/83ffb5097e84c54a621799d8601d9966d213cd67c039d876d078d866d869d8f7.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/83ffbd097e84c54a621799d8601d9966d253cc27c039d876d078d866d869d8f7.png", "https://scitools.github.io/test-iris-imagehash/images/v4/82ff950b7f81c0d6620199bcfc5e986695734da1816e1b2c85be2b65d96276d1.png" - ], + ], "iris.tests.test_plot.TestQuickplotPlot.test_y.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/a7ffb6067f008d87339bc973e435d86ef034c87ad07c586cd001da69897e5838.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/a7ffb6067f008d87339bc973e435d86ef034c87ad07cd86cd001da68897e58a8.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/a7efb6367f008d97338fc973e435d86ef030c86ed070d86cd030d86d89f0d82c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a7ffb6067f008d87339bc973e435d86ef034c87ad07c586cd001da69897e5838.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a7ffb6067f008d87339bc973e435d86ef034c87ad07cd86cd001da68897e58a8.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a7efb6367f008d97338fc973e435d86ef030c86ed070d86cd030d86d89f0d82c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/a2fbb46e7f10c99f2013d863e46498dcd06c0d2798421fa5dd221e7789ff6f10.png" - ], + ], "iris.tests.test_plot.TestQuickplotPlot.test_z.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/83ffc1dc7e00b0dc66179d95f127cfc9d44959ba846658e891075a3e99415a2f.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/83ffc1dc7e00b0dc66179d95f127cfc9d44959ba846658e891075a3e99415a2f.png", "https://scitools.github.io/test-iris-imagehash/images/v4/a3ffc1d87e00b49964179d28f16bce4b98724b268c6d58e1972e4874998b2e7e.png" - ], + ], "iris.tests.test_plot.TestSimple.test_bounds.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa9562fcc7c0b50bb53b9f8085727a157a95c0f67a85e07e0be08069e07d9fa0.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa9562fcc7c0b50bb53b9f8085727a157a95c0f67a85e07e0be08069e07d9fa0.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea856a85954a957ac17e954ac17a9c3e956ac07e3e81c07f3e857aa5c2753f80.png" - ], + ], "iris.tests.test_plot.TestSimple.test_points.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa8562b7c2763d09956a955a855a1d88d45ec57a3f81c07e6ae16b21c0ff7a81.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa8562b7c2763d09956a955a855a1d88d45ec57a3f81c07e6ae16b21c0ff7a81.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea856a85957a957ac17e954ac17e1ca2954ac07e3e81c07f3e807a85c1ff3f81.png" - ], + ], "iris.tests.test_plot.TestSymbols.test_cloud_cover.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e95a330c96a5ccf2695a330c96a5ccf2695a330c96b5ccf3694a330c96b5ccf3.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e95a330c96a5ccf2695a330c96a5ccf2695a330c96b5ccf3694a330c96b5ccf3.png", "https://scitools.github.io/test-iris-imagehash/images/v4/eb52916494ad6e1b6b5291e494ad6e1b6b5291e494ad6e1b6b5291e494ad6e1b.png" - ], + ], "iris.tests.test_quickplot.TestLabels.test_alignment.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa95350f952ad2f0c1f66ac1c55a4af4e550a52b3e05905e1e419e6f937e3b21.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/fa95350f952ad3f0c1f66a81e55a4af4e550a52b3e05905e1e419e6f937e1b21.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa95350f952ad2f0c1f66ac1c55a4af4e550a52b3e05905e1e419e6f937e3b21.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa95350f952ad3f0c1f66a81e55a4af4e550a52b3e05905e1e419e6f937e1b21.png", "https://scitools.github.io/test-iris-imagehash/images/v4/be8137f4954ac03fc0ff3e81d03f496a6d00b4af3ea0c07f6fa232c0db7f2d00.png" - ], + ], "iris.tests.test_quickplot.TestLabels.test_contour.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/a3fd956a7a01a5ee321fc96666919b6ec15fdca593600d2586785a259dfa5a01.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/a3fd956a7a01a5ee3217c9e66691996ec15fdca593680d2586785a259dfa5a01.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a3fd956a7a01a5ee321fc96666919b6ec15fdca593600d2586785a259dfa5a01.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a3fd956a7a01a5ee3217c9e66691996ec15fdca593680d2586785a259dfa5a01.png", "https://scitools.github.io/test-iris-imagehash/images/v4/a7fd95da7a01654a3217c962e4819a56c96f3c8593624da584da3b658db662db.png" - ], + ], "iris.tests.test_quickplot.TestLabels.test_contour.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/faa12bc1954ef43fc0bf9f02854a4ee48548c17a5ab5c17e7a0d7875a17e3a81.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/faa12bc1954ef43fc0bf9f02854a4ee48548c17a5ab5c17e7a0d7875a17e3a81.png", "https://scitools.github.io/test-iris-imagehash/images/v4/bf802f85c17fc17fc07eb42ac07f3f929130c07e3f80c07f7aa02e85c07f3e81.png" - ], + ], "iris.tests.test_quickplot.TestLabels.test_contourf.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fe812f88957a955a857a9257c17f7aa5c03dc0bf5a85c07e7f402d40a57a3f01.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fe812f88957a955a857a9257c17f7aa5c03dc0bf5a85c07e7f402d40a57a3f01.png", "https://scitools.github.io/test-iris-imagehash/images/v4/be816a95957a957ac0fe1e8bc07f7f806e01c07f3f80c07f3fa23f00c07f3d00.png" - ], + ], "iris.tests.test_quickplot.TestLabels.test_contourf.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/faa12bc1954ef43fc0bf9f02854a4ee48548c17a5ab5c17e7a0d7875a17e3a81.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/faa12bc1954ef43fc0bf9f02854a4ee48548c17a5ab5c17e7a0d7875a17e3a81.png", "https://scitools.github.io/test-iris-imagehash/images/v4/bf802f85c17fc17fc07eb42ac07f3f929130c07e3f80c07f7aa02e85c07f3e81.png" - ], + ], "iris.tests.test_quickplot.TestLabels.test_contourf.2": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa852f81955ac532c0bf9e89c57edae69357e13f4ea0c05a3f8561a4935a3e01.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa852f81955ac532c0bf9e89c57edae69357e13f4ea0c05a3f8561a4935a3e01.png", "https://scitools.github.io/test-iris-imagehash/images/v4/be816a95907ae508c17e955ac07f3fa0945bc07f3f80c07f3aa36f01c0ff3f80.png" - ], + ], "iris.tests.test_quickplot.TestLabels.test_contourf_nameless.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/faa52ec1955ac536c0bf9e09c57edae69357e13f4e80c0da2f81618493da3f01.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/faa52ec1955ac536c0bf9e09c57edae69357e13f4e80c0da2f81618493da3f01.png", "https://scitools.github.io/test-iris-imagehash/images/v4/be816af5907ee508c17e955ac03f3f809419c07f3f80c07f3a8b6f81c0ff3f80.png" - ], + ], "iris.tests.test_quickplot.TestLabels.test_map.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea5e618434ac36e5c1c9369b95b39c38c3a39a4fcee19a6e9b64cb609925cd25.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea5e618434ac36e5c1c9369b95b39c38c3a39a4fcee19a6e9b64cb609925cd25.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e85a636c86a597a593c9b49b94b79969c396c95bccc69a64db30d9b039a52c26.png" - ], + ], "iris.tests.test_quickplot.TestLabels.test_map.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea5e618434ac36e5c1c9369b95b39c38c3a39a4ecef19a6e9b64cb609925cd25.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea5e618434ac36e5c1c9369b95b39c38c3a39a4ecef19a6e9b64cb609925cd25.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e85a636c86a597a593c9b49b94b79969c396c95bccc69a64db30d9b039a52c26.png" - ], + ], "iris.tests.test_quickplot.TestLabels.test_pcolor.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/bb423d4e94a5c6b9c15adaadc1fb6a469c8de43a3e07904e5f016b57984e1ea1.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bb423d4e94a5c6b9c15adaadc1fb6a469c8de43a3e07904e5f016b57984e1ea1.png", "https://scitools.github.io/test-iris-imagehash/images/v4/eea16affc05ab500956e974ac53f3d80925ac03f2f81c07e3fa12da1c2fe3f80.png" - ], + ], "iris.tests.test_quickplot.TestLabels.test_pcolormesh.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/bb433d4e94a4c6b9c15adaadc1fb6a469c8de43a3e07904e5f016b57984e1ea1.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bb433d4e94a4c6b9c15adaadc1fb6a469c8de43a3e07904e5f016b57984e1ea1.png", "https://scitools.github.io/test-iris-imagehash/images/v4/eea16affc05ab500956e974ac53f3d80925ac03f3f81c07e3fa12da1c27e3f80.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_non_cube_coordinate.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa816a85857a955ae17e957ec57e7a81855fc17e3a81c57e1a813a85c57a1a05.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa816a85857a955ae17e957ec57e7a81855fc17e3a81c57e1a813a85c57a1a05.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fe816a85857a957ac07f957ac07f3e80956ac07f3e80c07f3e813e85c07e3f80.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_tx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa856a95e15ab51a953e9485857a1f409552857e1fc1c07e5abd4a35e07f4aa5.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa856a95e15ab51a953e9485857a1f409552857e1fc1c07e5abd4a35e07f4aa5.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea856a95955a956ac17f950ac07e3f48951ac07f3f81c0ff3ea16aa1c0be3e81.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_tx.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/fa8562b7c2763d09956a955a855a1d88d45ec57a3f81c07e6ae16b21c0ff7a81.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/fa8562b7c2763d09956a955a855a1d88d45ec57a3f81c07e6ae16b21c0ff7a81.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea856a85957a957ac17e954ac17e1ca2954ac07e3e81c07f3e807a85c1ff3f81.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_tx.2": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8aff878b7f00953062179561f087953ad167997a80784a7fc1e5d86d9978485f.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/8aff878b7f80953860179561f087953ad167997a80784a7fc1e5d86d9978485b.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8aff878b7f00953062179561f087953ad167997a80784a7fc1e5d86d9978485f.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/8aff878b7f80953860179561f087953ad167997a80784a7fc1e5d86d9978485b.png", "https://scitools.github.io/test-iris-imagehash/images/v4/eafdc6c9f720953030968d6795d28d6a95674b7b81304aedc9e51cad8d186c9a.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_tx.3": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/82ff8db67f94952e76159d6bb01dcd629059c962c1fbd9c1c062da74d820ca74.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/82be8db67f95952e761d9d6bb01dcd628059c962c1fbd9e1c072da64d060ca74.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/82fe8db67f95952e76159d6bb01dcd629059c962c1fbd9e1c072da64d020ca74.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/82ff8db67f94952e76159d6bb01dcd629059c962c1fbd9c1c062da74d820ca74.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/82be8db67f95952e761d9d6bb01dcd628059c962c1fbd9e1c072da64d060ca74.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/82fe8db67f95952e76159d6bb01dcd629059c962c1fbd9e1c072da64d020ca74.png", "https://scitools.github.io/test-iris-imagehash/images/v4/a2ff6a967f00952eb40d9d0f900fcd62c47069f3d1f93a909c266e34d8a56f68.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_tx.4": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/aa97b70ff5f0970f20b2956a6a17957af805da71d06f5a75d02cd870d800d8f2.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/e1faa549de9497090697971d60539f3ef171c87ac075487ad025d87ed801da3e.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/aa97b70ff5f0970f20b2956a6a17957af805da71d06f5a75d02cd870d800d8f2.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e1faa549de9497090697971d60539f3ef171c87ac075487ad025d87ed801da3e.png", "https://scitools.github.io/test-iris-imagehash/images/v4/eadab54fd7a1856d90819d6df8169962e946d862802ed8809ded7e809d2d03ff.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_tx.5": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e8faad47f784bd0596859d03969f9962c05dc96ee07189fe6870c862687178f8.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/a8fa2d4797859585b6959d07605f896ee051697ad061d9fad0619aaed801deae.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e8faad47f784bd0596859d03969f9962c05dc96ee07189fe6870c862687178f8.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a8fa2d4797859585b6959d07605f896ee051697ad061d9fad0619aaed801deae.png", "https://scitools.github.io/test-iris-imagehash/images/v4/aa5b3c0c978187a4b60199bc605f6976687e6873d07c99e390acdc0391fc2f7b.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_x.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/a6ffb5097e84cde2224598d1649f8d6cd2388c76d0799867d009da76c9f8d866.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/a6bfb5097f84cde2224599d1649f8d6cd2388c76d0799867d009da76c1f8d866.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a6ffb5097e84cde2224598d1649f8d6cd2388c76d0799867d009da76c9f8d866.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a6bfb5097f84cde2224599d1649f8d6cd2388c76d0799867d009da76c1f8d866.png", "https://scitools.github.io/test-iris-imagehash/images/v4/a6fbb50cfbd0c036203598dce4c88d26d32f8cf3886e1df3dc047b4289ec6e72.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_y.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/a7ff978b7f00c9c830d7992166179e969509d866c478d964d079c876d869da26.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/a7ff97837f00c9c830d79921661f9e9695099876c478d964c079c876d879da26.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a7ff978b7f00c9c830d7992166179e969509d866c478d964d079c876d869da26.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a7ff97837f00c9c830d79921661f9e9695099876c478d964c079c876d879da26.png", "https://scitools.github.io/test-iris-imagehash/images/v4/a2ffb6127f0dc9993085d960c6748d3e9b121ca49d6a1b048df34ce789ff7205.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_yx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea856a9ff16eb740954a9e05855a19a3c0fbc13e1ea5c07d5ad0cb58e45e3c35.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea856a9ff16eb740954a9e05855a19a3c0fbc13e1ea5c07d5ad0cb58e45e3c35.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea856a95957a957ac07e954ac17e3e87950bc07f3ea4c27d3e833ac1c1e03f80.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_yx.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e5a565b69e1a9a42917e1a19c17b3a619e59c47b3a25c53e3b8430e5c57a3e85.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e5a565b69e1a9a42917e1a19c17b3a619e59c47b3a25c53e3b8430e5c57a3e85.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e5a761a79a589e58c07d1e48c07c3f819e41c07f3d84c17e3fa62585c0fe3f83.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_yx.2": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/afffe6d67700958636179d92e019992dd039daf5817d987a807a48e499684a6d.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/aeffe6d67780958636179d92e019892dd139daf5815d987a807a48e699684a6d.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/afffe6d67700958636179d92e019992dd039daf5817d987a807a48e499684a6d.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/aeffe6d67780958636179d92e019892dd139daf5815d987a807a48e699684a6d.png", "https://scitools.github.io/test-iris-imagehash/images/v4/eaff6ad4f74ab16490109c9b942999add1b74bb785a41d4acd526a254acc6365.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_yx.3": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea5e618434ac36e5c1c9369b95b39c38c3a39a4fcee19a6e9b64cb609925cd25.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea5e618434ac36e5c1c9369b95b39c38c3a39a4fcee19a6e9b64cb609925cd25.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e85a636c86a597a593c9b49b94b79969c396c95bccc69a64db30d9b039a52c26.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_yx.4": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ad2f6d2fd2d09295c2d1c3d33c1bc2d67d2c696ce0653c3ac2b1d976da05c2c4.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/ad2f6d2fd2d09295c2d1c3d33c1bc2d27d2c696ce0e53c3ad2b1d976da01c2c4.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/e85e3e2f97a1c19996a1c8f26d1e3a0f684a3c2c6913dc2497b9db8095e502ff.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ad2f6d2fd2d09295c2d1c3d33c1bc2d67d2c696ce0653c3ac2b1d976da05c2c4.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ad2f6d2fd2d09295c2d1c3d33c1bc2d27d2c696ce0e53c3ad2b1d976da01c2c4.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e85e3e2f97a1c19996a1c8f26d1e3a0f684a3c2c6913dc2497b9db8095e502ff.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e85e3c1f97a1c3e197a1c9f37c5e390668521e0c390bdd8685b1d86096e5279f.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_yx.5": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e968658e969692c797879e3b86929e58696d49cd6869c9a37962c923990d9c6d.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/e9e1658e961e92569e9e3c7966d36c586165698c70e1ce739b3698619e1e984c.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e968658e969692c797879e3b86929e58696d49cd6869c9a37962c923990d9c6d.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e9e1658e961e92569e9e3c7966d36c586165698c70e1ce739b3698619e1e984c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e1a530e29e5ecf199a5acd8f64f1326161a538e665a198d29e52cb1d9a5e6669.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_zx.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/bf813f80c156c05dc0fec29dc17f1a6dd05fc0ff1aa1c57e3b243b20375a1e81.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/bf813f80c156c05dc0fec29dc17f1a6dd05fc0ff1aa1c57e3b243b20375a1e81.png", "https://scitools.github.io/test-iris-imagehash/images/v4/be816a81d17ec57ac07e952ac07f3aa0955ec17e3f80c07f3f803f80c0bf3f81.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_zx.1": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ea95629d956a996069939e9bc07f7aad856cc47e5e81857a1e254a35c1be1b81.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ea95629d956a996069939e9bc07f7aad856cc47e5e81857a1e254a35c1be1b81.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa816a85957a957ac03f957ac07f3ba1954ac07e3e81c07f3ea47a85c07e3e80.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_zx.2": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/87ed2f867f008d8220179852f01fd9bed1789a6c847cc877c46ac972987ec8fd.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/87ed2f067f008d8220179852f01fd9bed1789a6c847cc877c468c9f6987ec8fd.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/87ed2f067f008d8220179c52f01fd9bed1789a6c847cc877c560c976987ec8fd.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/87ed2f867f008d8220179852f01fd9bed1789a6c847cc877c46ac972987ec8fd.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/87ed2f067f008d8220179852f01fd9bed1789a6c847cc877c468c9f6987ec8fd.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/87ed2f067f008d8220179c52f01fd9bed1789a6c847cc877c560c976987ec8fd.png", "https://scitools.github.io/test-iris-imagehash/images/v4/a3eded05fe11a492b000985af07fdbb4d1e3366d8c644da79fa68993180f6ec1.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_zx.3": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/a2f9b5ba7600a56962df9e96f01dc926c498cc46847f9d6cd0244bf19a6b19f1.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/a2f9b5ba7600856962df9e96f01dcd26c498cc46847f9d6cd0244bf19a6b1975.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a2f9b5ba7600a56962df9e96f01dc926c498cc46847f9d6cd0244bf19a6b19f1.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/a2f9b5ba7600856962df9e96f01dcd26c498cc46847f9d6cd0244bf19a6b1975.png", "https://scitools.github.io/test-iris-imagehash/images/v4/aef9f93a770085e9205fd696d13c4b2485ca1a43952f1934daa66bd1ca6b3c71.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_zx.4": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/ae953f87d5e82d86801f91ee6e1591fe7e117876c07d6877d068d878d800d07a.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/ae953f87d5e82d87801b91ee6e1599fe7e117874c07d6877d068d878d800d07a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ae953f87d5e82d86801f91ee6e1591fe7e117876c07d6877d068d878d800d07a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ae953f87d5e82d87801b91ee6e1599fe7e117874c07d6877d068d878d800d07a.png", "https://scitools.github.io/test-iris-imagehash/images/v4/bec1329dc5be85dac01d58d73e419d423e41daa59822dc00c5fefe0091fe03ff.png" - ], + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_zx.5": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/e87a952d96856943969f694696858d4ee0519d6ee07f9b6a78619b2a79711a2a.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/e87a952d96856943969f694696858d4ae0519d6ee07f996a78719b2a79711a3a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e87a952d96856943969f694696858d4ee0519d6ee07f9b6a78619b2a79711a2a.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e87a952d96856943969f694696858d4ae0519d6ee07f996a78719b2a79711a3a.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e85e96ac97a168d897a5791695a19927913c3953687ecce3687c86e3487cc6c3.png" - ], + ], "iris.tests.test_quickplot.TestTimeReferenceUnitsLabels.test_not_reference_time_units.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/82faa1977fdf89976200ddf6e000d9e7f75f9866d560dae4dc00d966dc005e20.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/82b8a1977fdf89876200dde6e000d9e7f77f9866d560dfe4dc00d966fc005e20.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/82f8a1977fdf89876200ddf6e000d9e7f77f9866d560dee4dc00d966dc005e20.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/82f8a1977fdf89876200dde6e000d9e7f77f9866d560dfe4dc00dd64dc005e20.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/82faa1977fdf89976200ddf6e000d9e7f75f9866d560dae4dc00d966dc005e20.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/82b8a1977fdf89876200dde6e000d9e7f77f9866d560dfe4dc00d966fc005e20.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/82f8a1977fdf89876200ddf6e000d9e7f77f9866d560dee4dc00d966dc005e20.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/82f8a1977fdf89876200dde6e000d9e7f77f9866d560dfe4dc00dd64dc005e20.png", "https://scitools.github.io/test-iris-imagehash/images/v4/82faa19e7f51898c6001dd86845fd9a2dd7f996281ee19f389ef03ffdc007e00.png" - ], + ], "iris.tests.test_quickplot.TestTimeReferenceUnitsLabels.test_reference_time_units.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/82fe81987fd777ffe0002addd4002805dda8de65dde9d4625bfddc209841de20.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/82fe81987fdf77ffe0002a9dd4002805ddaade65d9a9d5625bfddc209841de20.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/82fe81987fdf77ffe0002addd4002805dd28df67d9a9d4625bfddc209841de20.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/82fe81987fd777ffe0002addd4002805dda8de65dde9d4625bfddc209841de20.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/82fe81987fdf77ffe0002a9dd4002805ddaade65d9a9d5625bfddc209841de20.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/82fe81987fdf77ffe0002addd4002805dd28df67d9a9d4625bfddc209841de20.png", "https://scitools.github.io/test-iris-imagehash/images/v4/82fa80997f547799a0037a00d52f0956ddaf9f7e98a1816e09f5d8260bfffe00.png" ] } \ No newline at end of file From 5923aaa8e725831a1575678fd93c938bfad36e95 Mon Sep 17 00:00:00 2001 From: lbdreyer Date: Wed, 5 Sep 2018 11:39:06 +0100 Subject: [PATCH 31/62] Fix contig_ tolerance kwarg bug (#3162) --- lib/iris/plot.py | 9 +++++---- lib/iris/tests/unit/coords/test_Coord.py | 10 ++++++++++ .../test__check_bounds_contiguity_and_mask.py | 15 +++++++++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/lib/iris/plot.py b/lib/iris/plot.py index f0dfccd8c5..e3193e96a4 100644 --- a/lib/iris/plot.py +++ b/lib/iris/plot.py @@ -317,13 +317,14 @@ def _check_bounds_contiguity_and_mask(coord, data, atol=None): elif coord.ndim == 2: if atol: - args = {'atol': atol} + kwargs = {'atol': atol} else: - args = {} - contiguous, diffs = coord._discontiguity_in_bounds(*args) - diffs_along_x, diffs_along_y = diffs + kwargs = {} + 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. not_masked_at_discontiguity_along_x = np.any( np.logical_and(mask_invert[:, :-1], diffs_along_x)) diff --git a/lib/iris/tests/unit/coords/test_Coord.py b/lib/iris/tests/unit/coords/test_Coord.py index 58fc3e1f9e..8cf79caad3 100644 --- a/lib/iris/tests/unit/coords/test_Coord.py +++ b/lib/iris/tests/unit/coords/test_Coord.py @@ -504,6 +504,16 @@ def test_2d_discontiguous_along_x_and_y(self): self.assertArrayEqual(diffs_along_x, exp_x_diffs) self.assertArrayEqual(diffs_along_y, exp_y_diffs) + 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) + diffs_along_x, diffs_along_y = diffs + self.assertTrue(contiguous) + self.assertArrayEqual(diffs_along_x, np.array([2, 2, 2]).reshape(3, 1)) + self.assertTrue(not diffs_along_y.any()) + def test_2d_one_cell(self): # Test a 2D coord with a single cell, where the coord has shape (1, 1). coord = AuxCoord(self.points_3by3[:1, :1], 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 b067b3f387..225aa2890e 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 @@ -24,6 +24,8 @@ # importing anything else. import iris.tests as tests +import mock + import numpy as np import numpy.ma as ma @@ -72,6 +74,19 @@ def test_2d_contiguous(self): iplt._check_bounds_contiguity_and_mask(cube.coord('longitude'), cube.data) + def test_2d_contiguous_atol(self): + # Check the atol is passed correctly. + cube = sample_2d_latlons() + with mock.patch('iris.coords.Coord._discontiguity_in_bounds' + ) as discontiguity_check: + # 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) + 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. From 5bd0d15b1e00ca27e062cb12c251e7ffc1e91fa7 Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Mon, 10 Sep 2018 15:58:08 +0100 Subject: [PATCH 32/62] Add simple lazy aggregations --- lib/iris/analysis/__init__.py | 38 ++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/lib/iris/analysis/__init__.py b/lib/iris/analysis/__init__.py index 8cf1a5d2a7..546ba50fba 100644 --- a/lib/iris/analysis/__init__.py +++ b/lib/iris/analysis/__init__.py @@ -1419,23 +1419,6 @@ def interp_order(length): """ -MAX = Aggregator('maximum', ma.max) -""" -An :class:`~iris.analysis.Aggregator` instance that calculates -the maximum over a :class:`~iris.cube.Cube`, as computed by -:func:`numpy.ma.max`. - -**For example**: - -To compute zonal maximums over the *longitude* axis of a cube:: - - result = cube.collapsed('longitude', iris.analysis.MAX) - -This aggregator handles masked data. - -""" - - def _build_dask_mdtol_function(dask_stats_function): """ Make a wrapped dask statistic function that supports the 'mdtol' keyword. @@ -1534,7 +1517,8 @@ def inner_stat(array, axis=-1, mdtol=None, **kwargs): """ -MIN = Aggregator('minimum', ma.min) +MIN = Aggregator('minimum', ma.min, + lazy_func=_build_dask_mdtol_function(da.min)) """ An :class:`~iris.analysis.Aggregator` instance that calculates the minimum over a :class:`~iris.cube.Cube`, as computed by @@ -1551,6 +1535,24 @@ def inner_stat(array, axis=-1, mdtol=None, **kwargs): """ +MAX = Aggregator('maximum', ma.max, + lazy_func=_build_dask_mdtol_function(da.max)) +""" +An :class:`~iris.analysis.Aggregator` instance that calculates +the maximum over a :class:`~iris.cube.Cube`, as computed by +:func:`numpy.ma.max`. + +**For example**: + +To compute zonal maximums over the *longitude* axis of a cube:: + + result = cube.collapsed('longitude', iris.analysis.MAX) + +This aggregator handles masked data. + +""" + + PEAK = Aggregator('peak', _peak) """ An :class:`~iris.analysis.Aggregator` instance that calculates From 6186081884ecba4f8d5f18affaff6bccff7bd5e4 Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Wed, 12 Sep 2018 10:58:55 +0100 Subject: [PATCH 33/62] Lazy SUM and COUNT, tests improved --- lib/iris/analysis/__init__.py | 40 +++++- lib/iris/tests/unit/analysis/test_COUNT.py | 80 ++++++++++- lib/iris/tests/unit/analysis/test_SUM.py | 149 +++++++++++++++++++++ 3 files changed, 259 insertions(+), 10 deletions(-) create mode 100644 lib/iris/tests/unit/analysis/test_SUM.py diff --git a/lib/iris/analysis/__init__.py b/lib/iris/analysis/__init__.py index 546ba50fba..776f40054f 100644 --- a/lib/iris/analysis/__init__.py +++ b/lib/iris/analysis/__init__.py @@ -1193,11 +1193,22 @@ def _weighted_percentile(data, axis, weights, percent, returned=False, def _count(array, function, axis, **kwargs): if not callable(function): - raise ValueError('function must be a callable. Got %s.' - % type(function)) + emsg = 'function must be a callable. Got {}.' + raise TypeError(emsg.format(type(function))) return ma.sum(function(array), axis=axis, **kwargs) +def _lazy_count(array, axis, **kwargs): + try: + func = kwargs.pop('function') + except KeyError: + raise KeyError('no selection function supplied.') + if not callable(func): + emsg = 'function must be a callable. Got {}.' + raise TypeError(emsg.format(type(func))) + return da.sum(func(array), axis=axis, **kwargs) + + def _proportion(array, function, axis, **kwargs): # if the incoming array is masked use that to count the total number of # values @@ -1248,6 +1259,25 @@ def _sum(array, **kwargs): return rvalue +def _lazy_sum(array, axis, **kwargs): + # weighted or scaled sum + weights_in = kwargs.pop('weights', None) + returned_in = kwargs.pop('returned', False) + if weights_in is not None: + wsum = da.sum(weights_in * array, axis=axis, **kwargs) + else: + wsum = da.sum(array, axis=axis, **kwargs) + if returned_in: + if weights_in is None: + weights = iris_lazy_data.as_lazy_data(np.ones_like(array)) + else: + weights = weights_in + rvalue = (wsum, da.sum(weights, axis=axis)) + else: + rvalue = wsum + return rvalue + + def _peak(array, **kwargs): def column_segments(column): nan_indices = np.where(np.isnan(column))[0] @@ -1353,7 +1383,8 @@ def interp_order(length): # Common partial Aggregation class constructors. # COUNT = Aggregator('count', _count, - units_func=lambda units: 1) + units_func=lambda units: 1, + lazy_func=_lazy_count) """ An :class:`~iris.analysis.Aggregator` instance that counts the number of :class:`~iris.cube.Cube` data occurrences that satisfy a particular @@ -1452,6 +1483,7 @@ def inner_stat(array, axis=-1, mdtol=None, **kwargs): return result return inner_stat + MEAN = WeightedAggregator('mean', ma.average, lazy_func=_build_dask_mdtol_function(da.mean)) """ @@ -1702,7 +1734,7 @@ def inner_stat(array, axis=-1, mdtol=None, **kwargs): """ -SUM = WeightedAggregator('sum', _sum) +SUM = WeightedAggregator('sum', _sum, lazy_func=_lazy_sum) """ An :class:`~iris.analysis.Aggregator` instance that calculates the sum over a :class:`~iris.cube.Cube`, as computed by :func:`numpy.ma.sum`. diff --git a/lib/iris/tests/unit/analysis/test_COUNT.py b/lib/iris/tests/unit/analysis/test_COUNT.py index 9248a17910..63603b6059 100644 --- a/lib/iris/tests/unit/analysis/test_COUNT.py +++ b/lib/iris/tests/unit/analysis/test_COUNT.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2013 - 2017, Met Office +# (C) British Crown Copyright 2013 - 2018, Met Office # # This file is part of Iris. # @@ -23,11 +23,51 @@ # importing anything else. import iris.tests as tests +import numpy as np import numpy.ma as ma from iris.analysis import COUNT -import iris.cube +from iris.cube import Cube from iris.coords import DimCoord +from iris._lazy_data import as_lazy_data + + +class Test_basics(tests.IrisTest): + def setUp(self): + data = np.array([1, 2, 3, 4, 5]) + coord = DimCoord([6, 7, 8, 9, 10], long_name='foo') + self.cube = Cube(data) + self.cube.add_dim_coord(coord, 0) + self.lazy_cube = Cube(as_lazy_data(data)) + self.lazy_cube.add_dim_coord(coord, 0) + self.func = lambda x: x >= 3 + + def test_name(self): + self.assertEqual(COUNT.name(), 'count') + + def test_no_function(self): + with self.assertRaisesRegexp(KeyError, 'no selection function'): + self.lazy_cube.collapsed("foo", COUNT) + + def test_not_callable(self): + with self.assertRaisesRegexp(TypeError, 'function must be a callable'): + self.cube.collapsed("foo", COUNT, function='wibble') + + def test_lazy_not_callable(self): + with self.assertRaisesRegexp(TypeError, 'function must be a callable'): + self.lazy_cube.collapsed("foo", COUNT, function='wibble') + + def test_collapse(self): + cube = self.cube.collapsed("foo", COUNT, function=self.func) + self.assertArrayEqual(cube.data, [3]) + + def test_lazy(self): + cube = self.lazy_cube.collapsed("foo", COUNT, function=self.func) + self.assertTrue(cube.has_lazy_data()) + + def test_lazy_collapse(self): + cube = self.lazy_cube.collapsed("foo", COUNT, function=self.func) + self.assertArrayEqual(cube.data, [3]) class Test_units_func(tests.IrisTest): @@ -39,7 +79,7 @@ def test(self): class Test_masked(tests.IrisTest): def setUp(self): - self.cube = iris.cube.Cube(ma.masked_equal([1, 2, 3, 4, 5], 3)) + self.cube = Cube(ma.masked_equal([1, 2, 3, 4, 5], 3)) self.cube.add_dim_coord(DimCoord([6, 7, 8, 9, 10], long_name='foo'), 0) self.func = lambda x: x >= 3 @@ -48,9 +88,37 @@ def test_ma(self): self.assertArrayEqual(cube.data, [2]) -class Test_name(tests.IrisTest): - def test(self): - self.assertEqual(COUNT.name(), 'count') +class Test_lazy(tests.IrisTest): + def setUp(self): + data = np.array([1, 2, 3, 4, 5]) + self.cube = Cube(as_lazy_data(data)) + self.cube.add_dim_coord(DimCoord([6, 7, 8, 9, 10], long_name='foo'), 0) + self.func = lambda x: x >= 3 + + def test_lazy_oper(self): + cube = self.cube.collapsed("foo", COUNT, function=self.func) + self.assertTrue(cube.has_lazy_data()) + + def test_collapse(self): + result = self.cube.collapsed("foo", COUNT, function=self.func) + self.cube.data + expected = self.cube.collapsed("foo", COUNT, function=self.func) + self.assertArrayEqual(result.data, expected.data) + + +class Test_lazy_masked(tests.IrisTest): + def setUp(self): + lazy_data = as_lazy_data(ma.masked_equal([1, 2, 3, 4, 5], 3)) + self.lazy_cube = Cube(lazy_data) + self.lazy_cube.add_dim_coord(DimCoord([6, 7, 8, 9, 10], + long_name='foo'), + 0) + self.func = lambda x: x >= 3 + + def test_ma(self): + cube = self.lazy_cube.collapsed("foo", COUNT, function=self.func) + self.assertTrue(cube.has_lazy_data()) + self.assertArrayEqual(cube.data, [2]) class Test_aggregate_shape(tests.IrisTest): diff --git a/lib/iris/tests/unit/analysis/test_SUM.py b/lib/iris/tests/unit/analysis/test_SUM.py new file mode 100644 index 0000000000..1cf94723af --- /dev/null +++ b/lib/iris/tests/unit/analysis/test_SUM.py @@ -0,0 +1,149 @@ +# (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 . +"""Unit tests for the :data:`iris.analysis.SUM` aggregator.""" + +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.analysis import SUM +from iris.cube import Cube +from iris.coords import DimCoord +from iris._lazy_data import as_lazy_data + + +class Test_basics(tests.IrisTest): + def setUp(self): + data = np.array([1, 2, 3, 4, 5]) + coord = DimCoord([6, 7, 8, 9, 10], long_name='foo') + self.cube = Cube(data) + self.cube.add_dim_coord(coord, 0) + self.lazy_cube = Cube(as_lazy_data(data)) + self.lazy_cube.add_dim_coord(coord, 0) + + def test_name(self): + self.assertEqual(SUM.name(), 'sum') + + def test_collapse(self): + cube = self.cube.collapsed("foo", SUM) + self.assertArrayEqual(cube.data, [15]) + + def test_lazy(self): + cube = self.lazy_cube.collapsed("foo", SUM) + self.assertTrue(cube.has_lazy_data()) + + def test_lazy_collapse(self): + cube = self.lazy_cube.collapsed("foo", SUM) + self.assertArrayEqual(cube.data, [15]) + + +class Test_masked(tests.IrisTest): + def setUp(self): + self.cube = Cube(ma.masked_equal([1, 2, 3, 4, 5], 3)) + self.cube.add_dim_coord(DimCoord([6, 7, 8, 9, 10], long_name='foo'), 0) + + def test_ma(self): + cube = self.cube.collapsed("foo", SUM) + self.assertArrayEqual(cube.data, [12]) + + +class Test_lazy_masked(tests.IrisTest): + def setUp(self): + masked_data = ma.masked_equal([1, 2, 3, 4, 5], 3) + self.cube = Cube(as_lazy_data(masked_data)) + self.cube.add_dim_coord(DimCoord([6, 7, 8, 9, 10], long_name='foo'), 0) + + def test_lazy_ma(self): + cube = self.cube.collapsed("foo", SUM) + self.assertTrue(cube.has_lazy_data()) + self.assertArrayEqual(cube.data, [12]) + + +class Test_weights_and_returned(tests.IrisTest): + def setUp(self): + data_2d = np.arange(1, 11).reshape(2, 5) + coord_0 = DimCoord([11, 12], long_name='bar') + coord_1 = DimCoord([6, 7, 8, 9, 10], long_name='foo') + self.cube_2d = Cube(data_2d) + self.cube_2d.add_dim_coord(coord_0, 0) + self.cube_2d.add_dim_coord(coord_1, 1) + self.weights = np.array([2, 1, 1, 1, 1] * 2).reshape(2, 5) + + def test_weights(self): + cube = self.cube_2d.collapsed('bar', SUM, weights=self.weights) + self.assertArrayEqual(cube.data, [14, 9, 11, 13, 15]) + + def test_returned(self): + cube, weights = self.cube_2d.collapsed('bar', SUM, returned=True) + self.assertArrayEqual(cube.data, [7, 9, 11, 13, 15]) + self.assertArrayEqual(weights, [2, 2, 2, 2, 2]) + + def test_weights_and_returned(self): + cube, weights = self.cube_2d.collapsed('bar', SUM, + weights=self.weights, + returned=True) + self.assertArrayEqual(cube.data, [14, 9, 11, 13, 15]) + self.assertArrayEqual(weights, [4, 2, 2, 2, 2]) + + +class Test_lazy_weights_and_returned(tests.IrisTest): + def setUp(self): + data_2d = np.arange(1, 11).reshape(2, 5) + coord_0 = DimCoord([11, 12], long_name='bar') + coord_1 = DimCoord([6, 7, 8, 9, 10], long_name='foo') + self.cube_2d = Cube(as_lazy_data(data_2d)) + self.cube_2d.add_dim_coord(coord_0, 0) + self.cube_2d.add_dim_coord(coord_1, 1) + self.weights = np.array([2, 1, 1, 1, 1] * 2).reshape(2, 5) + + def test_weights(self): + cube = self.cube_2d.collapsed('bar', SUM, weights=self.weights) + self.assertTrue(cube.has_lazy_data()) + self.assertArrayEqual(cube.data, [14, 9, 11, 13, 15]) + + def test_returned(self): + cube, weights = self.cube_2d.collapsed('bar', SUM, returned=True) + self.assertTrue(cube.has_lazy_data()) + self.assertArrayEqual(cube.data, [7, 9, 11, 13, 15]) + self.assertArrayEqual(weights, [2, 2, 2, 2, 2]) + + def test_weights_and_returned(self): + cube, weights = self.cube_2d.collapsed('bar', SUM, + weights=self.weights, + returned=True) + self.assertTrue(cube.has_lazy_data()) + self.assertArrayEqual(cube.data, [14, 9, 11, 13, 15]) + self.assertArrayEqual(weights, [4, 2, 2, 2, 2]) + + +class Test_aggregate_shape(tests.IrisTest): + def test(self): + shape = () + kwargs = dict() + self.assertTupleEqual(SUM.aggregate_shape(**kwargs), shape) + kwargs = dict(wibble='wobble') + self.assertTupleEqual(SUM.aggregate_shape(**kwargs), shape) + + +if __name__ == "__main__": + tests.main() From 2663a5a9b7aeb932f307cffc8298c4ac744460d7 Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Wed, 12 Sep 2018 11:10:07 +0100 Subject: [PATCH 34/62] Add tests for MAX and MIN --- lib/iris/tests/unit/analysis/test_MAX.py | 92 ++++++++++++++++++++++++ lib/iris/tests/unit/analysis/test_MIN.py | 92 ++++++++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 lib/iris/tests/unit/analysis/test_MAX.py create mode 100644 lib/iris/tests/unit/analysis/test_MIN.py diff --git a/lib/iris/tests/unit/analysis/test_MAX.py b/lib/iris/tests/unit/analysis/test_MAX.py new file mode 100644 index 0000000000..38bc426932 --- /dev/null +++ b/lib/iris/tests/unit/analysis/test_MAX.py @@ -0,0 +1,92 @@ +# (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 . +"""Unit tests for the :data:`iris.analysis.MAX` aggregator.""" + +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.analysis import MAX +from iris.cube import Cube +from iris.coords import DimCoord +from iris._lazy_data import as_lazy_data + + +class Test_basics(tests.IrisTest): + def setUp(self): + data = np.array([1, 2, 3, 4, 5]) + coord = DimCoord([6, 7, 8, 9, 10], long_name='foo') + self.cube = Cube(data) + self.cube.add_dim_coord(coord, 0) + self.lazy_cube = Cube(as_lazy_data(data)) + self.lazy_cube.add_dim_coord(coord, 0) + + def test_name(self): + self.assertEqual(MAX.name(), 'maximum') + + def test_collapse(self): + cube = self.cube.collapsed("foo", MAX) + self.assertArrayEqual(cube.data, [5]) + + def test_lazy(self): + cube = self.lazy_cube.collapsed("foo", MAX) + self.assertTrue(cube.has_lazy_data()) + + def test_lazy_collapse(self): + cube = self.lazy_cube.collapsed("foo", MAX) + self.assertArrayEqual(cube.data, [5]) + + +class Test_masked(tests.IrisTest): + def setUp(self): + self.cube = Cube(ma.masked_greater([1, 2, 3, 4, 5], 3)) + self.cube.add_dim_coord(DimCoord([6, 7, 8, 9, 10], long_name='foo'), 0) + + def test_ma(self): + cube = self.cube.collapsed("foo", MAX) + self.assertArrayEqual(cube.data, [3]) + + +class Test_lazy_masked(tests.IrisTest): + def setUp(self): + masked_data = ma.masked_greater([1, 2, 3, 4, 5], 3) + self.cube = Cube(as_lazy_data(masked_data)) + self.cube.add_dim_coord(DimCoord([6, 7, 8, 9, 10], long_name='foo'), 0) + + def test_lazy_ma(self): + cube = self.cube.collapsed("foo", MAX) + self.assertTrue(cube.has_lazy_data()) + self.assertArrayEqual(cube.data, [3]) + + +class Test_aggregate_shape(tests.IrisTest): + def test(self): + shape = () + kwargs = dict() + self.assertTupleEqual(MAX.aggregate_shape(**kwargs), shape) + kwargs = dict(wibble='wobble') + self.assertTupleEqual(MAX.aggregate_shape(**kwargs), shape) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/analysis/test_MIN.py b/lib/iris/tests/unit/analysis/test_MIN.py new file mode 100644 index 0000000000..82a1d68316 --- /dev/null +++ b/lib/iris/tests/unit/analysis/test_MIN.py @@ -0,0 +1,92 @@ +# (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 . +"""Unit tests for the :data:`iris.analysis.MIN` aggregator.""" + +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.analysis import MIN +from iris.cube import Cube +from iris.coords import DimCoord +from iris._lazy_data import as_lazy_data + + +class Test_basics(tests.IrisTest): + def setUp(self): + data = np.array([1, 2, 3, 4, 5]) + coord = DimCoord([6, 7, 8, 9, 10], long_name='foo') + self.cube = Cube(data) + self.cube.add_dim_coord(coord, 0) + self.lazy_cube = Cube(as_lazy_data(data)) + self.lazy_cube.add_dim_coord(coord, 0) + + def test_name(self): + self.assertEqual(MIN.name(), 'minimum') + + def test_collapse(self): + cube = self.cube.collapsed("foo", MIN) + self.assertArrayEqual(cube.data, [1]) + + def test_lazy(self): + cube = self.lazy_cube.collapsed("foo", MIN) + self.assertTrue(cube.has_lazy_data()) + + def test_lazy_collapse(self): + cube = self.lazy_cube.collapsed("foo", MIN) + self.assertArrayEqual(cube.data, [1]) + + +class Test_masked(tests.IrisTest): + def setUp(self): + self.cube = Cube(ma.masked_less([1, 2, 3, 4, 5], 3)) + self.cube.add_dim_coord(DimCoord([6, 7, 8, 9, 10], long_name='foo'), 0) + + def test_ma(self): + cube = self.cube.collapsed("foo", MIN) + self.assertArrayEqual(cube.data, [3]) + + +class Test_lazy_masked(tests.IrisTest): + def setUp(self): + masked_data = ma.masked_less([1, 2, 3, 4, 5], 3) + self.cube = Cube(as_lazy_data(masked_data)) + self.cube.add_dim_coord(DimCoord([6, 7, 8, 9, 10], long_name='foo'), 0) + + def test_lazy_ma(self): + cube = self.cube.collapsed("foo", MIN) + self.assertTrue(cube.has_lazy_data()) + self.assertArrayEqual(cube.data, [3]) + + +class Test_aggregate_shape(tests.IrisTest): + def test(self): + shape = () + kwargs = dict() + self.assertTupleEqual(MIN.aggregate_shape(**kwargs), shape) + kwargs = dict(wibble='wobble') + self.assertTupleEqual(MIN.aggregate_shape(**kwargs), shape) + + +if __name__ == "__main__": + tests.main() From b4bf847e068f73e492a4c6bbcc3394728090a443 Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Wed, 12 Sep 2018 11:40:22 +0100 Subject: [PATCH 35/62] Update tests for STD_DEV --- lib/iris/tests/unit/analysis/test_STD_DEV.py | 34 ++++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/lib/iris/tests/unit/analysis/test_STD_DEV.py b/lib/iris/tests/unit/analysis/test_STD_DEV.py index e1aac6ae63..dedc7cf053 100644 --- a/lib/iris/tests/unit/analysis/test_STD_DEV.py +++ b/lib/iris/tests/unit/analysis/test_STD_DEV.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2017, Met Office +# (C) British Crown Copyright 2014 - 2018, Met Office # # This file is part of Iris. # @@ -27,6 +27,33 @@ from iris._lazy_data import as_concrete_data, as_lazy_data from iris.analysis import STD_DEV +from iris.cube import Cube +from iris.coords import DimCoord + + +class Test_basics(tests.IrisTest): + def setUp(self): + data = np.array([1, 2, 3, 4, 5]) + coord = DimCoord([6, 7, 8, 9, 10], long_name='foo') + self.cube = Cube(data) + self.cube.add_dim_coord(coord, 0) + self.lazy_cube = Cube(as_lazy_data(data)) + self.lazy_cube.add_dim_coord(coord, 0) + + def test_name(self): + self.assertEqual(STD_DEV.name(), 'standard_deviation') + + def test_collapse(self): + cube = self.cube.collapsed("foo", STD_DEV) + self.assertArrayAlmostEqual(cube.data, [1.58113883]) + + def test_lazy(self): + cube = self.lazy_cube.collapsed("foo", STD_DEV) + self.assertTrue(cube.has_lazy_data()) + + def test_lazy_collapse(self): + cube = self.lazy_cube.collapsed("foo", STD_DEV) + self.assertArrayAlmostEqual(cube.data, [1.58113883]) class Test_lazy_aggregate(tests.IrisTest): @@ -56,11 +83,6 @@ def test_ddof_zero(self): self.assertArrayAlmostEqual(result, np.array(2.291287)) -class Test_name(tests.IrisTest): - def test(self): - self.assertEqual(STD_DEV.name(), 'standard_deviation') - - class Test_aggregate_shape(tests.IrisTest): def test(self): shape = () From 718f60412913adfbf3eb35411689269bd2d5be3d Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Thu, 13 Sep 2018 05:42:49 +0100 Subject: [PATCH 36/62] Use NetCDF variable's chunks on load (#3131) Preserve NetCDF chunks on dataset load --- .../newfeature_2018-Aug-14_load-nc-chunks.txt | 1 + .../fileformats/_pyke_rules/fc_rules_cf.krb | 26 +----- lib/iris/fileformats/netcdf.py | 15 +++- .../netcdf/test__get_cf_var_data.py | 83 +++++++++++++++++++ .../fileformats/netcdf/test__load_cube.py | 14 +++- .../test_build_auxiliary_coordinate.py | 27 ++++-- 6 files changed, 131 insertions(+), 35 deletions(-) create mode 100644 docs/iris/src/whatsnew/contributions_2.2.0/newfeature_2018-Aug-14_load-nc-chunks.txt create mode 100644 lib/iris/tests/unit/fileformats/netcdf/test__get_cf_var_data.py diff --git a/docs/iris/src/whatsnew/contributions_2.2.0/newfeature_2018-Aug-14_load-nc-chunks.txt b/docs/iris/src/whatsnew/contributions_2.2.0/newfeature_2018-Aug-14_load-nc-chunks.txt new file mode 100644 index 0000000000..2a3780115d --- /dev/null +++ b/docs/iris/src/whatsnew/contributions_2.2.0/newfeature_2018-Aug-14_load-nc-chunks.txt @@ -0,0 +1 @@ +* NetCDF data variable chunk sizes are utilised at load time for significant performance improvements. \ No newline at end of file diff --git a/lib/iris/fileformats/_pyke_rules/fc_rules_cf.krb b/lib/iris/fileformats/_pyke_rules/fc_rules_cf.krb index 942450847b..f301f0fc9b 100644 --- a/lib/iris/fileformats/_pyke_rules/fc_rules_cf.krb +++ b/lib/iris/fileformats/_pyke_rules/fc_rules_cf.krb @@ -1070,7 +1070,7 @@ fc_extras import iris.coord_systems import iris.fileformats.cf as cf import iris.fileformats.netcdf - from iris.fileformats.netcdf import parse_cell_methods, UnknownCellMethodWarning + from iris.fileformats.netcdf import _get_cf_var_data, parse_cell_methods, UnknownCellMethodWarning import iris.fileformats.pp as pp import iris.exceptions import iris.std_names @@ -1712,25 +1712,16 @@ fc_extras # Get units attr_units = get_attr_units(cf_coord_var, attributes) - def cf_var_as_array(cf_var): - dtype = iris.fileformats.netcdf._get_actual_dtype(cf_var) - fill_value = getattr(cf_var.cf_data, '_FillValue', - netCDF4.default_fillvals[dtype.str[1:]]) - proxy = iris.fileformats.netcdf.NetCDFDataProxy( - cf_var.shape, dtype, engine.filename, - cf_var.cf_name, fill_value) - return as_lazy_data(proxy) - # Get any coordinate point data. if isinstance(cf_coord_var, cf.CFLabelVariable): points_data = cf_coord_var.cf_label_data(cf_var) else: - points_data = cf_var_as_array(cf_coord_var) + points_data = _get_cf_var_data(cf_coord_var, engine.filename) # Get any coordinate bounds. cf_bounds_var = get_cf_bounds_var(cf_coord_var) if cf_bounds_var is not None: - bounds_data = cf_var_as_array(cf_bounds_var) + bounds_data = _get_cf_var_data(cf_bounds_var, engine.filename) # Handle transposed bounds where the vertex dimension is not # the last one. Test based on shape to support different @@ -1783,16 +1774,7 @@ fc_extras # Get units attr_units = get_attr_units(cf_cm_attr, attributes) - def cf_var_as_array(cf_var): - dtype = cf_var.dtype - fill_value = getattr(cf_var.cf_data, '_FillValue', - netCDF4.default_fillvals[dtype.str[1:]]) - proxy = iris.fileformats.netcdf.NetCDFDataProxy( - cf_var.shape, dtype, engine.filename, - cf_var.cf_name, fill_value) - return as_lazy_data(proxy) - - data = cf_var_as_array(cf_cm_attr) + data = _get_cf_var_data(cf_cm_attr, engine.filename) # Determine the name of the dimension/s shared between the CF-netCDF data variable # and the coordinate being built. diff --git a/lib/iris/fileformats/netcdf.py b/lib/iris/fileformats/netcdf.py index 6ac9c26811..8c965ef1a5 100644 --- a/lib/iris/fileformats/netcdf.py +++ b/lib/iris/fileformats/netcdf.py @@ -501,8 +501,8 @@ def _get_actual_dtype(cf_var): return dummy_data.dtype -def _load_cube(engine, cf, cf_var, filename): - """Create the cube associated with the CF-netCDF data variable.""" +def _get_cf_var_data(cf_var, filename): + # Get lazy chunked data out of a cf variable. dtype = _get_actual_dtype(cf_var) # Create cube with deferred data, but no metadata @@ -510,7 +510,16 @@ def _load_cube(engine, cf, cf_var, filename): netCDF4.default_fillvals[cf_var.dtype.str[1:]]) proxy = NetCDFDataProxy(cf_var.shape, dtype, filename, cf_var.cf_name, fill_value) - data = as_lazy_data(proxy) + chunks = cf_var.cf_data.chunking() + # Chunks can be an iterable, None, or `'contiguous'`. + if chunks == 'contiguous': + chunks = None + return as_lazy_data(proxy, chunks=chunks) + + +def _load_cube(engine, cf, cf_var, filename): + """Create the cube associated with the CF-netCDF data variable.""" + data = _get_cf_var_data(cf_var, filename) cube = iris.cube.Cube(data) # Reset the pyke inference engine. diff --git a/lib/iris/tests/unit/fileformats/netcdf/test__get_cf_var_data.py b/lib/iris/tests/unit/fileformats/netcdf/test__get_cf_var_data.py new file mode 100644 index 0000000000..6a1aeb9d61 --- /dev/null +++ b/lib/iris/tests/unit/fileformats/netcdf/test__get_cf_var_data.py @@ -0,0 +1,83 @@ +# (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 . +"""Unit tests for the `iris.fileformats.netcdf._get_cf_var_data` function.""" + +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 + +from dask.array import Array as dask_array +import numpy as np + +from iris._lazy_data import _limited_shape +import iris.fileformats.cf +from iris.fileformats.netcdf import _get_cf_var_data +from iris.tests import mock + + +class Test__get_cf_var_data(tests.IrisTest): + def setUp(self): + self.filename = 'DUMMY' + self.shape = (3, 240, 200) + self.expected_chunks = _limited_shape(self.shape) + + def _make(self, chunksizes): + cf_data = mock.Mock(_FillValue=None) + cf_data.chunking = mock.MagicMock(return_value=chunksizes) + cf_var = mock.MagicMock(spec=iris.fileformats.cf.CFVariable, + dtype=np.dtype('i4'), + cf_data=cf_data, + cf_name='DUMMY_VAR', + shape=self.shape) + return cf_var + + def test_cf_data_type(self): + chunks = [1, 12, 100] + cf_var = self._make(chunks) + lazy_data = _get_cf_var_data(cf_var, self.filename) + self.assertIsInstance(lazy_data, dask_array) + + def test_cf_data_chunks(self): + chunks = [1, 12, 100] + cf_var = self._make(chunks) + lazy_data = _get_cf_var_data(cf_var, self.filename) + lazy_data_chunks = [c[0] for c in lazy_data.chunks] + self.assertArrayEqual(chunks, lazy_data_chunks) + + def test_cf_data_no_chunks(self): + # No chunks means chunks are calculated from the array's shape by + # `iris._lazy_data._limited_shape()`. + chunks = None + cf_var = self._make(chunks) + lazy_data = _get_cf_var_data(cf_var, self.filename) + lazy_data_chunks = [c[0] for c in lazy_data.chunks] + self.assertArrayEqual(lazy_data_chunks, self.expected_chunks) + + def test_cf_data_contiguous(self): + # Chunks 'contiguous' is equivalent to no chunks. + chunks = 'contiguous' + cf_var = self._make(chunks) + lazy_data = _get_cf_var_data(cf_var, self.filename) + lazy_data_chunks = [c[0] for c in lazy_data.chunks] + self.assertArrayEqual(lazy_data_chunks, self.expected_chunks) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/fileformats/netcdf/test__load_cube.py b/lib/iris/tests/unit/fileformats/netcdf/test__load_cube.py index 1e38baac86..41b6ca5ff0 100644 --- a/lib/iris/tests/unit/fileformats/netcdf/test__load_cube.py +++ b/lib/iris/tests/unit/fileformats/netcdf/test__load_cube.py @@ -57,6 +57,7 @@ def setUp(self): def _make(self, names, attrs): coords = [DimCoord(i, long_name=name) for i, name in enumerate(names)] + shape = (1,) cf_group = {} for name, cf_attrs in zip(names, attrs): @@ -64,12 +65,14 @@ def _make(self, names, attrs): cf_group[name] = mock.Mock(cf_attrs_unused=cf_attrs_unused) cf = mock.Mock(cf_group=cf_group) + cf_data = mock.Mock(_FillValue=None) + cf_data.chunking = mock.MagicMock(return_value=shape) cf_var = mock.MagicMock(spec=iris.fileformats.cf.CFVariable, dtype=np.dtype('i4'), - cf_data=mock.Mock(_FillValue=None), + cf_data=cf_data, cf_name='DUMMY_VAR', cf_group=coords, - shape=(1,)) + shape=shape) return cf, cf_var def test_flag_pass_thru(self): @@ -129,14 +132,17 @@ def setUp(self): self.valid_max = mock.sentinel.valid_max def _make(self, attrs): + shape = (1,) cf_attrs_unused = mock.Mock(return_value=attrs) + cf_data = mock.Mock(_FillValue=None) + cf_data.chunking = mock.MagicMock(return_value=shape) cf_var = mock.MagicMock(spec=iris.fileformats.cf.CFVariable, dtype=np.dtype('i4'), - cf_data=mock.Mock(_FillValue=None), + cf_data=cf_data, cf_name='DUMMY_VAR', cf_group=mock.Mock(), cf_attrs_unused=cf_attrs_unused, - shape=(1,)) + shape=shape) return cf_var def test_flag_pass_thru(self): diff --git a/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_auxiliary_coordinate.py b/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_auxiliary_coordinate.py index a796621cc0..4bb653f23f 100644 --- a/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_auxiliary_coordinate.py +++ b/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_auxiliary_coordinate.py @@ -40,11 +40,13 @@ class TestBoundsVertexDim(tests.IrisTest): def setUp(self): # Create coordinate cf variables and pyke engine. points = np.arange(6).reshape(2, 3) + + cf_data = self._make_cf_data(points) self.cf_coord_var = mock.Mock( spec=CFVariable, dimensions=('foo', 'bar'), cf_name='wibble', - cf_data=mock.Mock(), + cf_data=cf_data, standard_name=None, long_name='wibble', units='m', @@ -54,7 +56,8 @@ def setUp(self): self.engine = mock.Mock( cube=mock.Mock(), - cf_var=mock.Mock(dimensions=('foo', 'bar')), + cf_var=mock.Mock(dimensions=('foo', 'bar'), + cf_data=cf_data), filename='DUMMY', provides=dict(coordinates=[])) @@ -72,14 +75,21 @@ def patched__getitem__(proxy_self, keys): 'iris.fileformats.netcdf.NetCDFDataProxy.__getitem__', new=patched__getitem__) + @staticmethod + def _make_cf_data(vals): + cf_data = mock.Mock(_FillValue=None) + cf_data.chunking = mock.MagicMock(return_value=vals.shape) + return cf_data + def test_slowest_varying_vertex_dim(self): # Create the bounds cf variable. bounds = np.arange(24).reshape(4, 2, 3) + cf_data = self._make_cf_data(bounds) self.cf_bounds_var = mock.Mock( spec=CFVariable, dimensions=('nv', 'foo', 'bar'), cf_name='wibble_bnds', - cf_data=mock.Mock(), + cf_data=cf_data, shape=bounds.shape, dtype=bounds.dtype, __getitem__=lambda self, key: bounds[key]) @@ -116,11 +126,12 @@ def test_slowest_varying_vertex_dim(self): def test_fastest_varying_vertex_dim(self): bounds = np.arange(24).reshape(2, 3, 4) + cf_data = self._make_cf_data(bounds) self.cf_bounds_var = mock.Mock( spec=CFVariable, dimensions=('foo', 'bar', 'nv'), cf_name='wibble_bnds', - cf_data=mock.Mock(), + cf_data=cf_data, shape=bounds.shape, dtype=bounds.dtype, __getitem__=lambda self, key: bounds[key]) @@ -155,11 +166,12 @@ def test_fastest_with_different_dim_names(self): # which are 'foo' and 'bar' (as permitted by the cf spec), # this should still work because the vertex dim is the fastest varying. bounds = np.arange(24).reshape(2, 3, 4) + cf_data = self._make_cf_data(bounds) self.cf_bounds_var = mock.Mock( spec=CFVariable, dimensions=('x', 'y', 'nv'), cf_name='wibble_bnds', - cf_data=mock.Mock(), + cf_data=cf_data, shape=bounds.shape, dtype=bounds.dtype, __getitem__=lambda self, key: bounds[key]) @@ -194,11 +206,14 @@ class TestDtype(tests.IrisTest): def setUp(self): # Create coordinate cf variables and pyke engine. points = np.arange(6).reshape(2, 3) + cf_data = mock.Mock(_FillValue=None) + cf_data.chunking = mock.MagicMock(return_value=points.shape) + self.cf_coord_var = mock.Mock( spec=CFVariable, dimensions=('foo', 'bar'), cf_name='wibble', - cf_data=mock.Mock(), + cf_data=cf_data, standard_name=None, long_name='wibble', units='m', From b1b4b598041ec390c75d0ccf5eea364180fd8641 Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Thu, 13 Sep 2018 15:14:47 +0100 Subject: [PATCH 37/62] Iteration and less duplication --- lib/iris/_lazy_data.py | 15 ++++ lib/iris/analysis/__init__.py | 128 ++++++++++++++-------------------- 2 files changed, 69 insertions(+), 74 deletions(-) diff --git a/lib/iris/_lazy_data.py b/lib/iris/_lazy_data.py index 0613ba57ec..f5312b7d3a 100644 --- a/lib/iris/_lazy_data.py +++ b/lib/iris/_lazy_data.py @@ -23,6 +23,8 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa +from functools import wraps + import dask import dask.array as da import dask.context @@ -31,6 +33,19 @@ import numpy.ma as ma +def non_lazy(func): + """ + Turn a lazy function into a function that returns a result immediately. + + """ + @wraps(func) + def inner(*args, **kwargs): + """Immediately return the results of a lazy function.""" + result = func(*args, **kwargs) + return dask.compute(result)[0] + return inner + + def is_lazy_data(data): """ Return whether the argument is an Iris 'lazy' data array. diff --git a/lib/iris/analysis/__init__.py b/lib/iris/analysis/__init__.py index 776f40054f..49d9bb8172 100644 --- a/lib/iris/analysis/__init__.py +++ b/lib/iris/analysis/__init__.py @@ -65,7 +65,7 @@ from iris.analysis._regrid import RectilinearRegridder import iris.coords from iris.exceptions import LazyAggregatorError -import iris._lazy_data as iris_lazy_data +import iris._lazy_data __all__ = ('COUNT', 'GMEAN', 'HMEAN', 'MAX', 'MEAN', 'MEDIAN', 'MIN', 'PEAK', 'PERCENTILE', 'PROPORTION', 'RMS', 'STD_DEV', 'SUM', @@ -468,7 +468,7 @@ def lazy_aggregate(self, data, axis, **kwargs): # provided to __init__. kwargs = dict(list(self._kwargs.items()) + list(kwargs.items())) - return self.lazy_func(data, axis, **kwargs) + return self.lazy_func(data, axis=axis, **kwargs) def aggregate(self, data, axis, **kwargs): """ @@ -1015,6 +1015,40 @@ def post_process(self, collapsed_cube, data_result, coords, **kwargs): return result +def _build_dask_mdtol_function(dask_stats_function): + """ + Make a wrapped dask statistic function that supports the 'mdtol' keyword. + + 'dask_function' must be a dask statistical function, compatible with the + call signature : "dask_stats_function(data, axis, **kwargs)". + It must be masked-data tolerant, i.e. it ignores masked input points and + performs a calculation on only the unmasked points. + For example, mean([1, --, 2]) = (1 + 2) / 2 = 1.5. + + The returned value is a new function operating on dask arrays. + It has the call signature `stat(data, axis=-1, mdtol=None, **kwargs)`. + + """ + @wraps(dask_stats_function) + def inner_stat(array, axis=-1, mdtol=None, **kwargs): + # Call the statistic to get the basic result (missing-data tolerant). + dask_result = dask_stats_function(array, axis=axis, **kwargs) + if mdtol is None or mdtol >= 1.0: + result = dask_result + else: + # Build a lazy computation to compare the fraction of missing + # input points at each output point to the 'mdtol' threshold. + point_mask_counts = da.sum(da.ma.getmaskarray(array), axis=axis) + points_per_calc = array.size / dask_result.size + masked_point_fractions = point_mask_counts / points_per_calc + boolean_mask = masked_point_fractions > mdtol + # Return an mdtol-masked version of the basic result. + result = da.ma.masked_array(da.ma.getdata(dask_result), + boolean_mask) + return result + return inner_stat + + def _percentile(data, axis, percent, fast_percentile_method=False, **kwargs): """ @@ -1191,14 +1225,9 @@ def _weighted_percentile(data, axis, weights, percent, returned=False, return result -def _count(array, function, axis, **kwargs): - if not callable(function): - emsg = 'function must be a callable. Got {}.' - raise TypeError(emsg.format(type(function))) - return ma.sum(function(array), axis=axis, **kwargs) - - -def _lazy_count(array, axis, **kwargs): +@_build_dask_mdtol_function +def _lazy_count(array, **kwargs): + array = iris._lazy_data.as_lazy_data(array) try: func = kwargs.pop('function') except KeyError: @@ -1206,16 +1235,17 @@ def _lazy_count(array, axis, **kwargs): if not callable(func): emsg = 'function must be a callable. Got {}.' raise TypeError(emsg.format(type(func))) - return da.sum(func(array), axis=axis, **kwargs) + return da.sum(func(array), **kwargs) def _proportion(array, function, axis, **kwargs): + count = iris._lazy_data.non_lazy(_lazy_count) # if the incoming array is masked use that to count the total number of # values if ma.isMaskedArray(array): # calculate the total number of non-masked values across the given axis - total_non_masked = _count(array.mask, np.logical_not, - axis=axis, **kwargs) + total_non_masked = count( + array.mask, axis=axis, function=np.logical_not, **kwargs) total_non_masked = ma.masked_equal(total_non_masked, 0) else: total_non_masked = array.shape[axis] @@ -1226,7 +1256,7 @@ def _proportion(array, function, axis, **kwargs): # a dtype for its data that is different to the dtype of the fill-value, # which can cause issues outside this function. # Reference - tests/unit/analyis/test_PROPORTION.py Test_masked.test_ma - numerator = _count(array, function, axis=axis, **kwargs) + numerator = count(array, axis=axis, function=function, **kwargs) result = ma.asarray(numerator / total_non_masked) return result @@ -1239,40 +1269,23 @@ def _rms(array, axis, **kwargs): return rval -def _sum(array, **kwargs): +@_build_dask_mdtol_function +def _lazy_sum(array, **kwargs): + array = iris._lazy_data.as_lazy_data(array) # weighted or scaled sum axis_in = kwargs.get('axis', None) weights_in = kwargs.pop('weights', None) returned_in = kwargs.pop('returned', False) if weights_in is not None: - wsum = ma.sum(weights_in * array, **kwargs) + wsum = da.sum(weights_in * array, **kwargs) else: - wsum = ma.sum(array, **kwargs) + wsum = da.sum(array, **kwargs) if returned_in: if weights_in is None: - weights = np.ones_like(array) + weights = iris._lazy_data.as_lazy_data(np.ones_like(array)) else: weights = weights_in - rvalue = (wsum, ma.sum(weights, axis=axis_in)) - else: - rvalue = wsum - return rvalue - - -def _lazy_sum(array, axis, **kwargs): - # weighted or scaled sum - weights_in = kwargs.pop('weights', None) - returned_in = kwargs.pop('returned', False) - if weights_in is not None: - wsum = da.sum(weights_in * array, axis=axis, **kwargs) - else: - wsum = da.sum(array, axis=axis, **kwargs) - if returned_in: - if weights_in is None: - weights = iris_lazy_data.as_lazy_data(np.ones_like(array)) - else: - weights = weights_in - rvalue = (wsum, da.sum(weights, axis=axis)) + rvalue = (wsum, da.sum(weights, axis=axis_in)) else: rvalue = wsum return rvalue @@ -1382,7 +1395,7 @@ def interp_order(length): # # Common partial Aggregation class constructors. # -COUNT = Aggregator('count', _count, +COUNT = Aggregator('count', iris._lazy_data.non_lazy(_lazy_count), units_func=lambda units: 1, lazy_func=_lazy_count) """ @@ -1450,40 +1463,6 @@ def interp_order(length): """ -def _build_dask_mdtol_function(dask_stats_function): - """ - Make a wrapped dask statistic function that supports the 'mdtol' keyword. - - 'dask_function' must be a dask statistical function, compatible with the - call signature : "dask_stats_function(data, axis, **kwargs)". - It must be masked-data tolerant, i.e. it ignores masked input points and - performs a calculation on only the unmasked points. - For example, mean([1, --, 2]) = (1 + 2) / 2 = 1.5. - - The returned value is a new function operating on dask arrays. - It has the call signature `stat(data, axis=-1, mdtol=None, **kwargs)`. - - """ - @wraps(dask_stats_function) - def inner_stat(array, axis=-1, mdtol=None, **kwargs): - # Call the statistic to get the basic result (missing-data tolerant). - dask_result = dask_stats_function(array, axis=axis, **kwargs) - if mdtol is None or mdtol >= 1.0: - result = dask_result - else: - # Build a lazy computation to compare the fraction of missing - # input points at each output point to the 'mdtol' threshold. - point_mask_counts = da.sum(da.ma.getmaskarray(array), axis=axis) - points_per_calc = array.size / dask_result.size - masked_point_fractions = point_mask_counts / points_per_calc - boolean_mask = masked_point_fractions > mdtol - # Return an mdtol-masked version of the basic result. - result = da.ma.masked_array(da.ma.getdata(dask_result), - boolean_mask) - return result - return inner_stat - - MEAN = WeightedAggregator('mean', ma.average, lazy_func=_build_dask_mdtol_function(da.mean)) """ @@ -1734,7 +1713,8 @@ def inner_stat(array, axis=-1, mdtol=None, **kwargs): """ -SUM = WeightedAggregator('sum', _sum, lazy_func=_lazy_sum) +SUM = WeightedAggregator('sum', iris._lazy_data.non_lazy(_lazy_sum), + lazy_func=_build_dask_mdtol_function(_lazy_sum)) """ An :class:`~iris.analysis.Aggregator` instance that calculates the sum over a :class:`~iris.cube.Cube`, as computed by :func:`numpy.ma.sum`. From a957ebcb289040fe1755859e2bfbebf444b24468 Mon Sep 17 00:00:00 2001 From: Thomas Green Date: Sun, 23 Sep 2018 06:49:21 +0100 Subject: [PATCH 38/62] Attempt to fix Matplotlib issue by resticting to 2.x --- requirements/core.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/core.txt b/requirements/core.txt index 997c91c665..4cad29fd1f 100644 --- a/requirements/core.txt +++ b/requirements/core.txt @@ -7,7 +7,7 @@ cartopy cf_units>=2 cftime dask[array]==0.18.1 #conda: dask==0.18.1 -matplotlib>=2 +matplotlib>=2,<3 netcdf4 numpy>=1.14 scipy From 00ee1598ecd07f5f4519e607ffa4d6979851ab74 Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Mon, 24 Sep 2018 11:45:57 +0100 Subject: [PATCH 39/62] Non-test review actions --- lib/iris/analysis/__init__.py | 7 ++----- lib/iris/tests/unit/analysis/test_COUNT.py | 3 ++- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/iris/analysis/__init__.py b/lib/iris/analysis/__init__.py index 49d9bb8172..ec4d341d25 100644 --- a/lib/iris/analysis/__init__.py +++ b/lib/iris/analysis/__init__.py @@ -1020,7 +1020,7 @@ def _build_dask_mdtol_function(dask_stats_function): Make a wrapped dask statistic function that supports the 'mdtol' keyword. 'dask_function' must be a dask statistical function, compatible with the - call signature : "dask_stats_function(data, axis, **kwargs)". + call signature : "dask_stats_function(data, axis=axis, **kwargs)". It must be masked-data tolerant, i.e. it ignores masked input points and performs a calculation on only the unmasked points. For example, mean([1, --, 2]) = (1 + 2) / 2 = 1.5. @@ -1228,10 +1228,7 @@ def _weighted_percentile(data, axis, weights, percent, returned=False, @_build_dask_mdtol_function def _lazy_count(array, **kwargs): array = iris._lazy_data.as_lazy_data(array) - try: - func = kwargs.pop('function') - except KeyError: - raise KeyError('no selection function supplied.') + func = kwargs.pop('function', None) if not callable(func): emsg = 'function must be a callable. Got {}.' raise TypeError(emsg.format(type(func))) diff --git a/lib/iris/tests/unit/analysis/test_COUNT.py b/lib/iris/tests/unit/analysis/test_COUNT.py index 63603b6059..8f7118e7c4 100644 --- a/lib/iris/tests/unit/analysis/test_COUNT.py +++ b/lib/iris/tests/unit/analysis/test_COUNT.py @@ -46,7 +46,8 @@ def test_name(self): self.assertEqual(COUNT.name(), 'count') def test_no_function(self): - with self.assertRaisesRegexp(KeyError, 'no selection function'): + exp_emsg = "function must be a callable. Got " + with self.assertRaisesRegexp(TypeError, exp_emsg): self.lazy_cube.collapsed("foo", COUNT) def test_not_callable(self): From 76b819b91e1c1d55db4a1206eac94aa6fc21b26f Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Mon, 24 Sep 2018 11:54:39 +0100 Subject: [PATCH 40/62] Make COUNT tests truer unit tests --- lib/iris/tests/unit/analysis/test_STD_DEV.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/iris/tests/unit/analysis/test_STD_DEV.py b/lib/iris/tests/unit/analysis/test_STD_DEV.py index dedc7cf053..57cc00132f 100644 --- a/lib/iris/tests/unit/analysis/test_STD_DEV.py +++ b/lib/iris/tests/unit/analysis/test_STD_DEV.py @@ -25,7 +25,7 @@ import numpy as np -from iris._lazy_data import as_concrete_data, as_lazy_data +from iris._lazy_data import as_concrete_data, as_lazy_data, is_lazy_data from iris.analysis import STD_DEV from iris.cube import Cube from iris.coords import DimCoord @@ -44,16 +44,16 @@ def test_name(self): self.assertEqual(STD_DEV.name(), 'standard_deviation') def test_collapse(self): - cube = self.cube.collapsed("foo", STD_DEV) - self.assertArrayAlmostEqual(cube.data, [1.58113883]) + data = STD_DEV.aggregate(self.cube.data, axis=0) + self.assertArrayAlmostEqual(data, [1.58113883]) def test_lazy(self): - cube = self.lazy_cube.collapsed("foo", STD_DEV) - self.assertTrue(cube.has_lazy_data()) + lazy_data = STD_DEV.lazy_aggregate(self.lazy_cube.lazy_data(), axis=0) + self.assertTrue(is_lazy_data(lazy_data)) def test_lazy_collapse(self): - cube = self.lazy_cube.collapsed("foo", STD_DEV) - self.assertArrayAlmostEqual(cube.data, [1.58113883]) + lazy_data = STD_DEV.lazy_aggregate(self.lazy_cube.lazy_data(), axis=0) + self.assertArrayAlmostEqual(lazy_data.compute(), [1.58113883]) class Test_lazy_aggregate(tests.IrisTest): From 9908eba0f85ae968589a793dfb950b3ffbd9f94d Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Mon, 24 Sep 2018 12:17:00 +0100 Subject: [PATCH 41/62] Convert MAX and MIN tests to truer unit tests --- lib/iris/tests/unit/analysis/test_MAX.py | 24 ++++++++++++------------ lib/iris/tests/unit/analysis/test_MIN.py | 24 ++++++++++++------------ 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/lib/iris/tests/unit/analysis/test_MAX.py b/lib/iris/tests/unit/analysis/test_MAX.py index 38bc426932..f3a523bc93 100644 --- a/lib/iris/tests/unit/analysis/test_MAX.py +++ b/lib/iris/tests/unit/analysis/test_MAX.py @@ -29,7 +29,7 @@ from iris.analysis import MAX from iris.cube import Cube from iris.coords import DimCoord -from iris._lazy_data import as_lazy_data +from iris._lazy_data import as_lazy_data, is_lazy_data class Test_basics(tests.IrisTest): @@ -45,16 +45,16 @@ def test_name(self): self.assertEqual(MAX.name(), 'maximum') def test_collapse(self): - cube = self.cube.collapsed("foo", MAX) - self.assertArrayEqual(cube.data, [5]) + data = MAX.aggregate(self.cube.data, axis=0) + self.assertArrayEqual(data, [5]) def test_lazy(self): - cube = self.lazy_cube.collapsed("foo", MAX) - self.assertTrue(cube.has_lazy_data()) + lazy_data = MAX.lazy_aggregate(self.lazy_cube.lazy_data(), axis=0) + self.assertTrue(is_lazy_data(lazy_data)) def test_lazy_collapse(self): - cube = self.lazy_cube.collapsed("foo", MAX) - self.assertArrayEqual(cube.data, [5]) + lazy_data = MAX.lazy_aggregate(self.lazy_cube.lazy_data(), axis=0) + self.assertArrayEqual(lazy_data.compute(), [5]) class Test_masked(tests.IrisTest): @@ -63,8 +63,8 @@ def setUp(self): self.cube.add_dim_coord(DimCoord([6, 7, 8, 9, 10], long_name='foo'), 0) def test_ma(self): - cube = self.cube.collapsed("foo", MAX) - self.assertArrayEqual(cube.data, [3]) + data = MAX.aggregate(self.cube.data, axis=0) + self.assertArrayEqual(data, [3]) class Test_lazy_masked(tests.IrisTest): @@ -74,9 +74,9 @@ def setUp(self): self.cube.add_dim_coord(DimCoord([6, 7, 8, 9, 10], long_name='foo'), 0) def test_lazy_ma(self): - cube = self.cube.collapsed("foo", MAX) - self.assertTrue(cube.has_lazy_data()) - self.assertArrayEqual(cube.data, [3]) + lazy_data = MAX.lazy_aggregate(self.cube.lazy_data(), axis=0) + self.assertTrue(is_lazy_data(lazy_data)) + self.assertArrayEqual(lazy_data.compute(), [3]) class Test_aggregate_shape(tests.IrisTest): diff --git a/lib/iris/tests/unit/analysis/test_MIN.py b/lib/iris/tests/unit/analysis/test_MIN.py index 82a1d68316..13860f5306 100644 --- a/lib/iris/tests/unit/analysis/test_MIN.py +++ b/lib/iris/tests/unit/analysis/test_MIN.py @@ -29,7 +29,7 @@ from iris.analysis import MIN from iris.cube import Cube from iris.coords import DimCoord -from iris._lazy_data import as_lazy_data +from iris._lazy_data import as_lazy_data, is_lazy_data class Test_basics(tests.IrisTest): @@ -45,16 +45,16 @@ def test_name(self): self.assertEqual(MIN.name(), 'minimum') def test_collapse(self): - cube = self.cube.collapsed("foo", MIN) - self.assertArrayEqual(cube.data, [1]) + data = MIN.aggregate(self.cube.data, axis=0) + self.assertArrayEqual(data, [1]) def test_lazy(self): - cube = self.lazy_cube.collapsed("foo", MIN) - self.assertTrue(cube.has_lazy_data()) + lazy_data = MIN.lazy_aggregate(self.lazy_cube.lazy_data(), axis=0) + self.assertTrue(is_lazy_data(lazy_data)) def test_lazy_collapse(self): - cube = self.lazy_cube.collapsed("foo", MIN) - self.assertArrayEqual(cube.data, [1]) + lazy_data = MIN.lazy_aggregate(self.lazy_cube.lazy_data(), axis=0) + self.assertArrayEqual(lazy_data.compute(), [1]) class Test_masked(tests.IrisTest): @@ -63,8 +63,8 @@ def setUp(self): self.cube.add_dim_coord(DimCoord([6, 7, 8, 9, 10], long_name='foo'), 0) def test_ma(self): - cube = self.cube.collapsed("foo", MIN) - self.assertArrayEqual(cube.data, [3]) + data = MIN.aggregate(self.cube.data, axis=0) + self.assertArrayEqual(data, [3]) class Test_lazy_masked(tests.IrisTest): @@ -74,9 +74,9 @@ def setUp(self): self.cube.add_dim_coord(DimCoord([6, 7, 8, 9, 10], long_name='foo'), 0) def test_lazy_ma(self): - cube = self.cube.collapsed("foo", MIN) - self.assertTrue(cube.has_lazy_data()) - self.assertArrayEqual(cube.data, [3]) + lazy_data = MIN.lazy_aggregate(self.cube.lazy_data(), axis=0) + self.assertTrue(is_lazy_data(lazy_data)) + self.assertArrayEqual(lazy_data.compute(), [3]) class Test_aggregate_shape(tests.IrisTest): From e4e07adfc10310acf95607f8c50794353a6b4536 Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Mon, 24 Sep 2018 12:50:19 +0100 Subject: [PATCH 42/62] Convert COUNT and SUM agg tests to truer unit tests --- lib/iris/tests/unit/analysis/test_COUNT.py | 56 ++++++++---------- lib/iris/tests/unit/analysis/test_SUM.py | 66 ++++++++++++---------- 2 files changed, 58 insertions(+), 64 deletions(-) diff --git a/lib/iris/tests/unit/analysis/test_COUNT.py b/lib/iris/tests/unit/analysis/test_COUNT.py index 8f7118e7c4..882ad2921e 100644 --- a/lib/iris/tests/unit/analysis/test_COUNT.py +++ b/lib/iris/tests/unit/analysis/test_COUNT.py @@ -29,7 +29,7 @@ from iris.analysis import COUNT from iris.cube import Cube from iris.coords import DimCoord -from iris._lazy_data import as_lazy_data +from iris._lazy_data import as_lazy_data, is_lazy_data class Test_basics(tests.IrisTest): @@ -48,27 +48,33 @@ def test_name(self): def test_no_function(self): exp_emsg = "function must be a callable. Got " with self.assertRaisesRegexp(TypeError, exp_emsg): - self.lazy_cube.collapsed("foo", COUNT) + COUNT.lazy_aggregate(self.lazy_cube.lazy_data(), axis=0) def test_not_callable(self): with self.assertRaisesRegexp(TypeError, 'function must be a callable'): - self.cube.collapsed("foo", COUNT, function='wibble') + COUNT.aggregate(self.cube.data, axis=0, function='wibble') def test_lazy_not_callable(self): with self.assertRaisesRegexp(TypeError, 'function must be a callable'): - self.lazy_cube.collapsed("foo", COUNT, function='wibble') + COUNT.lazy_aggregate(self.lazy_cube.lazy_data(), + axis=0, + function='wibble') def test_collapse(self): - cube = self.cube.collapsed("foo", COUNT, function=self.func) - self.assertArrayEqual(cube.data, [3]) + data = COUNT.aggregate(self.cube.data, axis=0, function=self.func) + self.assertArrayEqual(data, [3]) def test_lazy(self): - cube = self.lazy_cube.collapsed("foo", COUNT, function=self.func) - self.assertTrue(cube.has_lazy_data()) + lazy_data = COUNT.lazy_aggregate(self.lazy_cube.lazy_data(), + axis=0, + function=self.func) + self.assertTrue(is_lazy_data(lazy_data)) def test_lazy_collapse(self): - cube = self.lazy_cube.collapsed("foo", COUNT, function=self.func) - self.assertArrayEqual(cube.data, [3]) + lazy_data = COUNT.lazy_aggregate(self.lazy_cube.lazy_data(), + axis=0, + function=self.func) + self.assertArrayEqual(lazy_data.compute(), [3]) class Test_units_func(tests.IrisTest): @@ -85,26 +91,8 @@ def setUp(self): self.func = lambda x: x >= 3 def test_ma(self): - cube = self.cube.collapsed("foo", COUNT, function=self.func) - self.assertArrayEqual(cube.data, [2]) - - -class Test_lazy(tests.IrisTest): - def setUp(self): - data = np.array([1, 2, 3, 4, 5]) - self.cube = Cube(as_lazy_data(data)) - self.cube.add_dim_coord(DimCoord([6, 7, 8, 9, 10], long_name='foo'), 0) - self.func = lambda x: x >= 3 - - def test_lazy_oper(self): - cube = self.cube.collapsed("foo", COUNT, function=self.func) - self.assertTrue(cube.has_lazy_data()) - - def test_collapse(self): - result = self.cube.collapsed("foo", COUNT, function=self.func) - self.cube.data - expected = self.cube.collapsed("foo", COUNT, function=self.func) - self.assertArrayEqual(result.data, expected.data) + data = COUNT.aggregate(self.cube.data, axis=0, function=self.func) + self.assertArrayEqual(data, [2]) class Test_lazy_masked(tests.IrisTest): @@ -117,9 +105,11 @@ def setUp(self): self.func = lambda x: x >= 3 def test_ma(self): - cube = self.lazy_cube.collapsed("foo", COUNT, function=self.func) - self.assertTrue(cube.has_lazy_data()) - self.assertArrayEqual(cube.data, [2]) + lazy_data = COUNT.lazy_aggregate(self.lazy_cube.lazy_data(), + axis=0, + function=self.func) + self.assertTrue(is_lazy_data(lazy_data)) + self.assertArrayEqual(lazy_data.compute(), [2]) class Test_aggregate_shape(tests.IrisTest): diff --git a/lib/iris/tests/unit/analysis/test_SUM.py b/lib/iris/tests/unit/analysis/test_SUM.py index 1cf94723af..0a0f2332ab 100644 --- a/lib/iris/tests/unit/analysis/test_SUM.py +++ b/lib/iris/tests/unit/analysis/test_SUM.py @@ -29,7 +29,7 @@ from iris.analysis import SUM from iris.cube import Cube from iris.coords import DimCoord -from iris._lazy_data import as_lazy_data +from iris._lazy_data import as_lazy_data, is_lazy_data class Test_basics(tests.IrisTest): @@ -45,16 +45,16 @@ def test_name(self): self.assertEqual(SUM.name(), 'sum') def test_collapse(self): - cube = self.cube.collapsed("foo", SUM) - self.assertArrayEqual(cube.data, [15]) + data = SUM.aggregate(self.cube.data, axis=0) + self.assertArrayEqual(data, [15]) def test_lazy(self): - cube = self.lazy_cube.collapsed("foo", SUM) - self.assertTrue(cube.has_lazy_data()) + lazy_data = SUM.lazy_aggregate(self.lazy_cube.lazy_data(), axis=0) + self.assertTrue(is_lazy_data(lazy_data)) def test_lazy_collapse(self): - cube = self.lazy_cube.collapsed("foo", SUM) - self.assertArrayEqual(cube.data, [15]) + lazy_data = SUM.lazy_aggregate(self.lazy_cube.lazy_data(), axis=0) + self.assertArrayEqual(lazy_data.compute(), [15]) class Test_masked(tests.IrisTest): @@ -63,8 +63,8 @@ def setUp(self): self.cube.add_dim_coord(DimCoord([6, 7, 8, 9, 10], long_name='foo'), 0) def test_ma(self): - cube = self.cube.collapsed("foo", SUM) - self.assertArrayEqual(cube.data, [12]) + data = SUM.aggregate(self.cube.data, axis=0) + self.assertArrayEqual(data, [12]) class Test_lazy_masked(tests.IrisTest): @@ -74,9 +74,9 @@ def setUp(self): self.cube.add_dim_coord(DimCoord([6, 7, 8, 9, 10], long_name='foo'), 0) def test_lazy_ma(self): - cube = self.cube.collapsed("foo", SUM) - self.assertTrue(cube.has_lazy_data()) - self.assertArrayEqual(cube.data, [12]) + lazy_data = SUM.lazy_aggregate(self.cube.lazy_data(), axis=0) + self.assertTrue(is_lazy_data(lazy_data)) + self.assertArrayEqual(lazy_data.compute(), [12]) class Test_weights_and_returned(tests.IrisTest): @@ -90,19 +90,19 @@ def setUp(self): self.weights = np.array([2, 1, 1, 1, 1] * 2).reshape(2, 5) def test_weights(self): - cube = self.cube_2d.collapsed('bar', SUM, weights=self.weights) - self.assertArrayEqual(cube.data, [14, 9, 11, 13, 15]) + data = SUM.aggregate(self.cube_2d.data, axis=0, weights=self.weights) + self.assertArrayEqual(data, [14, 9, 11, 13, 15]) def test_returned(self): - cube, weights = self.cube_2d.collapsed('bar', SUM, returned=True) - self.assertArrayEqual(cube.data, [7, 9, 11, 13, 15]) + data, weights = SUM.aggregate(self.cube_2d.data, axis=0, returned=True) + self.assertArrayEqual(data, [7, 9, 11, 13, 15]) self.assertArrayEqual(weights, [2, 2, 2, 2, 2]) def test_weights_and_returned(self): - cube, weights = self.cube_2d.collapsed('bar', SUM, - weights=self.weights, - returned=True) - self.assertArrayEqual(cube.data, [14, 9, 11, 13, 15]) + data, weights = SUM.aggregate(self.cube_2d.data, axis=0, + weights=self.weights, + returned=True) + self.assertArrayEqual(data, [14, 9, 11, 13, 15]) self.assertArrayEqual(weights, [4, 2, 2, 2, 2]) @@ -117,22 +117,26 @@ def setUp(self): self.weights = np.array([2, 1, 1, 1, 1] * 2).reshape(2, 5) def test_weights(self): - cube = self.cube_2d.collapsed('bar', SUM, weights=self.weights) - self.assertTrue(cube.has_lazy_data()) - self.assertArrayEqual(cube.data, [14, 9, 11, 13, 15]) + lazy_data = SUM.lazy_aggregate(self.cube_2d.lazy_data(), axis=0, + weights=self.weights) + self.assertTrue(is_lazy_data(lazy_data)) + self.assertArrayEqual(lazy_data.compute(), [14, 9, 11, 13, 15]) def test_returned(self): - cube, weights = self.cube_2d.collapsed('bar', SUM, returned=True) - self.assertTrue(cube.has_lazy_data()) - self.assertArrayEqual(cube.data, [7, 9, 11, 13, 15]) + lazy_data, weights = SUM.lazy_aggregate(self.cube_2d.lazy_data(), + axis=0, + returned=True) + self.assertTrue(is_lazy_data(lazy_data)) + self.assertArrayEqual(lazy_data.compute(), [7, 9, 11, 13, 15]) self.assertArrayEqual(weights, [2, 2, 2, 2, 2]) def test_weights_and_returned(self): - cube, weights = self.cube_2d.collapsed('bar', SUM, - weights=self.weights, - returned=True) - self.assertTrue(cube.has_lazy_data()) - self.assertArrayEqual(cube.data, [14, 9, 11, 13, 15]) + lazy_data, weights = SUM.lazy_aggregate(self.cube_2d.lazy_data(), + axis=0, + weights=self.weights, + returned=True) + self.assertTrue(is_lazy_data(lazy_data)) + self.assertArrayEqual(lazy_data.compute(), [14, 9, 11, 13, 15]) self.assertArrayEqual(weights, [4, 2, 2, 2, 2]) From 50fad00b92f05604f77124b696d8f63b5ee17844 Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Mon, 24 Sep 2018 16:48:41 +0100 Subject: [PATCH 43/62] Tests for new _lazy_data function --- .../tests/unit/lazy_data/test_non_lazy.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 lib/iris/tests/unit/lazy_data/test_non_lazy.py diff --git a/lib/iris/tests/unit/lazy_data/test_non_lazy.py b/lib/iris/tests/unit/lazy_data/test_non_lazy.py new file mode 100644 index 0000000000..7a736a0584 --- /dev/null +++ b/lib/iris/tests/unit/lazy_data/test_non_lazy.py @@ -0,0 +1,51 @@ +# (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._lazy data.non_lazy`.""" + +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._lazy_data import as_lazy_data, is_lazy_data, non_lazy + + +class Test_non_lazy(tests.IrisTest): + def setUp(self): + self.array = np.arange(8).reshape(2, 4) + self.lazy_array = as_lazy_data(self.array) + self.func = non_lazy(lambda array: array.sum(axis=0)) + self.func_result = [4, 6, 8, 10] + + def test_lazy_input(self): + result = self.func(self.lazy_array) + self.assertFalse(is_lazy_data(result)) + self.assertArrayEqual(result, self.func_result) + + def test_non_lazy_input(self): + # Check that a non-lazy input doesn't trip up the functionality. + result = self.func(self.array) + self.assertFalse(is_lazy_data(result)) + self.assertArrayEqual(result, self.func_result) + + +if __name__ == '__main__': + tests.main() From 63f0b5e482bab82033e44c6bb03259a17a1a1666 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Fri, 28 Sep 2018 11:17:21 +0100 Subject: [PATCH 44/62] Fix for netCDF4 1.4 behaviour change. (#3177) --- lib/iris/fileformats/cf.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/iris/fileformats/cf.py b/lib/iris/fileformats/cf.py index f2fc895d1c..ddf7f2d64d 100644 --- a/lib/iris/fileformats/cf.py +++ b/lib/iris/fileformats/cf.py @@ -936,6 +936,11 @@ def __init__(self, filename, warn=False, monotonic=False): self._dataset = netCDF4.Dataset(self._filename, mode='r') + # Return variable data as np.ma.MaskedArray *only* when masked points + # are found in the slice requested, otherwise as 'normal' np.ndarray. + # Note: this suits us better, and was standard up to netcdf 1.3. + self._dataset.set_always_mask(False) + # Issue load optimisation warning. if warn and self._dataset.file_format in ['NETCDF3_CLASSIC', 'NETCDF3_64BIT']: warnings.warn('Optimise CF-netCDF loading by converting data from NetCDF3 ' \ From afee89267377147c5dfd908ca4686e3a8cb1ae1a Mon Sep 17 00:00:00 2001 From: Zeb Nicholls Date: Fri, 28 Sep 2018 18:09:00 +0200 Subject: [PATCH 45/62] Improve gracefully filling warning (#3171) * Realise that running the tests is simpler than I thought * Make Gracefully warning only appear when filling is required * Add tests of warnings * Get rid of install changes, they are in #3170 * Update after review * Add whatsnew entry --- docs/iris/src/conf.py | 2 +- ...fully-filling-warning-only-when-masked.txt | 1 + lib/iris/analysis/_regrid.py | 2 +- .../fileformats/_pyke_rules/fc_rules_cf.krb | 82 +++++++++---------- lib/iris/tests/integration/test_regridding.py | 2 +- lib/iris/tests/test_coordsystem.py | 2 +- .../fileformats/netcdf/test__load_cube.py | 2 +- .../tests/unit/fileformats/pp/test_save.py | 2 +- .../test_build_dimension_coordinate.py | 80 +++++++++++++++++- 9 files changed, 127 insertions(+), 48 deletions(-) create mode 100644 docs/iris/src/whatsnew/contributions_2.2.0/bugfix_2018-Sep-19_gracefully-filling-warning-only-when-masked.txt diff --git a/docs/iris/src/conf.py b/docs/iris/src/conf.py index 2833a01674..6cdfe634c4 100644 --- a/docs/iris/src/conf.py +++ b/docs/iris/src/conf.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2010 - 2017, Met Office +# (C) British Crown Copyright 2010 - 2018, Met Office # # This file is part of Iris. # diff --git a/docs/iris/src/whatsnew/contributions_2.2.0/bugfix_2018-Sep-19_gracefully-filling-warning-only-when-masked.txt b/docs/iris/src/whatsnew/contributions_2.2.0/bugfix_2018-Sep-19_gracefully-filling-warning-only-when-masked.txt new file mode 100644 index 0000000000..6b612ef109 --- /dev/null +++ b/docs/iris/src/whatsnew/contributions_2.2.0/bugfix_2018-Sep-19_gracefully-filling-warning-only-when-masked.txt @@ -0,0 +1 @@ +* Gracefully filling warnings are only issued when the co-ordinate or bound data is actually masked. diff --git a/lib/iris/analysis/_regrid.py b/lib/iris/analysis/_regrid.py index 71304a5dde..83561a2b94 100644 --- a/lib/iris/analysis/_regrid.py +++ b/lib/iris/analysis/_regrid.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2017, Met Office +# (C) British Crown Copyright 2014 - 2018, Met Office # # This file is part of Iris. # diff --git a/lib/iris/fileformats/_pyke_rules/fc_rules_cf.krb b/lib/iris/fileformats/_pyke_rules/fc_rules_cf.krb index f301f0fc9b..5fc68e58c2 100644 --- a/lib/iris/fileformats/_pyke_rules/fc_rules_cf.krb +++ b/lib/iris/fileformats/_pyke_rules/fc_rules_cf.krb @@ -40,7 +40,7 @@ fc_default # # Context: -# This rule will trigger iff a grid_mapping() case specific fact +# This rule will trigger iff a grid_mapping() case specific fact # has been asserted that refers to a rotated pole. # # Purpose: @@ -120,7 +120,7 @@ fc_provides_grid_mapping_mercator # # Context: -# This rule will trigger iff a grid_mapping() case specific fact +# This rule will trigger iff a grid_mapping() case specific fact # has been asserted that refers to a stereographic. # # Purpose: @@ -199,10 +199,10 @@ fc_provides_grid_mapping_albers_equal_area # # Context: # This rule will trigger iff a coordinate() case specific fact -# has been asserted that refers to a CF latitude coordinate. +# has been asserted that refers to a CF latitude coordinate. # # Purpose: -# Assert that the CF latitude coordinate exists. +# Assert that the CF latitude coordinate exists. # fc_provides_coordinate_latitude foreach @@ -435,10 +435,10 @@ fc_build_auxiliary_coordinate_longitude_rotated # # Context: # This rule will trigger for each auxiliary_coordinate() case specific fact -# that is not a spatio-temporal related auxiliary coordinate. +# that is not a spatio-temporal related auxiliary coordinate. # # Purpose: -# Add the auxiliary coordinate to the cube. +# Add the auxiliary coordinate to the cube. # fc_build_auxiliary_coordinate foreach @@ -457,7 +457,7 @@ fc_build_auxiliary_coordinate # This rule will trigger for each cell_measure case specific fact. # # Purpose: -# Add the cell measures attribute to the cube. +# Add the cell measures attribute to the cube. # fc_build_cell_measure foreach @@ -549,7 +549,7 @@ fc_build_coordinate_longitude_rotated python build_dimension_coordinate(engine, cf_coord_var, coord_name=CF_VALUE_STD_NAME_GRID_LON, coord_system=engine.provides['coordinate_system']) - python engine.rule_triggered.add(rule.name) + python engine.rule_triggered.add(rule.name) # @@ -597,7 +597,7 @@ fc_build_coordinate_longitude_nocs coord_system=None) python engine.rule_triggered.add(rule.name) - + # # Context: # This rule will trigger iff a projection_x_coordinate coordinate exists and @@ -917,8 +917,8 @@ fc_attribute_ukmo__process_flags python attr_value = engine.cf_var.ukmo__process_flags python engine.cube.attributes['ukmo__process_flags'] = tuple([x.replace("_", " ") for x in attr_value.split(" ")]) python engine.rule_triggered.add(rule.name) - - + + # # Context: # This rule will trigger iff a formula term that refers to a @@ -1076,8 +1076,8 @@ fc_extras import iris.std_names import iris.util from iris._lazy_data import as_lazy_data - - + + # # UD Units Constants (based on Unidata udunits.dat definition file) # @@ -1087,7 +1087,7 @@ fc_extras UD_UNITS_LON = ['degrees_east', 'degree_east', 'degree_e', 'degrees_e', 'degreee', 'degreese', 'degrees', 'degrees east', 'degree east', 'degree e', 'degrees e'] - + # # CF Dimensionless Vertical Coordinates # @@ -1119,7 +1119,7 @@ fc_extras CF_GRID_MAPPING_STEREO = 'stereographic' CF_GRID_MAPPING_TRANSVERSE = 'transverse_mercator' CF_GRID_MAPPING_VERTICAL = 'vertical_perspective' - + # # CF Attribute Names. # @@ -1149,7 +1149,7 @@ fc_extras CF_ATTR_LONG_NAME = 'long_name' CF_ATTR_UNITS = 'units' CF_ATTR_CELL_METHODS = 'cell_methods' - + # # CF Attribute Value Constants. # @@ -1158,11 +1158,11 @@ fc_extras CF_VALUE_AXIS_Y = 'y' CF_VALUE_AXIS_T = 't' CF_VALUE_AXIS_Z = 'z' - - + + # Attribute - positive. CF_VALUE_POSITIVE = ['down', 'up'] - + # Attribute - standard_name. CF_VALUE_STD_NAME_LAT = 'latitude' CF_VALUE_STD_NAME_LON = 'longitude' @@ -1213,7 +1213,7 @@ fc_extras msg = msg.replace('variable', 'variable {!r}'.format(name)) warnings.warn(message=msg, category=UnknownCellMethodWarning) - # Set the cube global attributes. + # Set the cube global attributes. for attr_name, attr_value in six.iteritems(cf_var.cf_group.global_attributes): try: if six.PY2 and isinstance(attr_value, six.text_type): @@ -1242,7 +1242,7 @@ fc_extras # Check for a default spherical earth. if major is None and minor is None and inverse_flattening is None: - major = getattr(cf_grid_var, CF_ATTR_GRID_EARTH_RADIUS, None) + major = getattr(cf_grid_var, CF_ATTR_GRID_EARTH_RADIUS, None) return major, minor, inverse_flattening @@ -1344,7 +1344,7 @@ fc_extras inverse_flattening is not None: ellipsoid = iris.coord_systems.GeogCS(major, minor, inverse_flattening) - + cs = iris.coord_systems.LambertConformal( latitude_of_projection_origin, longitude_of_central_meridian, false_easting, false_northing, standard_parallel, @@ -1401,7 +1401,7 @@ fc_extras # values for false_easting, false_northing, # scale_factor_at_projection_origin and standard_parallel. These are # checked elsewhere. - + ellipsoid = None if major is not None or minor is not None or \ inverse_flattening is not None: @@ -1621,7 +1621,7 @@ fc_extras attr_units = get_attr_units(cf_coord_var, attributes) points_data = cf_coord_var[:] # Gracefully fill points masked array. - if ma.isMaskedArray(points_data): + if ma.is_masked(points_data): points_data = ma.filled(points_data) msg = 'Gracefully filling {!r} dimension coordinate masked points' warnings.warn(msg.format(str(cf_coord_var.cf_name))) @@ -1631,7 +1631,7 @@ fc_extras if cf_bounds_var is not None: bounds_data = cf_bounds_var[:] # Gracefully fill bounds masked array. - if ma.isMaskedArray(bounds_data): + if ma.is_masked(bounds_data): bounds_data = ma.filled(bounds_data) msg = 'Gracefully filling {!r} dimension coordinate masked bounds' warnings.warn(msg.format(str(cf_coord_var.cf_name))) @@ -1696,7 +1696,7 @@ fc_extras else: # Scalar coords are placed in the aux_coords container. cube.add_aux_coord(coord, data_dims) - + # Update the coordinate to CF-netCDF variable mapping. engine.provides['coordinates'].append((coord, cf_coord_var.cf_name)) @@ -1739,7 +1739,7 @@ fc_extras # and the coordinate being built. common_dims = [dim for dim in cf_coord_var.dimensions if dim in cf_var.dimensions] - data_dims = None + data_dims = None if common_dims: # Calculate the offset of each common dimension. data_dims = [cf_var.dimensions.index(dim) for dim in common_dims] @@ -1763,7 +1763,7 @@ fc_extras # Update the coordinate to CF-netCDF variable mapping. engine.provides['coordinates'].append((coord, cf_coord_var.cf_name)) - + ################################################################################ def build_cell_measures(engine, cf_cm_attr, coord_name=None): """Create a CellMeasure instance and add it to the cube.""" @@ -1780,19 +1780,19 @@ fc_extras # and the coordinate being built. common_dims = [dim for dim in cf_cm_attr.dimensions if dim in cf_var.dimensions] - data_dims = None + data_dims = None if common_dims: # Calculate the offset of each common dimension. data_dims = [cf_var.dimensions.index(dim) for dim in common_dims] # Determine the standard_name, long_name and var_name standard_name, long_name, var_name = get_names(cf_cm_attr, coord_name, attributes) - + # Obtain the cf_measure. measure = cf_cm_attr.cf_measure - + # Create the CellMeasure - cell_measure = iris.coords.CellMeasure(data, + cell_measure = iris.coords.CellMeasure(data, standard_name=standard_name, long_name=long_name, var_name=var_name, @@ -1802,17 +1802,17 @@ fc_extras # Add it to the cube cube.add_cell_measure(cell_measure, data_dims) - + ################################################################################ def _is_lat_lon(cf_var, ud_units, std_name, std_name_grid, axis_name, prefixes): """ Determine whether the CF coordinate variable is a latitude/longitude variable. - + Ref: [CF] Section 4.1 Latitude Coordinate. [CF] Section 4.2 Longitude Coordinate. - + """ is_valid = False attr_units = getattr(cf_var, CF_ATTR_UNITS, None) @@ -1854,7 +1854,7 @@ fc_extras def is_latitude(engine, cf_name): """Determine whether the CF coordinate variable is a latitude variable.""" cf_var = engine.cf_var.cf_group[cf_name] - return _is_lat_lon(cf_var, UD_UNITS_LAT, CF_VALUE_STD_NAME_LAT, + return _is_lat_lon(cf_var, UD_UNITS_LAT, CF_VALUE_STD_NAME_LAT, CF_VALUE_STD_NAME_GRID_LAT, CF_VALUE_AXIS_Y, ['lat', 'rlat']) @@ -1910,7 +1910,7 @@ fc_extras is_time_reference = cf_units.Unit(attr_units or 1).is_time_reference() except ValueError: is_time_reference = False - + return is_time_reference and (attr_std_name=='time' or attr_axis.lower()==CF_VALUE_AXIS_T) @@ -1978,7 +1978,7 @@ fc_extras def has_supported_mercator_parameters(engine, cf_name): """Determine whether the CF grid mapping variable has the supported values for the parameters of the Mercator projection.""" - + is_valid = True cf_grid_var = engine.cf_var.cf_group[cf_name] @@ -2018,8 +2018,8 @@ fc_extras ################################################################################ def has_supported_stereographic_parameters(engine, cf_name): """Determine whether the CF grid mapping variable has a value of 1.0 - for the scale_factor_at_projection_origin attribute.""" - + for the scale_factor_at_projection_origin attribute.""" + is_valid = True cf_grid_var = engine.cf_var.cf_group[cf_name] @@ -2102,6 +2102,6 @@ fc_extras if len(d[CM_NAME]) != len(comment) and len(comment) == 1: comment = comment*len(d[CM_NAME]) d[CM_INTERVAL] = tuple(interval) - d[CM_COMMENT] = tuple(comment) + d[CM_COMMENT] = tuple(comment) cell_methods.append(iris.coords.CellMethod(d[CM_METHOD], coords=d[CM_NAME], intervals=d[CM_INTERVAL], comments=d[CM_COMMENT])) return tuple(cell_methods) diff --git a/lib/iris/tests/integration/test_regridding.py b/lib/iris/tests/integration/test_regridding.py index 47d155f3f2..80878c21b5 100644 --- a/lib/iris/tests/integration/test_regridding.py +++ b/lib/iris/tests/integration/test_regridding.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2013 - 2017, Met Office +# (C) British Crown Copyright 2013 - 2018, Met Office # # This file is part of Iris. # diff --git a/lib/iris/tests/test_coordsystem.py b/lib/iris/tests/test_coordsystem.py index 6def1a3123..5aef363533 100644 --- a/lib/iris/tests/test_coordsystem.py +++ b/lib/iris/tests/test_coordsystem.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2010 - 2017, Met Office +# (C) British Crown Copyright 2010 - 2018, Met Office # # This file is part of Iris. # diff --git a/lib/iris/tests/unit/fileformats/netcdf/test__load_cube.py b/lib/iris/tests/unit/fileformats/netcdf/test__load_cube.py index 41b6ca5ff0..603547b707 100644 --- a/lib/iris/tests/unit/fileformats/netcdf/test__load_cube.py +++ b/lib/iris/tests/unit/fileformats/netcdf/test__load_cube.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2017, Met Office +# (C) British Crown Copyright 2014 - 2018, Met Office # # This file is part of Iris. # diff --git a/lib/iris/tests/unit/fileformats/pp/test_save.py b/lib/iris/tests/unit/fileformats/pp/test_save.py index 081e01fcf9..361c01f936 100644 --- a/lib/iris/tests/unit/fileformats/pp/test_save.py +++ b/lib/iris/tests/unit/fileformats/pp/test_save.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2017, Met Office +# (C) British Crown Copyright 2014 - 2018, Met Office # # This file is part of Iris. # diff --git a/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_dimension_coordinate.py b/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_dimension_coordinate.py index c645f984cc..38239ea9a0 100644 --- a/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_dimension_coordinate.py +++ b/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_dimension_coordinate.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2018, Met Office # # This file is part of Iris. # @@ -112,6 +112,84 @@ def test_dim_coord_construction(self): self.engine.cube.add_dim_coord.assert_called_with( expected_coord, [0]) + def test_dim_coord_construction_masked_array(self): + self._set_cf_coord_var(np.ma.array( + np.arange(6), + mask=[True, False, False, False, False, False], + fill_value=-999, + )) + + expected_coord = DimCoord( + np.array([-999, 1, 2, 3, 4, 5]), + long_name=self.cf_coord_var.long_name, + var_name=self.cf_coord_var.cf_name, + units=self.cf_coord_var.units, + bounds=self.bounds) + + with warnings.catch_warnings(record=True) as w: + # Asserts must lie within context manager because of deferred + # loading. + with self.deferred_load_patch, self.get_cf_bounds_var_patch: + build_dimension_coordinate(self.engine, self.cf_coord_var) + + # Test that expected coord is built and added to cube. + self.engine.cube.add_dim_coord.assert_called_with( + expected_coord, [0]) + + # Assert warning is raised + assert len(w) == 1 + assert 'Gracefully filling' in w[0].message.args[0] + + def test_dim_coord_construction_masked_array_mask_does_nothing(self): + self._set_cf_coord_var(np.ma.array( + np.arange(6), + mask=False, + )) + + expected_coord = DimCoord( + self.cf_coord_var[:], + long_name=self.cf_coord_var.long_name, + var_name=self.cf_coord_var.cf_name, + units=self.cf_coord_var.units, + bounds=self.bounds) + + with warnings.catch_warnings(record=True) as w: + # Asserts must lie within context manager because of deferred + # loading. + with self.deferred_load_patch, self.get_cf_bounds_var_patch: + build_dimension_coordinate(self.engine, self.cf_coord_var) + + # Test that expected coord is built and added to cube. + self.engine.cube.add_dim_coord.assert_called_with( + expected_coord, [0]) + + # Assert no warning is raised + assert len(w) == 0 + + def test_dim_coord_construction_masked_bounds_mask_does_nothing(self): + self.bounds = np.ma.array(np.arange(12).reshape(6, 2), mask=False) + self._set_cf_coord_var(np.arange(6)) + + expected_coord = DimCoord( + self.cf_coord_var[:], + long_name=self.cf_coord_var.long_name, + var_name=self.cf_coord_var.cf_name, + units=self.cf_coord_var.units, + bounds=self.bounds) + + with warnings.catch_warnings(record=True) as w: + # Asserts must lie within context manager because of deferred + # loading. + with self.deferred_load_patch, self.get_cf_bounds_var_patch: + build_dimension_coordinate(self.engine, self.cf_coord_var) + + # Test that expected coord is built and added to cube. + self.engine.cube.add_dim_coord.assert_called_with( + expected_coord, [0]) + + # Assert no warning is raised + assert len(w) == 0 + def test_aux_coord_construction(self): # Use non monotonically increasing coordinates to force aux coord # construction. From 9088c4958baee4a2833461cc6952f359fa513e58 Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Fri, 28 Sep 2018 17:11:36 +0100 Subject: [PATCH 46/62] Minor clarification to entry via a little more punctuation --- ..._2018-Sep-19_gracefully-filling-warning-only-when-masked.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/iris/src/whatsnew/contributions_2.2.0/bugfix_2018-Sep-19_gracefully-filling-warning-only-when-masked.txt b/docs/iris/src/whatsnew/contributions_2.2.0/bugfix_2018-Sep-19_gracefully-filling-warning-only-when-masked.txt index 6b612ef109..c720e6f07a 100644 --- a/docs/iris/src/whatsnew/contributions_2.2.0/bugfix_2018-Sep-19_gracefully-filling-warning-only-when-masked.txt +++ b/docs/iris/src/whatsnew/contributions_2.2.0/bugfix_2018-Sep-19_gracefully-filling-warning-only-when-masked.txt @@ -1 +1 @@ -* Gracefully filling warnings are only issued when the co-ordinate or bound data is actually masked. +* "Gracefully filling..." warnings are only issued when the co-ordinate or bound data is actually masked. From 94022adcc507544556f42a144378b978591c8ad3 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Mon, 1 Oct 2018 11:48:48 +0100 Subject: [PATCH 47/62] Revert nc vars masking fix (temporarily). (#3178) --- lib/iris/fileformats/cf.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/iris/fileformats/cf.py b/lib/iris/fileformats/cf.py index ddf7f2d64d..f2fc895d1c 100644 --- a/lib/iris/fileformats/cf.py +++ b/lib/iris/fileformats/cf.py @@ -936,11 +936,6 @@ def __init__(self, filename, warn=False, monotonic=False): self._dataset = netCDF4.Dataset(self._filename, mode='r') - # Return variable data as np.ma.MaskedArray *only* when masked points - # are found in the slice requested, otherwise as 'normal' np.ndarray. - # Note: this suits us better, and was standard up to netcdf 1.3. - self._dataset.set_always_mask(False) - # Issue load optimisation warning. if warn and self._dataset.file_format in ['NETCDF3_CLASSIC', 'NETCDF3_64BIT']: warnings.warn('Optimise CF-netCDF loading by converting data from NetCDF3 ' \ From d7f943b744c7932be191899e40370443b1b7cfc4 Mon Sep 17 00:00:00 2001 From: Corinne Bosley Date: Tue, 2 Oct 2018 09:08:58 +0100 Subject: [PATCH 48/62] fix for pandas column object bug (#3176) * turn columns object into pandas array, plus test included cml this time * made things the way pandas like it; bambooish * regenerated cml to match output --- lib/iris/pandas.py | 2 +- .../pandas/as_cube/data_frame_multidim.cml | 15 +++++++++++++++ lib/iris/tests/test_pandas.py | 11 +++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 lib/iris/tests/results/pandas/as_cube/data_frame_multidim.cml diff --git a/lib/iris/pandas.py b/lib/iris/pandas.py index 244a7c011b..798e034017 100644 --- a/lib/iris/pandas.py +++ b/lib/iris/pandas.py @@ -115,7 +115,7 @@ def as_cube(pandas_array, copy=True, calendars=None): _add_iris_coord(cube, "index", pandas_array.index, 0, calendars.get(0, None)) if pandas_array.ndim == 2: - _add_iris_coord(cube, "columns", pandas_array.columns, 1, + _add_iris_coord(cube, "columns", pandas_array.columns.values, 1, calendars.get(1, None)) return cube diff --git a/lib/iris/tests/results/pandas/as_cube/data_frame_multidim.cml b/lib/iris/tests/results/pandas/as_cube/data_frame_multidim.cml new file mode 100644 index 0000000000..d377fa72d8 --- /dev/null +++ b/lib/iris/tests/results/pandas/as_cube/data_frame_multidim.cml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/lib/iris/tests/test_pandas.py b/lib/iris/tests/test_pandas.py index aec432692a..51a163b728 100644 --- a/lib/iris/tests/test_pandas.py +++ b/lib/iris/tests/test_pandas.py @@ -398,6 +398,17 @@ def test_data_frame_masked(self): tests.get_result_path(('pandas', 'as_cube', 'data_frame_masked.cml'))) + def test_data_frame_multidim(self): + data_frame = pandas.DataFrame([[0, 1, 2, 3, 4], + [5, 6, 7, 8, 9]], + index=[0, 1], + columns=['col_1', 'col_2', 'col_3', + 'col_4', 'col_5']) + self.assertCML( + iris.pandas.as_cube(data_frame), + tests.get_result_path(('pandas', 'as_cube', + 'data_frame_multidim.cml'))) + def test_data_frame_cftime_360(self): data_frame = pandas.DataFrame( [[0, 1, 2, 3, 4], From 0ac46010918e830659ad922ac422620e98fa0464 Mon Sep 17 00:00:00 2001 From: Zeb Nicholls Date: Tue, 2 Oct 2018 10:16:39 +0200 Subject: [PATCH 49/62] Update module headers and installation instructions (#3170) * Update module headers and installation instructions * Realise that running the tests is simpler than I thought * Update install instructions for developers * Update INSTALL to be consistent --- .gitignore | 3 +++ INSTALL | 24 ++++++++++++++++++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 4a65981ad5..f0420cbc22 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ *.py[co] +# Environment file which should be autogenerated +*conda_requirements.txt* + # Packages *.egg? *.egg-info diff --git a/INSTALL b/INSTALL index 3ae4855e25..c8a61769fe 100644 --- a/INSTALL +++ b/INSTALL @@ -32,8 +32,8 @@ Installing from source The latest Iris source release is available from https://github.com/SciTools/iris. -Iris makes use of a range of other libraries and python modules. These -dependencies must be in place before you can successfully install +Iris makes use of a range of other libraries and python modules. These +dependencies must be in place before you can successfully install Iris. Once you have satisfied the requirements detailed in the ``requirements`` directory, go to the root of Iris' and run:: @@ -42,8 +42,8 @@ Iris. Once you have satisfied the requirements detailed in the In-place build - an alternative for developers ============================================== -We are very keen to encourage contributions to Iris. For this type of -development activity an in-place build can be useful. Once you've cloned +We are very keen to encourage contributions to Iris. For this type of +development activity an in-place build can be useful. Once you've cloned the Iris git repository you can perform an in-place build with:: pip install -e . @@ -66,6 +66,22 @@ Alternatively, a full requirements file that includes all optional dependencies python requirements/gen_conda_requirements.py --groups all > conda_requirements.txt +Running the tests +''''''''''''''''' + +In order to run the tests, you will need to use the `test` and `docs` groups (we include the `docs` group so that you can run the pull request tests locally). +Hence the commands change to:: + + python requirements/gen_conda_requirements.py --groups test docs > conda_requirements.txt + conda create -n my_iris_env --file conda_requirements.txt + conda activate my_iris_env # or whatever other name you gave it + pip install -e . + +The tests can then be run with + + python setup.py test + + Custom site configuration ========================= The default site configuration values can be overridden by creating the file From aa0fc4eebd5903befdda38d515b1cc6e34d6c2f8 Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Tue, 2 Oct 2018 09:29:24 +0100 Subject: [PATCH 50/62] Correct some failing tests --- lib/iris/tests/unit/analysis/test_Aggregator.py | 8 ++++---- lib/iris/tests/unit/analysis/test_COUNT.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/iris/tests/unit/analysis/test_Aggregator.py b/lib/iris/tests/unit/analysis/test_Aggregator.py index 6895a7c691..e4e7b71656 100644 --- a/lib/iris/tests/unit/analysis/test_Aggregator.py +++ b/lib/iris/tests/unit/analysis/test_Aggregator.py @@ -283,7 +283,7 @@ def test_kwarg_pass_through_no_kwargs(self): axis = mock.sentinel.axis aggregator = Aggregator('', None, lazy_func=lazy_func) aggregator.lazy_aggregate(data, axis) - lazy_func.assert_called_once_with(data, axis) + lazy_func.assert_called_once_with(data, axis=axis) def test_kwarg_pass_through_call_kwargs(self): lazy_func = mock.Mock() @@ -292,7 +292,7 @@ def test_kwarg_pass_through_call_kwargs(self): kwargs = dict(wibble='wobble', foo='bar') aggregator = Aggregator('', None, lazy_func=lazy_func) aggregator.lazy_aggregate(data, axis, **kwargs) - lazy_func.assert_called_once_with(data, axis, **kwargs) + lazy_func.assert_called_once_with(data, axis=axis, **kwargs) def test_kwarg_pass_through_init_kwargs(self): lazy_func = mock.Mock() @@ -301,7 +301,7 @@ def test_kwarg_pass_through_init_kwargs(self): kwargs = dict(wibble='wobble', foo='bar') aggregator = Aggregator('', None, lazy_func=lazy_func, **kwargs) aggregator.lazy_aggregate(data, axis) - lazy_func.assert_called_once_with(data, axis, **kwargs) + lazy_func.assert_called_once_with(data, axis=axis, **kwargs) def test_kwarg_pass_through_combined_kwargs(self): lazy_func = mock.Mock() @@ -313,7 +313,7 @@ def test_kwarg_pass_through_combined_kwargs(self): aggregator.lazy_aggregate(data, axis, **call_kwargs) expected_kwargs = init_kwargs.copy() expected_kwargs.update(call_kwargs) - lazy_func.assert_called_once_with(data, axis, **expected_kwargs) + lazy_func.assert_called_once_with(data, axis=axis, **expected_kwargs) if __name__ == "__main__": diff --git a/lib/iris/tests/unit/analysis/test_COUNT.py b/lib/iris/tests/unit/analysis/test_COUNT.py index 882ad2921e..5422a197e0 100644 --- a/lib/iris/tests/unit/analysis/test_COUNT.py +++ b/lib/iris/tests/unit/analysis/test_COUNT.py @@ -46,7 +46,7 @@ def test_name(self): self.assertEqual(COUNT.name(), 'count') def test_no_function(self): - exp_emsg = "function must be a callable. Got " + exp_emsg = r"function must be a callable. Got <.* 'NoneType'>" with self.assertRaisesRegexp(TypeError, exp_emsg): COUNT.lazy_aggregate(self.lazy_cube.lazy_data(), axis=0) From f2171a14ba7b0ffa62f009f9b857d658e6f53d58 Mon Sep 17 00:00:00 2001 From: bascrezee Date: Tue, 2 Oct 2018 13:18:11 +0200 Subject: [PATCH 51/62] Update tests.rst (#3082) Added one line on how to test. This is mentioned in the Iris Check List in pulls.rst list, but would be good to have it here as well (useful for new contributors like me). --- docs/iris/src/developers_guide/tests.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/iris/src/developers_guide/tests.rst b/docs/iris/src/developers_guide/tests.rst index d2ac826f22..929073b569 100644 --- a/docs/iris/src/developers_guide/tests.rst +++ b/docs/iris/src/developers_guide/tests.rst @@ -3,6 +3,9 @@ Testing ******* +The Iris tests may be run with ``python setup.py test`` which has a +command line utility included. + There are three categories of tests within Iris: - Unit tests - Integration tests From 01e32852c377e25d4cf881389a54700ccdc6a383 Mon Sep 17 00:00:00 2001 From: tv3141 Date: Tue, 2 Oct 2018 15:13:19 +0100 Subject: [PATCH 52/62] Add link to developer guide. (#3093) --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index d7b4900c2c..a6e9dd525b 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ + [Documentation](#documentation) + [Installation](#installation) + [Copyright and licence](#copyright-and-licence) ++ [Contributing](#contributing) [](TOC) @@ -98,5 +99,7 @@ are available in [INSTALL](INSTALL). Iris may be freely distributed, modified and used commercially under the terms of its [GNU LGPLv3 license](COPYING.LESSER). +# Contributing +Information on how to contribute can be found in the [Iris developer guide](https://scitools.org.uk/iris/docs/latest/developers_guide/index.html). (C) British Crown Copyright 2010 - 2018, Met Office From 7ef798572c7f921c941c514ea41e242fa7ce2040 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Wed, 19 Sep 2018 11:32:21 +0100 Subject: [PATCH 53/62] Combine identical test routines for pcolor + pcolormesh. --- lib/iris/tests/unit/plot/_blockplot_common.py | 97 +++++++++++++++++++ lib/iris/tests/unit/plot/test_pcolor.py | 63 +++--------- lib/iris/tests/unit/plot/test_pcolormesh.py | 63 +++--------- 3 files changed, 125 insertions(+), 98 deletions(-) create mode 100644 lib/iris/tests/unit/plot/_blockplot_common.py diff --git a/lib/iris/tests/unit/plot/_blockplot_common.py b/lib/iris/tests/unit/plot/_blockplot_common.py new file mode 100644 index 0000000000..368d44f258 --- /dev/null +++ b/lib/iris/tests/unit/plot/_blockplot_common.py @@ -0,0 +1,97 @@ +# (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 . +"""Common test code for `iris.plot.pcolor` and `iris.plot.pcolormesh`.""" + +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_2d +from iris.tests.unit.plot import MixinCoords + +if tests.MPL_AVAILABLE: + import iris.plot as iplt + + +class MixinStringCoordPlot(object): + # Mixin for common string-coord tests on pcolor/pcolormesh. + # To use, make a class that inherits from this *and* + # :class:`iris.tests.unit.plot.TestGraphicStringCoord`, + # and defines "self.blockplot_func()", to return the `iris.plot` function. + def test_yaxis_labels(self): + self.blockplot_func()(self.cube, coords=('bar', 'str_coord')) + self.assertBoundsTickLabels('yaxis') + + def test_xaxis_labels(self): + self.blockplot_func()(self.cube, coords=('str_coord', 'bar')) + self.assertBoundsTickLabels('xaxis') + + def test_xaxis_labels_with_axes(self): + import matplotlib.pyplot as plt + fig = plt.figure() + ax = fig.add_subplot(111) + ax.set_xlim(0, 3) + self.blockplot_func()(self.cube, coords=('str_coord', 'bar'), axes=ax) + plt.close(fig) + self.assertPointsTickLabels('xaxis', ax) + + def test_yaxis_labels_with_axes(self): + import matplotlib.pyplot as plt + fig = plt.figure() + ax = fig.add_subplot(111) + ax.set_ylim(0, 3) + self.blockplot_func()(self.cube, axes=ax, coords=('bar', 'str_coord')) + plt.close(fig) + self.assertPointsTickLabels('yaxis', ax) + + def test_geoaxes_exception(self): + import matplotlib.pyplot as plt + fig = plt.figure() + ax = fig.add_subplot(111) + self.assertRaises(TypeError, self.blockplot_func(), + self.lat_lon_cube, axes=ax) + plt.close(fig) + + +class Mixin2dCoordsPlot(MixinCoords): + # Mixin for common coordinate tests on pcolor/pcolormesh. + # To use, make a class that inherits from this *and* + # :class:`iris.tests.IrisTest`, + # and defines "self.blockplot_func()", to return the `iris.plot` function. + def blockplot_setup(self): + # We have a 2d cube with dimensionality (bar: 3; foo: 4) + self.cube = simple_2d(with_bounds=True) + coord = self.cube.coord('foo') + self.foo = coord.contiguous_bounds() + self.foo_index = np.arange(coord.points.size + 1) + coord = self.cube.coord('bar') + self.bar = coord.contiguous_bounds() + self.bar_index = np.arange(coord.points.size + 1) + self.data = self.cube.data + self.dataT = self.data.T + self.draw_func = self.blockplot_func() + patch_target_name = 'matplotlib.pyplot.' + self.draw_func.__name__ + self.mpl_patch = self.patch(patch_target_name) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/plot/test_pcolor.py b/lib/iris/tests/unit/plot/test_pcolor.py index 46219965f9..ef681b3233 100644 --- a/lib/iris/tests/unit/plot/test_pcolor.py +++ b/lib/iris/tests/unit/plot/test_pcolor.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2016, Met Office +# (C) British Crown Copyright 2014 - 2018, Met Office # # This file is part of Iris. # @@ -26,64 +26,29 @@ import numpy as np from iris.tests.stock import simple_2d -from iris.tests.unit.plot import TestGraphicStringCoord, MixinCoords +import iris.plot +from iris.tests.unit.plot import TestGraphicStringCoord +from iris.tests.unit.plot._blockplot_common import \ + MixinStringCoordPlot, Mixin2dCoordsPlot + if tests.MPL_AVAILABLE: import iris.plot as iplt @tests.skip_plot -class TestStringCoordPlot(TestGraphicStringCoord): - def test_yaxis_labels(self): - iplt.pcolor(self.cube, coords=('bar', 'str_coord')) - self.assertBoundsTickLabels('yaxis') - - def test_xaxis_labels(self): - iplt.pcolor(self.cube, coords=('str_coord', 'bar')) - self.assertBoundsTickLabels('xaxis') - - def test_xaxis_labels_with_axes(self): - import matplotlib.pyplot as plt - fig = plt.figure() - ax = fig.add_subplot(111) - ax.set_xlim(0, 3) - iplt.pcolor(self.cube, coords=('str_coord', 'bar'), axes=ax) - plt.close(fig) - self.assertPointsTickLabels('xaxis', ax) - - def test_yaxis_labels_with_axes(self): - import matplotlib.pyplot as plt - fig = plt.figure() - ax = fig.add_subplot(111) - ax.set_ylim(0, 3) - iplt.pcolor(self.cube, axes=ax, coords=('bar', 'str_coord')) - plt.close(fig) - self.assertPointsTickLabels('yaxis', ax) - - def test_geoaxes_exception(self): - import matplotlib.pyplot as plt - fig = plt.figure() - ax = fig.add_subplot(111) - self.assertRaises(TypeError, iplt.pcolor, - self.lat_lon_cube, axes=ax) - plt.close(fig) +class TestStringCoordPlot(MixinStringCoordPlot, TestGraphicStringCoord): + def blockplot_func(self): + return iris.plot.pcolor @tests.skip_plot -class TestCoords(tests.IrisTest, MixinCoords): +class Test2dCoords(tests.IrisTest, Mixin2dCoordsPlot): def setUp(self): - # We have a 2d cube with dimensionality (bar: 3; foo: 4) - self.cube = simple_2d(with_bounds=True) - coord = self.cube.coord('foo') - self.foo = coord.contiguous_bounds() - self.foo_index = np.arange(coord.points.size + 1) - coord = self.cube.coord('bar') - self.bar = coord.contiguous_bounds() - self.bar_index = np.arange(coord.points.size + 1) - self.data = self.cube.data - self.dataT = self.data.T - self.mpl_patch = self.patch('matplotlib.pyplot.pcolor') - self.draw_func = iplt.pcolor + self.blockplot_setup() + + def blockplot_func(self): + return iris.plot.pcolor if __name__ == "__main__": diff --git a/lib/iris/tests/unit/plot/test_pcolormesh.py b/lib/iris/tests/unit/plot/test_pcolormesh.py index fb4ceffe28..a452a7af74 100644 --- a/lib/iris/tests/unit/plot/test_pcolormesh.py +++ b/lib/iris/tests/unit/plot/test_pcolormesh.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2016, Met Office +# (C) British Crown Copyright 2014 - 2018, Met Office # # This file is part of Iris. # @@ -26,64 +26,29 @@ import numpy as np from iris.tests.stock import simple_2d -from iris.tests.unit.plot import TestGraphicStringCoord, MixinCoords +import iris.plot +from iris.tests.unit.plot import TestGraphicStringCoord +from iris.tests.unit.plot._blockplot_common import \ + MixinStringCoordPlot, Mixin2dCoordsPlot + if tests.MPL_AVAILABLE: import iris.plot as iplt @tests.skip_plot -class TestStringCoordPlot(TestGraphicStringCoord): - def test_yaxis_labels(self): - iplt.pcolormesh(self.cube, coords=('bar', 'str_coord')) - self.assertBoundsTickLabels('yaxis') - - def test_xaxis_labels(self): - iplt.pcolormesh(self.cube, coords=('str_coord', 'bar')) - self.assertBoundsTickLabels('xaxis') - - def test_xaxis_labels_with_axes(self): - import matplotlib.pyplot as plt - fig = plt.figure() - ax = fig.add_subplot(111) - ax.set_xlim(0, 3) - iplt.pcolormesh(self.cube, axes=ax, coords=('str_coord', 'bar'),) - plt.close(fig) - self.assertPointsTickLabels('xaxis', ax) - - def test_yaxis_labels_with_axes(self): - import matplotlib.pyplot as plt - fig = plt.figure() - ax = fig.add_subplot(111) - ax.set_ylim(0, 3) - iplt.pcolormesh(self.cube, axes=ax, coords=('bar', 'str_coord')) - plt.close(fig) - self.assertPointsTickLabels('yaxis', ax) - - def test_geoaxes_exception(self): - import matplotlib.pyplot as plt - fig = plt.figure() - ax = fig.add_subplot(111) - self.assertRaises(TypeError, iplt.pcolormesh, - self.lat_lon_cube, axes=ax) - plt.close(fig) +class TestStringCoordPlot(MixinStringCoordPlot, TestGraphicStringCoord): + def blockplot_func(self): + return iris.plot.pcolormesh @tests.skip_plot -class TestCoords(tests.IrisTest, MixinCoords): +class Test2dCoords(tests.IrisTest, Mixin2dCoordsPlot): def setUp(self): - # We have a 2d cube with dimensionality (bar: 3; foo: 4) - self.cube = simple_2d(with_bounds=True) - coord = self.cube.coord('foo') - self.foo = coord.contiguous_bounds() - self.foo_index = np.arange(coord.points.size + 1) - coord = self.cube.coord('bar') - self.bar = coord.contiguous_bounds() - self.bar_index = np.arange(coord.points.size + 1) - self.data = self.cube.data - self.dataT = self.data.T - self.mpl_patch = self.patch('matplotlib.pyplot.pcolormesh') - self.draw_func = iplt.pcolormesh + self.blockplot_setup() + + def blockplot_func(self): + return iris.plot.pcolormesh if __name__ == "__main__": From a4b3fb77217147ee48c031ce15751499f793b410 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Wed, 3 Oct 2018 12:43:12 +0100 Subject: [PATCH 54/62] Test 'contiguity_tolerance' use in iris.plot.pcolor and pcolormesh. --- lib/iris/tests/unit/plot/_blockplot_common.py | 30 +++++++++++++++++-- lib/iris/tests/unit/plot/test_pcolor.py | 17 ++++++++--- lib/iris/tests/unit/plot/test_pcolormesh.py | 17 ++++++++--- 3 files changed, 53 insertions(+), 11 deletions(-) diff --git a/lib/iris/tests/unit/plot/_blockplot_common.py b/lib/iris/tests/unit/plot/_blockplot_common.py index 368d44f258..dc69a5c991 100644 --- a/lib/iris/tests/unit/plot/_blockplot_common.py +++ b/lib/iris/tests/unit/plot/_blockplot_common.py @@ -25,12 +25,10 @@ import numpy as np +from iris.tests import mock from iris.tests.stock import simple_2d from iris.tests.unit.plot import MixinCoords -if tests.MPL_AVAILABLE: - import iris.plot as iplt - class MixinStringCoordPlot(object): # Mixin for common string-coord tests on pcolor/pcolormesh. @@ -93,5 +91,31 @@ def blockplot_setup(self): self.mpl_patch = self.patch(patch_target_name) +class Mixin2dCoordsContigTol(object): + # Mixin for contiguity tolerance argument to pcolor/pcolormesh. + # To use, make a class that inherits from this *and* + # :class:`iris.tests.IrisTest`, + # and defines "self.blockplot_func()", to return the `iris.plot` function, + # and defines "self.additional_kwargs" for expected extra call args. + def test_contig_tol(self): + # Patch the inner call to ensure contiguity_tolerance is passed. + cube_argument = mock.sentinel.passed_arg + expected_result = mock.sentinel.returned_value + blockplot_patch = self.patch( + 'iris.plot._draw_2d_from_bounds', + mock.Mock(return_value=expected_result)) + # Make the call + draw_func = self.blockplot_func() + other_kwargs = self.additional_kwargs + result = draw_func(cube_argument, contiguity_tolerance=0.0123) + drawfunc_name = draw_func.__name__ + # Check details of the call that was made. + self.assertEqual( + blockplot_patch.call_args_list, + [mock.call(drawfunc_name, cube_argument, + contiguity_tolerance=0.0123, **other_kwargs)]) + self.assertEqual(result, expected_result) + + if __name__ == "__main__": tests.main() diff --git a/lib/iris/tests/unit/plot/test_pcolor.py b/lib/iris/tests/unit/plot/test_pcolor.py index ef681b3233..da9a461ade 100644 --- a/lib/iris/tests/unit/plot/test_pcolor.py +++ b/lib/iris/tests/unit/plot/test_pcolor.py @@ -26,20 +26,20 @@ import numpy as np from iris.tests.stock import simple_2d -import iris.plot from iris.tests.unit.plot import TestGraphicStringCoord from iris.tests.unit.plot._blockplot_common import \ - MixinStringCoordPlot, Mixin2dCoordsPlot + MixinStringCoordPlot, Mixin2dCoordsPlot, Mixin2dCoordsContigTol if tests.MPL_AVAILABLE: import iris.plot as iplt + PLOT_FUNCTION_TO_TEST = iplt.pcolor @tests.skip_plot class TestStringCoordPlot(MixinStringCoordPlot, TestGraphicStringCoord): def blockplot_func(self): - return iris.plot.pcolor + return PLOT_FUNCTION_TO_TEST @tests.skip_plot @@ -48,7 +48,16 @@ def setUp(self): self.blockplot_setup() def blockplot_func(self): - return iris.plot.pcolor + return PLOT_FUNCTION_TO_TEST + + +@tests.skip_plot +class Test2dContigTol(tests.IrisTest, Mixin2dCoordsContigTol): + # Extra call kwargs expected. + additional_kwargs = dict(antialiased=True, snap=False) + + def blockplot_func(self): + return PLOT_FUNCTION_TO_TEST if __name__ == "__main__": diff --git a/lib/iris/tests/unit/plot/test_pcolormesh.py b/lib/iris/tests/unit/plot/test_pcolormesh.py index a452a7af74..613a559415 100644 --- a/lib/iris/tests/unit/plot/test_pcolormesh.py +++ b/lib/iris/tests/unit/plot/test_pcolormesh.py @@ -26,20 +26,20 @@ import numpy as np from iris.tests.stock import simple_2d -import iris.plot from iris.tests.unit.plot import TestGraphicStringCoord from iris.tests.unit.plot._blockplot_common import \ - MixinStringCoordPlot, Mixin2dCoordsPlot + MixinStringCoordPlot, Mixin2dCoordsPlot, Mixin2dCoordsContigTol if tests.MPL_AVAILABLE: import iris.plot as iplt + PLOT_FUNCTION_TO_TEST = iplt.pcolormesh @tests.skip_plot class TestStringCoordPlot(MixinStringCoordPlot, TestGraphicStringCoord): def blockplot_func(self): - return iris.plot.pcolormesh + return PLOT_FUNCTION_TO_TEST @tests.skip_plot @@ -48,7 +48,16 @@ def setUp(self): self.blockplot_setup() def blockplot_func(self): - return iris.plot.pcolormesh + return PLOT_FUNCTION_TO_TEST + + +@tests.skip_plot +class Test2dContigTol(tests.IrisTest, Mixin2dCoordsContigTol): + # Extra call kwargs expected -- unlike 'pcolor', there are none. + additional_kwargs = {} + + def blockplot_func(self): + return PLOT_FUNCTION_TO_TEST if __name__ == "__main__": From 5f2aadc4c00278659be1cdd3e2035108314e5c1d Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Wed, 3 Oct 2018 13:13:14 +0100 Subject: [PATCH 55/62] Unpin dask --- requirements/core.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/core.txt b/requirements/core.txt index c22a43a36b..09ffb8954c 100644 --- a/requirements/core.txt +++ b/requirements/core.txt @@ -7,7 +7,7 @@ cartopy #conda: proj4<5 cf_units>=2 cftime -dask[array]==0.18.1 #conda: dask==0.18.1 +dask[array] #conda: dask matplotlib>=2,<3 netcdf4 numpy>=1.14 From c5be03eb1c0c6ba3b4e9db020d1ec54c15f18f3c Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Wed, 3 Oct 2018 13:15:13 +0100 Subject: [PATCH 56/62] Handle printing bounds of long time interval coords (#3140) * Handle printing time bounds * Whatsnew, whitespace fix for test result --- ...Aug-20_print-long-time-interval-bounds.txt | 2 ++ lib/iris/coords.py | 6 +++++- lib/iris/tests/unit/coords/test_Coord.py | 20 +++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 docs/iris/src/whatsnew/contributions_2.2.0/bugfix_2018-Aug-20_print-long-time-interval-bounds.txt diff --git a/docs/iris/src/whatsnew/contributions_2.2.0/bugfix_2018-Aug-20_print-long-time-interval-bounds.txt b/docs/iris/src/whatsnew/contributions_2.2.0/bugfix_2018-Aug-20_print-long-time-interval-bounds.txt new file mode 100644 index 0000000000..3651598b72 --- /dev/null +++ b/docs/iris/src/whatsnew/contributions_2.2.0/bugfix_2018-Aug-20_print-long-time-interval-bounds.txt @@ -0,0 +1,2 @@ +* Fixed a bug that prevented printing time coordinates with bounds when the time + coordinate was measured on a long interval (that is, ``months`` or ``years``). \ No newline at end of file diff --git a/lib/iris/coords.py b/lib/iris/coords.py index 42882fbc2c..6402e73263 100644 --- a/lib/iris/coords.py +++ b/lib/iris/coords.py @@ -782,7 +782,11 @@ def __str__(self): points = self._str_dates(self.points) bounds = '' if self.has_bounds(): - bounds = ', bounds=' + self._str_dates(self.bounds) + if self.units.is_long_time_interval(): + bounds_vals = self.bounds + else: + bounds_vals = self._str_dates(self.bounds) + bounds = ', bounds={vals}'.format(vals=bounds_vals) result = fmt.format(self=self, cls=type(self).__name__, points=points, bounds=bounds, other_metadata=self._repr_other_metadata()) diff --git a/lib/iris/tests/unit/coords/test_Coord.py b/lib/iris/tests/unit/coords/test_Coord.py index 8cf79caad3..27b468044d 100644 --- a/lib/iris/tests/unit/coords/test_Coord.py +++ b/lib/iris/tests/unit/coords/test_Coord.py @@ -677,6 +677,17 @@ def test_short_time_interval(self): result = coord.__str__() self.assertEqual(expected, result) + def test_short_time_interval__bounded(self): + coord = DimCoord([5, 6], standard_name='time', + units='days since 1970-01-01') + coord.guess_bounds() + expected = ("DimCoord([1970-01-06 00:00:00, 1970-01-07 00:00:00], " + "bounds=[[1970-01-05 12:00:00, 1970-01-06 12:00:00],\n" + " [1970-01-06 12:00:00, 1970-01-07 12:00:00]], " + "standard_name='time', calendar='gregorian')") + result = coord.__str__() + self.assertEqual(expected, result) + def test_long_time_interval(self): coord = DimCoord([5], standard_name='time', units='years since 1970-01-01') @@ -684,6 +695,15 @@ def test_long_time_interval(self): result = coord.__str__() self.assertEqual(expected, result) + def test_long_time_interval__bounded(self): + coord = DimCoord([5, 6], standard_name='time', + units='years since 1970-01-01') + coord.guess_bounds() + expected = ("DimCoord([5 6], bounds=[[4.5 5.5]\n [5.5 6.5]], " + "standard_name='time', calendar='gregorian')") + result = coord.__str__() + self.assertEqual(expected, result) + def test_non_time_unit(self): coord = DimCoord([1.]) expected = repr(coord) From e2c47f9580a5a3354d54185d3586302e8982b403 Mon Sep 17 00:00:00 2001 From: Ruth Comer Date: Wed, 3 Oct 2018 14:46:43 +0100 Subject: [PATCH 57/62] iris.util.reverse on cubes (#3155) * make cube reversing official * review: test conventions, etc. * review: enable coord specification * add whatsnew * review: AssertRaises --> AssertRaisesRegexp * cube error handling --- .../newfeature_2018-Oct-03_reverse_cube.txt | 1 + lib/iris/tests/test_util.py | 25 --- lib/iris/tests/unit/util/test_reverse.py | 185 ++++++++++++++++++ lib/iris/util.py | 50 +++-- 4 files changed, 224 insertions(+), 37 deletions(-) create mode 100644 docs/iris/src/whatsnew/contributions_2.2.0/newfeature_2018-Oct-03_reverse_cube.txt create mode 100644 lib/iris/tests/unit/util/test_reverse.py diff --git a/docs/iris/src/whatsnew/contributions_2.2.0/newfeature_2018-Oct-03_reverse_cube.txt b/docs/iris/src/whatsnew/contributions_2.2.0/newfeature_2018-Oct-03_reverse_cube.txt new file mode 100644 index 0000000000..c7e3bef8a4 --- /dev/null +++ b/docs/iris/src/whatsnew/contributions_2.2.0/newfeature_2018-Oct-03_reverse_cube.txt @@ -0,0 +1 @@ +* :func:`iris.util.reverse` can now be used to reverse a cube by specifying one or more coordinates. diff --git a/lib/iris/tests/test_util.py b/lib/iris/tests/test_util.py index 97bb4d627d..d8cfcf648c 100644 --- a/lib/iris/tests/test_util.py +++ b/lib/iris/tests/test_util.py @@ -93,31 +93,6 @@ def test_monotonic_strict(self): self.assertMonotonic(b) -class TestReverse(tests.IrisTest): - def test_simple(self): - a = np.arange(12).reshape(3, 4) - np.testing.assert_array_equal(a[::-1], iris.util.reverse(a, 0)) - np.testing.assert_array_equal(a[::-1, ::-1], iris.util.reverse(a, [0, 1])) - np.testing.assert_array_equal(a[:, ::-1], iris.util.reverse(a, 1)) - np.testing.assert_array_equal(a[:, ::-1], iris.util.reverse(a, [1])) - self.assertRaises(ValueError, iris.util.reverse, a, []) - self.assertRaises(ValueError, iris.util.reverse, a, -1) - self.assertRaises(ValueError, iris.util.reverse, a, 10) - self.assertRaises(ValueError, iris.util.reverse, a, [-1]) - self.assertRaises(ValueError, iris.util.reverse, a, [0, -1]) - - def test_single(self): - a = np.arange(36).reshape(3, 4, 3) - np.testing.assert_array_equal(a[::-1], iris.util.reverse(a, 0)) - np.testing.assert_array_equal(a[::-1, ::-1], iris.util.reverse(a, [0, 1])) - np.testing.assert_array_equal(a[:, ::-1, ::-1], iris.util.reverse(a, [1, 2])) - np.testing.assert_array_equal(a[..., ::-1], iris.util.reverse(a, 2)) - self.assertRaises(ValueError, iris.util.reverse, a, -1) - self.assertRaises(ValueError, iris.util.reverse, a, 10) - self.assertRaises(ValueError, iris.util.reverse, a, [-1]) - self.assertRaises(ValueError, iris.util.reverse, a, [0, -1]) - - class TestClipString(tests.IrisTest): def setUp(self): self.test_string = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." diff --git a/lib/iris/tests/unit/util/test_reverse.py b/lib/iris/tests/unit/util/test_reverse.py new file mode 100644 index 0000000000..f5a21d259b --- /dev/null +++ b/lib/iris/tests/unit/util/test_reverse.py @@ -0,0 +1,185 @@ +# (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.reverse`.""" + +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 unittest + +import iris +from iris.util import reverse +import numpy as np + + +class Test_array(tests.IrisTest): + def test_simple_array(self): + a = np.arange(12).reshape(3, 4) + self.assertArrayEqual(a[::-1], reverse(a, 0)) + self.assertArrayEqual(a[::-1, ::-1], reverse(a, [0, 1])) + self.assertArrayEqual(a[:, ::-1], reverse(a, 1)) + self.assertArrayEqual(a[:, ::-1], reverse(a, [1])) + + msg = 'Reverse was expecting a single axis or a 1d array *' + with self.assertRaisesRegexp(ValueError, msg): + reverse(a, []) + + msg = 'An axis value out of range for the number of dimensions *' + with self.assertRaisesRegexp(ValueError, msg): + reverse(a, -1) + with self.assertRaisesRegexp(ValueError, msg): + reverse(a, 10) + with self.assertRaisesRegexp(ValueError, msg): + reverse(a, [-1]) + with self.assertRaisesRegexp(ValueError, msg): + reverse(a, [0, -1]) + + msg = 'To reverse an array, provide an int *' + with self.assertRaisesRegexp(TypeError, msg): + reverse(a, 'latitude') + + def test_single_array(self): + a = np.arange(36).reshape(3, 4, 3) + self.assertArrayEqual(a[::-1], reverse(a, 0)) + self.assertArrayEqual(a[::-1, ::-1], reverse(a, [0, 1])) + self.assertArrayEqual(a[:, ::-1, ::-1], reverse(a, [1, 2])) + self.assertArrayEqual(a[..., ::-1], reverse(a, 2)) + + msg = 'Reverse was expecting a single axis or a 1d array *' + with self.assertRaisesRegexp(ValueError, msg): + reverse(a, []) + + msg = 'An axis value out of range for the number of dimensions *' + with self.assertRaisesRegexp(ValueError, msg): + reverse(a, -1) + with self.assertRaisesRegexp(ValueError, msg): + reverse(a, 10) + with self.assertRaisesRegexp(ValueError, msg): + reverse(a, [-1]) + with self.assertRaisesRegexp(ValueError, msg): + reverse(a, [0, -1]) + + with self.assertRaisesRegexp( + TypeError, 'To reverse an array, provide an int *'): + reverse(a, 'latitude') + + +class Test_cube(tests.IrisTest): + def setUp(self): + # On this cube pair, the coordinates to perform operations on have + # matching long names but the points array on one cube is reversed + # with respect to that on the other. + data = np.arange(12).reshape(3, 4) + self.a1 = iris.coords.DimCoord([1, 2, 3], long_name='a') + self.b1 = iris.coords.DimCoord([1, 2, 3, 4], long_name='b') + a2 = iris.coords.DimCoord([3, 2, 1], long_name='a') + b2 = iris.coords.DimCoord([4, 3, 2, 1], long_name='b') + self.span = iris.coords.AuxCoord(np.arange(12).reshape(3, 4), + long_name='spanning') + + self.cube1 = iris.cube.Cube( + data, dim_coords_and_dims=[(self.a1, 0), (self.b1, 1)], + aux_coords_and_dims=[(self.span, (0, 1))]) + + self.cube2 = iris.cube.Cube( + data, dim_coords_and_dims=[(a2, 0), (b2, 1)]) + + def test_cube_dim(self): + cube1_reverse0 = reverse(self.cube1, 0) + cube1_reverse1 = reverse(self.cube1, 1) + cube1_reverse_both = reverse(self.cube1, (0, 1)) + + self.assertArrayEqual(self.cube1.data[::-1], cube1_reverse0.data) + self.assertArrayEqual(self.cube2.coord('a').points, + cube1_reverse0.coord('a').points) + self.assertArrayEqual(self.cube1.coord('b').points, + cube1_reverse0.coord('b').points) + + self.assertArrayEqual(self.cube1.data[:, ::-1], cube1_reverse1.data) + self.assertArrayEqual(self.cube1.coord('a').points, + cube1_reverse1.coord('a').points) + self.assertArrayEqual(self.cube2.coord('b').points, + cube1_reverse1.coord('b').points) + + self.assertArrayEqual(self.cube1.data[::-1, ::-1], + cube1_reverse_both.data) + self.assertArrayEqual(self.cube2.coord('a').points, + cube1_reverse_both.coord('a').points) + self.assertArrayEqual(self.cube2.coord('b').points, + cube1_reverse_both.coord('b').points) + + def test_cube_coord(self): + cube1_reverse0 = reverse(self.cube1, self.a1) + cube1_reverse1 = reverse(self.cube1, 'b') + cube1_reverse_both = reverse(self.cube1, (self.a1, self.b1)) + cube1_reverse_spanning = reverse(self.cube1, 'spanning') + + self.assertArrayEqual(self.cube1.data[::-1], cube1_reverse0.data) + self.assertArrayEqual(self.cube2.coord('a').points, + cube1_reverse0.coord('a').points) + self.assertArrayEqual(self.cube1.coord('b').points, + cube1_reverse0.coord('b').points) + + self.assertArrayEqual(self.cube1.data[:, ::-1], cube1_reverse1.data) + self.assertArrayEqual(self.cube1.coord('a').points, + cube1_reverse1.coord('a').points) + self.assertArrayEqual(self.cube2.coord('b').points, + cube1_reverse1.coord('b').points) + + self.assertArrayEqual(self.cube1.data[::-1, ::-1], + cube1_reverse_both.data) + self.assertArrayEqual(self.cube2.coord('a').points, + cube1_reverse_both.coord('a').points) + self.assertArrayEqual(self.cube2.coord('b').points, + cube1_reverse_both.coord('b').points) + + self.assertArrayEqual(self.cube1.data[::-1, ::-1], + cube1_reverse_spanning.data) + self.assertArrayEqual(self.cube2.coord('a').points, + cube1_reverse_spanning.coord('a').points) + self.assertArrayEqual(self.cube2.coord('b').points, + cube1_reverse_spanning.coord('b').points) + self.assertArrayEqual( + self.span.points[::-1, ::-1], + cube1_reverse_spanning.coord('spanning').points) + + msg = 'Expected to find exactly 1 latitude coordinate, but found none.' + with self.assertRaisesRegexp( + iris.exceptions.CoordinateNotFoundError, msg): + reverse(self.cube1, 'latitude') + + msg = 'Reverse was expecting a single axis or a 1d array *' + with self.assertRaisesRegexp(ValueError, msg): + reverse(self.cube1, []) + + msg = ('coords_or_dims must be int, str, coordinate or sequence of ' + 'these. Got cube.') + with self.assertRaisesRegexp(TypeError, msg): + reverse(self.cube1, self.cube1) + + msg = ('coords_or_dims must be int, str, coordinate or sequence of ' + 'these.') + with self.assertRaisesRegexp(TypeError, msg): + reverse(self.cube1, 3.) + + +if __name__ == '__main__': + unittest.main() diff --git a/lib/iris/util.py b/lib/iris/util.py index 4f4c42dc4b..7e5b2c36a8 100644 --- a/lib/iris/util.py +++ b/lib/iris/util.py @@ -41,6 +41,7 @@ import iris import iris.exceptions +import iris.cube def broadcast_to_shape(array, shape, dim_map): @@ -406,16 +407,19 @@ def between(lh, rh, lh_inclusive=True, rh_inclusive=True): return lambda c: lh < c < rh -def reverse(array, axes): +def reverse(cube_or_array, coords_or_dims): """ - Reverse the array along the given axes. + Reverse the cube or array along the given dimensions. Args: - * array - The array to reverse - * axes - A single value or array of values of axes to reverse + * cube_or_array: :class:`iris.cube.Cube` or :class:`numpy.ndarray` + The cube or array to reverse. + * coords_or_dims: int, str, :class:`iris.coords.Coord` or sequence of these + Identify one or more dimensions to reverse. If cube_or_array is a + numpy array, use int or a sequence of ints, as in the examples below. + If cube_or_array is a Cube, a Coord or coordinate name (or sequence of + these) may be specified instead. :: @@ -447,20 +451,42 @@ def reverse(array, axes): [15 14 13 12]]] """ - index = [slice(None, None)] * array.ndim - axes = np.array(axes, ndmin=1) - if axes.ndim != 1: + index = [slice(None, None)] * cube_or_array.ndim + + if isinstance(coords_or_dims, iris.cube.Cube): + raise TypeError('coords_or_dims must be int, str, coordinate or ' + 'sequence of these. Got cube.') + + if iris.cube._is_single_item(coords_or_dims): + coords_or_dims = [coords_or_dims] + + axes = set() + for coord_or_dim in coords_or_dims: + if isinstance(coord_or_dim, int): + axes.add(coord_or_dim) + elif isinstance(cube_or_array, np.ndarray): + raise TypeError( + 'To reverse an array, provide an int or sequence of ints.') + else: + try: + axes.update(cube_or_array.coord_dims(coord_or_dim)) + except AttributeError: + raise TypeError('coords_or_dims must be int, str, coordinate ' + 'or sequence of these.') + + axes = np.array(list(axes), ndmin=1) + if axes.ndim != 1 or axes.size == 0: raise ValueError('Reverse was expecting a single axis or a 1d array ' 'of axes, got %r' % axes) - if np.min(axes) < 0 or np.max(axes) > array.ndim-1: + if np.min(axes) < 0 or np.max(axes) > cube_or_array.ndim-1: raise ValueError('An axis value out of range for the number of ' 'dimensions from the given array (%s) was received. ' - 'Got: %r' % (array.ndim, axes)) + 'Got: %r' % (cube_or_array.ndim, axes)) for axis in axes: index[axis] = slice(None, None, -1) - return array[tuple(index)] + return cube_or_array[tuple(index)] def monotonic(array, strict=False, return_direction=False): From a41db2d786e1b0fc4dcfc5d1648f14ad934a86ee Mon Sep 17 00:00:00 2001 From: hdyson Date: Wed, 3 Oct 2018 15:59:15 +0100 Subject: [PATCH 58/62] Improved Aux factory error handling: better message, delivered earlier (#3182) * Tests for expected behaviour * Raise error if aux factory relies on a coordinate external to cube * Include coordinate name in error message * Don't check whether dependencies that are "None" are in the coords. * Review changes: check cube name in error and variable rename * Truncated variable for flake8 --- lib/iris/cube.py | 6 ++++++ lib/iris/tests/unit/cube/test_Cube.py | 27 ++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 9c42299c42..4bd947bca2 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -994,6 +994,12 @@ def add_aux_factory(self, aux_factory): if not isinstance(aux_factory, iris.aux_factory.AuxCoordFactory): raise TypeError('Factory must be a subclass of ' 'iris.aux_factory.AuxCoordFactory.') + cube_coords = self.coords() + for dependency in aux_factory.dependencies: + ref_coord = aux_factory.dependencies[dependency] + if ref_coord is not None and ref_coord not in cube_coords: + msg = "{} coordinate for factory is not present on cube {}" + raise ValueError(msg.format(ref_coord.name(), self.name())) self._aux_factories.append(aux_factory) def add_cell_measure(self, cell_measure, data_dims=None): diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index 5633417c80..dcc3eca7cb 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -34,9 +34,9 @@ import iris.aux_factory import iris.coords import iris.exceptions -from iris import FUTURE from iris.analysis import WeightedAggregator, Aggregator from iris.analysis import MEAN +from iris.aux_factory import HybridHeightFactory from iris.cube import Cube from iris.coords import AuxCoord, DimCoord, CellMeasure from iris.exceptions import (CoordinateNotFoundError, CellMeasureNotFoundError, @@ -1546,6 +1546,31 @@ def test_add_cell_measure(self): cube.add_cell_measure(a_cell_measure, [0, 1]) self.assertEqual(cube.cell_measure('area'), a_cell_measure) + def test_add_valid_aux_factory(self): + cube = Cube(np.arange(8).reshape(2, 2, 2)) + delta = AuxCoord(points=[0, 1], long_name='delta', units='m') + sigma = AuxCoord(points=[0, 1], long_name='sigma') + orog = AuxCoord(np.arange(4).reshape(2, 2), units='m') + cube.add_aux_coord(delta, 0) + cube.add_aux_coord(sigma, 0) + cube.add_aux_coord(orog, (1, 2)) + factory = HybridHeightFactory(delta=delta, sigma=sigma, orography=orog) + self.assertIsNone(cube.add_aux_factory(factory)) + + def test_error_for_add_invalid_aux_factory(self): + cube = Cube(np.arange(8).reshape(2, 2, 2), long_name='bar') + delta = AuxCoord(points=[0, 1], long_name='delta', units='m') + sigma = AuxCoord(points=[0, 1], long_name='sigma') + orog = AuxCoord(np.arange(4).reshape(2, 2), units='m', long_name='foo') + cube.add_aux_coord(delta, 0) + cube.add_aux_coord(sigma, 0) + # Note orography is not added to the cube here + factory = HybridHeightFactory(delta=delta, sigma=sigma, orography=orog) + expected_error = ("foo coordinate for factory is not present on cube " + "bar") + with self.assertRaisesRegexp(ValueError, expected_error): + cube.add_aux_factory(factory) + class Test_remove_metadata(tests.IrisTest): def setUp(self): From 25c5f4d8139784f796e2c0437d194ccfce63fb3c Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Thu, 4 Oct 2018 13:57:29 +0100 Subject: [PATCH 59/62] Correct cube collapse code --- lib/iris/cube.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 9c42299c42..26faf91eb4 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -3241,10 +3241,10 @@ def collapsed(self, coords, aggregator, **kwargs): # on the cube lazy array. # NOTE: do not reform the data in this case, as 'lazy_aggregate' # accepts multiple axes (unlike 'aggregate'). - collapse_axis = dims_to_collapse + collapse_axis = list(dims_to_collapse) try: data_result = aggregator.lazy_aggregate(self.lazy_data(), - collapse_axis, + axis=collapse_axis, **kwargs) except TypeError: # TypeError - when unexpected keywords passed through (such as From 79639509cc83f9920c3a9830015a5e745c35b762 Mon Sep 17 00:00:00 2001 From: tv3141 Date: Mon, 23 Jul 2018 11:04:52 +0100 Subject: [PATCH 60/62] add contact options to README removed gitter from contact list --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index a6e9dd525b..6dedfee592 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ + [Documentation](#documentation) + [Installation](#installation) + [Copyright and licence](#copyright-and-licence) ++ [Get in touch](#get-in-touch) + [Contributing](#contributing) [](TOC) @@ -93,6 +94,12 @@ The easiest way to install Iris is with [conda](https://conda.io/miniconda.html) Detailed instructions, including information on installing from source, are available in [INSTALL](INSTALL). +# Get in touch + + * Report bugs, or suggest new features using an Issue or Pull Request on [Github](https://github.com/SciTools/iris). You can also comment on existing Issues and Pull Requests. + * For discussions from a user perspective you could join our [SciTools User's Google Group](https://groups.google.com/forum/#!forum/scitools-iris). + * For those involved in developing Iris we also have a [Google Group](https://groups.google.com/forum/#!forum/scitools-iris-dev). + * [StackOverflow](https://stackoverflow.com/questions/tagged/python-iris) For "How do I?". # Copyright and licence From a6c14809d44d83f0e03cdc4c7eaf10277874a09b Mon Sep 17 00:00:00 2001 From: Corinne Bosley Date: Mon, 15 Oct 2018 11:37:37 +0100 Subject: [PATCH 61/62] expanded name of dev group --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6dedfee592..bfed315936 100644 --- a/README.md +++ b/README.md @@ -97,8 +97,8 @@ are available in [INSTALL](INSTALL). # Get in touch * Report bugs, or suggest new features using an Issue or Pull Request on [Github](https://github.com/SciTools/iris). You can also comment on existing Issues and Pull Requests. - * For discussions from a user perspective you could join our [SciTools User's Google Group](https://groups.google.com/forum/#!forum/scitools-iris). - * For those involved in developing Iris we also have a [Google Group](https://groups.google.com/forum/#!forum/scitools-iris-dev). + * For discussions from a user perspective you could join our [SciTools Users Google Group](https://groups.google.com/forum/#!forum/scitools-iris). + * For those involved in developing Iris we also have a [Iris Developers Google Group](https://groups.google.com/forum/#!forum/scitools-iris-dev). * [StackOverflow](https://stackoverflow.com/questions/tagged/python-iris) For "How do I?". # Copyright and licence From 9b61b24bd9f56e70411b72a86f8287d58d94d0c5 Mon Sep 17 00:00:00 2001 From: Corinne Bosley Date: Mon, 15 Oct 2018 11:40:22 +0100 Subject: [PATCH 62/62] added an n for better flow --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bfed315936..bfb1353102 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ are available in [INSTALL](INSTALL). * Report bugs, or suggest new features using an Issue or Pull Request on [Github](https://github.com/SciTools/iris). You can also comment on existing Issues and Pull Requests. * For discussions from a user perspective you could join our [SciTools Users Google Group](https://groups.google.com/forum/#!forum/scitools-iris). - * For those involved in developing Iris we also have a [Iris Developers Google Group](https://groups.google.com/forum/#!forum/scitools-iris-dev). + * For those involved in developing Iris we also have an [Iris Developers Google Group](https://groups.google.com/forum/#!forum/scitools-iris-dev). * [StackOverflow](https://stackoverflow.com/questions/tagged/python-iris) For "How do I?". # Copyright and licence