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 coaddPsf = pexConfig.ConfigField(
112 doc=
"Configuration for CoaddPsf",
113 dtype=CoaddPsfConfig,
115 varianceBackground = pexConfig.ConfigurableField(
116 target=SubtractBackgroundTask,
117 doc=
"Task to estimate the background variance.",
119 highVarianceThreshold = pexConfig.RangeField(
123 doc=
"Set the HIGH_VARIANCE mask plane for regions with variance"
124 " greater than the median by this factor.",
126 highVarianceMaskFraction = pexConfig.Field(
129 doc=
"Minimum fraction of unmasked pixels needed to set the"
130 " HIGH_VARIANCE mask plane.",
132 doScaleVariance = pexConfig.Field(
135 doc=
"Scale variance of the template image?"
137 scaleVariance = pexConfig.ConfigurableField(
138 target=ScaleVarianceTask,
139 doc=
"Subtask to rescale the variance of the template to the statistically expected level."
141 requireCoaddAtGraphBuild = pexConfig.Field(
144 doc=
"If True, include the coadd dataset existence in the"
145 " initial butler query during QuantumGraph generation.",
148 def setDefaults(self):
151 self.warp.cacheSize = 100000
152 self.coaddPsf.cacheSize = self.warp.cacheSize
155 self.warp.interpLength = 100
156 self.warp.warpingKernelName =
"lanczos3"
157 self.coaddPsf.warpingKernelName = self.warp.warpingKernelName
160 self.varianceBackground.algorithm =
"LINEAR"
161 self.varianceBackground.binSize = 32
162 self.varianceBackground.useApprox =
False
163 self.varianceBackground.statisticsProperty =
"MEDIAN"
164 self.varianceBackground.doFilterSuperPixels =
True
165 self.varianceBackground.ignoredPixelMask = [
"BAD",
173class GetTemplateTask(pipeBase.PipelineTask):
174 ConfigClass = GetTemplateConfig
175 _DefaultName =
"getTemplate"
177 def __init__(self, *args, **kwargs):
178 super().__init__(*args, **kwargs)
179 if self.config.doScaleVariance:
180 self.makeSubtask(
"scaleVariance")
181 self.warper = afwMath.Warper.fromConfig(self.config.warp)
182 self.schema = afwTable.ExposureTable.makeMinimalSchema()
183 self.schema.addField(
184 "tract", type=np.int32, doc=
"Which tract this exposure came from."
186 self.schema.addField(
189 doc=
"Which patch in the tract this exposure came from.",
191 self.schema.addField(
194 doc=
"Weight for each exposure, used to make the CoaddPsf; should always be 1.",
196 self.makeSubtask(
"varianceBackground")
198 def runQuantum(self, butlerQC, inputRefs, outputRefs):
199 inputs = butlerQC.get(inputRefs)
200 bbox = inputs.pop(
"bbox")
201 wcs = inputs.pop(
"wcs")
202 coaddExposures = inputs.pop(
"coaddExposures")
203 skymap = inputs.pop(
"skyMap")
206 assert not inputs,
"runQuantum got more inputs than expected"
208 results = self.getExposures(coaddExposures, bbox, skymap, wcs)
209 physical_filter = butlerQC.quantum.dataId[
"physical_filter"]
211 coaddExposureHandles=results.coaddExposures,
214 dataIds=results.dataIds,
215 physical_filter=physical_filter,
216 visit=outputRefs.template.dataId[
"visit"],
218 butlerQC.put(outputs, outputRefs)
220 def getExposures(self, coaddExposureHandles, bbox, skymap, wcs):
221 """Return a data structure containing the coadds that overlap the
222 specified bbox projected onto the sky, and a corresponding data
223 structure of their dataIds.
224 These are the appropriate inputs to this task's `run` method.
226 The spatial index in the butler registry has generous padding and often
227 supplies patches near, but not directly overlapping the desired region.
228 This method filters the inputs so that `run` does not have to read in
229 all possibly-matching coadd exposures.
233 coaddExposureHandles : `iterable` \
234 [`lsst.daf.butler.DeferredDatasetHandle` of \
235 `lsst.afw.image.Exposure`]
236 Dataset handles to exposures that might overlap the desired
238 bbox : `lsst.geom.Box2I`
239 Template bounding box of the pixel geometry onto which the
240 coaddExposures will be resampled.
241 skymap : `lsst.skymap.SkyMap`
242 Geometry of the tracts and patches the coadds are defined on.
243 wcs : `lsst.afw.geom.SkyWcs`
244 Template WCS onto which the coadds will be resampled.
248 result : `lsst.pipe.base.Struct`
249 A struct with attributes:
252 Dict of coadd exposures that overlap the projected bbox,
254 (`dict` [`int`, `list` [`lsst.daf.butler.DeferredDatasetHandle` of
255 `lsst.afw.image.Exposure`] ]).
257 Dict of data IDs of the coadd exposures that overlap the
258 projected bbox, indexed on tract id
259 (`dict` [`int`, `list [`lsst.daf.butler.DataCoordinate`] ]).
264 Raised if no patches overlap the input detector bbox, or the input
268 raise pipeBase.NoWorkFound(
269 "WCS is None; cannot find overlapping exposures."
274 detectorCorners = wcs.pixelToSky(detectorPolygon.getCorners())
276 coaddExposures = collections.defaultdict(list)
277 dataIds = collections.defaultdict(list)
279 for coaddRef
in coaddExposureHandles:
280 dataId = coaddRef.dataId
281 patchWcs = skymap[dataId[
"tract"]].getWcs()
282 patchBBox = skymap[dataId[
"tract"]][dataId[
"patch"]].getOuterBBox()
288 detectorInPatchCoordinates = afwGeom.Polygon(patchWcs.skyToPixel(detectorCorners))
289 if patchPolygon.intersection(detectorInPatchCoordinates):
290 overlappingArea += patchPolygon.intersectionSingle(
291 detectorInPatchCoordinates
294 "Using template input tract=%s, patch=%s",
298 coaddExposures[dataId[
"tract"]].append(coaddRef)
299 dataIds[dataId[
"tract"]].append(dataId)
301 if not overlappingArea:
302 raise pipeBase.NoWorkFound(
"No patches overlap detector")
304 return pipeBase.Struct(coaddExposures=coaddExposures, dataIds=dataIds)
307 def run(self, *, coaddExposureHandles, bbox, wcs, dataIds, physical_filter, visit=None):
308 """Warp coadds from multiple tracts and patches to form a template to
309 subtract from a science image.
311 Tract and patch overlap regions are combined by a variance-weighted
312 average, and the variance planes are combined with the same weights,
313 not added in quadrature; the overlap regions are not statistically
314 independent, because they're derived from the same original data.
315 The PSF on the template is created by combining the CoaddPsf on each
316 template image into a meta-CoaddPsf.
320 coaddExposureHandles : `dict` [`int`, `list` of \
321 [`lsst.daf.butler.DeferredDatasetHandle` of \
322 `lsst.afw.image.Exposure`]]
323 Coadds to be mosaicked, indexed on tract id.
324 bbox : `lsst.geom.Box2I`
325 Template Bounding box of the detector geometry onto which to
326 resample the ``coaddExposureHandles``. Modified in-place to include the
328 wcs : `lsst.afw.geom.SkyWcs`
329 Template WCS onto which to resample the ``coaddExposureHandles``.
330 dataIds : `dict` [`int`, `list` [`lsst.daf.butler.DataCoordinate`]]
331 Record of the tract and patch of each coaddExposure, indexed on
333 physical_filter : `str`
334 Physical filter of the science image.
335 visit : `int`, optional
336 If supplied, over-write the visit ID in the template's visitInfo
337 so that downstream source injection tasks can link the template and
338 science image for the visit.
342 result : `lsst.pipe.base.Struct`
343 A struct with attributes:
346 A template coadd exposure assembled out of patches
347 (`lsst.afw.image.ExposureF`).
352 If no coadds are found with sufficient un-masked pixels.
354 band, photoCalib = self._checkInputs(dataIds, coaddExposureHandles)
356 bbox.grow(self.config.templateBorderSize)
360 for tract
in coaddExposureHandles:
361 maskedImages, catalog, totalBox = self._makeExposureCatalog(
362 coaddExposureHandles[tract], dataIds[tract]
364 warpedBox = computeWarpedBBox(catalog[0].wcs, bbox, wcs)
367 unwarped, count, included = self._merge(
368 maskedImages, warpedBox, catalog[0].wcs
374 "No valid pixels from coadd patches in tract %s; not including in output.",
378 warpedBox.clip(totalBox)
379 potentialInput = self.warper.warpExposure(
380 wcs, unwarped.subset(warpedBox), destBBox=bbox
386 potentialInput.mask.array
387 & potentialInput.mask.getPlaneBitMask(
"NO_DATA")
390 "No overlap from coadd patches in tract %s; not including in output.",
396 tempCatalog = afwTable.ExposureCatalog(self.schema)
397 tempCatalog.reserve(len(included))
399 tempCatalog.append(catalog[i])
400 catalogs.append(tempCatalog)
401 warped[tract] = potentialInput.maskedImage
404 raise pipeBase.NoWorkFound(
"No patches found to overlap science exposure.")
406 template, count, _ = self._merge(warped, bbox, wcs)
408 raise pipeBase.NoWorkFound(
"No valid pixels in warped template.")
410 if self.config.doScaleVariance:
414 varianceFactor = self.scaleVariance.
run(template.maskedImage)
415 self.log.info(
"Template variance scaling factor: %.2f", varianceFactor)
416 self.metadata[
"scaleTemplateVarianceFactor"] = varianceFactor
419 catalog = afwTable.ExposureCatalog(self.schema)
420 catalog.reserve(sum([len(c)
for c
in catalogs]))
425 self.checkHighVariance(template)
426 if visit
is not None:
427 template.getInfo().setVisitInfo(
VisitInfo(id=visit))
429 template.setPhotoCalib(photoCalib)
430 template.setPsf(self._makePsf(template, catalog, wcs))
434 coaddInputs.ccds.extend(catalog, deep=
True)
435 template.getInfo().setCoaddInputs(coaddInputs)
436 return pipeBase.Struct(template=template)
439 """Set a mask plane for regions with unusually high variance.
443 template : `lsst.afw.image.Exposure`
444 The warped template exposure, which will be modified in place.
446 highVarianceMaskPlaneBit = template.mask.addMaskPlane(
"HIGH_VARIANCE")
447 ignoredPixelBits = template.mask.getPlaneBitMask(self.varianceBackground.config.ignoredPixelMask)
448 goodMask = (template.mask.array & ignoredPixelBits) == 0
449 goodFraction = np.count_nonzero(goodMask)/template.mask.array.size
450 if goodFraction < self.config.highVarianceMaskFraction:
451 self.log.info(
"Not setting HIGH_VARIANCE mask plane, only %2.1f%% of"
452 " pixels were unmasked for background estimation, but"
453 " %2.1f%% are required", 100*goodFraction, 100*self.config.highVarianceMaskFraction)
455 varianceExposure = template.clone()
456 varianceExposure.image.array = varianceExposure.variance.array
457 varianceBackground = self.varianceBackground.
run(varianceExposure).background.getImage().array
458 threshold = self.config.highVarianceThreshold*np.nanmedian(varianceBackground)
459 highVariancePix = varianceBackground > threshold
460 template.mask.array[highVariancePix] |= 2**highVarianceMaskPlaneBit
463 def _checkInputs(dataIds, coaddExposures):
464 """Check that the all the dataIds are from the same band and that
465 the exposures all have the same photometric calibration.
469 dataIds : `dict` [`int`, `list` [`lsst.daf.butler.DataCoordinate`]]
470 Record of the tract and patch of each coaddExposure.
471 coaddExposures : `dict` [`int`, `list` of \
472 [`lsst.daf.butler.DeferredDatasetHandle` of \
473 `lsst.afw.image.Exposure` or
474 `lsst.afw.image.Exposure`]]
475 Coadds to be mosaicked.
480 Filter band of all the input exposures.
481 photoCalib : `lsst.afw.image.PhotoCalib`
482 Photometric calibration of all of the input exposures.
487 Raised if the bands or calibrations of the input exposures are not
490 bands = set(dataId[
"band"]
for tract
in dataIds
for dataId
in dataIds[tract])
492 raise RuntimeError(f
"GetTemplateTask called with multiple bands: {bands}")
495 exposure.get(component=
"photoCalib")
496 for exposures
in coaddExposures.values()
497 for exposure
in exposures
499 if not all([photoCalibs[0] == x
for x
in photoCalibs]):
500 msg = f
"GetTemplateTask called with exposures with different photoCalibs: {photoCalibs}"
501 raise RuntimeError(msg)
502 photoCalib = photoCalibs[0]
503 return band, photoCalib
506 """Make an exposure catalog for one tract.
510 exposureRefs : `list` of [`lsst.daf.butler.DeferredDatasetHandle` of \
511 `lsst.afw.image.Exposure`]
512 Exposures to include in the catalog.
513 dataIds : `list` [`lsst.daf.butler.DataCoordinate`]
514 Data ids of each of the included exposures; must have "tract" and
519 images : `dict` [`lsst.afw.image.MaskedImage`]
520 MaskedImages of each of the input exposures, for warping.
521 catalog : `lsst.afw.table.ExposureCatalog`
522 Catalog of metadata for each exposure
523 totalBox : `lsst.geom.Box2I`
524 The union of the bounding boxes of all the input exposures.
526 catalog = afwTable.ExposureCatalog(self.schema)
527 catalog.reserve(len(exposureRefs))
528 exposures = (exposureRef.get()
for exposureRef
in exposureRefs)
532 for coadd, dataId
in zip(exposures, dataIds):
533 images[dataId] = coadd.maskedImage
534 bbox = coadd.getBBox()
535 totalBox = totalBox.expandedTo(bbox)
536 record = catalog.addNew()
537 record.setPsf(coadd.psf)
538 record.setWcs(coadd.wcs)
539 record.setPhotoCalib(coadd.photoCalib)
541 record.setValidPolygon(afwGeom.Polygon(
geom.Box2D(bbox).getCorners()))
542 record.set(
"tract", dataId[
"tract"])
543 record.set(
"patch", dataId[
"patch"])
546 record.set(
"weight", 1)
548 return images, catalog, totalBox
550 def _merge(self, maskedImages, bbox, wcs):
551 """Merge the images that came from one tract into one larger image,
552 ignoring NaN pixels and non-finite variance pixels from individual
557 maskedImages : `dict` [`lsst.afw.image.MaskedImage` or
558 `lsst.afw.image.Exposure`]
559 Images to be merged into one larger bounding box.
560 bbox : `lsst.geom.Box2I`
561 Bounding box defining the image to merge into.
562 wcs : `lsst.afw.geom.SkyWcs`
563 WCS of all of the input images to set on the output image.
567 merged : `lsst.afw.image.MaskedImage`
568 Merged image with all of the inputs at their respective bbox
571 Count of the number of good pixels (those with positive weights)
573 included : `list` [`int`]
574 List of indexes of patches that were included in the merged
575 result, to be used to trim the exposure catalog.
577 merged = afwImage.ExposureF(bbox, wcs)
578 weights = afwImage.ImageF(bbox)
580 for i, (dataId, maskedImage)
in enumerate(maskedImages.items()):
582 clippedBox =
geom.Box2I(maskedImage.getBBox())
583 clippedBox.clip(bbox)
584 if clippedBox.area == 0:
585 self.log.debug(
"%s does not overlap template region.", dataId)
587 maskedImage = maskedImage.subset(clippedBox)
589 good = (maskedImage.variance.array > 0) & (
590 np.isfinite(maskedImage.variance.array)
592 weight = maskedImage.variance.array[good] ** (-0.5)
593 bad = np.isnan(maskedImage.image.array) | ~good
596 maskedImage.image.array[bad] = 0.0
597 maskedImage.variance.array[bad] = 0.0
599 maskedImage.mask.array[bad] = 0
603 maskedImage.image.array[good] *= weight
604 maskedImage.variance.array[good] *= weight
605 weights[clippedBox].array[good] += weight
608 merged.maskedImage[clippedBox] += maskedImage
611 good = weights.array > 0
616 weights = weights.array[good]
617 merged.image.array[good] /= weights
618 merged.variance.array[good] /= weights
620 merged.mask.array[~good] |= merged.mask.getPlaneBitMask(
"NO_DATA")
622 return merged, good.sum(), included
625 """Return a PSF containing the PSF at each of the input regions.
627 Note that although this includes all the exposures from the catalog,
628 the PSF knows which part of the template the inputs came from, so when
629 evaluated at a given position it will not include inputs that never
630 went in to those pixels.
634 template : `lsst.afw.image.Exposure`
635 Generated template the PSF is for.
636 catalog : `lsst.afw.table.ExposureCatalog`
637 Catalog of exposures that went into the template that contains all
639 wcs : `lsst.afw.geom.SkyWcs`
640 WCS of the template, to warp the PSFs to.
644 coaddPsf : `lsst.meas.algorithms.CoaddPsf`
645 The meta-psf constructed from all of the input catalogs.
649 boolmask = template.mask.array & template.mask.getPlaneBitMask(
"NO_DATA") == 0
651 centerCoord = afwGeom.SpanSet.fromMask(maskx, 1).computeCentroid()
653 ctrl = self.config.coaddPsf.makeControl()
655 catalog, wcs, centerCoord, ctrl.warpingKernelName, ctrl.cacheSize
661 GetTemplateConnections,
662 dimensions=(
"instrument",
"visit",
"detector"),
663 defaultTemplates={
"coaddName":
"dcr",
"warpTypeSuffix":
"",
"fakesType":
""},
665 visitInfo = pipeBase.connectionTypes.Input(
666 doc=
"VisitInfo of calexp used to determine observing conditions.",
667 name=
"{fakesType}calexp.visitInfo",
668 storageClass=
"VisitInfo",
669 dimensions=(
"instrument",
"visit",
"detector"),
671 dcrCoadds = pipeBase.connectionTypes.Input(
672 doc=
"Input DCR template to match and subtract from the exposure",
673 name=
"{fakesType}dcrCoadd{warpTypeSuffix}",
674 storageClass=
"ExposureF",
675 dimensions=(
"tract",
"patch",
"skymap",
"band",
"subfilter"),
680 def __init__(self, *, config=None):
681 super().__init__(config=config)
682 self.inputs.remove(
"coaddExposures")
685class GetDcrTemplateConfig(
686 GetTemplateConfig, pipelineConnections=GetDcrTemplateConnections
688 numSubfilters = pexConfig.Field(
689 doc=
"Number of subfilters in the DcrCoadd.",
693 effectiveWavelength = pexConfig.Field(
694 doc=
"Effective wavelength of the filter in nm.",
698 bandwidth = pexConfig.Field(
699 doc=
"Bandwidth of the physical filter.",
705 if self.effectiveWavelength
is None or self.bandwidth
is None:
707 "The effective wavelength and bandwidth of the physical filter "
708 "must be set in the getTemplate config for DCR coadds. "
709 "Required until transmission curves are used in DM-13668."
713class GetDcrTemplateTask(GetTemplateTask):
714 ConfigClass = GetDcrTemplateConfig
715 _DefaultName =
"getDcrTemplate"
717 def runQuantum(self, butlerQC, inputRefs, outputRefs):
718 inputs = butlerQC.get(inputRefs)
719 bbox = inputs.pop(
"bbox")
720 wcs = inputs.pop(
"wcs")
721 dcrCoaddExposureHandles = inputs.pop(
"dcrCoadds")
722 skymap = inputs.pop(
"skyMap")
723 visitInfo = inputs.pop(
"visitInfo")
726 assert not inputs,
"runQuantum got more inputs than expected"
728 results = self.getExposures(
729 dcrCoaddExposureHandles, bbox, skymap, wcs, visitInfo
731 physical_filter = butlerQC.quantum.dataId[
"physical_filter"]
733 coaddExposureHandles=results.coaddExposures,
736 dataIds=results.dataIds,
737 physical_filter=physical_filter,
739 butlerQC.put(outputs, outputRefs)
741 def getExposures(self, dcrCoaddExposureHandles, bbox, skymap, wcs, visitInfo):
742 """Return lists of coadds and their corresponding dataIds that overlap
745 The spatial index in the registry has generous padding and often
746 supplies patches near, but not directly overlapping the detector.
747 Filters inputs so that we don't have to read in all input coadds.
751 dcrCoaddExposureHandles : `list` \
752 [`lsst.daf.butler.DeferredDatasetHandle` of \
753 `lsst.afw.image.Exposure`]
754 Data references to exposures that might overlap the detector.
755 bbox : `lsst.geom.Box2I`
756 Template Bounding box of the detector geometry onto which to
757 resample the coaddExposures.
758 skymap : `lsst.skymap.SkyMap`
759 Input definition of geometry/bbox and projection/wcs for
761 wcs : `lsst.afw.geom.SkyWcs`
762 Template WCS onto which to resample the coaddExposures.
763 visitInfo : `lsst.afw.image.VisitInfo`
764 Metadata for the science image.
768 result : `lsst.pipe.base.Struct`
769 A struct with attibutes:
772 Dict of coadd exposures that overlap the projected bbox,
774 (`dict` [`int`, `list` [`lsst.afw.image.Exposure`] ]).
776 Dict of data IDs of the coadd exposures that overlap the
777 projected bbox, indexed on tract id
778 (`dict` [`int`, `list [`lsst.daf.butler.DataCoordinate`] ]).
783 Raised if no patches overlatp the input detector bbox.
788 raise pipeBase.NoWorkFound(
"Exposure has no WCS; cannot create a template.")
792 dataIds = collections.defaultdict(list)
794 for coaddRef
in dcrCoaddExposureHandles:
795 dataId = coaddRef.dataId
796 subfilter = dataId[
"subfilter"]
797 patchWcs = skymap[dataId[
"tract"]].getWcs()
798 patchBBox = skymap[dataId[
"tract"]][dataId[
"patch"]].getOuterBBox()
799 patchCorners = patchWcs.pixelToSky(
geom.Box2D(patchBBox).getCorners())
800 patchPolygon = afwGeom.Polygon(wcs.skyToPixel(patchCorners))
801 if patchPolygon.intersection(detectorPolygon):
802 overlappingArea += patchPolygon.intersectionSingle(
806 "Using template input tract=%s, patch=%s, subfilter=%s"
807 % (dataId[
"tract"], dataId[
"patch"], dataId[
"subfilter"])
809 if dataId[
"tract"]
in patchList:
810 patchList[dataId[
"tract"]].append(dataId[
"patch"])
812 patchList[dataId[
"tract"]] = [
816 dataIds[dataId[
"tract"]].append(dataId)
818 if not overlappingArea:
819 raise pipeBase.NoWorkFound(
"No patches overlap detector")
821 self.checkPatchList(patchList)
823 coaddExposures = self.getDcrModel(patchList, dcrCoaddExposureHandles, visitInfo)
824 return pipeBase.Struct(coaddExposures=coaddExposures, dataIds=dataIds)
827 """Check that all of the DcrModel subfilters are present for each
833 Dict of the patches containing valid data for each tract.
838 If the number of exposures found for a patch does not match the
839 number of subfilters.
841 for tract
in patchList:
842 for patch
in set(patchList[tract]):
843 if patchList[tract].count(patch) != self.config.numSubfilters:
845 "Invalid number of DcrModel subfilters found: %d vs %d expected",
846 patchList[tract].count(patch),
847 self.config.numSubfilters,
851 """Build DCR-matched coadds from a list of exposure references.
856 Dict of the patches containing valid data for each tract.
857 coaddRefs : `list` [`lsst.daf.butler.DeferredDatasetHandle`]
858 Data references to `~lsst.afw.image.Exposure` representing
859 DcrModels that overlap the detector.
860 visitInfo : `lsst.afw.image.VisitInfo`
861 Metadata for the science image.
865 coaddExposures : `list` [`lsst.afw.image.Exposure`]
866 Coadd exposures that overlap the detector.
868 coaddExposures = collections.defaultdict(list)
869 for tract
in patchList:
870 for patch
in set(patchList[tract]):
873 for coaddRef
in coaddRefs
877 dcrModel = DcrModel.fromQuantum(
879 self.config.effectiveWavelength,
880 self.config.bandwidth,
881 self.config.numSubfilters,
883 coaddExposures[tract].append(dcrModel.buildMatchedExposureHandle(visitInfo=visitInfo))
884 return coaddExposures
888 condition = (coaddRef.dataId[
"tract"] == tract) & (
889 coaddRef.dataId[
"patch"] == patch
run(self, *, coaddExposureHandles, bbox, wcs, dataIds, physical_filter, visit=None)
checkHighVariance(self, template)
_makePsf(self, template, catalog, wcs)
_makeExposureCatalog(self, exposureRefs, dataIds)
getDcrModel(self, patchList, coaddRefs, visitInfo)
_merge(self, maskedImages, bbox, wcs)
checkPatchList(self, patchList)
_selectDataRef(coaddRef, tract, patch)