Source code for pypeit.core.findobj_skymask

""" Methods to find objects

.. include:: ../include/links.rst

"""
import copy

import numpy as np
import scipy.interpolate
import scipy.ndimage
import matplotlib.pyplot as plt

import astropy.stats

from pypeit import msgs
from pypeit import utils
from pypeit import specobj
from pypeit import specobjs
from pypeit.core import pydl
from pypeit.core import fitting
from pypeit.core.moment import moment1d
from pypeit import tracepca
from pypeit.core.trace import fit_trace
from pypeit.core import arc
from pypeit.display import display
from pypeit.core import pixels
from pypeit.core import extract
from pypeit.utils import fast_running_median
from IPython import embed


[docs]def create_skymask(sobjs, thismask, slit_left, slit_righ, box_rad_pix=None, trim_edg=(5,5), skymask_snr_thresh=1.0): r""" Creates a sky mask from a :class:`~pypeit.specobjs.SpecObjs` using the fwhm of each object and/or the boxcar radius. Args: sobjs (:class:`~pypeit.specobjs.SpecObjs`): Objects for which you would like to create the mask. thismask (`numpy.ndarray`_): Boolean image selecting pixels that are on the slit. Shape is :math:`(N_{\rm spec}, N_{\rm spat})`. slit_left (`numpy.ndarray`_): Left boundary of slit/order to be extracted (given as floating point pixels). This a 1-d array with shape :math:`(N_{\rm spec}, 1)` or :math:`(N_{\rm spec},)` slit_righ (`numpy.ndarray`_): Right boundary of slit/order to be extracted (given as floating point pixels). This a 1-d array with shape :math:`(N_{\rm spec}, 1)` or :math:`(N_{\rm spec},)` box_rad_pix (:obj:`float`, optional): If set, the sky mask will be as wide as this radius in pixels. trim_edg (:obj:`tuple`, optional): A two-tuple of integers or floats used to ignore objects within this many pixels of the left and right slit boundaries, respectively. skymask_snr_thresh (:obj:`float`, optional): The multiple of the final object finding SNR threshold used to create the sky mask via a Gaussian model of the SNR profile. The (spatial) Gaussian model is determined from the image after collapsing along the spectral dimension. Returns: `numpy.ndarray`_: Boolean image with shape :math:`(N_{\rm spec}, N_{\rm spat})` (same as ``thismask``) indicating which pixels are usable for global sky subtraction (True means the pixel is usable for sky subtraction, False means it should be masked when subtracting sky). """ nobj = len(sobjs) ximg, _ = pixels.ximg_and_edgemask(slit_left, slit_righ, thismask, trim_edg=trim_edg) # How many pixels wide is the slit at each Y? xsize = slit_righ - slit_left #nsamp = np.ceil(np.median(xsize)) # JFH Changed 07-07-19 nsamp = np.ceil(xsize.max()) # Objmask skymask_objsnr = np.copy(thismask) if nobj == 0: msgs.info('No objects were detected. The entire slit will be used to determine the sky subtraction.') else: # Compute some inputs for the object mask xtmp = (np.arange(nsamp) + 0.5)/nsamp # threshold for object finding for iobj in range(nobj): # this will skip also sobjs with THRESHOLD=0, because are the same that have smash_snr=0. if (sobjs[iobj].smash_snr != 0.) and (sobjs[iobj].smash_snr != None): qobj = np.zeros_like(xtmp) sep = np.abs(xtmp-sobjs[iobj].SPAT_FRACPOS) sep_inc = sobjs[iobj].maskwidth/nsamp close = sep <= sep_inc # This is an analytical SNR profile with a Gaussian shape. # JFH modified to use SNR here instead of smash peakflux. I believe that the 2.77 is supposed to be # 2.355**2/2, i.e. the argument of a gaussian with sigma = FWHM/2.35 qobj[close] = sobjs[iobj].smash_snr * \ np.exp(np.fmax(-2.77*(sep[close]*nsamp)**2/sobjs[iobj].FWHM**2, -9.0)) skymask_objsnr[thismask] &= np.interp(ximg[thismask], xtmp, qobj) < skymask_snr_thresh # FWHM skymask_fwhm = np.copy(thismask) if nobj > 0: nspec, nspat = thismask.shape # spatial position everywhere along image spat_img = np.outer(np.ones(nspec, dtype=int),np.arange(nspat, dtype=int)) # Boxcar radius? if box_rad_pix is not None: msgs.info("Using boxcar radius for masking") # Loop me for iobj in range(nobj): # Create a mask for the pixels that will contribute to the object skymask_radius = box_rad_pix if box_rad_pix is not None else sobjs[iobj].FWHM msgs.info(f"Masking around object {iobj+1} within a radius = {skymask_radius} pixels") slit_img = np.outer(sobjs[iobj].TRACE_SPAT, np.ones(nspat)) # central trace replicated spatially objmask_now = thismask & (spat_img > (slit_img - skymask_radius)) & (spat_img < (slit_img + skymask_radius)) skymask_fwhm &= np.invert(objmask_now) # Check that we have not performed too much masking if (np.sum(skymask_fwhm)/np.sum(thismask) < 0.10): msgs.warn('More than 90% of usable area on this slit would be masked and not used by global sky subtraction. ' 'Something is probably wrong with object finding for this slit. Not masking object for global sky subtraction.') skymask_fwhm = np.copy(thismask) # Still have to make the skymask # # TODO -- Make sure this is right # if box_rad_pix is None: # skymask = skymask_objsnr | skymask_fwhm # else: # Enforces boxcar radius masking # skymask = skymask_objsnr & skymask_fwhm # DP: I think skymask should always be skymask_objsnr & skymask_fwhm (i.e., not only when box_rad_pix is not None). # In the case of skymask_objsnr | skymask_fwhm, if skymask_objsnr cannot be computed, the entire slit # is used for sky calculation (i.e., skymask_fwhm will not have effect). # DP's change which I don't think we should adopt at this time. #skymask = skymask_objsnr & skymask_fwhm # JFH restored old behavior after seeing spurious results for X-shooter. I think the issue here is that the fwhm # computation from objs_in_slit is not necessarily that reliable and when large amounts of masking are performed # on narrow slits/orders, we have problems. We should revisit this after object finding is refactored since # maybe then the fwhm estimates will be more robust. if box_rad_pix is None and np.all([sobj.smash_snr is not None for sobj in sobjs]) \ and np.all([sobj.smash_snr != 0. for sobj in sobjs]) and not np.all(skymask_objsnr == thismask): # TODO This is a kludge until we refactor this routine. Basically mask design objects that are not auto-ID # always have smash_snr undefined. If there is a hybrid situation of auto-ID and maskdesign, the logic # here does not really make sense. Soution would be to compute thershold and smash_snr for all objects. skymask = skymask_objsnr | skymask_fwhm else: # Enforces boxcar radius masking skymask = skymask_objsnr & skymask_fwhm # Return return skymask[thismask]
[docs]def ech_findobj_ineach_order( image, ivar, slitmask, slit_left, slit_righ, slit_spats, order_vec, spec_min_max, plate_scale_ord, det='DET01', inmask=None, std_trace=None, ncoeff=5, hand_extract_dict=None, box_radius=2.0, fwhm=3.0, use_user_fwhm=False, maxdev=2.0, nperorder=2, extract_maskwidth=3.0, snr_thresh=10.0, specobj_dict=None, trim_edg=(5,5), show_peaks=False, show_single_fits=False, show_single_trace=False, objfindQA_filename=None): """ Find objects in each echelle order, individually. This routine: - Loops over the good orders - Calls the :func:`objs_in_slit` method to find objects in the order. See that method for further details. Args: image (`numpy.ndarray`_): (Floating-point) Image to use for object search with shape (nspec, nspat). The first dimension (nspec) is spectral, and second dimension (nspat) is spatial. Note this image can either have the sky background in it, or have already been sky subtracted. Object finding works best on sky-subtracted images. Ideally, object finding is run in another routine, global sky-subtraction performed, and then this code should be run. However, it is also possible to run this code on non-sky-subtracted images. ivar (`numpy.ndarray`_): Floating-point inverse variance image for the input image. Shape must match ``image``, (nspec, nspat). slitmask (`numpy.ndarray`_): Integer image indicating the pixels that belong to each order. Pixels that are not on an order have value -1, and those that are on an order have a value equal to the slit number (i.e. 0 to nslits-1 from left to right on the image). Shape must match ``image``, (nspec, nspat). slit_left (`numpy.ndarray`_): Left boundary of orders to be extracted (given as floating point pixels). Shape is (nspec, norders), where norders is the total number of traced echelle orders. slit_righ (`numpy.ndarray`_): Right boundary of orders to be extracted (given as floating point pixels). Shape is (nspec, norders), where norders is the total number of traced echelle orders. slit_spats (`numpy.ndarray`_): slit_spat values (spatial position 1/2 way up the detector) for the orders order_vec (`numpy.ndarray`_): Vector identifying the Echelle orders for each pair of order edges found. This is saved to the output :class:`~pypeit.specobj.SpecObj` objects. If the orders are not known, this can be ``np.arange(norders)`` (but this is *not* recommended). spec_min_max (`numpy.ndarray`_): 2D array defining the minimum and maximum pixel in the spectral direction with useable data for each order. Shape must be (2, norders). This should only be used for echelle spectrographs for which the orders do not entirely cover the detector. PCA tracing will re-map the traces such that they all have the same length, compute the PCA, and then re-map the orders back. This improves performance for echelle spectrographs by removing the nonlinear shrinking of the orders so that the linear pca operation can better predict the traces. If None, the minimum and maximum values will be determined automatically from ``slitmask``. plate_scale_ord (`numpy.ndarray`_): An array with shape (norders,) providing the plate scale of each order in arcsec/pix, det (:obj:`str`, optional): The name of the detector containing the object. Only used if ``specobj_dict`` is None. inmask (`numpy.ndarray`_, optional): Good-pixel mask for input image. Must have the same shape as ``image``. If None, all pixels in ``slitmask`` with non-negative values are considered good. std_trace (`numpy.ndarray`_, optional): Vector with the standard star trace, which is used as a crutch for tracing. Shape must be (nspec,). If None, the slit boundaries are used as the crutch. ncoeff (:obj:`int`, optional): Order of polynomial fit to traces. box_radius (:obj:`float`, optional): Box_car extraction radius in arcseconds to assign to each detected object and to be used later for boxcar extraction. In this method ``box_radius`` is converted into pixels using ``plate_scale``. ``box_radius`` is also used for SNR calculation and trimming. fwhm (:obj:`float`, optional): Estimated fwhm of the objects in pixels use_user_fwhm (:obj:`bool`, optional): If True, ``PypeIt`` will use the spatial profile FWHM input by the user (see ``fwhm``) rather than determine the spatial FWHM from the smashed spatial profile via the automated algorithm. maxdev (:obj:`float`, optional): Maximum deviation of pixels from polynomial fit to trace used to reject bad pixels in trace fitting. nperorder (:obj:`int`, optional): Maximum number of objects allowed per order. If there are more detections than this number, the code will select the ``nperorder`` most significant detections. However, hand apertures will always be returned and do not count against this budget. specobj_dict (:obj:`dict`, optional): Dictionary containing meta-data for the objects that will be propagated into the :class:`~pypeit.specobj.SpecObj` objects. The expected components are: - SLITID: The slit ID number - DET: The detector identifier - OBJTYPE: The object type - PYPELINE: The class of pipeline algorithms applied If None, the dictionary is filled with the following placeholders:: specobj_dict = {'SLITID': 999, 'DET': det, 'ECH_ORDERINDX': 999, 'OBJTYPE': 'unknown', 'PYPELINE': 'Echelle'} trim_edg (:obj:`tuple`, optional): A two-tuple of integers or floats used to ignore objects within this many pixels of the left and right slit boundaries, respectively. show_peaks (:obj:`bool`, optional): Plot the QA of the object peak finding in each order. show_single_fits (:obj:`bool`, optional): Plot trace fitting for single order fits. show_single_trace (:obj:`bool`, optional): Display the object traces on top of the single order. objfindQA_filename (:obj:`str`, optional): Full path (directory and filename) for the object profile QA plot. If None, not plot is produced and saved. Returns: :class:`~pypeit.specobjs.SpecObjs`: Object containing the objects detected. """ if specobj_dict is None: specobj_dict = {'SLITID': 999, 'ECH_ORDERINDX': 999, 'DET': det, 'OBJTYPE': 'unknown', 'PYPELINE': 'Echelle'} allmask = slitmask > -1 if inmask is None: inmask = allmask # Loop over orders and find objects sobjs = specobjs.SpecObjs() for iord, iorder in enumerate(order_vec): qa_title = 'Finding objects on order # {:d}'.format(iorder) msgs.info(qa_title) thisslit_gpm = slitmask == slit_spats[iord] inmask_iord = inmask & thisslit_gpm specobj_dict['SLITID'] = slit_spats[iord] specobj_dict['ECH_ORDERINDX'] = iord specobj_dict['ECH_ORDER'] = iorder std_in = None if std_trace is None else std_trace[:, iord] # Get SLTIORD_ID for the objfind QA ech_objfindQA_filename = objfindQA_filename.replace('S0999', 'S{:04d}'.format(order_vec[iord])) \ if objfindQA_filename is not None else None # Run sobjs_slit = \ objs_in_slit( image, ivar, thisslit_gpm, slit_left[:,iord], slit_righ[:,iord], spec_min_max=spec_min_max[:,iord], inmask=inmask_iord,std_trace=std_in, ncoeff=ncoeff, fwhm=fwhm, use_user_fwhm=use_user_fwhm, maxdev=maxdev, hand_extract_dict=hand_extract_dict, nperslit=nperorder, extract_maskwidth=extract_maskwidth, snr_thresh=snr_thresh, trim_edg=trim_edg, boxcar_rad=box_radius/plate_scale_ord[iord], show_peaks=show_peaks, show_fits=show_single_fits, show_trace=show_single_trace, qa_title=qa_title, specobj_dict=specobj_dict, objfindQA_filename=ech_objfindQA_filename) sobjs.add_sobj(sobjs_slit) # Return return sobjs
[docs]def ech_fof_sobjs(sobjs:specobjs.SpecObjs, slit_left:np.ndarray, slit_righ:np.ndarray, order_vec:np.ndarray, plate_scale_ord:np.ndarray, fof_link:float=1.5): """ Links together objects previously found using a friends-of-friends algorithm on fractional order position. Each source from each order is then assigned an obj_id value Args: sobjs (:class:`~pypeit.specobj.SpecObj`): Previously found objects. There needs to be at least 1. slit_left (`numpy.ndarray`_): Left boundary of orders to be extracted (given as floating point pixels). Shape is (nspec, norders), where norders is the total number of traced echelle orders. slit_righ (`numpy.ndarray`_): Right boundary of orders to be extracted (given as floating point pixels). Shape is (nspec, norders), where norders is the total number of traced echelle orders. order_vec (`numpy.ndarray`_): Vector identifying the Echelle orders for each pair of order edges found. plate_scale_ord (`numpy.ndarray`_): An array with shape (norders,) providing the plate scale of each order in arcsec/pix, fof_link (:obj:`float`, optional): Friends-of-friends linking length in arcseconds used to link together traces across orders. The routine links together at the same fractional slit position and links them together with a friends-of-friends algorithm using this linking length. Returns: `numpy.ndarray`_: An array containing the object IDs of the objects linked together. This array is aligned with sobjs """ # Prep norders = slit_left.shape[1] slit_width = slit_righ - slit_left nfound = len(sobjs) # FOF_frac = fof_link/(np.median(np.median(slit_width,axis=0)*plate_scale_ord)) # Run the FOF. We use fake coordinates fracpos = sobjs.SPAT_FRACPOS ra_fake = fracpos/1000.0 # Divide all angles by 1000 to make geometry euclidian dec_fake = np.zeros_like(fracpos) if nfound>1: inobj_id, multobj_id, firstobj_id, nextobj_id \ = pydl.spheregroup(ra_fake, dec_fake, FOF_frac/1000.0) # Modify to 1-based indexing obj_id_init = inobj_id + 1 elif nfound==1: obj_id_init = np.ones(1,dtype='int') else: msgs.error('No objects found in ech_fof_sobjs. Should not have called this routine') uni_obj_id_init, uni_ind_init = np.unique(obj_id_init, return_index=True) # Now loop over the unique objects and check that there is only one object per order. If FOF # grouped > 1 objects on the same order, then this will be popped out as its own unique object obj_id = obj_id_init.copy() nobj_init = len(uni_obj_id_init) for iobj in range(nobj_init): for iord in range(norders): on_order = (obj_id_init == uni_obj_id_init[iobj]) & (sobjs.ECH_ORDER == order_vec[iord]) if (np.sum(on_order) > 1): msgs.warn('Found multiple objects in a FOF group on order iord={:d}'.format(order_vec[iord]) + msgs.newline() + 'Spawning new objects to maintain a single object per order.') off_order = (obj_id_init == uni_obj_id_init[iobj]) & (sobjs.ECH_ORDER != order_vec[iord]) ind = np.where(on_order)[0] if np.any(off_order): # Keep the closest object to the location of the rest of the group (on other orders) # as corresponding to this obj_id, and spawn new obj_ids for the others. frac_mean = np.mean(fracpos[off_order]) min_dist_ind = np.argmin(np.abs(fracpos[ind] - frac_mean)) else: # If there are no other objects with this obj_id to compare to, then we simply have multiple # objects grouped together on the same order, so just spawn new object IDs for them to maintain # one obj_id per order min_dist_ind = 0 ind_rest = np.setdiff1d(ind,ind[min_dist_ind]) # JFH OLD LINE with bug #obj_id[ind_rest] = (np.arange(len(ind_rest)) + 1) + obj_id_init.max() obj_id[ind_rest] = (np.arange(len(ind_rest)) + 1) + obj_id.max() # Finish uni_obj_id, uni_ind = np.unique(obj_id, return_index=True) nobj = len(uni_obj_id) msgs.info('FOF matching found {:d}'.format(nobj) + ' unique objects') return obj_id
[docs]def ech_fill_in_orders(sobjs:specobjs.SpecObjs, slit_left:np.ndarray, slit_righ:np.ndarray, slit_spat_id: np.ndarray, order_vec:np.ndarray, obj_id:np.ndarray, std_trace:specobjs.SpecObjs=None, show:bool=False): """ For objects which were only found on some orders, the standard (or the slit boundaries) are placed at the appropriate fractional position along the order. This routine: - Assigns each specobj a fractional order position and an obj_id number. - Fills in missing objects. Fit the fraction slit position of the good orders where an object was found and use that fit to predict the fractional slit position on the bad orders where no object was found. - Now loop over the orders and add objects on the orders for which the current object was not found. Args: sobjs (:class:`~pypeit.specobj.SpecObj`): Objects found on some orders thus far slit_left (`numpy.ndarray`_): Left boundary of orders to be extracted (given as floating point pixels). Shape is (nspec, norders), where norders is the total number of traced echelle orders. slit_righ (`numpy.ndarray`_): Right boundary of orders to be extracted (given as floating point pixels). Shape is (nspec, norders), where norders is the total number of traced echelle orders. slit_spat_id (`numpy.ndarray`_): slit_spat values (spatial position 1/2 way up the detector) for the orders order_vec (`numpy.ndarray`_): Vector identifying the Echelle orders for each pair of order edges found. This is saved to the output :class:`~pypeit.specobj.SpecObj` objects. If the orders are not known, this can be ``np.arange(norders)`` (but this is *not* recommended). obj_id (`numpy.ndarray`_): Object IDs of the objects linked together. std_trace (:class:`~pypeit.specobjs.SpecObjs`, optional): Standard star objects (including the traces) Defaults to None. show (bool, optional): Plot diagnostics related to filling the missing orders Returns: :class:`~pypeit.specobjs.SpecObjs`: A new SpecObjs object with the filled in orders """ # Prep nfound = len(sobjs) uni_obj_id, uni_ind = np.unique(obj_id, return_index=True) nobj = len(uni_obj_id) fracpos = sobjs.SPAT_FRACPOS # Prep norders = order_vec.size slit_width = slit_righ - slit_left # Check standard star if std_trace is not None and std_trace.shape[1] != norders: msgs.error('Standard star trace does not match the number of orders in the echelle data.') # For traces nspec = slit_left.shape[0] spec_vec = np.arange(nspec) slit_spec_pos = nspec/2.0 specmid = nspec // 2 gfrac = np.zeros(nfound) for jj in range(nobj): this_obj_id = obj_id == uni_obj_id[jj] gfrac[this_obj_id] = np.median(fracpos[this_obj_id]) uni_frac = gfrac[uni_ind] # Sort with respect to fractional slit location to guarantee that we have a similarly sorted list of objects later isort_frac = uni_frac.argsort() uni_obj_id = uni_obj_id[isort_frac] uni_frac = uni_frac[isort_frac] sobjs_align = sobjs.copy() # Loop over the orders and assign each specobj a fractional position and a obj_id number for iobj in range(nobj): #for iord in range(norders): for iord in order_vec: on_order = (obj_id == uni_obj_id[iobj]) & (sobjs_align.ECH_ORDER == iord) sobjs_align[on_order].ECH_FRACPOS = uni_frac[iobj] sobjs_align[on_order].ECH_OBJID = uni_obj_id[iobj] sobjs_align[on_order].OBJID = uni_obj_id[iobj] sobjs_align[on_order].ech_frac_was_fit = False # Reset names (just in case) sobjs_align.set_names() # Now loop over objects and fill in the missing objects and their traces. We will fit the fraction slit position of # the good orders where an object was found and use that fit to predict the fractional slit position on the bad orders # where no object was found for iobj in range(nobj): # Grab all the members of this obj_id from the object list indx_obj_id = sobjs_align.ECH_OBJID == uni_obj_id[iobj] nthisobj_id = np.sum(indx_obj_id) # Perform the fit if this objects shows up on more than three orders if (nthisobj_id > 3) and (nthisobj_id<norders): # JFH This line could also be done with spat_ids, but they are all aligned # Find the indices of the orders where this object was found thisorder = sobjs_align[indx_obj_id].ECH_ORDER thisorderindx = [i for i, ord in enumerate(order_vec) if ord in thisorder] #thisorderindx = sobjs_align[indx_obj_id].ECH_ORDERINDX # Old line replaced with the list comprehentsion above. # Allow for masked orders xcen_good = (sobjs_align[indx_obj_id].TRACE_SPAT).T slit_frac_good = (xcen_good-slit_left[:,thisorderindx])/slit_width[:,thisorderindx] # Fractional slit position averaged across the spectral direction for each order frac_mean_good = np.mean(slit_frac_good, 0) # Perform a linear fit to fractional slit position #TODO Do this as a S/N weighted fit similar to what is now in the pca_trace algorithm? #msk_frac, poly_coeff_frac = fitting.robust_fit(order_vec[goodorder], frac_mean_good, 1, pypeitFit = fitting.robust_fit( thisorder, frac_mean_good, 1, function='polynomial', maxiter=20, lower=2, upper=2, use_mad= True, sticky=False, minx = order_vec.min(), maxx=order_vec.max()) # Fill goodorder = np.in1d(order_vec, thisorder) badorder = np.invert(goodorder) frac_mean_new = np.zeros(norders) frac_mean_new[badorder] = pypeitFit.eval(order_vec[badorder]) frac_mean_new[goodorder] = frac_mean_good # TODO This QA needs some work if show: frac_mean_fit = pypeitFit.eval(order_vec) plt.plot(order_vec[goodorder][pypeitFit.bool_gpm], frac_mean_new[goodorder][pypeitFit.bool_gpm], 'ko', mfc='k', markersize=8.0, label='Good Orders Kept') plt.plot(order_vec[goodorder][np.invert(pypeitFit.bool_gpm)], frac_mean_new[goodorder][np.invert(pypeitFit.bool_gpm)], 'ro', mfc='k', markersize=8.0, label='Good Orders Rejected') plt.plot(order_vec[badorder], frac_mean_new[badorder], 'ko', mfc='None', markersize=8.0, label='Predicted Bad Orders') plt.plot(order_vec,frac_mean_new,'+',color='cyan',markersize=12.0,label='Final Order Fraction') plt.plot(order_vec, frac_mean_fit, 'r-', label='Fractional Order Position Fit') plt.xlabel('Order Index', fontsize=14) plt.ylabel('Fractional Slit Position', fontsize=14) plt.title('Fractional Slit Position Fit') plt.legend() plt.show() else: frac_mean_new = np.full(norders, uni_frac[iobj]) # Now loop over the orders and add objects on the orders for # which the current object was not found for iord, this_order in enumerate(order_vec): # Is the current object detected on this order? on_order = (sobjs_align.ECH_OBJID == uni_obj_id[iobj]) & (sobjs_align.ECH_ORDER == this_order) num_on_order = np.sum(on_order) if num_on_order == 0: msgs.info(f"Adding object={uni_obj_id[iobj]} to order={this_order}") # If it is not, create a new sobjs and add to sobjs_align and assign required tags thisobj = specobj.SpecObj('Echelle', sobjs_align[0].DET, OBJTYPE=sobjs_align[0].OBJTYPE, ECH_ORDERINDX=iord, ECH_ORDER=this_order) #thisobj.ECH_ORDERINDX = iord #thisobj.ech_order = order_vec[iord] thisobj.SPAT_FRACPOS = uni_frac[iobj] # Assign traces using the fractional position fit above if std_trace is not None: x_trace = np.interp(slit_spec_pos, spec_vec, std_trace[:,iord]) shift = np.interp( slit_spec_pos, spec_vec, slit_left[:,iord] + slit_width[:,iord]*frac_mean_new[iord]) - x_trace thisobj.TRACE_SPAT = std_trace[:,iord] + shift else: thisobj.TRACE_SPAT = slit_left[:, iord] + slit_width[:, iord] * frac_mean_new[iord] # new trace thisobj.trace_spec = spec_vec thisobj.SPAT_PIXPOS = thisobj.TRACE_SPAT[specmid] # Use the real detections of this objects for the FWHM this_obj_id = obj_id == uni_obj_id[iobj] # Assign to the fwhm of the nearest detected order imin = np.argmin(np.abs(sobjs_align[this_obj_id].ECH_ORDER - this_order)) thisobj.FWHM = sobjs_align[imin].FWHM thisobj.hand_extract_flag = sobjs_align[imin].hand_extract_flag thisobj.maskwidth = sobjs_align[imin].maskwidth thisobj.smash_peakflux = sobjs_align[imin].smash_peakflux thisobj.smash_snr = sobjs_align[imin].smash_snr thisobj.BOX_RADIUS = sobjs_align[imin].BOX_RADIUS thisobj.ECH_FRACPOS = uni_frac[iobj] thisobj.ECH_OBJID = uni_obj_id[iobj] thisobj.OBJID = uni_obj_id[iobj] thisobj.SLITID = slit_spat_id[iord] thisobj.ech_frac_was_fit = True thisobj.set_name() sobjs_align.add_sobj(thisobj) obj_id = np.append(obj_id, uni_obj_id[iobj]) gfrac = np.append(gfrac, uni_frac[iobj]) elif num_on_order == 1: # Object is already on this order so no need to do anything pass elif num_on_order > 1: msgs.error('Problem in echelle object finding. The same objid={:d} appears {:d} times on echelle orderindx ={:d}' ' even after duplicate obj_ids the orders were removed. ' 'Report this bug to PypeIt developers'.format(uni_obj_id[iobj],num_on_order, iord)) # Return return sobjs_align
[docs]def ech_cutobj_on_snr( sobjs_align, image:np.ndarray, ivar:np.ndarray, slitmask:np.ndarray, order_vec:np.ndarray, plate_scale_ord:np.ndarray, max_snr:float=2.0, nperorder:int=2, min_snr:float=1.0, nabove_min_snr:int=2, box_radius:float=2.0, inmask:np.ndarray=None): """ Cut down objects based on S/N This routine: - Loops over the objects and perform a quick and dirty extraction to assess S/N. - Purge objects with low SNR that don't show up in enough orders, sort the list of objects with respect to obj_id and orderindx. - Loop over objects from highest SNR to lowest SNR. Apply the S/N constraints. Once we hit the maximum number objects requested exit, except keep any hand apertures that were requested. Args: sobjs_align (:class:`~pypeit.specobj.SpecObj`): Previously found objects image (`numpy.ndarray`_): (Floating-point) Image to use for object search with shape (nspec, nspat). The first dimension (nspec) is spectral, and second dimension (nspat) is spatial. Note this image can either have the sky background in it, or have already been sky subtracted. Object finding works best on sky-subtracted images. Ideally, object finding is run in another routine, global sky-subtraction performed, and then this code should be run. However, it is also possible to run this code on non-sky-subtracted images. ivar (`numpy.ndarray`_): Floating-point inverse variance image for the input image. Shape must match ``image``, (nspec, nspat). slitmask (`numpy.ndarray`_): Integer image indicating the pixels that belong to each order. Pixels that are not on an order have value -1, and those that are on an order have a value equal to the slit number (i.e. 0 to nslits-1 from left to right on the image). Shape must match ``image``, (nspec, nspat). order_vec (`numpy.ndarray`_): :obj:`int` array of good orders plate_scale_ord (`numpy.ndarray`_): An array with shape (norders,) providing the plate scale of each order in arcsec/pix, max_snr (:obj:`float`, optional): For an object to be included in the output object, it must have a max S/N ratio above this value. min_snr (:obj:`float`, optional): For an object to be included in the output object, it must have a a median S/N ratio above this value for at least ``nabove_min_snr`` orders (see below). nabove_min_snr (:obj:`int`, optional): The required number of orders that an object must have with median SNR greater than ``min_snr`` in order to be included in the output object. box_radius (:obj:`float`, optional): Box_car extraction radius in arcseconds to assign to each detected object and to be used later for boxcar extraction. In this method ``box_radius`` is converted into pixels using ``plate_scale``. ``box_radius`` is also used for SNR calculation and trimming. inmask (`numpy.ndarray`_, optional): Good-pixel mask for input image. Must have the same shape as ``image``. If None, all pixels in ``slitmask`` with non-negative values are considered good. Returns: :class:`~pypeit.specobjs.SpecObjs`: The final set of objects """ allmask = slitmask > -1 if inmask is None: inmask = allmask # Prep nspec = image.shape[0] norders = order_vec.size uni_obj_id = np.unique(sobjs_align.ECH_OBJID) nobj = uni_obj_id.size # Loop over the objects and perform a quick and dirty extraction to assess S/N. varimg = utils.calc_ivar(ivar) flux_box = np.zeros((nspec, norders, nobj)) ivar_box = np.zeros((nspec, norders, nobj)) mask_box = np.zeros((nspec, norders, nobj)) SNR_arr = np.zeros((norders, nobj)) slitfracpos_arr = np.zeros((norders, nobj)) hand_flag = np.zeros(nobj, dtype=bool) for iobj in range(nobj): # Hand extraction? mt_obj = sobjs_align.ECH_OBJID == uni_obj_id[iobj] if np.any(sobjs_align[mt_obj].hand_extract_flag): hand_flag[iobj] = True # SNR for iord in range(norders): iorder_vec = order_vec[iord] indx = sobjs_align.slitorder_objid_indices( iorder_vec, uni_obj_id[iobj]) #indx = (sobjs_align.ECH_OBJID == uni_obj_id[iobj]) & (sobjs_align.ECH_ORDERINDX == iord) #spec = sobjs_align[indx][0] inmask_iord = inmask & (slitmask == sobjs_align[indx].SLITID)# gdslit_spat[iord]) # TODO make the snippet below its own function quick_extraction() box_rad_pix = box_radius/plate_scale_ord[iord] # TODO -- We probably shouldn't be operating on a SpecObjs but instead a SpecObj flux_tmp = moment1d(image*inmask_iord, sobjs_align[indx][0].TRACE_SPAT, 2*box_rad_pix, row=sobjs_align[indx][0].trace_spec)[0] var_tmp = moment1d(varimg*inmask_iord, sobjs_align[indx][0].TRACE_SPAT, 2*box_rad_pix, row=sobjs_align[indx][0].trace_spec)[0] ivar_tmp = utils.calc_ivar(var_tmp) pixtot = moment1d(ivar*0 + 1.0, sobjs_align[indx][0].TRACE_SPAT, 2*box_rad_pix, row=sobjs_align[indx][0].trace_spec)[0] mask_tmp = moment1d(ivar*inmask_iord == 0.0, sobjs_align[indx][0].TRACE_SPAT, 2*box_rad_pix, row=sobjs_align[indx][0].trace_spec)[0] != pixtot flux_box[:,iord,iobj] = flux_tmp*mask_tmp ivar_box[:,iord,iobj] = np.fmax(ivar_tmp*mask_tmp,0.0) mask_box[:,iord,iobj] = mask_tmp mean, med_sn, stddev = astropy.stats.sigma_clipped_stats( flux_box[mask_tmp,iord,iobj]*np.sqrt(ivar_box[mask_tmp,iord,iobj]), sigma_lower=5.0,sigma_upper=5.0 ) # ToDO assign this to sobjs_align for use in the extraction SNR_arr[iord,iobj] = med_sn sobjs_align[indx][0].ech_snr = med_sn # For hand extractions slitfracpos_arr[iord,iobj] = sobjs_align[indx][0].SPAT_FRACPOS # Purge objects with low SNR that don't show up in enough orders, sort the list of objects with respect to obj_id # and orderindx keep_obj = np.zeros(nobj,dtype=bool) # Empty specobjs object to hold the final objects sobjs_trim = specobjs.SpecObjs() # objids are 1 based so that we can easily asign the negative to negative objects iobj_keep = 1 iobj_keep_not_hand = 1 ## Loop over objects from highest SNR to lowest SNR. Apply the S/N constraints. Once we hit the maximum number # objects requested exit, except keep the hand apertures that were requested. isort_SNR_max = np.argsort(np.median(SNR_arr,axis=0))[::-1] for iobj in isort_SNR_max: hand_ap_flag = hand_flag[iobj] SNR_constraint = (SNR_arr[:,iobj].max() > max_snr) or ( np.sum(SNR_arr[:,iobj] > min_snr) >= nabove_min_snr) nperorder_constraint = (iobj_keep-1) < nperorder if (SNR_constraint and nperorder_constraint) or hand_ap_flag: keep_obj[iobj] = True ikeep = sobjs_align.ECH_OBJID == uni_obj_id[iobj] sobjs_keep = sobjs_align[ikeep].copy() sobjs_keep.ECH_OBJID = iobj_keep sobjs_keep.OBJID = iobj_keep sobjs_trim.add_sobj(sobjs_keep[np.argsort(sobjs_keep.SLITID)]) iobj_keep += 1 if not hand_ap_flag: iobj_keep_not_hand += 1 else: if not nperorder_constraint: msgs.info('Purging object #{:d}'.format(iobj) + ' since there are already {:d} objects automatically identified ' 'and you set nperorder={:d}'.format(iobj_keep_not_hand-1, nperorder)) else: msgs.info('Purging object #{:d}'.format(iobj) + ' which does not satisfy max_snr > {:5.2f} OR min_snr > {:5.2f}'.format(max_snr, min_snr) + ' on at least nabove_min_snr >= {:d}'.format(nabove_min_snr) + ' orders') nobj_trim = np.sum(keep_obj) if nobj_trim == 0: msgs.warn('No objects found') sobjs_final = specobjs.SpecObjs() return sobjs_final # TODO JFH: We need to think about how to implement returning a maximum number of objects, where the objects # returned are the highest S/N ones. It is a bit complicated with regards to the individual object finding and then # the linking that is performed above, and also making sure the hand apertures don't get removed. SNR_arr_trim = SNR_arr[:,keep_obj] return sobjs_trim
[docs]def ech_pca_traces( sobjs_final:specobjs.SpecObjs, image:np.ndarray, slitmask:np.ndarray, inmask:np.ndarray, order_vec:np.ndarray, spec_min_max, npca:int=None, coeff_npoly:int=None, pca_explained_var:float=99.0, ncoeff:int=5, maxdev:float=2.0, fwhm:float=3.0, show_trace:bool=False, show_fits:bool=False, show_pca:bool=False): """ A PCA fit to the traces is performed using the routine pca_fit It then applies iterative flux-weighted centroiding to refine the traces Args: sobjs_final (:class:`~pypeit.specobj.SpecObj`): Final set of objects ready for tracing image (`numpy.ndarray`_): (Floating-point) Image to use for object search with shape (nspec, nspat). The first dimension (nspec) is spectral, and second dimension (nspat) is spatial. Note this image can either have the sky background in it, or have already been sky subtracted. Object finding works best on sky-subtracted images. Ideally, object finding is run in another routine, global sky-subtraction performed, and then this code should be run. However, it is also possible to run this code on non-sky-subtracted images. slitmask (`numpy.ndarray`_): Integer image indicating the pixels that belong to each order. Pixels that are not on an order have value -1, and those that are on an order have a value equal to the slit number (i.e. 0 to nslits-1 from left to right on the image). Shape must match ``image``, (nspec, nspat). inmask (`numpy.ndarray`_, optional): Good-pixel mask for input image. Must have the same shape as ``image``. If None, all pixels in ``slitmask`` with non-negative values are considered good. order_vec (`numpy.ndarray`_): :obj:`int` array of good orders spec_min_max (`numpy.ndarray`_): _description_ `float` array of shape (2, norders) with the minimum and maximum spectral value for each order npca (:obj:`int`, optional): Number of PCA components to keep during PCA decomposition of the object traces. If None, the number of components set by requiring the PCA accounts for approximately 99% of the variance. coeff_npoly (:obj:`int`, optional): Order of polynomial used for PCA coefficients fitting. If None, value set automatically, see :func:`~pypeit.tracepca.pca_trace_object`. pca_explained_var (:obj:`float`, optional): The percentage (i.e., not the fraction) of the variance in the data accounted for by the PCA used to truncate the number of PCA coefficients to keep (see ``npca``). Ignored if ``npca`` is provided directly; see :func:`~pypeit.tracepca.pca_trace_object`. ncoeff (:obj:`int`, optional): Order of polynomial fit to traces. maxdev (:obj:`float`, optional): Maximum deviation of pixels from polynomial fit to trace used to reject bad pixels in trace fitting. fwhm (:obj:`float`, optional): Estimated fwhm of the objects in pixels show_trace (:obj:`bool`, optional): Display the object traces on top of the image. show_fits (:obj:`bool`, optional): Plot trace fitting for final fits using PCA as crutch. show_pca (:obj:`bool`, optional): Display debugging plots for the PCA decomposition. Returns: :class:`~pypeit.specobj.SpecObj`: Final set of objects, traced """ # Prep nspec, nspat = image.shape norders = order_vec.size uni_obj = np.unique(sobjs_final.ECH_OBJID) nobj_trim = uni_obj.size spec_vec = np.arange(nspec) specmid = nspec // 2 allmask = slitmask > -1 if inmask is None: inmask = allmask # Checks if norders != spec_min_max.shape[1]: msgs.error("Number of good orders does not match the number of orders in spec_min_max") # Loop over the objects one by one and adjust/predict the traces pca_fits = np.zeros((nspec, norders, nobj_trim)) # Create the trc_inmask for iterative fitting below trc_inmask = np.zeros((nspec, norders), dtype=bool) for iord in range(norders): trc_inmask[:,iord] = ( spec_vec >= spec_min_max[0,iord]) & ( spec_vec <= spec_min_max[1,iord]) for iobj in range(nobj_trim): indx_obj_id = sobjs_final.ECH_OBJID == (iobj + 1) # PCA predict all the orders now (where we have used the standard or slit boundary for the bad orders above) msgs.info('Fitting echelle object finding PCA for object {:d}/{:d} with median SNR = {:5.3f}'.format( iobj + 1,nobj_trim,np.median(sobjs_final[indx_obj_id].ech_snr))) pca_fits[:,:,iobj] \ = tracepca.pca_trace_object( sobjs_final[indx_obj_id].TRACE_SPAT.T, order=coeff_npoly, npca=npca, pca_explained_var=pca_explained_var, trace_wgt=np.fmax(sobjs_final[indx_obj_id].ech_snr, 1.0)**2, debug=show_pca) # Trial and error shows weighting by S/N instead of S/N^2 performs better # JXP -- Updated to now be S/N**2, i.e. inverse variance, with fitting fit # Perform iterative flux weighted centroiding using new PCA predictions xinit_fweight = pca_fits[:,:,iobj].copy() inmask_now = inmask & allmask xfit_fweight = fit_trace(image, xinit_fweight, ncoeff, bpm=np.invert(inmask_now), trace_bpm=np.invert(trc_inmask), fwhm=fwhm, maxdev=maxdev, debug=show_fits)[0] # Perform iterative Gaussian weighted centroiding xinit_gweight = xfit_fweight.copy() xfit_gweight = fit_trace(image, xinit_gweight, ncoeff, bpm=np.invert(inmask_now), trace_bpm=np.invert(trc_inmask), weighting='gaussian', fwhm=fwhm, maxdev=maxdev, debug=show_fits)[0] #TODO Assign the new traces. Only assign the orders that were not orginally detected and traced. If this works # well, we will avoid doing all of the iter_tracefits above to make the code faster. for iord, spec in enumerate(sobjs_final[indx_obj_id]): # JFH added the condition on ech_frac_was_fit with S/N cut on 7-7-19. # TODO is this robust against half the order being masked? if spec.ech_frac_was_fit and (spec.ech_snr > 1.0): spec.TRACE_SPAT = xfit_gweight[:,iord] spec.SPAT_PIXPOS = spec.TRACE_SPAT[specmid] #TODO Put in some criterion here that does not let the fractional position change too much during the iterative # tracefitting. The problem is spurious apertures identified on one slit can be pulled over to the center of flux # resulting in a bunch of objects landing on top of each other. # Set the IDs #sobjs_final[:].ECH_ORDER = order_vec[sobjs_final[:].ECH_ORDERINDX] #for spec in sobjs_final: # spec.ech_order = order_vec[spec.ECH_ORDERINDX] sobjs_final.set_names() if show_trace: viewer, ch = display.show_image(image*allmask) #for spec in sobjs_trim: for spec in sobjs_final: color = 'red' if spec.ech_frac_was_fit else 'magenta' ## Showing the final flux weighted centroiding from PCA predictions display.show_trace(viewer, ch, spec.TRACE_SPAT, spec.NAME, color=color) for iobj in range(nobj_trim): obj_idx = sobjs_final.ECH_OBJID == (iobj + 1) frac = sobjs_final[obj_idx].SPAT_FRACPOS[0] for iord in range(norders): ## Showing PCA predicted locations before recomputing flux/gaussian weighted centroiding display.show_trace( viewer, ch, pca_fits[:,iord, iobj], str(frac), color='yellow') #str(uni_frac[iobj]), color='yellow') ## Showing the final traces from this routine display.show_trace(viewer, ch, sobjs_final.TRACE_SPAT[iord].T, sobjs_final.NAME, color='cyan') # Labels for the points text_final = [dict(type='text', args=(nspat / 2 -40, nspec / 2, 'final trace'), kwargs=dict(color='cyan', fontsize=20))] text_pca = [dict(type='text', args=(nspat / 2 -40, nspec / 2 - 30, 'PCA fit'),kwargs=dict(color='yellow', fontsize=20))] text_fit = [dict(type='text', args=(nspat / 2 -40, nspec / 2 - 60, 'predicted'),kwargs=dict(color='red', fontsize=20))] text_notfit = [dict(type='text', args=(nspat / 2 -40, nspec / 2 - 90, 'originally found'),kwargs=dict(color='magenta', fontsize=20))] canvas = viewer.canvas(ch._chname) canvas_list = text_final + text_pca + text_fit + text_notfit canvas.add('constructedcanvas', canvas_list) # TODO two things need to be debugged. 1) For objects which were found and traced, i don't think we should be updating the tracing with # the PCA. This just adds a failutre mode. 2) The PCA fit is going wild for X-shooter. Debug that. # Vette for sobj in sobjs_final: if not sobj.ready_for_extraction(): msgs.error("Bad SpecObj. Can't proceed") return sobjs_final
[docs]def ech_objfind(image, ivar, slitmask, slit_left, slit_righ, slit_spat_id, order_vec, spec_min_max, det='DET01', inmask=None, fof_link=1.5, plate_scale=0.2, std_trace=None, ncoeff=5, npca=None, coeff_npoly=None, max_snr=2.0, min_snr=1.0, nabove_min_snr=2, pca_explained_var=99.0, box_radius=2.0, fwhm=3.0, use_user_fwhm=False, maxdev=2.0, nperorder=2, extract_maskwidth=3.0, snr_thresh=10.0, specobj_dict=None, trim_edg=(5,5), show_peaks=False, show_fits=False, show_single_fits=False, show_trace=False, show_single_trace=False, show_pca=False, debug_all=False, objfindQA_filename=None, manual_extract_dict=None): """ Object finding routine for Echelle spectrographs. This routine: #. Runs object finding on each order individually #. Links the objects found together using a friends-of-friends algorithm on fractional order position. #. For objects which were only found on some orders, the standard (or the slit boundaries) are placed at the appropriate fractional position along the order. #. A PCA fit to the traces is performed using the routine above pca_fit **Note on masking**: This routine requires that all masking be performed in the upstream calling routine (:class:`pypeit.find_objects.FindObjects`) and thus the ``slit_left`` and ``slit_righ`` slit edge arrays must only contain these slits. The number of orders in ``order_vec``, ``spec_min_max``, ``slit_spat_id``, and ``plate_scale`` must also match the number of slits in the ``slit_left`` and ``slit_righ`` arrays. Args: image (`numpy.ndarray`_): (Floating-point) Image to use for object search with shape (nspec, nspat). The first dimension (nspec) is spectral, and second dimension (nspat) is spatial. Note this image can either have the sky background in it, or have already been sky subtracted. Object finding works best on sky-subtracted images. Ideally, object finding is run in another routine, global sky-subtraction performed, and then this code should be run. However, it is also possible to run this code on non-sky-subtracted images. ivar (`numpy.ndarray`_): Floating-point inverse variance image for the input image. Shape must match ``image``, (nspec, nspat). slitmask (`numpy.ndarray`_): Integer image indicating the pixels that belong to each order. Pixels that are not on an order have value -1, and those that are on an order have a value equal to the slit number (i.e. 0 to nslits-1 from left to right on the image). Shape must match ``image``, (nspec, nspat). slit_left (`numpy.ndarray`_): Left boundary of orders to be extracted (given as floating point pixels). Shape is (nspec, norders), where norders is the total number of traced echelle orders. slit_righ (`numpy.ndarray`_): Right boundary of orders to be extracted (given as floating point pixels). Shape is (nspec, norders), where norders is the total number of traced echelle orders. slit_spat_id (`numpy.ndarray`_): slit_spat values (spatial position 1/2 way up the detector) for the orders order_vec (`numpy.ndarray`_): Vector identifying the Echelle orders for each pair of order edges found. This is saved to the output :class:`~pypeit.specobj.SpecObj` objects. If the orders are not known, this can be ``np.arange(norders)`` (but this is *not* recommended). slits_bpm (`numpy.ndarray`_): Boolean array selecting orders that should be ignored (i.e., good orders are False, bad orders are True). Shape must be (norders,). spec_min_max (`numpy.ndarray`_): A 2D array defining the minimum and maximum pixel in the spectral direction with useable data for each order. Shape must be (2, norders). This should only be used for echelle spectrographs for which the orders do not entirely cover the detector. PCA tracing will re-map the traces such that they all have the same length, compute the PCA, and then re-map the orders back. This improves performance for echelle spectrographs by removing the nonlinear shrinking of the orders so that the linear pca operation can better predict the traces. Otherwise set the values to -1 and nspec where nspec is the number of pixels in the spectral direction. det (:obj:`str`, optional): The name of the detector containing the object. Only used if ``specobj_dict`` is None. inmask (`numpy.ndarray`_, optional): Good-pixel mask for input image. Must have the same shape as ``image``. If None, all pixels in ``slitmask`` with non-negative values are considered good. fof_link (:obj:`float`, optional): Friends-of-friends linking length in arcseconds used to link together traces across orders. The routine links together at the same fractional slit position and links them together with a friends-of-friends algorithm using this linking length. plate_scale (:obj:`float`, `numpy.ndarray`_, optional): Plate scale in arcsec/pix. This can either be a single float for every order, or an array with shape (norders,) providing the plate scale of each order. std_trace (`numpy.ndarray`_, optional): Vector with the standard star trace, which is used as a crutch for tracing. Shape must be (nspec,). If None, the slit boundaries are used as the crutch. ncoeff (:obj:`int`, optional): Order of polynomial fit to traces. npca (:obj:`int`, optional): Number of PCA components to keep during PCA decomposition of the object traces. If None, the number of components set by requiring the PCA accounts for approximately 99% of the variance. coeff_npoly (:obj:`int`, optional): Order of polynomial used for PCA coefficients fitting. If None, value set automatically, see :func:`~pypeit.tracepca.pca_trace_object`. max_snr (:obj:`float`, optional): For an object to be included in the output object, it must have a max S/N ratio above this value. min_snr (:obj:`float`, optional): For an object to be included in the output object, it must have a a median S/N ratio above this value for at least ``nabove_min_snr`` orders (see below). nabove_min_snr (:obj:`int`, optional): The required number of orders that an object must have with median SNR greater than ``min_snr`` in order to be included in the output object. pca_explained_var (:obj:`float`, optional): The percentage (i.e., not the fraction) of the variance in the data accounted for by the PCA used to truncate the number of PCA coefficients to keep (see ``npca``). Ignored if ``npca`` is provided directly; see :func:`~pypeit.tracepca.pca_trace_object`. box_radius (:obj:`float`, optional): Box_car extraction radius in arcseconds to assign to each detected object and to be used later for boxcar extraction. In this method ``box_radius`` is converted into pixels using ``plate_scale``. ``box_radius`` is also used for SNR calculation and trimming. fwhm (:obj:`float`, optional): Estimated fwhm of the objects in pixels use_user_fwhm (:obj:`bool`, optional): If True, ``PypeIt`` will use the spatial profile FWHM input by the user (see ``fwhm``) rather than determine the spatial FWHM from the smashed spatial profile via the automated algorithm. maxdev (:obj:`float`, optional): Maximum deviation of pixels from polynomial fit to trace used to reject bad pixels in trace fitting. hand_extract_dict (:obj:`dict`, optional): Dictionary with info on manual extraction; see :class:`~pypeit.manual_extract.ManualExtractionObj`. nperorder (:obj:`int`, optional): Maximum number of objects allowed per order. If there are more detections than this number, the code will select the ``nperorder`` most significant detections. However, hand apertures will always be returned and do not count against this budget. extract_maskwidth (:obj:`float`, optional): Determines the initial size of the region in units of FWHM that will be used for local sky subtraction; See :func:`objs_in_slit` and :func:`~pypeit.core.skysub.local_skysub_extract`. snr_thresh (:obj:`float`, optional): SNR threshold for finding objects specobj_dict (:obj:`dict`, optional): Dictionary containing meta-data for the objects that will be propagated into the :class:`~pypeit.specobj.SpecObj` objects. The expected components are: - SLITID: The slit ID number - DET: The detector identifier - OBJTYPE: The object type - PYPELINE: The class of pipeline algorithms applied If None, the dictionary is filled with the following placeholders:: specobj_dict = {'SLITID': 999, 'DET': 'DET01', 'OBJTYPE': 'unknown', 'PYPELINE': 'unknown'} trim_edg (:obj:`tuple`, optional): A two-tuple of integers or floats used to ignore objects within this many pixels of the left and right slit boundaries, respectively. show_peaks (:obj:`bool`, optional): Plot the QA of the object peak finding in each order. show_fits (:obj:`bool`, optional): Plot trace fitting for final fits using PCA as crutch. show_single_fits (:obj:`bool`, optional): Plot trace fitting for single order fits. show_trace (:obj:`bool`, optional): Display the object traces on top of the image. show_single_trace (:obj:`bool`, optional): Display the object traces on top of the single order. show_pca (:obj:`bool`, optional): Display debugging plots for the PCA decomposition. debug_all (:obj:`bool`, optional): Show all the debugging plots. If True, this also overrides any provided values for ``show_peaks``, ``show_trace``, and ``show_pca``, setting them to True. objfindQA_filename (:obj:`str`, optional): Full path (directory and filename) for the object profile QA plot. If None, not plot is produced and saved. manual_extract_dict : :obj:`dict`, optional Dict guiding the manual extraction Returns: :class:`~pypeit.specobjs.SpecObjs`: Object containing the objects detected. """ #debug_all=True if debug_all: show_peaks = True #show_fits = True #show_single_fits = True show_trace = True show_pca = True #show_single_trace = True # Perform some input checking norders = slit_left.shape[1] # TODO JFH Relaxing this strict requirement on the slitmask image for the time being #gdslit_spat = np.unique(slitmask[slitmask >= 0]).astype(int) # Unique sorts #if gdslit_spat.size != norders: #msgs.error('Number of slitidsin slitmask and the number of left/right slits must be the same.') if slit_righ.shape[1] != norders: msgs.error('Number of left and right slits must be the same.') if order_vec.size != norders: msgs.error('Number of orders in order_vec and left/right slits must be the same.') if spec_min_max.shape[1] != norders: msgs.error('Number of orders in spec_min_max and left/right slits must be the same.') if specobj_dict is None: specobj_dict = {'SLITID': 999, 'ECH_ORDERINDX': 999, 'DET': det, 'OBJTYPE': 'unknown', 'PYPELINE': 'Echelle'} # Loop over the orders and find the objects within them (if any) sobjs_in_orders = ech_findobj_ineach_order( image, ivar, slitmask, slit_left, slit_righ, slit_spat_id, order_vec, spec_min_max, plate_scale, det=det, inmask=inmask, std_trace=std_trace, specobj_dict=specobj_dict, snr_thresh=snr_thresh, show_peaks=show_peaks, show_single_fits=show_single_fits, show_single_trace=show_single_trace, extract_maskwidth=extract_maskwidth, trim_edg=trim_edg, fwhm=fwhm, use_user_fwhm=use_user_fwhm, nperorder=nperorder, maxdev=maxdev, box_radius=box_radius, objfindQA_filename=objfindQA_filename, hand_extract_dict=manual_extract_dict) # No sources and no manual? if len(sobjs_in_orders) == 0: return sobjs_in_orders # Perform some additional work for slits with sources (found or input manually) # Friend of friend algorithm to group objects obj_id = ech_fof_sobjs(sobjs_in_orders, slit_left, slit_righ, order_vec, plate_scale, fof_link=fof_link) # Fill in Orders sobjs_filled = ech_fill_in_orders( sobjs_in_orders, slit_left, slit_righ, slit_spat_id, order_vec, obj_id, std_trace=std_trace) # Cut on SNR and number of objects sobjs_pre_final = ech_cutobj_on_snr( sobjs_filled, image, ivar, slitmask, order_vec, plate_scale, inmask=inmask, nperorder=nperorder, max_snr=max_snr, min_snr=min_snr, nabove_min_snr=nabove_min_snr, box_radius=box_radius) if len(sobjs_pre_final) == 0: return sobjs_pre_final # PCA sobjs_ech = ech_pca_traces( sobjs_pre_final, image, slitmask, inmask, order_vec, spec_min_max, coeff_npoly=coeff_npoly, ncoeff=ncoeff, npca=npca, pca_explained_var=pca_explained_var, maxdev=maxdev, fwhm=fwhm, show_trace=show_trace, show_fits=show_fits, show_pca=show_pca) return sobjs_ech
[docs]def orig_ech_objfind(image, ivar, slitmask, slit_left, slit_righ, order_vec, maskslits, det='DET01', inmask=None, spec_min_max=None, fof_link=1.5, plate_scale=0.2, std_trace=None, ncoeff=5, npca=None, coeff_npoly=None, max_snr=2.0, min_snr=1.0, nabove_min_snr=2, pca_explained_var=99.0, box_radius=2.0, fwhm=3.0, use_user_fwhm=False, maxdev=2.0, hand_extract_dict=None, nperorder=2, extract_maskwidth=3.0, snr_thresh=10.0, specobj_dict=None, trim_edg=(5,5), show_peaks=False, show_fits=False, show_single_fits=False, show_trace=False, show_single_trace=False, show_pca=False, debug_all=False, objfindQA_filename=None): """ Object finding routine for Echelle spectrographs. This routine: #. Runs object finding on each order individually #. Links the objects found together using a friends-of-friends algorithm on fractional order position. #. For objects which were only found on some orders, the standard (or the slit boundaries) are placed at the appropriate fractional position along the order. #. A PCA fit to the traces is performed using the routine above pca_fit Args: image (`numpy.ndarray`_): (Floating-point) Image to use for object search with shape (nspec, nspat). The first dimension (nspec) is spectral, and second dimension (nspat) is spatial. Note this image can either have the sky background in it, or have already been sky subtracted. Object finding works best on sky-subtracted images. Ideally, object finding is run in another routine, global sky-subtraction performed, and then this code should be run. However, it is also possible to run this code on non-sky-subtracted images. ivar (`numpy.ndarray`_): Floating-point inverse variance image for the input image. Shape must match ``image``, (nspec, nspat). slitmask (`numpy.ndarray`_): Integer image indicating the pixels that belong to each order. Pixels that are not on an order have value -1, and those that are on an order have a value equal to the slit number (i.e. 0 to nslits-1 from left to right on the image). Shape must match ``image``, (nspec, nspat). slit_left (`numpy.ndarray`_): Left boundary of orders to be extracted (given as floating point pixels). Shape is (nspec, norders), where norders is the total number of traced echelle orders. slit_righ (`numpy.ndarray`_): Right boundary of orders to be extracted (given as floating point pixels). Shape is (nspec, norders), where norders is the total number of traced echelle orders. order_vec (`numpy.ndarray`_): Vector identifying the Echelle orders for each pair of order edges found. This is saved to the output :class:`~pypeit.specobj.SpecObj` objects. If the orders are not known, this can be ``np.arange(norders)`` (but this is *not* recommended). maskslits (`numpy.ndarray`_): Boolean array selecting orders that should be ignored (i.e., good orders are False, bad orders are True). Shape must be (norders,). det (:obj:`str`, optional): The name of the detector containing the object. Only used if ``specobj_dict`` is None. inmask (`numpy.ndarray`_, optional): Good-pixel mask for input image. Must have the same shape as ``image``. If None, all pixels in ``slitmask`` with non-negative values are considered good. spec_min_max (`numpy.ndarray`_, optional): 2D array defining the minimum and maximum pixel in the spectral direction with useable data for each order. Shape must be (2, norders). This should only be used for echelle spectrographs for which the orders do not entirely cover the detector. PCA tracing will re-map the traces such that they all have the same length, compute the PCA, and then re-map the orders back. This improves performance for echelle spectrographs by removing the nonlinear shrinking of the orders so that the linear pca operation can better predict the traces. If None, the minimum and maximum values will be determined automatically from ``slitmask``. fof_link (:obj:`float`, optional): Friends-of-friends linking length in arcseconds used to link together traces across orders. The routine links together at the same fractional slit position and links them together with a friends-of-friends algorithm using this linking length. plate_scale (:obj:`float`, `numpy.ndarray`_, optional): Plate scale in arcsec/pix. This can either be a single float for every order, or an array with shape (norders,) providing the plate scale of each order. std_trace (`numpy.ndarray`_, optional): Vector with the standard star trace, which is used as a crutch for tracing. Shape must be (nspec,). If None, the slit boundaries are used as the crutch. ncoeff (:obj:`int`, optional): Order of polynomial fit to traces. npca (:obj:`int`, optional): Number of PCA components to keep during PCA decomposition of the object traces. If None, the number of components set by requiring the PCA accounts for approximately 99% of the variance. coeff_npoly (:obj:`int`, optional): Order of polynomial used for PCA coefficients fitting. If None, value set automatically, see :func:`~pypeit.tracepca.pca_trace_object`. max_snr (:obj:`float`, optional): For an object to be included in the output object, it must have a max S/N ratio above this value. min_snr (:obj:`float`, optional): For an object to be included in the output object, it must have a a median S/N ratio above this value for at least ``nabove_min_snr`` orders (see below). nabove_min_snr (:obj:`int`, optional): The required number of orders that an object must have with median SNR greater than ``min_snr`` in order to be included in the output object. pca_explained_var (:obj:`float`, optional): The percentage (i.e., not the fraction) of the variance in the data accounted for by the PCA used to truncate the number of PCA coefficients to keep (see ``npca``). Ignored if ``npca`` is provided directly; see :func:`~pypeit.tracepca.pca_trace_object`. box_radius (:obj:`float`, optional): Box_car extraction radius in arcseconds to assign to each detected object and to be used later for boxcar extraction. In this method ``box_radius`` is converted into pixels using ``plate_scale``. ``box_radius`` is also used for SNR calculation and trimming. fwhm (:obj:`float`, optional): Estimated fwhm of the objects in pixels use_user_fwhm (:obj:`bool`, optional): If True, ``PypeIt`` will use the spatial profile FWHM input by the user (see ``fwhm``) rather than determine the spatial FWHM from the smashed spatial profile via the automated algorithm. maxdev (:obj:`float`, optional): Maximum deviation of pixels from polynomial fit to trace used to reject bad pixels in trace fitting. hand_extract_dict (:obj:`dict`, optional): Dictionary with info on manual extraction; see :class:`~pypeit.manual_extract.ManualExtractionObj`. nperorder (:obj:`int`, optional): Maximum number of objects allowed per order. If there are more detections than this number, the code will select the ``nperorder`` most significant detections. However, hand apertures will always be returned and do not count against this budget. extract_maskwidth (:obj:`float`, optional): Determines the initial size of the region in units of FWHM that will be used for local sky subtraction; See :func:`objs_in_slit` and :func:`~pypeit.core.skysub.local_skysub_extract`. snr_thresh (:obj:`float`, optional): SNR threshold for finding objects specobj_dict (:obj:`dict`, optional): Dictionary containing meta-data for the objects that will be propagated into the :class:`~pypeit.specobj.SpecObj` objects. The expected components are: - SLITID: The slit ID number - DET: The detector identifier - OBJTYPE: The object type - PYPELINE: The class of pipeline algorithms applied If None, the dictionary is filled with the following placeholders:: specobj_dict = {'SLITID': 999, 'DET': 'DET01', 'OBJTYPE': 'unknown', 'PYPELINE': 'unknown'} trim_edg (:obj:`tuple`, optional): A two-tuple of integers or floats used to ignore objects within this many pixels of the left and right slit boundaries, respectively. show_peaks (:obj:`bool`, optional): Plot the QA of the object peak finding in each order. show_fits (:obj:`bool`, optional): Plot trace fitting for final fits using PCA as crutch. show_single_fits (:obj:`bool`, optional): Plot trace fitting for single order fits. show_trace (:obj:`bool`, optional): Display the object traces on top of the image. show_single_trace (:obj:`bool`, optional): Display the object traces on top of the single order. show_pca (:obj:`bool`, optional): Display debugging plots for the PCA decomposition. debug_all (:obj:`bool`, optional): Show all the debugging plots. If True, this also overrides any provided values for ``show_peaks``, ``show_trace``, and ``show_pca``, setting them to True. objfindQA_filename (:obj:`str`, optional): Full path (directory and filename) for the object profile QA plot. If None, not plot is produced and saved. Returns: :class:`~pypeit.specobjs.SpecObjs`: Object containing the objects detected. """ raise DeprecationWarning msgs.error("This ginormous method as been Deprecated") #debug_all=True if debug_all: show_peaks = True #show_fits = True #show_single_fits = True show_trace = True show_pca = True #show_single_trace = True # TODO: This isn't used, right? debug = True if specobj_dict is None: specobj_dict = {'SLITID': 999, 'ECH_ORDERINDX': 999, 'DET': det, 'OBJTYPE': 'unknown', 'PYPELINE': 'Echelle'} # TODO Update FOF algorithm here with the one from scikit-learn. allmask = slitmask > -1 if inmask is None: inmask = allmask nspec, nspat = image.shape norders = len(order_vec) # Find the spat IDs gdslit_spat = np.unique(slitmask[slitmask >= 0]).astype(int) # Unique sorts if gdslit_spat.size != np.sum(np.invert(maskslits)): msgs.error('Masking of slitmask not in sync with that of maskslits. This is a bug') #msgs.error('There is a mismatch between the number of valid orders found by PypeIt and ' # 'the number expected for this spectrograph. Unable to continue. Please ' # 'submit an issue on Github: https://github.com/pypeit/PypeIt/issues .') if spec_min_max is None: spec_min_max = np.zeros((2,norders), dtype=int) for iord in range(norders): ispec, ispat = np.where(slitmask == gdslit_spat[iord]) spec_min_max[:,iord] = ispec.min(), ispec.max() # Setup the plate scale if isinstance(plate_scale,(float, int)): plate_scale_ord = np.full(norders, plate_scale) elif isinstance(plate_scale,(np.ndarray, list, tuple)): if len(plate_scale) == norders: plate_scale_ord = plate_scale elif len(plate_scale) == 1: plate_scale_ord = np.full(norders, plate_scale[0]) else: msgs.error('Invalid size for plate_scale. It must either have one element or norders elements') else: msgs.error('Invalid type for plate scale') specmid = nspec // 2 spec_vec = np.arange(nspec) slit_width = slit_righ - slit_left slit_spec_pos = nspec/2.0 # TODO JFH This hand apertures in echelle needs to be completely refactored. # Hand prep # Determine the location of the source on *all* of the orders if hand_extract_dict is not None: f_spats = [] for ss, spat, spec in zip(range(len(hand_extract_dict['spec'])), hand_extract_dict['spat'], hand_extract_dict['spec']): # Find the input slit ispec = int(np.clip(np.round(spec),0,nspec-1)) ispat = int(np.clip(np.round(spat),0,nspat-1)) slit = slitmask[ispec, ispat] if slit == -1: msgs.error('You are requesting a manual extraction at a position ' + f'(spat, spec)={spat, spec} that is not on one of the echelle orders. Check your pypeit file.') # Fractions iord_hand = gdslit_spat.tolist().index(slit) f_spat = (spat - slit_left[ispec, iord_hand]) / ( slit_righ[ispec, iord_hand] - slit_left[ispec, iord_hand]) f_spats.append(f_spat) # Loop over orders and find objects sobjs = specobjs.SpecObjs() # TODO: replace orderindx with the true order number here? Maybe not. Clean # up SLITID and orderindx! gdorders = np.arange(norders)[np.invert(maskslits)] for iord in gdorders: #range(norders): qa_title = 'Finding objects on order # {:d}'.format(order_vec[iord]) msgs.info(qa_title) thisslit_gpm = slitmask == gdslit_spat[iord] inmask_iord = inmask & thisslit_gpm specobj_dict['SLITID'] = gdslit_spat[iord] specobj_dict['ECH_ORDERINDX'] = iord specobj_dict['ECH_ORDER'] = order_vec[iord] std_in = None if std_trace is None else std_trace[:, iord] # TODO JFH: Fix this. The way this code works, you should only need to create a single hand object, # not one at every location on the order if hand_extract_dict is not None: new_hand_extract_dict = copy.deepcopy(hand_extract_dict) for ss, spat, spec, f_spat in zip(range(len(hand_extract_dict['spec'])), hand_extract_dict['spat'], hand_extract_dict['spec'], f_spats): ispec = int(spec) new_hand_extract_dict['spec'][ss] = ispec new_hand_extract_dict['spat'][ss] = slit_left[ispec,iord] + f_spat*( slit_righ[ispec,iord]-slit_left[ispec,iord]) else: new_hand_extract_dict = None # Get SLTIORD_ID for the objfind QA ech_objfindQA_filename = objfindQA_filename.replace('S0999', 'S{:04d}'.format(order_vec[iord])) \ if objfindQA_filename is not None else None # Run sobjs_slit = \ objs_in_slit(image, ivar, thisslit_gpm, slit_left[:,iord], slit_righ[:,iord], spec_min_max=spec_min_max[:,iord], inmask=inmask_iord,std_trace=std_in, ncoeff=ncoeff, fwhm=fwhm, use_user_fwhm=use_user_fwhm, maxdev=maxdev, hand_extract_dict=new_hand_extract_dict, nperslit=nperorder, extract_maskwidth=extract_maskwidth, snr_thresh=snr_thresh, trim_edg=trim_edg, boxcar_rad=box_radius/plate_scale_ord[iord], show_peaks=show_peaks, show_fits=show_single_fits, show_trace=show_single_trace, qa_title=qa_title, specobj_dict=specobj_dict, objfindQA_filename=ech_objfindQA_filename) sobjs.add_sobj(sobjs_slit) nfound = len(sobjs) if nfound == 0: msgs.warn('No objects found') return sobjs FOF_frac = fof_link/(np.median(np.median(slit_width,axis=0)*plate_scale_ord)) # Run the FOF. We use fake coordinates fracpos = sobjs.SPAT_FRACPOS ra_fake = fracpos/1000.0 # Divide all angles by 1000 to make geometry euclidian dec_fake = np.zeros_like(fracpos) if nfound>1: inobj_id, multobj_id, firstobj_id, nextobj_id \ = pydl.spheregroup(ra_fake, dec_fake, FOF_frac/1000.0) # TODO spheregroup returns zero based indices but we use one based. We should probably add 1 to inobj_id here, # i.e. obj_id_init = inobj_id + 1 obj_id_init = inobj_id.copy() elif nfound==1: obj_id_init = np.zeros(1,dtype='int') uni_obj_id_init, uni_ind_init = np.unique(obj_id_init, return_index=True) # Now loop over the unique objects and check that there is only one object per order. If FOF # grouped > 1 objects on the same order, then this will be popped out as its own unique object obj_id = obj_id_init.copy() nobj_init = len(uni_obj_id_init) for iobj in range(nobj_init): for iord in range(norders): on_order = (obj_id_init == uni_obj_id_init[iobj]) & (sobjs.ECH_ORDERINDX == iord) if (np.sum(on_order) > 1): msgs.warn('Found multiple objects in a FOF group on order iord={:d}'.format(iord) + msgs.newline() + 'Spawning new objects to maintain a single object per order.') off_order = (obj_id_init == uni_obj_id_init[iobj]) & (sobjs.ECH_ORDERINDX != iord) ind = np.where(on_order)[0] if np.any(off_order): # Keep the closest object to the location of the rest of the group (on other orders) # as corresponding to this obj_id, and spawn new obj_ids for the others. frac_mean = np.mean(fracpos[off_order]) min_dist_ind = np.argmin(np.abs(fracpos[ind] - frac_mean)) else: # If there are no other objects with this obj_id to compare to, then we simply have multiple # objects grouped together on the same order, so just spawn new object IDs for them to maintain # one obj_id per order min_dist_ind = 0 ind_rest = np.setdiff1d(ind,ind[min_dist_ind]) # JFH OLD LINE with bug #obj_id[ind_rest] = (np.arange(len(ind_rest)) + 1) + obj_id_init.max() obj_id[ind_rest] = (np.arange(len(ind_rest)) + 1) + obj_id.max() uni_obj_id, uni_ind = np.unique(obj_id, return_index=True) nobj = len(uni_obj_id) msgs.info('FOF matching found {:d}'.format(nobj) + ' unique objects') gfrac = np.zeros(nfound) for jj in range(nobj): this_obj_id = obj_id == uni_obj_id[jj] gfrac[this_obj_id] = np.median(fracpos[this_obj_id]) uni_frac = gfrac[uni_ind] # Sort with respect to fractional slit location to guarantee that we have a similarly sorted list of objects later isort_frac = uni_frac.argsort() uni_obj_id = uni_obj_id[isort_frac] uni_frac = uni_frac[isort_frac] sobjs_align = sobjs.copy() # Loop over the orders and assign each specobj a fractional position and a obj_id number for iobj in range(nobj): for iord in range(norders): on_order = (obj_id == uni_obj_id[iobj]) & (sobjs_align.ECH_ORDERINDX == iord) sobjs_align[on_order].ECH_FRACPOS = uni_frac[iobj] sobjs_align[on_order].ECH_OBJID = uni_obj_id[iobj] sobjs_align[on_order].OBJID = uni_obj_id[iobj] sobjs_align[on_order].ech_frac_was_fit = False # Reset names (just in case) sobjs_align.set_names() # Now loop over objects and fill in the missing objects and their traces. We will fit the fraction slit position of # the good orders where an object was found and use that fit to predict the fractional slit position on the bad orders # where no object was found for iobj in range(nobj): # Grab all the members of this obj_id from the object list indx_obj_id = sobjs_align.ECH_OBJID == uni_obj_id[iobj] nthisobj_id = np.sum(indx_obj_id) # Perform the fit if this objects shows up on more than three orders if (nthisobj_id > 3) and (nthisobj_id<norders): thisorderindx = sobjs_align[indx_obj_id].ECH_ORDERINDX goodorder = np.zeros(norders, dtype=bool) goodorder[thisorderindx] = True badorder = np.invert(goodorder) xcen_good = (sobjs_align[indx_obj_id].TRACE_SPAT).T slit_frac_good = (xcen_good-slit_left[:,goodorder])/slit_width[:,goodorder] # Fractional slit position averaged across the spectral direction for each order frac_mean_good = np.mean(slit_frac_good, 0) # Perform a linear fit to fractional slit position #TODO Do this as a S/N weighted fit similar to what is now in the pca_trace algorithm? #msk_frac, poly_coeff_frac = fitting.robust_fit(order_vec[goodorder], frac_mean_good, 1, pypeitFit = fitting.robust_fit(order_vec[goodorder], frac_mean_good, 1, function='polynomial', maxiter=20, lower=2, upper=2, use_mad= True, sticky=False, minx = order_vec.min(), maxx=order_vec.max()) frac_mean_new = np.zeros(norders) frac_mean_new[badorder] = pypeitFit.eval(order_vec[badorder])#, minx = order_vec.min(),maxx=order_vec.max()) frac_mean_new[goodorder] = frac_mean_good # TODO This QA needs some work if show_pca: frac_mean_fit = pypeitFit.eval(order_vec) plt.plot(order_vec[goodorder][pypeitFit.bool_gpm], frac_mean_new[goodorder][pypeitFit.bool_gpm], 'ko', mfc='k', markersize=8.0, label='Good Orders Kept') plt.plot(order_vec[goodorder][np.invert(pypeitFit.bool_gpm)], frac_mean_new[goodorder][np.invert(pypeitFit.bool_gpm)], 'ro', mfc='k', markersize=8.0, label='Good Orders Rejected') plt.plot(order_vec[badorder], frac_mean_new[badorder], 'ko', mfc='None', markersize=8.0, label='Predicted Bad Orders') plt.plot(order_vec,frac_mean_new,'+',color='cyan',markersize=12.0,label='Final Order Fraction') plt.plot(order_vec, frac_mean_fit, 'r-', label='Fractional Order Position Fit') plt.xlabel('Order Index', fontsize=14) plt.ylabel('Fractional Slit Position', fontsize=14) plt.title('Fractional Slit Position Fit') plt.legend() plt.show() else: frac_mean_new = np.full(norders, uni_frac[iobj]) # Now loop over the orders and add objects on the ordrers for which the current object was not found for iord in range(norders): # Is the current object detected on this order? on_order = (sobjs_align.ECH_OBJID == uni_obj_id[iobj]) & (sobjs_align.ECH_ORDERINDX == iord) num_on_order = np.sum(on_order) if num_on_order == 0: # If it is not, create a new sobjs and add to sobjs_align and assign required tags thisobj = specobj.SpecObj('Echelle', sobjs_align[0].DET, OBJTYPE=sobjs_align[0].OBJTYPE, ECH_ORDERINDX=iord, ECH_ORDER=order_vec[iord]) #thisobj.ECH_ORDERINDX = iord #thisobj.ech_order = order_vec[iord] thisobj.SPAT_FRACPOS = uni_frac[iobj] # Assign traces using the fractional position fit above if std_trace is not None: x_trace = np.interp(slit_spec_pos, spec_vec, std_trace[:,iord]) shift = np.interp(slit_spec_pos, spec_vec,slit_left[:,iord] + slit_width[:,iord]*frac_mean_new[iord]) - x_trace thisobj.TRACE_SPAT = std_trace[:,iord] + shift else: thisobj.TRACE_SPAT = slit_left[:, iord] + slit_width[:, iord] * frac_mean_new[iord] # new trace thisobj.trace_spec = spec_vec thisobj.SPAT_PIXPOS = thisobj.TRACE_SPAT[specmid] # Use the real detections of this objects for the FWHM this_obj_id = obj_id == uni_obj_id[iobj] # Assign to the fwhm of the nearest detected order imin = np.argmin(np.abs(sobjs_align[this_obj_id].ECH_ORDERINDX - iord)) thisobj.FWHM = sobjs_align[imin].FWHM thisobj.maskwidth = sobjs_align[imin].maskwidth thisobj.smash_peakflux = sobjs_align[imin].smash_peakflux thisobj.smash_snr = sobjs_align[imin].smash_snr thisobj.BOX_RADIUS = sobjs_align[imin].BOX_RADIUS thisobj.ECH_FRACPOS = uni_frac[iobj] thisobj.ECH_OBJID = uni_obj_id[iobj] thisobj.OBJID = uni_obj_id[iobj] thisobj.SLITID = gdslit_spat[iord] thisobj.ech_frac_was_fit = True thisobj.set_name() sobjs_align.add_sobj(thisobj) obj_id = np.append(obj_id, uni_obj_id[iobj]) gfrac = np.append(gfrac, uni_frac[iobj]) elif num_on_order == 1: # Object is already on this order so no need to do anything pass elif num_on_order > 1: msgs.error('Problem in echelle object finding. The same objid={:d} appears {:d} times on echelle orderindx ={:d}' ' even after duplicate obj_ids the orders were removed. ' 'Report this bug to PypeIt developers'.format(uni_obj_id[iobj],num_on_order, iord)) # Loop over the objects and perform a quick and dirty extraction to assess S/N. varimg = utils.calc_ivar(ivar) flux_box = np.zeros((nspec, norders, nobj)) ivar_box = np.zeros((nspec, norders, nobj)) mask_box = np.zeros((nspec, norders, nobj)) SNR_arr = np.zeros((norders, nobj)) slitfracpos_arr = np.zeros((norders, nobj)) for iobj in range(nobj): for iord in range(norders): iorder_vec = order_vec[iord] indx = sobjs_align.slitorder_objid_indices(iorder_vec, uni_obj_id[iobj]) #indx = (sobjs_align.ECH_OBJID == uni_obj_id[iobj]) & (sobjs_align.ECH_ORDERINDX == iord) #spec = sobjs_align[indx][0] inmask_iord = inmask & (slitmask == gdslit_spat[iord]) # TODO make the snippet below its own function quick_extraction() box_rad_pix = box_radius/plate_scale_ord[iord] # TODO -- We probably shouldn't be operating on a SpecObjs but instead a SpecObj flux_tmp = moment1d(image*inmask_iord, sobjs_align[indx][0].TRACE_SPAT, 2*box_rad_pix, row=sobjs_align[indx][0].trace_spec)[0] var_tmp = moment1d(varimg*inmask_iord, sobjs_align[indx][0].TRACE_SPAT, 2*box_rad_pix, row=sobjs_align[indx][0].trace_spec)[0] ivar_tmp = utils.calc_ivar(var_tmp) pixtot = moment1d(ivar*0 + 1.0, sobjs_align[indx][0].TRACE_SPAT, 2*box_rad_pix, row=sobjs_align[indx][0].trace_spec)[0] mask_tmp = moment1d(ivar*inmask_iord == 0.0, sobjs_align[indx][0].TRACE_SPAT, 2*box_rad_pix, row=sobjs_align[indx][0].trace_spec)[0] != pixtot flux_box[:,iord,iobj] = flux_tmp*mask_tmp ivar_box[:,iord,iobj] = np.fmax(ivar_tmp*mask_tmp,0.0) mask_box[:,iord,iobj] = mask_tmp mean, med_sn, stddev = astropy.stats.sigma_clipped_stats( flux_box[mask_tmp,iord,iobj]*np.sqrt(ivar_box[mask_tmp,iord,iobj]), sigma_lower=5.0,sigma_upper=5.0 ) # ToDO assign this to sobjs_align for use in the extraction SNR_arr[iord,iobj] = med_sn sobjs_align[indx][0].ech_snr = med_sn # For hand extractions slitfracpos_arr[iord,iobj] = sobjs_align[indx][0].SPAT_FRACPOS # Purge objects with low SNR that don't show up in enough orders, sort the list of objects with respect to obj_id # and orderindx keep_obj = np.zeros(nobj,dtype=bool) sobjs_trim = specobjs.SpecObjs() # objids are 1 based so that we can easily asign the negative to negative objects iobj_keep = 1 iobj_keep_not_hand = 1 # TODO JFH: Fix this ugly and dangerous hack that was added to accomodate hand apertures hand_frac = [-1000] if hand_extract_dict is None else [int(np.round(ispat*1000)) for ispat in f_spats] ## Loop over objects from highest SNR to lowest SNR. Apply the S/N constraints. Once we hit the maximum number # objects requested exit, except keep the hand apertures that were requested. isort_SNR_max = np.argsort(np.median(SNR_arr,axis=0))[::-1] for iobj in isort_SNR_max: hand_ap_flag = int(np.round(slitfracpos_arr[0, iobj]*1000)) in hand_frac SNR_constraint = (SNR_arr[:,iobj].max() > max_snr) or (np.sum(SNR_arr[:,iobj] > min_snr) >= nabove_min_snr) nperorder_constraint = (iobj_keep-1) < nperorder if (SNR_constraint and nperorder_constraint) or hand_ap_flag: keep_obj[iobj] = True ikeep = sobjs_align.ECH_OBJID == uni_obj_id[iobj] sobjs_keep = sobjs_align[ikeep].copy() sobjs_keep.ECH_OBJID = iobj_keep sobjs_keep.OBJID = iobj_keep # for spec in sobjs_keep: # spec.ECH_OBJID = iobj_keep # #spec.OBJID = iobj_keep sobjs_trim.add_sobj(sobjs_keep[np.argsort(sobjs_keep.ECH_ORDERINDX)]) iobj_keep += 1 if not hand_ap_flag: iobj_keep_not_hand += 1 else: if not nperorder_constraint: msgs.info('Purging object #{:d}'.format(iobj) + ' since there are already {:d} objects automatically identified ' 'and you set nperorder={:d}'.format(iobj_keep_not_hand-1, nperorder)) else: msgs.info('Purging object #{:d}'.format(iobj) + ' which does not satisfy max_snr > {:5.2f} OR min_snr > {:5.2f}'.format(max_snr, min_snr) + ' on at least nabove_min_snr >= {:d}'.format(nabove_min_snr) + ' orders') nobj_trim = np.sum(keep_obj) if nobj_trim == 0: msgs.warn('No objects found') sobjs_final = specobjs.SpecObjs() return sobjs_final # TODO JFH: We need to think about how to implement returning a maximum number of objects, where the objects # returned are the highest S/N ones. It is a bit complicated with regards to the individual object finding and then # the linking that is performed above, and also making sure the hand apertures don't get removed. SNR_arr_trim = SNR_arr[:,keep_obj] sobjs_final = sobjs_trim.copy() # Loop over the objects one by one and adjust/predict the traces pca_fits = np.zeros((nspec, norders, nobj_trim)) # Create the trc_inmask for iterative fitting below trc_inmask = np.zeros((nspec, norders), dtype=bool) for iord in range(norders): trc_inmask[:,iord] = (spec_vec >= spec_min_max[0,iord]) & (spec_vec <= spec_min_max[1,iord]) for iobj in range(nobj_trim): indx_obj_id = sobjs_final.ECH_OBJID == (iobj + 1) # PCA predict all the orders now (where we have used the standard or slit boundary for the bad orders above) msgs.info('Fitting echelle object finding PCA for object {:d}/{:d} with median SNR = {:5.3f}'.format( iobj + 1,nobj_trim,np.median(sobjs_final[indx_obj_id].ech_snr))) pca_fits[:,:,iobj] \ = tracepca.pca_trace_object(sobjs_final[indx_obj_id].TRACE_SPAT.T, order=coeff_npoly, npca=npca, pca_explained_var=pca_explained_var, trace_wgt=np.fmax(sobjs_final[indx_obj_id].ech_snr, 1.0)**2, debug=show_pca) # Trial and error shows weighting by S/N instead of S/N^2 performs better # JXP -- Updated to now be S/N**2, i.e. inverse variance, with fitting fit # Perform iterative flux weighted centroiding using new PCA predictions xinit_fweight = pca_fits[:,:,iobj].copy() inmask_now = inmask & allmask xfit_fweight = fit_trace(image, xinit_fweight, ncoeff, bpm=np.invert(inmask_now), trace_bpm=np.invert(trc_inmask), fwhm=fwhm, maxdev=maxdev, debug=show_fits)[0] # Perform iterative Gaussian weighted centroiding xinit_gweight = xfit_fweight.copy() xfit_gweight = fit_trace(image, xinit_gweight, ncoeff, bpm=np.invert(inmask_now), trace_bpm=np.invert(trc_inmask), weighting='gaussian', fwhm=fwhm, maxdev=maxdev, debug=show_fits)[0] #TODO Assign the new traces. Only assign the orders that were not orginally detected and traced. If this works # well, we will avoid doing all of the iter_tracefits above to make the code faster. for iord, spec in enumerate(sobjs_final[indx_obj_id]): # JFH added the condition on ech_frac_was_fit with S/N cut on 7-7-19. # TODO is this robust against half the order being masked? if spec.ech_frac_was_fit & (spec.ech_snr > 1.0): spec.TRACE_SPAT = xfit_gweight[:,iord] spec.SPAT_PIXPOS = spec.TRACE_SPAT[specmid] #TODO Put in some criterion here that does not let the fractional position change too much during the iterative # tracefitting. The problem is spurious apertures identified on one slit can be pulled over to the center of flux # resulting in a bunch of objects landing on top of each other. # Set the IDs sobjs_final[:].ECH_ORDER = order_vec[sobjs_final[:].ECH_ORDERINDX] #for spec in sobjs_final: # spec.ech_order = order_vec[spec.ECH_ORDERINDX] sobjs_final.set_names() if show_trace: viewer, ch = display.show_image(image*allmask) for spec in sobjs_trim: color = 'red' if spec.ech_frac_was_fit else 'magenta' ## Showing the final flux weighted centroiding from PCA predictions display.show_trace(viewer, ch, spec.TRACE_SPAT, spec.NAME, color=color) for iobj in range(nobj_trim): for iord in range(norders): ## Showing PCA predicted locations before recomputing flux/gaussian weighted centroiding display.show_trace(viewer, ch, pca_fits[:,iord, iobj], str(uni_frac[iobj]), color='yellow') ## Showing the final traces from this routine display.show_trace(viewer, ch, sobjs_final.TRACE_SPAT[iord].T, sobjs_final.NAME, color='cyan') # Labels for the points text_final = [dict(type='text', args=(nspat / 2 -40, nspec / 2, 'final trace'), kwargs=dict(color='cyan', fontsize=20))] text_pca = [dict(type='text', args=(nspat / 2 -40, nspec / 2 - 30, 'PCA fit'),kwargs=dict(color='yellow', fontsize=20))] text_fit = [dict(type='text', args=(nspat / 2 -40, nspec / 2 - 60, 'predicted'),kwargs=dict(color='red', fontsize=20))] text_notfit = [dict(type='text', args=(nspat / 2 -40, nspec / 2 - 90, 'originally found'),kwargs=dict(color='magenta', fontsize=20))] canvas = viewer.canvas(ch._chname) canvas_list = text_final + text_pca + text_fit + text_notfit canvas.add('constructedcanvas', canvas_list) # TODO two things need to be debugged. 1) For objects which were found and traced, i don't think we should be updating the tracing with # the PCA. This just adds a failutre mode. 2) The PCA fit is going wild for X-shooter. Debug that. # Vette for sobj in sobjs_final: if not sobj.ready_for_extraction(): msgs.error("Bad SpecObj. Can't proceed") return sobjs_final
[docs]def objfind_QA(spat_peaks, snr_peaks, spat_vector, snr_vector, snr_thresh, qa_title, peak_gpm, near_edge_bpm, nperslit_bpm, objfindQA_filename=None, show=False): """ Utility routine for making object finding QA plots. Args: spat_peaks (`numpy.ndarray`_): Array of locations in the spatial direction at which objects were identified. Shape is ``(npeaks,)``, where ``npeaks`` is the number of peaks identified. snr_peaks (`numpy.ndarray`_): S/N ratio in the spectral direction after collapsing along the spectral direction, evaluated at the location of each spatial peak. Shape must match ``spat_peaks``. spat_vector (`numpy.ndarray`_): A 1D array of spatial locations along the slit. Shape is ``(nsamp,)``, where ``nsamp`` is the number of spatial pixels defined by the slit edges. snr_vector (`numpy.ndarray`_): A 1D array with the S/N ratio sampled along the slit at each spatial location (i.e., spectral direction has been smashed out) defined by ``spat_vector``. Shape must match ``spat_vector``. snr_thresh (:obj:`float`): The threshold S/N ratio adopted by the object finding. qa_title (:obj:`str`): Title for the QA file plot. peak_gpm (`numpy.ndarray`_): Boolean array containing a good pixel mask for each peak indicating whether it will be used as an object (True) or not (False). Shape must match ``spat_peaks``. near_edge_bpm (`numpy.ndarray`_): A bad pixel mask (True is masked, False is unmasked) indicating which objects are masked because they are near the slit edges. Shape must match ``spat_peaks``. nperslit_bpm (`numpy.ndarray`_): A bad pixel mask (True is masked, False is unmasked) indicating which objects are masked because they exceed the maximum number of objects (see :func:`objs_in_slit` parameter ``nperslit``) that were specified as being on this slit. objfindQA_filename (:obj:`str`, optional): Output filename for the QA plot. If None, plot is not saved. show (:obj:`bool`, optional): If True, show the plot as a matplotlib interactive plot. """ plt.plot(spat_vector, snr_vector, drawstyle='steps-mid', color='black', label = 'Collapsed SNR (FWHM convol)') plt.hlines(snr_thresh,spat_vector.min(),spat_vector.max(), color='red',linestyle='--', label='SNR_THRESH={:5.3f}'.format(snr_thresh)) if np.any(peak_gpm): plt.plot(spat_peaks[peak_gpm], snr_peaks[peak_gpm], color='red', marker='o', markersize=10.0, mfc='lawngreen', fillstyle='full',linestyle='None', zorder = 10,label='{:d} Good Objects'.format(np.sum(peak_gpm))) if np.any(near_edge_bpm): plt.plot(spat_peaks[near_edge_bpm], snr_peaks[near_edge_bpm], color='red', marker='o', markersize=10.0, mfc='cyan', fillstyle='full', linestyle='None', zorder = 10,label='{:d} Rejected: Near Edge'.format(np.sum(near_edge_bpm))) if np.any(nperslit_bpm): plt.plot(spat_peaks[nperslit_bpm], snr_peaks[nperslit_bpm], color='red', marker='o', markersize=10.0, mfc='yellow', fillstyle='full', linestyle='None', zorder = 10,label='{:d} Rejected: Nperslit'.format(np.sum(nperslit_bpm))) plt.legend() plt.xlabel('Approximate Spatial Position (pixels)') plt.ylabel('SNR') plt.title(qa_title) #plt.ylim(np.fmax(snr_vector.min(), -20.0), 1.3*snr_vector.max()) fig = plt.gcf() if show: plt.show() if objfindQA_filename is not None: fig.savefig(objfindQA_filename, dpi=400) plt.close('all')
[docs]def get_fwhm(fwhm_in, nsamp, smash_peakflux, spat_fracpos, flux_smash_smth): """ Utility routine to measure the FWHM of an object trace from the spectrally collapsed flux profile by determining the locations along the spatial direction where this profile reaches have its peak value. Args: fwhm_in (:obj:`float`): Best guess for the FWHM of this object. nsamp (:obj:`int`): Number of pixels along the spatial direction. smash_peakflux (:obj:`float`): The peak flux in the 1d (spectrally collapsed) flux profile at the object location. spat_fracpos (:obj:`float`): Fractional spatial position along the slit where the object is located and at which the ``flux_smash_smth`` array has values provided by ``smash_peakflux`` (see above and below). flux_smash_smth (`numpy.ndarray`_): A 1D array with the flux averaged along the spectral direction at each location along the slit in the spatial direction location. Shape is ``(nsamp,)``. Returns: :obj:`float`: The FWHM determined from the object flux profile, unless the FWHM could not be found from the profile, in which case the input guess (``fwhm_in``) is simply returned. """ # Determine the fwhm max yhalf = 0.5*smash_peakflux xpk = spat_fracpos*nsamp x0 = int(np.rint(xpk)) # TODO It seems we have two codes that do similar things, i.e. findfwhm in arextract.py. Could imagine having one # Find right location where smash profile croses yhalf if x0 < (int(nsamp) - 1): ind_righ, = np.where(flux_smash_smth[x0:] < yhalf) if len(ind_righ) > 0: i2 = ind_righ[0] if i2 == 0: xrigh = None else: xarr_righ = x0 + np.array([i2 - 1, i2], dtype=float) xrigh_int = scipy.interpolate.interp1d(flux_smash_smth[x0 + i2 - 1:x0 + i2 + 1], xarr_righ, assume_sorted=False, bounds_error=False, fill_value=(xarr_righ[0], xarr_righ[1])) xrigh = xrigh_int([yhalf])[0] else: xrigh = None else: xrigh = None # Find left location where smash profile crosses yhalf if x0 > 0: ind_left, = np.where(flux_smash_smth[0:np.fmin(x0 + 1, int(nsamp) - 1)] < yhalf) if len(ind_left) > 0: i1 = (ind_left[::-1])[0] if i1 == (int(nsamp) - 1): xleft = None else: xarr_left = np.array([i1, i1 + 1], dtype=float) xleft_int = scipy.interpolate.interp1d(flux_smash_smth[i1:i1 + 2], xarr_left, assume_sorted=False, bounds_error=False, fill_value=(xarr_left[0], xarr_left[1])) xleft = xleft_int([yhalf])[0] else: xleft = None else: xleft = None # Set FWHM for the object if (xleft is None) & (xrigh is None): fwhm_measure = None elif xrigh is None: fwhm_measure = 2.0 * (xpk - xleft) elif xleft is None: fwhm_measure = 2.0 * (xrigh - xpk) else: fwhm_measure = (xrigh - xleft) if fwhm_measure is not None: fwhm_out = np.sqrt(np.fmax(fwhm_measure ** 2 - fwhm_in ** 2, (fwhm_in / 2.0) ** 2)) # Set a floor of fwhm/2 on fwhm else: fwhm_out = fwhm_in return fwhm_out
[docs]def objs_in_slit(image, ivar, thismask, slit_left, slit_righ, inmask=None, fwhm=3.0, sigclip_smash=5.0, use_user_fwhm=False, boxcar_rad=7., maxdev=2.0, spec_min_max=None, hand_extract_dict=None, std_trace=None, ncoeff=5, nperslit=None, snr_thresh=10.0, trim_edg=(5,5), extract_maskwidth=4.0, specobj_dict=None, find_min_max=None, show_peaks=False, show_fits=False, show_trace=False, debug_all=False, qa_title='objfind', objfindQA_filename=None): """ Find the location of objects in a slitmask slit or a echelle order. The algorithm for this function is: - Rectify the image by extracting along the edge traces. - Compute the sigma-clipped mean spatial profile and its variance by collapsing the image along the spectral direction to constuct a S/N spatial profile of the slit/order. - Smooth the S/N profile by a Gaussian with the provided FWHM (see ``fwhm``) and detect peaks in the smoothed profile using :func:`~pypeit.core.arc.detect_lines`. Ignore peaks found near the slit edges, and limit the number of peaks to the number requested (see ``nperslit``). - Instantiate a :class:`~pypeit.specobj.SpecObj` object for each valid detection, construct preliminary spectral traces for them, and estimate the object spatial FWHM if one is not provided (see ``use_user_fwhm``). - For automatically identified objects (i.e., not manual extractions), improve the object trace by fitting the spatial position of the peak as a function of wavelength. For manual apertures, use either the form of the brightest object on the slit, the trace of a standard star (see ``std_trace``), or the left edge trace to set the trace for the object. Finally, remove automatically identified objects that overlap with manually defined extraction apertures. At the end of this function, the list of objects is ready for extraction. **Revision History:** - 10-Mar-2005 -- First version written by D. Schlegel, LBL - 2005-2018 -- Improved by J. F. Hennawi and J. X. Prochaska - 23-June-2018 -- Ported to python by J. F. Hennawi and significantly improved - 01-Feb-2022 -- Skymask stripped out by JXP Args: image (`numpy.ndarray`_): (Floating-point) Image to use for object search with shape (nspec, nspat). The first dimension (nspec) is spectral, and second dimension (nspat) is spatial. Note this image can either have the sky background in it, or have already been sky subtracted. Object finding works best on sky-subtracted images. Ideally, object finding is run in another routine, global sky-subtraction performed, and then this code should be run. However, it is also possible to run this code on non-sky-subtracted images. ivar (`numpy.ndarray`_): Floating-point inverse variance image for the input image. Shape must match ``image``, (nspec, nspat). thismask (`numpy.ndarray`_): Boolean mask image selecting pixels associated with the slit/order to search for objects on (True means on the slit/order). Shape must match ``image``. slit_left (`numpy.ndarray`_): Left boundary of a single slit/orders to be extracted (given as floating point pixels). Shape is (nspec,). slit_righ (`numpy.ndarray`_): Right boundary of a single slit/orders to be extracted (given as floating point pixels). Shape is (nspec,). inmask (`numpy.ndarray`_, optional): Good-pixel mask for input image. Shape must match ``image``. If None, set to be the same as ``thismask``. fwhm (:obj:`float`, optional): Estimated FWHM of the objects in pixels sigclip_smash (:obj:`float`, optional): Sigma clipping threshold passed to `astropy.stats.sigma_clipped_stats`_ to compute average slit emission profile by averaging the (rectified) image along the spatial direction. use_user_fwhm (:obj:`bool`, optional): If True, ``PypeIt`` will use the spatial profile FWHM input by the user (see ``fwhm``) rather than determine the spatial FWHM from the smashed spatial profile via the automated algorithm. boxcar_rad (:obj:`float`, optional): Box_car extraction radius *in pixels* to assign to each detected object and to be used later for boxcar extraction. maxdev (:obj:`float`, optional): Maximum deviation of pixels from polynomial fit to trace used to reject bad pixels in trace fitting. spec_min_max (:obj:`tuple`, optional): 2-tuple defining the minimum and maximum pixel in the spectral direction with useable data for this slit/order. If None, the values will be determined automatically from ``thismask``. Either element of the tuple can also None, which will then default to using the full min or max over which the slit is defined from ``thismask``. hand_extract_dict (:obj:`dict`, optional): Dictionary with info on manual extraction; see :class:`~pypeit.manual_extract.ManualExtractionObj`. std_trace (`numpy.ndarray`_, optional): Vector with the standard star trace, which is used as a crutch for tracing. Shape must be (nspec,). If None, the slit boundaries are used as the crutch. ncoeff (:obj:`int`, optional): Order of polynomial fit to traces. nperslit (:obj:`int`, optional): Maximum number of objects to find. If there are more detections than this number, the code will select the ``nperslit`` most significant detections. However, hand apertures will always be returned and do not count against this budget. snr_thresh (:obj:`float`, optional): SNR threshold for finding objects trim_edg (:obj:`tuple`, optional): A two-tuple of integers or floats used to ignore objects within this many pixels of the left and right slit boundaries, respectively. extract_maskwidth (:obj:`float`, optional): Determines the initial size of the region in units of FWHM that will be used for local sky subtraction; See :func:`objs_in_slit` and :func:`~pypeit.core.skysub.local_skysub_extract`. specobj_dict (:obj:`dict`, optional): Dictionary containing meta-data for the objects that will be propagated into the :class:`~pypeit.specobj.SpecObj` objects. The expected components are: - SLITID: The slit ID number - DET: The detector identifier - OBJTYPE: The object type - PYPELINE: The class of pipeline algorithms applied If None, the dictionary is filled with the following placeholders:: specobj_dict = {'SLITID': 999, 'DET': 'DET01', 'OBJTYPE': 'unknown', 'PYPELINE': 'Multislit'} find_min_max (:obj:`tuple`, optional): 2-tuple of integers that defines the minimum and maximum pixel location of your *object* in the spectral direction on the detector. It is only used for object finding. This parameter is helpful if your object only has emission lines or at high redshift and the trace only shows in part of the detector. Either element of the tuple can be None, which will then default to using the full range over which the slit is defined. This is distinct from ``spec_min_max`` in that ``spec_min_max`` indicates the range of the slit/order on the detector, whereas ``find_min_max`` indicates the range to be used for object finding. If None or if either member of the tuple is None, it will default to the values of ``spec_min_max``. show_peaks (:obj:`bool`, optional): Plot the QA of the object peak finding in each order. show_fits (:obj:`bool`, optional): Plot trace fitting for final fits using PCA as crutch. show_trace (:obj:`bool`, optional): Display the object traces on top of the image. debug_all (:obj:`bool`, optional): Show all the debugging plots. If True, this also overrides any provided values for ``show_peaks``, ``show_fits``, and ``show_trace``, setting them to True. qa_title (:obj:`str`, optional): Title to be printed in the QA plots objfindQA_filename (:obj:`str`, optional): Full path (directory and filename) for the object profile QA plot. If None, not plot is produced and saved. Returns: :class:`~pypeit.specobjs.SpecObjs`: Object containing the objects detected. """ #debug_all=True if debug_all: show_peaks=True show_fits = True show_trace = True if specobj_dict is None: specobj_dict = dict(SLITID=999, DET='DET01', OBJTYPE='unknown', PYPELINE='MultiSlit') nspec, nspat = image.shape specmid = nspec//2 # Some information about this slit we need for later when we instantiate specobj objects spec_vec = np.arange(nspec) spat_vec = np.arange(nspat) ximg, edgmask = pixels.ximg_and_edgemask(slit_left, slit_righ, thismask, trim_edg=trim_edg) # If a mask was not passed in, create it if inmask is None: inmask = thismask # If spec_min_max was not passed in, determine it from the thismask ispec, ispat = np.where(thismask) spec_min = ispec.min() spec_max = ispec.max() if spec_min_max is None or np.any([s is None or np.isinf(s) for s in spec_min_max]): if spec_min_max is None: spec_min_max_out = np.array([spec_min, spec_max]) else: spec_min_max_out = np.array(spec_min_max).copy() if spec_min_max_out[0] is None or np.isinf(spec_min_max_out[0]): spec_min_max_out[0] = spec_min if spec_min_max_out[1] is None or np.isinf(spec_min_max_out[1]): spec_min_max_out[1] = spec_max spec_min_max_out = np.array(spec_min_max_out).astype(int) else: spec_min_max_out = np.array(spec_min_max).astype(int) # If find_min_max was not passed in, set it to the values for spec_min_max if find_min_max is None or np.any([f is None or np.isinf(f) for f in find_min_max]): if find_min_max is None: find_min_max_out = spec_min_max_out else: find_min_max_out = np.array(find_min_max).copy() if find_min_max_out[0] is None or np.isinf(find_min_max_out[0]): find_min_max_out[0] = spec_min_max_out[0] if find_min_max_out[1] is None or np.isinf(find_min_max_out[1]): find_min_max_out[1] = spec_min_max_out[1] find_min_max_out = np.array(find_min_max_out).astype(int) else: find_min_max_out = np.array(find_min_max).astype(int) #totmask = thismask & inmask & np.invert(edgmask) # Smash the image (for this slit) into a single flux vector. How many pixels wide is the slit at each Y? xsize = slit_righ - slit_left #nsamp = np.ceil(np.median(xsize)) # JFH Changed 07-07-19 nsamp = np.ceil(xsize.max()) # Mask skypixels with 2 fwhm of edge left_asym = slit_left[:,None] + np.outer(xsize/nsamp, np.arange(nsamp)) righ_asym = left_asym + np.outer(xsize/nsamp, np.ones(int(nsamp))) # This extract_asymbox_boxcar call rectifies the image along the curved object traces gpm_tot = thismask & inmask & (ivar > 0.0) image_rect, gpm_rect, npix_rect, ivar_rect = extract.extract_asym_boxcar(image, left_asym, righ_asym, gpm=gpm_tot, ivar=ivar) # This smashes out the spatial direction to construct an aggregate sky model #sky_mean, sky_median, sky_sig = stats.sigma_clipped_stats(image_rect, mask=np.logical_not(gpm_rect), axis=1, sigma=3.0, # cenfunc='median', stdfunc=utils.nan_mad_std) #gpm_sky = np.sum(gpm_rect,axis=1) != 0 & np.isfinite(sky_median) #sky_fill_value = np.median(sky_median[gpm_sky]) if np.any(gpm_sky) else 0.0 #sky_median[np.logical_not(gpm_sky)] = sky_fill_value #sky_rect = np.repeat(sky_median[:, np.newaxis], nsamp, axis=1) #sky_rect = 0.0*image_rect # Apply find_min_max_out find_min_max_gpm = np.zeros_like(image_rect, dtype=bool) find_min_max_gpm[find_min_max_out[0]: find_min_max_out[1], :] = True data = np.ma.MaskedArray( image_rect, mask=np.logical_not(gpm_rect & find_min_max_gpm)) # the total gpm = gpm_rect & find_min_max_gpm sigclip = astropy.stats.SigmaClip( sigma=sigclip_smash, maxiters=25, cenfunc='median', stdfunc=utils.nan_mad_std ) data_clipped, lower, upper = sigclip(data, axis=0, masked=True, return_bounds=True) gpm_sigclip = np.logical_not(data_clipped.mask) # Compute the average flux over the set of pixels that are not masked by gpm_sigclip nsmash = find_min_max_out[1] - find_min_max_out[0] + 1 npix_smash = np.sum(gpm_sigclip[find_min_max_out[0]:find_min_max_out[1]], axis=0) gpm_smash = npix_smash > 0.3*nsmash flux_sum_smash = np.sum((image_rect*gpm_sigclip)[find_min_max_out[0]:find_min_max_out[1]], axis=0) flux_smash = flux_sum_smash*gpm_smash/(npix_smash + (npix_smash == 0.0)) flux_smash_mean, flux_smash_med, flux_smash_std = astropy.stats.sigma_clipped_stats( flux_smash, mask=np.logical_not(gpm_smash), sigma_lower=3.0, sigma_upper=3.0 ) flux_smash_recen = flux_smash - flux_smash_med # Return if none found and no hand extraction if not np.any(gpm_smash): sobjs = specobjs.SpecObjs() if hand_extract_dict is None: # Instantiate a null specobj and return msgs.info('No objects found automatically. Consider manual extraction.') return sobjs else: msgs.info('No objects found automatically.') else: # Compute the formal corresponding variance over the set of pixels that are not masked by gpm_sigclip var_rect = utils.inverse(ivar_rect) var_sum_smash = np.sum((var_rect*gpm_sigclip)[find_min_max_out[0]:find_min_max_out[1]], axis=0) var_smash = var_sum_smash/(npix_smash**2 + (npix_smash == 0.0)) ivar_smash = utils.inverse(var_smash)*gpm_smash snr_smash = flux_smash_recen*np.sqrt(ivar_smash) # Smooth this SNR image with a Gaussian set by the input fwhm gauss_smth_sigma = (fwhm/2.3548) snr_smash_smth = scipy.ndimage.gaussian_filter1d(snr_smash, gauss_smth_sigma, mode='nearest') flux_smash_smth = scipy.ndimage.gaussian_filter1d(flux_smash_recen, gauss_smth_sigma, mode='nearest') # Search for spatial direction peaks in the smoothed snr image _, _, x_peaks_out, x_width, x_err, igood, _, _ = arc.detect_lines( snr_smash_smth, input_thresh=snr_thresh, fit_frac_fwhm=1.5, fwhm=fwhm, min_pkdist_frac_fwhm=0.75, max_frac_fwhm=10.0, cont_subtract=False, debug_peak_find=False) x_peaks_all = x_peaks_out[igood] #x_peaks_all = arc.detect_peaks(snr_smash_smth, mph=snr_thresh, mpd=fwhm*0.75, show=False) snr_peaks_all = np.interp(x_peaks_all, np.arange(nsamp), snr_smash_smth) flux_peaks_all = np.interp(x_peaks_all, np.arange(nsamp), flux_smash_smth) npeaks_all = len(x_peaks_all) near_edge_bpm = (x_peaks_all < trim_edg[0]) | (x_peaks_all > (nsamp - trim_edg[1])) npeak_not_near_edge = np.sum(np.logical_not(near_edge_bpm)) if np.any(near_edge_bpm): msgs.warn('Discarding {:d}'.format(np.sum(near_edge_bpm)) + ' at spatial pixels spat = {:}'.format(x_peaks_all[near_edge_bpm]) + ' which land within trim_edg = (left, right) = {:}'.format(trim_edg) + ' pixels from the slit boundary for this nsamp = {:5.2f}'.format(nsamp) + ' wide slit') msgs.warn('You must decrease from the current value of trim_edg in order to keep them') msgs.warn('Such edge objects are often spurious') # If the user requested the nperslit most significant peaks have been requested, then only return these if nperslit is not None: # If the requested number is less than (the non-edge) number found, mask them out if nperslit < npeak_not_near_edge: snr_peaks_not_edge = np.sort(snr_peaks_all[np.logical_not(near_edge_bpm)])[::-1] snr_thresh_perslit = snr_peaks_not_edge[nperslit-1] nperslit_bpm = np.logical_not(near_edge_bpm) & (snr_peaks_all < snr_thresh_perslit) else: nperslit_bpm = np.zeros(npeaks_all, dtype=bool) else: nperslit_bpm = np.zeros(npeaks_all, dtype=bool) if np.any(nperslit_bpm): msgs.warn('Discarding {:d}'.format(np.sum(nperslit_bpm)) + ' at spatial pixels spat = {:} and SNR = {:}'.format( x_peaks_all[nperslit_bpm], snr_peaks_all[nperslit_bpm]) + ' which are below SNR_thresh={:5.3f} set because the maximum number of objects '.format(snr_thresh_perslit) + 'requested nperslit={:d} was exceeded'.format(nperslit)) peaks_gpm = np.logical_not(near_edge_bpm) & np.logical_not(nperslit_bpm) spat_vector = slit_left[specmid] + xsize[specmid] * np.arange(nsamp) / nsamp spat_peaks = slit_left[specmid] + xsize[specmid] * x_peaks_all/ nsamp # TODO: Change this to show_image or something if show_peaks: # Show rectified image here? Add this to QA viewer, ch = display.show_image(image_rect*gpm_rect*np.sqrt(ivar_rect), chname='objs_in_slit_show', cuts=(-5.0,5.0)) # QA objfind_QA(spat_peaks, snr_peaks_all, spat_vector, snr_smash_smth, snr_thresh, qa_title, peaks_gpm, near_edge_bpm, nperslit_bpm, objfindQA_filename=objfindQA_filename, show=show_peaks) #show_peaks) nobj_reg = np.sum(peaks_gpm) # Instantiate a null specobj sobjs = specobjs.SpecObjs() # Trim to the good peaks x_peaks = x_peaks_all[peaks_gpm] snr_peaks = snr_peaks_all[peaks_gpm] flux_peaks = flux_peaks_all[peaks_gpm] # Now create SpecObj objects for all of these and assign preliminary traces to them. for iobj in range(nobj_reg): thisobj = specobj.SpecObj(**specobj_dict) thisobj.SPAT_FRACPOS = x_peaks[iobj]/nsamp thisobj.smash_peakflux = flux_peaks[iobj] thisobj.smash_snr = snr_peaks[iobj] sobjs.add_sobj(thisobj) for iobj in range(nobj_reg): # Was a standard trace provided? If so, use that as a crutch. if std_trace is not None: # Print a status message for the first object if iobj == 0: msgs.info('Using input STANDARD star trace as crutch for object tracing') x_trace = np.interp(specmid, spec_vec, std_trace) shift = np.interp(specmid, spec_vec, slit_left + xsize * sobjs[iobj].SPAT_FRACPOS) - x_trace sobjs[iobj].TRACE_SPAT = std_trace + shift # If no standard trace is provided shift left slit boundary over to be initial trace else: # ToDO make this the average left and right boundary instead. That would be more robust. sobjs[iobj].TRACE_SPAT = slit_left + xsize*sobjs[iobj].SPAT_FRACPOS sobjs[iobj].trace_spec = spec_vec sobjs[iobj].SPAT_PIXPOS = sobjs[iobj].TRACE_SPAT[specmid] # Set the idx for any prelminary outputs we print out. These will be updated shortly sobjs[iobj].set_name() # assign FWHM to all these objects if use_user_fwhm: sobjs[iobj].FWHM = fwhm else: sobjs[iobj].FWHM = get_fwhm(fwhm, nsamp, sobjs[iobj].smash_peakflux, sobjs[iobj].SPAT_FRACPOS, flux_smash_smth) # assign BOX_RADIUS sobjs[iobj].BOX_RADIUS = boxcar_rad if len(sobjs) == 0 and hand_extract_dict is None: # TODO: Why is this not done way above? # It appears possible to have an initial object detection, but then # have it go away.. msgs.info('No objects found automatically. Consider manual extraction.') return specobjs.SpecObjs() msgs.info("Automatic finding routine found {0:d} objects".format(len(sobjs))) # Fit the object traces if len(sobjs) > 0: msgs.info('Fitting the object traces') # Note the transpose is here to pass in the TRACE_SPAT correctly. xinit_fweight = np.copy(sobjs.TRACE_SPAT.T) spec_mask = (spec_vec >= spec_min_max_out[0]) & (spec_vec <= spec_min_max_out[1]) trc_inmask = np.outer(spec_mask, np.ones(len(sobjs), dtype=bool)) xfit_fweight = fit_trace(image, xinit_fweight, ncoeff, bpm=np.invert(inmask), maxshift=1., trace_bpm=np.invert(trc_inmask), fwhm=fwhm, maxdev=maxdev, idx=sobjs.NAME, debug=show_fits)[0] xinit_gweight = np.copy(xfit_fweight) xfit_gweight = fit_trace(image, xinit_gweight, ncoeff, bpm=np.invert(inmask), maxshift=1., trace_bpm=np.invert(trc_inmask), fwhm=fwhm, maxdev=maxdev, weighting='gaussian', idx=sobjs.NAME, debug=show_fits)[0] # assign the final trace for iobj in range(nobj_reg): sobjs[iobj].TRACE_SPAT = xfit_gweight[:, iobj] sobjs[iobj].SPAT_PIXPOS = sobjs[iobj].TRACE_SPAT[specmid] sobjs[iobj].set_name() # Now deal with the hand apertures if a hand_extract_dict was passed in. Add these to the SpecObj objects if hand_extract_dict is not None: # First Parse the hand_dict hand_extract_spec, hand_extract_spat, hand_extract_det, hand_extract_fwhm, \ hand_extract_boxcar = [hand_extract_dict[key] for key in [ 'spec', 'spat', 'detname', 'fwhm', 'boxcar_rad']] # Determine if these hand apertures land on the slit in question hand_on_slit = np.where(np.array(thismask[np.rint(hand_extract_spec).astype(int), np.rint(hand_extract_spat).astype(int)])) hand_extract_spec = hand_extract_spec[hand_on_slit] hand_extract_spat = hand_extract_spat[hand_on_slit] hand_extract_det = hand_extract_det[hand_on_slit] hand_extract_fwhm = hand_extract_fwhm[hand_on_slit] hand_extract_boxcar = hand_extract_boxcar[hand_on_slit] nobj_hand = len(hand_extract_spec) msgs.info("Implementing hand apertures for {} sources on the slit".format(nobj_hand)) # Decide how to assign a trace to the hand objects if nobj_reg > 0: # Use brightest object on slit? smash_peakflux = sobjs.smash_peakflux ibri = smash_peakflux.argmax() trace_model = sobjs[ibri].TRACE_SPAT med_fwhm_reg = np.median(sobjs.FWHM) elif std_trace is not None: # If no objects found, use the standard? trace_model = std_trace else: # If no objects or standard use the slit boundary msgs.warn("No source to use as a trace. Using the slit boundary") trace_model = slit_left # Loop over hand_extract apertures and create and assign specobj for iobj in range(nobj_hand): # Proceed thisobj = specobj.SpecObj(**specobj_dict) thisobj.hand_extract_spec = hand_extract_spec[iobj] thisobj.hand_extract_spat = hand_extract_spat[iobj] thisobj.hand_extract_det = hand_extract_det[iobj] thisobj.hand_extract_fwhm = hand_extract_fwhm[iobj] thisobj.hand_extract_flag = True # SPAT_FRACPOS f_ximg = scipy.interpolate.RectBivariateSpline(spec_vec, spat_vec, ximg) thisobj.SPAT_FRACPOS = float(f_ximg(thisobj.hand_extract_spec, thisobj.hand_extract_spat, grid=False)) # interpolate from ximg thisobj.smash_peakflux = np.interp(thisobj.SPAT_FRACPOS*nsamp,np.arange(nsamp), flux_smash_smth) # interpolate from fluxconv thisobj.smash_snr = np.interp(thisobj.SPAT_FRACPOS*nsamp,np.arange(nsamp), snr_smash_smth) # interpolate from fluxconv # assign the trace spat_0 = np.interp(thisobj.hand_extract_spec, spec_vec, trace_model) shift = thisobj.hand_extract_spat - spat_0 thisobj.TRACE_SPAT = trace_model + shift thisobj.trace_spec = spec_vec thisobj.SPAT_PIXPOS = thisobj.TRACE_SPAT[specmid] thisobj.set_name() # assign FWHM # TODO -- I think FWHM *has* to be input if hand_extract_fwhm[iobj] is not None: # If a hand_extract_fwhm was input use that for the fwhm thisobj.FWHM = hand_extract_fwhm[iobj] elif nobj_reg > 0: # Otherwise is None was input, then use the median of objects on this slit if they are present thisobj.FWHM = med_fwhm_reg else: # Otherwise just use the FWHM parameter input to the code (or the default value) thisobj.FWHM = fwhm # assign BOX_RADIUS (pixels!) if hand_extract_boxcar[iobj] > 0.: thisobj.BOX_RADIUS = hand_extract_boxcar[iobj] else: thisobj.BOX_RADIUS = boxcar_rad # Finish sobjs.add_sobj(thisobj) nobj = len(sobjs) ## Okay now loop over all the regular aps and exclude any which within the fwhm of the hand_extract_APERTURES if nobj_reg > 0 and hand_extract_dict is not None: spat_pixpos = sobjs.SPAT_PIXPOS hand_flag = sobjs.hand_extract_flag spec_fwhm = sobjs.FWHM #spat_pixpos = np.array([spec.SPAT_PIXPOS for spec in specobjs]) #hand_flag = np.array([spec.hand_extract_flag for spec in specobjs]) #spec_fwhm = np.array([spec.FWHM for spec in specobjs]) reg_ind, = np.where(np.invert(hand_flag)) hand_ind, = np.where(hand_flag) #med_fwhm = np.median(spec_fwhm[~hand_flag]) #spat_pixpos_hand = spat_pixpos[hand_ind] keep = np.ones(nobj, dtype=bool) for ihand in hand_ind: close = np.abs(sobjs[reg_ind].SPAT_PIXPOS - spat_pixpos[ihand]) <= 0.6*spec_fwhm[ihand] if np.any(close): # Print out a warning msgs.warn('Deleting object(s) {}'.format(sobjs[reg_ind[close]].NAME) + ' because it collides with a user specified hand_extract aperture') keep[reg_ind[close]] = False sobjs = sobjs[keep] if len(sobjs) == 0: msgs.info('No hand or normal objects found on this slit. Returning') return specobjs.SpecObjs() # Sort objects according to their spatial location nobj = len(sobjs) spat_pixpos = sobjs.SPAT_PIXPOS sobjs = sobjs[spat_pixpos.argsort()] # Assign integer objids sobjs.OBJID = np.arange(nobj) + 1 # Assign the maskwidth for iobj in range(nobj): sobjs[iobj].maskwidth = extract_maskwidth*sobjs[iobj].FWHM*(1.0 + 0.5*np.log10(np.fmax(sobjs[iobj].smash_snr,1.0))) # If requested display the resulting traces on top of the image if show_trace: viewer, ch = display.show_image(image*(thismask*inmask)) display.show_slits(viewer, ch, slit_left.T, slit_righ.T, slit_ids = sobjs[0].SLITID) for iobj in range(nobj): if sobjs[iobj].hand_extract_flag == False: color = 'orange' else: color = 'blue' display.show_trace(viewer, ch,sobjs[iobj].TRACE_SPAT, trc_name = sobjs[iobj].NAME, color=color) msgs.info("Successfully traced a total of {0:d} objects".format(len(sobjs))) # Finish for sobj in sobjs: # Vet if not sobj.ready_for_extraction(): # embed(header=utils.embed_header()) msgs.error("Bad SpecObj. Can't proceed") # Return return sobjs