24from astropy.stats
import gaussian_sigma_to_fwhm
39from lsst.skymap
import BaseSkyMap
42from lsst.meas.algorithms
import CoaddPsf, CoaddPsfConfig, SubtractBackgroundTask, ScaleVarianceTask
43from lsst.utils.timer
import timeMethod
49 "GetDcrTemplateConfig",
53"""Width, in N sigma, of the Gaussian approximation to the CoaddPsf."""
57 pipeBase.PipelineTaskConnections,
58 dimensions=(
"instrument",
"visit",
"detector"),
59 defaultTemplates={
"coaddName":
"goodSeeing",
"warpTypeSuffix":
"",
"fakesType":
""},
61 bbox = pipeBase.connectionTypes.Input(
62 doc=
"Bounding box of exposure to determine the geometry of the output template.",
63 name=
"{fakesType}calexp.bbox",
65 dimensions=(
"instrument",
"visit",
"detector"),
67 wcs = pipeBase.connectionTypes.Input(
68 doc=
"WCS of the exposure that we will construct the template for.",
69 name=
"{fakesType}calexp.wcs",
71 dimensions=(
"instrument",
"visit",
"detector"),
73 skyMap = pipeBase.connectionTypes.Input(
74 doc=
"Geometry of the tracts and patches that the coadds are defined on.",
75 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
76 dimensions=(
"skymap",),
77 storageClass=
"SkyMap",
79 coaddExposures = pipeBase.connectionTypes.Input(
80 doc=
"Coadds that may overlap the desired region, as possible inputs to the template."
81 " Will be restricted to those that directly overlap the projected bounding box.",
82 dimensions=(
"tract",
"patch",
"skymap",
"band"),
83 storageClass=
"ExposureF",
84 name=
"{fakesType}{coaddName}Coadd{warpTypeSuffix}",
87 deferGraphConstraint=
True,
90 template = pipeBase.connectionTypes.Output(
91 doc=
"Warped template, pixel matched to the bounding box and WCS.",
92 dimensions=(
"instrument",
"visit",
"detector"),
93 storageClass=
"ExposureF",
94 name=
"{fakesType}{coaddName}Diff_templateExp{warpTypeSuffix}",
97 def __init__(self, *, config=None):
98 super().__init__(config=config)
99 if config.requireCoaddAtGraphBuild:
100 self.coaddExposures = dataclasses.replace(
102 deferGraphConstraint=
False,
106class GetTemplateConfig(
107 pipeBase.PipelineTaskConfig, pipelineConnections=GetTemplateConnections
109 templateBorderSize = pexConfig.Field(
112 doc=
"Number of pixels to grow the requested template image to account for warping",
114 warp = pexConfig.ConfigField(
115 dtype=afwMath.Warper.ConfigClass,
116 doc=
"warper configuration",
118 doCorrectVariancePlateScale = pexConfig.Field(
121 doc=
"Correct the template variance for the total change in pixel"
122 " area between the images the coadds were built from and the science"
123 " image? This matters when the two have different plate scales, such"
124 " as DECam templates used for Rubin science images. The coadd pixel"
125 " grid cancels out of the total, so the skymap does not enter. It is a"
126 " single scalar factor, applied to the final template variance plane.",
128 coaddPsf = pexConfig.ConfigField(
129 doc=
"Configuration for CoaddPsf",
130 dtype=CoaddPsfConfig,
132 fwhmExposureGrid = pexConfig.Field(
133 doc=
"Grid size to compute the average PSF FWHM in an exposure (pixels)",
137 fwhmExposureBuffer = pexConfig.Field(
138 doc=
"Fractional buffer margin to be left out of all sides of the image during construction"
139 " of grid to compute average PSF FWHM in an exposure",
143 varianceBackground = pexConfig.ConfigurableField(
144 target=SubtractBackgroundTask,
145 doc=
"Task to estimate the background variance.",
147 highVarianceThreshold = pexConfig.RangeField(
151 doc=
"Set the HIGH_VARIANCE mask plane for regions with variance"
152 " greater than the median by this factor.",
154 highVarianceMaskFraction = pexConfig.Field(
157 doc=
"Minimum fraction of unmasked pixels needed to set the"
158 " HIGH_VARIANCE mask plane.",
160 doScaleVariance = pexConfig.Field(
163 doc=
"Scale variance of the template image?"
165 scaleVariance = pexConfig.ConfigurableField(
166 target=ScaleVarianceTask,
167 doc=
"Subtask to rescale the variance of the template to the statistically expected level."
169 requireCoaddAtGraphBuild = pexConfig.Field(
172 doc=
"If True, include the coadd dataset existence in the"
173 " initial butler query during QuantumGraph generation.",
176 def setDefaults(self):
179 self.warp.cacheSize = 100000
180 self.coaddPsf.cacheSize = self.warp.cacheSize
183 self.warp.interpLength = 100
184 self.warp.warpingKernelName =
"lanczos3"
185 self.coaddPsf.warpingKernelName = self.warp.warpingKernelName
188 self.varianceBackground.algorithm =
"LINEAR"
189 self.varianceBackground.binSize = 32
190 self.varianceBackground.useApprox =
False
191 self.varianceBackground.statisticsProperty =
"MEDIAN"
192 self.varianceBackground.doFilterSuperPixels =
True
193 self.varianceBackground.ignoredPixelMask = [
"BAD",
201class GetTemplateTask(pipeBase.PipelineTask):
202 ConfigClass = GetTemplateConfig
203 _DefaultName =
"getTemplate"
205 def __init__(self, *args, **kwargs):
206 super().__init__(*args, **kwargs)
207 if self.config.doScaleVariance:
208 self.makeSubtask(
"scaleVariance")
209 self.warper = afwMath.Warper.fromConfig(self.config.warp)
210 self.schema = afwTable.ExposureTable.makeMinimalSchema()
211 self.schema.addField(
212 "tract", type=np.int32, doc=
"Which tract this exposure came from."
214 self.schema.addField(
217 doc=
"Which patch in the tract this exposure came from.",
219 self.schema.addField(
222 doc=
"Weight for each exposure, used to make the CoaddPsf; should always be 1.",
224 self.makeSubtask(
"varianceBackground")
226 def runQuantum(self, butlerQC, inputRefs, outputRefs):
227 inputs = butlerQC.get(inputRefs)
228 bbox = inputs.pop(
"bbox")
229 wcs = inputs.pop(
"wcs")
230 coaddExposures = inputs.pop(
"coaddExposures")
231 skymap = inputs.pop(
"skyMap")
234 assert not inputs,
"runQuantum got more inputs than expected"
236 results = self.getExposures(coaddExposures, bbox, skymap, wcs)
237 physical_filter = butlerQC.quantum.dataId[
"physical_filter"]
239 coaddExposureHandles=results.coaddExposures,
242 dataIds=results.dataIds,
243 physical_filter=physical_filter,
244 visit=outputRefs.template.dataId[
"visit"],
246 butlerQC.put(outputs, outputRefs)
248 def getExposures(self, coaddExposureHandles, bbox, skymap, wcs):
249 """Return a data structure containing the coadds that overlap the
250 specified bbox projected onto the sky, and a corresponding data
251 structure of their dataIds.
252 These are the appropriate inputs to this task's `run` method.
254 The spatial index in the butler registry has generous padding and often
255 supplies patches near, but not directly overlapping the desired region.
256 This method filters the inputs so that `run` does not have to read in
257 all possibly-matching coadd exposures.
261 coaddExposureHandles : `iterable` \
262 [`lsst.daf.butler.DeferredDatasetHandle` of \
263 `lsst.afw.image.Exposure`]
264 Dataset handles to exposures that might overlap the desired
266 bbox : `lsst.geom.Box2I`
267 Template bounding box of the pixel geometry onto which the
268 coaddExposures will be resampled.
269 skymap : `lsst.skymap.SkyMap`
270 Geometry of the tracts and patches the coadds are defined on.
271 wcs : `lsst.afw.geom.SkyWcs`
272 Template WCS onto which the coadds will be resampled.
276 result : `lsst.pipe.base.Struct`
277 A struct with attributes:
280 Dict of coadd exposures that overlap the projected bbox,
282 (`dict` [`int`, `list` [`lsst.daf.butler.DeferredDatasetHandle` of
283 `lsst.afw.image.Exposure`] ]).
285 Dict of data IDs of the coadd exposures that overlap the
286 projected bbox, indexed on tract id
287 (`dict` [`int`, `list [`lsst.daf.butler.DataCoordinate`] ]).
292 Raised if no patches overlap the input detector bbox, or the input
296 raise pipeBase.NoWorkFound(
297 "WCS is None; cannot find overlapping exposures."
302 detectorCorners = wcs.pixelToSky(detectorPolygon.getCorners())
304 coaddExposures = collections.defaultdict(list)
305 dataIds = collections.defaultdict(list)
307 for coaddRef
in coaddExposureHandles:
308 dataId = coaddRef.dataId
309 patchWcs = skymap[dataId[
"tract"]].getWcs()
310 patchBBox = skymap[dataId[
"tract"]][dataId[
"patch"]].getOuterBBox()
316 detectorInPatchCoordinates = afwGeom.Polygon(patchWcs.skyToPixel(detectorCorners))
317 if patchPolygon.intersection(detectorInPatchCoordinates):
318 overlappingArea += patchPolygon.intersectionSingle(
319 detectorInPatchCoordinates
322 "Using template input tract=%s, patch=%s",
326 coaddExposures[dataId[
"tract"]].append(coaddRef)
327 dataIds[dataId[
"tract"]].append(dataId)
329 if not overlappingArea:
330 raise pipeBase.NoWorkFound(
"No patches overlap detector")
332 return pipeBase.Struct(coaddExposures=coaddExposures, dataIds=dataIds)
335 def run(self, *, coaddExposureHandles, bbox, wcs, dataIds, physical_filter, visit=None):
336 """Warp coadds from multiple tracts and patches to form a template to
337 subtract from a science image.
339 Tract and patch overlap regions are combined by a variance-weighted
340 average, and the variance planes are combined with the same weights,
341 not added in quadrature; the overlap regions are not statistically
342 independent, because they're derived from the same original data.
343 The PSF of the template is a Gaussian with the same width as the
344 CoaddPsf evaluated at the center of the detector.
348 coaddExposureHandles : `dict` [`int`, `list` of \
349 [`lsst.daf.butler.DeferredDatasetHandle` of \
350 `lsst.afw.image.Exposure`]]
351 Coadds to be mosaicked, indexed on tract id.
352 bbox : `lsst.geom.Box2I`
353 Template Bounding box of the detector geometry onto which to
354 resample the ``coaddExposureHandles``. Modified in-place to include the
356 wcs : `lsst.afw.geom.SkyWcs`
357 Template WCS onto which to resample the ``coaddExposureHandles``.
358 dataIds : `dict` [`int`, `list` [`lsst.daf.butler.DataCoordinate`]]
359 Record of the tract and patch of each coaddExposure, indexed on
361 physical_filter : `str`
362 Physical filter of the science image.
363 visit : `int`, optional
364 If supplied, over-write the visit ID in the template's visitInfo
365 so that downstream source injection tasks can link the template and
366 science image for the visit.
370 result : `lsst.pipe.base.Struct`
371 A struct with attributes:
374 A template coadd exposure assembled out of patches, with
375 pixels in nJy. Its PSF is a Gaussian approximation to the
376 CoaddPsf of its inputs (`lsst.afw.image.ExposureF`).
381 If no coadds are found with sufficient un-masked pixels.
383 band, photoCalib = self._checkInputs(dataIds, coaddExposureHandles)
385 bbox.grow(self.config.templateBorderSize)
395 plateScaleFactor =
None
396 if self.config.doCorrectVariancePlateScale
and coaddExposureHandles:
397 plateScaleFactor = self._plateScaleFactor(
398 next(iter(coaddExposureHandles.values()))[0], wcs, bbox
401 for tract
in coaddExposureHandles:
402 maskedImages, catalog, totalBox = self._makeExposureCatalog(
403 coaddExposureHandles[tract], dataIds[tract]
405 warpedBox = computeWarpedBBox(catalog[0].wcs, bbox, wcs)
408 unwarped, count, included = self._merge(
409 maskedImages, warpedBox, catalog[0].wcs
415 "No valid pixels from coadd patches in tract %s; not including in output.",
419 warpedBox.clip(totalBox)
420 potentialInput = self.warper.warpExposure(
421 wcs, unwarped.subset(warpedBox), destBBox=bbox
426 potentialInput.mask.array
427 & potentialInput.mask.getPlaneBitMask(
"NO_DATA")
430 "No overlap from coadd patches in tract %s; not including in output.",
436 tempCatalog = afwTable.ExposureCatalog(self.schema)
437 tempCatalog.reserve(len(included))
439 tempCatalog.append(catalog[i])
440 catalogs.append(tempCatalog)
441 warped[tract] = potentialInput.maskedImage
444 raise pipeBase.NoWorkFound(
"No patches found to overlap science exposure.")
447 template, count, _ = self._merge(warped, bbox, wcs)
449 raise pipeBase.NoWorkFound(
"No valid pixels in warped template.")
451 if self.config.doScaleVariance:
455 varianceFactor = self.scaleVariance.
run(template.maskedImage)
456 self.log.info(
"Template variance scaling factor: %.2f", varianceFactor)
457 self.metadata[
"scaleTemplateVarianceFactor"] = varianceFactor
459 self._correctVariance(template, plateScaleFactor)
462 catalog = afwTable.ExposureCatalog(self.schema)
463 catalog.reserve(sum([len(c)
for c
in catalogs]))
468 self.checkHighVariance(template)
469 if visit
is not None:
470 template.getInfo().setVisitInfo(
VisitInfo(id=visit))
473 if photoCalib
is not None and photoCalib != identity:
474 template.maskedImage = photoCalib.calibrateImage(template.maskedImage)
475 photoCalib = identity
476 template.setPhotoCalib(photoCalib)
477 template.setPsf(self._makePsf(template, catalog, wcs))
478 template.setPsf(self._makeGaussianPsf(template))
482 coaddInputs.ccds.extend(catalog, deep=
True)
483 template.getInfo().setCoaddInputs(coaddInputs)
484 return pipeBase.Struct(template=template)
487 """Correct the template variance plane for the effects of warping.
489 Compute a multiplicative correction factor for the template variance
490 if the plate scale of the coadd's constituent images is different than
491 the science image the template is being constructed for. This should
492 only be necessary if the coadd images were from a different instrument
493 than the science image. If the plate scale of the science instrument is
494 smaller than the plate scale of the coadd instrument, then the template
495 pixels will be correlated and the true variance will be higher than the
496 image pixel noise level would suggest.
500 template : `lsst.afw.image.Exposure`
501 Assembled template; its variance plane is modified in place.
502 plateScaleFactor : `float` or `None`
503 Correction for the change in pixel area, from `_plateScaleFactor`.
508 Raised if ``doCorrectVariancePlateScale`` is set but the factor
509 could not be reconstructed from the coadd inputs.
512 if self.config.doCorrectVariancePlateScale:
513 if plateScaleFactor
is None:
515 "doCorrectVariancePlateScale is set but no usable coaddInputs were found."
517 scale *= plateScaleFactor
518 self.metadata[
"variancePlateScaleFactor"] = plateScaleFactor
520 "Applying a plate scale variance factor of %.4f, reconstructed from the"
527 template.variance.array *= scale
528 self.metadata[
"templateVarianceCorrectionFactor"] = scale
530 "Corrected the template variance plane for warping by a factor of %.4f.",
535 """Compute the variance correction for the total change in pixel area
536 between the coadd's constituent images and the science image.
540 coaddHandle : `lsst.daf.butler.DeferredDatasetHandle` of \
541 `lsst.afw.image.Exposure`
542 Handle to one of the input coadd patches. Only its ``coaddInputs``
543 component is read; the pixels are left alone.
544 wcs : `lsst.afw.geom.SkyWcs`
545 WCS of the science image the template is being built for.
546 bbox : `lsst.geom.Box2I`
547 Bounding box of the template, used only to choose where to
548 evaluate the science image pixel scale.
552 factor : `float` or `None`
553 The correction factor equivalent to the ratio of the area of a pixel
554 from the coadd's instrument to the science instrument, or `None` if
555 this patch carries no input record from which it could be computed.
557 coaddInputs = coaddHandle.get(component=
"coaddInputs")
558 if coaddInputs
is None:
560 scienceScale = wcs.getPixelScale(
geom.Box2D(bbox).getCenter()).asArcseconds()
562 for record
in coaddInputs.ccds:
565 originalWcs = record.getWcs()
566 if originalWcs
is None:
568 center =
geom.Box2D(record.getBBox()).getCenter()
569 pviScale = originalWcs.getPixelScale(center).asArcseconds()
571 factor = (pviScale/scienceScale)**2
572 if np.isfinite(factor)
and factor > 0:
577 """Set a mask plane for regions with unusually high variance.
581 template : `lsst.afw.image.Exposure`
582 The warped template exposure, which will be modified in place.
584 highVarianceMaskPlaneBit = template.mask.addMaskPlane(
"HIGH_VARIANCE")
585 ignoredPixelBits = template.mask.getPlaneBitMask(self.varianceBackground.config.ignoredPixelMask)
586 goodMask = (template.mask.array & ignoredPixelBits) == 0
587 goodFraction = np.count_nonzero(goodMask)/template.mask.array.size
588 if goodFraction < self.config.highVarianceMaskFraction:
589 self.log.info(
"Not setting HIGH_VARIANCE mask plane, only %2.1f%% of"
590 " pixels were unmasked for background estimation, but"
591 " %2.1f%% are required", 100*goodFraction, 100*self.config.highVarianceMaskFraction)
593 varianceExposure = template.clone()
594 varianceExposure.image.array = varianceExposure.variance.array
595 varianceBackground = self.varianceBackground.
run(varianceExposure).background.getImage().array
596 threshold = self.config.highVarianceThreshold*np.nanmedian(varianceBackground)
597 highVariancePix = varianceBackground > threshold
598 template.mask.array[highVariancePix] |= 2**highVarianceMaskPlaneBit
601 def _checkInputs(dataIds, coaddExposures):
602 """Check that the all the dataIds are from the same band and that
603 the exposures all have the same photometric calibration.
607 dataIds : `dict` [`int`, `list` [`lsst.daf.butler.DataCoordinate`]]
608 Record of the tract and patch of each coaddExposure.
609 coaddExposures : `dict` [`int`, `list` of \
610 [`lsst.daf.butler.DeferredDatasetHandle` of \
611 `lsst.afw.image.Exposure` or
612 `lsst.afw.image.Exposure`]]
613 Coadds to be mosaicked.
618 Filter band of all the input exposures.
619 photoCalib : `lsst.afw.image.PhotoCalib`
620 Photometric calibration of all of the input exposures.
625 Raised if the bands or calibrations of the input exposures are not
628 bands = set(dataId[
"band"]
for tract
in dataIds
for dataId
in dataIds[tract])
630 raise RuntimeError(f
"GetTemplateTask called with multiple bands: {bands}")
633 exposure.get(component=
"photoCalib")
634 for exposures
in coaddExposures.values()
635 for exposure
in exposures
637 if not all([photoCalibs[0] == x
for x
in photoCalibs]):
638 msg = f
"GetTemplateTask called with exposures with different photoCalibs: {photoCalibs}"
639 raise RuntimeError(msg)
640 photoCalib = photoCalibs[0]
641 return band, photoCalib
644 """Make an exposure catalog for one tract.
648 exposureRefs : `list` of [`lsst.daf.butler.DeferredDatasetHandle` of \
649 `lsst.afw.image.Exposure`]
650 Exposures to include in the catalog.
651 dataIds : `list` [`lsst.daf.butler.DataCoordinate`]
652 Data ids of each of the included exposures; must have "tract" and
657 images : `dict` [`lsst.afw.image.MaskedImage`]
658 MaskedImages of each of the input exposures, for warping.
659 catalog : `lsst.afw.table.ExposureCatalog`
660 Catalog of metadata for each exposure
661 totalBox : `lsst.geom.Box2I`
662 The union of the bounding boxes of all the input exposures.
664 catalog = afwTable.ExposureCatalog(self.schema)
665 catalog.reserve(len(exposureRefs))
666 exposures = (exposureRef.get()
for exposureRef
in exposureRefs)
670 for coadd, dataId
in zip(exposures, dataIds):
671 images[dataId] = coadd.maskedImage
672 bbox = coadd.getBBox()
673 totalBox = totalBox.expandedTo(bbox)
674 record = catalog.addNew()
675 record.setPsf(coadd.psf)
676 record.setWcs(coadd.wcs)
677 record.setPhotoCalib(coadd.photoCalib)
679 record.setValidPolygon(afwGeom.Polygon(
geom.Box2D(bbox).getCorners()))
680 record.set(
"tract", dataId[
"tract"])
681 record.set(
"patch", dataId[
"patch"])
684 record.set(
"weight", 1)
686 return images, catalog, totalBox
688 def _merge(self, maskedImages, bbox, wcs):
689 """Merge the images that came from one tract into one larger image,
690 ignoring NaN pixels and non-finite variance pixels from individual
695 maskedImages : `dict` [`lsst.afw.image.MaskedImage` or
696 `lsst.afw.image.Exposure`]
697 Images to be merged into one larger bounding box.
698 bbox : `lsst.geom.Box2I`
699 Bounding box defining the image to merge into.
700 wcs : `lsst.afw.geom.SkyWcs`
701 WCS of all of the input images to set on the output image.
705 merged : `lsst.afw.image.MaskedImage`
706 Merged image with all of the inputs at their respective bbox
709 Count of the number of good pixels (those with positive weights)
711 included : `list` [`int`]
712 List of indexes of patches that were included in the merged
713 result, to be used to trim the exposure catalog.
715 merged = afwImage.ExposureF(bbox, wcs)
716 weights = afwImage.ImageF(bbox)
718 for i, (dataId, maskedImage)
in enumerate(maskedImages.items()):
720 clippedBox =
geom.Box2I(maskedImage.getBBox())
721 clippedBox.clip(bbox)
722 if clippedBox.area == 0:
723 self.log.debug(
"%s does not overlap template region.", dataId)
725 maskedImage = maskedImage.subset(clippedBox)
727 good = (maskedImage.variance.array > 0) & (
728 np.isfinite(maskedImage.variance.array)
730 weight = maskedImage.variance.array[good] ** (-0.5)
731 bad = np.isnan(maskedImage.image.array) | ~good
734 maskedImage.image.array[bad] = 0.0
735 maskedImage.variance.array[bad] = 0.0
737 maskedImage.mask.array[bad] = 0
741 maskedImage.image.array[good] *= weight
742 maskedImage.variance.array[good] *= weight
743 weights[clippedBox].array[good] += weight
746 merged.maskedImage[clippedBox] += maskedImage
749 good = weights.array > 0
754 weights = weights.array[good]
755 merged.image.array[good] /= weights
756 merged.variance.array[good] /= weights
758 merged.mask.array[~good] |= merged.mask.getPlaneBitMask(
"NO_DATA")
760 return merged, good.sum(), included
763 """Return a PSF containing the PSF at each of the input regions.
765 Note that although this includes all the exposures from the catalog,
766 the PSF knows which part of the template the inputs came from, so when
767 evaluated at a given position it will not include inputs that never
768 went in to those pixels.
772 template : `lsst.afw.image.Exposure`
773 Generated template the PSF is for.
774 catalog : `lsst.afw.table.ExposureCatalog`
775 Catalog of exposures that went into the template that contains all
777 wcs : `lsst.afw.geom.SkyWcs`
778 WCS of the template, to warp the PSFs to.
782 coaddPsf : `lsst.meas.algorithms.CoaddPsf`
783 The meta-psf constructed from all of the input catalogs.
787 boolmask = template.mask.array & template.mask.getPlaneBitMask(
"NO_DATA") == 0
789 centerCoord = afwGeom.SpanSet.fromMask(maskx, 1).computeCentroid()
791 ctrl = self.config.coaddPsf.makeControl()
793 catalog, wcs, centerCoord, ctrl.warpingKernelName, ctrl.cacheSize
798 """Return a Gaussian PSF with the width of the template CoaddPsf.
802 template : `lsst.afw.image.Exposure`
803 Template with the CoaddPsf to approximate. The width is measured
804 at the average position of the PSF, or averaged over a grid of
805 positions if the PSF cannot be evaluated there.
809 psf : `lsst.afw.detection.GaussianPsf`
810 Gaussian with the width of the template PSF.
815 Raised if the PSF cannot be evaluated at any grid position.
820 fwhm = getPsfFwhm(template.psf)
827 self.log.info(
"Unable to evaluate PSF at the average position. "
828 "Evaluating PSF on a grid of points.")
829 fwhm = evaluateMeanPsfFwhm(template,
830 fwhmExposureBuffer=self.config.fwhmExposureBuffer,
831 fwhmExposureGrid=self.config.fwhmExposureGrid)
832 sigma = fwhm/gaussian_sigma_to_fwhm
833 size = 2*int(np.ceil(_PSF_STAMP_SIGMAS*sigma)) + 1
834 self.metadata[
"templatePsfSigma"] = float(sigma)
835 self.log.info(
"Approximated the template PSF with a Gaussian of sigma %.3f pixels.", sigma)
836 return afwDetection.GaussianPsf(size, size, sigma)
840 GetTemplateConnections,
841 dimensions=(
"instrument",
"visit",
"detector"),
842 defaultTemplates={
"coaddName":
"dcr",
"warpTypeSuffix":
"",
"fakesType":
""},
844 visitInfo = pipeBase.connectionTypes.Input(
845 doc=
"VisitInfo of calexp used to determine observing conditions.",
846 name=
"{fakesType}calexp.visitInfo",
847 storageClass=
"VisitInfo",
848 dimensions=(
"instrument",
"visit",
"detector"),
850 dcrCoadds = pipeBase.connectionTypes.Input(
851 doc=
"Input DCR template to match and subtract from the exposure",
852 name=
"{fakesType}dcrCoadd{warpTypeSuffix}",
853 storageClass=
"ExposureF",
854 dimensions=(
"tract",
"patch",
"skymap",
"band",
"subfilter"),
859 def __init__(self, *, config=None):
860 super().__init__(config=config)
861 self.inputs.remove(
"coaddExposures")
864class GetDcrTemplateConfig(
865 GetTemplateConfig, pipelineConnections=GetDcrTemplateConnections
867 numSubfilters = pexConfig.Field(
868 doc=
"Number of subfilters in the DcrCoadd.",
872 effectiveWavelength = pexConfig.Field(
873 doc=
"Effective wavelength of the filter in nm.",
877 bandwidth = pexConfig.Field(
878 doc=
"Bandwidth of the physical filter.",
884 if self.effectiveWavelength
is None or self.bandwidth
is None:
886 "The effective wavelength and bandwidth of the physical filter "
887 "must be set in the getTemplate config for DCR coadds. "
888 "Required until transmission curves are used in DM-13668."
892class GetDcrTemplateTask(GetTemplateTask):
893 ConfigClass = GetDcrTemplateConfig
894 _DefaultName =
"getDcrTemplate"
896 def runQuantum(self, butlerQC, inputRefs, outputRefs):
897 inputs = butlerQC.get(inputRefs)
898 bbox = inputs.pop(
"bbox")
899 wcs = inputs.pop(
"wcs")
900 dcrCoaddExposureHandles = inputs.pop(
"dcrCoadds")
901 skymap = inputs.pop(
"skyMap")
902 visitInfo = inputs.pop(
"visitInfo")
905 assert not inputs,
"runQuantum got more inputs than expected"
907 results = self.getExposures(
908 dcrCoaddExposureHandles, bbox, skymap, wcs, visitInfo
910 physical_filter = butlerQC.quantum.dataId[
"physical_filter"]
912 coaddExposureHandles=results.coaddExposures,
915 dataIds=results.dataIds,
916 physical_filter=physical_filter,
918 butlerQC.put(outputs, outputRefs)
920 def getExposures(self, dcrCoaddExposureHandles, bbox, skymap, wcs, visitInfo):
921 """Return lists of coadds and their corresponding dataIds that overlap
924 The spatial index in the registry has generous padding and often
925 supplies patches near, but not directly overlapping the detector.
926 Filters inputs so that we don't have to read in all input coadds.
930 dcrCoaddExposureHandles : `list` \
931 [`lsst.daf.butler.DeferredDatasetHandle` of \
932 `lsst.afw.image.Exposure`]
933 Data references to exposures that might overlap the detector.
934 bbox : `lsst.geom.Box2I`
935 Template Bounding box of the detector geometry onto which to
936 resample the coaddExposures.
937 skymap : `lsst.skymap.SkyMap`
938 Input definition of geometry/bbox and projection/wcs for
940 wcs : `lsst.afw.geom.SkyWcs`
941 Template WCS onto which to resample the coaddExposures.
942 visitInfo : `lsst.afw.image.VisitInfo`
943 Metadata for the science image.
947 result : `lsst.pipe.base.Struct`
948 A struct with attibutes:
951 Dict of coadd exposures that overlap the projected bbox,
953 (`dict` [`int`, `list` [`lsst.afw.image.Exposure`] ]).
955 Dict of data IDs of the coadd exposures that overlap the
956 projected bbox, indexed on tract id
957 (`dict` [`int`, `list [`lsst.daf.butler.DataCoordinate`] ]).
962 Raised if no patches overlatp the input detector bbox.
967 raise pipeBase.NoWorkFound(
"Exposure has no WCS; cannot create a template.")
971 dataIds = collections.defaultdict(list)
973 for coaddRef
in dcrCoaddExposureHandles:
974 dataId = coaddRef.dataId
975 subfilter = dataId[
"subfilter"]
976 patchWcs = skymap[dataId[
"tract"]].getWcs()
977 patchBBox = skymap[dataId[
"tract"]][dataId[
"patch"]].getOuterBBox()
978 patchCorners = patchWcs.pixelToSky(
geom.Box2D(patchBBox).getCorners())
979 patchPolygon = afwGeom.Polygon(wcs.skyToPixel(patchCorners))
980 if patchPolygon.intersection(detectorPolygon):
981 overlappingArea += patchPolygon.intersectionSingle(
985 "Using template input tract=%s, patch=%s, subfilter=%s"
986 % (dataId[
"tract"], dataId[
"patch"], dataId[
"subfilter"])
988 if dataId[
"tract"]
in patchList:
989 patchList[dataId[
"tract"]].append(dataId[
"patch"])
991 patchList[dataId[
"tract"]] = [
995 dataIds[dataId[
"tract"]].append(dataId)
997 if not overlappingArea:
998 raise pipeBase.NoWorkFound(
"No patches overlap detector")
1000 self.checkPatchList(patchList)
1002 coaddExposures = self.getDcrModel(patchList, dcrCoaddExposureHandles, visitInfo)
1003 return pipeBase.Struct(coaddExposures=coaddExposures, dataIds=dataIds)
1006 """Check that all of the DcrModel subfilters are present for each
1012 Dict of the patches containing valid data for each tract.
1017 If the number of exposures found for a patch does not match the
1018 number of subfilters.
1020 for tract
in patchList:
1021 for patch
in set(patchList[tract]):
1022 if patchList[tract].count(patch) != self.config.numSubfilters:
1024 "Invalid number of DcrModel subfilters found: %d vs %d expected",
1025 patchList[tract].count(patch),
1026 self.config.numSubfilters,
1030 """Build DCR-matched coadds from a list of exposure references.
1035 Dict of the patches containing valid data for each tract.
1036 coaddRefs : `list` [`lsst.daf.butler.DeferredDatasetHandle`]
1037 Data references to `~lsst.afw.image.Exposure` representing
1038 DcrModels that overlap the detector.
1039 visitInfo : `lsst.afw.image.VisitInfo`
1040 Metadata for the science image.
1044 coaddExposures : `list` [`lsst.afw.image.Exposure`]
1045 Coadd exposures that overlap the detector.
1047 coaddExposures = collections.defaultdict(list)
1048 for tract
in patchList:
1049 for patch
in set(patchList[tract]):
1052 for coaddRef
in coaddRefs
1056 dcrModel = DcrModel.fromQuantum(
1058 self.config.effectiveWavelength,
1059 self.config.bandwidth,
1060 self.config.numSubfilters,
1062 coaddExposures[tract].append(dcrModel.buildMatchedExposureHandle(visitInfo=visitInfo))
1063 return coaddExposures
1067 condition = (coaddRef.dataId[
"tract"] == tract) & (
1068 coaddRef.dataId[
"patch"] == patch
_correctVariance(self, template, plateScaleFactor)
run(self, *, coaddExposureHandles, bbox, wcs, dataIds, physical_filter, visit=None)
checkHighVariance(self, template)
_makePsf(self, template, catalog, wcs)
_makeExposureCatalog(self, exposureRefs, dataIds)
_plateScaleFactor(self, coaddHandle, wcs, bbox)
getDcrModel(self, patchList, coaddRefs, visitInfo)
_merge(self, maskedImages, bbox, wcs)
_makeGaussianPsf(self, template)
checkPatchList(self, patchList)
_selectDataRef(coaddRef, tract, patch)