24from deprecated.sphinx
import deprecated
26import lsst.afw.image
as afwImage
38from lsst.utils.timer
import timeMethod
44 "GetDcrTemplateConfig",
49 pipeBase.PipelineTaskConnections,
50 dimensions=(
"instrument",
"visit",
"detector",
"skymap"),
51 defaultTemplates={
"coaddName":
"goodSeeing",
"warpTypeSuffix":
"",
"fakesType":
""},
53 bbox = pipeBase.connectionTypes.Input(
54 doc=
"Bounding box of exposure to determine the geometry of the output template.",
55 name=
"{fakesType}calexp.bbox",
57 dimensions=(
"instrument",
"visit",
"detector"),
59 wcs = pipeBase.connectionTypes.Input(
60 doc=
"WCS of the exposure that we will construct the template for.",
61 name=
"{fakesType}calexp.wcs",
63 dimensions=(
"instrument",
"visit",
"detector"),
65 skyMap = pipeBase.connectionTypes.Input(
66 doc=
"Geometry of the tracts and patches that the coadds are defined on.",
67 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
68 dimensions=(
"skymap",),
69 storageClass=
"SkyMap",
71 coaddExposures = pipeBase.connectionTypes.Input(
72 doc=
"Coadds that may overlap the desired region, as possible inputs to the template."
73 " Will be restricted to those that directly overlap the projected bounding box.",
74 dimensions=(
"tract",
"patch",
"skymap",
"band"),
75 storageClass=
"ExposureF",
76 name=
"{fakesType}{coaddName}Coadd{warpTypeSuffix}",
79 deferGraphConstraint=
True,
82 template = pipeBase.connectionTypes.Output(
83 doc=
"Warped template, pixel matched to the bounding box and WCS.",
84 dimensions=(
"instrument",
"visit",
"detector"),
85 storageClass=
"ExposureF",
86 name=
"{fakesType}{coaddName}Diff_templateExp{warpTypeSuffix}",
90class GetTemplateConfig(
91 pipeBase.PipelineTaskConfig, pipelineConnections=GetTemplateConnections
93 templateBorderSize = pexConfig.Field(
96 doc=
"Number of pixels to grow the requested template image to account for warping",
98 warp = pexConfig.ConfigField(
99 dtype=afwMath.Warper.ConfigClass,
100 doc=
"warper configuration",
102 coaddPsf = pexConfig.ConfigField(
103 doc=
"Configuration for CoaddPsf",
104 dtype=CoaddPsfConfig,
107 def setDefaults(self):
110 self.warp.cacheSize = 100000
111 self.coaddPsf.cacheSize = self.warp.cacheSize
114 self.warp.interpLength = 100
115 self.warp.warpingKernelName =
"lanczos3"
116 self.coaddPsf.warpingKernelName = self.warp.warpingKernelName
119class GetTemplateTask(pipeBase.PipelineTask):
120 ConfigClass = GetTemplateConfig
121 _DefaultName =
"getTemplate"
123 def __init__(self, *args, **kwargs):
124 super().__init__(*args, **kwargs)
125 self.warper = afwMath.Warper.fromConfig(self.config.warp)
126 self.schema = afwTable.ExposureTable.makeMinimalSchema()
127 self.schema.addField(
128 "tract", type=np.int32, doc=
"Which tract this exposure came from."
130 self.schema.addField(
133 doc=
"Which patch in the tract this exposure came from.",
135 self.schema.addField(
138 doc=
"Weight for each exposure, used to make the CoaddPsf; should always be 1.",
141 def runQuantum(self, butlerQC, inputRefs, outputRefs):
142 inputs = butlerQC.get(inputRefs)
143 bbox = inputs.pop(
"bbox")
144 wcs = inputs.pop(
"wcs")
145 coaddExposures = inputs.pop(
"coaddExposures")
146 skymap = inputs.pop(
"skyMap")
149 assert not inputs,
"runQuantum got more inputs than expected"
151 results = self.getExposures(coaddExposures, bbox, skymap, wcs)
152 physical_filter = butlerQC.quantum.dataId[
"physical_filter"]
154 coaddExposureHandles=results.coaddExposures,
157 dataIds=results.dataIds,
158 physical_filter=physical_filter,
160 butlerQC.put(outputs, outputRefs)
163 reason=
"Replaced by getExposures, which uses explicit arguments instead of a kwargs dict. "
164 "This method will be removed after v29.",
166 category=FutureWarning,
168 def getOverlappingExposures(self, inputs):
169 return self.getExposures(
170 inputs[
"coaddExposures"], inputs[
"bbox"], inputs[
"skyMap"], inputs[
"wcs"]
173 def getExposures(self, coaddExposureHandles, bbox, skymap, wcs):
174 """Return a data structure containing the coadds that overlap the
175 specified bbox projected onto the sky, and a corresponding data
176 structure of their dataIds.
177 These are the appropriate inputs to this task's `run` method.
179 The spatial index in the butler registry has generous padding and often
180 supplies patches near, but not directly overlapping the desired region.
181 This method filters the inputs so that `run` does not have to read in
182 all possibly-matching coadd exposures.
186 coaddExposureHandles : `iterable` \
187 [`lsst.daf.butler.DeferredDatasetHandle` of \
188 `lsst.afw.image.Exposure`]
189 Dataset handles to exposures that might overlap the desired
191 bbox : `lsst.geom.Box2I`
192 Template bounding box of the pixel geometry onto which the
193 coaddExposures will be resampled.
194 skymap : `lsst.skymap.SkyMap`
195 Geometry of the tracts and patches the coadds are defined on.
196 wcs : `lsst.afw.geom.SkyWcs`
197 Template WCS onto which the coadds will be resampled.
201 result : `lsst.pipe.base.Struct`
202 A struct with attributes:
205 Dict of coadd exposures that overlap the projected bbox,
207 (`dict` [`int`, `list` [`lsst.daf.butler.DeferredDatasetHandle` of
208 `lsst.afw.image.Exposure`] ]).
210 Dict of data IDs of the coadd exposures that overlap the
211 projected bbox, indexed on tract id
212 (`dict` [`int`, `list [`lsst.daf.butler.DataCoordinate`] ]).
217 Raised if no patches overlap the input detector bbox, or the input
221 raise pipeBase.NoWorkFound(
222 "WCS is None; cannot find overlapping exposures."
228 coaddExposures = collections.defaultdict(list)
229 dataIds = collections.defaultdict(list)
231 for coaddRef
in coaddExposureHandles:
232 dataId = coaddRef.dataId
233 patchWcs = skymap[dataId[
"tract"]].getWcs()
234 patchBBox = skymap[dataId[
"tract"]][dataId[
"patch"]].getOuterBBox()
235 patchCorners = patchWcs.pixelToSky(
geom.Box2D(patchBBox).getCorners())
237 if patchPolygon.intersection(detectorPolygon):
238 overlappingArea += patchPolygon.intersectionSingle(
242 "Using template input tract=%s, patch=%s",
246 coaddExposures[dataId[
"tract"]].append(coaddRef)
247 dataIds[dataId[
"tract"]].append(dataId)
249 if not overlappingArea:
250 raise pipeBase.NoWorkFound(
"No patches overlap detector")
252 return pipeBase.Struct(coaddExposures=coaddExposures, dataIds=dataIds)
255 def run(self, *, coaddExposureHandles, bbox, wcs, dataIds, physical_filter):
256 """Warp coadds from multiple tracts and patches to form a template to
257 subtract from a science image.
259 Tract and patch overlap regions are combined by a variance-weighted
260 average, and the variance planes are combined with the same weights,
261 not added in quadrature; the overlap regions are not statistically
262 independent, because they're derived from the same original data.
263 The PSF on the template is created by combining the CoaddPsf on each
264 template image into a meta-CoaddPsf.
268 coaddExposureHandles : `dict` [`int`, `list` of \
269 [`lsst.daf.butler.DeferredDatasetHandle` of \
270 `lsst.afw.image.Exposure`]]
271 Coadds to be mosaicked, indexed on tract id.
272 bbox : `lsst.geom.Box2I`
273 Template Bounding box of the detector geometry onto which to
274 resample the ``coaddExposureHandles``. Modified in-place to include the
276 wcs : `lsst.afw.geom.SkyWcs`
277 Template WCS onto which to resample the ``coaddExposureHandles``.
278 dataIds : `dict` [`int`, `list` [`lsst.daf.butler.DataCoordinate`]]
279 Record of the tract and patch of each coaddExposure, indexed on
281 physical_filter : `str`
282 Physical filter of the science image.
286 result : `lsst.pipe.base.Struct`
287 A struct with attributes:
290 A template coadd exposure assembled out of patches
291 (`lsst.afw.image.ExposureF`).
296 If no coadds are found with sufficient un-masked pixels.
298 band, photoCalib = self._checkInputs(dataIds, coaddExposureHandles)
300 bbox.grow(self.config.templateBorderSize)
304 for tract
in coaddExposureHandles:
305 maskedImages, catalog, totalBox = self._makeExposureCatalog(
306 coaddExposureHandles[tract], dataIds[tract]
308 warpedBox = computeWarpedBBox(catalog[0].wcs, bbox, wcs)
311 unwarped, count, included = self._merge(
312 maskedImages, warpedBox, catalog[0].wcs
318 "No valid pixels from coadd patches in tract %s; not including in output.",
322 warpedBox.clip(totalBox)
323 potentialInput = self.warper.warpExposure(
324 wcs, unwarped.subset(warpedBox), destBBox=bbox
330 potentialInput.mask.array
331 & potentialInput.mask.getPlaneBitMask(
"NO_DATA")
334 "No overlap from coadd patches in tract %s; not including in output.",
341 tempCatalog.reserve(len(included))
343 tempCatalog.append(catalog[i])
344 catalogs.append(tempCatalog)
345 warped[tract] = potentialInput.maskedImage
348 raise pipeBase.NoWorkFound(
"No patches found to overlap science exposure.")
350 template, count, _ = self._merge(warped, bbox, wcs)
352 raise pipeBase.NoWorkFound(
"No valid pixels in warped template.")
356 catalog.reserve(sum([len(c)
for c
in catalogs]))
360 template.setPsf(self._makePsf(template, catalog, wcs))
362 template.setPhotoCalib(photoCalib)
363 return pipeBase.Struct(template=template)
255 def run(self, *, coaddExposureHandles, bbox, wcs, dataIds, physical_filter):
…
366 def _checkInputs(dataIds, coaddExposures):
367 """Check that the all the dataIds are from the same band and that
368 the exposures all have the same photometric calibration.
372 dataIds : `dict` [`int`, `list` [`lsst.daf.butler.DataCoordinate`]]
373 Record of the tract and patch of each coaddExposure.
374 coaddExposures : `dict` [`int`, `list` of \
375 [`lsst.daf.butler.DeferredDatasetHandle` of \
376 `lsst.afw.image.Exposure` or
377 `lsst.afw.image.Exposure`]]
378 Coadds to be mosaicked.
383 Filter band of all the input exposures.
384 photoCalib : `lsst.afw.image.PhotoCalib`
385 Photometric calibration of all of the input exposures.
390 Raised if the bands or calibrations of the input exposures are not
393 bands = set(dataId[
"band"]
for tract
in dataIds
for dataId
in dataIds[tract])
395 raise RuntimeError(f
"GetTemplateTask called with multiple bands: {bands}")
398 exposure.get(component=
"photoCalib")
399 for exposures
in coaddExposures.values()
400 for exposure
in exposures
402 if not all([photoCalibs[0] == x
for x
in photoCalibs]):
403 msg = f
"GetTemplateTask called with exposures with different photoCalibs: {photoCalibs}"
404 raise RuntimeError(msg)
405 photoCalib = photoCalibs[0]
406 return band, photoCalib
409 """Make an exposure catalog for one tract.
413 exposureRefs : `list` of [`lsst.daf.butler.DeferredDatasetHandle` of \
414 `lsst.afw.image.Exposure`]
415 Exposures to include in the catalog.
416 dataIds : `list` [`lsst.daf.butler.DataCoordinate`]
417 Data ids of each of the included exposures; must have "tract" and
422 images : `dict` [`lsst.afw.image.MaskedImage`]
423 MaskedImages of each of the input exposures, for warping.
424 catalog : `lsst.afw.table.ExposureCatalog`
425 Catalog of metadata for each exposure
426 totalBox : `lsst.geom.Box2I`
427 The union of the bounding boxes of all the input exposures.
430 catalog.reserve(len(exposureRefs))
431 exposures = (exposureRef.get()
for exposureRef
in exposureRefs)
435 for coadd, dataId
in zip(exposures, dataIds):
436 images[dataId] = coadd.maskedImage
437 bbox = coadd.getBBox()
438 totalBox = totalBox.expandedTo(bbox)
439 record = catalog.addNew()
440 record.setPsf(coadd.psf)
441 record.setWcs(coadd.wcs)
442 record.setPhotoCalib(coadd.photoCalib)
445 record.set(
"tract", dataId[
"tract"])
446 record.set(
"patch", dataId[
"patch"])
449 record.set(
"weight", 1)
451 return images, catalog, totalBox
453 def _merge(self, maskedImages, bbox, wcs):
454 """Merge the images that came from one tract into one larger image,
455 ignoring NaN pixels and non-finite variance pixels from individual
460 maskedImages : `dict` [`lsst.afw.image.MaskedImage` or
461 `lsst.afw.image.Exposure`]
462 Images to be merged into one larger bounding box.
463 bbox : `lsst.geom.Box2I`
464 Bounding box defining the image to merge into.
465 wcs : `lsst.afw.geom.SkyWcs`
466 WCS of all of the input images to set on the output image.
470 merged : `lsst.afw.image.MaskedImage`
471 Merged image with all of the inputs at their respective bbox
474 Count of the number of good pixels (those with positive weights)
476 included : `list` [`int`]
477 List of indexes of patches that were included in the merged
478 result, to be used to trim the exposure catalog.
480 merged = afwImage.ExposureF(bbox, wcs)
481 weights = afwImage.ImageF(bbox)
483 for i, (dataId, maskedImage)
in enumerate(maskedImages.items()):
485 clippedBox =
geom.Box2I(maskedImage.getBBox())
486 clippedBox.clip(bbox)
487 if clippedBox.area == 0:
488 self.log.debug(
"%s does not overlap template region.", dataId)
490 maskedImage = maskedImage.subset(clippedBox)
492 good = (maskedImage.variance.array > 0) & (
493 np.isfinite(maskedImage.variance.array)
495 weight = maskedImage.variance.array[good] ** (-0.5)
496 bad = np.isnan(maskedImage.image.array) | ~good
499 maskedImage.image.array[bad] = 0.0
500 maskedImage.variance.array[bad] = 0.0
502 maskedImage.mask.array[bad] = 0
506 maskedImage.image.array[good] *= weight
507 maskedImage.variance.array[good] *= weight
508 weights[clippedBox].array[good] += weight
511 merged.maskedImage[clippedBox] += maskedImage
514 good = weights.array > 0
519 weights = weights.array[good]
520 merged.image.array[good] /= weights
521 merged.variance.array[good] /= weights
523 merged.mask.array[~good] |= merged.mask.getPlaneBitMask(
"NO_DATA")
525 return merged, good.sum(), included
528 """Return a PSF containing the PSF at each of the input regions.
530 Note that although this includes all the exposures from the catalog,
531 the PSF knows which part of the template the inputs came from, so when
532 evaluated at a given position it will not include inputs that never
533 went in to those pixels.
537 template : `lsst.afw.image.Exposure`
538 Generated template the PSF is for.
539 catalog : `lsst.afw.table.ExposureCatalog`
540 Catalog of exposures that went into the template that contains all
542 wcs : `lsst.afw.geom.SkyWcs`
543 WCS of the template, to warp the PSFs to.
547 coaddPsf : `lsst.meas.algorithms.CoaddPsf`
548 The meta-psf constructed from all of the input catalogs.
552 boolmask = template.mask.array & template.mask.getPlaneBitMask(
"NO_DATA") == 0
554 centerCoord = afwGeom.SpanSet.fromMask(maskx, 1).computeCentroid()
556 ctrl = self.config.coaddPsf.makeControl()
558 catalog, wcs, centerCoord, ctrl.warpingKernelName, ctrl.cacheSize
564 GetTemplateConnections,
565 dimensions=(
"instrument",
"visit",
"detector",
"skymap"),
566 defaultTemplates={
"coaddName":
"dcr",
"warpTypeSuffix":
"",
"fakesType":
""},
568 visitInfo = pipeBase.connectionTypes.Input(
569 doc=
"VisitInfo of calexp used to determine observing conditions.",
570 name=
"{fakesType}calexp.visitInfo",
571 storageClass=
"VisitInfo",
572 dimensions=(
"instrument",
"visit",
"detector"),
574 dcrCoadds = pipeBase.connectionTypes.Input(
575 doc=
"Input DCR template to match and subtract from the exposure",
576 name=
"{fakesType}dcrCoadd{warpTypeSuffix}",
577 storageClass=
"ExposureF",
578 dimensions=(
"tract",
"patch",
"skymap",
"band",
"subfilter"),
583 def __init__(self, *, config=None):
584 super().__init__(config=config)
585 self.inputs.remove(
"coaddExposures")
588class GetDcrTemplateConfig(
589 GetTemplateConfig, pipelineConnections=GetDcrTemplateConnections
591 numSubfilters = pexConfig.Field(
592 doc=
"Number of subfilters in the DcrCoadd.",
596 effectiveWavelength = pexConfig.Field(
597 doc=
"Effective wavelength of the filter in nm.",
601 bandwidth = pexConfig.Field(
602 doc=
"Bandwidth of the physical filter.",
608 if self.effectiveWavelength
is None or self.bandwidth
is None:
610 "The effective wavelength and bandwidth of the physical filter "
611 "must be set in the getTemplate config for DCR coadds. "
612 "Required until transmission curves are used in DM-13668."
616class GetDcrTemplateTask(GetTemplateTask):
617 ConfigClass = GetDcrTemplateConfig
618 _DefaultName =
"getDcrTemplate"
620 def runQuantum(self, butlerQC, inputRefs, outputRefs):
621 inputs = butlerQC.get(inputRefs)
622 bbox = inputs.pop(
"bbox")
623 wcs = inputs.pop(
"wcs")
624 dcrCoaddExposureHandles = inputs.pop(
"dcrCoadds")
625 skymap = inputs.pop(
"skyMap")
626 visitInfo = inputs.pop(
"visitInfo")
629 assert not inputs,
"runQuantum got more inputs than expected"
631 results = self.getExposures(
632 dcrCoaddExposureHandles, bbox, skymap, wcs, visitInfo
634 physical_filter = butlerQC.quantum.dataId[
"physical_filter"]
636 coaddExposureHandles=results.coaddExposures,
639 dataIds=results.dataIds,
640 physical_filter=physical_filter,
642 butlerQC.put(outputs, outputRefs)
645 reason=
"Replaced by getExposures, which uses explicit arguments instead of a kwargs dict. "
646 "This method will be removed after v29.",
648 category=FutureWarning,
650 def getOverlappingExposures(self, inputs):
651 return self.getExposures(
659 def getExposures(self, dcrCoaddExposureHandles, bbox, skymap, wcs, visitInfo):
660 """Return lists of coadds and their corresponding dataIds that overlap
663 The spatial index in the registry has generous padding and often
664 supplies patches near, but not directly overlapping the detector.
665 Filters inputs so that we don't have to read in all input coadds.
669 dcrCoaddExposureHandles : `list` \
670 [`lsst.daf.butler.DeferredDatasetHandle` of \
671 `lsst.afw.image.Exposure`]
672 Data references to exposures that might overlap the detector.
673 bbox : `lsst.geom.Box2I`
674 Template Bounding box of the detector geometry onto which to
675 resample the coaddExposures.
676 skymap : `lsst.skymap.SkyMap`
677 Input definition of geometry/bbox and projection/wcs for
679 wcs : `lsst.afw.geom.SkyWcs`
680 Template WCS onto which to resample the coaddExposures.
681 visitInfo : `lsst.afw.image.VisitInfo`
682 Metadata for the science image.
686 result : `lsst.pipe.base.Struct`
687 A struct with attibutes:
690 Dict of coadd exposures that overlap the projected bbox,
692 (`dict` [`int`, `list` [`lsst.afw.image.Exposure`] ]).
694 Dict of data IDs of the coadd exposures that overlap the
695 projected bbox, indexed on tract id
696 (`dict` [`int`, `list [`lsst.daf.butler.DataCoordinate`] ]).
701 Raised if no patches overlatp the input detector bbox.
706 raise pipeBase.NoWorkFound(
"Exposure has no WCS; cannot create a template.")
710 dataIds = collections.defaultdict(list)
712 for coaddRef
in dcrCoaddExposureHandles:
713 dataId = coaddRef.dataId
714 subfilter = dataId[
"subfilter"]
715 patchWcs = skymap[dataId[
"tract"]].getWcs()
716 patchBBox = skymap[dataId[
"tract"]][dataId[
"patch"]].getOuterBBox()
717 patchCorners = patchWcs.pixelToSky(
geom.Box2D(patchBBox).getCorners())
719 if patchPolygon.intersection(detectorPolygon):
720 overlappingArea += patchPolygon.intersectionSingle(
724 "Using template input tract=%s, patch=%s, subfilter=%s"
725 % (dataId[
"tract"], dataId[
"patch"], dataId[
"subfilter"])
727 if dataId[
"tract"]
in patchList:
728 patchList[dataId[
"tract"]].append(dataId[
"patch"])
730 patchList[dataId[
"tract"]] = [
734 dataIds[dataId[
"tract"]].append(dataId)
736 if not overlappingArea:
737 raise pipeBase.NoWorkFound(
"No patches overlap detector")
739 self.checkPatchList(patchList)
741 coaddExposures = self.getDcrModel(patchList, dcrCoaddExposureHandles, visitInfo)
742 return pipeBase.Struct(coaddExposures=coaddExposures, dataIds=dataIds)
745 """Check that all of the DcrModel subfilters are present for each
751 Dict of the patches containing valid data for each tract.
756 If the number of exposures found for a patch does not match the
757 number of subfilters.
759 for tract
in patchList:
760 for patch
in set(patchList[tract]):
761 if patchList[tract].count(patch) != self.config.numSubfilters:
763 "Invalid number of DcrModel subfilters found: %d vs %d expected",
764 patchList[tract].count(patch),
765 self.config.numSubfilters,
769 """Build DCR-matched coadds from a list of exposure references.
774 Dict of the patches containing valid data for each tract.
775 coaddRefs : `list` [`lsst.daf.butler.DeferredDatasetHandle`]
776 Data references to `~lsst.afw.image.Exposure` representing
777 DcrModels that overlap the detector.
778 visitInfo : `lsst.afw.image.VisitInfo`
779 Metadata for the science image.
783 coaddExposures : `list` [`lsst.afw.image.Exposure`]
784 Coadd exposures that overlap the detector.
786 coaddExposures = collections.defaultdict(list)
787 for tract
in patchList:
788 for patch
in set(patchList[tract]):
791 for coaddRef
in coaddRefs
795 dcrModel = DcrModel.fromQuantum(
797 self.config.effectiveWavelength,
798 self.config.bandwidth,
799 self.config.numSubfilters,
801 coaddExposures[tract].append(dcrModel.buildMatchedExposureHandle(visitInfo=visitInfo))
802 return coaddExposures
806 condition = (coaddRef.dataId[
"tract"] == tract) & (
807 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)