24from deprecated.sphinx
import deprecated
26import lsst.afw.image
as afwImage
36from lsst.utils.timer
import timeMethod
38__all__ = [
"GetTemplateTask",
"GetTemplateConfig",
39 "GetDcrTemplateTask",
"GetDcrTemplateConfig"]
43 dimensions=(
"instrument",
"visit",
"detector",
"skymap"),
44 defaultTemplates={
"coaddName":
"goodSeeing",
47 bbox = pipeBase.connectionTypes.Input(
48 doc=
"Bounding box of exposure to determine the geometry of the output template.",
49 name=
"{fakesType}calexp.bbox",
51 dimensions=(
"instrument",
"visit",
"detector"),
53 wcs = pipeBase.connectionTypes.Input(
54 doc=
"WCS of the exposure that we will construct the template for.",
55 name=
"{fakesType}calexp.wcs",
57 dimensions=(
"instrument",
"visit",
"detector"),
59 skyMap = pipeBase.connectionTypes.Input(
60 doc=
"Geometry of the tracts and patches that the coadds are defined on.",
61 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
62 dimensions=(
"skymap", ),
63 storageClass=
"SkyMap",
65 coaddExposures = pipeBase.connectionTypes.Input(
66 doc=
"Coadds that may overlap the desired region, as possible inputs to the template."
67 " Will be restricted to those that directly overlap the projected bounding box.",
68 dimensions=(
"tract",
"patch",
"skymap",
"band"),
69 storageClass=
"ExposureF",
70 name=
"{fakesType}{coaddName}Coadd{warpTypeSuffix}",
73 deferGraphConstraint=
True
76 template = pipeBase.connectionTypes.Output(
77 doc=
"Warped template, pixel matched to the bounding box and WCS.",
78 dimensions=(
"instrument",
"visit",
"detector"),
79 storageClass=
"ExposureF",
80 name=
"{fakesType}{coaddName}Diff_templateExp{warpTypeSuffix}",
84class GetTemplateConfig(pipeBase.PipelineTaskConfig,
85 pipelineConnections=GetTemplateConnections):
86 templateBorderSize = pexConfig.Field(
89 doc=
"Number of pixels to grow the requested template image to account for warping"
91 warp = pexConfig.ConfigField(
92 dtype=afwMath.Warper.ConfigClass,
93 doc=
"warper configuration",
95 coaddPsf = pexConfig.ConfigField(
96 doc=
"Configuration for CoaddPsf",
100 def setDefaults(self):
103 self.warp.cacheSize = 100000
104 self.coaddPsf.cacheSize = self.warp.cacheSize
107 self.warp.interpLength = 100
108 self.warp.warpingKernelName =
'lanczos3'
109 self.coaddPsf.warpingKernelName = self.warp.warpingKernelName
112class GetTemplateTask(pipeBase.PipelineTask):
113 ConfigClass = GetTemplateConfig
114 _DefaultName =
"getTemplate"
116 def __init__(self, *args, **kwargs):
117 super().__init__(*args, **kwargs)
118 self.warper = afwMath.Warper.fromConfig(self.config.warp)
119 self.schema = afwTable.ExposureTable.makeMinimalSchema()
120 self.schema.addField(
'tract', type=np.int32, doc=
'Which tract this exposure came from.')
121 self.schema.addField(
'patch', type=np.int32, doc=
'Which patch in the tract this exposure came from.')
122 self.schema.addField(
'weight', type=float,
123 doc=
'Weight for each exposure, used to make the CoaddPsf; should always be 1.')
125 def runQuantum(self, butlerQC, inputRefs, outputRefs):
126 inputs = butlerQC.get(inputRefs)
127 bbox = inputs.pop(
"bbox")
128 wcs = inputs.pop(
"wcs")
129 coaddExposures = inputs.pop(
'coaddExposures')
130 skymap = inputs.pop(
"skyMap")
133 assert not inputs,
"runQuantum got more inputs than expected"
135 results = self.getExposures(coaddExposures, bbox, skymap, wcs)
136 physical_filter = butlerQC.quantum.dataId[
"physical_filter"]
137 outputs = self.run(coaddExposures=results.coaddExposures,
140 dataIds=results.dataIds,
141 physical_filter=physical_filter)
142 butlerQC.put(outputs, outputRefs)
144 @deprecated(reason=
"Replaced by getExposures, which uses explicit arguments instead of a kwargs dict. "
145 "This method will be removed after v29.",
146 version=
"v29.0", category=FutureWarning)
147 def getOverlappingExposures(self, inputs):
148 return self.getExposures(inputs[
"coaddExposures"],
153 def getExposures(self, coaddExposureHandles, bbox, skymap, wcs):
154 """Return a data structure containing the coadds that overlap the
155 specified bbox projected onto the sky, and a corresponding data
156 structure of their dataIds.
157 These are the appropriate inputs to this task's `run` method.
159 The spatial index in the butler registry has generous padding and often
160 supplies patches near, but not directly overlapping the desired region.
161 This method filters the inputs so that `run` does not have to read in
162 all possibly-matching coadd exposures.
166 coaddExposureHandles : `iterable` \
167 [`lsst.daf.butler.DeferredDatasetHandle` of \
168 `lsst.afw.image.Exposure`]
169 Dataset handles to exposures that might overlap the desired
171 bbox : `lsst.geom.Box2I`
172 Template bounding box of the pixel geometry onto which the
173 coaddExposures will be resampled.
174 skymap : `lsst.skymap.SkyMap`
175 Geometry of the tracts and patches the coadds are defined on.
176 wcs : `lsst.afw.geom.SkyWcs`
177 Template WCS onto which the coadds will be resampled.
181 result : `lsst.pipe.base.Struct`
182 A struct with attributes:
185 Dict of coadd exposures that overlap the projected bbox,
187 (`dict` [`int`, `list` [`lsst.afw.image.Exposure`] ]).
189 Dict of data IDs of the coadd exposures that overlap the
190 projected bbox, indexed on tract id
191 (`dict` [`int`, `list [`lsst.daf.butler.DataCoordinate`] ]).
196 Raised if no patches overlap the input detector bbox, or the input
200 raise pipeBase.NoWorkFound(
"WCS is None; cannot find overlapping exposures.")
205 coaddExposures = collections.defaultdict(list)
206 dataIds = collections.defaultdict(list)
208 for coaddRef
in coaddExposureHandles:
209 dataId = coaddRef.dataId
210 patchWcs = skymap[dataId[
'tract']].getWcs()
211 patchBBox = skymap[dataId[
'tract']][dataId[
'patch']].getOuterBBox()
212 patchCorners = patchWcs.pixelToSky(
geom.Box2D(patchBBox).getCorners())
214 if patchPolygon.intersection(detectorPolygon):
215 overlappingArea += patchPolygon.intersectionSingle(detectorPolygon).calculateArea()
216 self.log.info(
"Using template input tract=%s, patch=%s", dataId[
'tract'], dataId[
'patch'])
217 coaddExposures[dataId[
'tract']].append(coaddRef.get())
218 dataIds[dataId[
'tract']].append(dataId)
220 if not overlappingArea:
221 raise pipeBase.NoWorkFound(
'No patches overlap detector')
223 return pipeBase.Struct(coaddExposures=coaddExposures,
227 def run(self, *, coaddExposures, bbox, wcs, dataIds, physical_filter):
228 """Warp coadds from multiple tracts and patches to form a template to
229 subtract from a science image.
231 Tract and patch overlap regions are combined by a variance-weighted
232 average, and the variance planes are combined with the same weights,
233 not added in quadrature; the overlap regions are not statistically
234 independent, because they're derived from the same original data.
235 The PSF on the template is created by combining the CoaddPsf on each
236 template image into a meta-CoaddPsf.
240 coaddExposures : `dict` [`int`, `list` [`lsst.afw.image.Exposure`]]
241 Coadds to be mosaicked, indexed on tract id.
242 bbox : `lsst.geom.Box2I`
243 Template Bounding box of the detector geometry onto which to
244 resample the ``coaddExposures``. Modified in-place to include the
246 wcs : `lsst.afw.geom.SkyWcs`
247 Template WCS onto which to resample the ``coaddExposures``.
248 dataIds : `dict` [`int`, `list` [`lsst.daf.butler.DataCoordinate`]]
249 Record of the tract and patch of each coaddExposure, indexed on
251 physical_filter : `str`
252 Physical filter of the science image.
256 result : `lsst.pipe.base.Struct`
257 A struct with attributes:
260 A template coadd exposure assembled out of patches
261 (`lsst.afw.image.ExposureF`).
266 If no coadds are found with sufficient un-masked pixels.
268 band, photoCalib = self._checkInputs(dataIds, coaddExposures)
270 bbox.grow(self.config.templateBorderSize)
274 for tract
in coaddExposures:
275 maskedImages, catalog, totalBox = self._makeExposureCatalog(coaddExposures[tract],
278 unwarped = self._merge(maskedImages, totalBox, catalog[0].wcs)
279 potentialInput = self.warper.warpExposure(wcs, unwarped, destBBox=bbox)
280 if not np.any(np.isfinite(potentialInput.image.array)):
281 self.log.info(
"No overlap from coadds in tract %s; not including in output.", tract)
283 catalogs.append(catalog)
284 warped[tract] = potentialInput
285 warped[tract].setWcs(wcs)
288 raise pipeBase.NoWorkFound(
"No patches found to overlap science exposure.")
289 template = self._merge([x.maskedImage
for x
in warped.values()], bbox, wcs)
293 catalog.reserve(sum([len(c)
for c
in catalogs]))
297 template.setPsf(self._makePsf(template, catalog, wcs))
299 template.setPhotoCalib(photoCalib)
300 return pipeBase.Struct(template=template)
303 def _checkInputs(dataIds, coaddExposures):
304 """Check that the all the dataIds are from the same band and that
305 the exposures all have the same photometric calibration.
309 dataIds : `dict` [`int`, `list` [`lsst.daf.butler.DataCoordinate`]]
310 Record of the tract and patch of each coaddExposure.
311 coaddExposures : `dict` [`int`, `list` [`lsst.afw.image.Exposure`]]
312 Coadds to be mosaicked.
317 Filter band of all the input exposures.
318 photoCalib : `lsst.afw.image.PhotoCalib`
319 Photometric calibration of all of the input exposures.
324 Raised if the bands or calibrations of the input exposures are not
327 bands = set(dataId[
"band"]
for tract
in dataIds
for dataId
in dataIds[tract])
329 raise RuntimeError(f
"GetTemplateTask called with multiple bands: {bands}")
331 photoCalibs = [exposure.photoCalib
for exposures
in coaddExposures.values()
for exposure
in exposures]
332 if not all([photoCalibs[0] == x
for x
in photoCalibs]):
333 msg = f
"GetTemplateTask called with exposures with different photoCalibs: {photoCalibs}"
334 raise RuntimeError(msg)
335 photoCalib = photoCalibs[0]
336 return band, photoCalib
339 """Make an exposure catalog for one tract.
343 exposures : `list` [`lsst.afw.image.Exposuref`]
344 Exposures to include in the catalog.
345 dataIds : `list` [`lsst.daf.butler.DataCoordinate`]
346 Data ids of each of the included exposures; must have "tract" and
351 images : `list` [`lsst.afw.image.MaskedImage`]
352 MaskedImages of each of the input exposures, for warping.
353 catalog : `lsst.afw.table.ExposureCatalog`
354 Catalog of metadata for each exposure
355 totalBox : `lsst.geom.Box2I`
356 The union of the bounding boxes of all the input exposures.
359 catalog.reserve(len(exposures))
360 images = [exposure.maskedImage
for exposure
in exposures]
362 for coadd, dataId
in zip(exposures, dataIds):
363 totalBox = totalBox.expandedTo(coadd.getBBox())
364 record = catalog.addNew()
365 record.setPsf(coadd.psf)
366 record.setWcs(coadd.wcs)
367 record.setPhotoCalib(coadd.photoCalib)
368 record.setBBox(coadd.getBBox())
370 record.set(
"tract", dataId[
"tract"])
371 record.set(
"patch", dataId[
"patch"])
374 record.set(
"weight", 1)
376 return images, catalog, totalBox
379 def _merge(maskedImages, bbox, wcs):
380 """Merge the images that came from one tract into one larger image,
381 ignoring NaN pixels and non-finite variance pixels from individual
386 maskedImages : `list` [`lsst.afw.image.MaskedImage`]
387 Images to be merged into one larger bounding box.
388 bbox : `lsst.geom.Box2I`
389 Bounding box defining the image to merge into.
390 wcs : `lsst.afw.geom.SkyWcs`
391 WCS of all of the input images to set on the output image.
395 merged : `lsst.afw.image.MaskedImage`
396 Merged image with all of the inputs at their respective bbox
399 merged = afwImage.ExposureF(bbox, wcs)
400 weights = afwImage.ImageF(bbox)
401 for maskedImage
in maskedImages:
403 good = maskedImage.variance.array > 0
404 weight = afwImage.ImageF(maskedImage.getBBox())
405 weight.array[good] = maskedImage.variance.array[good]**(-0.5)
406 bad = np.isnan(maskedImage.image.array) | ~good
409 maskedImage.image.array[bad] = 0.0
410 maskedImage.variance.array[bad] = 0.0
412 maskedImage.mask.array[bad] = 0
416 maskedImage.image *= weight
417 maskedImage.variance *= weight
418 merged.maskedImage[maskedImage.getBBox()] += maskedImage
419 weights[maskedImage.getBBox()] += weight
421 inverseWeights = np.zeros_like(weights.array)
422 good = weights.array > 0
423 inverseWeights[good] = 1/weights.array[good]
428 merged.image.array *= inverseWeights
429 merged.variance.array *= inverseWeights
430 merged.mask.array |= merged.mask.getPlaneBitMask(
"NO_DATA") * (inverseWeights == 0)
435 """Return a PSF containing the PSF at each of the input regions.
437 Note that although this includes all the exposures from the catalog,
438 the PSF knows which part of the template the inputs came from, so when
439 evaluated at a given position it will not include inputs that never
440 went in to those pixels.
444 template : `lsst.afw.image.Exposure`
445 Generated template the PSF is for.
446 catalog : `lsst.afw.table.ExposureCatalog`
447 Catalog of exposures that went into the template that contains all
449 wcs : `lsst.afw.geom.SkyWcs`
450 WCS of the template, to warp the PSFs to.
454 coaddPsf : `lsst.meas.algorithms.CoaddPsf`
455 The meta-psf constructed from all of the input catalogs.
459 boolmask = template.mask.array & template.mask.getPlaneBitMask(
'NO_DATA') == 0
461 centerCoord = afwGeom.SpanSet.fromMask(maskx, 1).computeCentroid()
463 ctrl = self.config.coaddPsf.makeControl()
464 coaddPsf =
CoaddPsf(catalog, wcs, centerCoord, ctrl.warpingKernelName, ctrl.cacheSize)
469 dimensions=(
"instrument",
"visit",
"detector",
"skymap"),
470 defaultTemplates={
"coaddName":
"dcr",
471 "warpTypeSuffix":
"",
473 visitInfo = pipeBase.connectionTypes.Input(
474 doc=
"VisitInfo of calexp used to determine observing conditions.",
475 name=
"{fakesType}calexp.visitInfo",
476 storageClass=
"VisitInfo",
477 dimensions=(
"instrument",
"visit",
"detector"),
479 dcrCoadds = pipeBase.connectionTypes.Input(
480 doc=
"Input DCR template to match and subtract from the exposure",
481 name=
"{fakesType}dcrCoadd{warpTypeSuffix}",
482 storageClass=
"ExposureF",
483 dimensions=(
"tract",
"patch",
"skymap",
"band",
"subfilter"),
488 def __init__(self, *, config=None):
489 super().__init__(config=config)
490 self.inputs.remove(
"coaddExposures")
493class GetDcrTemplateConfig(GetTemplateConfig,
494 pipelineConnections=GetDcrTemplateConnections):
495 numSubfilters = pexConfig.Field(
496 doc=
"Number of subfilters in the DcrCoadd.",
500 effectiveWavelength = pexConfig.Field(
501 doc=
"Effective wavelength of the filter in nm.",
505 bandwidth = pexConfig.Field(
506 doc=
"Bandwidth of the physical filter.",
512 if self.effectiveWavelength
is None or self.bandwidth
is None:
513 raise ValueError(
"The effective wavelength and bandwidth of the physical filter "
514 "must be set in the getTemplate config for DCR coadds. "
515 "Required until transmission curves are used in DM-13668.")
518class GetDcrTemplateTask(GetTemplateTask):
519 ConfigClass = GetDcrTemplateConfig
520 _DefaultName =
"getDcrTemplate"
522 def runQuantum(self, butlerQC, inputRefs, outputRefs):
523 inputs = butlerQC.get(inputRefs)
524 bbox = inputs.pop(
"bbox")
525 wcs = inputs.pop(
"wcs")
526 dcrCoaddExposureHandles = inputs.pop(
'dcrCoadds')
527 skymap = inputs.pop(
"skyMap")
528 visitInfo = inputs.pop(
"visitInfo")
531 assert not inputs,
"runQuantum got more inputs than expected"
533 results = self.getExposures(dcrCoaddExposureHandles, bbox, skymap, wcs, visitInfo)
534 physical_filter = butlerQC.quantum.dataId[
"physical_filter"]
535 outputs = self.run(coaddExposures=results.coaddExposures,
538 dataIds=results.dataIds,
539 physical_filter=physical_filter)
540 butlerQC.put(outputs, outputRefs)
542 @deprecated(reason=
"Replaced by getExposures, which uses explicit arguments instead of a kwargs dict. "
543 "This method will be removed after v29.",
544 version=
"v29.0", category=FutureWarning)
545 def getOverlappingExposures(self, inputs):
546 return self.getExposures(inputs[
"dcrCoadds"],
552 def getExposures(self, dcrCoaddExposureHandles, bbox, skymap, wcs, visitInfo):
553 """Return lists of coadds and their corresponding dataIds that overlap
556 The spatial index in the registry has generous padding and often
557 supplies patches near, but not directly overlapping the detector.
558 Filters inputs so that we don't have to read in all input coadds.
562 dcrCoaddExposureHandles : `list` \
563 [`lsst.daf.butler.DeferredDatasetHandle` of \
564 `lsst.afw.image.Exposure`]
565 Data references to exposures that might overlap the detector.
566 bbox : `lsst.geom.Box2I`
567 Template Bounding box of the detector geometry onto which to
568 resample the coaddExposures.
569 skymap : `lsst.skymap.SkyMap`
570 Input definition of geometry/bbox and projection/wcs for
572 wcs : `lsst.afw.geom.SkyWcs`
573 Template WCS onto which to resample the coaddExposures.
574 visitInfo : `lsst.afw.image.VisitInfo`
575 Metadata for the science image.
579 result : `lsst.pipe.base.Struct`
580 A struct with attibutes:
583 Dict of coadd exposures that overlap the projected bbox,
585 (`dict` [`int`, `list` [`lsst.afw.image.Exposure`] ]).
587 Dict of data IDs of the coadd exposures that overlap the
588 projected bbox, indexed on tract id
589 (`dict` [`int`, `list [`lsst.daf.butler.DataCoordinate`] ]).
594 Raised if no patches overlatp the input detector bbox.
599 raise pipeBase.NoWorkFound(
"Exposure has no WCS; cannot create a template.")
603 dataIds = collections.defaultdict(list)
605 for coaddRef
in dcrCoaddExposureHandles:
606 dataId = coaddRef.dataId
607 patchWcs = skymap[dataId[
'tract']].getWcs()
608 patchBBox = skymap[dataId[
'tract']][dataId[
'patch']].getOuterBBox()
609 patchCorners = patchWcs.pixelToSky(
geom.Box2D(patchBBox).getCorners())
611 if patchPolygon.intersection(detectorPolygon):
612 overlappingArea += patchPolygon.intersectionSingle(detectorPolygon).calculateArea()
613 self.log.info(
"Using template input tract=%s, patch=%s, subfilter=%s" %
614 (dataId[
'tract'], dataId[
'patch'], dataId[
"subfilter"]))
615 if dataId[
'tract']
in patchList:
616 patchList[dataId[
'tract']].append(dataId[
'patch'])
618 patchList[dataId[
'tract']] = [dataId[
'patch'], ]
619 dataIds[dataId[
'tract']].append(dataId)
621 if not overlappingArea:
622 raise pipeBase.NoWorkFound(
'No patches overlap detector')
624 self.checkPatchList(patchList)
626 coaddExposures = self.getDcrModel(patchList, dcrCoaddExposureHandles, visitInfo)
627 return pipeBase.Struct(coaddExposures=coaddExposures,
631 """Check that all of the DcrModel subfilters are present for each
637 Dict of the patches containing valid data for each tract.
642 If the number of exposures found for a patch does not match the
643 number of subfilters.
645 for tract
in patchList:
646 for patch
in set(patchList[tract]):
647 if patchList[tract].count(patch) != self.config.numSubfilters:
648 raise RuntimeError(
"Invalid number of DcrModel subfilters found: %d vs %d expected",
649 patchList[tract].count(patch), self.config.numSubfilters)
652 """Build DCR-matched coadds from a list of exposure references.
657 Dict of the patches containing valid data for each tract.
658 coaddRefs : `list` [`lsst.daf.butler.DeferredDatasetHandle`]
659 Data references to `~lsst.afw.image.Exposure` representing
660 DcrModels that overlap the detector.
661 visitInfo : `lsst.afw.image.VisitInfo`
662 Metadata for the science image.
666 coaddExposures : `list` [`lsst.afw.image.Exposure`]
667 Coadd exposures that overlap the detector.
669 coaddExposures = collections.defaultdict(list)
670 for tract
in patchList:
671 for patch
in set(patchList[tract]):
672 coaddRefList = [coaddRef
for coaddRef
in coaddRefs
675 dcrModel = DcrModel.fromQuantum(coaddRefList,
676 self.config.effectiveWavelength,
677 self.config.bandwidth,
678 self.config.numSubfilters)
679 coaddExposures[tract].append(dcrModel.buildMatchedExposure(visitInfo=visitInfo))
680 return coaddExposures
684 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)
run(self, *, coaddExposures, bbox, wcs, dataIds, physical_filter)
_makeExposureCatalog(self, exposures, dataIds)
getDcrModel(self, patchList, coaddRefs, visitInfo)
_makePsf(self, template, catalog, wcs)