LSST Applications g0b6bd0c080+a72a5dd7e6,g1182afd7b4+2a019aa3bb,g17e5ecfddb+2b8207f7de,g1d67935e3f+06cf436103,g38293774b4+ac198e9f13,g396055baef+6a2097e274,g3b44f30a73+6611e0205b,g480783c3b1+98f8679e14,g48ccf36440+89c08d0516,g4b93dc025c+98f8679e14,g5c4744a4d9+a302e8c7f0,g613e996a0d+e1c447f2e0,g6c8d09e9e7+25247a063c,g7271f0639c+98f8679e14,g7a9cd813b8+124095ede6,g9d27549199+a302e8c7f0,ga1cf026fa3+ac198e9f13,ga32aa97882+7403ac30ac,ga786bb30fb+7a139211af,gaa63f70f4e+9994eb9896,gabf319e997+ade567573c,gba47b54d5d+94dc90c3ea,gbec6a3398f+06cf436103,gc6308e37c7+07dd123edb,gc655b1545f+ade567573c,gcc9029db3c+ab229f5caf,gd01420fc67+06cf436103,gd877ba84e5+06cf436103,gdb4cecd868+6f279b5b48,ge2d134c3d5+cc4dbb2e3f,ge448b5faa6+86d1ceac1d,gecc7e12556+98f8679e14,gf3ee170dca+25247a063c,gf4ac96e456+ade567573c,gf9f5ea5b4d+ac198e9f13,gff490e6085+8c2580be5c,w.2022.27
LSST Data Management Base Package
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 "GetTemplateTask", "GetTemplateConfig",
40 "GetDcrTemplateTask", "GetDcrTemplateConfig",
41 "GetMultiTractCoaddTemplateTask", "GetMultiTractCoaddTemplateConfig"]
42
43
44class GetCoaddAsTemplateConfig(pexConfig.Config):
45 templateBorderSize = pexConfig.Field(
46 dtype=int,
47 default=20,
48 doc="Number of pixels to grow the requested template image to account for warping"
49 )
50 coaddName = pexConfig.Field(
51 doc="coadd name: typically one of 'deep', 'goodSeeing', or 'dcr'",
52 dtype=str,
53 default="deep",
54 )
55 warpType = pexConfig.Field(
56 doc="Warp type of the coadd template: one of 'direct' or 'psfMatched'",
57 dtype=str,
58 default="direct",
59 )
60
61
62class GetCoaddAsTemplateTask(pipeBase.Task):
63 """Subtask to retrieve coadd for use as an image difference template.
64
65 This is the default getTemplate Task to be run as a subtask by
66 ``pipe.tasks.ImageDifferenceTask``. The main methods are ``run()`` and
67 ``runGen3()``.
68
69 Notes
70 -----
71 From the given skymap, the closest tract is selected; multiple tracts are
72 not supported. The assembled template inherits the WCS of the selected
73 skymap tract and the resolution of the template exposures. Overlapping box
74 regions of the input template patches are pixel by pixel copied into the
75 assembled template image. There is no warping or pixel resampling.
76
77 Pixels with no overlap of any available input patches are set to ``nan`` value
78 and ``NO_DATA`` flagged.
79 """
80
81 ConfigClass = GetCoaddAsTemplateConfig
82 _DefaultName = "GetCoaddAsTemplateTask"
83
84 def runDataRef(self, exposure, sensorRef, templateIdList=None):
85 """Gen2 task entry point. Retrieve and mosaic a template coadd exposure
86 that overlaps the science exposure.
87
88 Parameters
89 ----------
90 exposure: `lsst.afw.image.Exposure`
91 an exposure for which to generate an overlapping template
92 sensorRef : TYPE
93 a Butler data reference that can be used to obtain coadd data
94 templateIdList : TYPE, optional
95 list of data ids, unused here, in the case of coadd template
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 skyMap = sensorRef.get(datasetType=self.config.coaddName + "Coadd_skyMap")
105 tractInfo, patchList, skyCorners = self.getOverlapPatchListgetOverlapPatchList(exposure, skyMap)
106
107 availableCoaddRefs = dict()
108 for patchInfo in patchList:
109 patchNumber = tractInfo.getSequentialPatchIndex(patchInfo)
110 patchArgDict = dict(
111 datasetType=self.getCoaddDatasetNamegetCoaddDatasetName() + "_sub",
112 bbox=patchInfo.getOuterBBox(),
113 tract=tractInfo.getId(),
114 patch="%s,%s" % (patchInfo.getIndex()[0], patchInfo.getIndex()[1]),
115 )
116
117 if sensorRef.datasetExists(**patchArgDict):
118 self.log.info("Reading patch %s", patchArgDict)
119 availableCoaddRefs[patchNumber] = patchArgDict
120
121 templateExposure = self.runrun(
122 tractInfo, patchList, skyCorners, availableCoaddRefs,
123 sensorRef=sensorRef, visitInfo=exposure.getInfo().getVisitInfo()
124 )
125 return pipeBase.Struct(exposure=templateExposure, sources=None)
126
127 def runQuantum(self, exposure, butlerQC, skyMapRef, coaddExposureRefs):
128 """Gen3 task entry point. Retrieve and mosaic a template coadd exposure
129 that overlaps the science exposure.
130
131 Parameters
132 ----------
133 exposure : `lsst.afw.image.Exposure`
134 The science exposure to define the sky region of the template coadd.
135 butlerQC : `lsst.pipe.base.ButlerQuantumContext`
136 Butler like object that supports getting data by DatasetRef.
137 skyMapRef : `lsst.daf.butler.DatasetRef`
138 Reference to SkyMap object that corresponds to the template coadd.
139 coaddExposureRefs : iterable of `lsst.daf.butler.DeferredDatasetRef`
140 Iterable of references to the available template coadd patches.
141
142 Returns
143 -------
144 result : `lsst.pipe.base.Struct`
145 - ``exposure`` : `lsst.afw.image.ExposureF`
146 a template coadd exposure assembled out of patches
147 - ``sources`` : `None` for this subtask
148 """
149 self.log.warn("GetCoaddAsTemplateTask is deprecated. Use GetTemplateTask instead.")
150 skyMap = butlerQC.get(skyMapRef)
151 coaddExposureRefs = butlerQC.get(coaddExposureRefs)
152 tracts = [ref.dataId['tract'] for ref in coaddExposureRefs]
153 if tracts.count(tracts[0]) == len(tracts):
154 tractInfo = skyMap[tracts[0]]
155 else:
156 raise RuntimeError("Templates constructed from multiple Tracts not supported by this task. "
157 "Use GetTemplateTask instead.")
158
159 detectorBBox = exposure.getBBox()
160 detectorWcs = exposure.getWcs()
161 detectorCorners = detectorWcs.pixelToSky(geom.Box2D(detectorBBox).getCorners())
162 validPolygon = exposure.getInfo().getValidPolygon()
163 detectorPolygon = validPolygon if validPolygon else geom.Box2D(detectorBBox)
164
165 availableCoaddRefs = dict()
166 overlappingArea = 0
167 for coaddRef in coaddExposureRefs:
168 dataId = coaddRef.dataId
169 patchWcs = skyMap[dataId['tract']].getWcs()
170 patchBBox = skyMap[dataId['tract']][dataId['patch']].getOuterBBox()
171 patchCorners = patchWcs.pixelToSky(geom.Box2D(patchBBox).getCorners())
172 patchPolygon = afwGeom.Polygon(detectorWcs.skyToPixel(patchCorners))
173 if patchPolygon.intersection(detectorPolygon):
174 overlappingArea += patchPolygon.intersectionSingle(detectorPolygon).calculateArea()
175 if self.config.coaddName == 'dcr':
176 self.log.info("Using template input tract=%s, patch=%s, subfilter=%s",
177 dataId['tract'], dataId['patch'], dataId['subfilter'])
178 if dataId['patch'] in availableCoaddRefs:
179 availableCoaddRefs[dataId['patch']].append(coaddRef)
180 else:
181 availableCoaddRefs[dataId['patch']] = [coaddRef, ]
182 else:
183 self.log.info("Using template input tract=%s, patch=%s",
184 dataId['tract'], dataId['patch'])
185 availableCoaddRefs[dataId['patch']] = coaddRef
186
187 if overlappingArea == 0:
188 templateExposure = None
189 pixGood = 0
190 self.log.warning("No overlapping template patches found")
191 else:
192 patchList = [tractInfo[patch] for patch in availableCoaddRefs.keys()]
193 templateExposure = self.runrun(tractInfo, patchList, detectorCorners, availableCoaddRefs,
194 visitInfo=exposure.getInfo().getVisitInfo())
195 # Count the number of pixels with the NO_DATA mask bit set
196 # counting NaN pixels is insufficient because pixels without data are often intepolated over)
197 pixNoData = np.count_nonzero(templateExposure.mask.array
198 & templateExposure.mask.getPlaneBitMask('NO_DATA'))
199 pixGood = templateExposure.getBBox().getArea() - pixNoData
200 self.log.info("template has %d good pixels (%.1f%%)", pixGood,
201 100*pixGood/templateExposure.getBBox().getArea())
202 return pipeBase.Struct(exposure=templateExposure, sources=None, area=pixGood)
203
204 def getOverlapPatchList(self, exposure, skyMap):
205 """Select the relevant tract and its patches that overlap with the science exposure.
206
207 Parameters
208 ----------
209 exposure : `lsst.afw.image.Exposure`
210 The science exposure to define the sky region of the template coadd.
211
212 skyMap : `lsst.skymap.BaseSkyMap`
213 SkyMap object that corresponds to the template coadd.
214
215 Returns
216 -------
217 result : `tuple` of
218 - ``tractInfo`` : `lsst.skymap.TractInfo`
219 The selected tract.
220 - ``patchList`` : `list` of `lsst.skymap.PatchInfo`
221 List of all overlap patches of the selected tract.
222 - ``skyCorners`` : `list` of `lsst.geom.SpherePoint`
223 Corners of the exposure in the sky in the order given by `lsst.geom.Box2D.getCorners`.
224 """
225 expWcs = exposure.getWcs()
226 expBoxD = geom.Box2D(exposure.getBBox())
227 expBoxD.grow(self.config.templateBorderSize)
228 ctrSkyPos = expWcs.pixelToSky(expBoxD.getCenter())
229 tractInfo = skyMap.findTract(ctrSkyPos)
230 self.log.info("Using skyMap tract %s", tractInfo.getId())
231 skyCorners = [expWcs.pixelToSky(pixPos) for pixPos in expBoxD.getCorners()]
232 patchList = tractInfo.findPatchList(skyCorners)
233
234 if not patchList:
235 raise RuntimeError("No suitable tract found")
236
237 self.log.info("Assembling %d coadd patches", len(patchList))
238 self.log.info("exposure dimensions=%s", exposure.getDimensions())
239
240 return (tractInfo, patchList, skyCorners)
241
242 def run(self, tractInfo, patchList, skyCorners, availableCoaddRefs,
243 sensorRef=None, visitInfo=None):
244 """Gen2 and gen3 shared code: determination of exposure dimensions and
245 copying of pixels from overlapping patch regions.
246
247 Parameters
248 ----------
249 skyMap : `lsst.skymap.BaseSkyMap`
250 SkyMap object that corresponds to the template coadd.
251 tractInfo : `lsst.skymap.TractInfo`
252 The selected tract.
253 patchList : iterable of `lsst.skymap.patchInfo.PatchInfo`
254 Patches to consider for making the template exposure.
255 skyCorners : list of `lsst.geom.SpherePoint`
256 Sky corner coordinates to be covered by the template exposure.
257 availableCoaddRefs : `dict` [`int`]
258 Dictionary of spatially relevant retrieved coadd patches,
259 indexed by their sequential patch number. In Gen3 mode, values are
260 `lsst.daf.butler.DeferredDatasetHandle` and ``.get()`` is called,
261 in Gen2 mode, ``sensorRef.get(**coaddef)`` is called to retrieve the coadd.
262 sensorRef : `lsst.daf.persistence.ButlerDataRef`, Gen2 only
263 Butler data reference to get coadd data.
264 Must be `None` for Gen3.
265 visitInfo : `lsst.afw.image.VisitInfo`, Gen2 only
266 VisitInfo to make dcr model.
267
268 Returns
269 -------
270 templateExposure: `lsst.afw.image.ExposureF`
271 The created template exposure.
272 """
273 coaddWcs = tractInfo.getWcs()
274
275 # compute coadd bbox
276 coaddBBox = geom.Box2D()
277 for skyPos in skyCorners:
278 coaddBBox.include(coaddWcs.skyToPixel(skyPos))
279 coaddBBox = geom.Box2I(coaddBBox)
280 self.log.info("coadd dimensions=%s", coaddBBox.getDimensions())
281
282 coaddExposure = afwImage.ExposureF(coaddBBox, coaddWcs)
283 coaddExposure.maskedImage.set(np.nan, afwImage.Mask.getPlaneBitMask("NO_DATA"), np.nan)
284 nPatchesFound = 0
285 coaddFilterLabel = None
286 coaddPsf = None
287 coaddPhotoCalib = None
288 for patchInfo in patchList:
289 patchNumber = tractInfo.getSequentialPatchIndex(patchInfo)
290 patchSubBBox = patchInfo.getOuterBBox()
291 patchSubBBox.clip(coaddBBox)
292 if patchNumber not in availableCoaddRefs:
293 self.log.warning("skip patch=%d; patch does not exist for this coadd", patchNumber)
294 continue
295 if patchSubBBox.isEmpty():
296 if isinstance(availableCoaddRefs[patchNumber], DeferredDatasetHandle):
297 tract = availableCoaddRefs[patchNumber].dataId['tract']
298 else:
299 tract = availableCoaddRefs[patchNumber]['tract']
300 self.log.info("skip tract=%d patch=%d; no overlapping pixels", tract, patchNumber)
301 continue
302
303 if self.config.coaddName == 'dcr':
304 patchInnerBBox = patchInfo.getInnerBBox()
305 patchInnerBBox.clip(coaddBBox)
306 if np.min(patchInnerBBox.getDimensions()) <= 2*self.config.templateBorderSize:
307 self.log.info("skip tract=%(tract)s, patch=%(patch)s; too few pixels.",
308 availableCoaddRefs[patchNumber])
309 continue
310 self.log.info("Constructing DCR-matched template for patch %s",
311 availableCoaddRefs[patchNumber])
312
313 dcrModel = DcrModel.fromQuantum(availableCoaddRefs[patchNumber],
314 self.config.effectiveWavelength,
315 self.config.bandwidth)
316 # The edge pixels of the DcrCoadd may contain artifacts due to missing data.
317 # Each patch has significant overlap, and the contaminated edge pixels in
318 # a new patch will overwrite good pixels in the overlap region from
319 # previous patches.
320 # Shrink the BBox to remove the contaminated pixels,
321 # but make sure it is only the overlap region that is reduced.
322 dcrBBox = geom.Box2I(patchSubBBox)
323 dcrBBox.grow(-self.config.templateBorderSize)
324 dcrBBox.include(patchInnerBBox)
325 coaddPatch = dcrModel.buildMatchedExposure(bbox=dcrBBox,
326 visitInfo=visitInfo)
327 else:
328 if sensorRef is None:
329 # Gen3
330 coaddPatch = availableCoaddRefs[patchNumber].get()
331 else:
332 # Gen2
333 coaddPatch = sensorRef.get(**availableCoaddRefs[patchNumber])
334 nPatchesFound += 1
335
336 # Gen2 get() seems to clip based on bbox kwarg but we removed bbox
337 # calculation from caller code. Gen3 also does not do this.
338 overlapBox = coaddPatch.getBBox()
339 overlapBox.clip(coaddBBox)
340 coaddExposure.maskedImage.assign(coaddPatch.maskedImage[overlapBox], overlapBox)
341
342 if coaddFilterLabel is None:
343 coaddFilterLabel = coaddPatch.getFilter()
344
345 # Retrieve the PSF for this coadd tract, if not already retrieved
346 if coaddPsf is None and coaddPatch.hasPsf():
347 coaddPsf = coaddPatch.getPsf()
348
349 # Retrieve the calibration for this coadd tract, if not already retrieved
350 if coaddPhotoCalib is None:
351 coaddPhotoCalib = coaddPatch.getPhotoCalib()
352
353 if coaddPhotoCalib is None:
354 raise RuntimeError("No coadd PhotoCalib found!")
355 if nPatchesFound == 0:
356 raise RuntimeError("No patches found!")
357 if coaddPsf is None:
358 raise RuntimeError("No coadd Psf found!")
359
360 coaddExposure.setPhotoCalib(coaddPhotoCalib)
361 coaddExposure.setPsf(coaddPsf)
362 coaddExposure.setFilter(coaddFilterLabel)
363 return coaddExposure
364
366 """Return coadd name for given task config
367
368 Returns
369 -------
370 CoaddDatasetName : `string`
371
372 TODO: This nearly duplicates a method in CoaddBaseTask (DM-11985)
373 """
374 warpType = self.config.warpType
375 suffix = "" if warpType == "direct" else warpType[0].upper() + warpType[1:]
376 return self.config.coaddName + "Coadd" + suffix
377
378
379class GetCalexpAsTemplateConfig(pexConfig.Config):
380 doAddCalexpBackground = pexConfig.Field(
381 dtype=bool,
382 default=True,
383 doc="Add background to calexp before processing it."
384 )
385
386
387class GetCalexpAsTemplateTask(pipeBase.Task):
388 """Subtask to retrieve calexp of the same ccd number as the science image SensorRef
389 for use as an image difference template. Only gen2 supported.
390
391 To be run as a subtask by pipe.tasks.ImageDifferenceTask.
392 Intended for use with simulations and surveys that repeatedly visit the same pointing.
393 This code was originally part of Winter2013ImageDifferenceTask.
394 """
395
396 ConfigClass = GetCalexpAsTemplateConfig
397 _DefaultName = "GetCalexpAsTemplateTask"
398
399 def run(self, exposure, sensorRef, templateIdList):
400 """Return a calexp exposure with based on input sensorRef.
401
402 Construct a dataId based on the sensorRef.dataId combined
403 with the specifications from the first dataId in templateIdList
404
405 Parameters
406 ----------
407 exposure : `lsst.afw.image.Exposure`
408 exposure (unused)
409 sensorRef : `list` of `lsst.daf.persistence.ButlerDataRef`
410 Data reference of the calexp(s) to subtract from.
411 templateIdList : `list` of `lsst.daf.persistence.ButlerDataRef`
412 Data reference of the template calexp to be subtraced.
413 Can be incomplete, fields are initialized from `sensorRef`.
414 If there are multiple items, only the first one is used.
415
416 Returns
417 -------
418 result : `struct`
419
420 return a pipeBase.Struct:
421
422 - ``exposure`` : a template calexp
423 - ``sources`` : source catalog measured on the template
424 """
425
426 if len(templateIdList) == 0:
427 raise RuntimeError("No template data reference supplied.")
428 if len(templateIdList) > 1:
429 self.log.warning("Multiple template data references supplied. Using the first one only.")
430
431 templateId = sensorRef.dataId.copy()
432 templateId.update(templateIdList[0])
433
434 self.log.info("Fetching calexp (%s) as template.", templateId)
435
436 butler = sensorRef.getButler()
437 template = butler.get(datasetType="calexp", dataId=templateId)
438 if self.config.doAddCalexpBackground:
439 templateBg = butler.get(datasetType="calexpBackground", dataId=templateId)
440 mi = template.getMaskedImage()
441 mi += templateBg.getImage()
442
443 if not template.hasPsf():
444 raise pipeBase.TaskError("Template has no psf")
445
446 templateSources = butler.get(datasetType="src", dataId=templateId)
447 return pipeBase.Struct(exposure=template,
448 sources=templateSources)
449
450 def runDataRef(self, *args, **kwargs):
451 return self.runrun(*args, **kwargs)
452
453 def runQuantum(self, **kwargs):
454 raise NotImplementedError("Calexp template is not supported with gen3 middleware")
455
456
457class GetTemplateConnections(pipeBase.PipelineTaskConnections,
458 dimensions=("instrument", "visit", "detector", "skymap"),
459 defaultTemplates={"coaddName": "goodSeeing",
460 "warpTypeSuffix": "",
461 "fakesType": ""}):
462 bbox = pipeBase.connectionTypes.Input(
463 doc="BBoxes of calexp used determine geometry of output template",
464 name="{fakesType}calexp.bbox",
465 storageClass="Box2I",
466 dimensions=("instrument", "visit", "detector"),
467 )
468 wcs = pipeBase.connectionTypes.Input(
469 doc="WCS of the calexp that we want to fetch the template for",
470 name="{fakesType}calexp.wcs",
471 storageClass="Wcs",
472 dimensions=("instrument", "visit", "detector"),
473 )
474 skyMap = pipeBase.connectionTypes.Input(
475 doc="Input definition of geometry/bbox and projection/wcs for template exposures",
476 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
477 dimensions=("skymap", ),
478 storageClass="SkyMap",
479 )
480 # TODO DM-31292: Add option to use global external wcs from jointcal
481 # Needed for DRP HSC
482 coaddExposures = pipeBase.connectionTypes.Input(
483 doc="Input template to match and subtract from the exposure",
484 dimensions=("tract", "patch", "skymap", "band"),
485 storageClass="ExposureF",
486 name="{fakesType}{coaddName}Coadd{warpTypeSuffix}",
487 multiple=True,
488 deferLoad=True
489 )
490 outputExposure = pipeBase.connectionTypes.Output(
491 doc="Warped template used to create `subtractedExposure`.",
492 dimensions=("instrument", "visit", "detector"),
493 storageClass="ExposureF",
494 name="{fakesType}{coaddName}Diff_templateExp{warpTypeSuffix}",
495 )
496
497
498class GetTemplateConfig(pipeBase.PipelineTaskConfig,
499 pipelineConnections=GetTemplateConnections):
500 templateBorderSize = pexConfig.Field(
501 dtype=int,
502 default=20,
503 doc="Number of pixels to grow the requested template image to account for warping"
504 )
505 warp = pexConfig.ConfigField(
506 dtype=afwMath.Warper.ConfigClass,
507 doc="warper configuration",
508 )
509 coaddPsf = pexConfig.ConfigField(
510 doc="Configuration for CoaddPsf",
511 dtype=CoaddPsfConfig,
512 )
513
514 def setDefaults(self):
515 self.warp.warpingKernelName = 'lanczos5'
516 self.coaddPsf.warpingKernelName = 'lanczos5'
517
518
519class GetTemplateTask(pipeBase.PipelineTask):
520 ConfigClass = GetTemplateConfig
521 _DefaultName = "getTemplate"
522
523 def __init__(self, *args, **kwargs):
524 super().__init__(*args, **kwargs)
525 self.warper = afwMath.Warper.fromConfig(self.config.warp)
526
527 def runQuantum(self, butlerQC, inputRefs, outputRefs):
528 # Read in all inputs.
529 inputs = butlerQC.get(inputRefs)
530 results = self.getOverlappingExposures(inputs)
531 inputs["coaddExposures"] = results.coaddExposures
532 inputs["dataIds"] = results.dataIds
533 outputs = self.run(**inputs)
534 butlerQC.put(outputs, outputRefs)
535
536 def getOverlappingExposures(self, inputs):
537 """Return lists of coadds and their corresponding dataIds that overlap the detector.
538
539 The spatial index in the registry has generous padding and often supplies
540 patches near, but not directly overlapping the detector.
541 Filters inputs so that we don't have to read in all input coadds.
542
543 Parameters
544 ----------
545 inputs : `dict` of task Inputs, containing:
546 - coaddExposureRefs : list of elements of type
547 `lsst.daf.butler.DeferredDatasetHandle` of
549 Data references to exposures that might overlap the detector.
550 - bbox : `lsst.geom.Box2I`
551 Template Bounding box of the detector geometry onto which to
552 resample the coaddExposures
553 - skyMap : `lsst.skymap.SkyMap`
554 Input definition of geometry/bbox and projection/wcs for template exposures
555 - wcs : `lsst.afw.geom.SkyWcs`
556 Template WCS onto which to resample the coaddExposures
557
558 Returns
559 -------
560 result : `lsst.pipe.base.Struct` containing these fields:
561 - coaddExposures : `list` of elements of type `lsst.afw.image.Exposure`
562 Coadd exposures that overlap the detector.
563 - dataIds : `list` of `lsst.daf.butler.DataCoordinate`
564 Data IDs of the coadd exposures that overlap the detector.
565
566 Raises
567 ------
568 NoWorkFound
569 Raised if no patches overlap the input detector bbox
570 """
571 # Check that the patches actually overlap the detector
572 # Exposure's validPolygon would be more accurate
573 detectorPolygon = geom.Box2D(inputs['bbox'])
574 overlappingArea = 0
575 coaddExposureList = []
576 dataIds = []
577 for coaddRef in inputs['coaddExposures']:
578 dataId = coaddRef.dataId
579 patchWcs = inputs['skyMap'][dataId['tract']].getWcs()
580 patchBBox = inputs['skyMap'][dataId['tract']][dataId['patch']].getOuterBBox()
581 patchCorners = patchWcs.pixelToSky(geom.Box2D(patchBBox).getCorners())
582 patchPolygon = afwGeom.Polygon(inputs['wcs'].skyToPixel(patchCorners))
583 if patchPolygon.intersection(detectorPolygon):
584 overlappingArea += patchPolygon.intersectionSingle(detectorPolygon).calculateArea()
585 self.log.info("Using template input tract=%s, patch=%s" %
586 (dataId['tract'], dataId['patch']))
587 coaddExposureList.append(coaddRef.get())
588 dataIds.append(dataId)
589
590 if not overlappingArea:
591 raise pipeBase.NoWorkFound('No patches overlap detector')
592
593 return pipeBase.Struct(coaddExposures=coaddExposureList,
594 dataIds=dataIds)
595
596 def run(self, coaddExposures, bbox, wcs, dataIds, **kwargs):
597 """Warp coadds from multiple tracts to form a template for image diff.
598
599 Where the tracts overlap, the resulting template image is averaged.
600 The PSF on the template is created by combining the CoaddPsf on each
601 template image into a meta-CoaddPsf.
602
603 Parameters
604 ----------
605 coaddExposures : `list` of `lsst.afw.image.Exposure`
606 Coadds to be mosaicked
607 bbox : `lsst.geom.Box2I`
608 Template Bounding box of the detector geometry onto which to
609 resample the coaddExposures
611 Template WCS onto which to resample the coaddExposures
612 dataIds : `list` of `lsst.daf.butler.DataCoordinate`
613 Record of the tract and patch of each coaddExposure.
614 **kwargs
615 Any additional keyword parameters.
616
617 Returns
618 -------
619 result : `lsst.pipe.base.Struct` containing
620 - ``outputExposure`` : a template coadd exposure assembled out of patches
621 """
622 # Table for CoaddPSF
623 tractsSchema = afwTable.ExposureTable.makeMinimalSchema()
624 tractKey = tractsSchema.addField('tract', type=np.int32, doc='Which tract')
625 patchKey = tractsSchema.addField('patch', type=np.int32, doc='Which patch')
626 weightKey = tractsSchema.addField('weight', type=float, doc='Weight for each tract, should be 1')
627 tractsCatalog = afwTable.ExposureCatalog(tractsSchema)
628
629 finalWcs = wcs
630 bbox.grow(self.config.templateBorderSize)
631 finalBBox = bbox
632
633 nPatchesFound = 0
634 maskedImageList = []
635 weightList = []
636
637 for coaddExposure, dataId in zip(coaddExposures, dataIds):
638
639 # warp to detector WCS
640 warped = self.warper.warpExposure(finalWcs, coaddExposure, maxBBox=finalBBox)
641
642 # Check if warped image is viable
643 if not np.any(np.isfinite(warped.image.array)):
644 self.log.info("No overlap for warped %s. Skipping" % dataId)
645 continue
646
647 exp = afwImage.ExposureF(finalBBox, finalWcs)
648 exp.maskedImage.set(np.nan, afwImage.Mask.getPlaneBitMask("NO_DATA"), np.nan)
649 exp.maskedImage.assign(warped.maskedImage, warped.getBBox())
650
651 maskedImageList.append(exp.maskedImage)
652 weightList.append(1)
653 record = tractsCatalog.addNew()
654 record.setPsf(coaddExposure.getPsf())
655 record.setWcs(coaddExposure.getWcs())
656 record.setPhotoCalib(coaddExposure.getPhotoCalib())
657 record.setBBox(coaddExposure.getBBox())
658 record.setValidPolygon(afwGeom.Polygon(geom.Box2D(coaddExposure.getBBox()).getCorners()))
659 record.set(tractKey, dataId['tract'])
660 record.set(patchKey, dataId['patch'])
661 record.set(weightKey, 1.)
662 nPatchesFound += 1
663
664 if nPatchesFound == 0:
665 raise pipeBase.NoWorkFound("No patches found to overlap detector")
666
667 # Combine images from individual patches together
668 statsFlags = afwMath.stringToStatisticsProperty('MEAN')
669 statsCtrl = afwMath.StatisticsControl()
670 statsCtrl.setNanSafe(True)
671 statsCtrl.setWeighted(True)
672 statsCtrl.setCalcErrorFromInputVariance(True)
673
674 templateExposure = afwImage.ExposureF(finalBBox, finalWcs)
675 templateExposure.maskedImage.set(np.nan, afwImage.Mask.getPlaneBitMask("NO_DATA"), np.nan)
676 xy0 = templateExposure.getXY0()
677 # Do not mask any values
678 templateExposure.maskedImage = afwMath.statisticsStack(maskedImageList, statsFlags, statsCtrl,
679 weightList, clipped=0, maskMap=[])
680 templateExposure.maskedImage.setXY0(xy0)
681
682 # CoaddPsf centroid not only must overlap image, but must overlap the part of
683 # image with data. Use centroid of region with data
684 boolmask = templateExposure.mask.array & templateExposure.mask.getPlaneBitMask('NO_DATA') == 0
685 maskx = afwImage.makeMaskFromArray(boolmask.astype(afwImage.MaskPixel))
686 centerCoord = afwGeom.SpanSet.fromMask(maskx, 1).computeCentroid()
687
688 ctrl = self.config.coaddPsf.makeControl()
689 coaddPsf = CoaddPsf(tractsCatalog, finalWcs, centerCoord, ctrl.warpingKernelName, ctrl.cacheSize)
690 if coaddPsf is None:
691 raise RuntimeError("CoaddPsf could not be constructed")
692
693 templateExposure.setPsf(coaddPsf)
694 templateExposure.setFilter(coaddExposure.getFilter())
695 templateExposure.setPhotoCalib(coaddExposure.getPhotoCalib())
696 return pipeBase.Struct(outputExposure=templateExposure)
697
698
700 dimensions=("instrument", "visit", "detector", "skymap"),
701 defaultTemplates={"coaddName": "dcr",
702 "warpTypeSuffix": "",
703 "fakesType": ""}):
704 visitInfo = pipeBase.connectionTypes.Input(
705 doc="VisitInfo of calexp used to determine observing conditions.",
706 name="{fakesType}calexp.visitInfo",
707 storageClass="VisitInfo",
708 dimensions=("instrument", "visit", "detector"),
709 )
710 dcrCoadds = pipeBase.connectionTypes.Input(
711 doc="Input DCR template to match and subtract from the exposure",
712 name="{fakesType}dcrCoadd{warpTypeSuffix}",
713 storageClass="ExposureF",
714 dimensions=("tract", "patch", "skymap", "band", "subfilter"),
715 multiple=True,
716 deferLoad=True
717 )
718
719 def __init__(self, *, config=None):
720 super().__init__(config=config)
721 self.inputs.remove("coaddExposures")
722
723
724class GetDcrTemplateConfig(GetTemplateConfig,
725 pipelineConnections=GetDcrTemplateConnections):
726 numSubfilters = pexConfig.Field(
727 doc="Number of subfilters in the DcrCoadd.",
728 dtype=int,
729 default=3,
730 )
731 effectiveWavelength = pexConfig.Field(
732 doc="Effective wavelength of the filter.",
733 optional=False,
734 dtype=float,
735 )
736 bandwidth = pexConfig.Field(
737 doc="Bandwidth of the physical filter.",
738 optional=False,
739 dtype=float,
740 )
741
742 def validate(self):
743 if self.effectiveWavelength is None or self.bandwidth is None:
744 raise ValueError("The effective wavelength and bandwidth of the physical filter "
745 "must be set in the getTemplate config for DCR coadds. "
746 "Required until transmission curves are used in DM-13668.")
747
748
749class GetDcrTemplateTask(GetTemplateTask):
750 ConfigClass = GetDcrTemplateConfig
751 _DefaultName = "getDcrTemplate"
752
753 def getOverlappingExposures(self, inputs):
754 """Return lists of coadds and their corresponding dataIds that overlap the detector.
755
756 The spatial index in the registry has generous padding and often supplies
757 patches near, but not directly overlapping the detector.
758 Filters inputs so that we don't have to read in all input coadds.
759
760 Parameters
761 ----------
762 inputs : `dict` of task Inputs, containing:
763 - coaddExposureRefs : `list` of elements of type
764 `lsst.daf.butler.DeferredDatasetHandle` of
766 Data references to exposures that might overlap the detector.
767 - bbox : `lsst.geom.Box2I`
768 Template Bounding box of the detector geometry onto which to
769 resample the coaddExposures
770 - skyMap : `lsst.skymap.SkyMap`
771 Input definition of geometry/bbox and projection/wcs for template exposures
772 - wcs : `lsst.afw.geom.SkyWcs`
773 Template WCS onto which to resample the coaddExposures
774 - visitInfo : `lsst.afw.image.VisitInfo`
775 Metadata for the science image.
776
777 Returns
778 -------
779 result : `lsst.pipe.base.Struct` containing these fields:
780 - coaddExposures : `list` of elements of type `lsst.afw.image.Exposure`
781 Coadd exposures that overlap the detector.
782 - dataIds : `list` of `lsst.daf.butler.DataCoordinate`
783 Data IDs of the coadd exposures that overlap the detector.
784
785 Raises
786 ------
787 NoWorkFound
788 Raised if no patches overlatp the input detector bbox
789 """
790 # Check that the patches actually overlap the detector
791 # Exposure's validPolygon would be more accurate
792 detectorPolygon = geom.Box2D(inputs["bbox"])
793 overlappingArea = 0
794 coaddExposureRefList = []
795 dataIds = []
796 patchList = dict()
797 for coaddRef in inputs["dcrCoadds"]:
798 dataId = coaddRef.dataId
799 patchWcs = inputs["skyMap"][dataId['tract']].getWcs()
800 patchBBox = inputs["skyMap"][dataId['tract']][dataId['patch']].getOuterBBox()
801 patchCorners = patchWcs.pixelToSky(geom.Box2D(patchBBox).getCorners())
802 patchPolygon = afwGeom.Polygon(inputs["wcs"].skyToPixel(patchCorners))
803 if patchPolygon.intersection(detectorPolygon):
804 overlappingArea += patchPolygon.intersectionSingle(detectorPolygon).calculateArea()
805 self.log.info("Using template input tract=%s, patch=%s, subfilter=%s" %
806 (dataId['tract'], dataId['patch'], dataId["subfilter"]))
807 coaddExposureRefList.append(coaddRef)
808 if dataId['tract'] in patchList:
809 patchList[dataId['tract']].append(dataId['patch'])
810 else:
811 patchList[dataId['tract']] = [dataId['patch'], ]
812 dataIds.append(dataId)
813
814 if not overlappingArea:
815 raise pipeBase.NoWorkFound('No patches overlap detector')
816
817 self.checkPatchList(patchList)
818
819 coaddExposures = self.getDcrModel(patchList, inputs['dcrCoadds'], inputs['visitInfo'])
820 return pipeBase.Struct(coaddExposures=coaddExposures,
821 dataIds=dataIds)
822
823 def checkPatchList(self, patchList):
824 """Check that all of the DcrModel subfilters are present for each patch.
825
826 Parameters
827 ----------
828 patchList : `dict`
829 Dict of the patches containing valid data for each tract
830
831 Raises
832 ------
833 RuntimeError
834 If the number of exposures found for a patch does not match the number of subfilters.
835 """
836 for tract in patchList:
837 for patch in set(patchList[tract]):
838 if patchList[tract].count(patch) != self.config.numSubfilters:
839 raise RuntimeError("Invalid number of DcrModel subfilters found: %d vs %d expected",
840 patchList[tract].count(patch), self.config.numSubfilters)
841
842 def getDcrModel(self, patchList, coaddRefs, visitInfo):
843 """Build DCR-matched coadds from a list of exposure references.
844
845 Parameters
846 ----------
847 patchList : `dict`
848 Dict of the patches containing valid data for each tract
849 coaddRefs : `list` of elements of type
850 `lsst.daf.butler.DeferredDatasetHandle` of
852 Data references to DcrModels that overlap the detector.
853 visitInfo : `lsst.afw.image.VisitInfo`
854 Metadata for the science image.
855
856 Returns
857 -------
858 `list` of elements of type `lsst.afw.image.Exposure`
859 Coadd exposures that overlap the detector.
860 """
861 coaddExposureList = []
862 for tract in patchList:
863 for patch in set(patchList[tract]):
864 coaddRefList = [coaddRef for coaddRef in coaddRefs
865 if _selectDataRef(coaddRef, tract, patch)]
866
867 dcrModel = DcrModel.fromQuantum(coaddRefList,
868 self.config.effectiveWavelength,
869 self.config.bandwidth,
870 self.config.numSubfilters)
871 coaddExposureList.append(dcrModel.buildMatchedExposure(visitInfo=visitInfo))
872 return coaddExposureList
873
874
875def _selectDataRef(coaddRef, tract, patch):
876 condition = (coaddRef.dataId['tract'] == tract) & (coaddRef.dataId['patch'] == patch)
877 return condition
878
879
880class GetMultiTractCoaddTemplateConfig(GetTemplateConfig):
881 pass
882
883
884class GetMultiTractCoaddTemplateTask(GetTemplateTask):
885 ConfigClass = GetMultiTractCoaddTemplateConfig
886 _DefaultName = "getMultiTractCoaddTemplate"
887
888 def __init__(self, *args, **kwargs):
889 super().__init__(*args, **kwargs)
890 self.log.warn("GetMultiTractCoaddTemplateTask is deprecated. Use GetTemplateTask instead.")
A 2-dimensional celestial WCS that transform pixels to ICRS RA/Dec, using the LSST standard for pixel...
Definition: SkyWcs.h:117
Cartesian polygons.
Definition: Polygon.h:59
A class to contain the data, WCS, and other information needed to describe an image of the sky.
Definition: Exposure.h:72
Information about a single exposure of an imaging camera.
Definition: VisitInfo.h:68
Pass parameters to a Statistics object.
Definition: Statistics.h:92
Custom catalog class for ExposureRecord/Table.
Definition: Exposure.h:311
A floating-point coordinate rectangle geometry.
Definition: Box.h:413
An integer coordinate rectangle.
Definition: Box.h:55
Point in an unspecified spherical coordinate system.
Definition: SpherePoint.h:57
def run(self, exposure, sensorRef, templateIdList)
Definition: getTemplate.py:399
def runDataRef(self, exposure, sensorRef, templateIdList=None)
Definition: getTemplate.py:84
def run(self, tractInfo, patchList, skyCorners, availableCoaddRefs, sensorRef=None, visitInfo=None)
Definition: getTemplate.py:243
def runQuantum(self, exposure, butlerQC, skyMapRef, coaddExposureRefs)
Definition: getTemplate.py:127
CoaddPsf is the Psf derived to be used for non-PSF-matched Coadd images.
Definition: CoaddPsf.h:58
daf::base::PropertySet * set
Definition: fits.cc:912
std::shared_ptr< FrameSet > append(FrameSet const &first, FrameSet const &second)
Construct a FrameSet that performs two transformations in series.
Definition: functional.cc:33
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects.
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))
A function to compute some statistics of a stack of Images.
Property stringToStatisticsProperty(std::string const property)
Conversion function to switch a string to a Property (see Statistics.h)
Definition: Statistics.cc:738
int warpExposure(DestExposureT &destExposure, SrcExposureT const &srcExposure, WarpingControl const &control, typename DestExposureT::MaskedImageT::SinglePixel padValue=lsst::afw::math::edgePixel< typename DestExposureT::MaskedImageT >(typename lsst::afw::image::detail::image_traits< typename DestExposureT::MaskedImageT >::image_category()))
Warp (remap) one exposure to another.
def checkPatchList(self, patchList)
Definition: getTemplate.py:823
def getDcrModel(self, patchList, coaddRefs, visitInfo)
Definition: getTemplate.py:842
def run(self, coaddExposures, bbox, wcs, dataIds, **kwargs)
Definition: getTemplate.py:596
Fit spatial kernel using approximate fluxes for candidates, and solving a linear system of equations.