36from lsst.skymap
import BaseSkyMap
38from lsst.meas.algorithms
import CoaddPsf, CoaddPsfConfig, SubtractBackgroundTask, ScaleVarianceTask
39from lsst.utils.timer
import timeMethod
45 "GetDcrTemplateConfig",
50 pipeBase.PipelineTaskConnections,
51 dimensions=(
"instrument",
"visit",
"detector"),
52 defaultTemplates={
"coaddName":
"goodSeeing",
"warpTypeSuffix":
"",
"fakesType":
""},
54 bbox = pipeBase.connectionTypes.Input(
55 doc=
"Bounding box of exposure to determine the geometry of the output template.",
56 name=
"{fakesType}calexp.bbox",
58 dimensions=(
"instrument",
"visit",
"detector"),
60 wcs = pipeBase.connectionTypes.Input(
61 doc=
"WCS of the exposure that we will construct the template for.",
62 name=
"{fakesType}calexp.wcs",
64 dimensions=(
"instrument",
"visit",
"detector"),
66 skyMap = pipeBase.connectionTypes.Input(
67 doc=
"Geometry of the tracts and patches that the coadds are defined on.",
68 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
69 dimensions=(
"skymap",),
70 storageClass=
"SkyMap",
72 coaddExposures = pipeBase.connectionTypes.Input(
73 doc=
"Coadds that may overlap the desired region, as possible inputs to the template."
74 " Will be restricted to those that directly overlap the projected bounding box.",
75 dimensions=(
"tract",
"patch",
"skymap",
"band"),
76 storageClass=
"ExposureF",
77 name=
"{fakesType}{coaddName}Coadd{warpTypeSuffix}",
80 deferGraphConstraint=
True,
83 template = pipeBase.connectionTypes.Output(
84 doc=
"Warped template, pixel matched to the bounding box and WCS.",
85 dimensions=(
"instrument",
"visit",
"detector"),
86 storageClass=
"ExposureF",
87 name=
"{fakesType}{coaddName}Diff_templateExp{warpTypeSuffix}",
90 def __init__(self, *, config=None):
91 super().__init__(config=config)
92 if config.requireCoaddAtGraphBuild:
93 self.coaddExposures = dataclasses.replace(
95 deferGraphConstraint=
False,
99class GetTemplateConfig(
100 pipeBase.PipelineTaskConfig, pipelineConnections=GetTemplateConnections
102 templateBorderSize = pexConfig.Field(
105 doc=
"Number of pixels to grow the requested template image to account for warping",
107 warp = pexConfig.ConfigField(
108 dtype=afwMath.Warper.ConfigClass,
109 doc=
"warper configuration",
111 doCorrectVariancePlateScale = pexConfig.Field(
114 doc=
"Correct the template variance for the total change in pixel"
115 " area between the images the coadds were built from and the science"
116 " image? This matters when the two have different plate scales, such"
117 " as DECam templates used for Rubin science images. The coadd pixel"
118 " grid cancels out of the total, so the skymap does not enter. It is a"
119 " single scalar factor, applied to the final template variance plane.",
121 coaddPsf = pexConfig.ConfigField(
122 doc=
"Configuration for CoaddPsf",
123 dtype=CoaddPsfConfig,
125 varianceBackground = pexConfig.ConfigurableField(
126 target=SubtractBackgroundTask,
127 doc=
"Task to estimate the background variance.",
129 highVarianceThreshold = pexConfig.RangeField(
133 doc=
"Set the HIGH_VARIANCE mask plane for regions with variance"
134 " greater than the median by this factor.",
136 highVarianceMaskFraction = pexConfig.Field(
139 doc=
"Minimum fraction of unmasked pixels needed to set the"
140 " HIGH_VARIANCE mask plane.",
142 doScaleVariance = pexConfig.Field(
145 doc=
"Scale variance of the template image?"
147 scaleVariance = pexConfig.ConfigurableField(
148 target=ScaleVarianceTask,
149 doc=
"Subtask to rescale the variance of the template to the statistically expected level."
151 requireCoaddAtGraphBuild = pexConfig.Field(
154 doc=
"If True, include the coadd dataset existence in the"
155 " initial butler query during QuantumGraph generation.",
158 def setDefaults(self):
161 self.warp.cacheSize = 100000
162 self.coaddPsf.cacheSize = self.warp.cacheSize
165 self.warp.interpLength = 100
166 self.warp.warpingKernelName =
"lanczos3"
167 self.coaddPsf.warpingKernelName = self.warp.warpingKernelName
170 self.varianceBackground.algorithm =
"LINEAR"
171 self.varianceBackground.binSize = 32
172 self.varianceBackground.useApprox =
False
173 self.varianceBackground.statisticsProperty =
"MEDIAN"
174 self.varianceBackground.doFilterSuperPixels =
True
175 self.varianceBackground.ignoredPixelMask = [
"BAD",
183class GetTemplateTask(pipeBase.PipelineTask):
184 ConfigClass = GetTemplateConfig
185 _DefaultName =
"getTemplate"
187 def __init__(self, *args, **kwargs):
188 super().__init__(*args, **kwargs)
189 if self.config.doScaleVariance:
190 self.makeSubtask(
"scaleVariance")
191 self.warper = afwMath.Warper.fromConfig(self.config.warp)
192 self.schema = afwTable.ExposureTable.makeMinimalSchema()
193 self.schema.addField(
194 "tract", type=np.int32, doc=
"Which tract this exposure came from."
196 self.schema.addField(
199 doc=
"Which patch in the tract this exposure came from.",
201 self.schema.addField(
204 doc=
"Weight for each exposure, used to make the CoaddPsf; should always be 1.",
206 self.makeSubtask(
"varianceBackground")
208 def runQuantum(self, butlerQC, inputRefs, outputRefs):
209 inputs = butlerQC.get(inputRefs)
210 bbox = inputs.pop(
"bbox")
211 wcs = inputs.pop(
"wcs")
212 coaddExposures = inputs.pop(
"coaddExposures")
213 skymap = inputs.pop(
"skyMap")
216 assert not inputs,
"runQuantum got more inputs than expected"
218 results = self.getExposures(coaddExposures, bbox, skymap, wcs)
219 physical_filter = butlerQC.quantum.dataId[
"physical_filter"]
221 coaddExposureHandles=results.coaddExposures,
224 dataIds=results.dataIds,
225 physical_filter=physical_filter,
226 visit=outputRefs.template.dataId[
"visit"],
228 butlerQC.put(outputs, outputRefs)
230 def getExposures(self, coaddExposureHandles, bbox, skymap, wcs):
231 """Return a data structure containing the coadds that overlap the
232 specified bbox projected onto the sky, and a corresponding data
233 structure of their dataIds.
234 These are the appropriate inputs to this task's `run` method.
236 The spatial index in the butler registry has generous padding and often
237 supplies patches near, but not directly overlapping the desired region.
238 This method filters the inputs so that `run` does not have to read in
239 all possibly-matching coadd exposures.
243 coaddExposureHandles : `iterable` \
244 [`lsst.daf.butler.DeferredDatasetHandle` of \
245 `lsst.afw.image.Exposure`]
246 Dataset handles to exposures that might overlap the desired
248 bbox : `lsst.geom.Box2I`
249 Template bounding box of the pixel geometry onto which the
250 coaddExposures will be resampled.
251 skymap : `lsst.skymap.SkyMap`
252 Geometry of the tracts and patches the coadds are defined on.
253 wcs : `lsst.afw.geom.SkyWcs`
254 Template WCS onto which the coadds will be resampled.
258 result : `lsst.pipe.base.Struct`
259 A struct with attributes:
262 Dict of coadd exposures that overlap the projected bbox,
264 (`dict` [`int`, `list` [`lsst.daf.butler.DeferredDatasetHandle` of
265 `lsst.afw.image.Exposure`] ]).
267 Dict of data IDs of the coadd exposures that overlap the
268 projected bbox, indexed on tract id
269 (`dict` [`int`, `list [`lsst.daf.butler.DataCoordinate`] ]).
274 Raised if no patches overlap the input detector bbox, or the input
278 raise pipeBase.NoWorkFound(
279 "WCS is None; cannot find overlapping exposures."
284 detectorCorners = wcs.pixelToSky(detectorPolygon.getCorners())
286 coaddExposures = collections.defaultdict(list)
287 dataIds = collections.defaultdict(list)
289 for coaddRef
in coaddExposureHandles:
290 dataId = coaddRef.dataId
291 patchWcs = skymap[dataId[
"tract"]].getWcs()
292 patchBBox = skymap[dataId[
"tract"]][dataId[
"patch"]].getOuterBBox()
298 detectorInPatchCoordinates = afwGeom.Polygon(patchWcs.skyToPixel(detectorCorners))
299 if patchPolygon.intersection(detectorInPatchCoordinates):
300 overlappingArea += patchPolygon.intersectionSingle(
301 detectorInPatchCoordinates
304 "Using template input tract=%s, patch=%s",
308 coaddExposures[dataId[
"tract"]].append(coaddRef)
309 dataIds[dataId[
"tract"]].append(dataId)
311 if not overlappingArea:
312 raise pipeBase.NoWorkFound(
"No patches overlap detector")
314 return pipeBase.Struct(coaddExposures=coaddExposures, dataIds=dataIds)
317 def run(self, *, coaddExposureHandles, bbox, wcs, dataIds, physical_filter, visit=None):
318 """Warp coadds from multiple tracts and patches to form a template to
319 subtract from a science image.
321 Tract and patch overlap regions are combined by a variance-weighted
322 average, and the variance planes are combined with the same weights,
323 not added in quadrature; the overlap regions are not statistically
324 independent, because they're derived from the same original data.
325 The PSF on the template is created by combining the CoaddPsf on each
326 template image into a meta-CoaddPsf.
330 coaddExposureHandles : `dict` [`int`, `list` of \
331 [`lsst.daf.butler.DeferredDatasetHandle` of \
332 `lsst.afw.image.Exposure`]]
333 Coadds to be mosaicked, indexed on tract id.
334 bbox : `lsst.geom.Box2I`
335 Template Bounding box of the detector geometry onto which to
336 resample the ``coaddExposureHandles``. Modified in-place to include the
338 wcs : `lsst.afw.geom.SkyWcs`
339 Template WCS onto which to resample the ``coaddExposureHandles``.
340 dataIds : `dict` [`int`, `list` [`lsst.daf.butler.DataCoordinate`]]
341 Record of the tract and patch of each coaddExposure, indexed on
343 physical_filter : `str`
344 Physical filter of the science image.
345 visit : `int`, optional
346 If supplied, over-write the visit ID in the template's visitInfo
347 so that downstream source injection tasks can link the template and
348 science image for the visit.
352 result : `lsst.pipe.base.Struct`
353 A struct with attributes:
356 A template coadd exposure assembled out of patches
357 (`lsst.afw.image.ExposureF`).
362 If no coadds are found with sufficient un-masked pixels.
364 band, photoCalib = self._checkInputs(dataIds, coaddExposureHandles)
366 bbox.grow(self.config.templateBorderSize)
376 plateScaleFactor =
None
377 if self.config.doCorrectVariancePlateScale
and coaddExposureHandles:
378 plateScaleFactor = self._plateScaleFactor(
379 next(iter(coaddExposureHandles.values()))[0], wcs, bbox
382 for tract
in coaddExposureHandles:
383 maskedImages, catalog, totalBox = self._makeExposureCatalog(
384 coaddExposureHandles[tract], dataIds[tract]
386 warpedBox = computeWarpedBBox(catalog[0].wcs, bbox, wcs)
389 unwarped, count, included = self._merge(
390 maskedImages, warpedBox, catalog[0].wcs
396 "No valid pixels from coadd patches in tract %s; not including in output.",
400 warpedBox.clip(totalBox)
401 potentialInput = self.warper.warpExposure(
402 wcs, unwarped.subset(warpedBox), destBBox=bbox
407 potentialInput.mask.array
408 & potentialInput.mask.getPlaneBitMask(
"NO_DATA")
411 "No overlap from coadd patches in tract %s; not including in output.",
417 tempCatalog = afwTable.ExposureCatalog(self.schema)
418 tempCatalog.reserve(len(included))
420 tempCatalog.append(catalog[i])
421 catalogs.append(tempCatalog)
422 warped[tract] = potentialInput.maskedImage
425 raise pipeBase.NoWorkFound(
"No patches found to overlap science exposure.")
428 template, count, _ = self._merge(warped, bbox, wcs)
430 raise pipeBase.NoWorkFound(
"No valid pixels in warped template.")
432 if self.config.doScaleVariance:
436 varianceFactor = self.scaleVariance.
run(template.maskedImage)
437 self.log.info(
"Template variance scaling factor: %.2f", varianceFactor)
438 self.metadata[
"scaleTemplateVarianceFactor"] = varianceFactor
440 self._correctVariance(template, plateScaleFactor)
443 catalog = afwTable.ExposureCatalog(self.schema)
444 catalog.reserve(sum([len(c)
for c
in catalogs]))
449 self.checkHighVariance(template)
450 if visit
is not None:
451 template.getInfo().setVisitInfo(
VisitInfo(id=visit))
453 template.setPhotoCalib(photoCalib)
454 template.setPsf(self._makePsf(template, catalog, wcs))
458 coaddInputs.ccds.extend(catalog, deep=
True)
459 template.getInfo().setCoaddInputs(coaddInputs)
460 return pipeBase.Struct(template=template)
463 """Correct the template variance plane for the effects of warping.
465 Compute a multiplicative correction factor for the template variance
466 if the plate scale of the coadd's constituent images is different than
467 the science image the template is being constructed for. This should
468 only be necessary if the coadd images were from a different instrument
469 than the science image. If the plate scale of the science instrument is
470 smaller than the plate scale of the coadd instrument, then the template
471 pixels will be correlated and the true variance will be higher than the
472 image pixel noise level would suggest.
476 template : `lsst.afw.image.Exposure`
477 Assembled template; its variance plane is modified in place.
478 plateScaleFactor : `float` or `None`
479 Correction for the change in pixel area, from `_plateScaleFactor`.
484 Raised if ``doCorrectVariancePlateScale`` is set but the factor
485 could not be reconstructed from the coadd inputs.
488 if self.config.doCorrectVariancePlateScale:
489 if plateScaleFactor
is None:
491 "doCorrectVariancePlateScale is set but no usable coaddInputs were found."
493 scale *= plateScaleFactor
494 self.metadata[
"variancePlateScaleFactor"] = plateScaleFactor
496 "Applying a plate scale variance factor of %.4f, reconstructed from the"
503 template.variance.array *= scale
504 self.metadata[
"templateVarianceCorrectionFactor"] = scale
506 "Corrected the template variance plane for warping by a factor of %.4f.",
511 """Compute the variance correction for the total change in pixel area
512 between the coadd's constituent images and the science image.
516 coaddHandle : `lsst.daf.butler.DeferredDatasetHandle` of \
517 `lsst.afw.image.Exposure`
518 Handle to one of the input coadd patches. Only its ``coaddInputs``
519 component is read; the pixels are left alone.
520 wcs : `lsst.afw.geom.SkyWcs`
521 WCS of the science image the template is being built for.
522 bbox : `lsst.geom.Box2I`
523 Bounding box of the template, used only to choose where to
524 evaluate the science image pixel scale.
528 factor : `float` or `None`
529 The correction factor equivalent to the ratio of the area of a pixel
530 from the coadd's instrument to the science instrument, or `None` if
531 this patch carries no input record from which it could be computed.
533 coaddInputs = coaddHandle.get(component=
"coaddInputs")
534 if coaddInputs
is None:
536 scienceScale = wcs.getPixelScale(
geom.Box2D(bbox).getCenter()).asArcseconds()
538 for record
in coaddInputs.ccds:
541 originalWcs = record.getWcs()
542 if originalWcs
is None:
544 center =
geom.Box2D(record.getBBox()).getCenter()
545 pviScale = originalWcs.getPixelScale(center).asArcseconds()
547 factor = (pviScale/scienceScale)**2
548 if np.isfinite(factor)
and factor > 0:
553 """Set a mask plane for regions with unusually high variance.
557 template : `lsst.afw.image.Exposure`
558 The warped template exposure, which will be modified in place.
560 highVarianceMaskPlaneBit = template.mask.addMaskPlane(
"HIGH_VARIANCE")
561 ignoredPixelBits = template.mask.getPlaneBitMask(self.varianceBackground.config.ignoredPixelMask)
562 goodMask = (template.mask.array & ignoredPixelBits) == 0
563 goodFraction = np.count_nonzero(goodMask)/template.mask.array.size
564 if goodFraction < self.config.highVarianceMaskFraction:
565 self.log.info(
"Not setting HIGH_VARIANCE mask plane, only %2.1f%% of"
566 " pixels were unmasked for background estimation, but"
567 " %2.1f%% are required", 100*goodFraction, 100*self.config.highVarianceMaskFraction)
569 varianceExposure = template.clone()
570 varianceExposure.image.array = varianceExposure.variance.array
571 varianceBackground = self.varianceBackground.
run(varianceExposure).background.getImage().array
572 threshold = self.config.highVarianceThreshold*np.nanmedian(varianceBackground)
573 highVariancePix = varianceBackground > threshold
574 template.mask.array[highVariancePix] |= 2**highVarianceMaskPlaneBit
577 def _checkInputs(dataIds, coaddExposures):
578 """Check that the all the dataIds are from the same band and that
579 the exposures all have the same photometric calibration.
583 dataIds : `dict` [`int`, `list` [`lsst.daf.butler.DataCoordinate`]]
584 Record of the tract and patch of each coaddExposure.
585 coaddExposures : `dict` [`int`, `list` of \
586 [`lsst.daf.butler.DeferredDatasetHandle` of \
587 `lsst.afw.image.Exposure` or
588 `lsst.afw.image.Exposure`]]
589 Coadds to be mosaicked.
594 Filter band of all the input exposures.
595 photoCalib : `lsst.afw.image.PhotoCalib`
596 Photometric calibration of all of the input exposures.
601 Raised if the bands or calibrations of the input exposures are not
604 bands = set(dataId[
"band"]
for tract
in dataIds
for dataId
in dataIds[tract])
606 raise RuntimeError(f
"GetTemplateTask called with multiple bands: {bands}")
609 exposure.get(component=
"photoCalib")
610 for exposures
in coaddExposures.values()
611 for exposure
in exposures
613 if not all([photoCalibs[0] == x
for x
in photoCalibs]):
614 msg = f
"GetTemplateTask called with exposures with different photoCalibs: {photoCalibs}"
615 raise RuntimeError(msg)
616 photoCalib = photoCalibs[0]
617 return band, photoCalib
620 """Make an exposure catalog for one tract.
624 exposureRefs : `list` of [`lsst.daf.butler.DeferredDatasetHandle` of \
625 `lsst.afw.image.Exposure`]
626 Exposures to include in the catalog.
627 dataIds : `list` [`lsst.daf.butler.DataCoordinate`]
628 Data ids of each of the included exposures; must have "tract" and
633 images : `dict` [`lsst.afw.image.MaskedImage`]
634 MaskedImages of each of the input exposures, for warping.
635 catalog : `lsst.afw.table.ExposureCatalog`
636 Catalog of metadata for each exposure
637 totalBox : `lsst.geom.Box2I`
638 The union of the bounding boxes of all the input exposures.
640 catalog = afwTable.ExposureCatalog(self.schema)
641 catalog.reserve(len(exposureRefs))
642 exposures = (exposureRef.get()
for exposureRef
in exposureRefs)
646 for coadd, dataId
in zip(exposures, dataIds):
647 images[dataId] = coadd.maskedImage
648 bbox = coadd.getBBox()
649 totalBox = totalBox.expandedTo(bbox)
650 record = catalog.addNew()
651 record.setPsf(coadd.psf)
652 record.setWcs(coadd.wcs)
653 record.setPhotoCalib(coadd.photoCalib)
655 record.setValidPolygon(afwGeom.Polygon(
geom.Box2D(bbox).getCorners()))
656 record.set(
"tract", dataId[
"tract"])
657 record.set(
"patch", dataId[
"patch"])
660 record.set(
"weight", 1)
662 return images, catalog, totalBox
664 def _merge(self, maskedImages, bbox, wcs):
665 """Merge the images that came from one tract into one larger image,
666 ignoring NaN pixels and non-finite variance pixels from individual
671 maskedImages : `dict` [`lsst.afw.image.MaskedImage` or
672 `lsst.afw.image.Exposure`]
673 Images to be merged into one larger bounding box.
674 bbox : `lsst.geom.Box2I`
675 Bounding box defining the image to merge into.
676 wcs : `lsst.afw.geom.SkyWcs`
677 WCS of all of the input images to set on the output image.
681 merged : `lsst.afw.image.MaskedImage`
682 Merged image with all of the inputs at their respective bbox
685 Count of the number of good pixels (those with positive weights)
687 included : `list` [`int`]
688 List of indexes of patches that were included in the merged
689 result, to be used to trim the exposure catalog.
691 merged = afwImage.ExposureF(bbox, wcs)
692 weights = afwImage.ImageF(bbox)
694 for i, (dataId, maskedImage)
in enumerate(maskedImages.items()):
696 clippedBox =
geom.Box2I(maskedImage.getBBox())
697 clippedBox.clip(bbox)
698 if clippedBox.area == 0:
699 self.log.debug(
"%s does not overlap template region.", dataId)
701 maskedImage = maskedImage.subset(clippedBox)
703 good = (maskedImage.variance.array > 0) & (
704 np.isfinite(maskedImage.variance.array)
706 weight = maskedImage.variance.array[good] ** (-0.5)
707 bad = np.isnan(maskedImage.image.array) | ~good
710 maskedImage.image.array[bad] = 0.0
711 maskedImage.variance.array[bad] = 0.0
713 maskedImage.mask.array[bad] = 0
717 maskedImage.image.array[good] *= weight
718 maskedImage.variance.array[good] *= weight
719 weights[clippedBox].array[good] += weight
722 merged.maskedImage[clippedBox] += maskedImage
725 good = weights.array > 0
730 weights = weights.array[good]
731 merged.image.array[good] /= weights
732 merged.variance.array[good] /= weights
734 merged.mask.array[~good] |= merged.mask.getPlaneBitMask(
"NO_DATA")
736 return merged, good.sum(), included
739 """Return a PSF containing the PSF at each of the input regions.
741 Note that although this includes all the exposures from the catalog,
742 the PSF knows which part of the template the inputs came from, so when
743 evaluated at a given position it will not include inputs that never
744 went in to those pixels.
748 template : `lsst.afw.image.Exposure`
749 Generated template the PSF is for.
750 catalog : `lsst.afw.table.ExposureCatalog`
751 Catalog of exposures that went into the template that contains all
753 wcs : `lsst.afw.geom.SkyWcs`
754 WCS of the template, to warp the PSFs to.
758 coaddPsf : `lsst.meas.algorithms.CoaddPsf`
759 The meta-psf constructed from all of the input catalogs.
763 boolmask = template.mask.array & template.mask.getPlaneBitMask(
"NO_DATA") == 0
765 centerCoord = afwGeom.SpanSet.fromMask(maskx, 1).computeCentroid()
767 ctrl = self.config.coaddPsf.makeControl()
769 catalog, wcs, centerCoord, ctrl.warpingKernelName, ctrl.cacheSize
775 GetTemplateConnections,
776 dimensions=(
"instrument",
"visit",
"detector"),
777 defaultTemplates={
"coaddName":
"dcr",
"warpTypeSuffix":
"",
"fakesType":
""},
779 visitInfo = pipeBase.connectionTypes.Input(
780 doc=
"VisitInfo of calexp used to determine observing conditions.",
781 name=
"{fakesType}calexp.visitInfo",
782 storageClass=
"VisitInfo",
783 dimensions=(
"instrument",
"visit",
"detector"),
785 dcrCoadds = pipeBase.connectionTypes.Input(
786 doc=
"Input DCR template to match and subtract from the exposure",
787 name=
"{fakesType}dcrCoadd{warpTypeSuffix}",
788 storageClass=
"ExposureF",
789 dimensions=(
"tract",
"patch",
"skymap",
"band",
"subfilter"),
794 def __init__(self, *, config=None):
795 super().__init__(config=config)
796 self.inputs.remove(
"coaddExposures")
799class GetDcrTemplateConfig(
800 GetTemplateConfig, pipelineConnections=GetDcrTemplateConnections
802 numSubfilters = pexConfig.Field(
803 doc=
"Number of subfilters in the DcrCoadd.",
807 effectiveWavelength = pexConfig.Field(
808 doc=
"Effective wavelength of the filter in nm.",
812 bandwidth = pexConfig.Field(
813 doc=
"Bandwidth of the physical filter.",
819 if self.effectiveWavelength
is None or self.bandwidth
is None:
821 "The effective wavelength and bandwidth of the physical filter "
822 "must be set in the getTemplate config for DCR coadds. "
823 "Required until transmission curves are used in DM-13668."
827class GetDcrTemplateTask(GetTemplateTask):
828 ConfigClass = GetDcrTemplateConfig
829 _DefaultName =
"getDcrTemplate"
831 def runQuantum(self, butlerQC, inputRefs, outputRefs):
832 inputs = butlerQC.get(inputRefs)
833 bbox = inputs.pop(
"bbox")
834 wcs = inputs.pop(
"wcs")
835 dcrCoaddExposureHandles = inputs.pop(
"dcrCoadds")
836 skymap = inputs.pop(
"skyMap")
837 visitInfo = inputs.pop(
"visitInfo")
840 assert not inputs,
"runQuantum got more inputs than expected"
842 results = self.getExposures(
843 dcrCoaddExposureHandles, bbox, skymap, wcs, visitInfo
845 physical_filter = butlerQC.quantum.dataId[
"physical_filter"]
847 coaddExposureHandles=results.coaddExposures,
850 dataIds=results.dataIds,
851 physical_filter=physical_filter,
853 butlerQC.put(outputs, outputRefs)
855 def getExposures(self, dcrCoaddExposureHandles, bbox, skymap, wcs, visitInfo):
856 """Return lists of coadds and their corresponding dataIds that overlap
859 The spatial index in the registry has generous padding and often
860 supplies patches near, but not directly overlapping the detector.
861 Filters inputs so that we don't have to read in all input coadds.
865 dcrCoaddExposureHandles : `list` \
866 [`lsst.daf.butler.DeferredDatasetHandle` of \
867 `lsst.afw.image.Exposure`]
868 Data references to exposures that might overlap the detector.
869 bbox : `lsst.geom.Box2I`
870 Template Bounding box of the detector geometry onto which to
871 resample the coaddExposures.
872 skymap : `lsst.skymap.SkyMap`
873 Input definition of geometry/bbox and projection/wcs for
875 wcs : `lsst.afw.geom.SkyWcs`
876 Template WCS onto which to resample the coaddExposures.
877 visitInfo : `lsst.afw.image.VisitInfo`
878 Metadata for the science image.
882 result : `lsst.pipe.base.Struct`
883 A struct with attibutes:
886 Dict of coadd exposures that overlap the projected bbox,
888 (`dict` [`int`, `list` [`lsst.afw.image.Exposure`] ]).
890 Dict of data IDs of the coadd exposures that overlap the
891 projected bbox, indexed on tract id
892 (`dict` [`int`, `list [`lsst.daf.butler.DataCoordinate`] ]).
897 Raised if no patches overlatp the input detector bbox.
902 raise pipeBase.NoWorkFound(
"Exposure has no WCS; cannot create a template.")
906 dataIds = collections.defaultdict(list)
908 for coaddRef
in dcrCoaddExposureHandles:
909 dataId = coaddRef.dataId
910 subfilter = dataId[
"subfilter"]
911 patchWcs = skymap[dataId[
"tract"]].getWcs()
912 patchBBox = skymap[dataId[
"tract"]][dataId[
"patch"]].getOuterBBox()
913 patchCorners = patchWcs.pixelToSky(
geom.Box2D(patchBBox).getCorners())
914 patchPolygon = afwGeom.Polygon(wcs.skyToPixel(patchCorners))
915 if patchPolygon.intersection(detectorPolygon):
916 overlappingArea += patchPolygon.intersectionSingle(
920 "Using template input tract=%s, patch=%s, subfilter=%s"
921 % (dataId[
"tract"], dataId[
"patch"], dataId[
"subfilter"])
923 if dataId[
"tract"]
in patchList:
924 patchList[dataId[
"tract"]].append(dataId[
"patch"])
926 patchList[dataId[
"tract"]] = [
930 dataIds[dataId[
"tract"]].append(dataId)
932 if not overlappingArea:
933 raise pipeBase.NoWorkFound(
"No patches overlap detector")
935 self.checkPatchList(patchList)
937 coaddExposures = self.getDcrModel(patchList, dcrCoaddExposureHandles, visitInfo)
938 return pipeBase.Struct(coaddExposures=coaddExposures, dataIds=dataIds)
941 """Check that all of the DcrModel subfilters are present for each
947 Dict of the patches containing valid data for each tract.
952 If the number of exposures found for a patch does not match the
953 number of subfilters.
955 for tract
in patchList:
956 for patch
in set(patchList[tract]):
957 if patchList[tract].count(patch) != self.config.numSubfilters:
959 "Invalid number of DcrModel subfilters found: %d vs %d expected",
960 patchList[tract].count(patch),
961 self.config.numSubfilters,
965 """Build DCR-matched coadds from a list of exposure references.
970 Dict of the patches containing valid data for each tract.
971 coaddRefs : `list` [`lsst.daf.butler.DeferredDatasetHandle`]
972 Data references to `~lsst.afw.image.Exposure` representing
973 DcrModels that overlap the detector.
974 visitInfo : `lsst.afw.image.VisitInfo`
975 Metadata for the science image.
979 coaddExposures : `list` [`lsst.afw.image.Exposure`]
980 Coadd exposures that overlap the detector.
982 coaddExposures = collections.defaultdict(list)
983 for tract
in patchList:
984 for patch
in set(patchList[tract]):
987 for coaddRef
in coaddRefs
991 dcrModel = DcrModel.fromQuantum(
993 self.config.effectiveWavelength,
994 self.config.bandwidth,
995 self.config.numSubfilters,
997 coaddExposures[tract].append(dcrModel.buildMatchedExposureHandle(visitInfo=visitInfo))
998 return coaddExposures
1002 condition = (coaddRef.dataId[
"tract"] == tract) & (
1003 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)
checkPatchList(self, patchList)
_selectDataRef(coaddRef, tract, patch)