27 import lsst.pex.config 
as pexConfig
 
   31 __all__ = [
"GetCoaddAsTemplateTask", 
"GetCoaddAsTemplateConfig",
 
   32            "GetCalexpAsTemplateTask", 
"GetCalexpAsTemplateConfig"]
 
   36     templateBorderSize = pexConfig.Field(
 
   39         doc=
"Number of pixels to grow the requested template image to account for warping" 
   41     coaddName = pexConfig.Field(
 
   42         doc=
"coadd name: typically one of 'deep', 'goodSeeing', or 'dcr'",
 
   46     numSubfilters = pexConfig.Field(
 
   47         doc=
"Number of subfilters in the DcrCoadd, used only if ``coaddName``='dcr'",
 
   51     warpType = pexConfig.Field(
 
   52         doc=
"Warp type of the coadd template: one of 'direct' or 'psfMatched'",
 
   59     """Subtask to retrieve coadd for use as an image difference template. 
   61     This is the default getTemplate Task to be run as a subtask by 
   62     ``pipe.tasks.ImageDifferenceTask``. The main methods are ``run()`` and 
   67     From the given skymap, the closest tract is selected;  multiple tracts  are 
   68     not supported. The assembled template inherits the WCS of the selected 
   69     skymap tract and the resolution of the template exposures. Overlapping box 
   70     regions of the input template patches are pixel by pixel copied into the 
   71     assembled template image. There is no warping or pixel resampling. 
   73     Pixels with no overlap of any available input patches are set to ``nan`` value 
   74     and ``NO_DATA`` flagged. 
   77     ConfigClass = GetCoaddAsTemplateConfig
 
   78     _DefaultName = 
"GetCoaddAsTemplateTask" 
   80     def runDataRef(self, exposure, sensorRef, templateIdList=None):
 
   81         """Gen2 task entry point. Retrieve and mosaic a template coadd exposure 
   82         that overlaps the science exposure. 
   86         exposure: `lsst.afw.image.Exposure` 
   87             an exposure for which to generate an overlapping template 
   89             a Butler data reference that can be used to obtain coadd data 
   90         templateIdList : TYPE, optional 
   91             list of data ids, unused here, in the case of coadd template 
   95         result : `lsst.pipe.base.Struct` 
   96             - ``exposure`` : `lsst.afw.image.ExposureF` 
   97                 a template coadd exposure assembled out of patches 
   98             - ``sources`` :  None for this subtask 
  100         skyMap = sensorRef.get(datasetType=self.config.coaddName + 
"Coadd_skyMap")
 
  103         availableCoaddRefs = dict()
 
  104         for patchInfo 
in patchList:
 
  105             patchNumber = tractInfo.getSequentialPatchIndex(patchInfo)
 
  108                 bbox=patchInfo.getOuterBBox(),
 
  109                 tract=tractInfo.getId(),
 
  110                 patch=
"%s,%s" % (patchInfo.getIndex()[0], patchInfo.getIndex()[1]),
 
  111                 numSubfilters=self.config.numSubfilters,
 
  114             if sensorRef.datasetExists(**patchArgDict):
 
  115                 self.log.
info(
"Reading patch %s" % patchArgDict)
 
  116                 availableCoaddRefs[patchNumber] = patchArgDict
 
  118         templateExposure = self.
run(
 
  119             tractInfo, patchList, skyCorners, availableCoaddRefs,
 
  120             sensorRef=sensorRef, visitInfo=exposure.getInfo().getVisitInfo()
 
  122         return pipeBase.Struct(exposure=templateExposure, sources=
None)
 
  124     def runQuantum(self, exposure, butlerQC, skyMapRef, coaddExposureRefs):
 
  125         """Gen3 task entry point. Retrieve and mosaic a template coadd exposure 
  126         that overlaps the science exposure. 
  130         exposure : `lsst.afw.image.Exposure` 
  131             The science exposure to define the sky region of the template coadd. 
  132         butlerQC : `lsst.pipe.base.ButlerQuantumContext` 
  133             Butler like object that supports getting data by DatasetRef. 
  134         skyMapRef : `lsst.daf.butler.DatasetRef` 
  135             Reference to SkyMap object that corresponds to the template coadd. 
  136         coaddExposureRefs : iterable of `lsst.daf.butler.DeferredDatasetRef` 
  137             Iterable of references to the available template coadd patches. 
  141         result : `lsst.pipe.base.Struct` 
  142             - ``exposure`` : `lsst.afw.image.ExposureF` 
  143                 a template coadd exposure assembled out of patches 
  144             - ``sources`` :  `None` for this subtask 
  146         skyMap = butlerQC.get(skyMapRef)
 
  148         patchNumFilter = frozenset(tractInfo.getSequentialPatchIndex(p) 
for p 
in patchList)
 
  150         availableCoaddRefs = dict()
 
  151         for coaddRef 
in coaddExposureRefs:
 
  152             dataId = coaddRef.datasetRef.dataId
 
  153             if dataId[
'tract'] == tractInfo.getId() 
and dataId[
'patch'] 
in patchNumFilter:
 
  154                 if self.config.coaddName == 
'dcr':
 
  155                     self.log.
info(
"Using template input tract=%s, patch=%s, subfilter=%s" %
 
  156                                   (tractInfo.getId(), dataId[
'patch'], dataId[
'subfilter']))
 
  157                     if dataId[
'patch'] 
in availableCoaddRefs:
 
  158                         availableCoaddRefs[dataId[
'patch']].
append(butlerQC.get(coaddRef))
 
  160                         availableCoaddRefs[dataId[
'patch']] = [butlerQC.get(coaddRef), ]
 
  162                     self.log.
info(
"Using template input tract=%s, patch=%s" %
 
  163                                   (tractInfo.getId(), dataId[
'patch']))
 
  164                     availableCoaddRefs[dataId[
'patch']] = butlerQC.get(coaddRef)
 
  166         templateExposure = self.
run(tractInfo, patchList, skyCorners, availableCoaddRefs,
 
  167                                     visitInfo=exposure.getInfo().getVisitInfo())
 
  168         return pipeBase.Struct(exposure=templateExposure, sources=
None)
 
  171         """Select the relevant tract and its patches that overlap with the science exposure. 
  175         exposure : `lsst.afw.image.Exposure` 
  176             The science exposure to define the sky region of the template coadd. 
  178         skyMap : `lsst.skymap.BaseSkyMap` 
  179             SkyMap object that corresponds to the template coadd. 
  184          - ``tractInfo`` : `lsst.skymap.TractInfo` 
  186          - ``patchList`` : `list` of `lsst.skymap.PatchInfo` 
  187              List of all overlap patches of the selected tract. 
  188          - ``skyCorners`` : `list` of `lsst.geom.SpherePoint` 
  189              Corners of the exposure in the sky in the order given by `lsst.geom.Box2D.getCorners`. 
  191         expWcs = exposure.getWcs()
 
  193         expBoxD.grow(self.config.templateBorderSize)
 
  194         ctrSkyPos = expWcs.pixelToSky(expBoxD.getCenter())
 
  195         tractInfo = skyMap.findTract(ctrSkyPos)
 
  196         self.log.
info(
"Using skyMap tract %s" % (tractInfo.getId(),))
 
  197         skyCorners = [expWcs.pixelToSky(pixPos) 
for pixPos 
in expBoxD.getCorners()]
 
  198         patchList = tractInfo.findPatchList(skyCorners)
 
  201             raise RuntimeError(
"No suitable tract found")
 
  203         self.log.
info(
"Assembling %s coadd patches" % (len(patchList),))
 
  204         self.log.
info(
"exposure dimensions=%s" % exposure.getDimensions())
 
  206         return (tractInfo, patchList, skyCorners)
 
  208     def run(self, tractInfo, patchList, skyCorners, availableCoaddRefs,
 
  209             sensorRef=None, visitInfo=None):
 
  210         """Gen2 and gen3 shared code: determination of exposure dimensions and 
  211         copying of pixels from overlapping patch regions. 
  215         skyMap : `lsst.skymap.BaseSkyMap` 
  216             SkyMap object that corresponds to the template coadd. 
  217         tractInfo : `lsst.skymap.TractInfo` 
  219         patchList : iterable of `lsst.skymap.patchInfo.PatchInfo` 
  220             Patches to consider for making the template exposure. 
  221         skyCorners : list of `lsst.geom.SpherePoint` 
  222             Sky corner coordinates to be covered by the template exposure. 
  223         availableCoaddRefs : `dict` of `int` : `lsst.daf.butler.DeferredDatasetHandle` (Gen3) 
  225             Dictionary of spatially relevant retrieved coadd patches, 
  226             indexed by their sequential patch number. In Gen3 mode, .get() is called, 
  227             in Gen2 mode, sensorRef.get(**coaddef) is called to retrieve the coadd. 
  228         sensorRef : `lsst.daf.persistence.ButlerDataRef`, Gen2 only 
  229             TODO DM-22952 Butler data reference to get coadd data. 
  230             Must be `None` for Gen3. 
  231         visitInfo : `lsst.afw.image.VisitInfo`, Gen2 only 
  232             TODO DM-22952 VisitInfo to make dcr model. 
  236         templateExposure: `lsst.afw.image.ExposureF` 
  237             The created template exposure. 
  239         coaddWcs = tractInfo.getWcs()
 
  243         for skyPos 
in skyCorners:
 
  244             coaddBBox.include(coaddWcs.skyToPixel(skyPos))
 
  246         self.log.
info(
"coadd dimensions=%s" % coaddBBox.getDimensions())
 
  248         coaddExposure = afwImage.ExposureF(coaddBBox, coaddWcs)
 
  249         coaddExposure.maskedImage.set(np.nan, afwImage.Mask.getPlaneBitMask(
"NO_DATA"), np.nan)
 
  253         coaddPhotoCalib = 
None 
  254         for patchInfo 
in patchList:
 
  255             patchNumber = tractInfo.getSequentialPatchIndex(patchInfo)
 
  256             patchSubBBox = patchInfo.getOuterBBox()
 
  257             patchSubBBox.clip(coaddBBox)
 
  261                 tract=tractInfo.getId(),
 
  262                 patch=
"%s,%s" % (patchInfo.getIndex()[0], patchInfo.getIndex()[1]),
 
  263                 numSubfilters=self.config.numSubfilters,
 
  265             if patchSubBBox.isEmpty():
 
  266                 self.log.
info(f
"skip tract={patchArgDict['tract']}, " 
  267                               f
"patch={patchNumber}; no overlapping pixels")
 
  269             if patchNumber 
not in availableCoaddRefs:
 
  270                 self.log.
warn(f
"{patchArgDict['datasetType']}, " 
  271                               f
"tract={patchArgDict['tract']}, patch={patchNumber} does not exist")
 
  274             if self.config.coaddName == 
'dcr':
 
  275                 if sensorRef 
and not sensorRef.datasetExists(subfilter=0, **patchArgDict):
 
  276                     self.log.
warn(
"%(datasetType)s, tract=%(tract)s, patch=%(patch)s," 
  277                                   " numSubfilters=%(numSubfilters)s, subfilter=0 does not exist" 
  280                 patchInnerBBox = patchInfo.getInnerBBox()
 
  281                 patchInnerBBox.clip(coaddBBox)
 
  282                 if np.min(patchInnerBBox.getDimensions()) <= 2*self.config.templateBorderSize:
 
  283                     self.log.
info(
"skip tract=%(tract)s, patch=%(patch)s; too few pixels." % patchArgDict)
 
  285                 self.log.
info(
"Constructing DCR-matched template for patch %s" % patchArgDict)
 
  288                     dcrModel = DcrModel.fromDataRef(sensorRef, **patchArgDict)
 
  290                     dcrModel = DcrModel.fromQuantum(availableCoaddRefs[patchNumber])
 
  298                 dcrBBox.grow(-self.config.templateBorderSize)
 
  299                 dcrBBox.include(patchInnerBBox)
 
  300                 coaddPatch = dcrModel.buildMatchedExposure(bbox=dcrBBox,
 
  304                 if sensorRef 
is None:
 
  306                     coaddPatch = availableCoaddRefs[patchNumber].get()
 
  309                     coaddPatch = sensorRef.get(**availableCoaddRefs[patchNumber])
 
  314             overlapBox = coaddPatch.getBBox()
 
  315             overlapBox.clip(coaddBBox)
 
  316             coaddExposure.maskedImage.assign(coaddPatch.maskedImage[overlapBox], overlapBox)
 
  318             if coaddFilter 
is None:
 
  319                 coaddFilter = coaddPatch.getFilter()
 
  322             if coaddPsf 
is None and coaddPatch.hasPsf():
 
  323                 coaddPsf = coaddPatch.getPsf()
 
  326             if coaddPhotoCalib 
is None:
 
  327                 coaddPhotoCalib = coaddPatch.getPhotoCalib()
 
  329         if coaddPhotoCalib 
is None:
 
  330             raise RuntimeError(
"No coadd PhotoCalib found!")
 
  331         if nPatchesFound == 0:
 
  332             raise RuntimeError(
"No patches found!")
 
  334             raise RuntimeError(
"No coadd Psf found!")
 
  336         coaddExposure.setPhotoCalib(coaddPhotoCalib)
 
  337         coaddExposure.setPsf(coaddPsf)
 
  338         coaddExposure.setFilter(coaddFilter)
 
  342         """Return coadd name for given task config 
  346         CoaddDatasetName : `string` 
  348         TODO: This nearly duplicates a method in CoaddBaseTask (DM-11985) 
  350         warpType = self.config.warpType
 
  351         suffix = 
"" if warpType == 
"direct" else warpType[0].upper() + warpType[1:]
 
  352         return self.config.coaddName + 
"Coadd" + suffix
 
  356     doAddCalexpBackground = pexConfig.Field(
 
  359         doc=
"Add background to calexp before processing it." 
  364     """Subtask to retrieve calexp of the same ccd number as the science image SensorRef 
  365     for use as an image difference template. Only gen2 supported. 
  367     To be run as a subtask by pipe.tasks.ImageDifferenceTask. 
  368     Intended for use with simulations and surveys that repeatedly visit the same pointing. 
  369     This code was originally part of Winter2013ImageDifferenceTask. 
  372     ConfigClass = GetCalexpAsTemplateConfig
 
  373     _DefaultName = 
"GetCalexpAsTemplateTask" 
  375     def run(self, exposure, sensorRef, templateIdList):
 
  376         """Return a calexp exposure with based on input sensorRef. 
  378         Construct a dataId based on the sensorRef.dataId combined 
  379         with the specifications from the first dataId in templateIdList 
  383         exposure :  `lsst.afw.image.Exposure` 
  385         sensorRef : `list` of `lsst.daf.persistence.ButlerDataRef` 
  386             Data reference of the calexp(s) to subtract from. 
  387         templateIdList : `list` of `lsst.daf.persistence.ButlerDataRef` 
  388             Data reference of the template calexp to be subtraced. 
  389             Can be incomplete, fields are initialized from `sensorRef`. 
  390             If there are multiple items, only the first one is used. 
  396             return a pipeBase.Struct: 
  398                 - ``exposure`` : a template calexp 
  399                 - ``sources`` : source catalog measured on the template 
  402         if len(templateIdList) == 0:
 
  403             raise RuntimeError(
"No template data reference supplied.")
 
  404         if len(templateIdList) > 1:
 
  405             self.log.
warn(
"Multiple template data references supplied. Using the first one only.")
 
  407         templateId = sensorRef.dataId.copy()
 
  408         templateId.update(templateIdList[0])
 
  410         self.log.
info(
"Fetching calexp (%s) as template." % (templateId))
 
  412         butler = sensorRef.getButler()
 
  413         template = butler.get(datasetType=
"calexp", dataId=templateId)
 
  414         if self.config.doAddCalexpBackground:
 
  415             templateBg = butler.get(datasetType=
"calexpBackground", dataId=templateId)
 
  416             mi = template.getMaskedImage()
 
  417             mi += templateBg.getImage()
 
  419         if not template.hasPsf():
 
  420             raise pipeBase.TaskError(
"Template has no psf")
 
  422         templateSources = butler.get(datasetType=
"src", dataId=templateId)
 
  423         return pipeBase.Struct(exposure=template,
 
  424                                sources=templateSources)
 
  427         return self.
run(*args, **kwargs)
 
  430         raise NotImplementedError(
"Calexp template is not supported with gen3 middleware")