def helper_clip(f, unit1, unit2, unit3): # Treat the array being clipped as primary. converters = [None] if unit1 is None: result_unit = dimensionless_unscaled try: converters += [(None if unit is None else get_converter(unit, dimensionless_unscaled)) for unit in (unit2, unit3)] except UnitsError: raise UnitConversionError( "Can only apply '{}' function to quantities with " "compatible dimensions".format(f.__name__)) else: result_unit = unit1 for unit in unit2, unit3: try: converter = get_converter(_d(unit), result_unit) except UnitsError: if unit is None: # special case: OK if unitless number is zero, inf, nan converters.append(False) else: raise UnitConversionError( "Can only apply '{}' function to quantities with " "compatible dimensions".format(f.__name__)) else: converters.append(converter) return converters, result_unit
def quantity(self, val): val = u.Quantity(val) try: val.to(self.unit) self.value = val.value self.unit = val.unit except UnitConversionError: raise UnitConversionError( "{0} parameter must have units homogeneous with {1}".format( self.name, self.unit))
def get_converters_and_unit(f, unit1, unit2): converters = [None, None] # By default, we try adjusting unit2 to unit1, so that the result will # be unit1 as well. But if there is no second unit, we have to try # adjusting unit1 (to dimensionless, see below). if unit2 is None: if unit1 is None: # No units for any input -- e.g., np.add(a1, a2, out=q) return converters, dimensionless_unscaled changeable = 0 # swap units. unit2 = unit1 unit1 = None elif unit2 is unit1: # ensure identical units is fast ("==" is slow, so avoid that). return converters, unit1 else: changeable = 1 # Try to get a converter from unit2 to unit1. if unit1 is None: try: converters[changeable] = get_converter(unit2, dimensionless_unscaled) except UnitsError: # special case: would be OK if unitless number is zero, inf, nan converters[1-changeable] = False return converters, unit2 else: return converters, dimensionless_unscaled else: try: converters[changeable] = get_converter(unit2, unit1) except UnitsError: raise UnitConversionError( "Can only apply '{0}' function to quantities " "with compatible dimensions" .format(f.__name__)) return converters, unit1
def converters_and_unit(function, method, *args): """Determine the required converters and the unit of the ufunc result. Converters are functions required to convert to a ufunc's expected unit, e.g., radian for np.sin; or to ensure units of two inputs are consistent, e.g., for np.add. In these examples, the unit of the result would be dimensionless_unscaled for np.sin, and the same consistent unit for np.add. Parameters ---------- function : `~numpy.ufunc` Numpy universal function method : str Method with which the function is evaluated, e.g., '__call__', 'reduce', etc. *args : Quantity or other ndarray subclass Input arguments to the function Raises ------ TypeError : when the specified function cannot be used with Quantities (e.g., np.logical_or), or when the routine does not know how to handle the specified function (in which case an issue should be raised on https://github.com/astropy/astropy). UnitTypeError : when the conversion to the required (or consistent) units is not possible. """ # Check whether we support this ufunc, by getting the helper function # (defined in helpers) which returns a list of function(s) that convert the # input(s) to the unit required for the ufunc, as well as the unit the # result will have (a tuple of units if there are multiple outputs). ufunc_helper = UFUNC_HELPERS[function] if method == '__call__' or (method == 'outer' and function.nin == 2): # Find out the units of the arguments passed to the ufunc; usually, # at least one is a quantity, but for two-argument ufuncs, the second # could also be a Numpy array, etc. These are given unit=None. units = [getattr(arg, 'unit', None) for arg in args] # Determine possible conversion functions, and the result unit. converters, result_unit = ufunc_helper(function, *units) if any(converter is False for converter in converters): # for multi-argument ufuncs with a quantity and a non-quantity, # the quantity normally needs to be dimensionless, *except* # if the non-quantity can have arbitrary unit, i.e., when it # is all zero, infinity or NaN. In that case, the non-quantity # can just have the unit of the quantity # (this allows, e.g., `q > 0.` independent of unit) try: # Don't fold this loop in the test above: this rare case # should not make the common case slower. for i, converter in enumerate(converters): if converter is not False: continue if can_have_arbitrary_unit(args[i]): converters[i] = None else: raise UnitConversionError( "Can only apply '{}' function to " "dimensionless quantities when other " "argument is not a quantity (unless the " "latter is all zero/infinity/nan)".format( function.__name__)) except TypeError: # _can_have_arbitrary_unit failed: arg could not be compared # with zero or checked to be finite. Then, ufunc will fail too. raise TypeError( "Unsupported operand type(s) for ufunc {}: " "'{}'".format( function.__name__, ','.join([arg.__class__.__name__ for arg in args]))) # In the case of np.power and np.float_power, the unit itself needs to # be modified by an amount that depends on one of the input values, # so we need to treat this as a special case. # TODO: find a better way to deal with this. if result_unit is False: if units[0] is None or units[0] == dimensionless_unscaled: result_unit = dimensionless_unscaled else: if units[1] is None: p = args[1] else: p = args[1].to(dimensionless_unscaled).value try: result_unit = units[0]**p except ValueError as exc: # Changing the unit does not work for, e.g., array-shaped # power, but this is OK if we're (scaled) dimensionless. try: converters[0] = units[0]._get_converter( dimensionless_unscaled) except UnitConversionError: raise exc else: result_unit = dimensionless_unscaled else: # methods for which the unit should stay the same nin = function.nin unit = getattr(args[0], 'unit', None) if method == 'at' and nin <= 2: if nin == 1: units = [unit] else: units = [unit, getattr(args[2], 'unit', None)] converters, result_unit = ufunc_helper(function, *units) # ensure there is no 'converter' for indices (2nd argument) converters.insert(1, None) elif method in {'reduce', 'accumulate', 'reduceat'} and nin == 2: converters, result_unit = ufunc_helper(function, unit, unit) converters = converters[:1] if method == 'reduceat': # add 'scale' for indices (2nd argument) converters += [None] else: if method in {'reduce', 'accumulate', 'reduceat', 'outer' } and nin != 2: raise ValueError( f"{method} only supported for binary functions") raise TypeError( "Unexpected ufunc method {}. If this should " "work, please raise an issue on" "https://github.com/astropy/astropy".format(method)) # for all but __call__ method, scaling is not allowed if unit is not None and result_unit is None: raise TypeError("Cannot use '{1}' method on ufunc {0} with a " "Quantity instance as the result is not a " "Quantity.".format(function.__name__, method)) if (converters[0] is not None or (unit is not None and unit is not result_unit and (not result_unit.is_equivalent(unit) or result_unit.to(unit) != 1.))): # NOTE: this cannot be the more logical UnitTypeError, since # then things like np.cumprod will not longer fail (they check # for TypeError). raise UnitsError( "Cannot use '{1}' method on ufunc {0} with a " "Quantity instance as it would change the unit.".format( function.__name__, method)) return converters, result_unit