Source code for pypeit.core.combine

""" Module for image combining

.. include:: ../include/links.rst
"""
import numpy as np

from astropy import stats

from pypeit import msgs
from pypeit import utils

from IPython import embed


# TODO make weights optional and do uniform weighting without.
[docs]def weighted_combine(weights, sci_list, var_list, inmask_stack, sigma_clip=False, sigma_clip_stack=None, sigrej=None, maxiters=5): r""" Combine multiple sets of images, all using the same weights and mask. The multiple sets of images and variances to combine must have the same shape --- ``(nimgs, nspec, nspat)`` --- and this shape must match the provided *single* mask set (``inmask_stack``). The provided weights are broadcast to the necessary shape (see below), where one can provide one weight per image, one weight per image spatial coordinate (i.e., wavelength-dependent weights), or independent weights for each pixel. Optionally, the image stack can be sigma-clipped by setting ``sigma_clip=True``. If sigma-clipping is requested and no sigma-rejection thresholds are provided (``sigrej`` is None), the sigma-rejection thresholds are set *automatically* depending on the number of images to combine. The default rejection thresholds are 1.1, 1.3, 1.6, 1.9, or 2.0 for, respectively, 3, 4, 5, 6, or :math:`\geq 7` images. Sigma-clipping cannot be performed if there are fewer than 3 images. The pixel rejection is based on a *single* image stack provided by ``sigma_clip_stack``, which does not necessarily need to be any of the image stacks provided by ``sci_list``. Pixels rejected by sigma-clipping the ``sigma_clip_stack`` array are applied to all image stacks in ``sci_list``. The combined images are collected into the returned image list, where the order of the list is identical to the input ``sci_list``. The returned mask and pixel accounting array is identical for all stacked images. Parameters ---------- weights : `numpy.ndarray`_ Weights to use. Options for the shape of weights are: - ``(nimgs,)``: a single weight per image in the stack - ``(nimgs, nspec)``: wavelength dependent weights per image in the stack - ``(nimgs, nspec, nspat)``: weights input with the shape of the image stack Note that the weights are distinct from the mask, which is dealt with via the ``inmask_stack`` argument, meaning there should not be any weights that are set to zero (although in principle this would still work). sci_list : :obj:`list` List of floating-point `numpy.ndarray`_ image groups to stack. Each image group *must* have the same shape: ``(nimgs, nspec, nspat)``. var_list : :obj:`list` List of floating-point `numpy.ndarray`_ images providing the variance for each image group. The number of image groups and the shape of each group must match ``sci_list``. These are used to propagate the error in the combined images. inmask_stack : `numpy.ndarray`_, boolean, shape (nimgs, nspec, nspat) Good-pixel mask (True=Good, False=Bad) for the input image stacks. This single group of good-pixel masks is applied to *all* input image groups. sigma_clip : :obj:`bool`, optional, default = False Combine with a mask by sigma clipping the image stack. Stacks can only be sigma-clipped if there are 3 or more images. sigma_clip_stack : `numpy.ndarray`_, float, shape (nimgs, nspec, nspat), optional, default = None The image stack to be used for the sigma clipping. For example, if the list of images to be combined with the weights is ``[sciimg_stack, waveimg_stack, tilts_stack]`` and you want to clip based on ``sciimg_stack``, you would set ``sigma_clip_stack=sciimg_stack``. sigrej : :obj:`int`, :obj:`float`, optional, default = None Rejection threshold for sigma clipping. If None and ``sigma_clip`` is True, the rejection threshold is set based on the number of images to combine; see above. This value is passed directly to `astropy.stats.SigmaClip`_ as its ``sigma`` parameter. maxiters : :obj:`int`, optional, default=5 Maximum number of rejection iterations; see `astropy.stats.SigmaClip`_. Returns ------- sci_list_out : :obj:`list` The list of ndarray float combined images with shape ``(nspec, nspat)``. var_list_out : :obj:`list` The list of ndarray propagated variance images with shape ``(nspec, nspat)``. gpm : boolean `numpy.ndarray`_, shape (nspec, nspat) Good pixel mask for combined image (True=Good, False=Bad). nused : integer `numpy.ndarray`_, shape (nspec, nspat) Number of pixels combined at each location in the stacked images. """ shape = img_list_error_check(sci_list, var_list) nimgs = shape[0] img_shape = shape[1:] #nspec = shape[1] #nspat = shape[2] if nimgs == 1: # If only one image is passed in, simply return the input lists of images, but reshaped # to be (nspec, nspat) msgs.warn('Cannot combine a single image. Returning input images') sci_list_out = [] for sci_stack in sci_list: sci_list_out.append(sci_stack.reshape(img_shape)) var_list_out = [] for var_stack in var_list: var_list_out.append(var_stack.reshape(img_shape)) gpm = inmask_stack.reshape(img_shape) nused = gpm.astype(int) return sci_list_out, var_list_out, gpm, nused if sigma_clip and nimgs >= 3: if sigma_clip_stack is None: msgs.error('You must specify sigma_clip_stack; sigma-clipping is based on this array ' 'and propagated to the arrays to be stacked.') if sigrej is None: # NOTE: If these are changed, make sure to update the doc-string! if nimgs == 3: sigrej = 1.1 elif nimgs == 4: sigrej = 1.3 elif nimgs == 5: sigrej = 1.6 elif nimgs == 6: sigrej = 1.9 else: sigrej = 2.0 # sigma clip if we have enough images # mask_stack > 0 is a masked value. numpy masked arrays are True for masked (bad) values data = np.ma.MaskedArray(sigma_clip_stack, mask=np.logical_not(inmask_stack)) sigclip = stats.SigmaClip(sigma=sigrej, maxiters=maxiters, cenfunc='median', stdfunc=utils.nan_mad_std) data_clipped, lower, upper = sigclip(data, axis=0, masked=True, return_bounds=True) mask_stack = np.logical_not(data_clipped.mask) # mask_stack = True are good values else: if sigma_clip and nimgs < 3: msgs.warn('Sigma clipping requested, but you cannot sigma clip with less than 3 ' 'images. Proceeding without sigma clipping') mask_stack = inmask_stack # mask_stack = True are good values nused = np.sum(mask_stack, axis=0) weights_stack = broadcast_weights(weights, shape) weights_mask_stack = weights_stack*mask_stack.astype(float) weights_sum = np.sum(weights_mask_stack, axis=0) inv_w_sum = 1./(weights_sum + (weights_sum == 0.0)) sci_list_out = [] for sci_stack in sci_list: sci_list_out.append(np.sum(sci_stack * weights_mask_stack, axis=0) * inv_w_sum) var_list_out = [] for var_stack in var_list: var_list_out.append(np.sum(var_stack * weights_mask_stack**2, axis=0) * inv_w_sum**2) # Was it masked everywhere? gpm = np.any(mask_stack, axis=0) return sci_list_out, var_list_out, gpm, nused
[docs]def img_list_error_check(sci_list, var_list): """ Utility routine for dealing with lists of image stacks for :func:`weighted_combine`. This routine checks that the images sizes are correct and routines the shape of the image stacks. Parameters ---------- sci_list : :obj:`list` List of float `numpy.ndarray`_ images (each being an image stack with shape ``(nimgs, nspec, nspat)``) which are to be combined with the weights, mask, and possibly sigma clipping. var_list : :obj:`list` List of float `numpy.ndarray`_ variance images (each being an image stack with shape ``(nimgs, nspec, nspat)``) which are to be combined with proper erorr propagation, i.e. using the weights squared, mask, and possibly sigma clipping. Returns ------- shape : :obj:`tuple` The shapes of the image stacks -- ``(nimgs, nspec, nspat)`` """ shape_sci_list = [] for img in sci_list: shape_sci_list.append(img.shape) if img.ndim < 2: msgs.error('Dimensionality of an image in sci_list is < 2') shape_var_list = [] for img in var_list: shape_var_list.append(img.shape) if img.ndim < 2: msgs.error('Dimensionality of an image in var_list is < 2') for isci in shape_sci_list: if isci != shape_sci_list[0]: msgs.error('An image in sci_list have different dimensions') for ivar in shape_var_list: if ivar != shape_var_list[0]: msgs.error('An image in var_list have different dimensions') if isci != ivar: msgs.error('An image in sci_list had different dimensions than an image in var_list') shape = shape_sci_list[0] return shape
[docs]def broadcast_weights(weights, shape): """ Utility routine to broadcast weights to be the size of image stacks specified by shape Args: weights (`numpy.ndarray`_): Weights to use. Options for the shape of weights are: - (nimgs,) -- a single weight per image in the stack - (nimgs, nspec) -- wavelength dependent weights per image - (nimgs, nspec, nspat) -- weights already have the shape of the image stack and are simply returned shape (tuple): Shape of the image stacks for weighted coadding. This is either (nimgs, nspec) for 1d extracted spectra or (nimgs, nspec, nspat) for 2d spectrum images Returns: `numpy.ndarray`_: Weights for the stack images with output shape described in the Args above. """ # Create the weights stack images from the wavelength dependent weights, i.e. propagate these # weights to the spatial direction if weights.ndim == 1: # One float per image if len(shape) == 2: weights_stack = np.einsum('i,ij->ij', weights, np.ones(shape)) elif len(shape) == 3: weights_stack = np.einsum('i,ijk->ijk', weights, np.ones(shape)) else: msgs.error('Image shape is not supported') elif weights.ndim == 2: # Wavelength dependent weights per image if len(shape) == 2: if weights.shape != shape: msgs.error('The shape of weights does not match the shape of the image stack') weights_stack = weights elif len(shape) == 3: weights_stack = np.einsum('ij,k->ijk', weights, np.ones(shape[2])) elif weights.ndim == 3: # Full image stack of weights if weights.shape != shape: msgs.error('The shape of weights does not match the shape of the image stack') weights_stack = weights else: msgs.error('Unrecognized dimensionality for weights') return weights_stack
[docs]def broadcast_lists_of_weights(weights, shapes): """ Utility routine to broadcast weights to be the size of image stacks specified by shape Parameters ---------- weights : :obj:`list` List containing the weights to use. The length of weights must be nimgs, the number of images that are being combined. The options for the date type/shape for the individual elements of weights are: - :obj:`float`: a single weight per image in the stack - `numpy.ndarray`_ with shape ``(nspec,)``: wavelength dependent weights per image in the stack - `numpy.ndarray`_ with shape ``(nspec, nspat)``: weights input with the shape of each image stack and will be simply be returned shapes : :obj:`list` List with length of nimgs containing the tuples which are the shapes ``(nspec, nspat)`` of each image in the stack that should have their weights broadcast. Returns ------- weights_list : :obj:`list` of `numpy.ndarray`_ objects Weight images where each image in the list has shape that was input via the shapes input parameter. """ # Create the weights stack images from the wavelength dependent weights, i.e. propagate these # weights to the spatial direction weights_list = [] for weight, shape in zip(weights, shapes): if isinstance(weight, float): weights_list.append(np.ones(shape, dtype=float) * weight) elif isinstance(weight, np.ndarray): if weight.ndim == 1: weights_list.append(np.broadcast_to(weight[:, np.newaxis], shape)) elif weight.ndim == 2: weights_list.append(weight) else: msgs.error('Weights must be a float or a 1D or 2D ndarray') return weights_list