24 import lsst.pex.config 
as pexConfig
 
   29 __all__ = [
"BaseSelectImagesTask", 
"BaseExposureInfo", 
"WcsSelectImagesTask", 
"PsfWcsSelectImagesTask",
 
   30            "DatabaseSelectImagesConfig", 
"BestSeeingWcsSelectImagesTask"]
 
   34     """Base configuration for subclasses of BaseSelectImagesTask that use a database""" 
   35     host = pexConfig.Field(
 
   36         doc=
"Database server host name",
 
   39     port = pexConfig.Field(
 
   40         doc=
"Database server port",
 
   43     database = pexConfig.Field(
 
   44         doc=
"Name of database",
 
   47     maxExposures = pexConfig.Field(
 
   48         doc=
"maximum exposures to select; intended for debugging; ignored if None",
 
   55     """Data about a selected exposure 
   59         """Create exposure information that can be used to generate data references 
   61         The object has the following fields: 
   62         - dataId: data ID of exposure (a dict) 
   63         - coordList: ICRS coordinates of the corners of the exposure (list of lsst.geom.SpherePoint) 
   64         plus any others items that are desired 
   66         super(BaseExposureInfo, self).
__init__(dataId=dataId, coordList=coordList)
 
   70     """Base task for selecting images suitable for coaddition 
   72     ConfigClass = pexConfig.Config
 
   73     _DefaultName = 
"selectImages" 
   76     def run(self, coordList):
 
   77         """Select images suitable for coaddition in a particular region 
   79         @param[in] coordList: list of coordinates defining region of interest; if None then select all images 
   80         subclasses may add additional keyword arguments, as required 
   82         @return a pipeBase Struct containing: 
   83         - exposureInfoList: a list of exposure information objects (subclasses of BaseExposureInfo), 
   84             which have at least the following fields: 
   85             - dataId: data ID dictionary 
   86             - coordList: ICRS coordinates of the corners of the exposure (list of lsst.geom.SpherePoint) 
   88         raise NotImplementedError()
 
   90     def _runArgDictFromDataId(self, dataId):
 
   91         """Extract keyword arguments for run (other than coordList) from a data ID 
   93         @return keyword arguments for run (other than coordList), as a dict 
   95         raise NotImplementedError()
 
   97     def runDataRef(self, dataRef, coordList, makeDataRefList=True, selectDataList=[]):
 
   98         """Run based on a data reference 
  100         This delegates to run() and _runArgDictFromDataId() to do the actual 
  101         selection. In the event that the selectDataList is non-empty, this will 
  102         be used to further restrict the selection, providing the user with 
  103         additional control over the selection. 
  105         @param[in] dataRef: data reference; must contain any extra keys needed by the subclass 
  106         @param[in] coordList: list of coordinates defining region of interest; if None, search the whole sky 
  107         @param[in] makeDataRefList: if True, return dataRefList 
  108         @param[in] selectDataList: List of SelectStruct with dataRefs to consider for selection 
  109         @return a pipeBase Struct containing: 
  110         - exposureInfoList: a list of objects derived from ExposureInfo 
  111         - dataRefList: a list of data references (None if makeDataRefList False) 
  114         exposureInfoList = self.
run(coordList, **runArgDict).exposureInfoList
 
  116         if len(selectDataList) > 0 
and len(exposureInfoList) > 0:
 
  118             ccdKeys, ccdValues = _extractKeyValue(exposureInfoList)
 
  119             inKeys, inValues = _extractKeyValue([s.dataRef 
for s 
in selectDataList], keys=ccdKeys)
 
  120             inValues = 
set(inValues)
 
  121             newExposureInfoList = []
 
  122             for info, ccdVal 
in zip(exposureInfoList, ccdValues):
 
  123                 if ccdVal 
in inValues:
 
  124                     newExposureInfoList.append(info)
 
  126                     self.log.
info(
"De-selecting exposure %s: not in selectDataList" % info.dataId)
 
  127             exposureInfoList = newExposureInfoList
 
  130             butler = dataRef.butlerSubset.butler
 
  131             dataRefList = [butler.dataRef(datasetType=
"calexp",
 
  132                                           dataId=expInfo.dataId,
 
  133                                           ) 
for expInfo 
in exposureInfoList]
 
  137         return pipeBase.Struct(
 
  138             dataRefList=dataRefList,
 
  139             exposureInfoList=exposureInfoList,
 
  143 def _extractKeyValue(dataList, keys=None):
 
  144     """Extract the keys and values from a list of dataIds 
  146     The input dataList is a list of objects that have 'dataId' members. 
  147     This allows it to be used for both a list of data references and a 
  150     assert len(dataList) > 0
 
  152         keys = sorted(dataList[0].dataId.keys())
 
  155     for data 
in dataList:
 
  156         thisKeys = 
set(data.dataId.keys())
 
  157         if thisKeys != keySet:
 
  158             raise RuntimeError(
"DataId keys inconsistent: %s vs %s" % (keySet, thisKeys))
 
  159         values.append(tuple(data.dataId[k] 
for k 
in keys))
 
  164     """A container for data to be passed to the WcsSelectImagesTask""" 
  167         super(SelectStruct, self).
__init__(dataRef=dataRef, wcs=wcs, bbox=bbox)
 
  171     """Select images using their Wcs""" 
  173     def runDataRef(self, dataRef, coordList, makeDataRefList=True, selectDataList=[]):
 
  174         """Select images in the selectDataList that overlap the patch 
  176         We use the "convexHull" method of lsst.sphgeom.ConvexPolygon to define 
  177         polygons on the celestial sphere, and test the polygon of the 
  178         patch for overlap with the polygon of the image. 
  180         We use "convexHull" instead of generating a ConvexPolygon 
  181         directly because the standard for the inputs to ConvexPolygon 
  182         are pretty high and we don't want to be responsible for reaching them. 
  184         @param dataRef: Data reference for coadd/tempExp (with tract, patch) 
  185         @param coordList: List of ICRS coordinates (lsst.geom.SpherePoint) specifying boundary of patch 
  186         @param makeDataRefList: Construct a list of data references? 
  187         @param selectDataList: List of SelectStruct, to consider for selection 
  190         exposureInfoList = []
 
  192         patchVertices = [coord.getVector() 
for coord 
in coordList]
 
  195         for data 
in selectDataList:
 
  196             dataRef = data.dataRef
 
  201                 imageCorners = [imageWcs.pixelToSky(pix) 
for pix 
in geom.Box2D(imageBox).getCorners()]
 
  204                 self.log.
debug(
"WCS error in testing calexp %s (%s): deselecting", dataRef.dataId, e)
 
  208             if imagePoly 
is None:
 
  209                 self.log.
debug(
"Unable to create polygon from image %s: deselecting", dataRef.dataId)
 
  211             if patchPoly.intersects(imagePoly):  
 
  212                 self.log.
info(
"Selecting calexp %s" % dataRef.dataId)
 
  213                 dataRefList.append(dataRef)
 
  216         return pipeBase.Struct(
 
  217             dataRefList=dataRefList 
if makeDataRefList 
else None,
 
  218             exposureInfoList=exposureInfoList,
 
  223     maxEllipResidual = pexConfig.Field(
 
  224         doc=
"Maximum median ellipticity residual",
 
  229     maxSizeScatter = pexConfig.Field(
 
  230         doc=
"Maximum scatter in the size residuals",
 
  234     maxScaledSizeScatter = pexConfig.Field(
 
  235         doc=
"Maximum scatter in the size residuals, scaled by the median size",
 
  240     starSelection = pexConfig.Field(
 
  241         doc=
"select star with this field",
 
  243         default=
'calib_psf_used' 
  245     starShape = pexConfig.Field(
 
  246         doc=
"name of star shape",
 
  248         default=
'base_SdssShape' 
  250     psfShape = pexConfig.Field(
 
  251         doc=
"name of psf shape",
 
  253         default=
'base_SdssShape_psf' 
  258     "Return median absolute deviation scaled to normally distributed data" 
  259     return 1.4826*np.median(np.abs(array - np.median(array)))
 
  263     """Select images using their Wcs and cuts on the PSF properties""" 
  265     ConfigClass = PsfWcsSelectImagesConfig
 
  266     _DefaultName = 
"PsfWcsSelectImages" 
  268     def runDataRef(self, dataRef, coordList, makeDataRefList=True, selectDataList=[]):
 
  269         """Select images in the selectDataList that overlap the patch and satisfy PSF quality critera. 
  271         The PSF quality criteria are based on the size and ellipticity residuals from the 
  272         adaptive second moments of the star and the PSF. 
  275           - the median of the ellipticty residuals 
  276           - the robust scatter of the size residuals (using the median absolute deviation) 
  277           - the robust scatter of the size residuals scaled by the square of 
  280         @param dataRef: Data reference for coadd/tempExp (with tract, patch) 
  281         @param coordList: List of ICRS coordinates (lsst.geom.SpherePoint) specifying boundary of patch 
  282         @param makeDataRefList: Construct a list of data references? 
  283         @param selectDataList: List of SelectStruct, to consider for selection 
  285         result = super(PsfWcsSelectImagesTask, self).
runDataRef(dataRef, coordList, makeDataRefList,
 
  289         exposureInfoList = []
 
  290         for dataRef, exposureInfo 
in zip(result.dataRefList, result.exposureInfoList):
 
  291             butler = dataRef.butlerSubset.butler
 
  292             srcCatalog = butler.get(
'src', dataRef.dataId)
 
  293             mask = srcCatalog[self.config.starSelection]
 
  295             starXX = srcCatalog[self.config.starShape+
'_xx'][mask]
 
  296             starYY = srcCatalog[self.config.starShape+
'_yy'][mask]
 
  297             starXY = srcCatalog[self.config.starShape+
'_xy'][mask]
 
  298             psfXX = srcCatalog[self.config.psfShape+
'_xx'][mask]
 
  299             psfYY = srcCatalog[self.config.psfShape+
'_yy'][mask]
 
  300             psfXY = srcCatalog[self.config.psfShape+
'_xy'][mask]
 
  302             starSize = np.power(starXX*starYY - starXY**2, 0.25)
 
  303             starE1 = (starXX - starYY)/(starXX + starYY)
 
  304             starE2 = 2*starXY/(starXX + starYY)
 
  305             medianSize = np.median(starSize)
 
  307             psfSize = np.power(psfXX*psfYY - psfXY**2, 0.25)
 
  308             psfE1 = (psfXX - psfYY)/(psfXX + psfYY)
 
  309             psfE2 = 2*psfXY/(psfXX + psfYY)
 
  311             medianE1 = np.abs(np.median(starE1 - psfE1))
 
  312             medianE2 = np.abs(np.median(starE2 - psfE2))
 
  313             medianE = np.sqrt(medianE1**2 + medianE2**2)
 
  315             scatterSize = 
sigmaMad(starSize - psfSize)
 
  316             scaledScatterSize = scatterSize/medianSize**2
 
  319             if self.config.maxEllipResidual 
and medianE > self.config.maxEllipResidual:
 
  320                 self.log.
info(
"Removing visit %s because median e residual too large: %f vs %f" %
 
  321                               (dataRef.dataId, medianE, self.config.maxEllipResidual))
 
  323             elif self.config.maxSizeScatter 
and scatterSize > self.config.maxSizeScatter:
 
  324                 self.log.
info(
"Removing visit %s because size scatter is too large: %f vs %f" %
 
  325                               (dataRef.dataId, scatterSize, self.config.maxSizeScatter))
 
  327             elif self.config.maxScaledSizeScatter 
and scaledScatterSize > self.config.maxScaledSizeScatter:
 
  328                 self.log.
info(
"Removing visit %s because scaled size scatter is too large: %f vs %f" %
 
  329                               (dataRef.dataId, scaledScatterSize, self.config.maxScaledSizeScatter))
 
  335             dataRefList.append(dataRef)
 
  336             exposureInfoList.append(exposureInfo)
 
  338         return pipeBase.Struct(
 
  339             dataRefList=dataRefList,
 
  340             exposureInfoList=exposureInfoList,
 
  345     """Base configuration for BestSeeingSelectImagesTask. 
  347     nImagesMax = pexConfig.Field(
 
  349         doc=
"Maximum number of images to select",
 
  351     maxPsfFwhm = pexConfig.Field(
 
  353         doc=
"Maximum PSF FWHM (in arcseconds) to select",
 
  356     minPsfFwhm = pexConfig.Field(
 
  358         doc=
"Minimum PSF FWHM (in arcseconds) to select",
 
  364     """Select up to a maximum number of the best-seeing images using their Wcs. 
  366     ConfigClass = BestSeeingWcsSelectImageConfig
 
  368     def runDataRef(self, dataRef, coordList, makeDataRefList=True,
 
  369                    selectDataList=None):
 
  370         """Select the best-seeing images in the selectDataList that overlap the patch. 
  374         dataRef : `lsst.daf.persistence.ButlerDataRef` 
  375             Data reference for coadd/tempExp (with tract, patch) 
  376         coordList : `list` of `lsst.geom.SpherePoint` 
  377             List of ICRS sky coordinates specifying boundary of patch 
  378         makeDataRefList : `boolean`, optional 
  379             Construct a list of data references? 
  380         selectDataList : `list` of `SelectStruct` 
  381             List of SelectStruct, to consider for selection 
  385         result : `lsst.pipe.base.Struct` 
  386             Result struct with components: 
  387             - ``exposureList``: the selected exposures 
  388                 (`list` of `lsst.pipe.tasks.selectImages.BaseExposureInfo`). 
  389             - ``dataRefList``: the optional data references corresponding to 
  390                 each element of ``exposureList`` 
  391                 (`list` of `lsst.daf.persistence.ButlerDataRef`, or `None`). 
  393         if self.config.nImagesMax <= 0:
 
  394             raise RuntimeError(f
"nImagesMax must be greater than zero: {self.config.nImagesMax}")
 
  398         exposureInfoList = []
 
  400         if selectDataList 
is None:
 
  403         result = super().
runDataRef(dataRef, coordList, makeDataRefList=
True, selectDataList=selectDataList)
 
  405         for dataRef, exposureInfo 
in zip(result.dataRefList, result.exposureInfoList):
 
  406             cal = dataRef.get(
"calexp", immediate=
True)
 
  409             pixToArcseconds = cal.getWcs().getPixelScale().asArcseconds()
 
  410             psfSize = cal.getPsf().computeShape().getDeterminantRadius()*pixToArcseconds
 
  411             sizeFwhm = psfSize * np.sqrt(8.*np.log(2.))
 
  412             if self.config.maxPsfFwhm 
and sizeFwhm > self.config.maxPsfFwhm:
 
  414             if self.config.minPsfFwhm 
and sizeFwhm < self.config.minPsfFwhm:
 
  416             psfSizes.append(psfSize)
 
  417             dataRefList.append(dataRef)
 
  418             exposureInfoList.append(exposureInfo)
 
  420         if len(psfSizes) > self.config.nImagesMax:
 
  421             sortedIndices = np.argsort(psfSizes)[:self.config.nImagesMax]
 
  422             filteredDataRefList = [dataRefList[i] 
for i 
in sortedIndices]
 
  423             filteredExposureInfoList = [exposureInfoList[i] 
for i 
in sortedIndices]
 
  424             self.log.
info(f
"{len(sortedIndices)} images selected with FWHM " 
  425                           f
"range of {psfSizes[sortedIndices[0]]}--{psfSizes[sortedIndices[-1]]} arcseconds")
 
  428             if len(psfSizes) == 0:
 
  429                 self.log.
warn(f
"0 images selected.")
 
  431                 self.log.
debug(f
"{len(psfSizes)} images selected with FWHM range " 
  432                                f
"of {psfSizes[0]}--{psfSizes[-1]} arcseconds")
 
  433             filteredDataRefList = dataRefList
 
  434             filteredExposureInfoList = exposureInfoList
 
  436         return pipeBase.Struct(
 
  437             dataRefList=filteredDataRefList 
if makeDataRefList 
else None,
 
  438             exposureInfoList=filteredExposureInfoList,