You are viewing the latest unreleased documentation v3.2.dev0. You may prefer a stable version.

Source code for iris.analysis.maths

# Copyright Iris contributors
#
# This file is part of Iris and is released under the LGPL license.
# See COPYING and COPYING.LESSER in the root of the repository for full
# licensing details.
"""
Basic mathematical and statistical operations.

"""

from functools import lru_cache
import inspect
import math
import operator
import warnings

import cf_units
import dask.array as da
import numpy as np
from numpy import ma

import iris.analysis
from iris.common import SERVICES, Resolve
from iris.common.lenient import _lenient_client
from iris.config import get_logger
import iris.coords
import iris.exceptions
import iris.util

# Configure the logger.
logger = get_logger(__name__)


@lru_cache(maxsize=128, typed=True)
def _output_dtype(op, first_dtype, second_dtype=None, in_place=False):
    """
    Get the numpy dtype corresponding to the result of applying a unary or
    binary operation to arguments of specified dtype.

    Args:

    * op:
        A unary or binary operator which can be applied to array-like objects.
    * first_dtype:
        The dtype of the first or only argument to the operator.

    Kwargs:

    * second_dtype:
        The dtype of the second argument to the operator.

    * in_place:
        Whether the operation is to be performed in place.

    Returns:
        An instance of :class:`numpy.dtype`

    .. note::

        The function always returns the dtype which would result if the
        operation were successful, even if the operation could fail due to
        casting restrictions for in place operations.

    """
    if in_place:
        # Always return the first dtype, even if the operation would fail due
        # to failure to cast the result.
        result = first_dtype
    else:
        operand_dtypes = (
            (first_dtype, second_dtype)
            if second_dtype is not None
            else (first_dtype,)
        )
        arrays = [np.array([1], dtype=dtype) for dtype in operand_dtypes]
        result = op(*arrays).dtype
    return result


def _get_dtype(operand):
    """
    Get the numpy dtype corresponding to the numeric data in the object
    provided.

    Args:

    * operand:
        An instance of :class:`iris.cube.Cube` or :class:`iris.coords.Coord`,
        or a number or :class:`numpy.ndarray`.

    Returns:
        An instance of :class:`numpy.dtype`

    """
    return (
        np.min_scalar_type(operand) if np.isscalar(operand) else operand.dtype
    )


[docs]def abs(cube, in_place=False): """ Calculate the absolute values of the data in the Cube provided. Args: * cube: An instance of :class:`iris.cube.Cube`. Kwargs: * in_place: Whether to create a new Cube, or alter the given "cube". Returns: An instance of :class:`iris.cube.Cube`. """ _assert_is_cube(cube) new_dtype = _output_dtype(np.abs, cube.dtype, in_place=in_place) op = da.absolute if cube.has_lazy_data() else np.abs return _math_op_common( cube, op, cube.units, new_dtype=new_dtype, in_place=in_place )
[docs]def intersection_of_cubes(cube, other_cube): """ Return the two Cubes of intersection given two Cubes. .. note:: The intersection of cubes function will ignore all single valued coordinates in checking the intersection. Args: * cube: An instance of :class:`iris.cube.Cube`. * other_cube: An instance of :class:`iris.cube.Cube`. Returns: A pair of :class:`iris.cube.Cube` instances in a tuple corresponding to the original cubes restricted to their intersection. """ # Take references of the original cubes (which will be copied when # slicing later). new_cube_self = cube new_cube_other = other_cube # This routine has not been written to cope with multi-dimensional # coordinates. for coord in cube.coords() + other_cube.coords(): if coord.ndim != 1: raise iris.exceptions.CoordinateMultiDimError(coord) coord_comp = iris.analysis._dimensional_metadata_comparison( cube, other_cube ) if coord_comp["ungroupable_and_dimensioned"]: raise ValueError( "Cubes do not share all coordinates in common, " "cannot intersect." ) # cubes must have matching coordinates for coord in cube.coords(): other_coord = other_cube.coord(coord) # Only intersect coordinates which are different, single values # coordinates may differ. if coord.shape[0] > 1 and coord != other_coord: intersected_coord = coord.intersect(other_coord) new_cube_self = new_cube_self.subset(intersected_coord) new_cube_other = new_cube_other.subset(intersected_coord) return new_cube_self, new_cube_other
def _assert_is_cube(cube): from iris.cube import Cube if not isinstance(cube, Cube): raise TypeError( 'The "cube" argument must be an instance of ' "iris.cube.Cube." )
[docs]@_lenient_client(services=SERVICES) def add(cube, other, dim=None, in_place=False): """ Calculate the sum of two cubes, or the sum of a cube and a coordinate or scalar value. When summing two cubes, they must both have the same coordinate systems & data resolution. When adding a coordinate to a cube, they must both share the same number of elements along a shared axis. Args: * cube: An instance of :class:`iris.cube.Cube`. * other: An instance of :class:`iris.cube.Cube` or :class:`iris.coords.Coord`, or a number or :class:`numpy.ndarray`. Kwargs: * dim: If supplying a coord with no match on the cube, you must supply the dimension to process. * in_place: Whether to create a new Cube, or alter the given "cube". Returns: An instance of :class:`iris.cube.Cube`. """ _assert_is_cube(cube) new_dtype = _output_dtype( operator.add, cube.dtype, second_dtype=_get_dtype(other), in_place=in_place, ) if in_place: _inplace_common_checks(cube, other, "addition") op = operator.iadd else: op = operator.add return _add_subtract_common( op, "add", cube, other, new_dtype, dim=dim, in_place=in_place )
[docs]@_lenient_client(services=SERVICES) def subtract(cube, other, dim=None, in_place=False): """ Calculate the difference between two cubes, or the difference between a cube and a coordinate or scalar value. When subtracting two cubes, they must both have the same coordinate systems & data resolution. When subtracting a coordinate to a cube, they must both share the same number of elements along a shared axis. Args: * cube: An instance of :class:`iris.cube.Cube`. * other: An instance of :class:`iris.cube.Cube` or :class:`iris.coords.Coord`, or a number or :class:`numpy.ndarray`. Kwargs: * dim: If supplying a coord with no match on the cube, you must supply the dimension to process. * in_place: Whether to create a new Cube, or alter the given "cube". Returns: An instance of :class:`iris.cube.Cube`. """ _assert_is_cube(cube) new_dtype = _output_dtype( operator.sub, cube.dtype, second_dtype=_get_dtype(other), in_place=in_place, ) if in_place: _inplace_common_checks(cube, other, "subtraction") op = operator.isub else: op = operator.sub return _add_subtract_common( op, "subtract", cube, other, new_dtype, dim=dim, in_place=in_place )
def _add_subtract_common( operation_function, operation_name, cube, other, new_dtype, dim=None, 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_name - the public name of the operation (e.g. 'divide') cube - the cube whose data is used as the first argument to `operation_function` other - the cube, coord, ndarray or number whose data is used as the second argument new_dtype - the expected dtype of the output. Used in the case of scalar masked arrays dim - dimension along which to apply `other` if it's a coordinate that is not found in `cube` in_place - whether or not to apply the operation in place to `cube` and `cube.data` """ _assert_is_cube(cube) if cube.units != getattr(other, "units", cube.units): emsg = ( f"Cannot use {operation_name!r} with differing units " f"({cube.units} & {other.units})" ) raise iris.exceptions.NotYetImplementedError(emsg) result = _binary_op_common( operation_function, operation_name, cube, other, cube.units, new_dtype=new_dtype, dim=dim, in_place=in_place, ) return result
[docs]@_lenient_client(services=SERVICES) def multiply(cube, other, dim=None, in_place=False): """ Calculate the product of a cube and another cube or coordinate. Args: * cube: An instance of :class:`iris.cube.Cube`. * other: An instance of :class:`iris.cube.Cube` or :class:`iris.coords.Coord`, or a number or :class:`numpy.ndarray`. Kwargs: * dim: If supplying a coord with no match on the cube, you must supply the dimension to process. Returns: An instance of :class:`iris.cube.Cube`. """ _assert_is_cube(cube) new_dtype = _output_dtype( operator.mul, cube.dtype, second_dtype=_get_dtype(other), in_place=in_place, ) other_unit = getattr(other, "units", "1") new_unit = cube.units * other_unit if in_place: _inplace_common_checks(cube, other, "multiplication") op = operator.imul else: op = operator.mul result = _binary_op_common( op, "multiply", cube, other, new_unit, new_dtype=new_dtype, dim=dim, in_place=in_place, ) return result
def _inplace_common_checks(cube, other, math_op): """ Check whether an inplace math operation can take place between `cube` and `other`. It cannot if `cube` has integer data and `other` has float data as the operation will always produce float data that cannot be 'safely' cast back to the integer data of `cube`. """ other_dtype = _get_dtype(other) if not np.can_cast(other_dtype, cube.dtype, "same_kind"): aemsg = ( "Cannot perform inplace {} between {!r} " "with {} data and {!r} with {} data." ) raise ArithmeticError( aemsg.format(math_op, cube, cube.dtype, other, other_dtype) )
[docs]@_lenient_client(services=SERVICES) def divide(cube, other, dim=None, in_place=False): """ Calculate the division of a cube by a cube or coordinate. Args: * cube: An instance of :class:`iris.cube.Cube`. * other: An instance of :class:`iris.cube.Cube` or :class:`iris.coords.Coord`, or a number or :class:`numpy.ndarray`. Kwargs: * dim: If supplying a coord with no match on the cube, you must supply the dimension to process. Returns: An instance of :class:`iris.cube.Cube`. """ _assert_is_cube(cube) new_dtype = _output_dtype( operator.truediv, cube.dtype, second_dtype=_get_dtype(other), in_place=in_place, ) other_unit = getattr(other, "units", "1") new_unit = cube.units / other_unit if in_place: if cube.dtype.kind in "iu": # Cannot coerce float result from inplace division back to int. emsg = ( f"Cannot perform inplace division of cube {cube.name()!r} " "with integer data." ) raise ArithmeticError(emsg) op = operator.itruediv else: op = operator.truediv result = _binary_op_common( op, "divide", cube, other, new_unit, new_dtype=new_dtype, dim=dim, in_place=in_place, ) return result
[docs]def exponentiate(cube, exponent, in_place=False): """ Returns the result of the given cube to the power of a scalar. Args: * cube: An instance of :class:`iris.cube.Cube`. * exponent: The integer or floating point exponent. .. note:: When applied to the cube's unit, the exponent must result in a unit that can be described using only integer powers of the basic units. e.g. Unit('meter^-2 kilogram second^-1') Kwargs: * in_place: Whether to create a new Cube, or alter the given "cube". Returns: An instance of :class:`iris.cube.Cube`. """ _assert_is_cube(cube) new_dtype = _output_dtype( operator.pow, cube.dtype, second_dtype=_get_dtype(exponent), in_place=in_place, ) if cube.has_lazy_data(): def power(data): return operator.pow(data, exponent) else: def power(data, out=None): return np.power(data, exponent, out) return _math_op_common( cube, power, cube.units ** exponent, new_dtype=new_dtype, in_place=in_place, )
[docs]def exp(cube, 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: * in_place: Whether to create a new Cube, or alter the given "cube". Returns: An instance of :class:`iris.cube.Cube`. """ _assert_is_cube(cube) new_dtype = _output_dtype(np.exp, cube.dtype, in_place=in_place) op = da.exp if cube.has_lazy_data() else np.exp return _math_op_common( cube, op, cf_units.Unit("1"), new_dtype=new_dtype, in_place=in_place )
[docs]def log(cube, in_place=False): """ Calculate the natural logarithm (base-e logarithm) of the cube. Args: * cube: An instance of :class:`iris.cube.Cube`. Kwargs: * in_place: Whether to create a new Cube, or alter the given "cube". Returns: An instance of :class:`iris.cube.Cube`. """ _assert_is_cube(cube) new_dtype = _output_dtype(np.log, cube.dtype, in_place=in_place) op = da.log if cube.has_lazy_data() else np.log return _math_op_common( cube, op, cube.units.log(math.e), new_dtype=new_dtype, in_place=in_place, )
[docs]def log2(cube, in_place=False): """ Calculate the base-2 logarithm of the cube. Args: * cube: An instance of :class:`iris.cube.Cube`. Kwargs:lib/iris/tests/unit/analysis/maths/test_subtract.py * in_place: Whether to create a new Cube, or alter the given "cube". Returns: An instance of :class:`iris.cube.Cube`. """ _assert_is_cube(cube) new_dtype = _output_dtype(np.log2, cube.dtype, in_place=in_place) op = da.log2 if cube.has_lazy_data() else np.log2 return _math_op_common( cube, op, cube.units.log(2), new_dtype=new_dtype, in_place=in_place )
[docs]def log10(cube, in_place=False): """ Calculate the base-10 logarithm of the cube. Args: * cube: An instance of :class:`iris.cube.Cube`. Kwargs: * in_place: Whether to create a new Cube, or alter the given "cube". Returns: An instance of :class:`iris.cube.Cube`. """ _assert_is_cube(cube) new_dtype = _output_dtype(np.log10, cube.dtype, in_place=in_place) op = da.log10 if cube.has_lazy_data() else np.log10 return _math_op_common( cube, op, cube.units.log(10), new_dtype=new_dtype, in_place=in_place )
[docs]def apply_ufunc( ufunc, cube, other=None, new_unit=None, new_name=None, in_place=False ): """ Apply a `numpy universal function <http://docs.scipy.org/doc/numpy/reference/ufuncs.html>`_ to a cube or pair of cubes. .. note:: Many of the numpy.ufunc have been implemented explicitly in Iris e.g. :func:`numpy.abs`, :func:`numpy.add` are implemented in :func:`iris.analysis.maths.abs`, :func:`iris.analysis.maths.add`. It is usually preferable to use these functions rather than :func:`iris.analysis.maths.apply_ufunc` where possible. Args: * ufunc: An instance of :func:`numpy.ufunc` e.g. :func:`numpy.sin`, :func:`numpy.mod`. * cube: An instance of :class:`iris.cube.Cube`. Kwargs: * other: An instance of :class:`iris.cube.Cube` to be given as the second argument to :func:`numpy.ufunc`. * new_unit: Unit for the resulting Cube. * new_name: Name for the resulting Cube. * in_place: Whether to create a new Cube, or alter the given "cube". Returns: An instance of :class:`iris.cube.Cube`. Example:: cube = apply_ufunc(numpy.sin, cube, in_place=True) """ if not isinstance(ufunc, np.ufunc): ufunc_name = getattr( ufunc, "__name__", "function passed to apply_ufunc" ) emsg = f"{ufunc_name} is not recognised, it is not an instance of numpy.ufunc" raise TypeError(emsg) ufunc_name = ufunc.__name__ if ufunc.nout != 1: emsg = ( f"{ufunc_name} returns {ufunc.nout} objects, apply_ufunc currently " "only supports numpy.ufunc functions returning a single object." ) raise ValueError(emsg) if ufunc.nin == 1: if other is not None: dmsg = ( "ignoring surplus 'other' argument to apply_ufunc, " f"provided ufunc {ufunc_name!r} only requires 1 input" ) logger.debug(dmsg) new_dtype = _output_dtype(ufunc, cube.dtype, in_place=in_place) new_cube = _math_op_common( cube, ufunc, new_unit, new_dtype=new_dtype, in_place=in_place ) elif ufunc.nin == 2: if other is None: emsg = ( f"{ufunc_name} requires two arguments, another cube " "must also be passed to apply_ufunc." ) raise ValueError(emsg) _assert_is_cube(other) new_dtype = _output_dtype( ufunc, cube.dtype, second_dtype=other.dtype, in_place=in_place ) new_cube = _binary_op_common( ufunc, ufunc_name, cube, other, new_unit, new_dtype=new_dtype, in_place=in_place, ) else: emsg = f"Provided ufunc '{ufunc_name}.nin' must be 1 or 2." raise ValueError(emsg) new_cube.rename(new_name) return new_cube
def _binary_op_common( operation_function, operation_name, cube, other, new_unit, new_dtype=None, dim=None, in_place=False, ): """ Function which shares common code between binary operations. operation_function - function which does the operation (e.g. numpy.divide) operation_name - the public name of the operation (e.g. 'divide') cube - the cube whose data is used as the first argument to `operation_function` other - the cube, coord, ndarray or number whose data is used as the second argument new_dtype - the expected dtype of the output. Used in the case of scalar masked arrays new_unit - unit for the resulting quantity dim - dimension along which to apply `other` if it's a coordinate that is not found in `cube` in_place - whether or not to apply the operation in place to `cube` and `cube.data` """ from iris.cube import Cube _assert_is_cube(cube) # Flag to notify the _math_op_common function to simply wrap the resultant # data of the maths operation in a cube with no metadata. skeleton_cube = False if isinstance(other, iris.coords.Coord): # The rhs must be an array. rhs = _broadcast_cube_coord_data(cube, other, operation_name, dim=dim) elif isinstance(other, Cube): # Prepare to resolve the cube operands and associated coordinate # metadata into the resultant cube. resolver = Resolve(cube, other) # Get the broadcast, auto-transposed safe versions of the cube operands. cube = resolver.lhs_cube_resolved other = resolver.rhs_cube_resolved # Flag that it's safe to wrap the resultant data of the math operation # in a cube with no metadata, as all of the metadata of the resultant # cube is being managed by the resolver. skeleton_cube = True # The rhs must be an array. rhs = other.core_data() else: # The rhs must be an array. rhs = np.asanyarray(other) def unary_func(lhs): data = operation_function(lhs, rhs) if data is NotImplemented: # Explicitly raise the TypeError, so it gets raised even if, for # example, `iris.analysis.maths.multiply(cube, other)` is called # directly instead of `cube * other`. emsg = ( f"Cannot {operation_function.__name__} {type(lhs).__name__!r} " f"and {type(rhs).__name__} objects." ) raise TypeError(emsg) return data result = _math_op_common( cube, unary_func, new_unit, new_dtype=new_dtype, in_place=in_place, skeleton_cube=skeleton_cube, ) if isinstance(other, Cube): # Insert the resultant data from the maths operation # within the resolved cube. result = resolver.cube(result.core_data(), in_place=in_place) _sanitise_metadata(result, new_unit) return result def _broadcast_cube_coord_data(cube, other, operation_name, dim=None): # 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 %s. " "Use %s(cube, coord, dim=dim)" % (operation_name, operation_name) ) if other.ndim != 1: raise iris.exceptions.CoordinateMultiDimError(other) if other.has_bounds(): warnings.warn( "Using {!r} with a bounded coordinate is not well " "defined; ignoring bounds.".format(operation_name) ) points = other.points # If the `data_dimension` is defined then shape the provided points for # proper array broadcasting if data_dimension is not None: points_shape = [1] * cube.ndim points_shape[data_dimension] = -1 points = points.reshape(points_shape) return points def _sanitise_metadata(cube, unit): """ As part of the maths metadata contract, clear the necessary or unsupported metadata from the resultant cube of the maths operation. """ # Clear the cube names. cube.rename(None) # Clear the cube cell methods. cube.cell_methods = None # Clear the cell measures. for cm in cube.cell_measures(): cube.remove_cell_measure(cm) # Clear the ancillary variables. for av in cube.ancillary_variables(): cube.remove_ancillary_variable(av) # Clear the STASH attribute, if present. if "STASH" in cube.attributes: del cube.attributes["STASH"] # Set the cube units. cube.units = unit def _math_op_common( cube, operation_function, new_unit, new_dtype=None, in_place=False, skeleton_cube=False, ): from iris.cube import Cube _assert_is_cube(cube) if in_place and not skeleton_cube: if cube.has_lazy_data(): cube.data = operation_function(cube.lazy_data()) else: try: operation_function(cube.data, out=cube.data) except TypeError: # Non-ufunc function operation_function(cube.data) new_cube = cube else: data = operation_function(cube.core_data()) if skeleton_cube: # Simply wrap the resultant data in a cube, as no # cube metadata is required by the caller. new_cube = Cube(data) else: new_cube = cube.copy(data) # If the result of the operation is scalar and masked, we need to fix-up the dtype. if ( new_dtype is not None and not new_cube.has_lazy_data() and new_cube.data.shape == () and ma.is_masked(new_cube.data) ): new_cube.data = ma.masked_array(0, 1, dtype=new_dtype) _sanitise_metadata(new_cube, new_unit) return new_cube
[docs]class IFunc: """ :class:`IFunc` class for functions that can be applied to an iris cube. """ def __init__(self, data_func, units_func): """ Create an ifunc from a data function and units function. Args: * data_func: Function to be applied to one or two data arrays, which are given as positional arguments. Should return another data array, with the same shape as the first array. May also have keyword arguments. * units_func: Function to calculate the units of the resulting cube. Should take the cube/s as input and return an instance of :class:`cf_units.Unit`. Returns: An ifunc. **Example usage 1** Using an existing numpy ufunc, such as numpy.sin for the data function and a simple lambda function for the units function:: sine_ifunc = iris.analysis.maths.IFunc( numpy.sin, lambda cube: cf_units.Unit('1')) sine_cube = sine_ifunc(cube) **Example usage 2** Define a function for the data arrays of two cubes and define a units function that checks the units of the cubes for consistency, before giving the resulting cube the same units as the first cube:: def ws_data_func(u_data, v_data): return numpy.sqrt( u_data**2 + v_data**2 ) def ws_units_func(u_cube, v_cube): if u_cube.units != getattr(v_cube, 'units', u_cube.units): raise ValueError("units do not match") return u_cube.units ws_ifunc = iris.analysis.maths.IFunc(ws_data_func, ws_units_func) ws_cube = ws_ifunc(u_cube, v_cube, new_name='wind speed') **Example usage 3** Using a data function that allows a keyword argument:: cs_ifunc = iris.analysis.maths.IFunc(numpy.cumsum, lambda a: a.units) cs_cube = cs_ifunc(cube, axis=1) """ self._data_func_name = getattr( data_func, "__name__", "data_func argument passed to IFunc" ) if not callable(data_func): emsg = f"{self._data_func_name} is not callable." raise TypeError(emsg) self._unit_func_name = getattr( units_func, "__name__", "units_func argument passed to IFunc" ) if not callable(units_func): emsg = f"{self._unit_func_name} is not callable." raise TypeError(emsg) if hasattr(data_func, "nin"): self.nin = data_func.nin else: sig = inspect.signature(data_func) args = [ param for param in sig.parameters.values() if ( param.kind != param.KEYWORD_ONLY and param.default is param.empty ) ] self.nin = len(args) if self.nin not in [1, 2]: emsg = ( f"{self._data_func_name} requires {self.nin} input data " "arrays, the IFunc class currently only supports functions " "requiring 1 or 2 data arrays as input." ) raise ValueError(emsg) if hasattr(data_func, "nout"): if data_func.nout != 1: emsg = ( f"{self._data_func_name} returns {data_func.nout} objects, " "the IFunc class currently only supports functions " "returning a single object." ) raise ValueError(emsg) self.data_func = data_func self.units_func = units_func def __repr__(self): result = ( f"iris.analysis.maths.IFunc({self._data_func_name}, " f"{self._unit_func_name})" ) return result def __str__(self): result = ( f"IFunc constructed from the data function {self._data_func_name} " f"and the units function {self._unit_func_name}" ) return result
[docs] def __call__( self, cube, other=None, dim=None, in_place=False, new_name=None, **kwargs_data_func, ): """ Applies the ifunc to the cube(s). Args: * cube An instance of :class:`iris.cube.Cube`, whose data is used as the first argument to the data function. Kwargs: * other A cube, coord, ndarray or number whose data is used as the second argument to the data function. * new_name: Name for the resulting Cube. * in_place: Whether to create a new Cube, or alter the given "cube". * dim: Dimension along which to apply `other` if it's a coordinate that is not found in `cube` * kwargs_data_func: Keyword arguments that get passed on to the data_func. Returns: An instance of :class:`iris.cube.Cube`. """ _assert_is_cube(cube) def wrap_data_func(*args, **kwargs): kwargs_combined = dict(kwargs_data_func, **kwargs) return self.data_func(*args, **kwargs_combined) if self.nin == 1: if other is not None: dmsg = ( "ignoring surplus 'other' argument to IFunc.__call__, " f"provided data_func {self._data_func_name!r} only requires " "1 input" ) logger.debug(dmsg) new_unit = self.units_func(cube) new_cube = _math_op_common( cube, wrap_data_func, new_unit, in_place=in_place ) else: if other is None: emsg = ( f"{self._data_func_name} requires two arguments, another " "cube must also be passed to IFunc.__call__." ) raise ValueError(emsg) new_unit = self.units_func(cube, other) new_cube = _binary_op_common( wrap_data_func, self.data_func.__name__, cube, other, new_unit, dim=dim, in_place=in_place, ) if new_name is not None: new_cube.rename(new_name) return new_cube