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, with
357 pixels in nJy (`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))
454 if photoCalib
is not None and photoCalib != identity:
455 template.maskedImage = photoCalib.calibrateImage(template.maskedImage)
456 photoCalib = identity
457 template.setPhotoCalib(photoCalib)
458 template.setPsf(self._makePsf(template, catalog, wcs))
462 coaddInputs.ccds.extend(catalog, deep=
True)
463 template.getInfo().setCoaddInputs(coaddInputs)
464 return pipeBase.Struct(template=template)
467 """Correct the template variance plane for the effects of warping.
469 Compute a multiplicative correction factor for the template variance
470 if the plate scale of the coadd's constituent images is different than
471 the science image the template is being constructed for. This should
472 only be necessary if the coadd images were from a different instrument
473 than the science image. If the plate scale of the science instrument is
474 smaller than the plate scale of the coadd instrument, then the template
475 pixels will be correlated and the true variance will be higher than the
476 image pixel noise level would suggest.
480 template : `lsst.afw.image.Exposure`
481 Assembled template; its variance plane is modified in place.
482 plateScaleFactor : `float` or `None`
483 Correction for the change in pixel area, from `_plateScaleFactor`.
488 Raised if ``doCorrectVariancePlateScale`` is set but the factor
489 could not be reconstructed from the coadd inputs.
492 if self.config.doCorrectVariancePlateScale:
493 if plateScaleFactor
is None:
495 "doCorrectVariancePlateScale is set but no usable coaddInputs were found."
497 scale *= plateScaleFactor
498 self.metadata[
"variancePlateScaleFactor"] = plateScaleFactor
500 "Applying a plate scale variance factor of %.4f, reconstructed from the"
507 template.variance.array *= scale
508 self.metadata[
"templateVarianceCorrectionFactor"] = scale
510 "Corrected the template variance plane for warping by a factor of %.4f.",
515 """Compute the variance correction for the total change in pixel area
516 between the coadd's constituent images and the science image.
520 coaddHandle : `lsst.daf.butler.DeferredDatasetHandle` of \
521 `lsst.afw.image.Exposure`
522 Handle to one of the input coadd patches. Only its ``coaddInputs``
523 component is read; the pixels are left alone.
524 wcs : `lsst.afw.geom.SkyWcs`
525 WCS of the science image the template is being built for.
526 bbox : `lsst.geom.Box2I`
527 Bounding box of the template, used only to choose where to
528 evaluate the science image pixel scale.
532 factor : `float` or `None`
533 The correction factor equivalent to the ratio of the area of a pixel
534 from the coadd's instrument to the science instrument, or `None` if
535 this patch carries no input record from which it could be computed.
537 coaddInputs = coaddHandle.get(component=
"coaddInputs")
538 if coaddInputs
is None:
540 scienceScale = wcs.getPixelScale(
geom.Box2D(bbox).getCenter()).asArcseconds()
542 for record
in coaddInputs.ccds:
545 originalWcs = record.getWcs()
546 if originalWcs
is None:
548 center =
geom.Box2D(record.getBBox()).getCenter()
549 pviScale = originalWcs.getPixelScale(center).asArcseconds()
551 factor = (pviScale/scienceScale)**2
552 if np.isfinite(factor)
and factor > 0:
557 """Set a mask plane for regions with unusually high variance.
561 template : `lsst.afw.image.Exposure`
562 The warped template exposure, which will be modified in place.
564 highVarianceMaskPlaneBit = template.mask.addMaskPlane(
"HIGH_VARIANCE")
565 ignoredPixelBits = template.mask.getPlaneBitMask(self.varianceBackground.config.ignoredPixelMask)
566 goodMask = (template.mask.array & ignoredPixelBits) == 0
567 goodFraction = np.count_nonzero(goodMask)/template.mask.array.size
568 if goodFraction < self.config.highVarianceMaskFraction:
569 self.log.info(
"Not setting HIGH_VARIANCE mask plane, only %2.1f%% of"
570 " pixels were unmasked for background estimation, but"
571 " %2.1f%% are required", 100*goodFraction, 100*self.config.highVarianceMaskFraction)
573 varianceExposure = template.clone()
574 varianceExposure.image.array = varianceExposure.variance.array
575 varianceBackground = self.varianceBackground.
run(varianceExposure).background.getImage().array
576 threshold = self.config.highVarianceThreshold*np.nanmedian(varianceBackground)
577 highVariancePix = varianceBackground > threshold
578 template.mask.array[highVariancePix] |= 2**highVarianceMaskPlaneBit
581 def _checkInputs(dataIds, coaddExposures):
582 """Check that the all the dataIds are from the same band and that
583 the exposures all have the same photometric calibration.
587 dataIds : `dict` [`int`, `list` [`lsst.daf.butler.DataCoordinate`]]
588 Record of the tract and patch of each coaddExposure.
589 coaddExposures : `dict` [`int`, `list` of \
590 [`lsst.daf.butler.DeferredDatasetHandle` of \
591 `lsst.afw.image.Exposure` or
592 `lsst.afw.image.Exposure`]]
593 Coadds to be mosaicked.
598 Filter band of all the input exposures.
599 photoCalib : `lsst.afw.image.PhotoCalib`
600 Photometric calibration of all of the input exposures.
605 Raised if the bands or calibrations of the input exposures are not
608 bands = set(dataId[
"band"]
for tract
in dataIds
for dataId
in dataIds[tract])
610 raise RuntimeError(f
"GetTemplateTask called with multiple bands: {bands}")
613 exposure.get(component=
"photoCalib")
614 for exposures
in coaddExposures.values()
615 for exposure
in exposures
617 if not all([photoCalibs[0] == x
for x
in photoCalibs]):
618 msg = f
"GetTemplateTask called with exposures with different photoCalibs: {photoCalibs}"
619 raise RuntimeError(msg)
620 photoCalib = photoCalibs[0]
621 return band, photoCalib
624 """Make an exposure catalog for one tract.
628 exposureRefs : `list` of [`lsst.daf.butler.DeferredDatasetHandle` of \
629 `lsst.afw.image.Exposure`]
630 Exposures to include in the catalog.
631 dataIds : `list` [`lsst.daf.butler.DataCoordinate`]
632 Data ids of each of the included exposures; must have "tract" and
637 images : `dict` [`lsst.afw.image.MaskedImage`]
638 MaskedImages of each of the input exposures, for warping.
639 catalog : `lsst.afw.table.ExposureCatalog`
640 Catalog of metadata for each exposure
641 totalBox : `lsst.geom.Box2I`
642 The union of the bounding boxes of all the input exposures.
644 catalog = afwTable.ExposureCatalog(self.schema)
645 catalog.reserve(len(exposureRefs))
646 exposures = (exposureRef.get()
for exposureRef
in exposureRefs)
650 for coadd, dataId
in zip(exposures, dataIds):
651 images[dataId] = coadd.maskedImage
652 bbox = coadd.getBBox()
653 totalBox = totalBox.expandedTo(bbox)
654 record = catalog.addNew()
655 record.setPsf(coadd.psf)
656 record.setWcs(coadd.wcs)
657 record.setPhotoCalib(coadd.photoCalib)
659 record.setValidPolygon(afwGeom.Polygon(
geom.Box2D(bbox).getCorners()))
660 record.set(
"tract", dataId[
"tract"])
661 record.set(
"patch", dataId[
"patch"])
664 record.set(
"weight", 1)
666 return images, catalog, totalBox
668 def _merge(self, maskedImages, bbox, wcs):
669 """Merge the images that came from one tract into one larger image,
670 ignoring NaN pixels and non-finite variance pixels from individual
675 maskedImages : `dict` [`lsst.afw.image.MaskedImage` or
676 `lsst.afw.image.Exposure`]
677 Images to be merged into one larger bounding box.
678 bbox : `lsst.geom.Box2I`
679 Bounding box defining the image to merge into.
680 wcs : `lsst.afw.geom.SkyWcs`
681 WCS of all of the input images to set on the output image.
685 merged : `lsst.afw.image.MaskedImage`
686 Merged image with all of the inputs at their respective bbox
689 Count of the number of good pixels (those with positive weights)
691 included : `list` [`int`]
692 List of indexes of patches that were included in the merged
693 result, to be used to trim the exposure catalog.
695 merged = afwImage.ExposureF(bbox, wcs)
696 weights = afwImage.ImageF(bbox)
698 for i, (dataId, maskedImage)
in enumerate(maskedImages.items()):
700 clippedBox =
geom.Box2I(maskedImage.getBBox())
701 clippedBox.clip(bbox)
702 if clippedBox.area == 0:
703 self.log.debug(
"%s does not overlap template region.", dataId)
705 maskedImage = maskedImage.subset(clippedBox)
707 good = (maskedImage.variance.array > 0) & (
708 np.isfinite(maskedImage.variance.array)
710 weight = maskedImage.variance.array[good] ** (-0.5)
711 bad = np.isnan(maskedImage.image.array) | ~good
714 maskedImage.image.array[bad] = 0.0
715 maskedImage.variance.array[bad] = 0.0
717 maskedImage.mask.array[bad] = 0
721 maskedImage.image.array[good] *= weight
722 maskedImage.variance.array[good] *= weight
723 weights[clippedBox].array[good] += weight
726 merged.maskedImage[clippedBox] += maskedImage
729 good = weights.array > 0
734 weights = weights.array[good]
735 merged.image.array[good] /= weights
736 merged.variance.array[good] /= weights
738 merged.mask.array[~good] |= merged.mask.getPlaneBitMask(
"NO_DATA")
740 return merged, good.sum(), included
743 """Return a PSF containing the PSF at each of the input regions.
745 Note that although this includes all the exposures from the catalog,
746 the PSF knows which part of the template the inputs came from, so when
747 evaluated at a given position it will not include inputs that never
748 went in to those pixels.
752 template : `lsst.afw.image.Exposure`
753 Generated template the PSF is for.
754 catalog : `lsst.afw.table.ExposureCatalog`
755 Catalog of exposures that went into the template that contains all
757 wcs : `lsst.afw.geom.SkyWcs`
758 WCS of the template, to warp the PSFs to.
762 coaddPsf : `lsst.meas.algorithms.CoaddPsf`
763 The meta-psf constructed from all of the input catalogs.
767 boolmask = template.mask.array & template.mask.getPlaneBitMask(
"NO_DATA") == 0
769 centerCoord = afwGeom.SpanSet.fromMask(maskx, 1).computeCentroid()
771 ctrl = self.config.coaddPsf.makeControl()
773 catalog, wcs, centerCoord, ctrl.warpingKernelName, ctrl.cacheSize
779 GetTemplateConnections,
780 dimensions=(
"instrument",
"visit",
"detector"),
781 defaultTemplates={
"coaddName":
"dcr",
"warpTypeSuffix":
"",
"fakesType":
""},
783 visitInfo = pipeBase.connectionTypes.Input(
784 doc=
"VisitInfo of calexp used to determine observing conditions.",
785 name=
"{fakesType}calexp.visitInfo",
786 storageClass=
"VisitInfo",
787 dimensions=(
"instrument",
"visit",
"detector"),
789 dcrCoadds = pipeBase.connectionTypes.Input(
790 doc=
"Input DCR template to match and subtract from the exposure",
791 name=
"{fakesType}dcrCoadd{warpTypeSuffix}",
792 storageClass=
"ExposureF",
793 dimensions=(
"tract",
"patch",
"skymap",
"band",
"subfilter"),
798 def __init__(self, *, config=None):
799 super().__init__(config=config)
800 self.inputs.remove(
"coaddExposures")
803class GetDcrTemplateConfig(
804 GetTemplateConfig, pipelineConnections=GetDcrTemplateConnections
806 numSubfilters = pexConfig.Field(
807 doc=
"Number of subfilters in the DcrCoadd.",
811 effectiveWavelength = pexConfig.Field(
812 doc=
"Effective wavelength of the filter in nm.",
816 bandwidth = pexConfig.Field(
817 doc=
"Bandwidth of the physical filter.",
823 if self.effectiveWavelength
is None or self.bandwidth
is None:
825 "The effective wavelength and bandwidth of the physical filter "
826 "must be set in the getTemplate config for DCR coadds. "
827 "Required until transmission curves are used in DM-13668."
831class GetDcrTemplateTask(GetTemplateTask):
832 ConfigClass = GetDcrTemplateConfig
833 _DefaultName =
"getDcrTemplate"
835 def runQuantum(self, butlerQC, inputRefs, outputRefs):
836 inputs = butlerQC.get(inputRefs)
837 bbox = inputs.pop(
"bbox")
838 wcs = inputs.pop(
"wcs")
839 dcrCoaddExposureHandles = inputs.pop(
"dcrCoadds")
840 skymap = inputs.pop(
"skyMap")
841 visitInfo = inputs.pop(
"visitInfo")
844 assert not inputs,
"runQuantum got more inputs than expected"
846 results = self.getExposures(
847 dcrCoaddExposureHandles, bbox, skymap, wcs, visitInfo
849 physical_filter = butlerQC.quantum.dataId[
"physical_filter"]
851 coaddExposureHandles=results.coaddExposures,
854 dataIds=results.dataIds,
855 physical_filter=physical_filter,
857 butlerQC.put(outputs, outputRefs)
859 def getExposures(self, dcrCoaddExposureHandles, bbox, skymap, wcs, visitInfo):
860 """Return lists of coadds and their corresponding dataIds that overlap
863 The spatial index in the registry has generous padding and often
864 supplies patches near, but not directly overlapping the detector.
865 Filters inputs so that we don't have to read in all input coadds.
869 dcrCoaddExposureHandles : `list` \
870 [`lsst.daf.butler.DeferredDatasetHandle` of \
871 `lsst.afw.image.Exposure`]
872 Data references to exposures that might overlap the detector.
873 bbox : `lsst.geom.Box2I`
874 Template Bounding box of the detector geometry onto which to
875 resample the coaddExposures.
876 skymap : `lsst.skymap.SkyMap`
877 Input definition of geometry/bbox and projection/wcs for
879 wcs : `lsst.afw.geom.SkyWcs`
880 Template WCS onto which to resample the coaddExposures.
881 visitInfo : `lsst.afw.image.VisitInfo`
882 Metadata for the science image.
886 result : `lsst.pipe.base.Struct`
887 A struct with attibutes:
890 Dict of coadd exposures that overlap the projected bbox,
892 (`dict` [`int`, `list` [`lsst.afw.image.Exposure`] ]).
894 Dict of data IDs of the coadd exposures that overlap the
895 projected bbox, indexed on tract id
896 (`dict` [`int`, `list [`lsst.daf.butler.DataCoordinate`] ]).
901 Raised if no patches overlatp the input detector bbox.
906 raise pipeBase.NoWorkFound(
"Exposure has no WCS; cannot create a template.")
910 dataIds = collections.defaultdict(list)
912 for coaddRef
in dcrCoaddExposureHandles:
913 dataId = coaddRef.dataId
914 subfilter = dataId[
"subfilter"]
915 patchWcs = skymap[dataId[
"tract"]].getWcs()
916 patchBBox = skymap[dataId[
"tract"]][dataId[
"patch"]].getOuterBBox()
917 patchCorners = patchWcs.pixelToSky(
geom.Box2D(patchBBox).getCorners())
918 patchPolygon = afwGeom.Polygon(wcs.skyToPixel(patchCorners))
919 if patchPolygon.intersection(detectorPolygon):
920 overlappingArea += patchPolygon.intersectionSingle(
924 "Using template input tract=%s, patch=%s, subfilter=%s"
925 % (dataId[
"tract"], dataId[
"patch"], dataId[
"subfilter"])
927 if dataId[
"tract"]
in patchList:
928 patchList[dataId[
"tract"]].append(dataId[
"patch"])
930 patchList[dataId[
"tract"]] = [
934 dataIds[dataId[
"tract"]].append(dataId)
936 if not overlappingArea:
937 raise pipeBase.NoWorkFound(
"No patches overlap detector")
939 self.checkPatchList(patchList)
941 coaddExposures = self.getDcrModel(patchList, dcrCoaddExposureHandles, visitInfo)
942 return pipeBase.Struct(coaddExposures=coaddExposures, dataIds=dataIds)
945 """Check that all of the DcrModel subfilters are present for each
951 Dict of the patches containing valid data for each tract.
956 If the number of exposures found for a patch does not match the
957 number of subfilters.
959 for tract
in patchList:
960 for patch
in set(patchList[tract]):
961 if patchList[tract].count(patch) != self.config.numSubfilters:
963 "Invalid number of DcrModel subfilters found: %d vs %d expected",
964 patchList[tract].count(patch),
965 self.config.numSubfilters,
969 """Build DCR-matched coadds from a list of exposure references.
974 Dict of the patches containing valid data for each tract.
975 coaddRefs : `list` [`lsst.daf.butler.DeferredDatasetHandle`]
976 Data references to `~lsst.afw.image.Exposure` representing
977 DcrModels that overlap the detector.
978 visitInfo : `lsst.afw.image.VisitInfo`
979 Metadata for the science image.
983 coaddExposures : `list` [`lsst.afw.image.Exposure`]
984 Coadd exposures that overlap the detector.
986 coaddExposures = collections.defaultdict(list)
987 for tract
in patchList:
988 for patch
in set(patchList[tract]):
991 for coaddRef
in coaddRefs
995 dcrModel = DcrModel.fromQuantum(
997 self.config.effectiveWavelength,
998 self.config.bandwidth,
999 self.config.numSubfilters,
1001 coaddExposures[tract].append(dcrModel.buildMatchedExposureHandle(visitInfo=visitInfo))
1002 return coaddExposures
1006 condition = (coaddRef.dataId[
"tract"] == tract) & (
1007 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)