35from lsst.utils.timer
import timeMethod
37__all__ = [
"GetTemplateTask",
"GetTemplateConfig",
38 "GetDcrTemplateTask",
"GetDcrTemplateConfig"]
42 dimensions=(
"instrument",
"visit",
"detector",
"skymap"),
43 defaultTemplates={
"coaddName":
"goodSeeing",
46 bbox = pipeBase.connectionTypes.Input(
47 doc=
"Bounding box of exposure to determine the geometry of the output template.",
48 name=
"{fakesType}calexp.bbox",
50 dimensions=(
"instrument",
"visit",
"detector"),
52 wcs = pipeBase.connectionTypes.Input(
53 doc=
"WCS of the exposure that we will construct the template for.",
54 name=
"{fakesType}calexp.wcs",
56 dimensions=(
"instrument",
"visit",
"detector"),
58 skyMap = pipeBase.connectionTypes.Input(
59 doc=
"Geometry of the tracts and patches that the coadds are defined on.",
60 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
61 dimensions=(
"skymap", ),
62 storageClass=
"SkyMap",
64 coaddExposures = pipeBase.connectionTypes.Input(
65 doc=
"Coadds that may overlap the desired region, as possible inputs to the template."
66 " Will be restricted to those that directly overlap the projected bounding box.",
67 dimensions=(
"tract",
"patch",
"skymap",
"band"),
68 storageClass=
"ExposureF",
69 name=
"{fakesType}{coaddName}Coadd{warpTypeSuffix}",
74 template = pipeBase.connectionTypes.Output(
75 doc=
"Warped template, pixel matched to the bounding box and WCS.",
76 dimensions=(
"instrument",
"visit",
"detector"),
77 storageClass=
"ExposureF",
78 name=
"{fakesType}{coaddName}Diff_templateExp{warpTypeSuffix}",
82class GetTemplateConfig(pipeBase.PipelineTaskConfig,
83 pipelineConnections=GetTemplateConnections):
84 templateBorderSize = pexConfig.Field(
87 doc=
"Number of pixels to grow the requested template image to account for warping"
89 warp = pexConfig.ConfigField(
90 dtype=afwMath.Warper.ConfigClass,
91 doc=
"warper configuration",
93 coaddPsf = pexConfig.ConfigField(
94 doc=
"Configuration for CoaddPsf",
98 def setDefaults(self):
99 self.warp.warpingKernelName =
'lanczos5'
100 self.coaddPsf.warpingKernelName =
'lanczos5'
103class GetTemplateTask(pipeBase.PipelineTask):
104 ConfigClass = GetTemplateConfig
105 _DefaultName =
"getTemplate"
107 def __init__(self, *args, **kwargs):
108 super().__init__(*args, **kwargs)
109 self.warper = afwMath.Warper.fromConfig(self.config.warp)
110 self.schema = afwTable.ExposureTable.makeMinimalSchema()
111 self.schema.addField(
'tract', type=np.int32, doc=
'Which tract this exposure came from.')
112 self.schema.addField(
'patch', type=np.int32, doc=
'Which patch in the tract this exposure came from.')
113 self.schema.addField(
'weight', type=float,
114 doc=
'Weight for each exposure, used to make the CoaddPsf; should always be 1.')
116 def runQuantum(self, butlerQC, inputRefs, outputRefs):
117 inputs = butlerQC.get(inputRefs)
118 results = self.getOverlappingExposures(inputs)
120 inputs[
"coaddExposures"] = results.coaddExposures
121 inputs[
"dataIds"] = results.dataIds
122 inputs[
"physical_filter"] = butlerQC.quantum.dataId[
"physical_filter"]
123 outputs = self.run(**inputs)
124 butlerQC.put(outputs, outputRefs)
126 def getOverlappingExposures(self, inputs):
127 """Return a data structure containing the coadds that overlap the
128 specified bbox projected onto the sky, and a corresponding data
129 structure of their dataIds.
130 These are the appropriate inputs to this task's `run` method.
132 The spatial index in the butler registry has generous padding and often
133 supplies patches near, but not directly overlapping the desired region.
134 This method filters the inputs so that `run` does not have to read in
135 all possibly-matching coadd exposures.
139 inputs : `dict` of task Inputs, containing:
140 - coaddExposures : `list` \
141 [`lsst.daf.butler.DeferredDatasetHandle` of \
142 `lsst.afw.image.Exposure`]
143 Data references to exposures that might overlap the desired
145 - bbox : `lsst.geom.Box2I`
146 Template bounding box of the pixel geometry onto which the
147 coaddExposures will be resampled.
148 - skyMap : `lsst.skymap.SkyMap`
149 Geometry of the tracts and patches the coadds are defined on.
150 - wcs : `lsst.afw.geom.SkyWcs`
151 Template WCS onto which the coadds will be resampled.
155 result : `lsst.pipe.base.Struct`
156 A struct with attributes:
159 Dict of coadd exposures that overlap the projected bbox,
161 (`dict` [`int`, `list` [`lsst.afw.image.Exposure`] ]).
163 Dict of data IDs of the coadd exposures that overlap the
164 projected bbox, indexed on tract id
165 (`dict` [`int`, `list [`lsst.daf.butler.DataCoordinate`] ]).
170 Raised if no patches overlap the input detector bbox, or the input
173 if (wcs := inputs[
'wcs'])
is None:
174 raise pipeBase.NoWorkFound(
"Exposure has no WCS; cannot create a template.")
179 coaddExposures = collections.defaultdict(list)
180 dataIds = collections.defaultdict(list)
182 skymap = inputs[
'skyMap']
183 for coaddRef
in inputs[
'coaddExposures']:
184 dataId = coaddRef.dataId
185 patchWcs = skymap[dataId[
'tract']].getWcs()
186 patchBBox = skymap[dataId[
'tract']][dataId[
'patch']].getOuterBBox()
187 patchCorners = patchWcs.pixelToSky(
geom.Box2D(patchBBox).getCorners())
189 if patchPolygon.intersection(detectorPolygon):
190 overlappingArea += patchPolygon.intersectionSingle(detectorPolygon).calculateArea()
191 self.log.info(
"Using template input tract=%s, patch=%s", dataId[
'tract'], dataId[
'patch'])
192 coaddExposures[dataId[
'tract']].append(coaddRef.get())
193 dataIds[dataId[
'tract']].append(dataId)
195 if not overlappingArea:
196 raise pipeBase.NoWorkFound(
'No patches overlap detector')
198 return pipeBase.Struct(coaddExposures=coaddExposures,
202 def run(self, coaddExposures, bbox, wcs, dataIds, physical_filter):
203 """Warp coadds from multiple tracts and patches to form a template to
204 subtract from a science image.
206 Tract and patch overlap regions are combined by a variance-weighted
207 average, and the variance planes are combined with the same weights,
208 not added in quadrature; the overlap regions are not statistically
209 independent, because they're derived from the same original data.
210 The PSF on the template is created by combining the CoaddPsf on each
211 template image into a meta-CoaddPsf.
215 coaddExposures : `dict` [`int`, `list` [`lsst.afw.image.Exposure`]]
216 Coadds to be mosaicked, indexed on tract id.
217 bbox : `lsst.geom.Box2I`
218 Template Bounding box of the detector geometry onto which to
219 resample the ``coaddExposures``. Modified in-place to include the
221 wcs : `lsst.afw.geom.SkyWcs`
222 Template WCS onto which to resample the ``coaddExposures``.
223 dataIds : `dict` [`int`, `list` [`lsst.daf.butler.DataCoordinate`]]
224 Record of the tract and patch of each coaddExposure, indexed on
226 physical_filter : `str`
227 Physical filter of the science image.
231 result : `lsst.pipe.base.Struct`
232 A struct with attributes:
235 A template coadd exposure assembled out of patches
236 (`lsst.afw.image.ExposureF`).
241 If no coadds are found with sufficient un-masked pixels.
243 band, photoCalib = self._checkInputs(dataIds, coaddExposures)
245 bbox.grow(self.config.templateBorderSize)
249 for tract
in coaddExposures:
250 maskedImages, catalog, totalBox = self._makeExposureCatalog(coaddExposures[tract],
253 unwarped = self._merge(maskedImages, totalBox, catalog[0].wcs)
254 potentialInput = self.warper.warpExposure(wcs, unwarped, destBBox=bbox)
255 if not np.any(np.isfinite(potentialInput.image.array)):
256 self.log.info(
"No overlap from coadds in tract %s; not including in output.", tract)
258 catalogs.append(catalog)
259 warped[tract] = potentialInput
260 warped[tract].setWcs(wcs)
263 raise pipeBase.NoWorkFound(
"No patches found to overlap science exposure.")
264 template = self._merge([x.maskedImage
for x
in warped.values()], bbox, wcs)
268 catalog.reserve(sum([len(c)
for c
in catalogs]))
272 template.setPsf(self._makePsf(template, catalog, wcs))
274 template.setPhotoCalib(photoCalib)
275 return pipeBase.Struct(template=template)
278 def _checkInputs(dataIds, coaddExposures):
279 """Check that the all the dataIds are from the same band and that
280 the exposures all have the same photometric calibration.
284 dataIds : `dict` [`int`, `list` [`lsst.daf.butler.DataCoordinate`]]
285 Record of the tract and patch of each coaddExposure.
286 coaddExposures : `dict` [`int`, `list` [`lsst.afw.image.Exposure`]]
287 Coadds to be mosaicked.
292 Filter band of all the input exposures.
293 photoCalib : `lsst.afw.image.PhotoCalib`
294 Photometric calibration of all of the input exposures.
299 Raised if the bands or calibrations of the input exposures are not
302 bands = set(dataId[
"band"]
for tract
in dataIds
for dataId
in dataIds[tract])
304 raise RuntimeError(f
"GetTemplateTask called with multiple bands: {bands}")
306 photoCalibs = [exposure.photoCalib
for exposures
in coaddExposures.values()
for exposure
in exposures]
307 if not all([photoCalibs[0] == x
for x
in photoCalibs]):
308 msg = f
"GetTemplateTask called with exposures with different photoCalibs: {photoCalibs}"
309 raise RuntimeError(msg)
310 photoCalib = photoCalibs[0]
311 return band, photoCalib
314 """Make an exposure catalog for one tract.
318 exposures : `list` [`lsst.afw.image.Exposuref`]
319 Exposures to include in the catalog.
320 dataIds : `list` [`lsst.daf.butler.DataCoordinate`]
321 Data ids of each of the included exposures; must have "tract" and
326 images : `list` [`lsst.afw.image.MaskedImage`]
327 MaskedImages of each of the input exposures, for warping.
328 catalog : `lsst.afw.table.ExposureCatalog`
329 Catalog of metadata for each exposure
330 totalBox : `lsst.geom.Box2I`
331 The union of the bounding boxes of all the input exposures.
334 catalog.reserve(len(exposures))
335 images = [exposure.maskedImage
for exposure
in exposures]
337 for coadd, dataId
in zip(exposures, dataIds):
338 totalBox = totalBox.expandedTo(coadd.getBBox())
339 record = catalog.addNew()
340 record.setPsf(coadd.psf)
341 record.setWcs(coadd.wcs)
342 record.setPhotoCalib(coadd.photoCalib)
343 record.setBBox(coadd.getBBox())
345 record.set(
"tract", dataId[
"tract"])
346 record.set(
"patch", dataId[
"patch"])
349 record.set(
"weight", 1)
351 return images, catalog, totalBox
353 def _merge(self, maskedImages, bbox, wcs):
354 """Merge the images that came from one tract into one larger image,
355 ignoring NaN pixels and non-finite variance pixels from individual
360 maskedImages : `list` [`lsst.afw.image.MaskedImage`]
361 Images to be merged into one larger bounding box.
362 bbox : `lsst.geom.Box2I`
363 Bounding box defining the image to merge into.
364 wcs : `lsst.afw.geom.SkyWcs`
365 WCS of all of the input images to set on the output image.
369 merged : `lsst.afw.image.MaskedImage`
370 Merged image with all of the inputs at their respective bbox
373 merged = afwImage.ExposureF(bbox, wcs)
374 weights = afwImage.ImageF(bbox)
375 for maskedImage
in maskedImages:
376 weight = afwImage.ImageF(maskedImage.variance.array**(-0.5))
377 bad = np.isnan(maskedImage.image.array) | ~np.isfinite(maskedImage.variance.array)
380 maskedImage.image.array[bad] = 0.0
381 maskedImage.variance.array[bad] = 0.0
383 maskedImage.mask.array[bad] = 0
387 maskedImage.image *= weight
388 maskedImage.variance *= weight
389 merged.maskedImage[maskedImage.getBBox()] += maskedImage
392 weight.array[np.isnan(weight.array)] = 0
393 weights[maskedImage.getBBox()] += weight
397 merged.image /= weights
398 merged.variance /= weights
399 merged.mask.array |= merged.mask.getPlaneBitMask(
"NO_DATA") * (weights.array == 0)
404 """Return a PSF containing the PSF at each of the input regions.
406 Note that although this includes all the exposures from the catalog,
407 the PSF knows which part of the template the inputs came from, so when
408 evaluated at a given position it will not include inputs that never
409 went in to those pixels.
413 template : `lsst.afw.image.Exposure`
414 Generated template the PSF is for.
415 catalog : `lsst.afw.table.ExposureCatalog`
416 Catalog of exposures that went into the template that contains all
418 wcs : `lsst.afw.geom.SkyWcs`
419 WCS of the template, to warp the PSFs to.
423 coaddPsf : `lsst.meas.algorithms.CoaddPsf`
424 The meta-psf constructed from all of the input catalogs.
428 boolmask = template.mask.array & template.mask.getPlaneBitMask(
'NO_DATA') == 0
430 centerCoord = afwGeom.SpanSet.fromMask(maskx, 1).computeCentroid()
432 ctrl = self.config.coaddPsf.makeControl()
433 coaddPsf =
CoaddPsf(catalog, wcs, centerCoord, ctrl.warpingKernelName, ctrl.cacheSize)
438 dimensions=(
"instrument",
"visit",
"detector",
"skymap"),
439 defaultTemplates={
"coaddName":
"dcr",
440 "warpTypeSuffix":
"",
442 visitInfo = pipeBase.connectionTypes.Input(
443 doc=
"VisitInfo of calexp used to determine observing conditions.",
444 name=
"{fakesType}calexp.visitInfo",
445 storageClass=
"VisitInfo",
446 dimensions=(
"instrument",
"visit",
"detector"),
448 dcrCoadds = pipeBase.connectionTypes.Input(
449 doc=
"Input DCR template to match and subtract from the exposure",
450 name=
"{fakesType}dcrCoadd{warpTypeSuffix}",
451 storageClass=
"ExposureF",
452 dimensions=(
"tract",
"patch",
"skymap",
"band",
"subfilter"),
457 def __init__(self, *, config=None):
458 super().__init__(config=config)
459 self.inputs.remove(
"coaddExposures")
462class GetDcrTemplateConfig(GetTemplateConfig,
463 pipelineConnections=GetDcrTemplateConnections):
464 numSubfilters = pexConfig.Field(
465 doc=
"Number of subfilters in the DcrCoadd.",
469 effectiveWavelength = pexConfig.Field(
470 doc=
"Effective wavelength of the filter.",
474 bandwidth = pexConfig.Field(
475 doc=
"Bandwidth of the physical filter.",
481 if self.effectiveWavelength
is None or self.bandwidth
is None:
482 raise ValueError(
"The effective wavelength and bandwidth of the physical filter "
483 "must be set in the getTemplate config for DCR coadds. "
484 "Required until transmission curves are used in DM-13668.")
487class GetDcrTemplateTask(GetTemplateTask):
488 ConfigClass = GetDcrTemplateConfig
489 _DefaultName =
"getDcrTemplate"
491 def getOverlappingExposures(self, inputs):
492 """Return lists of coadds and their corresponding dataIds that overlap
495 The spatial index in the registry has generous padding and often
496 supplies patches near, but not directly overlapping the detector.
497 Filters inputs so that we don't have to read in all input coadds.
501 inputs : `dict` of task Inputs, containing:
502 - coaddExposureRefs : `list` \
503 [`lsst.daf.butler.DeferredDatasetHandle` of \
504 `lsst.afw.image.Exposure`]
505 Data references to exposures that might overlap the detector.
506 - bbox : `lsst.geom.Box2I`
507 Template Bounding box of the detector geometry onto which to
508 resample the coaddExposures.
509 - skyMap : `lsst.skymap.SkyMap`
510 Input definition of geometry/bbox and projection/wcs for
512 - wcs : `lsst.afw.geom.SkyWcs`
513 Template WCS onto which to resample the coaddExposures.
514 - visitInfo : `lsst.afw.image.VisitInfo`
515 Metadata for the science image.
519 result : `lsst.pipe.base.Struct`
520 A struct with attibutes:
523 Coadd exposures that overlap the detector (`list`
524 [`lsst.afw.image.Exposure`]).
526 Data IDs of the coadd exposures that overlap the detector
527 (`list` [`lsst.daf.butler.DataCoordinate`]).
532 Raised if no patches overlatp the input detector bbox.
538 coaddExposureRefList = []
541 for coaddRef
in inputs[
"dcrCoadds"]:
542 dataId = coaddRef.dataId
543 patchWcs = inputs[
"skyMap"][dataId[
'tract']].getWcs()
544 patchBBox = inputs[
"skyMap"][dataId[
'tract']][dataId[
'patch']].getOuterBBox()
545 patchCorners = patchWcs.pixelToSky(
geom.Box2D(patchBBox).getCorners())
547 if patchPolygon.intersection(detectorPolygon):
548 overlappingArea += patchPolygon.intersectionSingle(detectorPolygon).calculateArea()
549 self.log.info(
"Using template input tract=%s, patch=%s, subfilter=%s" %
550 (dataId[
'tract'], dataId[
'patch'], dataId[
"subfilter"]))
551 coaddExposureRefList.append(coaddRef)
552 if dataId[
'tract']
in patchList:
553 patchList[dataId[
'tract']].append(dataId[
'patch'])
555 patchList[dataId[
'tract']] = [dataId[
'patch'], ]
556 dataIds.append(dataId)
558 if not overlappingArea:
559 raise pipeBase.NoWorkFound(
'No patches overlap detector')
561 self.checkPatchList(patchList)
563 coaddExposures = self.getDcrModel(patchList, inputs[
'dcrCoadds'], inputs[
'visitInfo'])
564 return pipeBase.Struct(coaddExposures=coaddExposures,
568 """Check that all of the DcrModel subfilters are present for each
574 Dict of the patches containing valid data for each tract.
579 If the number of exposures found for a patch does not match the
580 number of subfilters.
582 for tract
in patchList:
583 for patch
in set(patchList[tract]):
584 if patchList[tract].count(patch) != self.config.numSubfilters:
585 raise RuntimeError(
"Invalid number of DcrModel subfilters found: %d vs %d expected",
586 patchList[tract].count(patch), self.config.numSubfilters)
589 """Build DCR-matched coadds from a list of exposure references.
594 Dict of the patches containing valid data for each tract.
595 coaddRefs : `list` [`lsst.daf.butler.DeferredDatasetHandle`]
596 Data references to `~lsst.afw.image.Exposure` representing
597 DcrModels that overlap the detector.
598 visitInfo : `lsst.afw.image.VisitInfo`
599 Metadata for the science image.
603 coaddExposures : `list` [`lsst.afw.image.Exposure`]
604 Coadd exposures that overlap the detector.
607 for tract
in patchList:
608 for patch
in set(patchList[tract]):
609 coaddRefList = [coaddRef
for coaddRef
in coaddRefs
612 dcrModel = DcrModel.fromQuantum(coaddRefList,
613 self.config.effectiveWavelength,
614 self.config.bandwidth,
615 self.config.numSubfilters)
616 coaddExposures.append(dcrModel.buildMatchedExposure(visitInfo=visitInfo))
617 return coaddExposures
621 condition = (coaddRef.dataId[
'tract'] == tract) & (coaddRef.dataId[
'patch'] == patch)
A group of labels for a filter in an exposure or coadd.
Custom catalog class for ExposureRecord/Table.
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)
run(self, coaddExposures, bbox, wcs, dataIds, physical_filter)
checkPatchList(self, patchList)
_merge(self, maskedImages, bbox, wcs)
_makeExposureCatalog(self, exposures, dataIds)
getDcrModel(self, patchList, coaddRefs, visitInfo)
_makePsf(self, template, catalog, wcs)