25from astropy.stats
import gaussian_sigma_to_fwhm
40from lsst.images
import (
42 DifferenceImageTemplateInfo,
43 get_legacy_optional_mask_planes,
44 get_legacy_template_mask_planes,
46from lsst.skymap
import BaseSkyMap
49from lsst.meas.algorithms
import CoaddPsf, CoaddPsfConfig, SubtractBackgroundTask, ScaleVarianceTask
50from lsst.utils.timer
import timeMethod
56 "GetDcrTemplateConfig",
60"""Width, in N sigma, of the Gaussian approximation to the CoaddPsf."""
64 pipeBase.PipelineTaskConnections,
65 dimensions=(
"instrument",
"visit",
"detector"),
66 defaultTemplates={
"coaddName":
"goodSeeing",
"warpTypeSuffix":
"",
"fakesType":
""},
68 bbox = pipeBase.connectionTypes.Input(
69 doc=
"Bounding box of exposure to determine the geometry of the output template.",
70 name=
"{fakesType}calexp.bbox",
72 dimensions=(
"instrument",
"visit",
"detector"),
73 deprecated=
"Replaced by the `detector` connection. Will be removed after v31.",
75 detector = pipeBase.connectionTypes.Input(
76 doc=
"Detector of the exposure that we will construct the template for."
77 " Its bounding box sets the geometry of the output template.",
78 name=
"{fakesType}calexp.detector",
79 storageClass=
"Detector",
80 dimensions=(
"instrument",
"visit",
"detector"),
82 obs_info = pipeBase.connectionTypes.Input(
83 doc=
"Observation metadata of the exposure the template is built for."
84 " Only read with output_image_type='future', which needs this to set"
85 " the obs_info component of a lsst.images.VisitImage.",
86 name=
"{fakesType}calexp.obs_info",
87 storageClass=
"ObservationInfo",
88 dimensions=(
"instrument",
"visit",
"detector"),
90 wcs = pipeBase.connectionTypes.Input(
91 doc=
"WCS of the exposure that we will construct the template for.",
92 name=
"{fakesType}calexp.wcs",
94 dimensions=(
"instrument",
"visit",
"detector"),
96 skyMap = pipeBase.connectionTypes.Input(
97 doc=
"Geometry of the tracts and patches that the coadds are defined on.",
98 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
99 dimensions=(
"skymap",),
100 storageClass=
"SkyMap",
102 coaddExposures = pipeBase.connectionTypes.Input(
103 doc=
"Coadds that may overlap the desired region, as possible inputs to the template."
104 " Will be restricted to those that directly overlap the projected bounding box.",
105 dimensions=(
"tract",
"patch",
"skymap",
"band"),
106 storageClass=
"ExposureF",
107 name=
"{fakesType}{coaddName}Coadd{warpTypeSuffix}",
110 deferGraphConstraint=
True,
113 template = pipeBase.connectionTypes.Output(
114 doc=
"Warped template, pixel matched to the bounding box and WCS.",
115 dimensions=(
"instrument",
"visit",
"detector"),
116 storageClass=
"ExposureF",
117 name=
"{fakesType}{coaddName}Diff_templateExp{warpTypeSuffix}",
120 def __init__(self, *, config=None):
121 super().__init__(config=config)
124 if config.requireCoaddAtGraphBuild:
125 self.coaddExposures = dataclasses.replace(
127 deferGraphConstraint=
False,
129 if config.output_image_type ==
"future":
130 self.template = dataclasses.replace(self.template, storageClass=
"DifferenceImage")
135class GetTemplateConfig(
136 pipeBase.PipelineTaskConfig, pipelineConnections=GetTemplateConnections
138 templateBorderSize = pexConfig.Field(
141 doc=
"Number of pixels to grow the requested template image to account for warping",
143 warp = pexConfig.ConfigField(
144 dtype=afwMath.Warper.ConfigClass,
145 doc=
"warper configuration",
147 doCorrectVariancePlateScale = pexConfig.Field(
150 doc=
"Correct the template variance for the total change in pixel"
151 " area between the images the coadds were built from and the science"
152 " image? This matters when the two have different plate scales, such"
153 " as DECam templates used for Rubin science images. The coadd pixel"
154 " grid cancels out of the total, so the skymap does not enter. It is a"
155 " single scalar factor, applied to the final template variance plane.",
157 coaddPsf = pexConfig.ConfigField(
158 doc=
"Configuration for CoaddPsf",
159 dtype=CoaddPsfConfig,
161 fwhmExposureGrid = pexConfig.Field(
162 doc=
"Grid size to compute the average PSF FWHM in an exposure (pixels)",
166 fwhmExposureBuffer = pexConfig.Field(
167 doc=
"Fractional buffer margin to be left out of all sides of the image during construction"
168 " of grid to compute average PSF FWHM in an exposure",
172 varianceBackground = pexConfig.ConfigurableField(
173 target=SubtractBackgroundTask,
174 doc=
"Task to estimate the background variance.",
176 highVarianceThreshold = pexConfig.RangeField(
180 doc=
"Set the HIGH_VARIANCE mask plane for regions with variance"
181 " greater than the median by this factor.",
183 highVarianceMaskFraction = pexConfig.Field(
186 doc=
"Minimum fraction of unmasked pixels needed to set the"
187 " HIGH_VARIANCE mask plane.",
189 doScaleVariance = pexConfig.Field(
192 doc=
"Scale variance of the template image?"
194 scaleVariance = pexConfig.ConfigurableField(
195 target=ScaleVarianceTask,
196 doc=
"Subtask to rescale the variance of the template to the statistically expected level."
198 requireCoaddAtGraphBuild = pexConfig.Field(
201 doc=
"If True, include the coadd dataset existence in the"
202 " initial butler query during QuantumGraph generation.",
204 output_image_type = pexConfig.ChoiceField[str](
205 "Which image type to use for the output template.",
207 "legacy":
"Write as a lsst.afw.image.ExposureF.",
208 "future":
"Write as a lsst.images.DifferenceImage.",
213 raiseOnUndefinedMaskMap = pexConfig.Field(
216 doc=
"Raise if the template has pixels set in a mask plane that"
217 " `lsst.images` does not map? If False, log a warning and drop that"
218 " plane from the output instead."
219 " Only used with output_image_type='future'.",
225 self.warp.cacheSize = 100000
226 self.coaddPsf.cacheSize = self.warp.cacheSize
229 self.warp.interpLength = 100
230 self.warp.warpingKernelName =
"lanczos3"
231 self.coaddPsf.warpingKernelName = self.warp.warpingKernelName
234 self.varianceBackground.algorithm =
"LINEAR"
235 self.varianceBackground.binSize = 32
236 self.varianceBackground.useApprox =
False
237 self.varianceBackground.statisticsProperty =
"MEDIAN"
238 self.varianceBackground.doFilterSuperPixels =
True
239 self.varianceBackground.ignoredPixelMask = [
"BAD",
248 ConfigClass = GetTemplateConfig
249 _DefaultName =
"getTemplate"
253 if self.config.doScaleVariance:
254 self.makeSubtask(
"scaleVariance")
255 self.
warper = afwMath.Warper.fromConfig(self.config.warp)
256 self.
schema = afwTable.ExposureTable.makeMinimalSchema()
258 "tract", type=np.int32, doc=
"Which tract this exposure came from."
263 doc=
"Which patch in the tract this exposure came from.",
268 doc=
"Weight for each exposure, used to make the CoaddPsf; should always be 1.",
270 self.makeSubtask(
"varianceBackground")
273 inputs = butlerQC.get(inputRefs)
274 detector = inputs.pop(
"detector")
275 bbox = detector.getBBox()
276 wcs = inputs.pop(
"wcs")
277 obsInfo = inputs.pop(
"obs_info",
None)
278 coaddExposures = inputs.pop(
"coaddExposures")
279 skymap = inputs.pop(
"skyMap")
282 assert not inputs,
"runQuantum got more inputs than expected"
284 results = self.
getExposures(coaddExposures, bbox, skymap, wcs)
285 physical_filter = butlerQC.quantum.dataId[
"physical_filter"]
287 coaddExposureHandles=results.coaddExposures,
290 dataIds=results.dataIds,
291 physical_filter=physical_filter,
292 visit=outputRefs.template.dataId[
"visit"],
294 if self.config.output_image_type ==
"future":
295 dataId = butlerQC.quantum.dataId
296 exposureRecord = record_from_obs_info(obsInfo, dataId[
"instrument"], dataId[
"visit"],
299 inputRefs.coaddExposures,
301 exposureRecord=exposureRecord)
302 butlerQC.put(outputs, outputRefs)
305 """Convert a template image to DifferenceImage format.
307 This replaces ``template`` with an `lsst.images.DifferenceImage`
308 instance and records the coadds contributed to it. The Gaussian PSF
309 set by `run` is converted to an
310 `lsst.images.psfs.GaussianPointSpreadFunction`.
314 template : `lsst.afw.image.Exposure`
315 Template to convert. Its detector is replaced with ``detector``
316 and its mask is modified in place.
317 coadd_refs : `list` [`lsst.daf.butler.DatasetRef`]
318 References to the coadds that may have gone into the template.
319 May be a superset of the coadds that were used; supplies the
320 dataset id and RUN collection recorded for each coadd that was.
321 detector : `lsst.afw.cameraGeom.Detector`
322 Detector of the science image that the template was built for.
323 exposureRecord : `lsst.daf.butler.DimensionRecord`
324 The ``exposure`` record of the observation the template was built
325 for, which supplies the observation metadata recorded on the
330 convertedTemplate : `lsst.images.DifferenceImage`
336 Raised if the template has pixels set in a mask plane that
337 `lsst.images.get_legacy_template_mask_planes` does not map and
338 ``config.raiseOnUndefinedMaskMap`` is True, or if ``coadd_refs``
339 spans more than one skymap.
343 coaddPsf = self.
_makePsf(template, template.getInfo().getCoaddInputs().ccds, template.getWcs())
344 template.setDetector(detector)
345 skymaps = {ref.dataId[
"skymap"]
for ref
in coadd_refs}
346 if len(skymaps) != 1:
347 raise RuntimeError(
"Expected the template inputs to come from one skymap;"
348 f
" got {sorted(skymaps)}.")
349 butler_info = {(ref.dataId[
"tract"], ref.dataId[
"patch"]): (ref.id, ref.run)
350 for ref
in coadd_refs}
351 planeMap = get_legacy_template_mask_planes()
352 if not self.config.raiseOnUndefinedMaskMap:
354 convertedTemplate = DifferenceImage.from_legacy(
356 exposure_record=exposureRecord,
357 unit=astropy.units.nJy,
361 convertedTemplate.templates = DifferenceImageTemplateInfo.from_legacy_psf(
362 convertedTemplate.sky_projection.pixel_frame,
368 return convertedTemplate
371 """Clear the pixels of every mask plane that conversion would reject.
375 mask : `lsst.afw.image.Mask`
376 Template mask to modify in place.
377 planeMap : `dict` [`str`, `lsst.images.MaskPlane`]
378 Plane map the template will be converted with. The optional
379 planes of `lsst.images.get_legacy_optional_mask_planes` are kept
380 as well, since conversion adds them when they have pixels set.
382 keep = planeMap.keys() | get_legacy_optional_mask_planes().keys()
383 for name
in mask.getMaskPlaneDict():
386 bitmask = mask.getPlaneBitMask(name)
387 nSet = np.count_nonzero(mask.array & bitmask)
389 self.log.warning(
"Dropping mask plane %s from the template: %d pixels are set, but"
390 " the template plane map does not include it.", name, nSet)
391 mask.array &= ~bitmask
394 """Return a data structure containing the coadds that overlap the
395 specified bbox projected onto the sky, and a corresponding data
396 structure of their dataIds.
397 These are the appropriate inputs to this task's `run` method.
399 The spatial index in the butler registry has generous padding and often
400 supplies patches near, but not directly overlapping the desired region.
401 This method filters the inputs so that `run` does not have to read in
402 all possibly-matching coadd exposures.
406 coaddExposureHandles : `iterable` \
407 [`lsst.daf.butler.DeferredDatasetHandle` of \
408 `lsst.afw.image.Exposure`]
409 Dataset handles to exposures that might overlap the desired
411 bbox : `lsst.geom.Box2I`
412 Template bounding box of the pixel geometry onto which the
413 coaddExposures will be resampled.
414 skymap : `lsst.skymap.SkyMap`
415 Geometry of the tracts and patches the coadds are defined on.
416 wcs : `lsst.afw.geom.SkyWcs`
417 Template WCS onto which the coadds will be resampled.
421 result : `lsst.pipe.base.Struct`
422 A struct with attributes:
425 Dict of coadd exposures that overlap the projected bbox,
427 (`dict` [`int`, `list` [`lsst.daf.butler.DeferredDatasetHandle` of
428 `lsst.afw.image.Exposure`] ]).
430 Dict of data IDs of the coadd exposures that overlap the
431 projected bbox, indexed on tract id
432 (`dict` [`int`, `list [`lsst.daf.butler.DataCoordinate`] ]).
437 Raised if no patches overlap the input detector bbox, or the input
441 raise pipeBase.NoWorkFound(
442 "WCS is None; cannot find overlapping exposures."
447 detectorCorners = wcs.pixelToSky(detectorPolygon.getCorners())
449 coaddExposures = collections.defaultdict(list)
450 dataIds = collections.defaultdict(list)
452 for coaddRef
in coaddExposureHandles:
453 dataId = coaddRef.dataId
454 patchWcs = skymap[dataId[
"tract"]].getWcs()
455 patchBBox = skymap[dataId[
"tract"]][dataId[
"patch"]].getOuterBBox()
456 patchPolygon = afwGeom.Polygon(
geom.Box2D(patchBBox))
461 detectorInPatchCoordinates = afwGeom.Polygon(patchWcs.skyToPixel(detectorCorners))
462 if patchPolygon.intersection(detectorInPatchCoordinates):
463 overlappingArea += patchPolygon.intersectionSingle(
464 detectorInPatchCoordinates
467 "Using template input tract=%s, patch=%s",
471 coaddExposures[dataId[
"tract"]].append(coaddRef)
472 dataIds[dataId[
"tract"]].append(dataId)
474 if not overlappingArea:
475 raise pipeBase.NoWorkFound(
"No patches overlap detector")
477 return pipeBase.Struct(coaddExposures=coaddExposures, dataIds=dataIds)
480 def run(self, *, coaddExposureHandles, bbox, wcs, dataIds, physical_filter, visit=None):
481 """Warp coadds from multiple tracts and patches to form a template to
482 subtract from a science image.
484 Tract and patch overlap regions are combined by a variance-weighted
485 average, and the variance planes are combined with the same weights,
486 not added in quadrature; the overlap regions are not statistically
487 independent, because they're derived from the same original data.
488 The PSF of the template is a Gaussian with the same width as the
489 CoaddPsf evaluated at the center of the detector.
493 coaddExposureHandles : `dict` [`int`, `list` of \
494 [`lsst.daf.butler.DeferredDatasetHandle` of \
495 `lsst.afw.image.Exposure`]]
496 Coadds to be mosaicked, indexed on tract id.
497 bbox : `lsst.geom.Box2I`
498 Input Bounding box of the detector geometry onto which to
499 resample the ``coaddExposureHandles``. The output template bbox
500 will be grown by ``templateBorderSize``.
501 wcs : `lsst.afw.geom.SkyWcs`
502 Template WCS onto which to resample the ``coaddExposureHandles``.
503 dataIds : `dict` [`int`, `list` [`lsst.daf.butler.DataCoordinate`]]
504 Record of the tract and patch of each coaddExposure, indexed on
506 physical_filter : `str`
507 Physical filter of the science image.
508 visit : `int`, optional
509 If supplied, over-write the visit ID in the template's visitInfo
510 so that downstream source injection tasks can link the template and
511 science image for the visit.
515 result : `lsst.pipe.base.Struct`
516 A struct with attributes:
519 A template coadd exposure assembled out of patches, with
520 pixels in nJy. Its PSF is a Gaussian approximation to the
521 CoaddPsf of its inputs (`lsst.afw.image.ExposureF`).
526 If no coadds are found with sufficient un-masked pixels.
528 band, photoCalib = self.
_checkInputs(dataIds, coaddExposureHandles)
531 bbox.grow(self.config.templateBorderSize)
541 plateScaleFactor =
None
542 if self.config.doCorrectVariancePlateScale
and coaddExposureHandles:
544 next(iter(coaddExposureHandles.values()))[0], wcs, bbox
547 for tract
in coaddExposureHandles:
549 coaddExposureHandles[tract], dataIds[tract]
551 warpedBox = computeWarpedBBox(catalog[0].wcs, bbox, wcs)
554 unwarped, count, included = self.
_merge(
555 maskedImages, warpedBox, catalog[0].wcs
561 "No valid pixels from coadd patches in tract %s; not including in output.",
565 warpedBox.clip(totalBox)
566 potentialInput = self.
warper.warpExposure(
567 wcs, unwarped.subset(warpedBox), destBBox=bbox
572 potentialInput.mask.array
573 & potentialInput.mask.getPlaneBitMask(
"NO_DATA")
576 "No overlap from coadd patches in tract %s; not including in output.",
582 tempCatalog = afwTable.ExposureCatalog(self.
schema)
583 tempCatalog.reserve(len(included))
585 tempCatalog.append(catalog[i])
586 catalogs.append(tempCatalog)
587 warped[tract] = potentialInput.maskedImage
590 raise pipeBase.NoWorkFound(
"No patches found to overlap science exposure.")
593 template, count, _ = self.
_merge(warped, bbox, wcs)
595 raise pipeBase.NoWorkFound(
"No valid pixels in warped template.")
597 if self.config.doScaleVariance:
601 varianceFactor = self.scaleVariance.
run(template.maskedImage)
602 self.log.info(
"Template variance scaling factor: %.2f", varianceFactor)
603 self.metadata[
"scaleTemplateVarianceFactor"] = varianceFactor
608 catalog = afwTable.ExposureCatalog(self.
schema)
609 catalog.reserve(sum([len(c)
for c
in catalogs]))
615 if visit
is not None:
616 template.getInfo().setVisitInfo(
VisitInfo(id=visit))
619 if photoCalib
is not None and photoCalib != identity:
620 template.maskedImage = photoCalib.calibrateImage(template.maskedImage)
621 photoCalib = identity
622 template.setPhotoCalib(photoCalib)
623 template.setPsf(self.
_makePsf(template, catalog, wcs))
628 coaddInputs.ccds.extend(catalog, deep=
True)
629 template.getInfo().setCoaddInputs(coaddInputs)
630 return pipeBase.Struct(template=template)
633 """Correct the template variance plane for the effects of warping.
635 Compute a multiplicative correction factor for the template variance
636 if the plate scale of the coadd's constituent images is different than
637 the science image the template is being constructed for. This should
638 only be necessary if the coadd images were from a different instrument
639 than the science image. If the plate scale of the science instrument is
640 smaller than the plate scale of the coadd instrument, then the template
641 pixels will be correlated and the true variance will be higher than the
642 image pixel noise level would suggest.
646 template : `lsst.afw.image.Exposure`
647 Assembled template; its variance plane is modified in place.
648 plateScaleFactor : `float` or `None`
649 Correction for the change in pixel area, from `_plateScaleFactor`.
654 Raised if ``doCorrectVariancePlateScale`` is set but the factor
655 could not be reconstructed from the coadd inputs.
658 if self.config.doCorrectVariancePlateScale:
659 if plateScaleFactor
is None:
661 "doCorrectVariancePlateScale is set but no usable coaddInputs were found."
663 scale *= plateScaleFactor
664 self.metadata[
"variancePlateScaleFactor"] = plateScaleFactor
666 "Applying a plate scale variance factor of %.4f, reconstructed from the"
673 template.variance.array *= scale
674 self.metadata[
"templateVarianceCorrectionFactor"] = scale
676 "Corrected the template variance plane for warping by a factor of %.4f.",
681 """Compute the variance correction for the total change in pixel area
682 between the coadd's constituent images and the science image.
686 coaddHandle : `lsst.daf.butler.DeferredDatasetHandle` of \
687 `lsst.afw.image.Exposure`
688 Handle to one of the input coadd patches. Only its ``coaddInputs``
689 component is read; the pixels are left alone.
690 wcs : `lsst.afw.geom.SkyWcs`
691 WCS of the science image the template is being built for.
692 bbox : `lsst.geom.Box2I`
693 Bounding box of the template, used only to choose where to
694 evaluate the science image pixel scale.
698 factor : `float` or `None`
699 The correction factor equivalent to the ratio of the area of a pixel
700 from the coadd's instrument to the science instrument, or `None` if
701 this patch carries no input record from which it could be computed.
703 coaddInputs = coaddHandle.get(component=
"coaddInputs")
704 if coaddInputs
is None:
706 scienceScale = wcs.getPixelScale(
geom.Box2D(bbox).getCenter()).asArcseconds()
708 for record
in coaddInputs.ccds:
711 originalWcs = record.getWcs()
712 if originalWcs
is None:
714 center =
geom.Box2D(record.getBBox()).getCenter()
715 pviScale = originalWcs.getPixelScale(center).asArcseconds()
717 factor = (pviScale/scienceScale)**2
718 if np.isfinite(factor)
and factor > 0:
723 """Set a mask plane for regions with unusually high variance.
727 template : `lsst.afw.image.Exposure`
728 The warped template exposure, which will be modified in place.
730 highVarianceMaskPlaneBit = template.mask.addMaskPlane(
"HIGH_VARIANCE")
731 ignoredPixelBits = template.mask.getPlaneBitMask(self.varianceBackground.config.ignoredPixelMask)
732 goodMask = (template.mask.array & ignoredPixelBits) == 0
733 goodFraction = np.count_nonzero(goodMask)/template.mask.array.size
734 if goodFraction < self.config.highVarianceMaskFraction:
735 self.log.info(
"Not setting HIGH_VARIANCE mask plane, only %2.1f%% of"
736 " pixels were unmasked for background estimation, but"
737 " %2.1f%% are required", 100*goodFraction, 100*self.config.highVarianceMaskFraction)
739 varianceExposure = template.clone()
740 varianceExposure.image.array = varianceExposure.variance.array
741 varianceBackground = self.varianceBackground.
run(varianceExposure).background.getImage().array
742 threshold = self.config.highVarianceThreshold*np.nanmedian(varianceBackground)
743 highVariancePix = varianceBackground > threshold
744 template.mask.array[highVariancePix] |= 2**highVarianceMaskPlaneBit
748 """Check that the all the dataIds are from the same band and that
749 the exposures all have the same photometric calibration.
753 dataIds : `dict` [`int`, `list` [`lsst.daf.butler.DataCoordinate`]]
754 Record of the tract and patch of each coaddExposure.
755 coaddExposures : `dict` [`int`, `list` of \
756 [`lsst.daf.butler.DeferredDatasetHandle` of \
757 `lsst.afw.image.Exposure` or
758 `lsst.afw.image.Exposure`]]
759 Coadds to be mosaicked.
764 Filter band of all the input exposures.
765 photoCalib : `lsst.afw.image.PhotoCalib`
766 Photometric calibration of all of the input exposures.
771 Raised if the bands or calibrations of the input exposures are not
774 bands = set(dataId[
"band"]
for tract
in dataIds
for dataId
in dataIds[tract])
776 raise RuntimeError(f
"GetTemplateTask called with multiple bands: {bands}")
779 exposure.get(component=
"photoCalib")
780 for exposures
in coaddExposures.values()
781 for exposure
in exposures
783 if not all([photoCalibs[0] == x
for x
in photoCalibs]):
784 msg = f
"GetTemplateTask called with exposures with different photoCalibs: {photoCalibs}"
785 raise RuntimeError(msg)
786 photoCalib = photoCalibs[0]
787 return band, photoCalib
790 """Make an exposure catalog for one tract.
794 exposureRefs : `list` of [`lsst.daf.butler.DeferredDatasetHandle` of \
795 `lsst.afw.image.Exposure`]
796 Exposures to include in the catalog.
797 dataIds : `list` [`lsst.daf.butler.DataCoordinate`]
798 Data ids of each of the included exposures; must have "tract" and
803 images : `dict` [`lsst.afw.image.MaskedImage`]
804 MaskedImages of each of the input exposures, for warping.
805 catalog : `lsst.afw.table.ExposureCatalog`
806 Catalog of metadata for each exposure
807 totalBox : `lsst.geom.Box2I`
808 The union of the bounding boxes of all the input exposures.
810 catalog = afwTable.ExposureCatalog(self.
schema)
811 catalog.reserve(len(exposureRefs))
812 exposures = (exposureRef.get()
for exposureRef
in exposureRefs)
816 for coadd, dataId
in zip(exposures, dataIds):
817 images[dataId] = coadd.maskedImage
818 bbox = coadd.getBBox()
819 totalBox = totalBox.expandedTo(bbox)
820 record = catalog.addNew()
821 record.setPsf(coadd.psf)
822 record.setWcs(coadd.wcs)
823 record.setPhotoCalib(coadd.photoCalib)
825 record.setValidPolygon(afwGeom.Polygon(
geom.Box2D(bbox).getCorners()))
826 record.set(
"tract", dataId[
"tract"])
827 record.set(
"patch", dataId[
"patch"])
830 record.set(
"weight", 1)
832 return images, catalog, totalBox
834 def _merge(self, maskedImages, bbox, wcs):
835 """Merge the images that came from one tract into one larger image,
836 ignoring NaN pixels and non-finite variance pixels from individual
841 maskedImages : `dict` [`lsst.afw.image.MaskedImage` or
842 `lsst.afw.image.Exposure`]
843 Images to be merged into one larger bounding box.
844 bbox : `lsst.geom.Box2I`
845 Bounding box defining the image to merge into.
846 wcs : `lsst.afw.geom.SkyWcs`
847 WCS of all of the input images to set on the output image.
851 merged : `lsst.afw.image.MaskedImage`
852 Merged image with all of the inputs at their respective bbox
855 Count of the number of good pixels (those with positive weights)
857 included : `list` [`int`]
858 List of indexes of patches that were included in the merged
859 result, to be used to trim the exposure catalog.
861 merged = afwImage.ExposureF(bbox, wcs)
862 weights = afwImage.ImageF(bbox)
864 for i, (dataId, maskedImage)
in enumerate(maskedImages.items()):
866 clippedBox =
geom.Box2I(maskedImage.getBBox())
867 clippedBox.clip(bbox)
868 if clippedBox.area == 0:
869 self.log.debug(
"%s does not overlap template region.", dataId)
871 maskedImage = maskedImage.subset(clippedBox)
873 good = (maskedImage.variance.array > 0) & (
874 np.isfinite(maskedImage.variance.array)
876 weight = maskedImage.variance.array[good] ** (-0.5)
877 bad = np.isnan(maskedImage.image.array) | ~good
880 maskedImage.image.array[bad] = 0.0
881 maskedImage.variance.array[bad] = 0.0
883 maskedImage.mask.array[bad] = 0
887 maskedImage.image.array[good] *= weight
888 maskedImage.variance.array[good] *= weight
889 weights[clippedBox].array[good] += weight
892 merged.maskedImage[clippedBox] += maskedImage
895 good = weights.array > 0
900 weights = weights.array[good]
901 merged.image.array[good] /= weights
902 merged.variance.array[good] /= weights
904 merged.mask.array[~good] |= merged.mask.getPlaneBitMask(
"NO_DATA")
906 return merged, good.sum(), included
909 """Return a PSF containing the PSF at each of the input regions.
911 Note that although this includes all the exposures from the catalog,
912 the PSF knows which part of the template the inputs came from, so when
913 evaluated at a given position it will not include inputs that never
914 went in to those pixels.
918 template : `lsst.afw.image.Exposure`
919 Generated template the PSF is for.
920 catalog : `lsst.afw.table.ExposureCatalog`
921 Catalog of exposures that went into the template that contains all
923 wcs : `lsst.afw.geom.SkyWcs`
924 WCS of the template, to warp the PSFs to.
928 coaddPsf : `lsst.meas.algorithms.CoaddPsf`
929 The meta-psf constructed from all of the input catalogs.
933 boolmask = template.mask.array & template.mask.getPlaneBitMask(
"NO_DATA") == 0
935 centerCoord = afwGeom.SpanSet.fromMask(maskx, 1).computeCentroid()
937 ctrl = self.config.coaddPsf.makeControl()
939 catalog, wcs, centerCoord, ctrl.warpingKernelName, ctrl.cacheSize
944 """Return a Gaussian PSF with the width of the template CoaddPsf.
948 template : `lsst.afw.image.Exposure`
949 Template with the CoaddPsf to approximate. The width is measured
950 at the average position of the PSF, or averaged over a grid of
951 positions if the PSF cannot be evaluated there.
955 psf : `lsst.afw.detection.GaussianPsf`
956 Gaussian with the width of the template PSF.
961 Raised if the PSF cannot be evaluated at any grid position.
966 fwhm = getPsfFwhm(template.psf)
973 self.log.info(
"Unable to evaluate PSF at the average position. "
974 "Evaluating PSF on a grid of points.")
975 fwhm = evaluateMeanPsfFwhm(template,
976 fwhmExposureBuffer=self.config.fwhmExposureBuffer,
977 fwhmExposureGrid=self.config.fwhmExposureGrid)
978 sigma = fwhm/gaussian_sigma_to_fwhm
979 size = 2*int(np.ceil(_PSF_STAMP_SIGMAS*sigma)) + 1
980 self.metadata[
"templatePsfSigma"] = float(sigma)
981 self.log.info(
"Approximated the template PSF with a Gaussian of sigma %.3f pixels.", sigma)
982 return afwDetection.GaussianPsf(size, size, sigma)
986 GetTemplateConnections,
987 dimensions=(
"instrument",
"visit",
"detector"),
988 defaultTemplates={
"coaddName":
"dcr",
"warpTypeSuffix":
"",
"fakesType":
""},
990 visitInfo = pipeBase.connectionTypes.Input(
991 doc=
"VisitInfo of calexp used to determine observing conditions.",
992 name=
"{fakesType}calexp.visitInfo",
993 storageClass=
"VisitInfo",
994 dimensions=(
"instrument",
"visit",
"detector"),
996 dcrCoadds = pipeBase.connectionTypes.Input(
997 doc=
"Input DCR template to match and subtract from the exposure",
998 name=
"{fakesType}dcrCoadd{warpTypeSuffix}",
999 storageClass=
"ExposureF",
1000 dimensions=(
"tract",
"patch",
"skymap",
"band",
"subfilter"),
1005 def __init__(self, *, config=None):
1006 super().__init__(config=config)
1007 self.inputs.remove(
"coaddExposures")
1010class GetDcrTemplateConfig(
1011 GetTemplateConfig, pipelineConnections=GetDcrTemplateConnections
1013 """Config for GetDcrTemplateTask.
1015 Only ``output_image_type="legacy"`` is supported: the task always writes
1016 an `lsst.afw.image.ExposureF`.
1019 numSubfilters = pexConfig.Field(
1020 doc=
"Number of subfilters in the DcrCoadd.",
1024 effectiveWavelength = pexConfig.Field(
1025 doc=
"Effective wavelength of the filter in nm.",
1029 bandwidth = pexConfig.Field(
1030 doc=
"Bandwidth of the physical filter.",
1036 if self.effectiveWavelength
is None or self.bandwidth
is None:
1038 "The effective wavelength and bandwidth of the physical filter "
1039 "must be set in the getTemplate config for DCR coadds. "
1040 "Required until transmission curves are used in DM-13668."
1045 ConfigClass = GetDcrTemplateConfig
1046 _DefaultName =
"getDcrTemplate"
1048 def runQuantum(self, butlerQC, inputRefs, outputRefs):
1049 inputs = butlerQC.get(inputRefs)
1050 detector = inputs.pop(
"detector")
1051 bbox = detector.getBBox()
1052 wcs = inputs.pop(
"wcs")
1053 dcrCoaddExposureHandles = inputs.pop(
"dcrCoadds")
1054 skymap = inputs.pop(
"skyMap")
1055 visitInfo = inputs.pop(
"visitInfo")
1058 inputs.pop(
"obs_info",
None)
1061 assert not inputs,
"runQuantum got more inputs than expected"
1063 results = self.getExposures(
1064 dcrCoaddExposureHandles, bbox, skymap, wcs, visitInfo
1066 physical_filter = butlerQC.quantum.dataId[
"physical_filter"]
1068 coaddExposureHandles=results.coaddExposures,
1071 dataIds=results.dataIds,
1072 physical_filter=physical_filter,
1074 butlerQC.put(outputs, outputRefs)
1076 def getExposures(self, dcrCoaddExposureHandles, bbox, skymap, wcs, visitInfo):
1077 """Return lists of coadds and their corresponding dataIds that overlap
1080 The spatial index in the registry has generous padding and often
1081 supplies patches near, but not directly overlapping the detector.
1082 Filters inputs so that we don't have to read in all input coadds.
1086 dcrCoaddExposureHandles : `list` \
1087 [`lsst.daf.butler.DeferredDatasetHandle` of \
1088 `lsst.afw.image.Exposure`]
1089 Data references to exposures that might overlap the detector.
1090 bbox : `lsst.geom.Box2I`
1091 Template Bounding box of the detector geometry onto which to
1092 resample the coaddExposures.
1093 skymap : `lsst.skymap.SkyMap`
1094 Input definition of geometry/bbox and projection/wcs for
1096 wcs : `lsst.afw.geom.SkyWcs`
1097 Template WCS onto which to resample the coaddExposures.
1098 visitInfo : `lsst.afw.image.VisitInfo`
1099 Metadata for the science image.
1103 result : `lsst.pipe.base.Struct`
1104 A struct with attibutes:
1107 Dict of coadd exposures that overlap the projected bbox,
1109 (`dict` [`int`, `list` [`lsst.afw.image.Exposure`] ]).
1111 Dict of data IDs of the coadd exposures that overlap the
1112 projected bbox, indexed on tract id
1113 (`dict` [`int`, `list [`lsst.daf.butler.DataCoordinate`] ]).
1117 pipeBase.NoWorkFound
1118 Raised if no patches overlatp the input detector bbox.
1123 raise pipeBase.NoWorkFound(
"Exposure has no WCS; cannot create a template.")
1127 dataIds = collections.defaultdict(list)
1129 for coaddRef
in dcrCoaddExposureHandles:
1130 dataId = coaddRef.dataId
1131 subfilter = dataId[
"subfilter"]
1132 patchWcs = skymap[dataId[
"tract"]].getWcs()
1133 patchBBox = skymap[dataId[
"tract"]][dataId[
"patch"]].getOuterBBox()
1134 patchCorners = patchWcs.pixelToSky(
geom.Box2D(patchBBox).getCorners())
1135 patchPolygon = afwGeom.Polygon(wcs.skyToPixel(patchCorners))
1136 if patchPolygon.intersection(detectorPolygon):
1137 overlappingArea += patchPolygon.intersectionSingle(
1141 "Using template input tract=%s, patch=%s, subfilter=%s"
1142 % (dataId[
"tract"], dataId[
"patch"], dataId[
"subfilter"])
1144 if dataId[
"tract"]
in patchList:
1145 patchList[dataId[
"tract"]].append(dataId[
"patch"])
1147 patchList[dataId[
"tract"]] = [
1151 dataIds[dataId[
"tract"]].append(dataId)
1153 if not overlappingArea:
1154 raise pipeBase.NoWorkFound(
"No patches overlap detector")
1156 self.checkPatchList(patchList)
1158 coaddExposures = self.getDcrModel(patchList, dcrCoaddExposureHandles, visitInfo)
1159 return pipeBase.Struct(coaddExposures=coaddExposures, dataIds=dataIds)
1162 """Check that all of the DcrModel subfilters are present for each
1168 Dict of the patches containing valid data for each tract.
1173 If the number of exposures found for a patch does not match the
1174 number of subfilters.
1176 for tract
in patchList:
1177 for patch
in set(patchList[tract]):
1178 if patchList[tract].count(patch) != self.config.numSubfilters:
1180 "Invalid number of DcrModel subfilters found: %d vs %d expected",
1181 patchList[tract].count(patch),
1182 self.config.numSubfilters,
1186 """Build DCR-matched coadds from a list of exposure references.
1191 Dict of the patches containing valid data for each tract.
1192 coaddRefs : `list` [`lsst.daf.butler.DeferredDatasetHandle`]
1193 Data references to `~lsst.afw.image.Exposure` representing
1194 DcrModels that overlap the detector.
1195 visitInfo : `lsst.afw.image.VisitInfo`
1196 Metadata for the science image.
1200 coaddExposures : `list` [`lsst.afw.image.Exposure`]
1201 Coadd exposures that overlap the detector.
1203 coaddExposures = collections.defaultdict(list)
1204 for tract
in patchList:
1205 for patch
in set(patchList[tract]):
1208 for coaddRef
in coaddRefs
1212 dcrModel = DcrModel.fromQuantum(
1214 self.config.effectiveWavelength,
1215 self.config.bandwidth,
1216 self.config.numSubfilters,
1218 coaddExposures[tract].append(dcrModel.buildMatchedExposureHandle(visitInfo=visitInfo))
1219 return coaddExposures
1223 condition = (coaddRef.dataId[
"tract"] == tract) & (
1224 coaddRef.dataId[
"patch"] == patch
checkHighVariance(self, template)
_clearUnmappedMaskPlanes(self, mask, planeMap)
_correctVariance(self, template, plateScaleFactor)
_makeGaussianPsf(self, template)
_checkInputs(dataIds, coaddExposures)
getExposures(self, coaddExposureHandles, bbox, skymap, wcs)
run(self, *, coaddExposureHandles, bbox, wcs, dataIds, physical_filter, visit=None)
_makeExposureCatalog(self, exposureRefs, dataIds)
convert_outputs_to_future(self, template, coadd_refs, detector, exposureRecord)
_makePsf(self, template, catalog, wcs)
_merge(self, maskedImages, bbox, wcs)
runQuantum(self, butlerQC, inputRefs, outputRefs)
__init__(self, *args, **kwargs)
_plateScaleFactor(self, coaddHandle, wcs, bbox)
getDcrModel(self, patchList, coaddRefs, visitInfo)
checkPatchList(self, patchList)
_selectDataRef(coaddRef, tract, patch)