Source code for pypeit.core.scattlight

""" Module for sky subtraction

.. include common links, assuming primary doc root is up one directory
.. include:: ../include/links.rst
"""
import numpy as np

from scipy.optimize import least_squares
from scipy import signal, interpolate, ndimage
from IPython import embed

from pypeit import msgs, utils


[docs]def pad_frame(_frame, detpad=300): """ Clean the edges of the input frame and then pad the frame to avoid edge effects. Parameters ---------- _frame : `numpy.ndarray`_ Frame to be padded detpad : int Number of pixels to pad the frame on each side Returns ------- _frame_pad : `numpy.ndarray`_ Padded frame """ _frame[0, :] = np.median(_frame[0:10, :], axis=0) _frame[-1, :] = np.median(_frame[-10:, :], axis=0) return np.pad(_frame, detpad, mode='edge') # Model should be generated on padded data
[docs]def scattered_light_model_pad(param, img, detpad=300): """ Construct a scattered light model for the input image, with the model parameters defined by param. This function is used to generate a model of the scattered light, based on a set of model parameters that have first been optimized using scattered_light(). The model is generated on a padded version of the input image, and then trimmed to match the input image size. Parameters ---------- param : `numpy.ndarray`_ Model parameters that determine the scattered light based on the input img. Here is a list of the individual parameter meanings: * param[0] = Gaussian kernel width in the spectral direction * param[1] = Gaussian kernel width in the spatial direction * param[2] = Lorentzian kernel width in the spectral direction * param[3] = Lorentzian kernel width in the spatial direction * param[4] = Pixel shift of the scattered light in the spectral direction * param[5] = Pixel shift of the scattered light in the spatial direction * param[6] = Zoom factor of the scattered light (~1) * param[7] = constant offset for scattered light (independent of img) * param[8] = Kernel angle * param[9] = Relative importance of Gaussian vs Lorentzian. 0 < value < 1 means Lorentzian is weighted more value > 1 means Gaussian is weighted more. * param[10:] = Polynomial scaling coefficients img : `numpy.ndarray`_ Image used to generate the scattered light model detpad : int Number of pixels to pad the frame on each side Returns ------- model : `numpy.ndarray`_ Model of the scattered light for the input """ _frame_pad = pad_frame(img, detpad=detpad) return scattered_light_model(param, _frame_pad)[detpad:-detpad, detpad:-detpad]
[docs]def scattered_light_model(param, img): """ Model used to calculate the scattered light. The current model to generate the scattered light is a shifted, scaled, and blurred version of the data recorded on the detector. We allow for a shift in the spatial and spectral direction, and the blurring kernel can have different widths in the spectral and spatial directions. Finally, the model includes a polynomial correction in the spectral direction to account for a change in the spectral efficiency of the scattered light relative to the detector image. Parameters ---------- param : `numpy.ndarray`_ Model parameters that determine the scattered light based on the input img. Here is a list of the individual parameter meanings: * param[0] = Gaussian kernel width in the spectral direction * param[1] = Gaussian kernel width in the spatial direction * param[2] = Lorentzian kernel width in the spectral direction * param[3] = Lorentzian kernel width in the spatial direction * param[4] = Pixel shift of the scattered light in the spectral direction * param[5] = Pixel shift of the scattered light in the spatial direction * param[6] = Zoom factor of the scattered light (~1) * param[7] = constant offset for scattered light (independent of img) * param[8] = Kernel angle * param[9] = Relative importance of Gaussian vs Lorentzian. 0 < value < 1 means Lorentzian is weighted more value > 1 means Gaussian is weighted more. * param[10:] = Polynomial scaling coefficients img : `numpy.ndarray`_ Raw image that you want to compute the scattered light model. shape is (nspec, nspat) Model used to calculate the scattered light. This function is used to generate a model of the scattered light, based on a set of model parameters that have been optimized using self.scattered_light(). Returns ------- model : `numpy.ndarray`_ Model of the scattered light for the input """ # Extract the parameters into more conveniently named variables sigmx_g, sigmy_g, sigmx_l, sigmy_l = param[0], param[1], param[2], param[3] shft_spec, shft_spat, zoom_spec, zoom_spat = param[4], param[5], param[6], param[7] constant, kern_angle, kern_scale = param[8], param[9], param[10] polyterms_spat = param[11:13] polyterms_spec = param[13:] # Make a grid of coordinates specvec, spatvec = np.arange(img.shape[0]), np.arange(img.shape[1]) spat, spec = np.meshgrid(spatvec/(spatvec.size-1), specvec/(specvec.size - 1)) # Generate the polynomial efficiency scaling in the spatial direction polyscale = spat*(polyterms_spat[0] + polyterms_spat[1]*spec) # linear term and a spectral cross-term # Now include the spectral direction for pp in range(polyterms_spec.size): polyscale += polyterms_spec[pp] * spec**pp # Generate a 2D smoothing kernel, composed of a 2D Gaussian and a 2D Lorentzian sigmx, sigmy = max(sigmx_g, sigmx_l), max(sigmy_g, sigmy_l), xkern, ykern = np.meshgrid(np.arange(int(10 * sigmx)) - 5 * sigmx, np.arange(int(10 * sigmy)) - 5 * sigmy) # Rotate the kernel xkernrot = (xkern * np.cos(kern_angle) - ykern * np.sin(kern_angle)) ykernrot = (xkern * np.sin(kern_angle) + ykern * np.cos(kern_angle)) # Create and normalise the Gaussian kernel kernel_gaussian = np.exp(-((xkernrot/sigmx_g)**2 + (ykernrot/sigmy_g)**2)) kernel_gaussian /= np.sum(kernel_gaussian) # Create and normalise the Lorenztian kernel kernel_lorentzian = 1 / ((xkernrot/sigmx_l) ** 2 + (ykernrot/sigmy_l) ** 2 + 1) kernel_lorentzian /= np.sum(kernel_lorentzian) # Add the individual kernels into a single kernel. Arbitrarily scale the Gaussian kernel (either is fine). # The point of this is to make it so either a lorentzian, gaussian, or something in-between can be # used as the kernel, making it more flexible for different spectrographs. kernel = kernel_lorentzian + kern_scale * kernel_gaussian kernel /= np.sum(kernel) scale_img = polyscale * signal.oaconvolve(img, kernel, mode='same') spl = interpolate.RectBivariateSpline(specvec, spatvec, scale_img, kx=1, ky=1) return constant + spl(zoom_spec * (specvec + shft_spec), zoom_spat * (spatvec + shft_spat))
[docs]def scattlight_resid(param, wpix, img): """ Residual function used to optimize the model parameters Parameters ---------- param : `numpy.ndarray`_ 1D array of model parameters to use for the fitting function. wpix : tuple A tuple containing the x,y coordinates of the pixels in img to be used for computing the residual. img : `numpy.ndarray`_ Data image to be used to compute the residual. Shape is (nspec, nspat) Returns ------- resid : `numpy.ndarray`_ A 1D vector of the residuals """ model = scattered_light_model(param, img) return img[wpix] - model[wpix]
[docs]def scattered_light(frame, bpm, offslitmask, x0, bounds, detpad=300, debug=False): """ Calculate a model of the scattered light of the input frame. Parameters ---------- frame : `numpy.ndarray`_ Raw 2D data frame (nspec, nspat) to be used to compute the scattered light. bpm : `numpy.ndarray`_ 2D boolean array indicating the bad pixels (True=bad), same shape as frame offslitmask : `numpy.ndarray`_ A boolean mask indicating the pixels that are on/off the slit (True = off the slit), same shape as frame x0 : `numpy.ndarray`_ A 1D array containing the best-fitting model parameters bounds : :obj:`tuple` A tuple of two elements, containing two `numpy.ndarray`_ of the same length as x0. These two arrays contain the lower (first element of the tuple) and upper (second element of the tuple) bounds to consider on the scattered light model parameters. debug : :obj:`bool`, optional If True, debug the final model fit that's been output Returns ------- scatt_img : `numpy.ndarray`_ A 2D image of the scattered light determined from the input frame. Alternatively, if a constant value is used, a constant floating point value can be returned as well. modelpar : `numpy.ndarray`_ A 1D array containing the best-fitting model parameters success : :obj:`bool` True if the fit was successful, False otherwise """ # Convert the BPM to a GPM for convenience gpm = np.logical_not(bpm) # Replace bad pixels with the nearest good pixel _frame = utils.replace_bad(frame, bpm) # Pad the edges of the data _frame_pad = pad_frame(_frame, detpad) offslitmask_pad = np.pad(offslitmask * gpm, detpad, mode='constant', constant_values=0) # but don't include padded data in the fit # Grab the pixels to be included in the fit wpix = np.where(offslitmask_pad) # Compute the best-fitting model parameters msgs.info("Performing a least-squares fit to the scattered light") res_lsq = least_squares(scattlight_resid, x0, bounds=bounds, args=(wpix, _frame_pad), verbose=2, ftol=1.0E-4) # Store if this is a successful fit success = res_lsq.success if success: msgs.info("Generating best-fitting scattered light model") scatt_img = scattered_light_model(res_lsq.x, _frame_pad)[detpad:-detpad, detpad:-detpad] else: msgs.warn("Scattered light model fitting failed") scatt_img = np.zeros_like(frame) if debug: # Do some checks on the results embed() scatt_img_alt = scattered_light_model(x0, _frame_pad)[detpad:-detpad, detpad:-detpad] from matplotlib import pyplot as plt vmin, vmax = 0, np.max(scatt_img)#40 plt.imshow(frame - scatt_img, vmin=-vmax/2, vmax=vmax/2) plt.show() print(res_lsq.x) plt.subplot(221) plt.imshow(_frame, vmin=vmin, vmax=vmax) plt.subplot(222) plt.imshow(scatt_img, vmin=vmin, vmax=vmax) plt.subplot(223) plt.imshow(frame - scatt_img, vmin=-vmax/2, vmax=vmax/2) plt.subplot(224) plt.imshow(_frame, vmin=-vmax/2, vmax=vmax/2) # plt.subplot(235) # plt.imshow(scatt_img_alt, vmin=vmin, vmax=vmax) # plt.subplot(236) # plt.imshow(_frame - scatt_img_alt, vmin=vmin, vmax=vmax) plt.show() return scatt_img, res_lsq.x, success
[docs]def mask_slit_regions(offslitmask, centrace, mask_regions=None): """ Exclude some user-specified inter-slit regions for the fine correction to the scattered light determination Parameters ---------- offslitmask : `numpy.ndarray`_ A boolean mask indicating the pixels that are on/off the slit (True = off the slit) centrace : `numpy.ndarray`_ A 2D array, shape is (nspec, nslit), containing the central trace of each slit. mask_regions : :obj:`int`, :obj:`list`, optional A list of regions that should be excluded in the fine correction to the scattered light. A zero value indicates that all pixels left of the first slit will be masked. A value of one indicates that all pixels between the first and second slit will be masked, and so forth. A list of these integers can be supplied. If None, no inter-slit regions will be excluded. If an integer is specified, just one region will be excluded. Returns ------- good_mask : `numpy.ndarray`_ A 2D boolean array indicating the pixels that are suitable to use for the fine scattered light correction. """ # Check if there are regions to be masked if mask_regions is None: msgs.warn("There are no inter-slit regions specified that need to be masked") return offslitmask elif isinstance(mask_regions, int): # Convert this to a list _mask_regions = [mask_regions] else: _mask_regions = mask_regions # Grab the dimensions nspec, nspat = offslitmask.shape nslit = centrace.shape[1] # Setup the pixel coordinates spat = np.arange(nspat) # Loop through all user-specified inter-slit regions to mask bad_mask = np.zeros_like(offslitmask) for ii in _mask_regions: if ii == 0: # All pixels to the left of the first slit mask_pix = spat[None, :] < centrace[:, ii, None] elif ii == nslit: # All pixels to the right of the last slit mask_pix = spat[None, :] > centrace[:, ii-1, None] else: # Everything else in between mask_pix = (spat[None, :] > centrace[:, ii-1, None]) \ & (spat[None, :] < centrace[:, ii, None]) # Exclude these pixels bad_mask[mask_pix] = True # Return the mask of good inter-slit pixels return offslitmask & np.logical_not(bad_mask)
[docs]def fine_correction(frame, bpm, offslitmask, method='median', polyord=2, debug=False): """ Calculate a fine correction to the residual scattered light of the input frame. Parameters ---------- frame : `numpy.ndarray`_ Raw 2D data frame (nspec, nspat) to be used to compute the fine correction of the scattered light. This frame should be the raw frame, minus the first estimate of the scattered light that has been derived from the :func:`scattered_light_model` function. bpm : `numpy.ndarray`_ 2D boolean array indicating the bad pixels (True=bad), same shape as frame offslitmask : `numpy.ndarray`_ A boolean mask indicating the pixels that are on/off the slit (True = off the slit), same shape as frame method : :obj:`str`, optional Method to use to determine the fine correction to the scattered light. Options are: - 'median': Use the median of the off-slit pixels to determine the scattered light - 'poly': Use a polynomial fit to the off-slit pixels to determine the scattered light. If this option is chosen, the polynomial order must be specified. See `polyord` below. polyord : :obj:`int`, optional Polynomial order to use for fitting the residual scattered light in the spatial direction. debug : :obj:`bool`, optional If True, debug the final model fit that's been output Returns ------- scatt_img : `numpy.ndarray`_ A 2D image (nspec, nspat) of the fine correction to the scattered light determined from the input frame. """ if method not in ['median', 'poly']: msgs.error("Unrecognized method to determine the fine correction to the scattered light: {:s}".format(method)) msgs.info("Performing a fine correction to the scattered light using the {:s} method".format(method)) nspec, nspat = frame.shape if method == 'median': # Use the median of the off-slit pixels to determine the scattered light mask = bpm | np.logical_not(offslitmask) frame_msk = np.ma.array(frame, mask=mask) scatt_light_fine = np.repeat(np.ma.median(frame_msk, axis=1).data[:, np.newaxis], nspat, axis=1) elif method == 'poly': # Convert the BPM to a GPM for convenience gpm = np.logical_not(bpm) # Define some useful variables xspat = np.linspace(0, 1, nspat) model = np.zeros_like(frame) # Loop over the residual scattered light in the spectral direction and perform # a low order polynomial fit to the scattered light in the spatial direction. for yy in range(nspec): ext = frame[yy, :] gd = np.where(offslitmask[yy, :] & gpm[yy, :]) coeff = np.polyfit(xspat[gd], ext[gd], polyord) model[yy, :] = np.polyval(coeff, xspat) # Median filter in the spectral direction to smooth out irregularities in the fine correction model_med = ndimage.median_filter(model, size=(50, 1)) # Median filter to get rid of CRs scatt_light_fine = ndimage.gaussian_filter(model_med, sigma=10) # Gaussian filter to smooth median filter if debug: from matplotlib import pyplot as plt vmin, vmax = -np.max(scatt_light_fine), np.max(scatt_light_fine) plt.subplot(121) plt.imshow(frame, vmin=vmin, vmax=vmax) plt.subplot(122) plt.imshow(frame-scatt_light_fine, vmin=vmin, vmax=vmax) plt.show() # Return the fine correction model of the scattered light return scatt_light_fine