22__all__ = [
"MakeWarpTask",
"MakeWarpConfig"]
28import lsst.afw.image
as afwImage
31import lsst.pipe.base.connectionTypes
as connectionTypes
34from deprecated.sphinx
import deprecated
35from lsst.daf.butler
import DeferredDatasetHandle
39from lsst.utils.timer
import timeMethod
40from .coaddBase
import CoaddBaseTask, growValidPolygons, makeSkyInfo, reorderAndPadList
41from .warpAndPsfMatch
import WarpAndPsfMatchTask
44log = logging.getLogger(__name__)
48 dimensions=(
"tract",
"patch",
"skymap",
"instrument",
"visit"),
49 defaultTemplates={
"coaddName":
"deep",
51 calExpList = connectionTypes.Input(
52 doc=
"Input exposures to be resampled and optionally PSF-matched onto a SkyMap projection/patch",
53 name=
"{calexpType}calexp",
54 storageClass=
"ExposureF",
55 dimensions=(
"instrument",
"visit",
"detector"),
59 backgroundList = connectionTypes.Input(
60 doc=
"Input backgrounds to be added back into the calexp if bgSubtracted=False",
61 name=
"calexpBackground",
62 storageClass=
"Background",
63 dimensions=(
"instrument",
"visit",
"detector"),
66 skyCorrList = connectionTypes.Input(
67 doc=
"Input Sky Correction to be subtracted from the calexp if doApplySkyCorr=True",
69 storageClass=
"Background",
70 dimensions=(
"instrument",
"visit",
"detector"),
73 skyMap = connectionTypes.Input(
74 doc=
"Input definition of geometry/bbox and projection/wcs for warped exposures",
75 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
76 storageClass=
"SkyMap",
77 dimensions=(
"skymap",),
79 direct = connectionTypes.Output(
80 doc=(
"Output direct warped exposure (previously called CoaddTempExp), produced by resampling "
81 "calexps onto the skyMap patch geometry."),
82 name=
"{coaddName}Coadd_directWarp",
83 storageClass=
"ExposureF",
84 dimensions=(
"tract",
"patch",
"skymap",
"visit",
"instrument"),
86 psfMatched = connectionTypes.Output(
87 doc=(
"Output PSF-Matched warped exposure (previously called CoaddTempExp), produced by resampling "
88 "calexps onto the skyMap patch geometry and PSF-matching to a model PSF."),
89 name=
"{coaddName}Coadd_psfMatchedWarp",
90 storageClass=
"ExposureF",
91 dimensions=(
"tract",
"patch",
"skymap",
"visit",
"instrument"),
93 visitSummary = connectionTypes.Input(
94 doc=
"Input visit-summary catalog with updated calibration objects.",
95 name=
"finalVisitSummary",
96 storageClass=
"ExposureCatalog",
97 dimensions=(
"instrument",
"visit",),
100 def __init__(self, *, config=None):
101 if config.bgSubtracted:
102 del self.backgroundList
103 if not config.doApplySkyCorr:
105 if not config.makeDirect:
107 if not config.makePsfMatched:
111@deprecated(reason=
"The Task corresponding to this Config is no longer in use. Will be removed after v29.",
112 version=
"v29.0", category=FutureWarning)
113class MakeWarpConfig(pipeBase.PipelineTaskConfig, CoaddBaseTask.ConfigClass,
114 pipelineConnections=MakeWarpConnections):
115 """Config for MakeWarpTask."""
117 warpAndPsfMatch = pexConfig.ConfigurableField(
118 target=WarpAndPsfMatchTask,
119 doc=
"Task to warp and PSF-match calexp",
121 doWrite = pexConfig.Field(
122 doc=
"persist <coaddName>Coadd_<warpType>Warp",
126 bgSubtracted = pexConfig.Field(
127 doc=
"Work with a background subtracted calexp?",
131 coaddPsf = pexConfig.ConfigField(
132 doc=
"Configuration for CoaddPsf",
133 dtype=CoaddPsfConfig,
135 makeDirect = pexConfig.Field(
136 doc=
"Make direct Warp/Coadds",
140 makePsfMatched = pexConfig.Field(
141 doc=
"Make Psf-Matched Warp/Coadd?",
145 modelPsf = GaussianPsfFactory.makeField(doc=
"Model Psf factory")
146 useVisitSummaryPsf = pexConfig.Field(
148 "If True, use the PSF model and aperture corrections from the 'visitSummary' connection. "
149 "If False, use the PSF model and aperture corrections from the 'exposure' connection. "
154 doWriteEmptyWarps = pexConfig.Field(
157 doc=
"Write out warps even if they are empty"
159 hasFakes = pexConfig.Field(
160 doc=
"Should be set to True if fake sources have been inserted into the input data.",
164 doApplySkyCorr = pexConfig.Field(
167 doc=
"Apply sky correction?",
169 idGenerator = DetectorVisitIdGeneratorConfig.make_field()
172 CoaddBaseTask.ConfigClass.validate(self)
174 if not self.makePsfMatched
and not self.makeDirect:
175 raise ValueError(
"At least one of config.makePsfMatched and config.makeDirect must be True")
176 if self.warpAndPsfMatch.warp.cacheSize != self.coaddPsf.cacheSize:
181 raise ValueError(
"Image warping cache size and CoaddPSf warping cache size do not agree.")
183 def setDefaults(self):
184 CoaddBaseTask.ConfigClass.setDefaults(self)
185 self.warpAndPsfMatch.warp.cacheSize = 0
186 self.coaddPsf.cacheSize = 0
189@deprecated(reason=
"The MakeWarpTask is replaced by MakeDirectWarpTask and MakePsfMatchedWarpTask. "
190 "This Task will be removed after v29.",
191 version=
"v29.0", category=FutureWarning)
193 """Warp and optionally PSF-Match calexps onto an a common projection.
195 Warp and optionally PSF-Match calexps onto a common projection, by
196 performing the following operations:
197 - Group calexps by visit/run
198 - For each visit, generate a Warp by calling method @ref run.
199 `run` loops over the visit's calexps calling
200 `~lsst.pipe.tasks.warpAndPsfMatch.WarpAndPsfMatchTask` on each visit
203 ConfigClass = MakeWarpConfig
204 _DefaultName =
"makeWarp"
206 def __init__(self, **kwargs):
207 CoaddBaseTask.__init__(self, **kwargs)
208 self.makeSubtask(
"warpAndPsfMatch")
209 if self.config.hasFakes:
210 self.calexpType =
"fakes_calexp"
212 self.calexpType =
"calexp"
214 @utils.inheritDoc(pipeBase.PipelineTask)
215 def runQuantum(self, butlerQC, inputRefs, outputRefs):
219 Obtain the list of input detectors from calExpList. Sort them by
220 detector order (to ensure reproducibility). Then ensure all input
221 lists are in the same sorted detector order.
223 detectorOrder = [handle.datasetRef.dataId[
'detector']
for handle
in inputRefs.calExpList]
225 inputRefs =
reorderRefs(inputRefs, detectorOrder, dataIdKey=
'detector')
228 inputs = butlerQC.get(inputRefs)
232 skyMap = inputs.pop(
"skyMap")
233 quantumDataId = butlerQC.quantum.dataId
234 skyInfo = makeSkyInfo(skyMap, tractId=quantumDataId[
'tract'], patchId=quantumDataId[
'patch'])
237 dataIdList = [ref.datasetRef.dataId
for ref
in inputRefs.calExpList]
240 self.config.idGenerator.apply(dataId).catalog_id
241 for dataId
in dataIdList
245 visitSummary = inputs[
"visitSummary"]
248 for dataId
in dataIdList:
249 row = visitSummary.find(dataId[
"detector"])
251 bboxList.append(
None)
254 bboxList.append(row.getBBox())
255 wcsList.append(row.getWcs())
256 inputs[
"bboxList"] = bboxList
257 inputs[
"wcsList"] = wcsList
261 completeIndices = self._prepareCalibratedExposures(**inputs)
262 inputs = self.filterInputs(indices=completeIndices, inputs=inputs)
268 coordList = [skyInfo.wcs.pixelToSky(pos)
for pos
in cornerPosList]
269 goodIndices = self.select.run(**inputs, coordList=coordList, dataIds=dataIdList)
270 inputs = self.filterInputs(indices=goodIndices, inputs=inputs)
273 visitId = dataIdList[0][
"visit"]
275 results = self.run(**inputs,
277 ccdIdList=[ccdIdList[i]
for i
in goodIndices],
278 dataIdList=[dataIdList[i]
for i
in goodIndices],
280 if self.config.makeDirect
and results.exposures[
"direct"]
is not None:
281 butlerQC.put(results.exposures[
"direct"], outputRefs.direct)
282 if self.config.makePsfMatched
and results.exposures[
"psfMatched"]
is not None:
283 butlerQC.put(results.exposures[
"psfMatched"], outputRefs.psfMatched)
286 def run(self, calExpList, ccdIdList, skyInfo, visitId=0, dataIdList=None, **kwargs):
287 """Create a Warp from inputs.
289 We iterate over the multiple calexps in a single exposure to construct
290 the warp (previously called a coaddTempExp) of that exposure to the
291 supplied tract/patch.
293 Pixels that receive no pixels are set to NAN; this is not correct
294 (violates LSST algorithms group policy), but will be fixed up by
295 interpolating after the coaddition.
297 calExpList : `list` [ `lsst.afw.image.Exposure` ]
298 List of single-detector input images that (may) overlap the patch
300 skyInfo : `lsst.pipe.base.Struct`
301 Struct from `~lsst.pipe.base.coaddBase.makeSkyInfo()` with
302 geometric information about the patch.
304 Integer identifier for visit, for the table that will
305 produce the CoaddPsf.
309 result : `lsst.pipe.base.Struct`
310 Results as a struct with attributes:
313 A dictionary containing the warps requested:
314 "direct": direct warp if ``config.makeDirect``
315 "psfMatched": PSF-matched warp if ``config.makePsfMatched``
318 warpTypeList = self.getWarpTypeList()
320 totGoodPix = {warpType: 0
for warpType
in warpTypeList}
321 didSetMetadata = {warpType:
False for warpType
in warpTypeList}
322 warps = {warpType: self._prepareEmptyExposure(skyInfo)
for warpType
in warpTypeList}
323 inputRecorder = {warpType: self.inputRecorder.makeCoaddTempExpRecorder(visitId, len(calExpList))
324 for warpType
in warpTypeList}
326 modelPsf = self.config.modelPsf.apply()
if self.config.makePsfMatched
else None
327 if dataIdList
is None:
328 dataIdList = ccdIdList
330 for calExpInd, (calExp, ccdId, dataId)
in enumerate(zip(calExpList, ccdIdList, dataIdList)):
331 self.log.info(
"Processing calexp %d of %d for this Warp: id=%s",
332 calExpInd+1, len(calExpList), dataId)
334 warpedAndMatched = self.warpAndPsfMatch.run(calExp, modelPsf=modelPsf,
335 wcs=skyInfo.wcs, maxBBox=skyInfo.bbox,
336 makeDirect=self.config.makeDirect,
337 makePsfMatched=self.config.makePsfMatched)
338 except Exception
as e:
339 self.log.warning(
"WarpAndPsfMatch failed for calexp %s; skipping it: %s", dataId, e)
342 numGoodPix = {warpType: 0
for warpType
in warpTypeList}
343 for warpType
in warpTypeList:
344 exposure = warpedAndMatched.getDict()[warpType]
347 warp = warps[warpType]
349 warp.getMaskedImage(), exposure.getMaskedImage(), self.getBadPixelMask())
350 totGoodPix[warpType] += numGoodPix[warpType]
351 self.log.debug(
"Calexp %s has %d good pixels in this patch (%.1f%%) for %s",
352 dataId, numGoodPix[warpType],
353 100.0*numGoodPix[warpType]/skyInfo.bbox.getArea(), warpType)
354 if numGoodPix[warpType] > 0
and not didSetMetadata[warpType]:
355 warp.info.id = exposure.info.id
356 warp.setPhotoCalib(exposure.getPhotoCalib())
357 warp.setFilter(exposure.getFilter())
358 warp.getInfo().setVisitInfo(exposure.getInfo().getVisitInfo())
361 warp.setPsf(exposure.getPsf())
362 didSetMetadata[warpType] =
True
366 inputRecorder[warpType].addCalExp(calExp, ccdId, numGoodPix[warpType])
368 except Exception
as e:
369 self.log.warning(
"Error processing calexp %s; skipping it: %s", dataId, e)
372 for warpType
in warpTypeList:
373 self.log.info(
"%sWarp has %d good pixels (%.1f%%)",
374 warpType, totGoodPix[warpType], 100.0*totGoodPix[warpType]/skyInfo.bbox.getArea())
376 if totGoodPix[warpType] > 0
and didSetMetadata[warpType]:
377 inputRecorder[warpType].finish(warps[warpType], totGoodPix[warpType])
378 if warpType ==
"direct":
379 warps[warpType].setPsf(
380 CoaddPsf(inputRecorder[warpType].coaddInputs.ccds, skyInfo.wcs,
381 self.config.coaddPsf.makeControl()))
384 inputRecorder[warpType].coaddInputs,
385 -self.config.warpAndPsfMatch.psfMatch.kernel.active.kernelSize // 2,
388 if not self.config.doWriteEmptyWarps:
390 warps[warpType] =
None
394 result = pipeBase.Struct(exposures=warps)
397 def filterInputs(self, indices, inputs):
398 """Filter task inputs by their indices.
402 indices : `list` [`int`]
403 inputs : `dict` [`list`]
404 A dictionary of input connections to be passed to run.
408 inputs : `dict` [`list`]
409 Task inputs with their lists filtered by indices.
411 for key
in inputs.keys():
413 if isinstance(inputs[key], list):
414 inputs[key] = [inputs[key][ind]
for ind
in indices]
417 def _prepareCalibratedExposures(self, *, visitSummary, calExpList=[], wcsList=None,
418 backgroundList=None, skyCorrList=None, **kwargs):
419 """Calibrate and add backgrounds to input calExpList in place.
423 visitSummary : `lsst.afw.table.ExposureCatalog`
424 Exposure catalog with potentially all calibrations. Attributes set
425 to `None` are ignored.
426 calExpList : `list` [`lsst.afw.image.Exposure` or
427 `lsst.daf.butler.DeferredDatasetHandle`]
428 Sequence of single-epoch images (or deferred load handles for
429 images) to be modified in place. On return this always has images,
431 wcsList : `list` [`lsst.afw.geom.SkyWcs` or `None` ]
432 The WCSs of the calexps in ``calExpList``. These will be used to
433 determine if the calexp should be used in the warp. The list is
434 dynamically updated with the WCSs from the visitSummary.
435 backgroundList : `list` [`lsst.afw.math.BackgroundList`], optional
436 Sequence of backgrounds to be added back in if bgSubtracted=False.
437 skyCorrList : `list` [`lsst.afw.math.BackgroundList`], optional
438 Sequence of background corrections to be subtracted if
441 Additional keyword arguments.
445 indices : `list` [`int`]
446 Indices of ``calExpList`` and friends that have valid
449 wcsList = len(calExpList)*[
None]
if wcsList
is None else wcsList
450 backgroundList = len(calExpList)*[
None]
if backgroundList
is None else backgroundList
451 skyCorrList = len(calExpList)*[
None]
if skyCorrList
is None else skyCorrList
454 for index, (calexp, background, skyCorr)
in enumerate(zip(calExpList,
457 if isinstance(calexp, DeferredDatasetHandle):
458 calexp = calexp.get()
460 if not self.config.bgSubtracted:
461 calexp.maskedImage += background.getImage()
463 detectorId = calexp.info.getDetector().getId()
466 row = visitSummary.find(detectorId)
469 "Detector id %d has no row in the visitSummary and will "
470 "not be used in the warp", detectorId,
473 if (photoCalib := row.getPhotoCalib())
is not None:
474 calexp.setPhotoCalib(photoCalib)
477 "Detector id %d for visit %d has None for photoCalib in the visitSummary and will "
478 "not be used in the warp", detectorId, row[
"visit"],
481 if (skyWcs := row.getWcs())
is not None:
482 calexp.setWcs(skyWcs)
483 wcsList[index] = skyWcs
486 "Detector id %d for visit %d has None for wcs in the visitSummary and will "
487 "not be used in the warp", detectorId, row[
"visit"],
490 if self.config.useVisitSummaryPsf:
491 if (psf := row.getPsf())
is not None:
495 "Detector id %d for visit %d has None for psf in the visitSummary and will "
496 "not be used in the warp", detectorId, row[
"visit"],
499 if (apCorrMap := row.getApCorrMap())
is not None:
500 calexp.info.setApCorrMap(apCorrMap)
503 "Detector id %d for visit %d has None for apCorrMap in the visitSummary and will "
504 "not be used in the warp", detectorId, row[
"visit"],
508 if calexp.getPsf()
is None:
510 "Detector id %d for visit %d has None for psf for the calexp and will "
511 "not be used in the warp", detectorId, row[
"visit"],
514 if calexp.info.getApCorrMap()
is None:
516 "Detector id %d for visit %d has None for apCorrMap in the calexp and will "
517 "not be used in the warp", detectorId, row[
"visit"],
522 if self.config.doApplySkyCorr:
523 calexp.maskedImage -= skyCorr.getImage()
526 calexp.maskedImage = photoCalib.calibrateImage(calexp.maskedImage)
532 indices.append(index)
533 calExpList[index] = calexp
538 def _prepareEmptyExposure(skyInfo):
539 """Produce an empty exposure for a given patch.
543 skyInfo : `lsst.pipe.base.Struct`
544 Struct from `~lsst.pipe.base.coaddBase.makeSkyInfo()` with
545 geometric information about the patch.
549 exp : `lsst.afw.image.exposure.ExposureF`
550 An empty exposure for a given patch.
552 exp = afwImage.ExposureF(skyInfo.bbox, skyInfo.wcs)
554 .getPlaneBitMask(
"NO_DATA"), numpy.inf)
556 exp.metadata[
"BUNIT"] =
"nJy"
560 """Return list of requested warp types per the config.
563 if self.config.makeDirect:
564 warpTypeList.append(
"direct")
565 if self.config.makePsfMatched:
566 warpTypeList.append(
"psfMatched")
571 """Reorder inputRefs per outputSortKeyOrder.
573 Any inputRefs which are lists will be resorted per specified key e.g.,
574 'detector.' Only iterables will be reordered, and values can be of type
575 `lsst.pipe.base.connections.DeferredDatasetRef` or
576 `lsst.daf.butler.core.datasets.ref.DatasetRef`.
578 Returned lists of refs have the same length as the outputSortKeyOrder.
579 If an outputSortKey not in the inputRef, then it will be padded with None.
580 If an inputRef contains an inputSortKey that is not in the
581 outputSortKeyOrder it will be removed.
585 inputRefs : `lsst.pipe.base.connections.QuantizedConnection`
586 Input references to be reordered and padded.
587 outputSortKeyOrder : `iterable`
588 Iterable of values to be compared with inputRef's dataId[dataIdKey].
590 The data ID key in the dataRefs to compare with the outputSortKeyOrder.
594 inputRefs : `lsst.pipe.base.connections.QuantizedConnection`
595 Quantized Connection with sorted DatasetRef values sorted if iterable.
597 for connectionName, refs
in inputRefs:
598 if isinstance(refs, Iterable):
599 if hasattr(refs[0],
"dataId"):
600 inputSortKeyOrder = [ref.dataId[dataIdKey]
for ref
in refs]
602 inputSortKeyOrder = [handle.datasetRef.dataId[dataIdKey]
for handle
in refs]
603 if inputSortKeyOrder != outputSortKeyOrder:
604 setattr(inputRefs, connectionName,
605 reorderAndPadList(refs, inputSortKeyOrder, outputSortKeyOrder))
Represent a 2-dimensional array of bitmask pixels.
The photometric calibration of an exposure.
A floating-point coordinate rectangle geometry.
CoaddPsf is the Psf derived to be used for non-PSF-matched Coadd images.
int copyGoodPixels(lsst::afw::image::Image< ImagePixelT > &destImage, lsst::afw::image::Image< ImagePixelT > const &srcImage)
copy good pixels from one image to another
reorderRefs(inputRefs, outputSortKeyOrder, dataIdKey)