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")