Esempio n. 1
0
def differentiate(cube, coord_to_differentiate):
    r"""
    Calculate the differential of a given cube with respect to the coord_to_differentiate.
    
    Args:

    * coord_to_differentiate:
        Either a Coord instance or the unique name of a coordinate which exists in the cube.
        If a Coord instance is provided, it does not necessarily have to exist on the cube.

    Example usage::
    
        u_wind_acceleration = differentiate(u_wind_cube, 'forecast_time')

    The algorithm used is equivalent to:
    
    .. math::
    
        d_i = \frac{v_{i+1}-v_i}{c_{i+1}-c_i}
    
    Where ``d`` is the differential, ``v`` is the data value, ``c`` is the coordinate value and ``i`` is the index in the differential
    direction. Hence, in a normal situation if a cube has a shape (x: n; y: m) differentiating with respect to x will result in a cube
    of shape (x: n-1; y: m) and differentiating with respect to y will result in (x: n; y: m-1). If the coordinate to differentiate is
    :attr:`circular <iris.coords.DimCoord.circular>` then the resultant shape will be the same as the input cube. 
    

    .. note:: Difference method used is the same as :func:`cube_delta` and therefore has the same limitations.
    
    .. note:: Spherical differentiation does not occur in this routine.

    """
    # Get the delta cube in the required differential direction. Don't add this to the resultant
    # cube's history as we will do that ourself.
    # This operation results in a copy of the original cube.
    delta_cube = cube_delta(cube, coord_to_differentiate, update_history=False)

    if isinstance(coord_to_differentiate, basestring):
        coord = cube.coord(coord_to_differentiate)
    else:
        coord = coord_to_differentiate

    delta_coord = _construct_delta_coord(coord)
    delta_dim = cube.coord_dims(coord)[0]

    # calculate delta_cube / delta_coord to give the differential. Don't update the history, as we will
    # do this ourself.
    delta_cube = iris.analysis.maths.divide(delta_cube,
                                            delta_coord,
                                            delta_dim,
                                            update_history=False)

    # Update the history of the new cube
    delta_cube.add_history('differential of %s wrt to %s' %
                           (cube.name(), coord.name()))

    # Update the standard name
    delta_cube.rename(
        ('derivative_of_%s_wrt_%s' % (cube.name(), coord.name())))
    return delta_cube
Esempio n. 2
0
def differentiate(cube, coord_to_differentiate):
    r"""
    Calculate the differential of a given cube with respect to the coord_to_differentiate.
    
    Args:

    * coord_to_differentiate:
        Either a Coord instance or the unique name of a coordinate which exists in the cube.
        If a Coord instance is provided, it does not necessarily have to exist on the cube.

    Example usage::
    
        u_wind_acceleration = differentiate(u_wind_cube, 'forecast_time')

    The algorithm used is equivalent to:
    
    .. math::
    
        d_i = \frac{v_{i+1}-v_i}{c_{i+1}-c_i}
    
    Where ``d`` is the differential, ``v`` is the data value, ``c`` is the coordinate value and ``i`` is the index in the differential
    direction. Hence, in a normal situation if a cube has a shape (x: n; y: m) differentiating with respect to x will result in a cube
    of shape (x: n-1; y: m) and differentiating with respect to y will result in (x: n; y: m-1). If the coordinate to differentiate is
    :attr:`circular <iris.coords.DimCoord.circular>` then the resultant shape will be the same as the input cube. 
    

    .. note:: Difference method used is the same as :func:`cube_delta` and therefore has the same limitations.
    
    .. note:: Spherical differentiation does not occur in this routine.

    """
    # Get the delta cube in the required differential direction. Don't add this to the resultant
    # cube's history as we will do that ourself.
    # This operation results in a copy of the original cube.
    delta_cube = cube_delta(cube, coord_to_differentiate, update_history=False)
    
    if isinstance(coord_to_differentiate, basestring):
        coord = cube.coord(coord_to_differentiate)
    else:
        coord = coord_to_differentiate
    
    delta_coord = _construct_delta_coord(coord)
    delta_dim = cube.coord_dims(coord)[0]

    # calculate delta_cube / delta_coord to give the differential. Don't update the history, as we will
    # do this ourself.
    delta_cube = iris.analysis.maths.divide(delta_cube, delta_coord, delta_dim,
                                            update_history=False)

    # Update the history of the new cube
    delta_cube.add_history('differential of %s wrt to %s' % (cube.name(), coord.name()) )
    
    # Update the standard name
    delta_cube.rename(('derivative_of_%s_wrt_%s' % (cube.name(), coord.name())) )
    return delta_cube
Esempio n. 3
0
def exp(cube, update_history=True, in_place=False):
    """
    Calculate the exponential (exp(x)) of the cube.

    Args:

    * cube:
        An instance of :class:`iris.cube.Cube`.

    .. note::

        Taking an exponential will return a cube with dimensionless units.

    Kwargs:

    * update_history:
        Whether to add an entry into the resulting cube's "history" attribute.
    * in_place:
        Whether to create a new Cube, or alter the given "cube".

    Returns:
        An instance of :class:`iris.cube.Cube`.

    """
    return _math_op_common(cube, np.exp, iris.unit.Unit('1'),
                           history='exp(%s)' % (cube.name()),
                           update_history=update_history, in_place=in_place)
Esempio n. 4
0
def category_map(*args):
    """Create a category map from multiple binary cubes

    Args:
        *args: Any number of :py:class:`iris.cube.Cube`'s with a 0/1 mapping

    Returns:
        iris.cube.Cube: A single cube with data 0-N, where N is the number of
        cubes input. The data then represents the cube number which is True at
        that location. A `names` attribute, which is a dictionary mapping of
        numbers to cube names, is also added to the cube.
    """
    names = {}

    # Copy the shape and coordinates from one of the cubes
    mapping = args[0].copy()
    mapping.data[:] = 0

    # For each cube set a number for it's mapping
    for n, cube in enumerate(args):
        names[n] = cube.name()
        mapping.data += n * cube.data

    # Save the key mapping numbers to names
    mapping.attributes['names'] = names

    return mapping
Esempio n. 5
0
def exp(cube, update_history=True, in_place=False):
    """
    Calculate the exponential (exp(x)) of the cube.

    Args:

    * cube:
        An instance of :class:`iris.cube.Cube`.

    .. note::

        Taking an exponential will return a cube with dimensionless units.

    Kwargs:

    * update_history:
        Whether to add an entry into the resulting cube's "history" attribute.
    * in_place:
        Whether to create a new Cube, or alter the given "cube".

    Returns:
        An instance of :class:`iris.cube.Cube`.

    """
    return _math_op_common(cube, np.exp, iris.unit.Unit('1'),
                           history='exp(%s)' % (cube.name()),
                           update_history=update_history, in_place=in_place)
Esempio n. 6
0
    def _get_horizontal_coord(cube, axis):
        """
        Gets the horizontal coordinate on the supplied cube along the
        specified axis.

        Args:

        * cube:
            An instance of :class:`iris.cube.Cube`.
        * axis:
            Locate coordinates on `cube` along this axis.

        Returns:
            The horizontal coordinate on the specified axis of the supplied
            cube.

        """
        coords = cube.coords(axis=axis, dim_coords=False)
        if len(coords) != 1:
            raise ValueError(
                "Cube {!r} must contain a single 1D {} "
                "coordinate.".format(cube.name()),
                axis,
            )
        return coords[0]
Esempio n. 7
0
def spatial_vectors_with_phenom_name(i_cube, j_cube, k_cube=None):
    """
    Given 2 or 3 spatially dependent cubes, return a list of the spatial coordinate names with appropriate phenomenon name.

    This routine is designed to identify the vector quantites which each of the cubes provided represent
    and return a list of their 3d spatial dimension names and associated phenomenon.
    For example, given a cube of "u wind" and "v wind" the return value would be (['u', 'v', 'w'], 'wind')::

        >>> spatial_vectors_with_phenom_name(u_wind_cube, v_wind_cube) #doctest: +SKIP
        (['u', 'v', 'w'], 'wind')

    """
    directional_names = (
        ("u", "v", "w"),
        ("x", "y", "z"),
        ("i", "j", "k"),
        ("eastward", "northward", "upward"),
        ("easterly", "northerly", "vertical"),
        ("easterly", "northerly", "radial"),
    )

    # Create a list of the standard_names of our incoming cubes (excluding the k_cube if it is None)
    cube_standard_names = [cube.name() for cube in (i_cube, j_cube, k_cube) if cube is not None]

    # Define a regular expr which represents (direction, phenomenon) from the standard name of a cube
    # e.g from "w wind" -> ("w", "wind")
    vector_qty = re.compile(r"([^\W_]+)[\W_]+(.*)")

    # Make a dictionary of {direction: phenomenon quantity}
    cube_directions, cube_phenomena = zip(
        *[re.match(vector_qty, std_name).groups() for std_name in cube_standard_names]
    )

    # Check that there is only one distinct phenomenon
    if len(set(cube_phenomena)) != 1:
        raise ValueError(
            "Vector phenomenon name not consistent between vector cubes. Got "
            "cube phenomena: %s; from standard names: %s." % (", ".join(cube_phenomena), ", ".join(cube_standard_names))
        )

    # Get the appropriate direction list from the cube_directions we have got from the standard name
    direction = None
    for possible_direction in directional_names:
        # if this possible direction (minus the k_cube if it is none) matches direction from the given cubes use it.
        if possible_direction[0 : len(cube_directions)] == cube_directions:
            direction = possible_direction

    # If we didn't get a match, raise an Exception
    if direction is None:
        direction_string = "; ".join((", ".join(possible_direction) for possible_direction in directional_names))
        raise ValueError(
            "%s are not recognised vector cube_directions. Possible cube_directions are: %s."
            % (cube_directions, direction_string)
        )

    return (direction, cube_phenomena[0])
Esempio n. 8
0
def spatial_vectors_with_phenom_name(i_cube, j_cube, k_cube=None):
    """
    Given 2 or 3 spatially dependent cubes, return a list of the spatial coordinate names with appropriate phenomenon name.

    This routine is designed to identify the vector quantites which each of the cubes provided represent
    and return a list of their 3d spatial dimension names and associated phenomenon.
    For example, given a cube of "u wind" and "v wind" the return value would be (['u', 'v', 'w'], 'wind')::

        >>> spatial_vectors_with_phenom_name(u_wind_cube, v_wind_cube) #doctest: +SKIP
        (['u', 'v', 'w'], 'wind')

    """
    directional_names = (('u', 'v', 'w'), ('x', 'y', 'z'), ('i', 'j', 'k'),
                         ('eastward', 'northward',
                          'upward'), ('easterly', 'northerly', 'vertical'),
                         ('easterly', 'northerly', 'radial'))

    # Create a list of the standard_names of our incoming cubes (excluding the k_cube if it is None)
    cube_standard_names = [
        cube.name() for cube in (i_cube, j_cube, k_cube) if cube is not None
    ]

    # Define a regular expr which represents (direction, phenomenon) from the standard name of a cube
    # e.g from "w wind" -> ("w", "wind")
    vector_qty = re.compile(r'([^\W_]+)[\W_]+(.*)')

    # Make a dictionary of {direction: phenomenon quantity}
    cube_directions, cube_phenomena = zip(*[
        re.match(vector_qty, std_name).groups()
        for std_name in cube_standard_names
    ])

    # Check that there is only one distinct phenomenon
    if len(set(cube_phenomena)) != 1:
        raise ValueError('Vector phenomenon name not consistent between vector cubes. Got '
                         'cube phenomena: %s; from standard names: %s.' % \
                         (', '.join(cube_phenomena), ', '.join(cube_standard_names))
                         )

    # Get the appropriate direction list from the cube_directions we have got from the standard name
    direction = None
    for possible_direction in directional_names:
        # if this possible direction (minus the k_cube if it is none) matches direction from the given cubes use it.
        if possible_direction[0:len(cube_directions)] == cube_directions:
            direction = possible_direction

    # If we didn't get a match, raise an Exception
    if direction is None:
        direction_string = '; '.join(
            (', '.join(possible_direction)
             for possible_direction in directional_names))
        raise ValueError('%s are not recognised vector cube_directions. Possible cube_directions are: %s.' % \
                         (cube_directions, direction_string) )

    return (direction, cube_phenomena[0])
Esempio n. 9
0
def _get_xy_dim_coords(cube):
    """
    Return the x and y dimension coordinates from a cube.

    This function raises a ValueError if the cube does not contain one and
    only one set of x and y dimension coordinates. It also raises a ValueError
    if the identified x and y coordinates do not have coordinate systems that
    are equal.

    Args:

    * cube:
        An instance of :class:`iris.cube.Cube`.

    Returns:
        A tuple containing the cube's x and y dimension coordinates.

    """
    x_coords = cube.coords(axis='x', dim_coords=True)
    if len(x_coords) != 1:
        raise ValueError('Cube {!r} must contain a single 1D x '
                         'coordinate.'.format(cube.name()))
    x_coord = x_coords[0]

    y_coords = cube.coords(axis='y', dim_coords=True)
    if len(y_coords) != 1:
        raise ValueError('Cube {!r} must contain a single 1D y '
                         'coordinate.'.format(cube.name()))
    y_coord = y_coords[0]

    if x_coord.coord_system != y_coord.coord_system:
        raise ValueError("The cube's x ({!r}) and y ({!r}) "
                         "coordinates must have the same coordinate "
                         "system.".format(x_coord.name(), y_coord.name()))

    return x_coord, y_coord
Esempio n. 10
0
def _cube_netcdf_variable_name(cube):
    """
    Returns a CF-netCDF variable name for the given cube.

    Args:

    * cube (class:`iris.cube.Cube`):
        An instance of a cube for which a CF-netCDF variable
        name is required.

    Returns:
        A CF-netCDF variable name as a string.

    """
    if cube.var_name is not None:
        cf_name = cube.var_name
    else:
        # Convert to lower case and replace whitespace by underscores.
        cf_name = '_'.join(cube.name().lower().split())

    return cf_name
Esempio n. 11
0
    def _get_cube_variable_name(self, cube):
        """
        Returns a CF-netCDF variable name for the given cube.

        Args:

        * cube (class:`iris.cube.Cube`):
            An instance of a cube for which a CF-netCDF variable
            name is required.

        Returns:
            A CF-netCDF variable name as a string.

        """
        if cube.var_name is not None:
            cf_name = cube.var_name
        else:
            # Convert to lower case and replace whitespace by underscores.
            cf_name = '_'.join(cube.name().lower().split())

        return cf_name
Esempio n. 12
0
def remap_3d(cube, target, vert_coord=None):
    """Remap one cube on to the target mapping

    Args:
        cube (iris.cube.Cube): The cube to be re-mapped

        target (iris.cube.Cube): The cube to re-map to

        vert_coord (str, optional): The name of the coordinate for the vertical
            re-mapping to be done on. Default is None and will use the DimCoord
            for the z-axis

    Returns:
        iris.cube.Cube:
    """
    # Regrid in the horizontal
    cube = cube.regrid(target, iris.analysis.Linear())

    # Interpolate in the vertical
    if vert_coord is None:
        z = target.coord(axis='z', dim_coords=True)
    else:
        z = target.coord(vert_coord)
    cube = cube.interpolate([(z.name(), z.points)], iris.analysis.Linear())

    # Match coordinate information
    newcube = target.copy(data=cube.data)
    newcube.rename(cube.name())
    newcube.units = cube.units

    # Put back correct time information
    for coord in newcube.aux_coords:
        if iris.util.guess_coord_axis(coord) == 'T':
            newcube.remove_coord(coord)

    for coord in cube.aux_coords:
        if iris.util.guess_coord_axis(coord) == 'T':
            newcube.add_aux_coord(coord)

    return newcube
Esempio n. 13
0
    def _get_horizontal_coord(cube, axis):
        """
        Gets the horizontal coordinate on the supplied cube along the
        specified axis.

        Args:

        * cube:
            An instance of :class:`iris.cube.Cube`.
        * axis:
            Locate coordinates on `cube` along this axis.

        Returns:
            The horizontal coordinate on the specified axis of the supplied
            cube.

        """
        coords = cube.coords(axis=axis, dim_coords=False)
        if len(coords) != 1:
            raise ValueError('Cube {!r} must contain a single 1D {} '
                             'coordinate.'.format(cube.name()), axis)
        return coords[0]
Esempio n. 14
0
def spatial_vectors_with_phenom_name(i_cube, j_cube, k_cube=None):
    """
    Given 2 or 3 spatially dependent cubes, return a list of the spatial
    coordinate names with appropriate phenomenon name.

    This routine is designed to identify the vector quantites which each
    of the cubes provided represent and return a list of their 3d
    spatial dimension names and associated phenomenon.
    For example, given a cube of "u wind" and "v wind" the return value
    would be (['u', 'v', 'w'], 'wind')::

        >>> spatial_vectors_with_phenom_name(u_wind_cube, v_wind_cube) \
#doctest: +SKIP
        (['u', 'v', 'w'], 'wind')

    """
    directional_names = (
        ("u", "v", "w"),
        ("x", "y", "z"),
        ("i", "j", "k"),
        ("eastward", "northward", "upward"),
        ("easterly", "northerly", "vertical"),
        ("easterly", "northerly", "radial"),
    )

    # Create a list of the standard_names of our incoming cubes
    # (excluding the k_cube if it is None).
    cube_standard_names = [
        cube.name() for cube in (i_cube, j_cube, k_cube) if cube is not None
    ]

    # Define a regular expr which represents (direction, phenomenon)
    # from the standard name of a cube.
    # e.g from "w wind" -> ("w", "wind")
    vector_qty = re.compile(r"([^\W_]+)[\W_]+(.*)")

    # Make a dictionary of {direction: phenomenon quantity}
    cube_directions, cube_phenomena = zip(
        *[
            re.match(vector_qty, std_name).groups()
            for std_name in cube_standard_names
        ]
    )

    # Check that there is only one distinct phenomenon
    if len(set(cube_phenomena)) != 1:
        raise ValueError(
            "Vector phenomenon name not consistent between "
            "vector cubes. Got cube phenomena: {}; from "
            "standard names: {}.".format(
                ", ".join(cube_phenomena), ", ".join(cube_standard_names)
            )
        )

    # Get the appropriate direction list from the cube_directions we
    # have got from the standard name.
    direction = None
    for possible_direction in directional_names:
        # If this possible direction (minus the k_cube if it is none)
        # matches direction from the given cubes use it.
        if possible_direction[0 : len(cube_directions)] == cube_directions:
            direction = possible_direction

    # If we didn't get a match, raise an Exception
    if direction is None:
        direction_string = "; ".join(
            ", ".join(possible_direction)
            for possible_direction in directional_names
        )
        raise ValueError(
            "{} are not recognised vector cube_directions. "
            "Possible cube_directions are: {}.".format(
                cube_directions, direction_string
            )
        )

    return (direction, cube_phenomena[0])
Esempio n. 15
0
def _create_cf_data_variable(dataset, cube, dimension_names):
    """
    Create CF-netCDF data variable for the cube and any associated grid mapping.
    
    Args:
    
    * dataset (:class:`netCDF4.Dataset`):
        The CF-netCDF data file being created.
    * cube (:class:`iris.cube.Cube`):
        The associated cube being saved to CF-netCDF file.
    * dimension_names:
        List of string names for each dimension of the cube.
        
    Returns:
        The newly created CF-netCDF data variable. 
    
    """
    cf_name = cube.name()
    
    # Determine whether there is a cube MDI value.
    fill_value = None
    if isinstance(cube.data, np.ma.core.MaskedArray):
        fill_value = cube.data.fill_value
        
    # Create the cube CF-netCDF data variable with data payload.
    cf_var = dataset.createVariable(cf_name, cube.data.dtype, dimension_names, fill_value=fill_value)
    cf_var[:] = cube.data
    
    if cube.standard_name:
    	cf_var.standard_name = cube.standard_name

    if cube.long_name:
        cf_var.long_name = cube.long_name

    if cube.units != 'unknown':
        cf_var.units = str(cube.units)

    # Add any other cube attributes as CF-netCDF data variable attributes.
    for attr_name in sorted(cube.attributes):
        value = cube.attributes[attr_name]
        
        if attr_name == 'STASH':
            # Adopting provisional Metadata Conventions for representing MO Scientific Data encoded in NetCDF Format.
            attr_name = 'ukmo__um_stash_source'
            value = str(value)

        if attr_name == "ukmo__process_flags":
            value = " ".join([x.replace(" ", "_") for x in value])

        if attr_name.lower() != 'conventions':
           setattr(cf_var, attr_name, value)

    # Declare the CF conventions versions.
    dataset.Conventions = _CF_CONVENTIONS_VERSION

    # Create the CF-netCDF data variable cell method attribute.
    cell_methods = _create_cf_cell_methods(cube, dimension_names)
    
    if cell_methods:
        cf_var.cell_methods = cell_methods
    
    # Create the CF-netCDF grid mapping.
    _create_cf_grid_mapping(dataset, cube, cf_var)

    return cf_var
Esempio n. 16
0
def _get_xy_coords(cube):
    """
    Return the x and y coordinates from a cube.

    This function will preferentially return a pair of dimension
    coordinates (if there are more than one potential x or y dimension
    coordinates a ValueError will be raised). If the cube does not have
    a pair of x and y dimension coordinates it will return 1D auxiliary
    coordinates (including scalars). If there is not one and only one set
    of x and y auxiliary coordinates a ValueError will be raised.

    Having identified the x and y coordinates, the function checks that they
    have equal coordinate systems and that they do not occupy the same
    dimension on the cube.

    Args:

    * cube:
        An instance of :class:`iris.cube.Cube`.

    Returns:
        A tuple containing the cube's x and y coordinates.

    """
    # Look for a suitable dimension coords first.
    x_coords = cube.coords(axis='x', dim_coords=True)
    if not x_coords:
        # If there is no x coord in dim_coords look for scalars or
        # monotonic coords in aux_coords.
        x_coords = [
            coord for coord in cube.coords(axis='x', dim_coords=False)
            if coord.ndim == 1 and coord.is_monotonic()
        ]
    if len(x_coords) != 1:
        raise ValueError('Cube {!r} must contain a single 1D x '
                         'coordinate.'.format(cube.name()))
    x_coord = x_coords[0]

    # Look for a suitable dimension coords first.
    y_coords = cube.coords(axis='y', dim_coords=True)
    if not y_coords:
        # If there is no y coord in dim_coords look for scalars or
        # monotonic coords in aux_coords.
        y_coords = [
            coord for coord in cube.coords(axis='y', dim_coords=False)
            if coord.ndim == 1 and coord.is_monotonic()
        ]
    if len(y_coords) != 1:
        raise ValueError('Cube {!r} must contain a single 1D y '
                         'coordinate.'.format(cube.name()))
    y_coord = y_coords[0]

    if x_coord.coord_system != y_coord.coord_system:
        raise ValueError("The cube's x ({!r}) and y ({!r}) "
                         "coordinates must have the same coordinate "
                         "system.".format(x_coord.name(), y_coord.name()))

    # The x and y coordinates must describe different dimensions
    # or be scalar coords.
    x_dims = cube.coord_dims(x_coord)
    x_dim = None
    if x_dims:
        x_dim = x_dims[0]

    y_dims = cube.coord_dims(y_coord)
    y_dim = None
    if y_dims:
        y_dim = y_dims[0]

    if x_dim is not None and y_dim == x_dim:
        raise ValueError("The cube's x and y coords must not describe the "
                         "same data dimension.")

    return x_coord, y_coord
Esempio n. 17
0
def _multiply_divide_common(operation_function, operation_symbol, operation_noun,
                            cube, other, dim=None, update_history=True):
    """
    Function which shares common code between multiplication and division of cubes.

    operation_function   - function which does the operation (e.g. numpy.divide)
    operation_symbol     - the textual symbol of the operation (e.g. '/')
    operation_noun       - the noun of the operation (e.g. 'division')
    operation_past_tense - the past tesnse of the operation (e.g. 'divided')

    .. seealso:: For information on the dim keyword argument see :func:`multiply`.

    """
    if not isinstance(cube, iris.cube.Cube):
        raise TypeError('The "cube" argument must be an instance of iris.Cube.')

    if isinstance(other, (int, float)):
        other = np.array(other)

    other_unit = None
    history = None

    if isinstance(other, np.ndarray):
        _assert_compatible(cube, other)

        copy_cube = cube.copy(data=operation_function(cube.data, other))

        if update_history:
            if other.ndim == 0:
                history = '%s %s %s' % (cube.name(), operation_symbol, other)
            else:
                history = '%s %s array' % (cube.name(), operation_symbol)

        other_unit = '1'
    elif isinstance(other, iris.coords.Coord):
        # Deal with cube multiplication/division by coordinate

        # What dimension are we processing?
        data_dimension = None
        if dim is not None:
            # Ensure the given dim matches the coord
            if other in cube.coords() and cube.coord_dims(other) != [dim]:
                raise ValueError("dim provided does not match dim found for coord")
            data_dimension = dim
        else:
            # Try and get a coord dim
            if other.shape != (1,):
                try:
                    coord_dims = cube.coord_dims(other)
                    data_dimension = coord_dims[0] if coord_dims else None
                except iris.exceptions.CoordinateNotFoundError:
                    raise ValueError("Could not determine dimension for mul/div. Use mul(coord, dim=dim)")

        if other.ndim != 1:
            raise iris.exceptions.CoordinateMultiDimError(other)

        if other.has_bounds():
            warnings.warn('%s by a bounded coordinate not well defined, ignoring bounds.' % operation_noun)

        points = other.points

        # If the axis is defined then shape the provided points so that we can do the
        # division (this is needed as there is no "axis" keyword to numpy's divide/multiply)
        if data_dimension is not None:
            points_shape = [1] * cube.data.ndim
            points_shape[data_dimension] = -1
            points = points.reshape(points_shape)

        copy_cube = cube.copy(data=operation_function(cube.data, points))

        if update_history:
            history = '%s %s %s' % (cube.name(), operation_symbol, other.name())

        other_unit = other.units
    elif isinstance(other, iris.cube.Cube):
        # Deal with cube multiplication/division by cube
        copy_cube = cube.copy(data=operation_function(cube.data, other.data))

        if update_history:
            history = '%s %s %s' % (cube.name() or 'unknown', operation_symbol,
                                    other.name() or 'unknown')

        other_unit = other.units
    else:
        return NotImplemented

    # Update the units
    if operation_function == np.multiply:
        copy_cube.units = cube.units * other_unit
    elif operation_function == np.divide:
        copy_cube.units = cube.units / other_unit

    iris.analysis.clear_phenomenon_identity(copy_cube)

    if history is not None:
        copy_cube.add_history(history)

    return copy_cube
Esempio n. 18
0
def _add_subtract_common(operation_function, operation_symbol, operation_noun, operation_past_tense,
                         cube, other, dim=None, ignore=True, update_history=True, in_place=False):
    """
    Function which shares common code between addition and subtraction of cubes.

    operation_function   - function which does the operation (e.g. numpy.subtract)
    operation_symbol     - the textual symbol of the operation (e.g. '-')
    operation_noun       - the noun of the operation (e.g. 'subtraction')
    operation_past_tense - the past tense of the operation (e.g. 'subtracted')

    """
    if not isinstance(cube, iris.cube.Cube):
        raise TypeError('The "cube" argument must be an instance of iris.Cube.')

    if isinstance(other, (int, float)):
        # Promote scalar to a coordinate and associate unit type with cube unit type
        other = np.array(other)

    # Check that the units of the cube and the other item are the same, or if the other does not have a unit, skip this test
    if cube.units != getattr(other, 'units', cube.units) :
        raise iris.exceptions.NotYetImplementedError('Differing units (%s & %s) %s not implemented' % \
                                                     (cube.units, other.units, operation_noun))

    history = None

    if isinstance(other, np.ndarray):
        _assert_compatible(cube, other)

        if in_place:
            new_cube = cube
            operation_function(new_cube.data, other, new_cube.data)
        else:
            new_cube = cube.copy(data=operation_function(cube.data, other))

        if update_history:
            if other.ndim == 0:
                history = '%s %s %s' % (cube.name(), operation_symbol, other)
            else:
                history = '%s %s array' % (cube.name(), operation_symbol)
    elif isinstance(other, iris.coords.Coord):
        # Deal with cube addition/subtraction by coordinate

        # What dimension are we processing?
        data_dimension = None
        if dim is not None:
            # Ensure the given dim matches the coord
            if other in cube.coords() and cube.coord_dims(other) != [dim]:
                raise ValueError("dim provided does not match dim found for coord")
            data_dimension = dim
        else:
            # Try and get a coord dim
            if other.shape != (1,):
                try:
                    coord_dims = cube.coord_dims(other)
                    data_dimension = coord_dims[0] if coord_dims else None
                except iris.exceptions.CoordinateNotFoundError:
                    raise ValueError("Could not determine dimension for add/sub. Use add(coord, dim=dim)")

        if other.ndim != 1:
            raise iris.exceptions.CoordinateMultiDimError(other)

        if other.has_bounds():
            warnings.warn('%s by a bounded coordinate not well defined, ignoring bounds.' % operation_noun)

        points = other.points

        if data_dimension is not None:
            points_shape = [1] * cube.data.ndim
            points_shape[data_dimension] = -1
            points = points.reshape(points_shape)

        if in_place:
            new_cube = cube
            operation_function(new_cube.data, points, new_cube.data)
        else:
            new_cube = cube.copy(data=operation_function(cube.data, points))

        if update_history:
            history = '%s %s %s (coordinate)' % (cube.name(), operation_symbol, other.name())
    elif isinstance(other, iris.cube.Cube):
        # Deal with cube addition/subtraction by cube

        # get a coordinate comparison of this cube and the cube to do the operation with
        coord_comp = iris.analysis.coord_comparison(cube, other)

        if coord_comp['transposable']:
            raise ValueError('Cubes cannot be %s, differing axes. '
                                 'cube.transpose() may be required to re-order the axes.' % operation_past_tense)

        # provide a deprecation warning if the ignore keyword has been set
        if ignore is not True:
            warnings.warn('The "ignore" keyword has been deprecated in add/subtract. This functionality is now automatic. '
                          'The provided value to "ignore" has been ignored, and has been automatically calculated.')

        bad_coord_grps = (coord_comp['ungroupable_and_dimensioned'] + coord_comp['resamplable'])
        if bad_coord_grps:
            raise ValueError('This operation cannot be performed as there are differing coordinates (%s) remaining '
                             'which cannot be ignored.' % ', '.join({coord_grp.name() for coord_grp in bad_coord_grps}))

        if in_place:
            new_cube = cube
            operation_function(new_cube.data, other.data, new_cube.data)
        else:
            new_cube = cube.copy(data=operation_function(cube.data, other.data))

        # If a coordinate is to be ignored - remove it
        ignore = filter(None, [coord_grp[0] for coord_grp in coord_comp['ignorable']])
        if not ignore:
            ignore_string = ''
        else:
            ignore_string = ' (ignoring %s)' % ', '.join([coord.name() for coord in ignore])
        for coord in ignore:
            new_cube.remove_coord(coord)

        if update_history:
            history = '%s %s %s%s' % (cube.name() or 'unknown', operation_symbol,
                                      other.name() or 'unknown', ignore_string)

    else:
        return NotImplemented

    iris.analysis.clear_phenomenon_identity(new_cube)

    if history is not None:
        new_cube.add_history(history)

    return new_cube
Esempio n. 19
0
def to_level(cube, order=0, **kwargs):
    """ Interpolates to the vertical co-ordinate level

    Args:
        cube (iris.cube.Cube):

        order (int): Order of interpolation. Currently only supports linear (1).

        **kwargs: Provides the coordinate value pair to be interpolated to. Must
            be specified as a list.

            e.g. to interpolate a cube onto a vertical surface of 1000m

            >>> to_level(cube, altitude=[1000])

    Returns:
        iris.cube.Cube: A cube interpolated onto the new vertical co-ordinate.
        Has the same properties as the input cube but with new vertical
        co-ordinates
    """
    if len(kwargs) > 1:
        raise Exception('Can only specify a single vertical co-ordinate')

    # Extract the specified output co-ordinate information
    coord_name = list(kwargs)[0]
    coord_in = cube.coord(coord_name)
    coord_out = kwargs[coord_name]

    # Broadcast array to cube shape
    dims = np.ndim(coord_out)
    if dims == 1:
        ny, nx = cube.shape[1:]
        coord_out_3d = coord_out * np.ones([nx, ny, len(coord_out)])
        coord_out_3d = coord_out_3d.transpose()
    elif dims == 3:
        coord_out_3d = coord_out

    else:
        raise Exception('Coordinate must be 3d or a list of levels')

    # Select the interpolation flag based on the coordinate
    if 'pressure' in coord_name:
        # Air pressure is interpolated logarithmically
        interp_flag = 1
    else:
        # Otherwise interpolation is linear
        interp_flag = 0

    # Interpolate data
    newdata, mask = finterpolate.to_level(
        cube.data, coord_in.points, coord_out_3d, interp_flag, order)
    newdata = np.ma.masked_where(mask, newdata)

    # Create a new cube with the new number of vertical levels
    newcube = iris.cube.Cube(
        newdata, long_name=cube.name(), units=cube.units,
        attributes=cube.attributes,
        dim_coords_and_dims=[(cube.coord(axis='y', dim_coords=True), 1),
                             (cube.coord(axis='x', dim_coords=True), 2)])

    # Add the new co-ordinate to the output cube
    newcoord = iris.coords.AuxCoord(
        coord_out, long_name=coord_name, units=coord_in.units)
    newcube.add_aux_coord(newcoord, range(newcoord.ndim))

    # Promote single dimensional coordinates to dimensional coordinates
    try:
        iris.util.promote_aux_coord_to_dim_coord(newcube, coord_name)
    except ValueError:
        dummy_coord = iris.coords.DimCoord(range(len(coord_out)),
                               long_name='level_number')
        newcube.add_dim_coord(dummy_coord, 0)

    # Add single value coordinates back to the newcube
    add_scalar_coords(cube, newcube)

    return newcube
Esempio n. 20
0
def _add_subtract_common(operation_function, operation_symbol, operation_noun, operation_past_tense,
                         cube, other, dim=None, ignore=True, update_history=True, in_place=False):
    """
    Function which shares common code between addition and subtraction of cubes.

    operation_function   - function which does the operation (e.g. numpy.subtract)
    operation_symbol     - the textual symbol of the operation (e.g. '-')
    operation_noun       - the noun of the operation (e.g. 'subtraction')
    operation_past_tense - the past tense of the operation (e.g. 'subtracted')

    """
    if not isinstance(cube, iris.cube.Cube):
        raise TypeError('The "cube" argument must be an instance of iris.Cube.')

    if isinstance(other, (int, float)):
        # Promote scalar to a coordinate and associate unit type with cube unit type
        other = np.array(other)

    # Check that the units of the cube and the other item are the same, or if the other does not have a unit, skip this test
    if cube.units != getattr(other, 'units', cube.units) :
        raise iris.exceptions.NotYetImplementedError('Differing units (%s & %s) %s not implemented' % \
                                                     (cube.units, other.units, operation_noun))

    history = None

    if isinstance(other, np.ndarray):
        _assert_compatible(cube, other)

        if in_place:
            new_cube = cube
            operation_function(new_cube.data, other, new_cube.data)
        else:
            new_cube = cube.copy(data=operation_function(cube.data, other))

        if update_history:
            if other.ndim == 0:
                history = '%s %s %s' % (cube.name(), operation_symbol, other)
            else:
                history = '%s %s array' % (cube.name(), operation_symbol)
    elif isinstance(other, iris.coords.Coord):
        # Deal with cube addition/subtraction by coordinate

        # What dimension are we processing?
        data_dimension = None
        if dim is not None:
            # Ensure the given dim matches the coord
            if other in cube.coords() and cube.coord_dims(other) != [dim]:
                raise ValueError("dim provided does not match dim found for coord")
            data_dimension = dim
        else:
            # Try and get a coord dim
            if other.shape != (1,):
                try:
                    coord_dims = cube.coord_dims(other)
                    data_dimension = coord_dims[0] if coord_dims else None
                except iris.exceptions.CoordinateNotFoundError:
                    raise ValueError("Could not determine dimension for add/sub. Use add(coord, dim=dim)")

        if other.ndim != 1:
            raise iris.exceptions.CoordinateMultiDimError(other)

        if other.has_bounds():
            warnings.warn('%s by a bounded coordinate not well defined, ignoring bounds.' % operation_noun)

        points = other.points

        if data_dimension is not None:
            points_shape = [1] * cube.data.ndim
            points_shape[data_dimension] = -1
            points = points.reshape(points_shape)

        if in_place:
            new_cube = cube
            operation_function(new_cube.data, points, new_cube.data)
        else:
            new_cube = cube.copy(data=operation_function(cube.data, points))

        if update_history:
            history = '%s %s %s (coordinate)' % (cube.name(), operation_symbol, other.name())
    elif isinstance(other, iris.cube.Cube):
        # Deal with cube addition/subtraction by cube

        # get a coordinate comparison of this cube and the cube to do the operation with
        coord_comp = iris.analysis.coord_comparison(cube, other)

        if coord_comp['transposable']:
            raise ValueError('Cubes cannot be %s, differing axes. '
                                 'cube.transpose() may be required to re-order the axes.' % operation_past_tense)

        # provide a deprecation warning if the ignore keyword has been set
        if ignore is not True:
            warnings.warn('The "ignore" keyword has been deprecated in add/subtract. This functionality is now automatic. '
                          'The provided value to "ignore" has been ignored, and has been automatically calculated.')

        bad_coord_grps = (coord_comp['ungroupable_and_dimensioned'] + coord_comp['resamplable'])
        if bad_coord_grps:
            raise ValueError('This operation cannot be performed as there are differing coordinates (%s) remaining '
                             'which cannot be ignored.' % ', '.join({coord_grp.name() for coord_grp in bad_coord_grps}))

        if in_place:
            new_cube = cube
            operation_function(new_cube.data, other.data, new_cube.data)
        else:
            new_cube = cube.copy(data=operation_function(cube.data, other.data))

        # If a coordinate is to be ignored - remove it
        ignore = filter(None, [coord_grp[0] for coord_grp in coord_comp['ignorable']])
        if not ignore:
            ignore_string = ''
        else:
            ignore_string = ' (ignoring %s)' % ', '.join([coord.name() for coord in ignore])
        for coord in ignore:
            new_cube.remove_coord(coord)

        if update_history:
            history = '%s %s %s%s' % (cube.name() or 'unknown', operation_symbol,
                                      other.name() or 'unknown', ignore_string)

    else:
        return NotImplemented

    iris.analysis.clear_phenomenon_identity(new_cube)

    if history is not None:
        new_cube.add_history(history)

    return new_cube
Esempio n. 21
0
def spatial_vectors_with_phenom_name(i_cube, j_cube, k_cube=None):
    """
    Given 2 or 3 spatially dependent cubes, return a list of the spatial
    coordinate names with appropriate phenomenon name.

    This routine is designed to identify the vector quantites which each
    of the cubes provided represent and return a list of their 3d
    spatial dimension names and associated phenomenon.
    For example, given a cube of "u wind" and "v wind" the return value
    would be (['u', 'v', 'w'], 'wind')::

        >>> spatial_vectors_with_phenom_name(u_wind_cube, v_wind_cube) \
#doctest: +SKIP
        (['u', 'v', 'w'], 'wind')

    """
    directional_names = (('u', 'v', 'w'), ('x', 'y', 'z'), ('i', 'j', 'k'),
                         ('eastward', 'northward', 'upward'),
                         ('easterly', 'northerly', 'vertical'),
                         ('easterly', 'northerly', 'radial'))

    # Create a list of the standard_names of our incoming cubes
    # (excluding the k_cube if it is None).
    cube_standard_names = [cube.name() for cube in (i_cube, j_cube, k_cube)
                           if cube is not None]

    # Define a regular expr which represents (direction, phenomenon)
    # from the standard name of a cube.
    # e.g from "w wind" -> ("w", "wind")
    vector_qty = re.compile(r'([^\W_]+)[\W_]+(.*)')

    # Make a dictionary of {direction: phenomenon quantity}
    cube_directions, cube_phenomena = zip(
        *[re.match(vector_qty, std_name).groups()
            for std_name in cube_standard_names])

    # Check that there is only one distinct phenomenon
    if len(set(cube_phenomena)) != 1:
        raise ValueError('Vector phenomenon name not consistent between '
                         'vector cubes. Got cube phenomena: {}; from '
                         'standard names: {}.'.format(
                             ', '.join(cube_phenomena),
                             ', '.join(cube_standard_names)))

    # Get the appropriate direction list from the cube_directions we
    # have got from the standard name.
    direction = None
    for possible_direction in directional_names:
        # If this possible direction (minus the k_cube if it is none)
        # matches direction from the given cubes use it.
        if possible_direction[0:len(cube_directions)] == cube_directions:
            direction = possible_direction

    # If we didn't get a match, raise an Exception
    if direction is None:
        direction_string = '; '.join(', '.join(possible_direction)
                                     for possible_direction
                                     in directional_names)
        raise ValueError('{} are not recognised vector cube_directions. '
                         'Possible cube_directions are: {}.'.format(
                             cube_directions, direction_string))

    return (direction, cube_phenomena[0])
Esempio n. 22
0
def _multiply_divide_common(operation_function, operation_symbol, operation_noun,
                            cube, other, dim=None, update_history=True):
    """
    Function which shares common code between multiplication and division of cubes.

    operation_function   - function which does the operation (e.g. numpy.divide)
    operation_symbol     - the textual symbol of the operation (e.g. '/')
    operation_noun       - the noun of the operation (e.g. 'division')
    operation_past_tense - the past tesnse of the operation (e.g. 'divided')

    .. seealso:: For information on the dim keyword argument see :func:`multiply`.

    """
    if not isinstance(cube, iris.cube.Cube):
        raise TypeError('The "cube" argument must be an instance of iris.Cube.')

    if isinstance(other, (int, float)):
        other = np.array(other)

    other_unit = None
    history = None

    if isinstance(other, np.ndarray):
        _assert_compatible(cube, other)

        copy_cube = cube.copy(data=operation_function(cube.data, other))

        if update_history:
            if other.ndim == 0:
                history = '%s %s %s' % (cube.name(), operation_symbol, other)
            else:
                history = '%s %s array' % (cube.name(), operation_symbol)

        other_unit = '1'
    elif isinstance(other, iris.coords.Coord):
        # Deal with cube multiplication/division by coordinate

        # What dimension are we processing?
        data_dimension = None
        if dim is not None:
            # Ensure the given dim matches the coord
            if other in cube.coords() and cube.coord_dims(other) != [dim]:
                raise ValueError("dim provided does not match dim found for coord")
            data_dimension = dim
        else:
            # Try and get a coord dim
            if other.shape != (1,):
                try:
                    coord_dims = cube.coord_dims(other)
                    data_dimension = coord_dims[0] if coord_dims else None
                except iris.exceptions.CoordinateNotFoundError:
                    raise ValueError("Could not determine dimension for mul/div. Use mul(coord, dim=dim)")

        if other.ndim != 1:
            raise iris.exceptions.CoordinateMultiDimError(other)

        if other.has_bounds():
            warnings.warn('%s by a bounded coordinate not well defined, ignoring bounds.' % operation_noun)

        points = other.points

        # If the axis is defined then shape the provided points so that we can do the
        # division (this is needed as there is no "axis" keyword to numpy's divide/multiply)
        if data_dimension is not None:
            points_shape = [1] * cube.data.ndim
            points_shape[data_dimension] = -1
            points = points.reshape(points_shape)

        copy_cube = cube.copy(data=operation_function(cube.data, points))

        if update_history:
            history = '%s %s %s' % (cube.name(), operation_symbol, other.name())

        other_unit = other.units
    elif isinstance(other, iris.cube.Cube):
        # Deal with cube multiplication/division by cube
        copy_cube = cube.copy(data=operation_function(cube.data, other.data))

        if update_history:
            history = '%s %s %s' % (cube.name() or 'unknown', operation_symbol,
                                    other.name() or 'unknown')

        other_unit = other.units
    else:
        return NotImplemented

    # Update the units
    if operation_function == np.multiply:
        copy_cube.units = cube.units * other_unit
    elif operation_function == np.divide:
        copy_cube.units = cube.units / other_unit

    iris.analysis.clear_phenomenon_identity(copy_cube)

    if history is not None:
        copy_cube.add_history(history)

    return copy_cube
Esempio n. 23
0
def _get_xy_coords(cube):
    """
    Return the x and y coordinates from a cube.

    This function will preferentially return a pair of dimension
    coordinates (if there are more than one potential x or y dimension
    coordinates a ValueError will be raised). If the cube does not have
    a pair of x and y dimension coordinates it will return 1D auxiliary
    coordinates (including scalars). If there is not one and only one set
    of x and y auxiliary coordinates a ValueError will be raised.

    Having identified the x and y coordinates, the function checks that they
    have equal coordinate systems and that they do not occupy the same
    dimension on the cube.

    Args:

    * cube:
        An instance of :class:`iris.cube.Cube`.

    Returns:
        A tuple containing the cube's x and y coordinates.

    """
    # Look for a suitable dimension coords first.
    x_coords = cube.coords(axis='x', dim_coords=True)
    if not x_coords:
        # If there is no x coord in dim_coords look for scalars or
        # monotonic coords in aux_coords.
        x_coords = [coord for coord in cube.coords(axis='x', dim_coords=False)
                    if coord.ndim == 1 and coord.is_monotonic()]
    if len(x_coords) != 1:
        raise ValueError('Cube {!r} must contain a single 1D x '
                         'coordinate.'.format(cube.name()))
    x_coord = x_coords[0]

    # Look for a suitable dimension coords first.
    y_coords = cube.coords(axis='y', dim_coords=True)
    if not y_coords:
        # If there is no y coord in dim_coords look for scalars or
        # monotonic coords in aux_coords.
        y_coords = [coord for coord in cube.coords(axis='y', dim_coords=False)
                    if coord.ndim == 1 and coord.is_monotonic()]
    if len(y_coords) != 1:
        raise ValueError('Cube {!r} must contain a single 1D y '
                         'coordinate.'.format(cube.name()))
    y_coord = y_coords[0]

    if x_coord.coord_system != y_coord.coord_system:
        raise ValueError("The cube's x ({!r}) and y ({!r}) "
                         "coordinates must have the same coordinate "
                         "system.".format(x_coord.name(), y_coord.name()))

    # The x and y coordinates must describe different dimensions
    # or be scalar coords.
    x_dims = cube.coord_dims(x_coord)
    x_dim = None
    if x_dims:
        x_dim = x_dims[0]

    y_dims = cube.coord_dims(y_coord)
    y_dim = None
    if y_dims:
        y_dim = y_dims[0]

    if x_dim is not None and y_dim == x_dim:
        raise ValueError("The cube's x and y coords must not describe the "
                         "same data dimension.")

    return x_coord, y_coord
Esempio n. 24
0
def differentiate(cube, coord_to_differentiate):
    r"""
    Calculate the differential of a given cube with respect to the
    coord_to_differentiate.

    Args:

    * coord_to_differentiate:
        Either a Coord instance or the unique name of a coordinate which
        exists in the cube.
        If a Coord instance is provided, it does not necessarily have to
        exist on the cube.

    Example usage::

        u_wind_acceleration = differentiate(u_wind_cube, 'forecast_time')

    The algorithm used is equivalent to:

    .. math::

        d_i = \frac{v_{i+1}-v_i}{c_{i+1}-c_i}

    Where ``d`` is the differential, ``v`` is the data value, ``c`` is
    the coordinate value and ``i`` is the index in the differential
    direction. Hence, in a normal situation if a cube has a shape
    (x: n; y: m) differentiating with respect to x will result in a cube
    of shape (x: n-1; y: m) and differentiating with respect to y will
    result in (x: n; y: m-1). If the coordinate to differentiate is
    :attr:`circular <iris.coords.DimCoord.circular>` then the resultant
    shape will be the same as the input cube.

    In the returned cube the `coord_to_differentiate` object is
    redefined such that the output coordinate values are set to the
    averages of the original coordinate values (i.e. the mid-points).
    Similarly, the output lower bounds values are set to the averages of
    the original lower bounds values and the output upper bounds values
    are set to the averages of the original upper bounds values. In more
    formal terms:

    * `C[i] = (c[i] + c[i+1]) / 2`
    * `B[i, 0] = (b[i, 0] + b[i+1, 0]) / 2`
    * `B[i, 1] = (b[i, 1] + b[i+1, 1]) / 2`

    where `c` and `b` represent the input coordinate values and bounds,
    and `C` and `B` the output coordinate values and bounds.

    .. note:: Difference method used is the same as :func:`cube_delta`
        and therefore has the same limitations.

    .. note:: Spherical differentiation does not occur in this routine.

    """
    # Get the delta cube in the required differential direction.
    # This operation results in a copy of the original cube.
    delta_cube = cube_delta(cube, coord_to_differentiate)

    if isinstance(coord_to_differentiate, str):
        coord = cube.coord(coord_to_differentiate)
    else:
        coord = coord_to_differentiate

    delta_coord = _construct_delta_coord(coord)
    delta_dim = cube.coord_dims(coord.name())[0]

    # calculate delta_cube / delta_coord to give the differential.
    delta_cube = iris.analysis.maths.divide(delta_cube, delta_coord, delta_dim)

    # Update the standard name
    delta_cube.rename('derivative_of_{}_wrt_{}'.format(cube.name(),
                                                       coord.name()))
    return delta_cube
Esempio n. 25
0
def differentiate(cube, coord_to_differentiate):
    r"""
    Calculate the differential of a given cube with respect to the
    coord_to_differentiate.

    Args:

    * coord_to_differentiate:
        Either a Coord instance or the unique name of a coordinate which
        exists in the cube.
        If a Coord instance is provided, it does not necessarily have to
        exist on the cube.

    Example usage::

        u_wind_acceleration = differentiate(u_wind_cube, 'forecast_time')

    The algorithm used is equivalent to:

    .. math::

        d_i = \frac{v_{i+1}-v_i}{c_{i+1}-c_i}

    Where ``d`` is the differential, ``v`` is the data value, ``c`` is
    the coordinate value and ``i`` is the index in the differential
    direction. Hence, in a normal situation if a cube has a shape
    (x: n; y: m) differentiating with respect to x will result in a cube
    of shape (x: n-1; y: m) and differentiating with respect to y will
    result in (x: n; y: m-1). If the coordinate to differentiate is
    :attr:`circular <iris.coords.DimCoord.circular>` then the resultant
    shape will be the same as the input cube.

    In the returned cube the `coord_to_differentiate` object is
    redefined such that the output coordinate values are set to the
    averages of the original coordinate values (i.e. the mid-points).
    Similarly, the output lower bounds values are set to the averages of
    the original lower bounds values and the output upper bounds values
    are set to the averages of the original upper bounds values. In more
    formal terms:

    * `C[i] = (c[i] + c[i+1]) / 2`
    * `B[i, 0] = (b[i, 0] + b[i+1, 0]) / 2`
    * `B[i, 1] = (b[i, 1] + b[i+1, 1]) / 2`

    where `c` and `b` represent the input coordinate values and bounds,
    and `C` and `B` the output coordinate values and bounds.

    .. note:: Difference method used is the same as :func:`cube_delta`
    and therefore has the same limitations.

    .. note:: Spherical differentiation does not occur in this routine.

    """
    # Get the delta cube in the required differential direction.
    # This operation results in a copy of the original cube.
    delta_cube = cube_delta(cube, coord_to_differentiate)

    if isinstance(coord_to_differentiate, basestring):
        coord = cube.coord(coord_to_differentiate)
    else:
        coord = coord_to_differentiate

    delta_coord = _construct_delta_coord(coord)
    delta_dim = cube.coord_dims(coord.name())[0]

    # calculate delta_cube / delta_coord to give the differential.
    delta_cube = iris.analysis.maths.divide(delta_cube, delta_coord, delta_dim)

    # Update the standard name
    delta_cube.rename('derivative_of_{}_wrt_{}'.format(cube.name(),
                                                       coord.name()))
    return delta_cube