lsst.ip.diffim g9ac972b7e6+fa75a2bf84
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 "GetCalexpAsTemplateTask", "GetCalexpAsTemplateConfig",
39 "GetMultiTractCoaddTemplateTask", "GetMultiTractCoaddTemplateConfig"]
40
41
42class GetCoaddAsTemplateConfig(pexConfig.Config):
43 templateBorderSize = pexConfig.Field(
44 dtype=int,
45 default=10,
46 doc="Number of pixels to grow the requested template image to account for warping"
47 )
48 coaddName = pexConfig.Field(
49 doc="coadd name: typically one of 'deep', 'goodSeeing', or 'dcr'",
50 dtype=str,
51 default="deep",
52 )
53 numSubfilters = pexConfig.Field(
54 doc="Number of subfilters in the DcrCoadd. Used only if ``coaddName``='dcr'",
55 dtype=int,
56 default=3,
57 )
58 effectiveWavelength = pexConfig.Field(
59 doc="Effective wavelength of the filter. Used only if ``coaddName``='dcr'",
60 optional=True,
61 dtype=float,
62 )
63 bandwidth = pexConfig.Field(
64 doc="Bandwidth of the physical filter. Used only if ``coaddName``='dcr'",
65 optional=True,
66 dtype=float,
67 )
68 warpType = pexConfig.Field(
69 doc="Warp type of the coadd template: one of 'direct' or 'psfMatched'",
70 dtype=str,
71 default="direct",
72 )
73
74 def validate(self):
75 if self.coaddNamecoaddName == 'dcr':
76 if self.effectiveWavelengtheffectiveWavelength is None or self.bandwidthbandwidth is None:
77 raise ValueError("The effective wavelength and bandwidth of the physical filter "
78 "must be set in the getTemplate config for DCR coadds. "
79 "Required until transmission curves are used in DM-13668.")
80
81
82class GetCoaddAsTemplateTask(pipeBase.Task):
83 """Subtask to retrieve coadd for use as an image difference template.
84
85 This is the default getTemplate Task to be run as a subtask by
86 ``pipe.tasks.ImageDifferenceTask``. The main methods are ``run()`` and
87 ``runGen3()``.
88
89 Notes
90 -----
91 From the given skymap, the closest tract is selected; multiple tracts are
92 not supported. The assembled template inherits the WCS of the selected
93 skymap tract and the resolution of the template exposures. Overlapping box
94 regions of the input template patches are pixel by pixel copied into the
95 assembled template image. There is no warping or pixel resampling.
96
97 Pixels with no overlap of any available input patches are set to ``nan`` value
98 and ``NO_DATA`` flagged.
99 """
100
101 ConfigClass = GetCoaddAsTemplateConfig
102 _DefaultName = "GetCoaddAsTemplateTask"
103
104 def runDataRef(self, exposure, sensorRef, templateIdList=None):
105 """Gen2 task entry point. Retrieve and mosaic a template coadd exposure
106 that overlaps the science exposure.
107
108 Parameters
109 ----------
110 exposure: `lsst.afw.image.Exposure`
111 an exposure for which to generate an overlapping template
112 sensorRef : TYPE
113 a Butler data reference that can be used to obtain coadd data
114 templateIdList : TYPE, optional
115 list of data ids, unused here, in the case of coadd template
116
117 Returns
118 -------
119 result : `lsst.pipe.base.Struct`
120 - ``exposure`` : `lsst.afw.image.ExposureF`
121 a template coadd exposure assembled out of patches
122 - ``sources`` : None for this subtask
123 """
124 skyMap = sensorRef.get(datasetType=self.config.coaddName + "Coadd_skyMap")
125 tractInfo, patchList, skyCorners = self.getOverlapPatchListgetOverlapPatchList(exposure, skyMap)
126
127 availableCoaddRefs = dict()
128 for patchInfo in patchList:
129 patchNumber = tractInfo.getSequentialPatchIndex(patchInfo)
130 patchArgDict = dict(
131 datasetType=self.getCoaddDatasetNamegetCoaddDatasetName() + "_sub",
132 bbox=patchInfo.getOuterBBox(),
133 tract=tractInfo.getId(),
134 patch="%s,%s" % (patchInfo.getIndex()[0], patchInfo.getIndex()[1]),
135 subfilter=0,
136 numSubfilters=self.config.numSubfilters,
137 )
138
139 if sensorRef.datasetExists(**patchArgDict):
140 self.log.info("Reading patch %s", patchArgDict)
141 availableCoaddRefs[patchNumber] = patchArgDict
142
143 templateExposure = self.runrun(
144 tractInfo, patchList, skyCorners, availableCoaddRefs,
145 sensorRef=sensorRef, visitInfo=exposure.getInfo().getVisitInfo()
146 )
147 return pipeBase.Struct(exposure=templateExposure, sources=None)
148
149 def runQuantum(self, exposure, butlerQC, skyMapRef, coaddExposureRefs):
150 """Gen3 task entry point. Retrieve and mosaic a template coadd exposure
151 that overlaps the science exposure.
152
153 Parameters
154 ----------
155 exposure : `lsst.afw.image.Exposure`
156 The science exposure to define the sky region of the template coadd.
157 butlerQC : `lsst.pipe.base.ButlerQuantumContext`
158 Butler like object that supports getting data by DatasetRef.
159 skyMapRef : `lsst.daf.butler.DatasetRef`
160 Reference to SkyMap object that corresponds to the template coadd.
161 coaddExposureRefs : iterable of `lsst.daf.butler.DeferredDatasetRef`
162 Iterable of references to the available template coadd patches.
163
164 Returns
165 -------
166 result : `lsst.pipe.base.Struct`
167 - ``exposure`` : `lsst.afw.image.ExposureF`
168 a template coadd exposure assembled out of patches
169 - ``sources`` : `None` for this subtask
170 """
171 skyMap = butlerQC.get(skyMapRef)
172 coaddExposureRefs = butlerQC.get(coaddExposureRefs)
173 tracts = [ref.dataId['tract'] for ref in coaddExposureRefs]
174 if tracts.count(tracts[0]) == len(tracts):
175 tractInfo = skyMap[tracts[0]]
176 else:
177 raise RuntimeError("Templates constructed from multiple Tracts not supported by this task. "
178 "Use GetMultiTractCoaddTemplateTask instead.")
179
180 detectorBBox = exposure.getBBox()
181 detectorWcs = exposure.getWcs()
182 detectorCorners = detectorWcs.pixelToSky(geom.Box2D(detectorBBox).getCorners())
183 validPolygon = exposure.getInfo().getValidPolygon()
184 detectorPolygon = validPolygon if validPolygon else geom.Box2D(detectorBBox)
185
186 availableCoaddRefs = dict()
187 overlappingArea = 0
188 for coaddRef in coaddExposureRefs:
189 dataId = coaddRef.dataId
190 patchWcs = skyMap[dataId['tract']].getWcs()
191 patchBBox = skyMap[dataId['tract']][dataId['patch']].getOuterBBox()
192 patchCorners = patchWcs.pixelToSky(geom.Box2D(patchBBox).getCorners())
193 patchPolygon = afwGeom.Polygon(detectorWcs.skyToPixel(patchCorners))
194 if patchPolygon.intersection(detectorPolygon):
195 overlappingArea += patchPolygon.intersectionSingle(detectorPolygon).calculateArea()
196 if self.config.coaddName == 'dcr':
197 self.log.info("Using template input tract=%s, patch=%s, subfilter=%s",
198 dataId['tract'], dataId['patch'], dataId['subfilter'])
199 if dataId['patch'] in availableCoaddRefs:
200 availableCoaddRefs[dataId['patch']].append(coaddRef)
201 else:
202 availableCoaddRefs[dataId['patch']] = [coaddRef, ]
203 else:
204 self.log.info("Using template input tract=%s, patch=%s",
205 dataId['tract'], dataId['patch'])
206 availableCoaddRefs[dataId['patch']] = coaddRef
207
208 if overlappingArea == 0:
209 templateExposure = None
210 pixGood = 0
211 self.log.warning("No overlapping template patches found")
212 else:
213 patchList = [tractInfo[patch] for patch in availableCoaddRefs.keys()]
214 templateExposure = self.runrun(tractInfo, patchList, detectorCorners, availableCoaddRefs,
215 visitInfo=exposure.getInfo().getVisitInfo())
216 # Count the number of pixels with the NO_DATA mask bit set
217 # counting NaN pixels is insufficient because pixels without data are often intepolated over)
218 pixNoData = np.count_nonzero(templateExposure.mask.array
219 & templateExposure.mask.getPlaneBitMask('NO_DATA'))
220 pixGood = templateExposure.getBBox().getArea() - pixNoData
221 self.log.info("template has %d good pixels (%.1f%%)", pixGood,
222 100*pixGood/templateExposure.getBBox().getArea())
223 return pipeBase.Struct(exposure=templateExposure, sources=None, area=pixGood)
224
225 def getOverlapPatchList(self, exposure, skyMap):
226 """Select the relevant tract and its patches that overlap with the science exposure.
227
228 Parameters
229 ----------
230 exposure : `lsst.afw.image.Exposure`
231 The science exposure to define the sky region of the template coadd.
232
233 skyMap : `lsst.skymap.BaseSkyMap`
234 SkyMap object that corresponds to the template coadd.
235
236 Returns
237 -------
238 result : `tuple` of
239 - ``tractInfo`` : `lsst.skymap.TractInfo`
240 The selected tract.
241 - ``patchList`` : `list` of `lsst.skymap.PatchInfo`
242 List of all overlap patches of the selected tract.
243 - ``skyCorners`` : `list` of `lsst.geom.SpherePoint`
244 Corners of the exposure in the sky in the order given by `lsst.geom.Box2D.getCorners`.
245 """
246 expWcs = exposure.getWcs()
247 expBoxD = geom.Box2D(exposure.getBBox())
248 expBoxD.grow(self.config.templateBorderSize)
249 ctrSkyPos = expWcs.pixelToSky(expBoxD.getCenter())
250 tractInfo = skyMap.findTract(ctrSkyPos)
251 self.log.info("Using skyMap tract %s", tractInfo.getId())
252 skyCorners = [expWcs.pixelToSky(pixPos) for pixPos in expBoxD.getCorners()]
253 patchList = tractInfo.findPatchList(skyCorners)
254
255 if not patchList:
256 raise RuntimeError("No suitable tract found")
257
258 self.log.info("Assembling %d coadd patches", len(patchList))
259 self.log.info("exposure dimensions=%s", exposure.getDimensions())
260
261 return (tractInfo, patchList, skyCorners)
262
263 def run(self, tractInfo, patchList, skyCorners, availableCoaddRefs,
264 sensorRef=None, visitInfo=None):
265 """Gen2 and gen3 shared code: determination of exposure dimensions and
266 copying of pixels from overlapping patch regions.
267
268 Parameters
269 ----------
270 skyMap : `lsst.skymap.BaseSkyMap`
271 SkyMap object that corresponds to the template coadd.
272 tractInfo : `lsst.skymap.TractInfo`
273 The selected tract.
274 patchList : iterable of `lsst.skymap.patchInfo.PatchInfo`
275 Patches to consider for making the template exposure.
276 skyCorners : list of `lsst.geom.SpherePoint`
277 Sky corner coordinates to be covered by the template exposure.
278 availableCoaddRefs : `dict` [`int`]
279 Dictionary of spatially relevant retrieved coadd patches,
280 indexed by their sequential patch number. In Gen3 mode, values are
281 `lsst.daf.butler.DeferredDatasetHandle` and ``.get()`` is called,
282 in Gen2 mode, ``sensorRef.get(**coaddef)`` is called to retrieve the coadd.
283 sensorRef : `lsst.daf.persistence.ButlerDataRef`, Gen2 only
284 Butler data reference to get coadd data.
285 Must be `None` for Gen3.
286 visitInfo : `lsst.afw.image.VisitInfo`, Gen2 only
287 VisitInfo to make dcr model.
288
289 Returns
290 -------
291 templateExposure: `lsst.afw.image.ExposureF`
292 The created template exposure.
293 """
294 coaddWcs = tractInfo.getWcs()
295
296 # compute coadd bbox
297 coaddBBox = geom.Box2D()
298 for skyPos in skyCorners:
299 coaddBBox.include(coaddWcs.skyToPixel(skyPos))
300 coaddBBox = geom.Box2I(coaddBBox)
301 self.log.info("coadd dimensions=%s", coaddBBox.getDimensions())
302
303 coaddExposure = afwImage.ExposureF(coaddBBox, coaddWcs)
304 coaddExposure.maskedImage.set(np.nan, afwImage.Mask.getPlaneBitMask("NO_DATA"), np.nan)
305 nPatchesFound = 0
306 coaddFilterLabel = None
307 coaddPsf = None
308 coaddPhotoCalib = None
309 for patchInfo in patchList:
310 patchNumber = tractInfo.getSequentialPatchIndex(patchInfo)
311 patchSubBBox = patchInfo.getOuterBBox()
312 patchSubBBox.clip(coaddBBox)
313 if patchNumber not in availableCoaddRefs:
314 self.log.warning("skip patch=%d; patch does not exist for this coadd", patchNumber)
315 continue
316 if patchSubBBox.isEmpty():
317 if isinstance(availableCoaddRefs[patchNumber], DeferredDatasetHandle):
318 tract = availableCoaddRefs[patchNumber].dataId['tract']
319 else:
320 tract = availableCoaddRefs[patchNumber]['tract']
321 self.log.info("skip tract=%d patch=%d; no overlapping pixels", tract, patchNumber)
322 continue
323
324 if self.config.coaddName == 'dcr':
325 patchInnerBBox = patchInfo.getInnerBBox()
326 patchInnerBBox.clip(coaddBBox)
327 if np.min(patchInnerBBox.getDimensions()) <= 2*self.config.templateBorderSize:
328 self.log.info("skip tract=%(tract)s, patch=%(patch)s; too few pixels.",
329 availableCoaddRefs[patchNumber])
330 continue
331 self.log.info("Constructing DCR-matched template for patch %s",
332 availableCoaddRefs[patchNumber])
333
334 if sensorRef:
335 dcrModel = DcrModel.fromDataRef(sensorRef,
336 self.config.effectiveWavelength,
337 self.config.bandwidth,
338 **availableCoaddRefs[patchNumber])
339 else:
340 dcrModel = DcrModel.fromQuantum(availableCoaddRefs[patchNumber],
341 self.config.effectiveWavelength,
342 self.config.bandwidth)
343 # The edge pixels of the DcrCoadd may contain artifacts due to missing data.
344 # Each patch has significant overlap, and the contaminated edge pixels in
345 # a new patch will overwrite good pixels in the overlap region from
346 # previous patches.
347 # Shrink the BBox to remove the contaminated pixels,
348 # but make sure it is only the overlap region that is reduced.
349 dcrBBox = geom.Box2I(patchSubBBox)
350 dcrBBox.grow(-self.config.templateBorderSize)
351 dcrBBox.include(patchInnerBBox)
352 coaddPatch = dcrModel.buildMatchedExposure(bbox=dcrBBox,
353 visitInfo=visitInfo)
354 else:
355 if sensorRef is None:
356 # Gen3
357 coaddPatch = availableCoaddRefs[patchNumber].get()
358 else:
359 # Gen2
360 coaddPatch = sensorRef.get(**availableCoaddRefs[patchNumber])
361 nPatchesFound += 1
362
363 # Gen2 get() seems to clip based on bbox kwarg but we removed bbox
364 # calculation from caller code. Gen3 also does not do this.
365 overlapBox = coaddPatch.getBBox()
366 overlapBox.clip(coaddBBox)
367 coaddExposure.maskedImage.assign(coaddPatch.maskedImage[overlapBox], overlapBox)
368
369 if coaddFilterLabel is None:
370 coaddFilterLabel = coaddPatch.getFilterLabel()
371
372 # Retrieve the PSF for this coadd tract, if not already retrieved
373 if coaddPsf is None and coaddPatch.hasPsf():
374 coaddPsf = coaddPatch.getPsf()
375
376 # Retrieve the calibration for this coadd tract, if not already retrieved
377 if coaddPhotoCalib is None:
378 coaddPhotoCalib = coaddPatch.getPhotoCalib()
379
380 if coaddPhotoCalib is None:
381 raise RuntimeError("No coadd PhotoCalib found!")
382 if nPatchesFound == 0:
383 raise RuntimeError("No patches found!")
384 if coaddPsf is None:
385 raise RuntimeError("No coadd Psf found!")
386
387 coaddExposure.setPhotoCalib(coaddPhotoCalib)
388 coaddExposure.setPsf(coaddPsf)
389 coaddExposure.setFilterLabel(coaddFilterLabel)
390 return coaddExposure
391
393 """Return coadd name for given task config
394
395 Returns
396 -------
397 CoaddDatasetName : `string`
398
399 TODO: This nearly duplicates a method in CoaddBaseTask (DM-11985)
400 """
401 warpType = self.config.warpType
402 suffix = "" if warpType == "direct" else warpType[0].upper() + warpType[1:]
403 return self.config.coaddName + "Coadd" + suffix
404
405
406class GetCalexpAsTemplateConfig(pexConfig.Config):
407 doAddCalexpBackground = pexConfig.Field(
408 dtype=bool,
409 default=True,
410 doc="Add background to calexp before processing it."
411 )
412
413
414class GetCalexpAsTemplateTask(pipeBase.Task):
415 """Subtask to retrieve calexp of the same ccd number as the science image SensorRef
416 for use as an image difference template. Only gen2 supported.
417
418 To be run as a subtask by pipe.tasks.ImageDifferenceTask.
419 Intended for use with simulations and surveys that repeatedly visit the same pointing.
420 This code was originally part of Winter2013ImageDifferenceTask.
421 """
422
423 ConfigClass = GetCalexpAsTemplateConfig
424 _DefaultName = "GetCalexpAsTemplateTask"
425
426 def run(self, exposure, sensorRef, templateIdList):
427 """Return a calexp exposure with based on input sensorRef.
428
429 Construct a dataId based on the sensorRef.dataId combined
430 with the specifications from the first dataId in templateIdList
431
432 Parameters
433 ----------
434 exposure : `lsst.afw.image.Exposure`
435 exposure (unused)
436 sensorRef : `list` of `lsst.daf.persistence.ButlerDataRef`
437 Data reference of the calexp(s) to subtract from.
438 templateIdList : `list` of `lsst.daf.persistence.ButlerDataRef`
439 Data reference of the template calexp to be subtraced.
440 Can be incomplete, fields are initialized from `sensorRef`.
441 If there are multiple items, only the first one is used.
442
443 Returns
444 -------
445 result : `struct`
446
447 return a pipeBase.Struct:
448
449 - ``exposure`` : a template calexp
450 - ``sources`` : source catalog measured on the template
451 """
452
453 if len(templateIdList) == 0:
454 raise RuntimeError("No template data reference supplied.")
455 if len(templateIdList) > 1:
456 self.log.warning("Multiple template data references supplied. Using the first one only.")
457
458 templateId = sensorRef.dataId.copy()
459 templateId.update(templateIdList[0])
460
461 self.log.info("Fetching calexp (%s) as template.", templateId)
462
463 butler = sensorRef.getButler()
464 template = butler.get(datasetType="calexp", dataId=templateId)
465 if self.config.doAddCalexpBackground:
466 templateBg = butler.get(datasetType="calexpBackground", dataId=templateId)
467 mi = template.getMaskedImage()
468 mi += templateBg.getImage()
469
470 if not template.hasPsf():
471 raise pipeBase.TaskError("Template has no psf")
472
473 templateSources = butler.get(datasetType="src", dataId=templateId)
474 return pipeBase.Struct(exposure=template,
475 sources=templateSources)
476
477 def runDataRef(self, *args, **kwargs):
478 return self.runrun(*args, **kwargs)
479
480 def runQuantum(self, **kwargs):
481 raise NotImplementedError("Calexp template is not supported with gen3 middleware")
482
483
484class GetMultiTractCoaddTemplateConnections(pipeBase.PipelineTaskConnections,
485 dimensions=("instrument", "visit", "detector", "skymap"),
486 defaultTemplates={"coaddName": "goodSeeing",
487 "warpTypeSuffix": "",
488 "fakesType": ""}):
489 bbox = pipeBase.connectionTypes.Input(
490 doc="BBoxes of calexp used determine geometry of output template",
491 name="{fakesType}calexp.bbox",
492 storageClass="Box2I",
493 dimensions=("instrument", "visit", "detector"),
494 )
495 wcs = pipeBase.connectionTypes.Input(
496 doc="WCSs of calexps that we want to fetch the template for",
497 name="{fakesType}calexp.wcs",
498 storageClass="Wcs",
499 dimensions=("instrument", "visit", "detector"),
500 )
501 skyMap = pipeBase.connectionTypes.Input(
502 doc="Input definition of geometry/bbox and projection/wcs for template exposures",
503 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
504 dimensions=("skymap", ),
505 storageClass="SkyMap",
506 )
507 # TODO DM-31292: Add option to use global external wcs from jointcal
508 # Needed for DRP HSC
509 coaddExposures = pipeBase.connectionTypes.Input(
510 doc="Input template to match and subtract from the exposure",
511 dimensions=("tract", "patch", "skymap", "band"),
512 storageClass="ExposureF",
513 name="{fakesType}{coaddName}Coadd{warpTypeSuffix}",
514 multiple=True,
515 deferLoad=True
516 )
517 outputExposure = pipeBase.connectionTypes.Output(
518 doc="Warped template used to create `subtractedExposure`.",
519 dimensions=("instrument", "visit", "detector"),
520 storageClass="ExposureF",
521 name="{fakesType}{coaddName}Diff_templateExp{warpTypeSuffix}",
522 )
523
524
525class GetMultiTractCoaddTemplateConfig(pipeBase.PipelineTaskConfig, GetCoaddAsTemplateConfig,
526 pipelineConnections=GetMultiTractCoaddTemplateConnections):
527 warp = pexConfig.ConfigField(
528 dtype=afwMath.Warper.ConfigClass,
529 doc="warper configuration",
530 )
531 coaddPsf = pexConfig.ConfigField(
532 doc="Configuration for CoaddPsf",
533 dtype=CoaddPsfConfig,
534 )
535
536 def setDefaults(self):
537 self.warp.warpingKernelName = 'lanczos5'
538 self.coaddPsf.warpingKernelName = 'lanczos5'
539
540
541class GetMultiTractCoaddTemplateTask(pipeBase.PipelineTask):
542 ConfigClass = GetMultiTractCoaddTemplateConfig
543 _DefaultName = "getMultiTractCoaddTemplateTask"
544
545 def __init__(self, *args, **kwargs):
546 super().__init__(*args, **kwargs)
547 self.warper = afwMath.Warper.fromConfig(self.config.warp)
548
549 def runQuantum(self, butlerQC, inputRefs, outputRefs):
550 # Read in all inputs.
551 inputs = butlerQC.get(inputRefs)
552 inputs['coaddExposures'] = self.getOverlappingExposures(inputs)
553 # SkyMap only needed for filtering without
554 inputs.pop('skyMap')
555 outputs = self.run(**inputs)
556 butlerQC.put(outputs, outputRefs)
557
558 def getOverlappingExposures(self, inputs):
559 """Return list of coaddExposure DeferredDatasetHandles that overlap detector
560
561 The spatial index in the registry has generous padding and often supplies
562 patches near, but not directly overlapping the detector.
563 Filters inputs so that we don't have to read in all input coadds.
564
565 Parameters
566 ----------
567 inputs : `dict` of task Inputs
568
569 Returns
570 -------
571 coaddExposures : list of elements of type
572 `lsst.daf.butler.DeferredDatasetHandle` of
574
575 Raises
576 ------
577 NoWorkFound
578 Raised if no patches overlap the input detector bbox
579 """
580 # Check that the patches actually overlap the detector
581 # Exposure's validPolygon would be more accurate
582 detectorPolygon = geom.Box2D(inputs['bbox'])
583 overlappingArea = 0
584 coaddExposureList = []
585 for coaddRef in inputs['coaddExposures']:
586 dataId = coaddRef.dataId
587 patchWcs = inputs['skyMap'][dataId['tract']].getWcs()
588 patchBBox = inputs['skyMap'][dataId['tract']][dataId['patch']].getOuterBBox()
589 patchCorners = patchWcs.pixelToSky(geom.Box2D(patchBBox).getCorners())
590 patchPolygon = afwGeom.Polygon(inputs['wcs'].skyToPixel(patchCorners))
591 if patchPolygon.intersection(detectorPolygon):
592 overlappingArea += patchPolygon.intersectionSingle(detectorPolygon).calculateArea()
593 self.log.info("Using template input tract=%s, patch=%s" %
594 (dataId['tract'], dataId['patch']))
595 coaddExposureList.append(coaddRef)
596
597 if not overlappingArea:
598 raise pipeBase.NoWorkFound('No patches overlap detector')
599
600 return coaddExposureList
601
602 def run(self, coaddExposures, bbox, wcs):
603 """Warp coadds from multiple tracts to form a template for image diff.
604
605 Where the tracts overlap, the resulting template image is averaged.
606 The PSF on the template is created by combining the CoaddPsf on each
607 template image into a meta-CoaddPsf.
608
609 Parameters
610 ----------
611 coaddExposures: list of DeferredDatasetHandle to `lsst.afw.image.Exposure`
612 Coadds to be mosaicked
613 bbox : `lsst.geom.Box2I`
614 Template Bounding box of the detector geometry onto which to
615 resample the coaddExposures
617 Template WCS onto which to resample the coaddExposures
618
619 Returns
620 -------
621 result : `struct`
622 return a pipeBase.Struct:
623 - ``outputExposure`` : a template coadd exposure assembled out of patches
624
625
626 Raises
627 ------
628 NoWorkFound
629 Raised if no patches overlatp the input detector bbox
630
631 """
632 # Table for CoaddPSF
633 tractsSchema = afwTable.ExposureTable.makeMinimalSchema()
634 tractKey = tractsSchema.addField('tract', type=np.int32, doc='Which tract')
635 patchKey = tractsSchema.addField('patch', type=np.int32, doc='Which patch')
636 weightKey = tractsSchema.addField('weight', type=float, doc='Weight for each tract, should be 1')
637 tractsCatalog = afwTable.ExposureCatalog(tractsSchema)
638
639 finalWcs = wcs
640 bbox.grow(self.config.templateBorderSize)
641 finalBBox = bbox
642
643 nPatchesFound = 0
644 maskedImageList = []
645 weightList = []
646
647 for coaddExposure in coaddExposures:
648 coaddPatch = coaddExposure.get()
649
650 # warp to detector WCS
651 warped = self.warper.warpExposure(finalWcs, coaddPatch, maxBBox=finalBBox)
652
653 # Check if warped image is viable
654 if not np.any(np.isfinite(warped.image.array)):
655 self.log.info("No overlap for warped %s. Skipping" % coaddExposure.ref.dataId)
656 continue
657
658 exp = afwImage.ExposureF(finalBBox, finalWcs)
659 exp.maskedImage.set(np.nan, afwImage.Mask.getPlaneBitMask("NO_DATA"), np.nan)
660 exp.maskedImage.assign(warped.maskedImage, warped.getBBox())
661
662 maskedImageList.append(exp.maskedImage)
663 weightList.append(1)
664 record = tractsCatalog.addNew()
665 record.setPsf(coaddPatch.getPsf())
666 record.setWcs(coaddPatch.getWcs())
667 record.setPhotoCalib(coaddPatch.getPhotoCalib())
668 record.setBBox(coaddPatch.getBBox())
669 record.setValidPolygon(afwGeom.Polygon(geom.Box2D(coaddPatch.getBBox()).getCorners()))
670 record.set(tractKey, coaddExposure.ref.dataId['tract'])
671 record.set(patchKey, coaddExposure.ref.dataId['patch'])
672 record.set(weightKey, 1.)
673 nPatchesFound += 1
674
675 if nPatchesFound == 0:
676 raise pipeBase.NoWorkFound("No patches found to overlap detector")
677
678 # Combine images from individual patches together
679 statsFlags = afwMath.stringToStatisticsProperty('MEAN')
680 statsCtrl = afwMath.StatisticsControl()
681 statsCtrl.setNanSafe(True)
682 statsCtrl.setWeighted(True)
683 statsCtrl.setCalcErrorFromInputVariance(True)
684
685 templateExposure = afwImage.ExposureF(finalBBox, finalWcs)
686 templateExposure.maskedImage.set(np.nan, afwImage.Mask.getPlaneBitMask("NO_DATA"), np.nan)
687 xy0 = templateExposure.getXY0()
688 # Do not mask any values
689 templateExposure.maskedImage = afwMath.statisticsStack(maskedImageList, statsFlags, statsCtrl,
690 weightList, clipped=0, maskMap=[])
691 templateExposure.maskedImage.setXY0(xy0)
692
693 # CoaddPsf centroid not only must overlap image, but must overlap the part of
694 # image with data. Use centroid of region with data
695 boolmask = templateExposure.mask.array & templateExposure.mask.getPlaneBitMask('NO_DATA') == 0
696 maskx = afwImage.makeMaskFromArray(boolmask.astype(afwImage.MaskPixel))
697 centerCoord = afwGeom.SpanSet.fromMask(maskx, 1).computeCentroid()
698
699 ctrl = self.config.coaddPsf.makeControl()
700 coaddPsf = CoaddPsf(tractsCatalog, finalWcs, centerCoord, ctrl.warpingKernelName, ctrl.cacheSize)
701 if coaddPsf is None:
702 raise RuntimeError("CoaddPsf could not be constructed")
703
704 templateExposure.setPsf(coaddPsf)
705 templateExposure.setFilterLabel(coaddPatch.getFilterLabel())
706 templateExposure.setPhotoCalib(coaddPatch.getPhotoCalib())
707 return pipeBase.Struct(outputExposure=templateExposure)
def run(self, exposure, sensorRef, templateIdList)
Definition: getTemplate.py:426
def runDataRef(self, exposure, sensorRef, templateIdList=None)
Definition: getTemplate.py:104
def run(self, tractInfo, patchList, skyCorners, availableCoaddRefs, sensorRef=None, visitInfo=None)
Definition: getTemplate.py:264
def runQuantum(self, exposure, butlerQC, skyMapRef, coaddExposureRefs)
Definition: getTemplate.py:149
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 run(self, coaddExposures, bbox, wcs)
Definition: getTemplate.py:602