Source code for esmvalcore.preprocessor._units

"""Metadata operations on data cubes.

Allows for unit conversions.
"""
import logging

from cf_units import Unit
import iris
import numpy as np

logger = logging.getLogger(__name__)


# List containing special cases for convert_units. Each list item is another
# list. Each of these sublists defines one special conversion. Each element in
# the sublists is a tuple (standard_name, units). Note: All units for a single
# special case need to be "physically identical", e.g., 1 kg m-2 s-1 "equals" 1
# mm s-1 for precipitation
SPECIAL_CASES = [
    [
        ('precipitation_flux', 'kg m-2 s-1'),
        ('lwe_precipitation_rate', 'mm s-1'),
    ],
]


def _try_special_conversions(cube, units):
    """Try special conversion."""
    for special_case in SPECIAL_CASES:
        for (std_name, special_units) in special_case:
            # Special unit conversion only works if all of the following
            # criteria are met:
            # - the cube's standard_name is one of the supported
            #   standard_names
            # - the cube's units are convertible to the ones defined for
            #   that given standard_name
            # - the desired target units are convertible to the units of
            #   one of the other standard_names in that special case

            # Step 1: find suitable source name and units
            if (cube.standard_name == std_name and
                    cube.units.is_convertible(special_units)):
                for (target_std_name, target_units) in special_case:
                    if target_std_name == std_name:
                        continue

                    # Step 2: find suitable target name and units
                    if Unit(units).is_convertible(target_units):
                        cube.standard_name = target_std_name

                        # In order to avoid two calls to cube.convert_units,
                        # determine the conversion factor between the cube's
                        # units and the source units first and simply add this
                        # factor to the target units (remember that the source
                        # units and the target units should be "physically
                        # identical").
                        factor = cube.units.convert(1.0, special_units)
                        cube.units = f"{factor} {target_units}"
                        cube.convert_units(units)
                        return True

    # If no special case has been detected, return False
    return False


[docs]def convert_units(cube, units): """Convert the units of a cube to new ones. This converts units of a cube. Note ---- Allows special unit conversions which transforms one quantity to another (physically related) quantity. These quantities are identified via their ``standard_name`` and their ``units`` (units convertible to the ones defined are also supported). For example, this enables conversions between precipitation fluxes measured in ``kg m-2 s-1`` and precipitation rates measured in ``mm day-1`` (and vice versa). Currently, the following special conversions are supported: * ``precipitation_flux`` (``kg m-2 s-1``) -- ``lwe_precipitation_rate`` (``mm day-1``) Names in the list correspond to ``standard_names`` of the input data. Conversions are allowed from each quantity to any other quantity given in a bullet point. The corresponding target quantity is inferred from the desired target units. In addition, any other units convertible to the ones given are also supported (e.g., instead of ``mm day-1``, ``m s-1`` is also supported). Note that for precipitation variables, a water density of ``1000 kg m-3`` is assumed. Arguments --------- cube: iris.cube.Cube Input cube. units: str New units in udunits form. Returns ------- iris.cube.Cube converted cube. """ try: cube.convert_units(units) except ValueError: if not _try_special_conversions(cube, units): raise return cube
[docs]def accumulate_coordinate(cube, coordinate): """Weight data using the bounds from a given coordinate. The resulting cube will then have units given by ``cube_units * coordinate_units``. Parameters ---------- cube : iris.cube.Cube Data cube for the flux coordinate: str Name of the coordinate that will be used as weights. Returns ------- iris.cube.Cube Cube with the aggregated data Raises ------ ValueError If the coordinate is not found in the cube. NotImplementedError If the coordinate is multidimensional. """ try: coord = cube.coord(coordinate) except iris.exceptions.CoordinateNotFoundError as err: raise ValueError( "Requested coordinate %s not found in cube %s", coordinate, cube.summary(shorten=True)) from err if coord.ndim > 1: raise NotImplementedError( f'Multidimensional coordinate {coord} not supported.') factor = iris.coords.AuxCoord( np.diff(coord.bounds)[..., -1], var_name=coord.var_name, long_name=coord.long_name, units=coord.units, ) result = cube * factor unit = result.units.format().split(' ')[-1] result.convert_units(unit) result.long_name = f"{cube.long_name} * {factor.long_name}" return result