Beispiel #1
0
def gen_psffield_direct(positions, fluxes=None, shape=(32, 32), kernel=None, factor=None):
    """Generate a point spread function field given a catalog of point source.

    Direct method

    Parameters
    ----------
    positions : array_like, shape (2, M)
        x, y positions in pixel coordinates
    fluxes : array_like, shape (M,)
        corresponding peak fluxes
    shape : int or [int, int], optional
        the output image shape
    kernel : ~astropy.convolution.Kernel2D, optional
        the 2D kernel to be used for the PSF
    factor : [int], optional
        a overpixelization factor used for the projection before smoothing, by default None

    Returns
    -------
    array : ndarray, shape(nx, ny)
        The corresponding map
    """
    if factor is None:
        factor = 1
    if fluxes is None:
        fluxes = np.ones(positions.shape[1])

    if isinstance(shape, (int, np.int)):
        shape = [shape, shape]

    _shape = np.array(shape) * factor
    # _positions = (np.asarray(positions) + 0.5) * factor - 0.5

    if kernel is not None:
        # Upscale the kernel with factor
        kernel = deepcopy(kernel)
        for param in ["x_stddev", "y_stddev", "width", "radius", "radius_in"]:
            if param in kernel._model.param_names:
                getattr(kernel._model, param).value *= factor

        Kernel2D.__init__(
            kernel,
            x_size=_round_up_to_odd_integer(kernel.shape[1] * factor),
            y_size=_round_up_to_odd_integer(kernel.shape[0] * factor),
        ),

    xx, yy = np.meshgrid(np.arange(_shape[1]), np.arange(_shape[0]))
    array = np.zeros(_shape)
    for position, flux in zip(positions.T, fluxes):
        kernel._model.x_mean = position[0]
        kernel._model.y_mean = position[1]
        kernel._model.amplitude = flux
        array += kernel._model(xx, yy)

    array = array.reshape((shape[0], factor, shape[1], factor)).sum(-1).sum(1)
    return array
Beispiel #2
0
    def gauss_weightratio_kernel(self, dist=1, row=0.5):
        '''
        Creates a 2D gauss window defined by the desired sum of weighting
        (ratio of weigthing, roe) between (centre - dist) and (centre + dist).
        This means, the inter-quartile range (IQR) has the
        extent of 2*dist.
        '''

        # interquartile range (IQR) of normal distribution
        iqr = 1.0 / norm.ppf(0.5 + row / 2)

        # standard deviaton of the gaussian kernel
        sdev = dist * iqr / 2

        # recommended size of array carrying the kernel
        rec_size = kernels._round_up_to_odd_integer(4.0 * sdev)
        if self.size != rec_size:
            print(
                "Kernel size is not properly chosen, to reflect gaussian function!"
            )

        # normalized gaussian kernel
        GaussKernel = kernels.Gaussian2DKernel(stddev=sdev,
                                               x_size=self.size,
                                               y_size=self.size)
        kernel = GaussKernel._array / np.sum(GaussKernel._array)

        self.array = kernel
Beispiel #3
0
    def __init__(self,
                 stddev_maj,
                 stddev_min,
                 position_angle,
                 support_scaling=8,
                 **kwargs):
        self._model = Gaussian2D(1. / (2 * np.pi * stddev_maj * stddev_min),
                                 0,
                                 0,
                                 x_stddev=stddev_maj,
                                 y_stddev=stddev_min,
                                 theta=position_angle)

        try:
            from astropy.modeling.utils import ellipse_extent
        except ImportError:
            raise NotImplementedError("EllipticalGaussian2DKernel requires"
                                      " astropy 1.1b1 or greater.")

        max_extent = \
            np.max(ellipse_extent(stddev_maj, stddev_min, position_angle))
        self._default_size = \
            _round_up_to_odd_integer(support_scaling * 2 * max_extent)
        super(EllipticalGaussian2DKernel, self).__init__(**kwargs)
        self._truncation = np.abs(1. - 1 / self._array.sum())
Beispiel #4
0
    def gauss_fwhm_kernel(self, fwhm=2.0):
        '''
        Creates a 2D gauss window defined by the desired full width at half maximum (FWHM).
        A window is created that features the centres' half weight at a distance
        of FWHM/2 from the center.

        '''

        # factor between sigma of filter and FWHM
        kappa = 2.0 * np.sqrt(2 * np.log(2))

        # "sigma" auf gaussian kernel
        sdev = fwhm / kappa

        # recommended size of array carrying the kernel
        rec_size = kernels._round_up_to_odd_integer(4.0 * sdev)
        if self.size != rec_size:
            print(
                "Kernel size is not properly chosen, to reflect gaussian function!"
            )

        # normalized gaussian kernel
        GaussKernel = kernels.Gaussian2DKernel(stddev=sdev,
                                               x_size=self.size,
                                               y_size=self.size)
        kernel = GaussKernel._array / np.sum(GaussKernel._array)

        self.array = kernel
Beispiel #5
0
    def __init__(self, stddev, ratio,
                 support_scaling=8, **kwargs):

        amp = 1. / (2 * np.pi * stddev**2 * (ratio**2 - 1))
        self._model = GaussianAnnulus2D(amp, 0, 0, stddev, ratio)

        self._default_size = _round_up_to_odd_integer(8 * stddev)
        super(AnnulusKernel, self).__init__(**kwargs)
        self._truncation = np.abs(1. - self._array.sum())
Beispiel #6
0
    def __init__(self, stddev_maj, stddev_min, position_angle, support_scaling=1,
                 **kwargs):

        self._model = Ellipse2D(1. / (np.pi * stddev_maj * stddev_min), 0, 0,
                                stddev_maj, stddev_min, position_angle)

        try:
            from astropy.modeling.utils import ellipse_extent
        except ImportError:
            raise NotImplementedError("EllipticalTophat2DKernel requires"
                                      " astropy 1.1b1 or greater.")

        max_extent = \
            np.max(ellipse_extent(stddev_maj, stddev_min, position_angle))
        self._default_size = \
            _round_up_to_odd_integer(support_scaling * 2 * max_extent)
        super(EllipticalTophat2DKernel, self).__init__(**kwargs)
        self._truncation = 0
Beispiel #7
0
    def __init__(self, stddev_maj, stddev_min, position_angle,
                 support_scaling=8, **kwargs):
        self._model = Gaussian2D(1. / (2 * np.pi * stddev_maj * stddev_min), 0,
                                 0, x_stddev=stddev_maj, y_stddev=stddev_min,
                                 theta=position_angle)

        try:
            from astropy.modeling.utils import ellipse_extent
        except ImportError:
            raise NotImplementedError("EllipticalGaussian2DKernel requires"
                                      " astropy 1.1b1 or greater.")

        max_extent = \
            np.max(ellipse_extent(stddev_maj, stddev_min, position_angle))
        self._default_size = \
            _round_up_to_odd_integer(support_scaling * 2 * max_extent)
        super(EllipticalGaussian2DKernel, self).__init__(**kwargs)
        self._truncation = np.abs(1. - 1 / self._array.sum())
Beispiel #8
0
    def __init__(self, stddev_maj, stddev_min, position_angle, support_scaling=1,
                 **kwargs):

        self._model = Ellipse2D(1. / (np.pi * stddev_maj * stddev_min), 0, 0,
                                stddev_maj, stddev_min, position_angle)

        try:
            from astropy.modeling.utils import ellipse_extent
        except ImportError:
            raise NotImplementedError("EllipticalTophat2DKernel requires"
                                      " astropy 1.1b1 or greater.")

        max_extent = \
            np.max(ellipse_extent(stddev_maj, stddev_min, position_angle))
        self._default_size = \
            _round_up_to_odd_integer(support_scaling * 2 * max_extent)
        super(EllipticalTophat2DKernel, self).__init__(**kwargs)
        self._truncation = 0
Beispiel #9
0
def gen_psffield(positions, fluxes=None, shape=(32, 32), kernel=None, factor=None):
    """Generate a point spread function field given a catalog of point source.

    Fourier method

    Parameters
    ----------
    positions : array_like, shape (2, M)
        x, y positions in pixel coordinates
    fluxes : array_like, shape (M,)
        corresponding peak fluxes
    shape : int or [int, int], optional
        the output image shape
    kernel : ~astropy.convolution.Kernel2D, optional
        the 2D kernel to be used for the PSF
    factor : [int], optional
        a overpixelization factor used for the projection before smoothing, by default None

    Returns
    -------
    array : ndarray, shape(nx, ny)
        The corresponding map
    """

    if factor is None:
        factor = 1
    if fluxes is None:
        fluxes = np.ones(positions.shape[1])

    if isinstance(shape, (int, np.int)):
        shape = [shape, shape]

    _shape = np.array(shape) * factor
    _positions = (np.asarray(positions) + 0.5) * factor - 0.5

    if kernel is not None:
        # Upscale the kernel with factor
        kernel = deepcopy(kernel)
        for param in ["x_stddev", "y_stddev", "width", "radius", "radius_in"]:
            if param in kernel._model.param_names:
                getattr(kernel._model, param).value *= factor

        Kernel2D.__init__(
            kernel,
            x_size=_round_up_to_odd_integer(kernel.shape[1] * factor),
            y_size=_round_up_to_odd_integer(kernel.shape[0] * factor),
        ),

    # Range are maximum bins edges
    hist2d_kwd = {"bins": _shape, "range": ((-0.5, _shape[0] - 0.5), (-0.5, _shape[1] - 0.5))}

    # reverse the _positions because it needs to be y x
    array = np.histogram2d(*_positions[::-1], weights=fluxes, **hist2d_kwd)[0]

    # Remove nan if present
    array[np.isnan(array)] = 0

    if kernel is not None:
        kernel.normalize("peak")
        array = convolve_fft(array, kernel, normalize_kernel=False, boundary="wrap") / factor ** 2

    # Average rebinning onto the input shape
    array = array.reshape((shape[0], factor, shape[1], factor)).sum(-1).sum(1)
    return array
Beispiel #10
0
def compute_ts_map_multiscale(maps, psf_parameters, scales=[0], downsample='auto',
                              residual=False, morphology='Gaussian2D', width=None,
                              *args, **kwargs):
    """
    Compute multiscale TS maps using compute_ts_map.

    High level TS map computation using a multi gauss PSF kernel and assuming
    a given source morphology. To optimize the performance the input data
    can be sampled down when computing TS maps on larger scales.

    Parameters
    ----------
    maps : `astropy.io.fits.HDUList`
        HDU list containing the data. The list must contain the following HDU extensions:
            * 'On', Counts image
            * 'Background', Background image
            * 'Diffuse', Diffuse model image
            * 'ExpGammaMap', Exposure image
    psf_parameters : dict
        Dict defining the multi gauss PSF parameters.
        See `~gammapy.irf.multi_gauss_psf` for details.
    scales : list ([0])
        List of scales to use for TS map computation.
    downsample : int ('auto')
        Down sampling factor. Can be set to 'auto' if the down sampling
        factor should be chosen automatically.
    residual : bool (False)
        Compute a TS residual map.
    morphology : str ('Gaussian2D')
        Source morphology assumption. Either 'Gaussian2D' or 'Shell2D'.

    Returns
    -------
    multiscale_result : list
        List of `TSMapResult` objects.
    """
    BINSZ = abs(maps[0].header['CDELT1'])
    shape = maps[0].data.shape
    multiscale_result = []

    for scale in scales:
        log.info('Computing {0}TS map for scale {1:.3f} deg and {2}'
                 ' morphology.'.format('residual ' if residual else '',
                                       scale, morphology))

        # Sample down and require that scale parameters is at least 5 pix
        if downsample == 'auto':
            factor = int(np.select([scale < 5 * BINSZ, scale < 10 * BINSZ,
                                    scale < 20 * BINSZ, scale < 40 * BINSZ],
                                   [1, 2, 4, 4], 8))
        else:
            factor = int(downsample)
        if factor == 1:
            log.info('No down sampling used.')
            downsampled = False
        else:
            if morphology == 'Shell2D':
                factor /= 2
            log.info('Using down sampling factor of {0}'.format(factor))
            downsampled = True

        funcs = [np.nansum, np.mean, np.nansum, np.nansum, np.nansum]
        maps_ = {}
        for map_, func in zip(maps, funcs):
            if downsampled:
                maps_[map_.name.lower()] = downsample_2N(map_.data, factor, func,
                                                         shape=shape_2N(shape))
            else:
                maps_[map_.name.lower()] = map_.data

        # Set up PSF and source kernel
        kernel = multi_gauss_psf_kernel(psf_parameters, BINSZ=BINSZ,
                                        NEW_BINSZ=BINSZ * factor,
                                        mode='oversample')

        if scale > 0:
            from astropy.convolution import convolve
            sigma = scale / (BINSZ * factor)
            if morphology == 'Gaussian2D':
                source_kernel = Gaussian2DKernel(sigma, mode='oversample')
            elif morphology == 'Shell2D':
                model = Shell2D(1, 0, 0, sigma, sigma * width)
                x_size = _round_up_to_odd_integer(2 * sigma * (1 + width)
                                                  + kernel.shape[0] / 2)
                source_kernel = Model2DKernel(model, x_size=x_size, mode='oversample')
            else:
                raise ValueError('Unknown morphology: {}'.format(morphology))
            kernel = convolve(source_kernel, kernel)
            kernel.normalize()

        # Compute TS map
        if residual:
            background = (maps_['background'] + maps_['diffuse'] + maps_['onmodel'])
        else:
            background = maps_['background']  # + maps_['diffuse']
        ts_results = compute_ts_map(maps_['on'], background, maps_['expgammamap'],
                                    kernel, *args, **kwargs)
        log.info('TS map computation took {0:.1f} s \n'.format(ts_results.runtime))
        ts_results['scale'] = scale
        ts_results['morphology'] = morphology
        if downsampled:
            for name, order in zip(['ts', 'sqrt_ts', 'amplitude', 'niter'], [1, 1, 1, 0]):
                ts_results[name] = upsample_2N(ts_results[name], factor,
                                               order=order, shape=shape)
        multiscale_result.append(ts_results)
    return multiscale_result
Beispiel #11
0
def compute_ts_image_multiscale(images, psf_parameters, scales=[0], downsample='auto',
                                residual=False, morphology='Gaussian2D', width=None,
                                **kwargs):
    """Compute multi-scale TS images using ``compute_ts_image``.

    High level TS image computation using a multi-Gauss PSF kernel and assuming
    a given source morphology. To optimize the performance the input data
    can be sampled down when computing TS images on larger scales.

    Parameters
    ----------
    images : `~gammapy.image.SkyImageList`
        Image collection containing the data. Must contain the following:
            * 'counts', Counts image
            * 'background', Background image
            * 'exposure', Exposure image
    psf_parameters : dict
        Dict defining the multi gauss PSF parameters.
        See `~gammapy.irf.multi_gauss_psf` for details.
    scales : list ([0])
        List of scales to use for TS image computation.
    downsample : int ('auto')
        Down sampling factor. Can be set to 'auto' if the down sampling
        factor should be chosen automatically.
    residual : bool (False)
        Compute a TS residual image.
    morphology : str ('Gaussian2D')
        Source morphology assumption. Either 'Gaussian2D' or 'Shell2D'.
    **kwargs : dict
        Keyword arguments forwarded to `TSImageEstimator`.

    Returns
    -------
    multiscale_result : list
        List of `~gammapy.image.SkyImageList` objects.
    """
    BINSZ = abs(images['counts'].wcs.wcs.cdelt[0])
    shape = images['counts'].data.shape

    multiscale_result = []

    ts_estimator = TSImageEstimator(**kwargs)

    for scale in scales:
        log.info('Computing {}TS image for scale {:.3f} deg and {}'
                 ' morphology.'.format('residual ' if residual else '',
                                       scale,
                                       morphology))

        # Sample down and require that scale parameters is at least 5 pix
        if downsample == 'auto':
            factor = int(np.select([scale < 5 * BINSZ, scale < 10 * BINSZ,
                                    scale < 20 * BINSZ, scale < 40 * BINSZ],
                                   [1, 2, 4, 4], 8))
        else:
            factor = int(downsample)

        if factor == 1:
            log.info('No down sampling used.')
            downsampled = False
        else:
            if morphology == 'Shell2D':
                factor /= 2
            log.info('Using down sampling factor of {}'.format(factor))
            downsampled = True

        funcs = [np.nansum, np.mean, np.nansum, np.nansum, np.nansum]

        images_downsampled = SkyImageList()
        for name, func in zip(images.names, funcs):
            if downsampled:
                pad_width = symmetric_crop_pad_width(shape, shape_2N(shape))
                images_downsampled[name] = images[name].pad(pad_width)
                images_downsampled[name] = images_downsampled[name].downsample(factor, func)
            else:
                images_downsampled[name] = images[name]

        # Set up PSF and source kernel
        kernel = multi_gauss_psf_kernel(psf_parameters, BINSZ=BINSZ,
                                        NEW_BINSZ=BINSZ * factor,
                                        mode='oversample')

        if scale > 0:
            from astropy.convolution import convolve
            sigma = scale / (BINSZ * factor)
            if morphology == 'Gaussian2D':
                source_kernel = Gaussian2DKernel(sigma, mode='oversample')
            elif morphology == 'Shell2D':
                model = Shell2D(1, 0, 0, sigma, sigma * width)
                x_size = _round_up_to_odd_integer(2 * sigma * (1 + width) + kernel.shape[0] / 2)
                source_kernel = Model2DKernel(model, x_size=x_size, mode='oversample')
            else:
                raise ValueError('Unknown morphology: {}'.format(morphology))
            kernel = convolve(source_kernel, kernel)
            kernel.normalize()

        if residual:
            images_downsampled['background'].data += images_downsampled['model'].data

        # Compute TS image
        ts_results = ts_estimator.run(images_downsampled, kernel)

        log.info('TS image computation took {0:.1f} s \n'.format(ts_results.meta['runtime']))
        ts_results.meta['MORPH'] = (morphology, 'Source morphology assumption')
        ts_results.meta['SCALE'] = (scale, 'Source morphology size scale in deg')

        if downsampled:
            for name, order in zip(['ts', 'sqrt_ts', 'flux', 'flux_err', 'niter'], [1, 1, 1, 0]):
                ts_results[name] = ts_results[name].upsample(factor, order=order)
                ts_results[name] = ts_results[name].crop(crop_width=pad_width)

        multiscale_result.append(ts_results)

    return multiscale_result
Beispiel #12
0
def compute_ts_image_multiscale(images,
                                psf_parameters,
                                scales=[0],
                                downsample='auto',
                                residual=False,
                                morphology='Gaussian2D',
                                width=None,
                                *args,
                                **kwargs):
    """
    Compute multi-scale TS images using ``compute_ts_image``.

    High level TS image computation using a multi-Gauss PSF kernel and assuming
    a given source morphology. To optimize the performance the input data
    can be sampled down when computing TS images on larger scales.

    Parameters
    ----------
    images : `~gammapy.image.SkyImageList`
        Image collection containing the data. Must contain the following:
            * 'counts', Counts image
            * 'background', Background image
            * 'exposure', Exposure image
    psf_parameters : dict
        Dict defining the multi gauss PSF parameters.
        See `~gammapy.irf.multi_gauss_psf` for details.
    scales : list ([0])
        List of scales to use for TS image computation.
    downsample : int ('auto')
        Down sampling factor. Can be set to 'auto' if the down sampling
        factor should be chosen automatically.
    residual : bool (False)
        Compute a TS residual image.
    morphology : str ('Gaussian2D')
        Source morphology assumption. Either 'Gaussian2D' or 'Shell2D'.

    Returns
    -------
    multiscale_result : list
        List of `~gammapy.image.SkyImageList` objects.
    """
    BINSZ = abs(images['counts'].wcs.wcs.cdelt[0])
    shape = images['counts'].data.shape

    multiscale_result = []

    for scale in scales:
        log.info(
            'Computing {0}TS image for scale {1:.3f} deg and {2}'
            ' morphology.'.format('residual ' if residual else '', scale,
                                  morphology)
        )  # Sample down and require that scale parameters is at least 5 pix
        if downsample == 'auto':
            factor = int(
                np.select([
                    scale < 5 * BINSZ, scale < 10 * BINSZ, scale < 20 * BINSZ,
                    scale < 40 * BINSZ
                ], [1, 2, 4, 4], 8))
        else:
            factor = int(downsample)

        if factor == 1:
            log.info('No down sampling used.')
            downsampled = False
        else:
            if morphology == 'Shell2D':
                factor /= 2
            log.info('Using down sampling factor of {0}'.format(factor))
            downsampled = True

        funcs = [np.nansum, np.mean, np.nansum, np.nansum, np.nansum]

        images2 = SkyImageList()
        for name, func in zip(images.names, funcs):
            if downsampled:
                pad_width = symmetric_crop_pad_width(shape, shape_2N(shape))
                images2[name] = images[name].pad(pad_width)
                images2[name] = images2[name].downsample(factor, func)
            else:
                images2[name] = images[name]

        # Set up PSF and source kernel
        kernel = multi_gauss_psf_kernel(psf_parameters,
                                        BINSZ=BINSZ,
                                        NEW_BINSZ=BINSZ * factor,
                                        mode='oversample')

        if scale > 0:
            from astropy.convolution import convolve
            sigma = scale / (BINSZ * factor)
            if morphology == 'Gaussian2D':
                source_kernel = Gaussian2DKernel(sigma, mode='oversample')
            elif morphology == 'Shell2D':
                model = Shell2D(1, 0, 0, sigma, sigma * width)
                x_size = _round_up_to_odd_integer(2 * sigma * (1 + width) +
                                                  kernel.shape[0] / 2)
                source_kernel = Model2DKernel(model,
                                              x_size=x_size,
                                              mode='oversample')
            else:
                raise ValueError('Unknown morphology: {}'.format(morphology))
            kernel = convolve(source_kernel, kernel)
            kernel.normalize()

        if residual:
            images2['background'].data += images2['model'].data

        # Compute TS image
        ts_results = compute_ts_image(images2['counts'], images2['background'],
                                      images2['exposure'], kernel, *args,
                                      **kwargs)
        log.info('TS image computation took {0:.1f} s \n'.format(
            ts_results.meta['runtime']))
        ts_results.meta['MORPH'] = (morphology, 'Source morphology assumption')
        ts_results.meta['SCALE'] = (scale,
                                    'Source morphology size scale in deg')

        if downsampled:
            for name, order in zip(['ts', 'sqrt_ts', 'amplitude', 'niter'],
                                   [1, 1, 1, 0]):
                ts_results[name] = ts_results[name].upsample(factor,
                                                             order=order)
                ts_results[name] = ts_results[name].crop(crop_width=pad_width)

        multiscale_result.append(ts_results)

    return multiscale_result
Beispiel #13
0
def compute_ts_map_multiscale(maps,
                              psf_parameters,
                              scales=[0],
                              downsample='auto',
                              residual=False,
                              morphology='Gaussian2D',
                              width=None,
                              *args,
                              **kwargs):
    """
    Compute multiscale TS maps using compute_ts_map.

    High level TS map computation using a multi gauss PSF kernel and assuming
    a given source morphology. To optimize the performance the input data
    can be sampled down when computing TS maps on larger scales.

    Parameters
    ----------
    maps : `astropy.io.fits.HDUList`
        HDU list containing the data. The list must contain the following HDU extensions:
            * 'On', Counts image
            * 'Background', Background image
            * 'Diffuse', Diffuse model image
            * 'ExpGammaMap', Exposure image
    psf_parameters : dict
        Dict defining the multi gauss PSF parameters.
        See `~gammapy.irf.multi_gauss_psf` for details.
    scales : list ([0])
        List of scales to use for TS map computation.
    downsample : int ('auto')
        Down sampling factor. Can be set to 'auto' if the down sampling
        factor should be chosen automatically.
    residual : bool (False)
        Compute a TS residual map.
    morphology : str ('Gaussian2D')
        Source morphology assumption. Either 'Gaussian2D' or 'Shell2D'.

    Returns
    -------
    multiscale_result : list
        List of `TSMapResult` objects.
    """
    BINSZ = abs(maps[0].header['CDELT1'])
    shape = maps[0].data.shape
    multiscale_result = []

    for scale in scales:
        logging.info('Computing {0}TS map for scale {1:.3f} deg and {2}'
                     ' morphology.'.format('residual ' if residual else '',
                                           scale, morphology))

        # Sample down and require that scale parameters is at least 5 pix
        if downsample == 'auto':
            factor = int(
                np.select([
                    scale < 5 * BINSZ, scale < 10 * BINSZ, scale < 20 * BINSZ,
                    scale < 40 * BINSZ
                ], [1, 2, 4, 8], 16))
        else:
            factor = int(downsample)
        if factor == 1:
            logging.info('No down sampling used.')
            downsampled = False
        else:
            if morphology == 'Shell2D':
                factor /= 2
            logging.info('Using down sampling factor of {0}'.format(factor))
            downsampled = True

        funcs = [np.nansum, np.mean, np.nansum, np.nansum, np.nansum]
        maps_ = {}
        for map_, func in zip(maps, funcs):
            if downsampled:
                maps_[map_.name.lower()] = downsample_2N(map_.data,
                                                         factor,
                                                         func,
                                                         shape=shape_2N(shape))
            else:
                maps_[map_.name.lower()] = map_.data

        # Set up PSF and source kernel
        kernel = multi_gauss_psf_kernel(psf_parameters,
                                        BINSZ=BINSZ,
                                        NEW_BINSZ=BINSZ * factor,
                                        mode='oversample')

        if scale > 0:
            from astropy.convolution import convolve
            sigma = scale / (BINSZ * factor)
            if morphology == 'Gaussian2D':
                source_kernel = Gaussian2DKernel(sigma, mode='oversample')
            elif morphology == 'Shell2D':
                model = Shell2D(1, 0, 0, sigma, sigma * width)
                x_size = _round_up_to_odd_integer(2 * sigma * (1 + width) +
                                                  kernel.shape[0] / 2)
                source_kernel = Model2DKernel(model,
                                              x_size=x_size,
                                              mode='oversample')
            else:
                raise ValueError('Unknown morphology: {}'.format(morphology))
            kernel = convolve(source_kernel, kernel)
            kernel.normalize()

        # Compute TS map
        if residual:
            background = (maps_['background'] + maps_['diffuse'] +
                          maps_['onmodel'])
        else:
            background = maps_['background']  # + maps_['diffuse']
        ts_results = compute_ts_map(maps_['on'], background,
                                    maps_['expgammamap'], kernel, *args,
                                    **kwargs)
        logging.info('TS map computation took {0:.1f} s \n'.format(
            ts_results.runtime))
        ts_results['scale'] = scale
        ts_results['morphology'] = morphology
        if downsampled:
            for name, order in zip(['ts', 'sqrt_ts', 'amplitude', 'niter'],
                                   [1, 1, 1, 0]):
                ts_results[name] = upsample_2N(ts_results[name],
                                               factor,
                                               order=order,
                                               shape=shape)
        multiscale_result.append(ts_results)
    return multiscale_result