def idft_map(input_vis, shape, uv, center=(0.0, 0.0) * u.arcsec, pixel_size=(1.0, 1.0) * u.arcsec): r""" Inverse discrete Fourier transform in terms of coordinates returning a 2D real array or image. Parameters ---------- input_vis : `numpy.ndarray` Array of N `complex` input visibilities shape : `float` (m,n) The shape of the output arry to create uv : `numpy.ndarray` Array of 2xN u, v coordinates corresponding to the input visibilities in `input_vis` center : `float` (x, y), optional Coordinates of the center of the map e.g. ``(0,0)`` or ``[5.0, -2.0]`` pixel_size : `float` (dx, dy), optional The pixel size in x and y directions, need not be square e.g. ``(1, 3)`` Returns ------- `numpy.ndarray` The complex visibilities evaluated at the u, v coordinates """ m, n = shape # size = m * n y = generate_xy(m, center[1], pixel_size[1]) x = generate_xy(n, center[0], pixel_size[0]) x, y = np.meshgrid(x, y) # Check units are correct for exp need to be dimensionless and then remove units for speed if (uv[0, :] * x[0, 0]).unit == u.dimensionless_unscaled and \ (uv[1, :] * y[0, 0]).unit == u.dimensionless_unscaled: uv = uv.value x = x.value y = y.value image = np.sum( (1 / input_vis.size) * input_vis * np.exp(2j * np.pi * (x[..., np.newaxis] * uv[np.newaxis, 0, :] + y[..., np.newaxis] * uv[np.newaxis, 1, :])), axis=2) return np.real(image) else: raise UnitsError( "Incompatible units on uv {uv.unit} should cancel with xy " "to leave a dimensionless quantity")
def dft_map(input_array, uv, center=(0.0, 0.0) * u.arcsec, pixel_size=(1.0, 1.0) * u.arcsec): r""" Discrete Fourier transform in terms of coordinates returning 1-D array complex visibilities. Parameters ---------- input_array : `numpy.ndarray` Input array to be transformed should be 2D (m, n) uv : `numpy.array` Array of 2xN u, v coordinates where the visibilities will be evaluated center : `float` (x, y), optional Coordinates of the center of the map e.g. ``(0,0)`` or ``[5.0, -2.0]`` pixel_size : `float` (dx, dy), optional The pixel size in x and y directions, need not be square e.g. ``(1, 3)`` Returns ------- `numpy.ndarray` Array of N `complex` visibilities evaluated at the u, v coordinates \ given bu `uv` """ m, n = input_array.shape y = generate_xy(m, center[1], pixel_size[1]) x = generate_xy(n, center[0], pixel_size[0]) x, y = np.meshgrid(x, y) # Check units are correct for exp need to be dimensionless and then remove units for speed if (uv[0, :] * x[0, 0]).unit == u.dimensionless_unscaled and \ (uv[1, :] * y[0, 0]).unit == u.dimensionless_unscaled: uv = uv.value x = x.value y = y.value vis = np.sum(input_array[..., np.newaxis] * np.exp(-2j * np.pi * (x[..., np.newaxis] * uv[np.newaxis, 0, :] + y[..., np.newaxis] * uv[np.newaxis, 1, :])), axis=(0, 1)) return vis else: raise UnitsError( "Incompatible units on uv {uv.unit} should cancel with xy " "to leave a dimensionless quantity")
def wrapper(*func_args, **func_kwargs): # Bind the arguments to our new function to the # signature of the original. bound_args = wrapped_signature.bind(*func_args, **func_kwargs) # Iterate through the parameters of the original signature for param in wrapped_signature.parameters.values(): # We do not support variable arguments (*args, **kwargs) if param.kind in (funcsigs.Parameter.VAR_KEYWORD, funcsigs.Parameter.VAR_POSITIONAL): continue # Catch the (never triggered) case where bind relied on # a default value. if (param.name not in bound_args.arguments and param.default is not param.empty): bound_args.arguments[param.name] = param.default # Get the value of this parameter (argument to new function) arg = bound_args.arguments[param.name] # Get target unit, either from decorator kwargs or annotations if param.name in self.decorator_kwargs: (target_min, target_max, target_unit) = self.decorator_kwargs[param.name] else: continue # If the target unit is empty, then no unit was specified # so we move past it if target_unit is not funcsigs.Parameter.empty: # skip over None values, if desired if arg is None and self.allow_none: continue try: equivalent = arg.unit.is_equivalent( target_unit, equivalencies=self.equivalencies) if not equivalent: raise UnitsError("Argument '{0}' to function '{1}'" " must be in units convertible to" " '{2}'.".format( param.name, wrapped_function.__name__, target_unit.to_string())) # Either there is no .unit or no .is_equivalent except AttributeError: if hasattr(arg, "unit"): error_msg = ("a 'unit' attribute without an " "'is_equivalent' method") else: error_msg = "no 'unit' attribute" raise TypeError( "Argument '{0}' to function '{1}' has {2}. You " "may want to pass in an astropy Quantity " "instead.".format(param.name, wrapped_function.__name__, error_msg)) # test value range if target_min is not None: quantity = bound_args.arguments[param.name] value = quantity.to(target_unit).value if np.any(value < target_min): raise ValueError( "Argument '{0}' to function '{1}' out of " "range (allowed {2} to {3} {4}).".format( param.name, wrapped_function.__name__, target_min, target_max, target_unit, )) if target_max is not None: quantity = bound_args.arguments[param.name] value = quantity.to(target_unit).value if np.any(value > target_max): raise ValueError( "Argument '{0}' to function '{1}' out of " "range (allowed {2} to {3} {4}).".format( param.name, wrapped_function.__name__, target_min, target_max, target_unit, )) if self.strip_input_units: bound_args.arguments[param.name] = ( bound_args.arguments[param.name].to( target_unit).value) # Call the original function with any equivalencies in force. with add_enabled_equivalencies(self.equivalencies): # result = wrapped_function(*func_args, **func_kwargs) result = wrapped_function(*bound_args.args, **bound_args.kwargs) if self.output_unit is not None: # test, if return values are tuple-like try: # make namedtuples work (as well as tuples) if hasattr(result, '_fields'): cls = result.__class__ return cls(*( # r if u is None else Quantity(r, u, subok=True) r if u is None else r * u # deal with astropy bug for r, u in zip(result, self.output_unit))) else: return tuple( # r if u is None else Quantity(r, u, subok=True) r if u is None else r * u # deal with astropy bug for r, u in zip(result, self.output_unit)) except TypeError: return (result if self.output_unit is None else # Quantity(result, self.output_unit, subok=True) result * self.output_unit # deal with astropy bug ) else: return result
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