lsst.ip.diffim g67c5bb01cf+764cf0b5d9
getTemplate.py
Go to the documentation of this file.
2# LSST Data Management System
3# Copyright 2016 LSST Corporation.
4#
5# This product includes software developed by the
6# LSST Project (http://www.lsst.org/).
7#
8# This program is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the LSST License Statement and
19# the GNU General Public License along with this program. If not,
20# see <http://www.lsstcorp.org/LegalNotices/>.
21#
22
23import numpy as np
24
25import lsst.afw.image as afwImage
26import lsst.geom as geom
27import lsst.afw.geom as afwGeom
28import lsst.afw.table as afwTable
29import lsst.afw.math as afwMath
30import lsst.pex.config as pexConfig
31import lsst.pipe.base as pipeBase
32from lsst.skymap import BaseSkyMap
33from lsst.daf.butler import DeferredDatasetHandle
34from lsst.ip.diffim.dcrModel import DcrModel
35from lsst.meas.algorithms import CoaddPsf, CoaddPsfConfig
36
37__all__ = ["GetCoaddAsTemplateTask", "GetCoaddAsTemplateConfig",
38 "GetTemplateTask", "GetTemplateConfig",
39 "GetDcrTemplateTask", "GetDcrTemplateConfig",
40 "GetMultiTractCoaddTemplateTask", "GetMultiTractCoaddTemplateConfig"]
41
42
43class GetCoaddAsTemplateConfig(pexConfig.Config):
44 templateBorderSize = pexConfig.Field(
45 dtype=int,
46 default=20,
47 doc="Number of pixels to grow the requested template image to account for warping"
48 )
49 coaddName = pexConfig.Field(
50 doc="coadd name: typically one of 'deep', 'goodSeeing', or 'dcr'",
51 dtype=str,
52 default="deep",
53 )
54 warpType = pexConfig.Field(
55 doc="Warp type of the coadd template: one of 'direct' or 'psfMatched'",
56 dtype=str,
57 default="direct",
58 )
59
60
61class GetCoaddAsTemplateTask(pipeBase.Task):
62 """Subtask to retrieve coadd for use as an image difference template.
63
64 This is the default getTemplate Task to be run as a subtask by
65 ``pipe.tasks.ImageDifferenceTask``.
66
67 Notes
68 -----
69 From the given skymap, the closest tract is selected; multiple tracts are
70 not supported. The assembled template inherits the WCS of the selected
71 skymap tract and the resolution of the template exposures. Overlapping box
72 regions of the input template patches are pixel by pixel copied into the
73 assembled template image. There is no warping or pixel resampling.
74
75 Pixels with no overlap of any available input patches are set to ``nan`` value
76 and ``NO_DATA`` flagged.
77 """
78
79 ConfigClass = GetCoaddAsTemplateConfig
80 _DefaultName = "GetCoaddAsTemplateTask"
81
82 def runQuantum(self, exposure, butlerQC, skyMapRef, coaddExposureRefs):
83 """Gen3 task entry point. Retrieve and mosaic a template coadd exposure
84 that overlaps the science exposure.
85
86 Parameters
87 ----------
88 exposure : `lsst.afw.image.Exposure`
89 The science exposure to define the sky region of the template coadd.
90 butlerQC : `lsst.pipe.base.ButlerQuantumContext`
91 Butler like object that supports getting data by DatasetRef.
92 skyMapRef : `lsst.daf.butler.DatasetRef`
93 Reference to SkyMap object that corresponds to the template coadd.
94 coaddExposureRefs : iterable of `lsst.daf.butler.DeferredDatasetRef`
95 Iterable of references to the available template coadd patches.
96
97 Returns
98 -------
99 result : `lsst.pipe.base.Struct`
100 - ``exposure`` : `lsst.afw.image.ExposureF`
101 a template coadd exposure assembled out of patches
102 - ``sources`` : `None` for this subtask
103 """
104 self.log.warn("GetCoaddAsTemplateTask is deprecated. Use GetTemplateTask instead.")
105 skyMap = butlerQC.get(skyMapRef)
106 coaddExposureRefs = butlerQC.get(coaddExposureRefs)
107 tracts = [ref.dataId['tract'] for ref in coaddExposureRefs]
108 if tracts.count(tracts[0]) == len(tracts):
109 tractInfo = skyMap[tracts[0]]
110 else:
111 raise RuntimeError("Templates constructed from multiple Tracts not supported by this task. "
112 "Use GetTemplateTask instead.")
113
114 detectorBBox = exposure.getBBox()
115 detectorWcs = exposure.getWcs()
116 detectorCorners = detectorWcs.pixelToSky(geom.Box2D(detectorBBox).getCorners())
117 validPolygon = exposure.getInfo().getValidPolygon()
118 detectorPolygon = validPolygon if validPolygon else geom.Box2D(detectorBBox)
119
120 availableCoaddRefs = dict()
121 overlappingArea = 0
122 for coaddRef in coaddExposureRefs:
123 dataId = coaddRef.dataId
124 patchWcs = skyMap[dataId['tract']].getWcs()
125 patchBBox = skyMap[dataId['tract']][dataId['patch']].getOuterBBox()
126 patchCorners = patchWcs.pixelToSky(geom.Box2D(patchBBox).getCorners())
127 patchPolygon = afwGeom.Polygon(detectorWcs.skyToPixel(patchCorners))
128 if patchPolygon.intersection(detectorPolygon):
129 overlappingArea += patchPolygon.intersectionSingle(detectorPolygon).calculateArea()
130 if self.config.coaddName == 'dcr':
131 self.log.info("Using template input tract=%s, patch=%s, subfilter=%s",
132 dataId['tract'], dataId['patch'], dataId['subfilter'])
133 if dataId['patch'] in availableCoaddRefs:
134 availableCoaddRefs[dataId['patch']].append(coaddRef)
135 else:
136 availableCoaddRefs[dataId['patch']] = [coaddRef, ]
137 else:
138 self.log.info("Using template input tract=%s, patch=%s",
139 dataId['tract'], dataId['patch'])
140 availableCoaddRefs[dataId['patch']] = coaddRef
141
142 if overlappingArea == 0:
143 templateExposure = None
144 pixGood = 0
145 self.log.warning("No overlapping template patches found")
146 else:
147 patchList = [tractInfo[patch] for patch in availableCoaddRefs.keys()]
148 templateExposure = self.run(tractInfo, patchList, detectorCorners, availableCoaddRefs,
149 visitInfo=exposure.getInfo().getVisitInfo())
150 # Count the number of pixels with the NO_DATA mask bit set
151 # counting NaN pixels is insufficient because pixels without data are often intepolated over)
152 pixNoData = np.count_nonzero(templateExposure.mask.array
153 & templateExposure.mask.getPlaneBitMask('NO_DATA'))
154 pixGood = templateExposure.getBBox().getArea() - pixNoData
155 self.log.info("template has %d good pixels (%.1f%%)", pixGood,
156 100*pixGood/templateExposure.getBBox().getArea())
157 return pipeBase.Struct(exposure=templateExposure, sources=None, area=pixGood)
158
159 def getOverlapPatchList(self, exposure, skyMap):
160 """Select the relevant tract and its patches that overlap with the science exposure.
161
162 Parameters
163 ----------
164 exposure : `lsst.afw.image.Exposure`
165 The science exposure to define the sky region of the template coadd.
166
167 skyMap : `lsst.skymap.BaseSkyMap`
168 SkyMap object that corresponds to the template coadd.
169
170 Returns
171 -------
172 result : `tuple` of
173 - ``tractInfo`` : `lsst.skymap.TractInfo`
174 The selected tract.
175 - ``patchList`` : `list` of `lsst.skymap.PatchInfo`
176 List of all overlap patches of the selected tract.
177 - ``skyCorners`` : `list` of `lsst.geom.SpherePoint`
178 Corners of the exposure in the sky in the order given by `lsst.geom.Box2D.getCorners`.
179 """
180 expWcs = exposure.getWcs()
181 expBoxD = geom.Box2D(exposure.getBBox())
182 expBoxD.grow(self.config.templateBorderSize)
183 ctrSkyPos = expWcs.pixelToSky(expBoxD.getCenter())
184 tractInfo = skyMap.findTract(ctrSkyPos)
185 self.log.info("Using skyMap tract %s", tractInfo.getId())
186 skyCorners = [expWcs.pixelToSky(pixPos) for pixPos in expBoxD.getCorners()]
187 patchList = tractInfo.findPatchList(skyCorners)
188
189 if not patchList:
190 raise RuntimeError("No suitable tract found")
191
192 self.log.info("Assembling %d coadd patches", len(patchList))
193 self.log.info("exposure dimensions=%s", exposure.getDimensions())
194
195 return (tractInfo, patchList, skyCorners)
196
197 def run(self, tractInfo, patchList, skyCorners, availableCoaddRefs,
198 sensorRef=None, visitInfo=None):
199 """Determination of exposure dimensions and copying of pixels from
200 overlapping patch regions.
201
202 Parameters
203 ----------
204 skyMap : `lsst.skymap.BaseSkyMap`
205 SkyMap object that corresponds to the template coadd.
206 tractInfo : `lsst.skymap.TractInfo`
207 The selected tract.
208 patchList : iterable of `lsst.skymap.patchInfo.PatchInfo`
209 Patches to consider for making the template exposure.
210 skyCorners : list of `lsst.geom.SpherePoint`
211 Sky corner coordinates to be covered by the template exposure.
212 availableCoaddRefs : `dict` [`int`]
213 Dictionary of spatially relevant retrieved coadd patches,
214 indexed by their sequential patch number. Values are
215 `lsst.daf.butler.DeferredDatasetHandle` and ``.get()`` is called.
216 sensorRef : `None`
217 Must always be `None`. Gen2 parameters are no longer used.
218 visitInfo : `lsst.afw.image.VisitInfo`
219 VisitInfo to make dcr model.
220
221 Returns
222 -------
223 templateExposure: `lsst.afw.image.ExposureF`
224 The created template exposure.
225 """
226 if sensorRef is not None:
227 raise ValueError("sensorRef parameter is a Gen2 parameter that is no longer usable."
228 " Please move to Gen3 middleware.")
229 coaddWcs = tractInfo.getWcs()
230
231 # compute coadd bbox
232 coaddBBox = geom.Box2D()
233 for skyPos in skyCorners:
234 coaddBBox.include(coaddWcs.skyToPixel(skyPos))
235 coaddBBox = geom.Box2I(coaddBBox)
236 self.log.info("coadd dimensions=%s", coaddBBox.getDimensions())
237
238 coaddExposure = afwImage.ExposureF(coaddBBox, coaddWcs)
239 coaddExposure.maskedImage.set(np.nan, afwImage.Mask.getPlaneBitMask("NO_DATA"), np.nan)
240 nPatchesFound = 0
241 coaddFilterLabel = None
242 coaddPsf = None
243 coaddPhotoCalib = None
244 for patchInfo in patchList:
245 patchNumber = tractInfo.getSequentialPatchIndex(patchInfo)
246 patchSubBBox = patchInfo.getOuterBBox()
247 patchSubBBox.clip(coaddBBox)
248 if patchNumber not in availableCoaddRefs:
249 self.log.warning("skip patch=%d; patch does not exist for this coadd", patchNumber)
250 continue
251 if patchSubBBox.isEmpty():
252 if isinstance(availableCoaddRefs[patchNumber], DeferredDatasetHandle):
253 tract = availableCoaddRefs[patchNumber].dataId['tract']
254 else:
255 tract = availableCoaddRefs[patchNumber]['tract']
256 self.log.info("skip tract=%d patch=%d; no overlapping pixels", tract, patchNumber)
257 continue
258
259 if self.config.coaddName == 'dcr':
260 patchInnerBBox = patchInfo.getInnerBBox()
261 patchInnerBBox.clip(coaddBBox)
262 if np.min(patchInnerBBox.getDimensions()) <= 2*self.config.templateBorderSize:
263 self.log.info("skip tract=%(tract)s, patch=%(patch)s; too few pixels.",
264 availableCoaddRefs[patchNumber])
265 continue
266 self.log.info("Constructing DCR-matched template for patch %s",
267 availableCoaddRefs[patchNumber])
268
269 dcrModel = DcrModel.fromQuantum(availableCoaddRefs[patchNumber],
270 self.config.effectiveWavelength,
271 self.config.bandwidth)
272 # The edge pixels of the DcrCoadd may contain artifacts due to missing data.
273 # Each patch has significant overlap, and the contaminated edge pixels in
274 # a new patch will overwrite good pixels in the overlap region from
275 # previous patches.
276 # Shrink the BBox to remove the contaminated pixels,
277 # but make sure it is only the overlap region that is reduced.
278 dcrBBox = geom.Box2I(patchSubBBox)
279 dcrBBox.grow(-self.config.templateBorderSize)
280 dcrBBox.include(patchInnerBBox)
281 coaddPatch = dcrModel.buildMatchedExposure(bbox=dcrBBox,
282 visitInfo=visitInfo)
283 else:
284 coaddPatch = availableCoaddRefs[patchNumber].get()
285
286 nPatchesFound += 1
287
288 # Gen2 get() seems to clip based on bbox kwarg but we removed bbox
289 # calculation from caller code. Gen3 also does not do this.
290 overlapBox = coaddPatch.getBBox()
291 overlapBox.clip(coaddBBox)
292 coaddExposure.maskedImage.assign(coaddPatch.maskedImage[overlapBox], overlapBox)
293
294 if coaddFilterLabel is None:
295 coaddFilterLabel = coaddPatch.getFilter()
296
297 # Retrieve the PSF for this coadd tract, if not already retrieved
298 if coaddPsf is None and coaddPatch.hasPsf():
299 coaddPsf = coaddPatch.getPsf()
300
301 # Retrieve the calibration for this coadd tract, if not already retrieved
302 if coaddPhotoCalib is None:
303 coaddPhotoCalib = coaddPatch.getPhotoCalib()
304
305 if coaddPhotoCalib is None:
306 raise RuntimeError("No coadd PhotoCalib found!")
307 if nPatchesFound == 0:
308 raise RuntimeError("No patches found!")
309 if coaddPsf is None:
310 raise RuntimeError("No coadd Psf found!")
311
312 coaddExposure.setPhotoCalib(coaddPhotoCalib)
313 coaddExposure.setPsf(coaddPsf)
314 coaddExposure.setFilter(coaddFilterLabel)
315 return coaddExposure
316
318 """Return coadd name for given task config
319
320 Returns
321 -------
322 CoaddDatasetName : `string`
323
324 TODO: This nearly duplicates a method in CoaddBaseTask (DM-11985)
325 """
326 warpType = self.config.warpType
327 suffix = "" if warpType == "direct" else warpType[0].upper() + warpType[1:]
328 return self.config.coaddName + "Coadd" + suffix
329
330
331class GetTemplateConnections(pipeBase.PipelineTaskConnections,
332 dimensions=("instrument", "visit", "detector", "skymap"),
333 defaultTemplates={"coaddName": "goodSeeing",
334 "warpTypeSuffix": "",
335 "fakesType": ""}):
336 bbox = pipeBase.connectionTypes.Input(
337 doc="BBoxes of calexp used determine geometry of output template",
338 name="{fakesType}calexp.bbox",
339 storageClass="Box2I",
340 dimensions=("instrument", "visit", "detector"),
341 )
342 wcs = pipeBase.connectionTypes.Input(
343 doc="WCS of the calexp that we want to fetch the template for",
344 name="{fakesType}calexp.wcs",
345 storageClass="Wcs",
346 dimensions=("instrument", "visit", "detector"),
347 )
348 skyMap = pipeBase.connectionTypes.Input(
349 doc="Input definition of geometry/bbox and projection/wcs for template exposures",
350 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
351 dimensions=("skymap", ),
352 storageClass="SkyMap",
353 )
354 # TODO DM-31292: Add option to use global external wcs from jointcal
355 # Needed for DRP HSC
356 coaddExposures = pipeBase.connectionTypes.Input(
357 doc="Input template to match and subtract from the exposure",
358 dimensions=("tract", "patch", "skymap", "band"),
359 storageClass="ExposureF",
360 name="{fakesType}{coaddName}Coadd{warpTypeSuffix}",
361 multiple=True,
362 deferLoad=True
363 )
364 template = pipeBase.connectionTypes.Output(
365 doc="Warped template used to create `subtractedExposure`.",
366 dimensions=("instrument", "visit", "detector"),
367 storageClass="ExposureF",
368 name="{fakesType}{coaddName}Diff_templateExp{warpTypeSuffix}",
369 )
370
371
372class GetTemplateConfig(pipeBase.PipelineTaskConfig,
373 pipelineConnections=GetTemplateConnections):
374 templateBorderSize = pexConfig.Field(
375 dtype=int,
376 default=20,
377 doc="Number of pixels to grow the requested template image to account for warping"
378 )
379 warp = pexConfig.ConfigField(
380 dtype=afwMath.Warper.ConfigClass,
381 doc="warper configuration",
382 )
383 coaddPsf = pexConfig.ConfigField(
384 doc="Configuration for CoaddPsf",
385 dtype=CoaddPsfConfig,
386 )
387
388 def setDefaults(self):
389 self.warp.warpingKernelName = 'lanczos5'
390 self.coaddPsf.warpingKernelName = 'lanczos5'
391
392
393class GetTemplateTask(pipeBase.PipelineTask):
394 ConfigClass = GetTemplateConfig
395 _DefaultName = "getTemplate"
396
397 def __init__(self, *args, **kwargs):
398 super().__init__(*args, **kwargs)
399 self.warper = afwMath.Warper.fromConfig(self.config.warp)
400
401 def runQuantum(self, butlerQC, inputRefs, outputRefs):
402 # Read in all inputs.
403 inputs = butlerQC.get(inputRefs)
404 results = self.getOverlappingExposures(inputs)
405 inputs["coaddExposures"] = results.coaddExposures
406 inputs["dataIds"] = results.dataIds
407 outputs = self.run(**inputs)
408 butlerQC.put(outputs, outputRefs)
409
410 def getOverlappingExposures(self, inputs):
411 """Return lists of coadds and their corresponding dataIds that overlap the detector.
412
413 The spatial index in the registry has generous padding and often supplies
414 patches near, but not directly overlapping the detector.
415 Filters inputs so that we don't have to read in all input coadds.
416
417 Parameters
418 ----------
419 inputs : `dict` of task Inputs, containing:
420 - coaddExposureRefs : list of elements of type
421 `lsst.daf.butler.DeferredDatasetHandle` of
423 Data references to exposures that might overlap the detector.
424 - bbox : `lsst.geom.Box2I`
425 Template Bounding box of the detector geometry onto which to
426 resample the coaddExposures
427 - skyMap : `lsst.skymap.SkyMap`
428 Input definition of geometry/bbox and projection/wcs for template exposures
429 - wcs : `lsst.afw.geom.SkyWcs`
430 Template WCS onto which to resample the coaddExposures
431
432 Returns
433 -------
434 result : `lsst.pipe.base.Struct` containing these fields:
435 - coaddExposures : `list` of elements of type `lsst.afw.image.Exposure`
436 Coadd exposures that overlap the detector.
437 - dataIds : `list` of `lsst.daf.butler.DataCoordinate`
438 Data IDs of the coadd exposures that overlap the detector.
439
440 Raises
441 ------
442 NoWorkFound
443 Raised if no patches overlap the input detector bbox
444 """
445 # Check that the patches actually overlap the detector
446 # Exposure's validPolygon would be more accurate
447 detectorPolygon = geom.Box2D(inputs['bbox'])
448 overlappingArea = 0
449 coaddExposureList = []
450 dataIds = []
451 for coaddRef in inputs['coaddExposures']:
452 dataId = coaddRef.dataId
453 patchWcs = inputs['skyMap'][dataId['tract']].getWcs()
454 patchBBox = inputs['skyMap'][dataId['tract']][dataId['patch']].getOuterBBox()
455 patchCorners = patchWcs.pixelToSky(geom.Box2D(patchBBox).getCorners())
456 patchPolygon = afwGeom.Polygon(inputs['wcs'].skyToPixel(patchCorners))
457 if patchPolygon.intersection(detectorPolygon):
458 overlappingArea += patchPolygon.intersectionSingle(detectorPolygon).calculateArea()
459 self.log.info("Using template input tract=%s, patch=%s" %
460 (dataId['tract'], dataId['patch']))
461 coaddExposureList.append(coaddRef.get())
462 dataIds.append(dataId)
463
464 if not overlappingArea:
465 raise pipeBase.NoWorkFound('No patches overlap detector')
466
467 return pipeBase.Struct(coaddExposures=coaddExposureList,
468 dataIds=dataIds)
469
470 def run(self, coaddExposures, bbox, wcs, dataIds, **kwargs):
471 """Warp coadds from multiple tracts to form a template for image diff.
472
473 Where the tracts overlap, the resulting template image is averaged.
474 The PSF on the template is created by combining the CoaddPsf on each
475 template image into a meta-CoaddPsf.
476
477 Parameters
478 ----------
479 coaddExposures : `list` of `lsst.afw.image.Exposure`
480 Coadds to be mosaicked
481 bbox : `lsst.geom.Box2I`
482 Template Bounding box of the detector geometry onto which to
483 resample the coaddExposures
485 Template WCS onto which to resample the coaddExposures
486 dataIds : `list` of `lsst.daf.butler.DataCoordinate`
487 Record of the tract and patch of each coaddExposure.
488 **kwargs
489 Any additional keyword parameters.
490
491 Returns
492 -------
493 result : `lsst.pipe.base.Struct` containing
494 - ``template`` : a template coadd exposure assembled out of patches
495 """
496 # Table for CoaddPSF
497 tractsSchema = afwTable.ExposureTable.makeMinimalSchema()
498 tractKey = tractsSchema.addField('tract', type=np.int32, doc='Which tract')
499 patchKey = tractsSchema.addField('patch', type=np.int32, doc='Which patch')
500 weightKey = tractsSchema.addField('weight', type=float, doc='Weight for each tract, should be 1')
501 tractsCatalog = afwTable.ExposureCatalog(tractsSchema)
502
503 finalWcs = wcs
504 bbox.grow(self.config.templateBorderSize)
505 finalBBox = bbox
506
507 nPatchesFound = 0
508 maskedImageList = []
509 weightList = []
510
511 for coaddExposure, dataId in zip(coaddExposures, dataIds):
512
513 # warp to detector WCS
514 warped = self.warper.warpExposure(finalWcs, coaddExposure, maxBBox=finalBBox)
515
516 # Check if warped image is viable
517 if not np.any(np.isfinite(warped.image.array)):
518 self.log.info("No overlap for warped %s. Skipping" % dataId)
519 continue
520
521 exp = afwImage.ExposureF(finalBBox, finalWcs)
522 exp.maskedImage.set(np.nan, afwImage.Mask.getPlaneBitMask("NO_DATA"), np.nan)
523 exp.maskedImage.assign(warped.maskedImage, warped.getBBox())
524
525 maskedImageList.append(exp.maskedImage)
526 weightList.append(1)
527 record = tractsCatalog.addNew()
528 record.setPsf(coaddExposure.getPsf())
529 record.setWcs(coaddExposure.getWcs())
530 record.setPhotoCalib(coaddExposure.getPhotoCalib())
531 record.setBBox(coaddExposure.getBBox())
532 record.setValidPolygon(afwGeom.Polygon(geom.Box2D(coaddExposure.getBBox()).getCorners()))
533 record.set(tractKey, dataId['tract'])
534 record.set(patchKey, dataId['patch'])
535 record.set(weightKey, 1.)
536 nPatchesFound += 1
537
538 if nPatchesFound == 0:
539 raise pipeBase.NoWorkFound("No patches found to overlap detector")
540
541 # Combine images from individual patches together
542 statsFlags = afwMath.stringToStatisticsProperty('MEAN')
543 statsCtrl = afwMath.StatisticsControl()
544 statsCtrl.setNanSafe(True)
545 statsCtrl.setWeighted(True)
546 statsCtrl.setCalcErrorFromInputVariance(True)
547
548 templateExposure = afwImage.ExposureF(finalBBox, finalWcs)
549 templateExposure.maskedImage.set(np.nan, afwImage.Mask.getPlaneBitMask("NO_DATA"), np.nan)
550 xy0 = templateExposure.getXY0()
551 # Do not mask any values
552 templateExposure.maskedImage = afwMath.statisticsStack(maskedImageList, statsFlags, statsCtrl,
553 weightList, clipped=0, maskMap=[])
554 templateExposure.maskedImage.setXY0(xy0)
555
556 # CoaddPsf centroid not only must overlap image, but must overlap the part of
557 # image with data. Use centroid of region with data
558 boolmask = templateExposure.mask.array & templateExposure.mask.getPlaneBitMask('NO_DATA') == 0
559 maskx = afwImage.makeMaskFromArray(boolmask.astype(afwImage.MaskPixel))
560 centerCoord = afwGeom.SpanSet.fromMask(maskx, 1).computeCentroid()
561
562 ctrl = self.config.coaddPsf.makeControl()
563 coaddPsf = CoaddPsf(tractsCatalog, finalWcs, centerCoord, ctrl.warpingKernelName, ctrl.cacheSize)
564 if coaddPsf is None:
565 raise RuntimeError("CoaddPsf could not be constructed")
566
567 templateExposure.setPsf(coaddPsf)
568 templateExposure.setFilter(coaddExposure.getFilter())
569 templateExposure.setPhotoCalib(coaddExposure.getPhotoCalib())
570 return pipeBase.Struct(template=templateExposure)
571
572
574 dimensions=("instrument", "visit", "detector", "skymap"),
575 defaultTemplates={"coaddName": "dcr",
576 "warpTypeSuffix": "",
577 "fakesType": ""}):
578 visitInfo = pipeBase.connectionTypes.Input(
579 doc="VisitInfo of calexp used to determine observing conditions.",
580 name="{fakesType}calexp.visitInfo",
581 storageClass="VisitInfo",
582 dimensions=("instrument", "visit", "detector"),
583 )
584 dcrCoadds = pipeBase.connectionTypes.Input(
585 doc="Input DCR template to match and subtract from the exposure",
586 name="{fakesType}dcrCoadd{warpTypeSuffix}",
587 storageClass="ExposureF",
588 dimensions=("tract", "patch", "skymap", "band", "subfilter"),
589 multiple=True,
590 deferLoad=True
591 )
592
593 def __init__(self, *, config=None):
594 super().__init__(config=config)
595 self.inputs.remove("coaddExposures")
596
597
598class GetDcrTemplateConfig(GetTemplateConfig,
599 pipelineConnections=GetDcrTemplateConnections):
600 numSubfilters = pexConfig.Field(
601 doc="Number of subfilters in the DcrCoadd.",
602 dtype=int,
603 default=3,
604 )
605 effectiveWavelength = pexConfig.Field(
606 doc="Effective wavelength of the filter.",
607 optional=False,
608 dtype=float,
609 )
610 bandwidth = pexConfig.Field(
611 doc="Bandwidth of the physical filter.",
612 optional=False,
613 dtype=float,
614 )
615
616 def validate(self):
617 if self.effectiveWavelength is None or self.bandwidth is None:
618 raise ValueError("The effective wavelength and bandwidth of the physical filter "
619 "must be set in the getTemplate config for DCR coadds. "
620 "Required until transmission curves are used in DM-13668.")
621
622
623class GetDcrTemplateTask(GetTemplateTask):
624 ConfigClass = GetDcrTemplateConfig
625 _DefaultName = "getDcrTemplate"
626
627 def getOverlappingExposures(self, inputs):
628 """Return lists of coadds and their corresponding dataIds that overlap the detector.
629
630 The spatial index in the registry has generous padding and often supplies
631 patches near, but not directly overlapping the detector.
632 Filters inputs so that we don't have to read in all input coadds.
633
634 Parameters
635 ----------
636 inputs : `dict` of task Inputs, containing:
637 - coaddExposureRefs : `list` of elements of type
638 `lsst.daf.butler.DeferredDatasetHandle` of
640 Data references to exposures that might overlap the detector.
641 - bbox : `lsst.geom.Box2I`
642 Template Bounding box of the detector geometry onto which to
643 resample the coaddExposures
644 - skyMap : `lsst.skymap.SkyMap`
645 Input definition of geometry/bbox and projection/wcs for template exposures
646 - wcs : `lsst.afw.geom.SkyWcs`
647 Template WCS onto which to resample the coaddExposures
648 - visitInfo : `lsst.afw.image.VisitInfo`
649 Metadata for the science image.
650
651 Returns
652 -------
653 result : `lsst.pipe.base.Struct` containing these fields:
654 - coaddExposures : `list` of elements of type `lsst.afw.image.Exposure`
655 Coadd exposures that overlap the detector.
656 - dataIds : `list` of `lsst.daf.butler.DataCoordinate`
657 Data IDs of the coadd exposures that overlap the detector.
658
659 Raises
660 ------
661 NoWorkFound
662 Raised if no patches overlatp the input detector bbox
663 """
664 # Check that the patches actually overlap the detector
665 # Exposure's validPolygon would be more accurate
666 detectorPolygon = geom.Box2D(inputs["bbox"])
667 overlappingArea = 0
668 coaddExposureRefList = []
669 dataIds = []
670 patchList = dict()
671 for coaddRef in inputs["dcrCoadds"]:
672 dataId = coaddRef.dataId
673 patchWcs = inputs["skyMap"][dataId['tract']].getWcs()
674 patchBBox = inputs["skyMap"][dataId['tract']][dataId['patch']].getOuterBBox()
675 patchCorners = patchWcs.pixelToSky(geom.Box2D(patchBBox).getCorners())
676 patchPolygon = afwGeom.Polygon(inputs["wcs"].skyToPixel(patchCorners))
677 if patchPolygon.intersection(detectorPolygon):
678 overlappingArea += patchPolygon.intersectionSingle(detectorPolygon).calculateArea()
679 self.log.info("Using template input tract=%s, patch=%s, subfilter=%s" %
680 (dataId['tract'], dataId['patch'], dataId["subfilter"]))
681 coaddExposureRefList.append(coaddRef)
682 if dataId['tract'] in patchList:
683 patchList[dataId['tract']].append(dataId['patch'])
684 else:
685 patchList[dataId['tract']] = [dataId['patch'], ]
686 dataIds.append(dataId)
687
688 if not overlappingArea:
689 raise pipeBase.NoWorkFound('No patches overlap detector')
690
691 self.checkPatchList(patchList)
692
693 coaddExposures = self.getDcrModel(patchList, inputs['dcrCoadds'], inputs['visitInfo'])
694 return pipeBase.Struct(coaddExposures=coaddExposures,
695 dataIds=dataIds)
696
697 def checkPatchList(self, patchList):
698 """Check that all of the DcrModel subfilters are present for each patch.
699
700 Parameters
701 ----------
702 patchList : `dict`
703 Dict of the patches containing valid data for each tract
704
705 Raises
706 ------
707 RuntimeError
708 If the number of exposures found for a patch does not match the number of subfilters.
709 """
710 for tract in patchList:
711 for patch in set(patchList[tract]):
712 if patchList[tract].count(patch) != self.config.numSubfilters:
713 raise RuntimeError("Invalid number of DcrModel subfilters found: %d vs %d expected",
714 patchList[tract].count(patch), self.config.numSubfilters)
715
716 def getDcrModel(self, patchList, coaddRefs, visitInfo):
717 """Build DCR-matched coadds from a list of exposure references.
718
719 Parameters
720 ----------
721 patchList : `dict`
722 Dict of the patches containing valid data for each tract
723 coaddRefs : `list` of elements of type
724 `lsst.daf.butler.DeferredDatasetHandle` of
726 Data references to DcrModels that overlap the detector.
727 visitInfo : `lsst.afw.image.VisitInfo`
728 Metadata for the science image.
729
730 Returns
731 -------
732 `list` of elements of type `lsst.afw.image.Exposure`
733 Coadd exposures that overlap the detector.
734 """
735 coaddExposureList = []
736 for tract in patchList:
737 for patch in set(patchList[tract]):
738 coaddRefList = [coaddRef for coaddRef in coaddRefs
739 if _selectDataRef(coaddRef, tract, patch)]
740
741 dcrModel = DcrModel.fromQuantum(coaddRefList,
742 self.config.effectiveWavelength,
743 self.config.bandwidth,
744 self.config.numSubfilters)
745 coaddExposureList.append(dcrModel.buildMatchedExposure(visitInfo=visitInfo))
746 return coaddExposureList
747
748
749def _selectDataRef(coaddRef, tract, patch):
750 condition = (coaddRef.dataId['tract'] == tract) & (coaddRef.dataId['patch'] == patch)
751 return condition
752
753
754class GetMultiTractCoaddTemplateConfig(GetTemplateConfig):
755 pass
756
757
758class GetMultiTractCoaddTemplateTask(GetTemplateTask):
759 ConfigClass = GetMultiTractCoaddTemplateConfig
760 _DefaultName = "getMultiTractCoaddTemplate"
761
762 def __init__(self, *args, **kwargs):
763 super().__init__(*args, **kwargs)
764 self.log.warn("GetMultiTractCoaddTemplateTask is deprecated. Use GetTemplateTask instead.")
def run(self, tractInfo, patchList, skyCorners, availableCoaddRefs, sensorRef=None, visitInfo=None)
Definition: getTemplate.py:198
def runQuantum(self, exposure, butlerQC, skyMapRef, coaddExposureRefs)
Definition: getTemplate.py:82
std::shared_ptr< lsst::afw::image::Image< PixelT > > statisticsStack(std::vector< std::shared_ptr< lsst::afw::image::Image< PixelT > > > &images, Property flags, StatisticsControl const &sctrl=StatisticsControl(), std::vector< lsst::afw::image::VariancePixel > const &wvector=std::vector< lsst::afw::image::VariancePixel >(0))
Property stringToStatisticsProperty(std::string const property)
def checkPatchList(self, patchList)
Definition: getTemplate.py:697
def getDcrModel(self, patchList, coaddRefs, visitInfo)
Definition: getTemplate.py:716
def run(self, coaddExposures, bbox, wcs, dataIds, **kwargs)
Definition: getTemplate.py:470