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."""
62_UNKNOWN_INSTRUMENT =
"UNKNOWN"
63"""Instrument recorded for a coadd or template made from observations of an
69 pipeBase.PipelineTaskConnections,
70 dimensions=(
"instrument",
"visit",
"detector"),
71 defaultTemplates={
"coaddName":
"goodSeeing",
"warpTypeSuffix":
"",
"fakesType":
""},
73 bbox = pipeBase.connectionTypes.Input(
74 doc=
"Bounding box of exposure to determine the geometry of the output template.",
75 name=
"{fakesType}calexp.bbox",
77 dimensions=(
"instrument",
"visit",
"detector"),
78 deprecated=
"Replaced by the `detector` connection. Will be removed after v31.",
80 detector = pipeBase.connectionTypes.Input(
81 doc=
"Detector of the exposure that we will construct the template for."
82 " Its bounding box sets the geometry of the output template.",
83 name=
"{fakesType}calexp.detector",
84 storageClass=
"Detector",
85 dimensions=(
"instrument",
"visit",
"detector"),
87 obs_info = pipeBase.connectionTypes.Input(
88 doc=
"Observation metadata of the exposure the template is built for."
89 " Only read with output_image_type='future', which needs this to set"
90 " the obs_info component of a lsst.images.VisitImage.",
91 name=
"{fakesType}calexp.obs_info",
92 storageClass=
"ObservationInfo",
93 dimensions=(
"instrument",
"visit",
"detector"),
95 wcs = pipeBase.connectionTypes.Input(
96 doc=
"WCS of the exposure that we will construct the template for.",
97 name=
"{fakesType}calexp.wcs",
99 dimensions=(
"instrument",
"visit",
"detector"),
101 skyMap = pipeBase.connectionTypes.Input(
102 doc=
"Geometry of the tracts and patches that the coadds are defined on.",
103 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
104 dimensions=(
"skymap",),
105 storageClass=
"SkyMap",
107 coaddExposures = pipeBase.connectionTypes.Input(
108 doc=
"Coadds that may overlap the desired region, as possible inputs to the template."
109 " Will be restricted to those that directly overlap the projected bounding box.",
110 dimensions=(
"tract",
"patch",
"skymap",
"band"),
111 storageClass=
"ExposureF",
112 name=
"{fakesType}{coaddName}Coadd{warpTypeSuffix}",
115 deferGraphConstraint=
True,
118 template = pipeBase.connectionTypes.Output(
119 doc=
"Warped template, pixel matched to the bounding box and WCS.",
120 dimensions=(
"instrument",
"visit",
"detector"),
121 storageClass=
"ExposureF",
122 name=
"{fakesType}{coaddName}Diff_templateExp{warpTypeSuffix}",
125 def __init__(self, *, config=None):
126 super().__init__(config=config)
129 if config.requireCoaddAtGraphBuild:
130 self.coaddExposures = dataclasses.replace(
132 deferGraphConstraint=
False,
134 if config.output_image_type ==
"future":
135 self.template = dataclasses.replace(self.template, storageClass=
"DifferenceImage")
140class GetTemplateConfig(
141 pipeBase.PipelineTaskConfig, pipelineConnections=GetTemplateConnections
143 templateBorderSize = pexConfig.Field(
146 doc=
"Number of pixels to grow the requested template image to account for warping",
148 warp = pexConfig.ConfigField(
149 dtype=afwMath.Warper.ConfigClass,
150 doc=
"warper configuration",
152 coaddPsf = pexConfig.ConfigField(
153 doc=
"Configuration for CoaddPsf",
154 dtype=CoaddPsfConfig,
156 fwhmExposureGrid = pexConfig.Field(
157 doc=
"Grid size to compute the average PSF FWHM in an exposure (pixels)",
161 fwhmExposureBuffer = pexConfig.Field(
162 doc=
"Fractional buffer margin to be left out of all sides of the image during construction"
163 " of grid to compute average PSF FWHM in an exposure",
167 varianceBackground = pexConfig.ConfigurableField(
168 target=SubtractBackgroundTask,
169 doc=
"Task to estimate the background variance.",
171 highVarianceThreshold = pexConfig.RangeField(
175 doc=
"Set the HIGH_VARIANCE mask plane for regions with variance"
176 " greater than the median by this factor.",
178 highVarianceMaskFraction = pexConfig.Field(
181 doc=
"Minimum fraction of unmasked pixels needed to set the"
182 " HIGH_VARIANCE mask plane.",
184 doScaleVariance = pexConfig.Field(
187 doc=
"Scale variance of the template image?"
189 scaleVariance = pexConfig.ConfigurableField(
190 target=ScaleVarianceTask,
191 doc=
"Subtask to rescale the variance of the template to the statistically expected level."
193 requireCoaddAtGraphBuild = pexConfig.Field(
196 doc=
"If True, include the coadd dataset existence in the"
197 " initial butler query during QuantumGraph generation.",
199 output_image_type = pexConfig.ChoiceField[str](
200 "Which image type to use for the output template.",
202 "legacy":
"Write as a lsst.afw.image.ExposureF.",
203 "future":
"Write as a lsst.images.DifferenceImage.",
208 raiseOnUndefinedMaskMap = pexConfig.Field(
211 doc=
"Raise if the template has pixels set in a mask plane that"
212 " `lsst.images` does not map? If False, log a warning and drop that"
213 " plane from the output instead."
214 " Only used with output_image_type='future'.",
220 self.warp.cacheSize = 100000
221 self.coaddPsf.cacheSize = self.warp.cacheSize
224 self.warp.interpLength = 100
225 self.warp.warpingKernelName =
"lanczos3"
226 self.coaddPsf.warpingKernelName = self.warp.warpingKernelName
229 self.varianceBackground.algorithm =
"LINEAR"
230 self.varianceBackground.binSize = 32
231 self.varianceBackground.useApprox =
False
232 self.varianceBackground.statisticsProperty =
"MEDIAN"
233 self.varianceBackground.doFilterSuperPixels =
True
234 self.varianceBackground.ignoredPixelMask = [
"BAD",
243 ConfigClass = GetTemplateConfig
244 _DefaultName =
"getTemplate"
248 if self.config.doScaleVariance:
249 self.makeSubtask(
"scaleVariance")
250 self.
warper = afwMath.Warper.fromConfig(self.config.warp)
251 self.
schema = afwTable.ExposureTable.makeMinimalSchema()
253 "tract", type=np.int32, doc=
"Which tract this exposure came from."
258 doc=
"Which patch in the tract this exposure came from.",
263 doc=
"Weight for each exposure, used to make the CoaddPsf; should always be 1.",
269 doc=
"Instrument whose observations were coadded to make the template, or UNKNOWN.",
271 self.makeSubtask(
"varianceBackground")
274 inputs = butlerQC.get(inputRefs)
275 detector = inputs.pop(
"detector")
276 bbox = detector.getBBox()
277 wcs = inputs.pop(
"wcs")
278 obsInfo = inputs.pop(
"obs_info",
None)
279 coaddExposures = inputs.pop(
"coaddExposures")
280 skymap = inputs.pop(
"skyMap")
283 assert not inputs,
"runQuantum got more inputs than expected"
285 results = self.
getExposures(coaddExposures, bbox, skymap, wcs)
286 physical_filter = butlerQC.quantum.dataId[
"physical_filter"]
288 coaddExposureHandles=results.coaddExposures,
291 dataIds=results.dataIds,
292 physical_filter=physical_filter,
293 visit=outputRefs.template.dataId[
"visit"],
294 instrument=butlerQC.quantum.dataId[
"instrument"],
296 if self.config.output_image_type ==
"future":
297 dataId = butlerQC.quantum.dataId
298 exposureRecord = record_from_obs_info(obsInfo, dataId[
"instrument"], dataId[
"visit"],
301 inputRefs.coaddExposures,
303 exposureRecord=exposureRecord)
304 butlerQC.put(outputs, outputRefs)
307 """Convert a template image to DifferenceImage format.
309 This replaces ``template`` with an `lsst.images.DifferenceImage`
310 instance and records the coadds contributed to it, along with the
311 instrument they were made from. The Gaussian PSF set by `run` is
312 converted to an `lsst.images.psfs.GaussianPointSpreadFunction`.
316 template : `lsst.afw.image.Exposure`
317 Template to convert. Its detector is replaced with ``detector``
318 and its mask is modified in place.
319 coadd_refs : `list` [`lsst.daf.butler.DatasetRef`]
320 References to the coadds that may have gone into the template.
321 May be a superset of the coadds that were used; supplies the
322 dataset id and RUN collection recorded for each coadd that was.
323 detector : `lsst.afw.cameraGeom.Detector`
324 Detector of the science image that the template was built for.
325 exposureRecord : `lsst.daf.butler.DimensionRecord`
326 The ``exposure`` record of the observation the template was built
327 for, which supplies the observation metadata recorded on the
332 convertedTemplate : `lsst.images.DifferenceImage`
338 Raised if the template has pixels set in a mask plane that
339 `lsst.images.get_legacy_template_mask_planes` does not map and
340 ``config.raiseOnUndefinedMaskMap`` is True, or if ``coadd_refs``
341 spans more than one skymap.
345 catalog = template.getInfo().getCoaddInputs().ccds
346 coaddPsf = self.
_makePsf(template, catalog, template.getWcs())
348 instrument = catalog[0][
"instrument"]
349 template.setDetector(detector)
350 skymaps = {ref.dataId[
"skymap"]
for ref
in coadd_refs}
351 if len(skymaps) != 1:
352 raise RuntimeError(
"Expected the template inputs to come from one skymap;"
353 f
" got {sorted(skymaps)}.")
354 butler_info = {(ref.dataId[
"tract"], ref.dataId[
"patch"]): (ref.id, ref.run)
355 for ref
in coadd_refs}
356 planeMap = get_legacy_template_mask_planes()
357 if not self.config.raiseOnUndefinedMaskMap:
359 convertedTemplate = DifferenceImage.from_legacy(
361 exposure_record=exposureRecord,
362 unit=astropy.units.nJy,
366 convertedTemplate.templates = DifferenceImageTemplateInfo.from_legacy_psf(
367 convertedTemplate.sky_projection.pixel_frame,
372 instrument=instrument,
374 return convertedTemplate
377 """Clear the pixels of every mask plane that conversion would reject.
381 mask : `lsst.afw.image.Mask`
382 Template mask to modify in place.
383 planeMap : `dict` [`str`, `lsst.images.MaskPlane`]
384 Plane map the template will be converted with. The optional
385 planes of `lsst.images.get_legacy_optional_mask_planes` are kept
386 as well, since conversion adds them when they have pixels set.
388 keep = planeMap.keys() | get_legacy_optional_mask_planes().keys()
389 for name
in mask.getMaskPlaneDict():
392 bitmask = mask.getPlaneBitMask(name)
393 nSet = np.count_nonzero(mask.array & bitmask)
395 self.log.warning(
"Dropping mask plane %s from the template: %d pixels are set, but"
396 " the template plane map does not include it.", name, nSet)
397 mask.array &= ~bitmask
400 """Return a data structure containing the coadds that overlap the
401 specified bbox projected onto the sky, and a corresponding data
402 structure of their dataIds.
403 These are the appropriate inputs to this task's `run` method.
405 The spatial index in the butler registry has generous padding and often
406 supplies patches near, but not directly overlapping the desired region.
407 This method filters the inputs so that `run` does not have to read in
408 all possibly-matching coadd exposures.
412 coaddExposureHandles : `iterable` \
413 [`lsst.daf.butler.DeferredDatasetHandle` of \
414 `lsst.afw.image.Exposure`]
415 Dataset handles to exposures that might overlap the desired
417 bbox : `lsst.geom.Box2I`
418 Template bounding box of the pixel geometry onto which the
419 coaddExposures will be resampled.
420 skymap : `lsst.skymap.SkyMap`
421 Geometry of the tracts and patches the coadds are defined on.
422 wcs : `lsst.afw.geom.SkyWcs`
423 Template WCS onto which the coadds will be resampled.
427 result : `lsst.pipe.base.Struct`
428 A struct with attributes:
431 Dict of coadd exposures that overlap the projected bbox,
433 (`dict` [`int`, `list` [`lsst.daf.butler.DeferredDatasetHandle` of
434 `lsst.afw.image.Exposure`] ]).
436 Dict of data IDs of the coadd exposures that overlap the
437 projected bbox, indexed on tract id
438 (`dict` [`int`, `list [`lsst.daf.butler.DataCoordinate`] ]).
443 Raised if no patches overlap the input detector bbox, or the input
447 raise pipeBase.NoWorkFound(
448 "WCS is None; cannot find overlapping exposures."
453 detectorCorners = wcs.pixelToSky(detectorPolygon.getCorners())
455 coaddExposures = collections.defaultdict(list)
456 dataIds = collections.defaultdict(list)
458 for coaddRef
in coaddExposureHandles:
459 dataId = coaddRef.dataId
460 patchWcs = skymap[dataId[
"tract"]].getWcs()
461 patchBBox = skymap[dataId[
"tract"]][dataId[
"patch"]].getOuterBBox()
462 patchPolygon = afwGeom.Polygon(
geom.Box2D(patchBBox))
467 detectorInPatchCoordinates = afwGeom.Polygon(patchWcs.skyToPixel(detectorCorners))
468 if patchPolygon.intersection(detectorInPatchCoordinates):
469 overlappingArea += patchPolygon.intersectionSingle(
470 detectorInPatchCoordinates
473 "Using template input tract=%s, patch=%s",
477 coaddExposures[dataId[
"tract"]].append(coaddRef)
478 dataIds[dataId[
"tract"]].append(dataId)
480 if not overlappingArea:
481 raise pipeBase.NoWorkFound(
"No patches overlap detector")
483 return pipeBase.Struct(coaddExposures=coaddExposures, dataIds=dataIds)
486 def run(self, *, coaddExposureHandles, bbox, wcs, dataIds, physical_filter, visit=None, instrument=None):
487 """Warp coadds from multiple tracts and patches to form a template to
488 subtract from a science image.
490 Tract and patch overlap regions are combined by a variance-weighted
491 average, and the variance planes are combined with the same weights,
492 not added in quadrature; the overlap regions are not statistically
493 independent, because they're derived from the same original data.
494 The PSF of the template is a Gaussian with the same width as the
495 CoaddPsf evaluated at the center of the detector.
497 The template is built from coadds of one instrument: the science
498 image's ``instrument`` if any coadd was made from it, and otherwise the
499 one that most of the coadds were made from. Coadds made from another
500 instrument are dropped, and so are coadds that record no instrument
501 unless none of the coadds records one.
505 coaddExposureHandles : `dict` [`int`, `list` of \
506 [`lsst.daf.butler.DeferredDatasetHandle` of \
507 `lsst.afw.image.Exposure`]]
508 Coadds to be mosaicked, indexed on tract id.
509 bbox : `lsst.geom.Box2I`
510 Input Bounding box of the detector geometry onto which to
511 resample the ``coaddExposureHandles``. The output template bbox
512 will be grown by ``templateBorderSize``.
513 wcs : `lsst.afw.geom.SkyWcs`
514 Template WCS onto which to resample the ``coaddExposureHandles``.
515 dataIds : `dict` [`int`, `list` [`lsst.daf.butler.DataCoordinate`]]
516 Record of the tract and patch of each coaddExposure, indexed on
518 physical_filter : `str`
519 Physical filter of the science image.
520 visit : `int`, optional
521 If supplied, over-write the visit ID in the template's visitInfo
522 so that downstream source injection tasks can link the template and
523 science image for the visit.
524 instrument : `str`, optional
525 Instrument of the science image. Coadds made from it are used in
526 preference to coadds from any other instrument.
530 result : `lsst.pipe.base.Struct`
531 A struct with attributes:
534 A template coadd exposure assembled out of patches, with
535 pixels in nJy. Its PSF is a Gaussian approximation to the
536 CoaddPsf of its inputs (`lsst.afw.image.ExposureF`).
541 If no coadds are found with sufficient un-masked pixels.
543 If the template is not known to be from the science image's
544 instrument and no coadd records the plate scale of its input
545 images, so the template variance cannot be corrected.
547 band, photoCalib, templateInstrument = self.
_checkInputs(dataIds, coaddExposureHandles, instrument)
550 bbox.grow(self.config.templateBorderSize)
554 plateScaleFactor = 1.0
if instrument == templateInstrument
else None
556 for tract
in coaddExposureHandles:
558 coaddExposureHandles[tract], dataIds[tract], templateInstrument
560 if len(catalog) == 0:
567 if plateScaleFactor
is None:
569 warpedBox = computeWarpedBBox(catalog[0].wcs, bbox, wcs)
572 unwarped, count, included = self.
_merge(
573 maskedImages, warpedBox, catalog[0].wcs
579 "No valid pixels from coadd patches in tract %s; not including in output.",
583 warpedBox.clip(totalBox)
584 potentialInput = self.
warper.warpExposure(
585 wcs, unwarped.subset(warpedBox), destBBox=bbox
590 potentialInput.mask.array
591 & potentialInput.mask.getPlaneBitMask(
"NO_DATA")
594 "No overlap from coadd patches in tract %s; not including in output.",
600 tempCatalog = afwTable.ExposureCatalog(self.
schema)
601 tempCatalog.reserve(len(included))
603 tempCatalog.append(catalog[i])
604 catalogs.append(tempCatalog)
605 warped[tract] = potentialInput.maskedImage
608 raise pipeBase.NoWorkFound(
"No patches found to overlap science exposure.")
611 template, count, _ = self.
_merge(warped, bbox, wcs)
613 raise pipeBase.NoWorkFound(
"No valid pixels in warped template.")
615 if self.config.doScaleVariance:
619 varianceFactor = self.scaleVariance.
run(template.maskedImage)
620 self.log.info(
"Template variance scaling factor: %.2f", varianceFactor)
621 self.metadata[
"scaleTemplateVarianceFactor"] = varianceFactor
626 catalog = afwTable.ExposureCatalog(self.
schema)
627 catalog.reserve(sum([len(c)
for c
in catalogs]))
633 if visit
is not None:
634 template.getInfo().setVisitInfo(
VisitInfo(id=visit))
637 if photoCalib
is not None and photoCalib != identity:
638 template.maskedImage = photoCalib.calibrateImage(template.maskedImage)
639 photoCalib = identity
640 template.setPhotoCalib(photoCalib)
641 template.setPsf(self.
_makePsf(template, catalog, wcs))
646 coaddInputs.ccds.extend(catalog, deep=
True)
647 template.getInfo().setCoaddInputs(coaddInputs)
648 return pipeBase.Struct(template=template)
651 """Correct the template variance plane for the effects of warping.
653 Apply a multiplicative correction factor to the template variance if
654 the plate scale of the coadd's constituent images is different than
655 the science image the template is being constructed for. This should
656 only be necessary if the coadd images were from a different instrument
657 than the science image. If the plate scale of the science instrument is
658 smaller than the plate scale of the coadd instrument, then the template
659 pixels will be correlated and the true variance will be higher than the
660 image pixel noise level would suggest.
664 template : `lsst.afw.image.Exposure`
665 Assembled template; its variance plane is modified in place.
666 plateScaleFactor : `float` or `None`
667 Correction for the change in pixel area, from `_plateScaleFactor`.
672 Raised if ``plateScaleFactor`` is `None`.
674 if plateScaleFactor
is None:
676 "Could not determine the plate scale of the images the template coadds were made from,"
677 " so the template variance cannot be corrected for the change in pixel area."
679 self.metadata[
"variancePlateScaleFactor"] = plateScaleFactor
680 if plateScaleFactor == 1.0:
682 template.variance.array *= plateScaleFactor
683 self.metadata[
"templateVarianceCorrectionFactor"] = plateScaleFactor
685 "Corrected the template variance plane for the change in pixel area by a factor of %.4f.",
690 """Compute the variance correction for the total change in pixel area
691 between the coadd's constituent images and the science image.
695 coaddInputs : `lsst.afw.image.CoaddInputs` or `None`
696 Input records of one of the coadd patches.
697 wcs : `lsst.afw.geom.SkyWcs`
698 WCS of the science image the template is being built for.
699 bbox : `lsst.geom.Box2I`
700 Bounding box of the template, used only to choose where to
701 evaluate the science image pixel scale.
705 factor : `float` or `None`
706 The correction factor equivalent to the ratio of the area of a pixel
707 from the coadd's instrument to the science instrument, or `None` if
708 this patch carries no input record from which it could be computed.
710 if coaddInputs
is None:
712 scienceScale = wcs.getPixelScale(
geom.Box2D(bbox).getCenter()).asArcseconds()
714 for record
in coaddInputs.ccds:
717 originalWcs = record.getWcs()
718 if originalWcs
is None:
720 center =
geom.Box2D(record.getBBox()).getCenter()
721 pviScale = originalWcs.getPixelScale(center).asArcseconds()
723 factor = (pviScale/scienceScale)**2
724 if np.isfinite(factor)
and factor > 0:
729 """Set a mask plane for regions with unusually high variance.
733 template : `lsst.afw.image.Exposure`
734 The warped template exposure, which will be modified in place.
736 highVarianceMaskPlaneBit = template.mask.addMaskPlane(
"HIGH_VARIANCE")
737 ignoredPixelBits = template.mask.getPlaneBitMask(self.varianceBackground.config.ignoredPixelMask)
738 goodMask = (template.mask.array & ignoredPixelBits) == 0
739 goodFraction = np.count_nonzero(goodMask)/template.mask.array.size
740 if goodFraction < self.config.highVarianceMaskFraction:
741 self.log.info(
"Not setting HIGH_VARIANCE mask plane, only %2.1f%% of"
742 " pixels were unmasked for background estimation, but"
743 " %2.1f%% are required", 100*goodFraction, 100*self.config.highVarianceMaskFraction)
745 varianceExposure = template.clone()
746 varianceExposure.image.array = varianceExposure.variance.array
747 varianceBackground = self.varianceBackground.
run(varianceExposure).background.getImage().array
748 threshold = self.config.highVarianceThreshold*np.nanmedian(varianceBackground)
749 highVariancePix = varianceBackground > threshold
750 template.mask.array[highVariancePix] |= 2**highVarianceMaskPlaneBit
754 """Check that the all the dataIds are from the same band and that
755 the exposures all have the same photometric calibration, and choose
756 the instrument of the template.
760 dataIds : `dict` [`int`, `list` [`lsst.daf.butler.DataCoordinate`]]
761 Record of the tract and patch of each coaddExposure.
762 coaddExposures : `dict` [`int`, `list` of \
763 [`lsst.daf.butler.DeferredDatasetHandle` of \
764 `lsst.afw.image.Exposure` or
765 `lsst.afw.image.Exposure`]]
766 Coadds to be mosaicked.
767 scienceInstrument : `str`, optional
768 Instrument of the science image.
773 Filter band of all the input exposures.
774 photoCalib : `lsst.afw.image.PhotoCalib`
775 Photometric calibration of all of the input exposures.
777 ``scienceInstrument`` if any coadd was made from it, otherwise the
778 instrument that the most coadds were made from, or ``"UNKNOWN"`` if
779 no coadd records one. A tie goes to the instrument whose name sorts
785 Raised if the bands or calibrations of the input exposures are not
788 bands = set(dataId[
"band"]
for tract
in dataIds
for dataId
in dataIds[tract])
790 raise RuntimeError(f
"GetTemplateTask called with multiple bands: {bands}")
793 instrumentCounts = collections.Counter()
794 for exposures
in coaddExposures.values():
795 for exposure
in exposures:
798 photoCalibs.append(exposure.get(component=
"photoCalib"))
799 instrumentCounts[GetTemplateTask._getInstrument(exposure.get(component=
"coaddInputs"))] += 1
800 if not all([photoCalibs[0] == x
for x
in photoCalibs]):
801 msg = f
"GetTemplateTask called with exposures with different photoCalibs: {photoCalibs}"
802 raise RuntimeError(msg)
803 photoCalib = photoCalibs[0]
805 del instrumentCounts[_UNKNOWN_INSTRUMENT]
806 if scienceInstrument
in instrumentCounts:
807 return band, photoCalib, scienceInstrument
808 if not instrumentCounts:
809 return band, photoCalib, _UNKNOWN_INSTRUMENT
810 instrument = max(sorted(instrumentCounts), key=instrumentCounts.get)
811 return band, photoCalib, instrument
815 """Return the instrument whose observations were coadded to make a
820 coaddInputs : `lsst.afw.image.CoaddInputs` or `None`
821 Input records of the coadd.
826 Instrument label of the coadd, or ``"UNKNOWN"`` if it records none.
830 Every input of a coadd comes from the same instrument, so the first
831 input record stands for all of them.
833 if coaddInputs
is None or len(coaddInputs.ccds) == 0:
834 return _UNKNOWN_INSTRUMENT
835 visitInfo = coaddInputs.ccds[0].getVisitInfo()
836 label = visitInfo.getInstrumentLabel()
if visitInfo
is not None else ""
837 return label
or _UNKNOWN_INSTRUMENT
840 """Make an exposure catalog for one tract.
844 exposureRefs : `list` of [`lsst.daf.butler.DeferredDatasetHandle` of \
845 `lsst.afw.image.Exposure`]
846 Exposures to include in the catalog.
847 dataIds : `list` [`lsst.daf.butler.DataCoordinate`]
848 Data ids of each of the included exposures; must have "tract" and
850 instrument : `str`, optional
851 Instrument of the template, recorded in the catalog. Exposures
852 made from another instrument are dropped, as are exposures that
853 record no instrument unless ``instrument`` is ``"UNKNOWN"``.
857 images : `dict` [`lsst.afw.image.MaskedImage`]
858 MaskedImages of each of the input exposures, for warping.
859 catalog : `lsst.afw.table.ExposureCatalog`
860 Catalog of metadata for each exposure
861 totalBox : `lsst.geom.Box2I`
862 The union of the bounding boxes of all the input exposures.
863 coaddInputs : `lsst.afw.image.CoaddInputs` or `None`
864 Input records of the first exposure kept, or `None` if none is
867 catalog = afwTable.ExposureCatalog(self.
schema)
868 catalog.reserve(len(exposureRefs))
869 exposures = (exposureRef.get()
for exposureRef
in exposureRefs)
872 firstCoaddInputs =
None
874 for coadd, dataId
in zip(exposures, dataIds):
875 coaddInputs = coadd.getInfo().getCoaddInputs()
877 if coaddInstrument != instrument:
879 "Dropping template coadd %s: it was made from %s, but the template is made from %s.",
886 firstCoaddInputs = coaddInputs
887 images[dataId] = coadd.maskedImage
888 bbox = coadd.getBBox()
889 totalBox = totalBox.expandedTo(bbox)
890 record = catalog.addNew()
891 record.setPsf(coadd.psf)
892 record.setWcs(coadd.wcs)
893 record.setPhotoCalib(coadd.photoCalib)
895 record.setValidPolygon(afwGeom.Polygon(
geom.Box2D(bbox).getCorners()))
896 record.set(
"tract", dataId[
"tract"])
897 record.set(
"patch", dataId[
"patch"])
898 record.set(
"instrument", instrument)
901 record.set(
"weight", 1)
903 return images, catalog, totalBox, firstCoaddInputs
905 def _merge(self, maskedImages, bbox, wcs):
906 """Merge the images that came from one tract into one larger image,
907 ignoring NaN pixels and non-finite variance pixels from individual
912 maskedImages : `dict` [`lsst.afw.image.MaskedImage` or
913 `lsst.afw.image.Exposure`]
914 Images to be merged into one larger bounding box.
915 bbox : `lsst.geom.Box2I`
916 Bounding box defining the image to merge into.
917 wcs : `lsst.afw.geom.SkyWcs`
918 WCS of all of the input images to set on the output image.
922 merged : `lsst.afw.image.MaskedImage`
923 Merged image with all of the inputs at their respective bbox
926 Count of the number of good pixels (those with positive weights)
928 included : `list` [`int`]
929 List of indexes of patches that were included in the merged
930 result, to be used to trim the exposure catalog.
932 merged = afwImage.ExposureF(bbox, wcs)
933 weights = afwImage.ImageF(bbox)
935 for i, (dataId, maskedImage)
in enumerate(maskedImages.items()):
937 clippedBox =
geom.Box2I(maskedImage.getBBox())
938 clippedBox.clip(bbox)
939 if clippedBox.area == 0:
940 self.log.debug(
"%s does not overlap template region.", dataId)
942 maskedImage = maskedImage.subset(clippedBox)
944 good = (maskedImage.variance.array > 0) & (
945 np.isfinite(maskedImage.variance.array)
947 weight = maskedImage.variance.array[good] ** (-0.5)
948 bad = np.isnan(maskedImage.image.array) | ~good
951 maskedImage.image.array[bad] = 0.0
952 maskedImage.variance.array[bad] = 0.0
954 maskedImage.mask.array[bad] = 0
958 maskedImage.image.array[good] *= weight
959 maskedImage.variance.array[good] *= weight
960 weights[clippedBox].array[good] += weight
963 merged.maskedImage[clippedBox] += maskedImage
966 good = weights.array > 0
971 weights = weights.array[good]
972 merged.image.array[good] /= weights
973 merged.variance.array[good] /= weights
975 merged.mask.array[~good] |= merged.mask.getPlaneBitMask(
"NO_DATA")
977 return merged, good.sum(), included
980 """Return a PSF containing the PSF at each of the input regions.
982 Note that although this includes all the exposures from the catalog,
983 the PSF knows which part of the template the inputs came from, so when
984 evaluated at a given position it will not include inputs that never
985 went in to those pixels.
989 template : `lsst.afw.image.Exposure`
990 Generated template the PSF is for.
991 catalog : `lsst.afw.table.ExposureCatalog`
992 Catalog of exposures that went into the template that contains all
994 wcs : `lsst.afw.geom.SkyWcs`
995 WCS of the template, to warp the PSFs to.
999 coaddPsf : `lsst.meas.algorithms.CoaddPsf`
1000 The meta-psf constructed from all of the input catalogs.
1004 boolmask = template.mask.array & template.mask.getPlaneBitMask(
"NO_DATA") == 0
1006 centerCoord = afwGeom.SpanSet.fromMask(maskx, 1).computeCentroid()
1008 ctrl = self.config.coaddPsf.makeControl()
1009 coaddPsf = CoaddPsf(
1010 catalog, wcs, centerCoord, ctrl.warpingKernelName, ctrl.cacheSize
1015 """Return a Gaussian PSF with the width of the template CoaddPsf.
1019 template : `lsst.afw.image.Exposure`
1020 Template with the CoaddPsf to approximate. The width is measured
1021 at the average position of the PSF, or averaged over a grid of
1022 positions if the PSF cannot be evaluated there.
1026 psf : `lsst.afw.detection.GaussianPsf`
1027 Gaussian with the width of the template PSF.
1032 Raised if the PSF cannot be evaluated at any grid position.
1037 fwhm = getPsfFwhm(template.psf)
1044 self.log.info(
"Unable to evaluate PSF at the average position. "
1045 "Evaluating PSF on a grid of points.")
1046 fwhm = evaluateMeanPsfFwhm(template,
1047 fwhmExposureBuffer=self.config.fwhmExposureBuffer,
1048 fwhmExposureGrid=self.config.fwhmExposureGrid)
1049 sigma = fwhm/gaussian_sigma_to_fwhm
1050 size = 2*int(np.ceil(_PSF_STAMP_SIGMAS*sigma)) + 1
1051 self.metadata[
"templatePsfSigma"] = float(sigma)
1052 self.log.info(
"Approximated the template PSF with a Gaussian of sigma %.3f pixels.", sigma)
1053 return afwDetection.GaussianPsf(size, size, sigma)
1057 GetTemplateConnections,
1058 dimensions=(
"instrument",
"visit",
"detector"),
1059 defaultTemplates={
"coaddName":
"dcr",
"warpTypeSuffix":
"",
"fakesType":
""},
1061 visitInfo = pipeBase.connectionTypes.Input(
1062 doc=
"VisitInfo of calexp used to determine observing conditions.",
1063 name=
"{fakesType}calexp.visitInfo",
1064 storageClass=
"VisitInfo",
1065 dimensions=(
"instrument",
"visit",
"detector"),
1067 dcrCoadds = pipeBase.connectionTypes.Input(
1068 doc=
"Input DCR template to match and subtract from the exposure",
1069 name=
"{fakesType}dcrCoadd{warpTypeSuffix}",
1070 storageClass=
"ExposureF",
1071 dimensions=(
"tract",
"patch",
"skymap",
"band",
"subfilter"),
1076 def __init__(self, *, config=None):
1077 super().__init__(config=config)
1078 self.inputs.remove(
"coaddExposures")
1081class GetDcrTemplateConfig(
1082 GetTemplateConfig, pipelineConnections=GetDcrTemplateConnections
1084 """Config for GetDcrTemplateTask.
1086 Only ``output_image_type="legacy"`` is supported: the task always writes
1087 an `lsst.afw.image.ExposureF`.
1090 numSubfilters = pexConfig.Field(
1091 doc=
"Number of subfilters in the DcrCoadd.",
1095 effectiveWavelength = pexConfig.Field(
1096 doc=
"Effective wavelength of the filter in nm.",
1100 bandwidth = pexConfig.Field(
1101 doc=
"Bandwidth of the physical filter.",
1107 if self.effectiveWavelength
is None or self.bandwidth
is None:
1109 "The effective wavelength and bandwidth of the physical filter "
1110 "must be set in the getTemplate config for DCR coadds. "
1111 "Required until transmission curves are used in DM-13668."
1116 ConfigClass = GetDcrTemplateConfig
1117 _DefaultName =
"getDcrTemplate"
1119 def runQuantum(self, butlerQC, inputRefs, outputRefs):
1120 inputs = butlerQC.get(inputRefs)
1121 detector = inputs.pop(
"detector")
1122 bbox = detector.getBBox()
1123 wcs = inputs.pop(
"wcs")
1124 dcrCoaddExposureHandles = inputs.pop(
"dcrCoadds")
1125 skymap = inputs.pop(
"skyMap")
1126 visitInfo = inputs.pop(
"visitInfo")
1129 inputs.pop(
"obs_info",
None)
1132 assert not inputs,
"runQuantum got more inputs than expected"
1134 results = self.getExposures(
1135 dcrCoaddExposureHandles, bbox, skymap, wcs, visitInfo
1137 physical_filter = butlerQC.quantum.dataId[
"physical_filter"]
1139 coaddExposureHandles=results.coaddExposures,
1142 dataIds=results.dataIds,
1143 physical_filter=physical_filter,
1144 visit=outputRefs.template.dataId[
"visit"],
1145 instrument=butlerQC.quantum.dataId[
"instrument"],
1147 butlerQC.put(outputs, outputRefs)
1149 def getExposures(self, dcrCoaddExposureHandles, bbox, skymap, wcs, visitInfo):
1150 """Return lists of coadds and their corresponding dataIds that overlap
1153 The spatial index in the registry has generous padding and often
1154 supplies patches near, but not directly overlapping the detector.
1155 Filters inputs so that we don't have to read in all input coadds.
1159 dcrCoaddExposureHandles : `list` \
1160 [`lsst.daf.butler.DeferredDatasetHandle` of \
1161 `lsst.afw.image.Exposure`]
1162 Data references to exposures that might overlap the detector.
1163 bbox : `lsst.geom.Box2I`
1164 Template Bounding box of the detector geometry onto which to
1165 resample the coaddExposures.
1166 skymap : `lsst.skymap.SkyMap`
1167 Input definition of geometry/bbox and projection/wcs for
1169 wcs : `lsst.afw.geom.SkyWcs`
1170 Template WCS onto which to resample the coaddExposures.
1171 visitInfo : `lsst.afw.image.VisitInfo`
1172 Metadata for the science image.
1176 result : `lsst.pipe.base.Struct`
1177 A struct with attibutes:
1180 Dict of coadd exposures that overlap the projected bbox,
1182 (`dict` [`int`, `list` [`lsst.afw.image.Exposure`] ]).
1184 Dict of data IDs of the coadd exposures that overlap the
1185 projected bbox, indexed on tract id
1186 (`dict` [`int`, `list [`lsst.daf.butler.DataCoordinate`] ]).
1190 pipeBase.NoWorkFound
1191 Raised if no patches overlatp the input detector bbox.
1196 raise pipeBase.NoWorkFound(
"Exposure has no WCS; cannot create a template.")
1200 dataIds = collections.defaultdict(list)
1202 for coaddRef
in dcrCoaddExposureHandles:
1203 dataId = coaddRef.dataId
1204 subfilter = dataId[
"subfilter"]
1205 patchWcs = skymap[dataId[
"tract"]].getWcs()
1206 patchBBox = skymap[dataId[
"tract"]][dataId[
"patch"]].getOuterBBox()
1207 patchCorners = patchWcs.pixelToSky(
geom.Box2D(patchBBox).getCorners())
1208 patchPolygon = afwGeom.Polygon(wcs.skyToPixel(patchCorners))
1209 if patchPolygon.intersection(detectorPolygon):
1210 overlappingArea += patchPolygon.intersectionSingle(
1214 "Using template input tract=%s, patch=%s, subfilter=%s"
1215 % (dataId[
"tract"], dataId[
"patch"], dataId[
"subfilter"])
1217 if dataId[
"tract"]
in patchList:
1218 patchList[dataId[
"tract"]].append(dataId[
"patch"])
1220 patchList[dataId[
"tract"]] = [
1224 dataIds[dataId[
"tract"]].append(dataId)
1226 if not overlappingArea:
1227 raise pipeBase.NoWorkFound(
"No patches overlap detector")
1229 self.checkPatchList(patchList)
1231 coaddExposures = self.getDcrModel(patchList, dcrCoaddExposureHandles, visitInfo)
1232 return pipeBase.Struct(coaddExposures=coaddExposures, dataIds=dataIds)
1235 """Check that all of the DcrModel subfilters are present for each
1241 Dict of the patches containing valid data for each tract.
1246 If the number of exposures found for a patch does not match the
1247 number of subfilters.
1249 for tract
in patchList:
1250 for patch
in set(patchList[tract]):
1251 if patchList[tract].count(patch) != self.config.numSubfilters:
1253 "Invalid number of DcrModel subfilters found: %d vs %d expected",
1254 patchList[tract].count(patch),
1255 self.config.numSubfilters,
1259 """Build DCR-matched coadds from a list of exposure references.
1264 Dict of the patches containing valid data for each tract.
1265 coaddRefs : `list` [`lsst.daf.butler.DeferredDatasetHandle`]
1266 Data references to `~lsst.afw.image.Exposure` representing
1267 DcrModels that overlap the detector.
1268 visitInfo : `lsst.afw.image.VisitInfo`
1269 Metadata for the science image.
1273 coaddExposures : `list` [`lsst.afw.image.Exposure`]
1274 Coadd exposures that overlap the detector.
1276 coaddExposures = collections.defaultdict(list)
1277 for tract
in patchList:
1278 for patch
in set(patchList[tract]):
1281 for coaddRef
in coaddRefs
1285 dcrModel = DcrModel.fromQuantum(
1287 self.config.effectiveWavelength,
1288 self.config.bandwidth,
1289 self.config.numSubfilters,
1291 coaddExposures[tract].append(dcrModel.buildMatchedExposureHandle(visitInfo=visitInfo))
1292 return coaddExposures
1296 condition = (coaddRef.dataId[
"tract"] == tract) & (
1297 coaddRef.dataId[
"patch"] == patch
checkHighVariance(self, template)
_clearUnmappedMaskPlanes(self, mask, planeMap)
_correctVariance(self, template, plateScaleFactor)
_getInstrument(coaddInputs)
_makeGaussianPsf(self, template)
_plateScaleFactor(self, coaddInputs, wcs, bbox)
getExposures(self, coaddExposureHandles, bbox, skymap, wcs)
run(self, *, coaddExposureHandles, bbox, wcs, dataIds, physical_filter, visit=None, instrument=None)
convert_outputs_to_future(self, template, coadd_refs, detector, exposureRecord)
_checkInputs(dataIds, coaddExposures, scienceInstrument=None)
_makePsf(self, template, catalog, wcs)
_makeExposureCatalog(self, exposureRefs, dataIds, instrument=_UNKNOWN_INSTRUMENT)
_merge(self, maskedImages, bbox, wcs)
runQuantum(self, butlerQC, inputRefs, outputRefs)
__init__(self, *args, **kwargs)
getDcrModel(self, patchList, coaddRefs, visitInfo)
checkPatchList(self, patchList)
_selectDataRef(coaddRef, tract, patch)