# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------------
# (C) British Crown Copyright 2017-2019 Met Office.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
""" Provides support utilities for checking cubes."""
import iris
import numpy as np
from iris.exceptions import CoordinateNotFoundError
[docs]def check_for_x_and_y_axes(cube, require_dim_coords=False):
"""
Check whether the cube has an x and y axis, otherwise raise an error.
Args:
cube (iris.cube.Cube):
Cube to be checked for x and y axes.
require_dim_coords (bool):
If true the x and y coordinates must be dimension coordinates.
Raises:
ValueError : Raise an error if non-uniform increments exist between
grid points.
"""
for axis in ["x", "y"]:
if require_dim_coords:
coord = cube.coords(axis=axis, dim_coords=True)
else:
coord = cube.coords(axis=axis)
if coord:
pass
else:
msg = ("The cube does not contain the expected {}"
"coordinates.".format(axis))
raise ValueError(msg)
[docs]def check_cube_coordinates(cube, new_cube, exception_coordinates=None):
"""Find and promote to dimension coordinates any scalar coordinates in
new_cube that were originally dimension coordinates in the progenitor
cube. If coordinate is in new_cube that is not in the old cube, keep
coordinate in its current position.
Args:
cube (iris.cube.Cube):
The input cube that will be checked to identify the preferred
coordinate order for the output cube.
new_cube (iris.cube.Cube):
The cube that must be checked and adjusted using the coordinate
order from the original cube.
exception_coordinates (list of str or None):
The names of the coordinates that are permitted to be within the
new_cube but are not available within the original cube.
Returns:
iris.cube.Cube:
Modified cube with relevant scalar coordinates promoted to
dimension coordinates with the dimension coordinates re-ordered,
as best as can be done based on the original cube.
Raises:
CoordinateNotFoundError : Raised if the final dimension
coordinates of the returned cube do not match the input cube.
CoordinateNotFoundError : If a coordinate is within in the permitted
exceptions but is not in the new_cube.
"""
if exception_coordinates is None:
exception_coordinates = []
# Promote available and relevant scalar coordinates
cube_dim_names = [coord.name() for coord in cube.dim_coords]
for coord in new_cube.aux_coords[::-1]:
if coord.name() in cube_dim_names:
new_cube = iris.util.new_axis(new_cube, coord)
new_cube_dim_names = [coord.name() for coord in new_cube.dim_coords]
# If we have the wrong number of dimensions then raise an error.
if (len(cube.dim_coords)+len(exception_coordinates) !=
len(new_cube.dim_coords)):
msg = ('The number of dimension coordinates within the new cube '
'do not match the number of dimension coordinates within the '
'original cube plus the number of exception coordinates. '
'\n input cube dimensions {}, new cube dimensions {}'.format(
cube_dim_names, new_cube_dim_names))
raise CoordinateNotFoundError(msg)
# Ensure dimension order matches
new_cube_dimension_order = {coord.name(): new_cube.coord_dims(
coord.name())[0] for coord in new_cube.dim_coords}
correct_order = []
new_cube_only_dims = []
for coord_name in cube_dim_names:
correct_order.append(new_cube_dimension_order[coord_name])
for coord_name in exception_coordinates:
try:
new_coord_dim = new_cube.coord_dims(coord_name)[0]
new_cube_only_dims.append(new_coord_dim)
except CoordinateNotFoundError:
msg = ("All permitted exception_coordinates must be on the"
" new_cube. In this case, coordinate {0} within the list "
"of permitted exception_coordinates ({1}) is not available"
" on the new_cube.").format(
coord_name, exception_coordinates)
raise CoordinateNotFoundError(msg)
correct_order = np.array(correct_order)
for dim in new_cube_only_dims:
correct_order = np.insert(correct_order, dim, dim)
new_cube.transpose(correct_order)
return new_cube
[docs]def find_dimension_coordinate_mismatch(
first_cube, second_cube, two_way_mismatch=True):
"""Determine if there is a mismatch between the dimension coordinates in
two cubes.
Args:
first_cube (iris.cube.Cube):
First cube to compare.
second_cube (iris.cube.Cube):
Second cube to compare.
two_way_mismatch (Logical):
If True, a two way mismatch is calculated e.g.
second_cube - first_cube AND
first_cube - second_cube
If False, a one way mismatch is calculated e.g.
second_cube - first_cube
Returns:
list of str:
List of the dimension coordinates that are only present in
one out of the two cubes.
"""
first_dim_names = [coord.name() for coord in first_cube.dim_coords]
second_dim_names = [coord.name() for coord in second_cube.dim_coords]
if two_way_mismatch:
mismatch = (list(set(second_dim_names) - set(first_dim_names)) +
list(set(first_dim_names) - set(second_dim_names)))
else:
mismatch = list(set(second_dim_names) - set(first_dim_names))
return mismatch
[docs]def spatial_coords_match(first_cube, second_cube):
"""
Determine if the x and y coords in the two cubes are the same.
Args:
first_cube (iris.cube.Cube):
First cube to compare.
second_cube (iris.cube.Cube):
Second cube to compare.
Returns:
bool:
True if the x and y coords are the exactly the same to the
precision of the floating-point values (this should be true for
any cubes derived using cube.regrid()), otherwise False.
"""
return (first_cube.coord(axis='x') == second_cube.coord(axis='x') and
first_cube.coord(axis='y') == second_cube.coord(axis='y'))
[docs]def time_coords_match(first_cube, second_cube, raise_exception=False):
"""
Determine if two cubes have equivalent time, forecast_period, and
forecast_reference_time points.
Args:
first_cube (iris.cube.Cube):
First cube to compare.
second_cube (iris.cube.Cube):
Second cube to compare.
raise_exception (bool):
By default this function returns a boolean, but if this argument is
set to True it will raise an exception if there is a mismatch in
the coordinates.
Returns:
bool:
True if the cube time coordinates are equivalent, False if they are
not.
Raised:
ValueError: The two cubes are not equivalent.
CoordinateNotFoundError: One of the expected temporal coordinates is
not present on one or more cubes.
"""
cubes_equivalent = True
mismatches = []
for coord_name in ["forecast_period", "time", "forecast_reference_time"]:
if (first_cube.coord(coord_name) != second_cube.coord(coord_name)):
mismatches.append(coord_name)
cubes_equivalent = False
if mismatches and raise_exception:
msg = "The following coordinates of the two cubes do not match: {}"
raise ValueError(msg.format(', '.join(mismatches)))
return cubes_equivalent