Coverage for python/lsst/ip/diffim/getTemplate.py: 16%
315 statements
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-11 03:56 -0700
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-11 03:56 -0700
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
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
35__all__ = ["GetCoaddAsTemplateTask", "GetCoaddAsTemplateConfig",
36 "GetTemplateTask", "GetTemplateConfig",
37 "GetDcrTemplateTask", "GetDcrTemplateConfig",
38 "GetMultiTractCoaddTemplateTask", "GetMultiTractCoaddTemplateConfig"]
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 )
59class GetCoaddAsTemplateTask(pipeBase.Task):
60 """Subtask to retrieve coadd for use as an image difference template.
62 This is the default getTemplate Task to be run as a subtask by
63 ``pipe.tasks.ImageDifferenceTask``.
65 """
67 ConfigClass = GetCoaddAsTemplateConfig
68 _DefaultName = "GetCoaddAsTemplateTask"
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.
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.
86 Returns
87 -------
88 result : `lsst.pipe.base.Struct`
89 A struct with attibutes:
91 ``exposure``
92 Template coadd exposure assembled out of patches
93 (`lsst.afw.image.ExposureF`).
94 ``sources``
95 Always `None` for this subtask.
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.")
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)
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
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)
159 def getOverlapPatchList(self, exposure, skyMap):
160 """Select the relevant tract and its patches that overlap with the
161 science exposure.
163 Parameters
164 ----------
165 exposure : `lsst.afw.image.Exposure`
166 The science exposure to define the sky region of the template
167 coadd.
169 skyMap : `lsst.skymap.BaseSkyMap`
170 SkyMap object that corresponds to the template coadd.
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)
192 if not patchList:
193 raise RuntimeError("No suitable tract found")
195 self.log.info("Assembling %d coadd patches", len(patchList))
196 self.log.info("exposure dimensions=%s", exposure.getDimensions())
198 return (tractInfo, patchList, skyCorners)
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.
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.
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()
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())
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
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])
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()
289 nPatchesFound += 1
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)
297 if coaddFilterLabel is None:
298 coaddFilterLabel = coaddPatch.getFilter()
300 # Retrieve the PSF for this coadd tract, if not already retrieved.
301 if coaddPsf is None and coaddPatch.hasPsf():
302 coaddPsf = coaddPatch.getPsf()
304 # Retrieve the calibration for this coadd tract, if not already
305 # retrieved>
306 if coaddPhotoCalib is None:
307 coaddPhotoCalib = coaddPatch.getPhotoCalib()
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!")
316 coaddExposure.setPhotoCalib(coaddPhotoCalib)
317 coaddExposure.setPsf(coaddPsf)
318 coaddExposure.setFilter(coaddFilterLabel)
319 return coaddExposure
321 def getCoaddDatasetName(self):
322 """Return coadd name for given task config
324 Returns
325 -------
326 CoaddDatasetName : `string`
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
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 )
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 )
392 def setDefaults(self):
393 self.warp.warpingKernelName = 'lanczos5'
394 self.coaddPsf.warpingKernelName = 'lanczos5'
397class GetTemplateTask(pipeBase.PipelineTask):
398 ConfigClass = GetTemplateConfig
399 _DefaultName = "getTemplate"
401 def __init__(self, *args, **kwargs):
402 super().__init__(*args, **kwargs)
403 self.warper = afwMath.Warper.fromConfig(self.config.warp)
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 inputs["physical_filter"] = butlerQC.quantum.dataId["physical_filter"]
412 outputs = self.run(**inputs)
413 butlerQC.put(outputs, outputRefs)
415 def getOverlappingExposures(self, inputs):
416 """Return lists of coadds and their corresponding dataIds that overlap
417 the detector.
419 The spatial index in the registry has generous padding and often
420 supplies patches near, but not directly overlapping the detector.
421 Filters inputs so that we don't have to read in all input coadds.
423 Parameters
424 ----------
425 inputs : `dict` of task Inputs, containing:
426 - coaddExposureRefs : `list`
427 [`lsst.daf.butler.DeferredDatasetHandle` of
428 `lsst.afw.image.Exposure`]
429 Data references to exposures that might overlap the detector.
430 - bbox : `lsst.geom.Box2I`
431 Template Bounding box of the detector geometry onto which to
432 resample the coaddExposures.
433 - skyMap : `lsst.skymap.SkyMap`
434 Input definition of geometry/bbox and projection/wcs for
435 template exposures.
436 - wcs : `lsst.afw.geom.SkyWcs`
437 Template WCS onto which to resample the coaddExposures.
439 Returns
440 -------
441 result : `lsst.pipe.base.Struct`
442 A struct with attributes:
444 ``coaddExposures``
445 List of Coadd exposures that overlap the detector (`list`
446 [`lsst.afw.image.Exposure`]).
447 ``dataIds``
448 List of data IDs of the coadd exposures that overlap the
449 detector (`list` [`lsst.daf.butler.DataCoordinate`]).
451 Raises
452 ------
453 NoWorkFound
454 Raised if no patches overlap the input detector bbox.
455 """
456 # Check that the patches actually overlap the detector
457 # Exposure's validPolygon would be more accurate
458 detectorPolygon = geom.Box2D(inputs['bbox'])
459 overlappingArea = 0
460 coaddExposureList = []
461 dataIds = []
462 for coaddRef in inputs['coaddExposures']:
463 dataId = coaddRef.dataId
464 patchWcs = inputs['skyMap'][dataId['tract']].getWcs()
465 patchBBox = inputs['skyMap'][dataId['tract']][dataId['patch']].getOuterBBox()
466 patchCorners = patchWcs.pixelToSky(geom.Box2D(patchBBox).getCorners())
467 inputsWcs = inputs['wcs']
468 if inputsWcs is not None:
469 patchPolygon = afwGeom.Polygon(inputsWcs.skyToPixel(patchCorners))
470 if patchPolygon.intersection(detectorPolygon):
471 overlappingArea += patchPolygon.intersectionSingle(detectorPolygon).calculateArea()
472 self.log.info("Using template input tract=%s, patch=%s" %
473 (dataId['tract'], dataId['patch']))
474 coaddExposureList.append(coaddRef.get())
475 dataIds.append(dataId)
476 else:
477 self.log.info("Exposure has no WCS, so cannot create associated template.")
479 if not overlappingArea:
480 raise pipeBase.NoWorkFound('No patches overlap detector')
482 return pipeBase.Struct(coaddExposures=coaddExposureList,
483 dataIds=dataIds)
485 def run(self, coaddExposures, bbox, wcs, dataIds, physical_filter=None, **kwargs):
486 """Warp coadds from multiple tracts to form a template for image diff.
488 Where the tracts overlap, the resulting template image is averaged.
489 The PSF on the template is created by combining the CoaddPsf on each
490 template image into a meta-CoaddPsf.
492 Parameters
493 ----------
494 coaddExposures : `list` [`lsst.afw.image.Exposure`]
495 Coadds to be mosaicked.
496 bbox : `lsst.geom.Box2I`
497 Template Bounding box of the detector geometry onto which to
498 resample the ``coaddExposures``.
499 wcs : `lsst.afw.geom.SkyWcs`
500 Template WCS onto which to resample the ``coaddExposures``.
501 dataIds : `list` [`lsst.daf.butler.DataCoordinate`]
502 Record of the tract and patch of each coaddExposure.
503 physical_filter : `str`, optional
504 The physical filter of the science image.
505 **kwargs
506 Any additional keyword parameters.
508 Returns
509 -------
510 result : `lsst.pipe.base.Struct`
511 A struct with attributes:
513 ``template``
514 A template coadd exposure assembled out of patches
515 (`lsst.afw.image.ExposureF`).
517 Raises
518 ------
519 NoWorkFound
520 If no coadds are found with sufficient un-masked pixels.
521 RuntimeError
522 If the PSF of the template can't be calculated.
523 """
524 # Table for CoaddPSF
525 tractsSchema = afwTable.ExposureTable.makeMinimalSchema()
526 tractKey = tractsSchema.addField('tract', type=np.int32, doc='Which tract')
527 patchKey = tractsSchema.addField('patch', type=np.int32, doc='Which patch')
528 weightKey = tractsSchema.addField('weight', type=float, doc='Weight for each tract, should be 1')
529 tractsCatalog = afwTable.ExposureCatalog(tractsSchema)
531 finalWcs = wcs
532 bbox.grow(self.config.templateBorderSize)
533 finalBBox = bbox
535 nPatchesFound = 0
536 maskedImageList = []
537 weightList = []
539 for coaddExposure, dataId in zip(coaddExposures, dataIds):
541 # warp to detector WCS
542 warped = self.warper.warpExposure(finalWcs, coaddExposure, maxBBox=finalBBox)
544 # Check if warped image is viable
545 if not np.any(np.isfinite(warped.image.array)):
546 self.log.info("No overlap for warped %s. Skipping" % dataId)
547 continue
549 exp = afwImage.ExposureF(finalBBox, finalWcs)
550 exp.maskedImage.set(np.nan, afwImage.Mask.getPlaneBitMask("NO_DATA"), np.nan)
551 exp.maskedImage.assign(warped.maskedImage, warped.getBBox())
553 maskedImageList.append(exp.maskedImage)
554 weightList.append(1)
555 record = tractsCatalog.addNew()
556 record.setPsf(coaddExposure.getPsf())
557 record.setWcs(coaddExposure.getWcs())
558 record.setPhotoCalib(coaddExposure.getPhotoCalib())
559 record.setBBox(coaddExposure.getBBox())
560 record.setValidPolygon(afwGeom.Polygon(geom.Box2D(coaddExposure.getBBox()).getCorners()))
561 record.set(tractKey, dataId['tract'])
562 record.set(patchKey, dataId['patch'])
563 record.set(weightKey, 1.)
564 nPatchesFound += 1
566 if nPatchesFound == 0:
567 raise pipeBase.NoWorkFound("No patches found to overlap detector")
569 # Combine images from individual patches together
570 statsFlags = afwMath.stringToStatisticsProperty('MEAN')
571 statsCtrl = afwMath.StatisticsControl()
572 statsCtrl.setNanSafe(True)
573 statsCtrl.setWeighted(True)
574 statsCtrl.setCalcErrorMosaicMode(True)
576 templateExposure = afwImage.ExposureF(finalBBox, finalWcs)
577 templateExposure.maskedImage.set(np.nan, afwImage.Mask.getPlaneBitMask("NO_DATA"), np.nan)
578 xy0 = templateExposure.getXY0()
579 # Do not mask any values
580 templateExposure.maskedImage = afwMath.statisticsStack(maskedImageList, statsFlags, statsCtrl,
581 weightList, clipped=0, maskMap=[])
582 templateExposure.maskedImage.setXY0(xy0)
584 # CoaddPsf centroid not only must overlap image, but must overlap the
585 # part of image with data. Use centroid of region with data.
586 boolmask = templateExposure.mask.array & templateExposure.mask.getPlaneBitMask('NO_DATA') == 0
587 maskx = afwImage.makeMaskFromArray(boolmask.astype(afwImage.MaskPixel))
588 centerCoord = afwGeom.SpanSet.fromMask(maskx, 1).computeCentroid()
590 ctrl = self.config.coaddPsf.makeControl()
591 coaddPsf = CoaddPsf(tractsCatalog, finalWcs, centerCoord, ctrl.warpingKernelName, ctrl.cacheSize)
592 if coaddPsf is None:
593 raise RuntimeError("CoaddPsf could not be constructed")
595 templateExposure.setPsf(coaddPsf)
596 # Coadds do not have a physical filter, so fetch it from the butler to prevent downstream warnings.
597 if physical_filter is None:
598 filterLabel = coaddExposure.getFilter()
599 else:
600 filterLabel = afwImage.FilterLabel(dataId['band'], physical_filter)
601 templateExposure.setFilter(filterLabel)
602 templateExposure.setPhotoCalib(coaddExposure.getPhotoCalib())
603 return pipeBase.Struct(template=templateExposure)
606class GetDcrTemplateConnections(GetTemplateConnections,
607 dimensions=("instrument", "visit", "detector", "skymap"),
608 defaultTemplates={"coaddName": "dcr",
609 "warpTypeSuffix": "",
610 "fakesType": ""}):
611 visitInfo = pipeBase.connectionTypes.Input(
612 doc="VisitInfo of calexp used to determine observing conditions.",
613 name="{fakesType}calexp.visitInfo",
614 storageClass="VisitInfo",
615 dimensions=("instrument", "visit", "detector"),
616 )
617 dcrCoadds = pipeBase.connectionTypes.Input(
618 doc="Input DCR template to match and subtract from the exposure",
619 name="{fakesType}dcrCoadd{warpTypeSuffix}",
620 storageClass="ExposureF",
621 dimensions=("tract", "patch", "skymap", "band", "subfilter"),
622 multiple=True,
623 deferLoad=True
624 )
626 def __init__(self, *, config=None):
627 super().__init__(config=config)
628 self.inputs.remove("coaddExposures")
631class GetDcrTemplateConfig(GetTemplateConfig,
632 pipelineConnections=GetDcrTemplateConnections):
633 numSubfilters = pexConfig.Field(
634 doc="Number of subfilters in the DcrCoadd.",
635 dtype=int,
636 default=3,
637 )
638 effectiveWavelength = pexConfig.Field(
639 doc="Effective wavelength of the filter.",
640 optional=False,
641 dtype=float,
642 )
643 bandwidth = pexConfig.Field(
644 doc="Bandwidth of the physical filter.",
645 optional=False,
646 dtype=float,
647 )
649 def validate(self):
650 if self.effectiveWavelength is None or self.bandwidth is None:
651 raise ValueError("The effective wavelength and bandwidth of the physical filter "
652 "must be set in the getTemplate config for DCR coadds. "
653 "Required until transmission curves are used in DM-13668.")
656class GetDcrTemplateTask(GetTemplateTask):
657 ConfigClass = GetDcrTemplateConfig
658 _DefaultName = "getDcrTemplate"
660 def getOverlappingExposures(self, inputs):
661 """Return lists of coadds and their corresponding dataIds that overlap
662 the detector.
664 The spatial index in the registry has generous padding and often
665 supplies patches near, but not directly overlapping the detector.
666 Filters inputs so that we don't have to read in all input coadds.
668 Parameters
669 ----------
670 inputs : `dict` of task Inputs, containing:
671 - coaddExposureRefs : `list`
672 [`lsst.daf.butler.DeferredDatasetHandle` of
673 `lsst.afw.image.Exposure`]
674 Data references to exposures that might overlap the detector.
675 - bbox : `lsst.geom.Box2I`
676 Template Bounding box of the detector geometry onto which to
677 resample the coaddExposures.
678 - skyMap : `lsst.skymap.SkyMap`
679 Input definition of geometry/bbox and projection/wcs for
680 template exposures.
681 - wcs : `lsst.afw.geom.SkyWcs`
682 Template WCS onto which to resample the coaddExposures.
683 - visitInfo : `lsst.afw.image.VisitInfo`
684 Metadata for the science image.
686 Returns
687 -------
688 result : `lsst.pipe.base.Struct`
689 A struct with attibutes:
691 ``coaddExposures``
692 Coadd exposures that overlap the detector (`list`
693 [`lsst.afw.image.Exposure`]).
694 ``dataIds``
695 Data IDs of the coadd exposures that overlap the detector
696 (`list` [`lsst.daf.butler.DataCoordinate`]).
698 Raises
699 ------
700 NoWorkFound
701 Raised if no patches overlatp the input detector bbox.
702 """
703 # Check that the patches actually overlap the detector
704 # Exposure's validPolygon would be more accurate
705 detectorPolygon = geom.Box2D(inputs["bbox"])
706 overlappingArea = 0
707 coaddExposureRefList = []
708 dataIds = []
709 patchList = dict()
710 for coaddRef in inputs["dcrCoadds"]:
711 dataId = coaddRef.dataId
712 patchWcs = inputs["skyMap"][dataId['tract']].getWcs()
713 patchBBox = inputs["skyMap"][dataId['tract']][dataId['patch']].getOuterBBox()
714 patchCorners = patchWcs.pixelToSky(geom.Box2D(patchBBox).getCorners())
715 patchPolygon = afwGeom.Polygon(inputs["wcs"].skyToPixel(patchCorners))
716 if patchPolygon.intersection(detectorPolygon):
717 overlappingArea += patchPolygon.intersectionSingle(detectorPolygon).calculateArea()
718 self.log.info("Using template input tract=%s, patch=%s, subfilter=%s" %
719 (dataId['tract'], dataId['patch'], dataId["subfilter"]))
720 coaddExposureRefList.append(coaddRef)
721 if dataId['tract'] in patchList:
722 patchList[dataId['tract']].append(dataId['patch'])
723 else:
724 patchList[dataId['tract']] = [dataId['patch'], ]
725 dataIds.append(dataId)
727 if not overlappingArea:
728 raise pipeBase.NoWorkFound('No patches overlap detector')
730 self.checkPatchList(patchList)
732 coaddExposures = self.getDcrModel(patchList, inputs['dcrCoadds'], inputs['visitInfo'])
733 return pipeBase.Struct(coaddExposures=coaddExposures,
734 dataIds=dataIds)
736 def checkPatchList(self, patchList):
737 """Check that all of the DcrModel subfilters are present for each
738 patch.
740 Parameters
741 ----------
742 patchList : `dict`
743 Dict of the patches containing valid data for each tract.
745 Raises
746 ------
747 RuntimeError
748 If the number of exposures found for a patch does not match the
749 number of subfilters.
750 """
751 for tract in patchList:
752 for patch in set(patchList[tract]):
753 if patchList[tract].count(patch) != self.config.numSubfilters:
754 raise RuntimeError("Invalid number of DcrModel subfilters found: %d vs %d expected",
755 patchList[tract].count(patch), self.config.numSubfilters)
757 def getDcrModel(self, patchList, coaddRefs, visitInfo):
758 """Build DCR-matched coadds from a list of exposure references.
760 Parameters
761 ----------
762 patchList : `dict`
763 Dict of the patches containing valid data for each tract.
764 coaddRefs : `list` [`lsst.daf.butler.DeferredDatasetHandle`]
765 Data references to `~lsst.afw.image.Exposure` representing
766 DcrModels that overlap the detector.
767 visitInfo : `lsst.afw.image.VisitInfo`
768 Metadata for the science image.
770 Returns
771 -------
772 coaddExposureList : `list` [`lsst.afw.image.Exposure`]
773 Coadd exposures that overlap the detector.
774 """
775 coaddExposureList = []
776 for tract in patchList:
777 for patch in set(patchList[tract]):
778 coaddRefList = [coaddRef for coaddRef in coaddRefs
779 if _selectDataRef(coaddRef, tract, patch)]
781 dcrModel = DcrModel.fromQuantum(coaddRefList,
782 self.config.effectiveWavelength,
783 self.config.bandwidth,
784 self.config.numSubfilters)
785 coaddExposureList.append(dcrModel.buildMatchedExposure(visitInfo=visitInfo))
786 return coaddExposureList
789def _selectDataRef(coaddRef, tract, patch):
790 condition = (coaddRef.dataId['tract'] == tract) & (coaddRef.dataId['patch'] == patch)
791 return condition
794class GetMultiTractCoaddTemplateConfig(GetTemplateConfig):
795 pass
798class GetMultiTractCoaddTemplateTask(GetTemplateTask):
799 ConfigClass = GetMultiTractCoaddTemplateConfig
800 _DefaultName = "getMultiTractCoaddTemplate"
802 def __init__(self, *args, **kwargs):
803 super().__init__(*args, **kwargs)
804 self.log.warning("GetMultiTractCoaddTemplateTask is deprecated. Use GetTemplateTask instead.")