24from deprecated.sphinx
import deprecated
26import lsst.afw.image
as afwImage
37from lsst.utils.timer
import timeMethod
39__all__ = [
"GetTemplateTask",
"GetTemplateConfig",
40 "GetDcrTemplateTask",
"GetDcrTemplateConfig"]
44 dimensions=(
"instrument",
"visit",
"detector",
"skymap"),
45 defaultTemplates={
"coaddName":
"goodSeeing",
48 bbox = pipeBase.connectionTypes.Input(
49 doc=
"Bounding box of exposure to determine the geometry of the output template.",
50 name=
"{fakesType}calexp.bbox",
52 dimensions=(
"instrument",
"visit",
"detector"),
54 wcs = pipeBase.connectionTypes.Input(
55 doc=
"WCS of the exposure that we will construct the template for.",
56 name=
"{fakesType}calexp.wcs",
58 dimensions=(
"instrument",
"visit",
"detector"),
60 skyMap = pipeBase.connectionTypes.Input(
61 doc=
"Geometry of the tracts and patches that the coadds are defined on.",
62 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
63 dimensions=(
"skymap", ),
64 storageClass=
"SkyMap",
66 coaddExposures = pipeBase.connectionTypes.Input(
67 doc=
"Coadds that may overlap the desired region, as possible inputs to the template."
68 " Will be restricted to those that directly overlap the projected bounding box.",
69 dimensions=(
"tract",
"patch",
"skymap",
"band"),
70 storageClass=
"ExposureF",
71 name=
"{fakesType}{coaddName}Coadd{warpTypeSuffix}",
74 deferGraphConstraint=
True
77 template = pipeBase.connectionTypes.Output(
78 doc=
"Warped template, pixel matched to the bounding box and WCS.",
79 dimensions=(
"instrument",
"visit",
"detector"),
80 storageClass=
"ExposureF",
81 name=
"{fakesType}{coaddName}Diff_templateExp{warpTypeSuffix}",
85class GetTemplateConfig(pipeBase.PipelineTaskConfig,
86 pipelineConnections=GetTemplateConnections):
87 templateBorderSize = pexConfig.Field(
90 doc=
"Number of pixels to grow the requested template image to account for warping"
92 warp = pexConfig.ConfigField(
93 dtype=afwMath.Warper.ConfigClass,
94 doc=
"warper configuration",
96 coaddPsf = pexConfig.ConfigField(
97 doc=
"Configuration for CoaddPsf",
101 def setDefaults(self):
104 self.warp.cacheSize = 100000
105 self.coaddPsf.cacheSize = self.warp.cacheSize
108 self.warp.interpLength = 100
109 self.warp.warpingKernelName =
'lanczos3'
110 self.coaddPsf.warpingKernelName = self.warp.warpingKernelName
113class GetTemplateTask(pipeBase.PipelineTask):
114 ConfigClass = GetTemplateConfig
115 _DefaultName =
"getTemplate"
117 def __init__(self, *args, **kwargs):
118 super().__init__(*args, **kwargs)
119 self.warper = afwMath.Warper.fromConfig(self.config.warp)
120 self.schema = afwTable.ExposureTable.makeMinimalSchema()
121 self.schema.addField(
'tract', type=np.int32, doc=
'Which tract this exposure came from.')
122 self.schema.addField(
'patch', type=np.int32, doc=
'Which patch in the tract this exposure came from.')
123 self.schema.addField(
'weight', type=float,
124 doc=
'Weight for each exposure, used to make the CoaddPsf; should always be 1.')
126 def runQuantum(self, butlerQC, inputRefs, outputRefs):
127 inputs = butlerQC.get(inputRefs)
128 bbox = inputs.pop(
"bbox")
129 wcs = inputs.pop(
"wcs")
130 coaddExposures = inputs.pop(
'coaddExposures')
131 skymap = inputs.pop(
"skyMap")
134 assert not inputs,
"runQuantum got more inputs than expected"
136 results = self.getExposures(coaddExposures, bbox, skymap, wcs)
137 physical_filter = butlerQC.quantum.dataId[
"physical_filter"]
138 outputs = self.run(coaddExposureHandles=results.coaddExposures,
141 dataIds=results.dataIds,
142 physical_filter=physical_filter)
143 butlerQC.put(outputs, outputRefs)
145 @deprecated(reason=
"Replaced by getExposures, which uses explicit arguments instead of a kwargs dict. "
146 "This method will be removed after v29.",
147 version=
"v29.0", category=FutureWarning)
148 def getOverlappingExposures(self, inputs):
149 return self.getExposures(inputs[
"coaddExposures"],
154 def getExposures(self, coaddExposureHandles, bbox, skymap, wcs):
155 """Return a data structure containing the coadds that overlap the
156 specified bbox projected onto the sky, and a corresponding data
157 structure of their dataIds.
158 These are the appropriate inputs to this task's `run` method.
160 The spatial index in the butler registry has generous padding and often
161 supplies patches near, but not directly overlapping the desired region.
162 This method filters the inputs so that `run` does not have to read in
163 all possibly-matching coadd exposures.
167 coaddExposureHandles : `iterable` \
168 [`lsst.daf.butler.DeferredDatasetHandle` of \
169 `lsst.afw.image.Exposure`]
170 Dataset handles to exposures that might overlap the desired
172 bbox : `lsst.geom.Box2I`
173 Template bounding box of the pixel geometry onto which the
174 coaddExposures will be resampled.
175 skymap : `lsst.skymap.SkyMap`
176 Geometry of the tracts and patches the coadds are defined on.
177 wcs : `lsst.afw.geom.SkyWcs`
178 Template WCS onto which the coadds will be resampled.
182 result : `lsst.pipe.base.Struct`
183 A struct with attributes:
186 Dict of coadd exposures that overlap the projected bbox,
188 (`dict` [`int`, `list` [`lsst.daf.butler.DeferredDatasetHandle` of
189 `lsst.afw.image.Exposure`] ]).
191 Dict of data IDs of the coadd exposures that overlap the
192 projected bbox, indexed on tract id
193 (`dict` [`int`, `list [`lsst.daf.butler.DataCoordinate`] ]).
198 Raised if no patches overlap the input detector bbox, or the input
202 raise pipeBase.NoWorkFound(
"WCS is None; cannot find overlapping exposures.")
207 coaddExposures = collections.defaultdict(list)
208 dataIds = collections.defaultdict(list)
210 for coaddRef
in coaddExposureHandles:
211 dataId = coaddRef.dataId
212 patchWcs = skymap[dataId[
'tract']].getWcs()
213 patchBBox = skymap[dataId[
'tract']][dataId[
'patch']].getOuterBBox()
214 patchCorners = patchWcs.pixelToSky(
geom.Box2D(patchBBox).getCorners())
216 if patchPolygon.intersection(detectorPolygon):
217 overlappingArea += patchPolygon.intersectionSingle(detectorPolygon).calculateArea()
218 self.log.info(
"Using template input tract=%s, patch=%s", dataId[
'tract'], dataId[
'patch'])
219 coaddExposures[dataId[
'tract']].append(coaddRef)
220 dataIds[dataId[
'tract']].append(dataId)
222 if not overlappingArea:
223 raise pipeBase.NoWorkFound(
'No patches overlap detector')
225 return pipeBase.Struct(coaddExposures=coaddExposures,
229 def run(self, *, coaddExposureHandles, bbox, wcs, dataIds, physical_filter):
230 """Warp coadds from multiple tracts and patches to form a template to
231 subtract from a science image.
233 Tract and patch overlap regions are combined by a variance-weighted
234 average, and the variance planes are combined with the same weights,
235 not added in quadrature; the overlap regions are not statistically
236 independent, because they're derived from the same original data.
237 The PSF on the template is created by combining the CoaddPsf on each
238 template image into a meta-CoaddPsf.
242 coaddExposureHandles : `dict` [`int`, `list` of \
243 [`lsst.daf.butler.DeferredDatasetHandle` of \
244 `lsst.afw.image.Exposure`]]
245 Coadds to be mosaicked, indexed on tract id.
246 bbox : `lsst.geom.Box2I`
247 Template Bounding box of the detector geometry onto which to
248 resample the ``coaddExposureHandles``. Modified in-place to include the
250 wcs : `lsst.afw.geom.SkyWcs`
251 Template WCS onto which to resample the ``coaddExposureHandles``.
252 dataIds : `dict` [`int`, `list` [`lsst.daf.butler.DataCoordinate`]]
253 Record of the tract and patch of each coaddExposure, indexed on
255 physical_filter : `str`
256 Physical filter of the science image.
260 result : `lsst.pipe.base.Struct`
261 A struct with attributes:
264 A template coadd exposure assembled out of patches
265 (`lsst.afw.image.ExposureF`).
270 If no coadds are found with sufficient un-masked pixels.
272 band, photoCalib = self._checkInputs(dataIds, coaddExposureHandles)
274 bbox.grow(self.config.templateBorderSize)
278 for tract
in coaddExposureHandles:
279 maskedImages, catalog, totalBox = self._makeExposureCatalog(coaddExposureHandles[tract],
281 warpedBox = computeWarpedBBox(catalog[0].wcs, bbox, wcs)
284 unwarped, count, included = self._merge(maskedImages, warpedBox, catalog[0].wcs)
288 self.log.info(
"No valid pixels from coadd patches in tract %s; not including in output.",
291 warpedBox.clip(totalBox)
292 potentialInput = self.warper.warpExposure(wcs, unwarped.subset(warpedBox), destBBox=bbox)
296 if np.all(potentialInput.mask.array & potentialInput.mask.getPlaneBitMask(
"NO_DATA")):
297 self.log.info(
"No overlap from coadd patches in tract %s; not including in output.", tract)
302 tempCatalog.reserve(len(included))
304 tempCatalog.append(catalog[i])
305 catalogs.append(tempCatalog)
306 warped[tract] = potentialInput.maskedImage
309 raise pipeBase.NoWorkFound(
"No patches found to overlap science exposure.")
311 template, count, _ = self._merge(warped, bbox, wcs)
313 raise pipeBase.NoWorkFound(
"No valid pixels in warped template.")
317 catalog.reserve(sum([len(c)
for c
in catalogs]))
321 template.setPsf(self._makePsf(template, catalog, wcs))
323 template.setPhotoCalib(photoCalib)
324 return pipeBase.Struct(template=template)
229 def run(self, *, coaddExposureHandles, bbox, wcs, dataIds, physical_filter):
…
327 def _checkInputs(dataIds, coaddExposures):
328 """Check that the all the dataIds are from the same band and that
329 the exposures all have the same photometric calibration.
333 dataIds : `dict` [`int`, `list` [`lsst.daf.butler.DataCoordinate`]]
334 Record of the tract and patch of each coaddExposure.
335 coaddExposures : `dict` [`int`, `list` of \
336 [`lsst.daf.butler.DeferredDatasetHandle` of \
337 `lsst.afw.image.Exposure`]]
338 Coadds to be mosaicked.
343 Filter band of all the input exposures.
344 photoCalib : `lsst.afw.image.PhotoCalib`
345 Photometric calibration of all of the input exposures.
350 Raised if the bands or calibrations of the input exposures are not
353 bands = set(dataId[
"band"]
for tract
in dataIds
for dataId
in dataIds[tract])
355 raise RuntimeError(f
"GetTemplateTask called with multiple bands: {bands}")
357 photoCalibs = [exposure.get(component=
"photoCalib")
358 for exposures
in coaddExposures.values()
359 for exposure
in exposures]
360 if not all([photoCalibs[0] == x
for x
in photoCalibs]):
361 msg = f
"GetTemplateTask called with exposures with different photoCalibs: {photoCalibs}"
362 raise RuntimeError(msg)
363 photoCalib = photoCalibs[0]
364 return band, photoCalib
367 """Make an exposure catalog for one tract.
371 exposureRefs : `list` of [`lsst.daf.butler.DeferredDatasetHandle` of \
372 `lsst.afw.image.Exposure`]
373 Exposures to include in the catalog.
374 dataIds : `list` [`lsst.daf.butler.DataCoordinate`]
375 Data ids of each of the included exposures; must have "tract" and
380 images : `dict` [`lsst.afw.image.MaskedImage`]
381 MaskedImages of each of the input exposures, for warping.
382 catalog : `lsst.afw.table.ExposureCatalog`
383 Catalog of metadata for each exposure
384 totalBox : `lsst.geom.Box2I`
385 The union of the bounding boxes of all the input exposures.
388 catalog.reserve(len(exposureRefs))
389 exposures = (exposureRef.get()
for exposureRef
in exposureRefs)
393 for coadd, dataId
in zip(exposures, dataIds):
394 images[dataId] = coadd.maskedImage
395 bbox = coadd.getBBox()
396 totalBox = totalBox.expandedTo(bbox)
397 record = catalog.addNew()
398 record.setPsf(coadd.psf)
399 record.setWcs(coadd.wcs)
400 record.setPhotoCalib(coadd.photoCalib)
403 record.set(
"tract", dataId[
"tract"])
404 record.set(
"patch", dataId[
"patch"])
407 record.set(
"weight", 1)
409 return images, catalog, totalBox
411 def _merge(self, maskedImages, bbox, wcs):
412 """Merge the images that came from one tract into one larger image,
413 ignoring NaN pixels and non-finite variance pixels from individual
418 maskedImages : `dict` [`lsst.afw.image.MaskedImage` or
419 `lsst.afw.image.Exposure`]
420 Images to be merged into one larger bounding box.
421 bbox : `lsst.geom.Box2I`
422 Bounding box defining the image to merge into.
423 wcs : `lsst.afw.geom.SkyWcs`
424 WCS of all of the input images to set on the output image.
428 merged : `lsst.afw.image.MaskedImage`
429 Merged image with all of the inputs at their respective bbox
432 Count of the number of good pixels (those with positive weights)
434 included : `list` [`int`]
435 List of indexes of patches that were included in the merged
436 result, to be used to trim the exposure catalog.
438 merged = afwImage.ExposureF(bbox, wcs)
439 weights = afwImage.ImageF(bbox)
441 for i, (dataId, maskedImage)
in enumerate(maskedImages.items()):
443 clippedBox =
geom.Box2I(maskedImage.getBBox())
444 clippedBox.clip(bbox)
445 if clippedBox.area == 0:
446 self.log.debug(
"%s does not overlap template region.", dataId)
448 maskedImage = maskedImage.subset(clippedBox)
450 good = (maskedImage.variance.array > 0) & (np.isfinite(maskedImage.variance.array))
451 weight = maskedImage.variance.array[good]**(-0.5)
452 bad = np.isnan(maskedImage.image.array) | ~good
455 maskedImage.image.array[bad] = 0.0
456 maskedImage.variance.array[bad] = 0.0
458 maskedImage.mask.array[bad] = 0
462 maskedImage.image.array[good] *= weight
463 maskedImage.variance.array[good] *= weight
464 weights[clippedBox].array[good] += weight
467 merged.maskedImage[clippedBox] += maskedImage
470 good = weights.array > 0
475 weights = weights.array[good]
476 merged.image.array[good] /= weights
477 merged.variance.array[good] /= weights
479 merged.mask.array[~good] |= merged.mask.getPlaneBitMask(
"NO_DATA")
481 return merged, good.sum(), included
484 """Return a PSF containing the PSF at each of the input regions.
486 Note that although this includes all the exposures from the catalog,
487 the PSF knows which part of the template the inputs came from, so when
488 evaluated at a given position it will not include inputs that never
489 went in to those pixels.
493 template : `lsst.afw.image.Exposure`
494 Generated template the PSF is for.
495 catalog : `lsst.afw.table.ExposureCatalog`
496 Catalog of exposures that went into the template that contains all
498 wcs : `lsst.afw.geom.SkyWcs`
499 WCS of the template, to warp the PSFs to.
503 coaddPsf : `lsst.meas.algorithms.CoaddPsf`
504 The meta-psf constructed from all of the input catalogs.
508 boolmask = template.mask.array & template.mask.getPlaneBitMask(
'NO_DATA') == 0
510 centerCoord = afwGeom.SpanSet.fromMask(maskx, 1).computeCentroid()
512 ctrl = self.config.coaddPsf.makeControl()
513 coaddPsf =
CoaddPsf(catalog, wcs, centerCoord, ctrl.warpingKernelName, ctrl.cacheSize)
518 dimensions=(
"instrument",
"visit",
"detector",
"skymap"),
519 defaultTemplates={
"coaddName":
"dcr",
520 "warpTypeSuffix":
"",
522 visitInfo = pipeBase.connectionTypes.Input(
523 doc=
"VisitInfo of calexp used to determine observing conditions.",
524 name=
"{fakesType}calexp.visitInfo",
525 storageClass=
"VisitInfo",
526 dimensions=(
"instrument",
"visit",
"detector"),
528 dcrCoadds = pipeBase.connectionTypes.Input(
529 doc=
"Input DCR template to match and subtract from the exposure",
530 name=
"{fakesType}dcrCoadd{warpTypeSuffix}",
531 storageClass=
"ExposureF",
532 dimensions=(
"tract",
"patch",
"skymap",
"band",
"subfilter"),
537 def __init__(self, *, config=None):
538 super().__init__(config=config)
539 self.inputs.remove(
"coaddExposures")
542class GetDcrTemplateConfig(GetTemplateConfig,
543 pipelineConnections=GetDcrTemplateConnections):
544 numSubfilters = pexConfig.Field(
545 doc=
"Number of subfilters in the DcrCoadd.",
549 effectiveWavelength = pexConfig.Field(
550 doc=
"Effective wavelength of the filter in nm.",
554 bandwidth = pexConfig.Field(
555 doc=
"Bandwidth of the physical filter.",
561 if self.effectiveWavelength
is None or self.bandwidth
is None:
562 raise ValueError(
"The effective wavelength and bandwidth of the physical filter "
563 "must be set in the getTemplate config for DCR coadds. "
564 "Required until transmission curves are used in DM-13668.")
567class GetDcrTemplateTask(GetTemplateTask):
568 ConfigClass = GetDcrTemplateConfig
569 _DefaultName =
"getDcrTemplate"
571 def runQuantum(self, butlerQC, inputRefs, outputRefs):
572 inputs = butlerQC.get(inputRefs)
573 bbox = inputs.pop(
"bbox")
574 wcs = inputs.pop(
"wcs")
575 dcrCoaddExposureHandles = inputs.pop(
'dcrCoadds')
576 skymap = inputs.pop(
"skyMap")
577 visitInfo = inputs.pop(
"visitInfo")
580 assert not inputs,
"runQuantum got more inputs than expected"
582 results = self.getExposures(dcrCoaddExposureHandles, bbox, skymap, wcs, visitInfo)
583 physical_filter = butlerQC.quantum.dataId[
"physical_filter"]
584 outputs = self.run(coaddExposures=results.coaddExposures,
587 dataIds=results.dataIds,
588 physical_filter=physical_filter)
589 butlerQC.put(outputs, outputRefs)
591 @deprecated(reason=
"Replaced by getExposures, which uses explicit arguments instead of a kwargs dict. "
592 "This method will be removed after v29.",
593 version=
"v29.0", category=FutureWarning)
594 def getOverlappingExposures(self, inputs):
595 return self.getExposures(inputs[
"dcrCoadds"],
601 def getExposures(self, dcrCoaddExposureHandles, bbox, skymap, wcs, visitInfo):
602 """Return lists of coadds and their corresponding dataIds that overlap
605 The spatial index in the registry has generous padding and often
606 supplies patches near, but not directly overlapping the detector.
607 Filters inputs so that we don't have to read in all input coadds.
611 dcrCoaddExposureHandles : `list` \
612 [`lsst.daf.butler.DeferredDatasetHandle` of \
613 `lsst.afw.image.Exposure`]
614 Data references to exposures that might overlap the detector.
615 bbox : `lsst.geom.Box2I`
616 Template Bounding box of the detector geometry onto which to
617 resample the coaddExposures.
618 skymap : `lsst.skymap.SkyMap`
619 Input definition of geometry/bbox and projection/wcs for
621 wcs : `lsst.afw.geom.SkyWcs`
622 Template WCS onto which to resample the coaddExposures.
623 visitInfo : `lsst.afw.image.VisitInfo`
624 Metadata for the science image.
628 result : `lsst.pipe.base.Struct`
629 A struct with attibutes:
632 Dict of coadd exposures that overlap the projected bbox,
634 (`dict` [`int`, `list` [`lsst.afw.image.Exposure`] ]).
636 Dict of data IDs of the coadd exposures that overlap the
637 projected bbox, indexed on tract id
638 (`dict` [`int`, `list [`lsst.daf.butler.DataCoordinate`] ]).
643 Raised if no patches overlatp the input detector bbox.
648 raise pipeBase.NoWorkFound(
"Exposure has no WCS; cannot create a template.")
652 dataIds = collections.defaultdict(list)
654 for coaddRef
in dcrCoaddExposureHandles:
655 dataId = coaddRef.dataId
656 patchWcs = skymap[dataId[
'tract']].getWcs()
657 patchBBox = skymap[dataId[
'tract']][dataId[
'patch']].getOuterBBox()
658 patchCorners = patchWcs.pixelToSky(
geom.Box2D(patchBBox).getCorners())
660 if patchPolygon.intersection(detectorPolygon):
661 overlappingArea += patchPolygon.intersectionSingle(detectorPolygon).calculateArea()
662 self.log.info(
"Using template input tract=%s, patch=%s, subfilter=%s" %
663 (dataId[
'tract'], dataId[
'patch'], dataId[
"subfilter"]))
664 if dataId[
'tract']
in patchList:
665 patchList[dataId[
'tract']].append(dataId[
'patch'])
667 patchList[dataId[
'tract']] = [dataId[
'patch'], ]
668 dataIds[dataId[
'tract']].append(dataId)
670 if not overlappingArea:
671 raise pipeBase.NoWorkFound(
'No patches overlap detector')
673 self.checkPatchList(patchList)
675 coaddExposures = self.getDcrModel(patchList, dcrCoaddExposureHandles, visitInfo)
676 return pipeBase.Struct(coaddExposures=coaddExposures,
680 """Check that all of the DcrModel subfilters are present for each
686 Dict of the patches containing valid data for each tract.
691 If the number of exposures found for a patch does not match the
692 number of subfilters.
694 for tract
in patchList:
695 for patch
in set(patchList[tract]):
696 if patchList[tract].count(patch) != self.config.numSubfilters:
697 raise RuntimeError(
"Invalid number of DcrModel subfilters found: %d vs %d expected",
698 patchList[tract].count(patch), self.config.numSubfilters)
701 """Build DCR-matched coadds from a list of exposure references.
706 Dict of the patches containing valid data for each tract.
707 coaddRefs : `list` [`lsst.daf.butler.DeferredDatasetHandle`]
708 Data references to `~lsst.afw.image.Exposure` representing
709 DcrModels that overlap the detector.
710 visitInfo : `lsst.afw.image.VisitInfo`
711 Metadata for the science image.
715 coaddExposures : `list` [`lsst.afw.image.Exposure`]
716 Coadd exposures that overlap the detector.
718 coaddExposures = collections.defaultdict(list)
719 for tract
in patchList:
720 for patch
in set(patchList[tract]):
721 coaddRefList = [coaddRef
for coaddRef
in coaddRefs
724 dcrModel = DcrModel.fromQuantum(coaddRefList,
725 self.config.effectiveWavelength,
726 self.config.bandwidth,
727 self.config.numSubfilters)
728 coaddExposures[tract].append(dcrModel.buildMatchedExposure(visitInfo=visitInfo))
729 return coaddExposures
733 condition = (coaddRef.dataId[
'tract'] == tract) & (coaddRef.dataId[
'patch'] == patch)
A group of labels for a filter in an exposure or coadd.
A floating-point coordinate rectangle geometry.
An integer coordinate rectangle.
CoaddPsf is the Psf derived to be used for non-PSF-matched Coadd images.
_selectDataRef(coaddRef, tract, patch)
checkPatchList(self, patchList)
_merge(self, maskedImages, bbox, wcs)
_makeExposureCatalog(self, exposureRefs, dataIds)
run(self, *, coaddExposureHandles, bbox, wcs, dataIds, physical_filter)
getDcrModel(self, patchList, coaddRefs, visitInfo)
_makePsf(self, template, catalog, wcs)