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