22__all__ = [
"AssembleCoaddTask",
"AssembleCoaddConnections",
"AssembleCoaddConfig",
23 "CompareWarpAssembleCoaddTask",
"CompareWarpAssembleCoaddConfig"]
42from .coaddBase
import CoaddBaseTask, makeSkyInfo, reorderAndPadList
43from .interpImage
import InterpImageTask
44from .scaleZeroPoint
import ScaleZeroPointTask
45from .maskStreaks
import MaskStreaksTask
46from .healSparseMapping
import HealSparseInputMapTask
48from lsst.utils.timer
import timeMethod
49from deprecated.sphinx
import deprecated
51log = logging.getLogger(__name__)
55 dimensions=(
"tract",
"patch",
"band",
"skymap"),
56 defaultTemplates={
"inputCoaddName":
"deep",
57 "outputCoaddName":
"deep",
59 "warpTypeSuffix":
""}):
61 inputWarps = pipeBase.connectionTypes.Input(
62 doc=(
"Input list of warps to be assemebled i.e. stacked."
63 "WarpType (e.g. direct, psfMatched) is controlled by the warpType config parameter"),
64 name=
"{inputCoaddName}Coadd_{warpType}Warp",
65 storageClass=
"ExposureF",
66 dimensions=(
"tract",
"patch",
"skymap",
"visit",
"instrument"),
70 skyMap = pipeBase.connectionTypes.Input(
71 doc=
"Input definition of geometry/bbox and projection/wcs for coadded exposures",
72 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
73 storageClass=
"SkyMap",
74 dimensions=(
"skymap", ),
76 selectedVisits = pipeBase.connectionTypes.Input(
77 doc=
"Selected visits to be coadded.",
78 name=
"{outputCoaddName}Visits",
79 storageClass=
"StructuredDataDict",
80 dimensions=(
"instrument",
"tract",
"patch",
"skymap",
"band")
82 brightObjectMask = pipeBase.connectionTypes.PrerequisiteInput(
83 doc=(
"Input Bright Object Mask mask produced with external catalogs to be applied to the mask plane"
85 name=
"brightObjectMask",
86 storageClass=
"ObjectMaskCatalog",
87 dimensions=(
"tract",
"patch",
"skymap",
"band"),
89 coaddExposure = pipeBase.connectionTypes.Output(
90 doc=
"Output coadded exposure, produced by stacking input warps",
91 name=
"{outputCoaddName}Coadd{warpTypeSuffix}",
92 storageClass=
"ExposureF",
93 dimensions=(
"tract",
"patch",
"skymap",
"band"),
95 nImage = pipeBase.connectionTypes.Output(
96 doc=
"Output image of number of input images per pixel",
97 name=
"{outputCoaddName}Coadd_nImage",
98 storageClass=
"ImageU",
99 dimensions=(
"tract",
"patch",
"skymap",
"band"),
101 inputMap = pipeBase.connectionTypes.Output(
102 doc=
"Output healsparse map of input images",
103 name=
"{outputCoaddName}Coadd_inputMap",
104 storageClass=
"HealSparseMap",
105 dimensions=(
"tract",
"patch",
"skymap",
"band"),
108 def __init__(self, *, config=None):
109 super().__init__(config=config)
111 if not config.doMaskBrightObjects:
112 self.prerequisiteInputs.remove(
"brightObjectMask")
114 if not config.doSelectVisits:
115 self.inputs.remove(
"selectedVisits")
117 if not config.doNImage:
118 self.outputs.remove(
"nImage")
120 if not self.config.doInputMap:
121 self.outputs.remove(
"inputMap")
124class AssembleCoaddConfig(CoaddBaseTask.ConfigClass, pipeBase.PipelineTaskConfig,
125 pipelineConnections=AssembleCoaddConnections):
126 warpType = pexConfig.Field(
127 doc=
"Warp name: one of 'direct' or 'psfMatched'",
131 subregionSize = pexConfig.ListField(
133 doc=
"Width, height of stack subregion size; "
134 "make small enough that a full stack of images will fit into memory at once.",
136 default=(2000, 2000),
138 statistic = pexConfig.Field(
140 doc=
"Main stacking statistic for aggregating over the epochs.",
143 doOnlineForMean = pexConfig.Field(
145 doc=
"Perform online coaddition when statistic=\"MEAN\" to save memory?",
148 doSigmaClip = pexConfig.Field(
150 doc=
"Perform sigma clipped outlier rejection with MEANCLIP statistic? (DEPRECATED)",
153 sigmaClip = pexConfig.Field(
155 doc=
"Sigma for outlier rejection; ignored if non-clipping statistic selected.",
158 clipIter = pexConfig.Field(
160 doc=
"Number of iterations of outlier rejection; ignored if non-clipping statistic selected.",
163 calcErrorFromInputVariance = pexConfig.Field(
165 doc=
"Calculate coadd variance from input variance by stacking statistic."
166 "Passed to StatisticsControl.setCalcErrorFromInputVariance()",
169 scaleZeroPoint = pexConfig.ConfigurableField(
170 target=ScaleZeroPointTask,
171 doc=
"Task to adjust the photometric zero point of the coadd temp exposures",
173 doInterp = pexConfig.Field(
174 doc=
"Interpolate over NaN pixels? Also extrapolate, if necessary, but the results are ugly.",
178 interpImage = pexConfig.ConfigurableField(
179 target=InterpImageTask,
180 doc=
"Task to interpolate (and extrapolate) over NaN pixels",
182 doWrite = pexConfig.Field(
183 doc=
"Persist coadd?",
187 doNImage = pexConfig.Field(
188 doc=
"Create image of number of contributing exposures for each pixel",
192 doUsePsfMatchedPolygons = pexConfig.Field(
193 doc=
"Use ValidPolygons from shrunk Psf-Matched Calexps? Should be set to True by CompareWarp only.",
197 maskPropagationThresholds = pexConfig.DictField(
200 doc=(
"Threshold (in fractional weight) of rejection at which we propagate a mask plane to "
201 "the coadd; that is, we set the mask bit on the coadd if the fraction the rejected frames "
202 "would have contributed exceeds this value."),
203 default={
"SAT": 0.1},
205 removeMaskPlanes = pexConfig.ListField(dtype=str, default=[
"NOT_DEBLENDED"],
206 doc=
"Mask planes to remove before coadding")
207 doMaskBrightObjects = pexConfig.Field(dtype=bool, default=
False,
208 doc=
"Set mask and flag bits for bright objects?")
209 brightObjectMaskName = pexConfig.Field(dtype=str, default=
"BRIGHT_OBJECT",
210 doc=
"Name of mask bit used for bright objects")
211 coaddPsf = pexConfig.ConfigField(
212 doc=
"Configuration for CoaddPsf",
213 dtype=measAlg.CoaddPsfConfig,
215 doAttachTransmissionCurve = pexConfig.Field(
216 dtype=bool, default=
False, optional=
False,
217 doc=(
"Attach a piecewise TransmissionCurve for the coadd? "
218 "(requires all input Exposures to have TransmissionCurves).")
220 hasFakes = pexConfig.Field(
223 doc=
"Should be set to True if fake sources have been inserted into the input data."
225 doSelectVisits = pexConfig.Field(
226 doc=
"Coadd only visits selected by a SelectVisitsTask",
230 doInputMap = pexConfig.Field(
231 doc=
"Create a bitwise map of coadd inputs",
235 inputMapper = pexConfig.ConfigurableField(
236 doc=
"Input map creation subtask.",
237 target=HealSparseInputMapTask,
242 self.badMaskPlanes = [
"NO_DATA",
"BAD",
"SAT",
"EDGE"]
249 log.warning(
"Config doPsfMatch deprecated. Setting warpType='psfMatched'")
250 self.warpType =
'psfMatched'
251 if self.doSigmaClip
and self.statistic !=
"MEANCLIP":
252 log.warning(
'doSigmaClip deprecated. To replicate behavior, setting statistic to "MEANCLIP"')
253 self.statistic =
"MEANCLIP"
254 if self.doInterp
and self.statistic
not in [
'MEAN',
'MEDIAN',
'MEANCLIP',
'VARIANCE',
'VARIANCECLIP']:
255 raise ValueError(
"Must set doInterp=False for statistic=%s, which does not "
256 "compute and set a non-zero coadd variance estimate." % (self.statistic))
258 unstackableStats = [
'NOTHING',
'ERROR',
'ORMASK']
259 if not hasattr(afwMath.Property, self.statistic)
or self.statistic
in unstackableStats:
260 stackableStats = [
str(k)
for k
in afwMath.Property.__members__.keys()
261 if str(k)
not in unstackableStats]
262 raise ValueError(
"statistic %s is not allowed. Please choose one of %s."
263 % (self.statistic, stackableStats))
267 """Assemble a coadded image from a set of warps.
269 Each Warp that goes into a coadd will typically have an independent
270 photometric zero-point. Therefore, we must scale each Warp to set it to
271 a common photometric zeropoint. WarpType may be one of 'direct' or
272 'psfMatched',
and the boolean configs `config.makeDirect`
and
273 `config.makePsfMatched` set which of the warp types will be coadded.
274 The coadd
is computed
as a mean
with optional outlier rejection.
275 Criteria
for outlier rejection are set
in `AssembleCoaddConfig`.
276 Finally, Warps can have bad
'NaN' pixels which received no input
from the
277 source calExps. We interpolate over these bad (NaN) pixels.
279 `AssembleCoaddTask` uses several sub-tasks. These are
281 - `~lsst.pipe.tasks.ScaleZeroPointTask`
282 - create
and use an ``imageScaler`` object to scale the photometric zeropoint
for each Warp
283 - `~lsst.pipe.tasks.InterpImageTask`
284 - interpolate across bad pixels (NaN)
in the final coadd
286 You can retarget these subtasks
if you wish.
291 Raised
if unable to define mask plane
for bright objects.
296 `AssembleCoaddTask` has no debug variables of its own. Some of the
297 subtasks may support `~lsst.base.lsstDebug` variables. See the
298 documentation
for the subtasks
for further information.
302 `AssembleCoaddTask` assembles a set of warped images into a coadded image.
303 The `AssembleCoaddTask` can be invoked by running ``assembleCoadd.py``
304 with the flag
'--legacyCoadd'. Usage of assembleCoadd.py expects two
305 inputs: a data reference to the tract patch
and filter to be coadded,
and
306 a list of Warps to attempt to coadd. These are specified using ``--id``
and
307 ``--selectId``, respectively:
311 --id = [KEY=VALUE1[^VALUE2[^VALUE3...] [KEY=VALUE1[^VALUE2[^VALUE3...] ...]]
312 --selectId [KEY=VALUE1[^VALUE2[^VALUE3...] [KEY=VALUE1[^VALUE2[^VALUE3...] ...]]
314 Only the Warps that cover the specified tract
and patch will be coadded.
315 A list of the available optional arguments can be obtained by calling
316 ``assembleCoadd.py``
with the ``--help`` command line argument:
320 assembleCoadd.py --help
322 To demonstrate usage of the `AssembleCoaddTask`
in the larger context of
323 multi-band processing, we will generate the HSC-I & -R band coadds
from
324 HSC engineering test data provided
in the ``ci_hsc`` package. To begin,
325 assuming that the lsst stack has been already set up, we must set up the
326 obs_subaru
and ``ci_hsc`` packages. This defines the environment variable
327 ``$CI_HSC_DIR``
and points at the location of the package. The raw HSC
328 data live
in the ``$CI_HSC_DIR/raw directory``. To begin assembling the
329 coadds, we must first run:
332 - process the individual ccds
in $CI_HSC_RAW to produce calibrated exposures
334 - create a skymap that covers the area of the sky present
in the raw exposures
336 - warp the individual calibrated exposures to the tangent plane of the coadd
338 We can perform all of these steps by running
342 $CI_HSC_DIR scons warp-903986 warp-904014 warp-903990 warp-904010 warp-903988
344 This will produce warped exposures
for each visit. To coadd the warped
345 data, we call assembleCoadd.py
as follows:
349 assembleCoadd.py --legacyCoadd $CI_HSC_DIR/DATA --id patch=5,4 tract=0 filter=HSC-I \
350 --selectId visit=903986 ccd=16 --selectId visit=903986 ccd=22 --selectId visit=903986 ccd=23 \
351 --selectId visit=903986 ccd=100 --selectId visit=904014 ccd=1 --selectId visit=904014 ccd=6 \
352 --selectId visit=904014 ccd=12 --selectId visit=903990 ccd=18 --selectId visit=903990 ccd=25 \
353 --selectId visit=904010 ccd=4 --selectId visit=904010 ccd=10 --selectId visit=904010 ccd=100 \
354 --selectId visit=903988 ccd=16 --selectId visit=903988 ccd=17 --selectId visit=903988 ccd=23 \
355 --selectId visit=903988 ccd=24
357 that will process the HSC-I band data. The results are written
in
358 ``$CI_HSC_DIR/DATA/deepCoadd-results/HSC-I``.
360 You may also choose to run:
364 scons warp-903334 warp-903336 warp-903338 warp-903342 warp-903344 warp-903346
365 assembleCoadd.py --legacyCoadd $CI_HSC_DIR/DATA --id patch=5,4 tract=0 filter=HSC-R \
366 --selectId visit=903334 ccd=16 --selectId visit=903334 ccd=22 --selectId visit=903334 ccd=23 \
367 --selectId visit=903334 ccd=100 --selectId visit=903336 ccd=17 --selectId visit=903336 ccd=24 \
368 --selectId visit=903338 ccd=18 --selectId visit=903338 ccd=25 --selectId visit=903342 ccd=4 \
369 --selectId visit=903342 ccd=10 --selectId visit=903342 ccd=100 --selectId visit=903344 ccd=0 \
370 --selectId visit=903344 ccd=5 --selectId visit=903344 ccd=11 --selectId visit=903346 ccd=1 \
371 --selectId visit=903346 ccd=6 --selectId visit=903346 ccd=12
373 to generate the coadd
for the HSC-R band
if you are interested
in
374 following multiBand Coadd processing
as discussed
in `pipeTasks_multiBand`
375 (but note that normally, one would use the `SafeClipAssembleCoaddTask`
376 rather than `AssembleCoaddTask` to make the coadd.
379 ConfigClass = AssembleCoaddConfig
380 _DefaultName = "assembleCoadd"
382 def __init__(self, *args, **kwargs):
385 argNames = [
"config",
"name",
"parentTask",
"log"]
386 kwargs.update({k: v
for k, v
in zip(argNames, args)})
387 warnings.warn(
"AssembleCoadd received positional args, and casting them as kwargs: %s. "
388 "PipelineTask will not take positional args" % argNames, FutureWarning)
390 super().__init__(**kwargs)
391 self.makeSubtask(
"interpImage")
392 self.makeSubtask(
"scaleZeroPoint")
394 if self.config.doMaskBrightObjects:
398 except pexExceptions.LsstCppException:
399 raise RuntimeError(
"Unable to define mask plane for bright objects; planes used are %s" %
400 mask.getMaskPlaneDict().keys())
403 if self.config.doInputMap:
404 self.makeSubtask(
"inputMapper")
408 @utils.inheritDoc(pipeBase.PipelineTask)
410 inputData = butlerQC.get(inputRefs)
414 skyMap = inputData[
"skyMap"]
415 outputDataId = butlerQC.quantum.dataId
417 inputData[
'skyInfo'] = makeSkyInfo(skyMap,
418 tractId=outputDataId[
'tract'],
419 patchId=outputDataId[
'patch'])
421 if self.config.doSelectVisits:
422 warpRefList = self.
filterWarps(inputData[
'inputWarps'], inputData[
'selectedVisits'])
424 warpRefList = inputData[
'inputWarps']
427 self.log.info(
"Found %d %s", len(inputs.tempExpRefList),
429 if len(inputs.tempExpRefList) == 0:
430 raise pipeBase.NoWorkFound(
"No coadd temporary exposures found")
433 retStruct = self.
run(inputData[
'skyInfo'], inputs.tempExpRefList, inputs.imageScalerList,
434 inputs.weightList, supplementaryData=supplementaryData)
436 inputData.setdefault(
'brightObjectMask',
None)
437 self.
processResults(retStruct.coaddExposure, inputData[
'brightObjectMask'], outputDataId)
439 if self.config.doWrite:
440 butlerQC.put(retStruct, outputRefs)
444 """Interpolate over missing data and mask bright stars.
449 The coadded exposure to process.
451 Table of bright objects to mask.
452 dataId : `lsst.daf.butler.DataId`
or `
None`, optional
455 if self.config.doInterp:
456 self.interpImage.run(coaddExposure.getMaskedImage(), planeName=
"NO_DATA")
458 varArray = coaddExposure.variance.array
459 with numpy.errstate(invalid=
"ignore"):
460 varArray[:] = numpy.where(varArray > 0, varArray, numpy.inf)
462 if self.config.doMaskBrightObjects:
465 def _makeSupplementaryData(self, butlerQC, inputRefs, outputRefs):
466 """Make additional inputs to run() specific to subclasses (Gen3).
468 Duplicates interface of `runQuantum` method.
469 Available to be implemented by subclasses only if they need the
470 coadd dataRef
for performing preliminary processing before
471 assembling the coadd.
475 butlerQC : `~lsst.pipe.base.ButlerQuantumContext`
476 Gen3 Butler object
for fetching additional data products before
477 running the Task specialized
for quantum being processed.
478 inputRefs : `~lsst.pipe.base.InputQuantizedConnection`
479 Attributes are the names of the connections describing input dataset types.
480 Values are DatasetRefs that task consumes
for corresponding dataset type.
481 DataIds are guaranteed to match data objects
in ``inputData``.
482 outputRefs : `~lsst.pipe.base.OutputQuantizedConnection`
483 Attributes are the names of the connections describing output dataset types.
484 Values are DatasetRefs that task
is to produce
485 for corresponding dataset type.
487 return pipeBase.Struct()
490 reason=
"makeSupplementaryDataGen3 is deprecated in favor of _makeSupplementaryData",
492 category=FutureWarning
498 """Prepare the input warps for coaddition by measuring the weight for
499 each warp and the scaling
for the photometric zero point.
501 Each Warp has its own photometric zeropoint
and background variance.
502 Before coadding these Warps together, compute a scale factor to
503 normalize the photometric zeropoint
and compute the weight
for each Warp.
508 List of data references to tempExp.
512 result : `~lsst.pipe.base.Struct`
513 Results
as a struct
with attributes:
516 `list` of data references to tempExp.
518 `list` of weightings.
520 `list` of image scalers.
523 statsCtrl.setNumSigmaClip(self.config.sigmaClip)
524 statsCtrl.setNumIter(self.config.clipIter)
526 statsCtrl.setNanSafe(True)
534 for tempExpRef
in refList:
535 tempExp = tempExpRef.get()
537 if numpy.isnan(tempExp.image.array).all():
539 maskedImage = tempExp.getMaskedImage()
540 imageScaler = self.scaleZeroPoint.computeImageScaler(
545 imageScaler.scaleMaskedImage(maskedImage)
546 except Exception
as e:
547 self.log.warning(
"Scaling failed for %s (skipping it): %s", tempExpRef.dataId, e)
550 afwMath.MEANCLIP, statsCtrl)
551 meanVar, meanVarErr = statObj.getResult(afwMath.MEANCLIP)
552 weight = 1.0 / float(meanVar)
553 if not numpy.isfinite(weight):
554 self.log.warning(
"Non-finite weight for %s: skipping", tempExpRef.dataId)
556 self.log.info(
"Weight of %s %s = %0.3f", tempExpName, tempExpRef.dataId, weight)
561 tempExpRefList.append(tempExpRef)
562 weightList.append(weight)
563 imageScalerList.append(imageScaler)
565 return pipeBase.Struct(tempExpRefList=tempExpRefList, weightList=weightList,
566 imageScalerList=imageScalerList)
569 """Prepare the statistics for coadding images.
573 mask : `int`, optional
574 Bit mask value to exclude from coaddition.
578 stats : `~lsst.pipe.base.Struct`
579 Statistics
as a struct
with attributes:
584 Statistic
for coadd (`~lsst.afw.math.Property`).
589 statsCtrl.setNumSigmaClip(self.config.sigmaClip)
590 statsCtrl.setNumIter(self.config.clipIter)
591 statsCtrl.setAndMask(mask)
592 statsCtrl.setNanSafe(
True)
593 statsCtrl.setWeighted(
True)
594 statsCtrl.setCalcErrorFromInputVariance(self.config.calcErrorFromInputVariance)
595 for plane, threshold
in self.config.maskPropagationThresholds.items():
596 bit = afwImage.Mask.getMaskPlane(plane)
597 statsCtrl.setMaskPropagationThreshold(bit, threshold)
599 return pipeBase.Struct(ctrl=statsCtrl, flags=statsFlags)
602 def run(self, skyInfo, tempExpRefList, imageScalerList, weightList,
603 altMaskList=None, mask=None, supplementaryData=None):
604 """Assemble a coadd from input warps.
606 Assemble the coadd using the provided list of coaddTempExps. Since
607 the full coadd covers a patch (a large area), the assembly is
608 performed over small areas on the image at a time
in order to
609 conserve memory usage. Iterate over subregions within the outer
610 bbox of the patch using `assembleSubregion` to stack the corresponding
611 subregions
from the coaddTempExps
with the statistic specified.
612 Set the edge bits the coadd mask based on the weight map.
616 skyInfo : `~lsst.pipe.base.Struct`
617 Struct
with geometric information about the patch.
618 tempExpRefList : `list`
619 List of data references to Warps (previously called CoaddTempExps).
620 imageScalerList : `list`
621 List of image scalers.
624 altMaskList : `list`, optional
625 List of alternate masks to use rather than those stored
with
627 mask : `int`, optional
628 Bit mask value to exclude
from coaddition.
629 supplementaryData : `~lsst.pipe.base.Struct`, optional
630 Struct
with additional data products needed to assemble coadd.
631 Only used by subclasses that implement ``_makeSupplementaryData``
636 result : `~lsst.pipe.base.Struct`
637 Results
as a struct
with attributes:
644 Bit-wise map of inputs,
if requested.
646 Input list of refs to the warps (``lsst.daf.butler.DeferredDatasetHandle``)
649 Input list of image scalers (`list`) (unmodified).
651 Input list of weights (`list`) (unmodified).
654 self.log.info("Assembling %s %s", len(tempExpRefList), tempExpName)
657 if altMaskList
is None:
658 altMaskList = [
None]*len(tempExpRefList)
660 coaddExposure = afwImage.ExposureF(skyInfo.bbox, skyInfo.wcs)
661 coaddExposure.setPhotoCalib(self.scaleZeroPoint.getPhotoCalib())
662 coaddExposure.getInfo().setCoaddInputs(self.inputRecorder.makeCoaddInputs())
664 coaddMaskedImage = coaddExposure.getMaskedImage()
665 subregionSizeArr = self.config.subregionSize
666 subregionSize =
geom.Extent2I(subregionSizeArr[0], subregionSizeArr[1])
668 if self.config.doNImage:
669 nImage = afwImage.ImageU(skyInfo.bbox)
674 if self.config.doInputMap:
675 self.inputMapper.build_ccd_input_map(skyInfo.bbox,
677 coaddExposure.getInfo().getCoaddInputs().ccds)
679 if self.config.doOnlineForMean
and self.config.statistic ==
"MEAN":
682 weightList, altMaskList, stats.ctrl,
684 except Exception
as e:
685 self.log.exception(
"Cannot compute online coadd %s", e)
688 for subBBox
in self.
_subBBoxIter(skyInfo.bbox, subregionSize):
691 weightList, altMaskList, stats.flags, stats.ctrl,
693 except Exception
as e:
694 self.log.exception(
"Cannot compute coadd %s: %s", subBBox, e)
698 if self.config.doInputMap:
699 self.inputMapper.finalize_ccd_input_map_mask()
700 inputMap = self.inputMapper.ccd_input_map
708 return pipeBase.Struct(coaddExposure=coaddExposure, nImage=nImage,
709 warpRefList=tempExpRefList, imageScalerList=imageScalerList,
710 weightList=weightList, inputMap=inputMap)
713 """Set the metadata for the coadd.
715 This basic implementation sets the filter from the first input.
720 The target exposure
for the coadd.
721 tempExpRefList : `list`
722 List of data references to tempExp.
729 Raised
if there
is a length mismatch.
731 assert len(tempExpRefList) == len(weightList),
"Length mismatch"
738 tempExpList = [tempExpRef.get(parameters={
'bbox': bbox})
for tempExpRef
in tempExpRefList]
740 numCcds = sum(len(tempExp.getInfo().getCoaddInputs().ccds)
for tempExp
in tempExpList)
745 coaddInputs = coaddExposure.getInfo().getCoaddInputs()
746 coaddInputs.ccds.reserve(numCcds)
747 coaddInputs.visits.reserve(len(tempExpList))
749 for tempExp, weight
in zip(tempExpList, weightList):
750 self.inputRecorder.addVisitToCoadd(coaddInputs, tempExp, weight)
752 if self.config.doUsePsfMatchedPolygons:
755 coaddInputs.visits.sort()
756 coaddInputs.ccds.sort()
762 modelPsfList = [tempExp.getPsf()
for tempExp
in tempExpList]
763 modelPsfWidthList = [modelPsf.computeBBox(modelPsf.getAveragePosition()).getWidth()
764 for modelPsf
in modelPsfList]
765 psf = modelPsfList[modelPsfWidthList.index(
max(modelPsfWidthList))]
767 psf = measAlg.CoaddPsf(coaddInputs.ccds, coaddExposure.getWcs(),
768 self.config.coaddPsf.makeControl())
769 coaddExposure.setPsf(psf)
770 apCorrMap = measAlg.makeCoaddApCorrMap(coaddInputs.ccds, coaddExposure.getBBox(afwImage.PARENT),
771 coaddExposure.getWcs())
772 coaddExposure.getInfo().setApCorrMap(apCorrMap)
773 if self.config.doAttachTransmissionCurve:
774 transmissionCurve = measAlg.makeCoaddTransmissionCurve(coaddExposure.getWcs(), coaddInputs.ccds)
775 coaddExposure.getInfo().setTransmissionCurve(transmissionCurve)
778 altMaskList, statsFlags, statsCtrl, nImage=None):
779 """Assemble the coadd for a sub-region.
781 For each coaddTempExp, check for (
and swap
in) an alternative mask
782 if one
is passed. Remove mask planes listed
in
783 `config.removeMaskPlanes`. Finally, stack the actual exposures using
784 `lsst.afw.math.statisticsStack`
with the statistic specified by
785 statsFlags. Typically, the statsFlag will be one of lsst.afw.math.MEAN
for
786 a mean-stack
or `lsst.afw.math.MEANCLIP`
for outlier rejection using
787 an N-sigma clipped mean where N
and iterations are specified by
788 statsCtrl. Assign the stacked subregion back to the coadd.
793 The target exposure
for the coadd.
794 bbox : `lsst.geom.Box`
796 tempExpRefList : `list`
797 List of data reference to tempExp.
798 imageScalerList : `list`
799 List of image scalers.
803 List of alternate masks to use rather than those stored
with
804 tempExp,
or None. Each element
is dict
with keys = mask plane
805 name to which to add the spans.
806 statsFlags : `lsst.afw.math.Property`
807 Property object
for statistic
for coadd.
809 Statistics control object
for coadd.
810 nImage : `lsst.afw.image.ImageU`, optional
811 Keeps track of exposure count
for each pixel.
813 self.log.debug("Computing coadd over %s", bbox)
815 coaddExposure.mask.addMaskPlane(
"REJECTED")
816 coaddExposure.mask.addMaskPlane(
"CLIPPED")
817 coaddExposure.mask.addMaskPlane(
"SENSOR_EDGE")
819 clipped = afwImage.Mask.getPlaneBitMask(
"CLIPPED")
821 if nImage
is not None:
822 subNImage = afwImage.ImageU(bbox.getWidth(), bbox.getHeight())
823 for tempExpRef, imageScaler, altMask
in zip(tempExpRefList, imageScalerList, altMaskList):
825 exposure = tempExpRef.get(parameters={
'bbox': bbox})
827 maskedImage = exposure.getMaskedImage()
828 mask = maskedImage.getMask()
829 if altMask
is not None:
831 imageScaler.scaleMaskedImage(maskedImage)
835 if nImage
is not None:
836 subNImage.getArray()[maskedImage.getMask().getArray() & statsCtrl.getAndMask() == 0] += 1
837 if self.config.removeMaskPlanes:
839 maskedImageList.append(maskedImage)
841 if self.config.doInputMap:
842 visit = exposure.getInfo().getCoaddInputs().visits[0].getId()
843 self.inputMapper.mask_warp_bbox(bbox, visit, mask, statsCtrl.getAndMask())
845 with self.timer(
"stack"):
849 coaddExposure.maskedImage.assign(coaddSubregion, bbox)
850 if nImage
is not None:
851 nImage.assign(subNImage, bbox)
854 altMaskList, statsCtrl, nImage=None):
855 """Assemble the coadd using the "online" method.
857 This method takes a running sum of images and weights to save memory.
858 It only works
for MEAN statistics.
863 The target exposure
for the coadd.
864 tempExpRefList : `list`
865 List of data reference to tempExp.
866 imageScalerList : `list`
867 List of image scalers.
871 List of alternate masks to use rather than those stored
with
872 tempExp,
or None. Each element
is dict
with keys = mask plane
873 name to which to add the spans.
875 Statistics control object
for coadd.
876 nImage : `lsst.afw.image.ImageU`, optional
877 Keeps track of exposure count
for each pixel.
879 self.log.debug("Computing online coadd.")
881 coaddExposure.mask.addMaskPlane(
"REJECTED")
882 coaddExposure.mask.addMaskPlane(
"CLIPPED")
883 coaddExposure.mask.addMaskPlane(
"SENSOR_EDGE")
885 thresholdDict = AccumulatorMeanStack.stats_ctrl_to_threshold_dict(statsCtrl)
887 bbox = coaddExposure.maskedImage.getBBox()
890 coaddExposure.image.array.shape,
891 statsCtrl.getAndMask(),
892 mask_threshold_dict=thresholdDict,
894 no_good_pixels_mask=statsCtrl.getNoGoodPixelsMask(),
895 calc_error_from_input_variance=self.config.calcErrorFromInputVariance,
896 compute_n_image=(nImage
is not None)
899 for tempExpRef, imageScaler, altMask, weight
in zip(tempExpRefList,
903 exposure = tempExpRef.get()
904 maskedImage = exposure.getMaskedImage()
905 mask = maskedImage.getMask()
906 if altMask
is not None:
908 imageScaler.scaleMaskedImage(maskedImage)
909 if self.config.removeMaskPlanes:
912 stacker.add_masked_image(maskedImage, weight=weight)
914 if self.config.doInputMap:
915 visit = exposure.getInfo().getCoaddInputs().visits[0].getId()
916 self.inputMapper.mask_warp_bbox(bbox, visit, mask, statsCtrl.getAndMask())
918 stacker.fill_stacked_masked_image(coaddExposure.maskedImage)
920 if nImage
is not None:
921 nImage.array[:, :] = stacker.n_image
924 """Unset the mask of an image for mask planes specified in the config.
929 The masked image to be modified.
933 InvalidParameterError
934 Raised if no mask plane
with that name was found.
936 mask = maskedImage.getMask()
937 for maskPlane
in self.config.removeMaskPlanes:
939 mask &= ~mask.getPlaneBitMask(maskPlane)
941 self.log.debug(
"Unable to remove mask plane %s: no mask plane with that name was found.",
946 """Map certain mask planes of the warps to new planes for the coadd.
948 If a pixel is rejected due to a mask value other than EDGE, NO_DATA,
949 or CLIPPED, set it to REJECTED on the coadd.
950 If a pixel
is rejected due to EDGE, set the coadd pixel to SENSOR_EDGE.
951 If a pixel
is rejected due to CLIPPED, set the coadd pixel to CLIPPED.
956 Statistics control object
for coadd.
960 maskMap : `list` of `tuple` of `int`
961 A list of mappings of mask planes of the warped exposures to
962 mask planes of the coadd.
964 edge = afwImage.Mask.getPlaneBitMask("EDGE")
965 noData = afwImage.Mask.getPlaneBitMask(
"NO_DATA")
966 clipped = afwImage.Mask.getPlaneBitMask(
"CLIPPED")
967 toReject = statsCtrl.getAndMask() & (~noData) & (~edge) & (~clipped)
968 maskMap = [(toReject, afwImage.Mask.getPlaneBitMask(
"REJECTED")),
969 (edge, afwImage.Mask.getPlaneBitMask(
"SENSOR_EDGE")),
974 """Apply in place alt mask formatted as SpanSets to a mask.
980 altMaskSpans : `dict`
981 SpanSet lists to apply. Each element contains the new mask
982 plane name (e.g. "CLIPPED and/or "NO_DATA
") as the key,
983 and list of SpanSets to apply to the mask.
990 if self.config.doUsePsfMatchedPolygons:
991 if (
"NO_DATA" in altMaskSpans)
and (
"NO_DATA" in self.config.badMaskPlanes):
996 for spanSet
in altMaskSpans[
'NO_DATA']:
997 spanSet.clippedTo(mask.getBBox()).clearMask(mask, self.
getBadPixelMask())
999 for plane, spanSetList
in altMaskSpans.items():
1000 maskClipValue = mask.addMaskPlane(plane)
1001 for spanSet
in spanSetList:
1002 spanSet.clippedTo(mask.getBBox()).setMask(mask, 2**maskClipValue)
1006 """Shrink coaddInputs' ccds' ValidPolygons in place.
1008 Either modify each ccd's validPolygon in place, or if CoaddInputs
1009 does not have a validPolygon, create one
from its bbox.
1013 coaddInputs : `lsst.afw.image.coaddInputs`
1016 for ccd
in coaddInputs.ccds:
1017 polyOrig = ccd.getValidPolygon()
1018 validPolyBBox = polyOrig.getBBox()
if polyOrig
else ccd.getBBox()
1019 validPolyBBox.grow(-self.config.matchingKernelSize//2)
1021 validPolygon = polyOrig.intersectionSingle(validPolyBBox)
1023 validPolygon = afwGeom.polygon.Polygon(
geom.Box2D(validPolyBBox))
1024 ccd.setValidPolygon(validPolygon)
1027 """Set the bright object masks.
1032 Exposure under consideration.
1034 Table of bright objects to mask.
1035 dataId : `lsst.daf.butler.DataId`, optional
1036 Data identifier dict for patch.
1038 if brightObjectMasks
is None:
1039 self.log.warning(
"Unable to apply bright object mask: none supplied")
1041 self.log.info(
"Applying %d bright object masks to %s", len(brightObjectMasks), dataId)
1042 mask = exposure.getMaskedImage().getMask()
1043 wcs = exposure.getWcs()
1044 plateScale = wcs.getPixelScale().asArcseconds()
1046 for rec
in brightObjectMasks:
1047 center =
geom.PointI(wcs.skyToPixel(rec.getCoord()))
1048 if rec[
"type"] ==
"box":
1049 assert rec[
"angle"] == 0.0, (
"Angle != 0 for mask object %s" % rec[
"id"])
1050 width = rec[
"width"].asArcseconds()/plateScale
1051 height = rec[
"height"].asArcseconds()/plateScale
1054 bbox =
geom.Box2I(center - halfSize, center + halfSize)
1057 geom.PointI(int(center[0] + 0.5*width), int(center[1] + 0.5*height)))
1059 elif rec[
"type"] ==
"circle":
1060 radius = int(rec[
"radius"].asArcseconds()/plateScale)
1061 spans = afwGeom.SpanSet.fromShape(radius, offset=center)
1063 self.log.warning(
"Unexpected region type %s at %s", rec[
"type"], center)
1068 """Set INEXACT_PSF mask plane.
1070 If any of the input images isn't represented in the coadd (due to
1071 clipped pixels or chip gaps), the `CoaddPsf` will be inexact. Flag
1077 Coadded exposure
's mask, modified in-place.
1079 mask.addMaskPlane("INEXACT_PSF")
1080 inexactPsf = mask.getPlaneBitMask(
"INEXACT_PSF")
1081 sensorEdge = mask.getPlaneBitMask(
"SENSOR_EDGE")
1082 clipped = mask.getPlaneBitMask(
"CLIPPED")
1083 rejected = mask.getPlaneBitMask(
"REJECTED")
1084 array = mask.getArray()
1085 selected = array & (sensorEdge | clipped | rejected) > 0
1086 array[selected] |= inexactPsf
1089 def _subBBoxIter(bbox, subregionSize):
1090 """Iterate over subregions of a bbox.
1095 Bounding box over which to iterate.
1102 Next sub-bounding box of size ``subregionSize`` or smaller; each ``subBBox``
1103 is contained within ``bbox``, so it may be smaller than ``subregionSize`` at
1104 the edges of ``bbox``, but it will never be empty.
1109 Raised
if any of the following occur:
1110 - The given bbox
is empty.
1111 - The subregionSize
is 0.
1114 raise RuntimeError(
"bbox %s is empty" % (bbox,))
1115 if subregionSize[0] < 1
or subregionSize[1] < 1:
1116 raise RuntimeError(
"subregionSize %s must be nonzero" % (subregionSize,))
1118 for rowShift
in range(0, bbox.getHeight(), subregionSize[1]):
1119 for colShift
in range(0, bbox.getWidth(), subregionSize[0]):
1122 if subBBox.isEmpty():
1123 raise RuntimeError(
"Bug: empty bbox! bbox=%s, subregionSize=%s, "
1124 "colShift=%s, rowShift=%s" %
1125 (bbox, subregionSize, colShift, rowShift))
1129 """Return list of only inputRefs with visitId in goodVisits ordered by goodVisit.
1133 inputs : `list` of `~lsst.pipe.base.connections.DeferredDatasetRef`
1134 List of `lsst.pipe.base.connections.DeferredDatasetRef` with dataId containing visit.
1136 Dictionary
with good visitIds
as the keys. Value ignored.
1140 filteredInputs : `list` of `~lsst.pipe.base.connections.DeferredDatasetRef`
1141 Filtered
and sorted list of inputRefs
with visitId
in goodVisits ordered by goodVisit.
1143 inputWarpDict = {inputRef.ref.dataId['visit']: inputRef
for inputRef
in inputs}
1145 for visit
in goodVisits.keys():
1146 if visit
in inputWarpDict:
1147 filteredInputs.append(inputWarpDict[visit])
1148 return filteredInputs
1152 """Function to count the number of pixels with a specific mask in a
1155 Find the intersection of mask & footprint. Count all pixels in the mask
1156 that are
in the intersection that have bitmask set but do
not have
1157 ignoreMask set. Return the count.
1162 Mask to define intersection region by.
1164 Footprint to define the intersection region by.
1166 Specific mask that we wish to count the number of occurances of.
1167 ignoreMask : `Unknown`
1168 Pixels to
not consider.
1173 Number of pixels
in footprint
with specified mask.
1175 bbox = footprint.getBBox()
1176 bbox.clip(mask.getBBox(afwImage.PARENT))
1178 subMask = mask.Factory(mask, bbox, afwImage.PARENT)
1179 footprint.spans.setMask(fp, bitmask)
1180 return numpy.logical_and((subMask.getArray() & fp.getArray()) > 0,
1181 (subMask.getArray() & ignoreMask) == 0).sum()
1185 psfMatchedWarps = pipeBase.connectionTypes.Input(
1186 doc=(
"PSF-Matched Warps are required by CompareWarp regardless of the coadd type requested. "
1187 "Only PSF-Matched Warps make sense for image subtraction. "
1188 "Therefore, they must be an additional declared input."),
1189 name=
"{inputCoaddName}Coadd_psfMatchedWarp",
1190 storageClass=
"ExposureF",
1191 dimensions=(
"tract",
"patch",
"skymap",
"visit"),
1195 templateCoadd = pipeBase.connectionTypes.Output(
1196 doc=(
"Model of the static sky, used to find temporal artifacts. Typically a PSF-Matched, "
1197 "sigma-clipped coadd. Written if and only if assembleStaticSkyModel.doWrite=True"),
1198 name=
"{outputCoaddName}CoaddPsfMatched",
1199 storageClass=
"ExposureF",
1200 dimensions=(
"tract",
"patch",
"skymap",
"band"),
1205 if not config.assembleStaticSkyModel.doWrite:
1206 self.outputs.remove(
"templateCoadd")
1211 pipelineConnections=CompareWarpAssembleCoaddConnections):
1212 assembleStaticSkyModel = pexConfig.ConfigurableField(
1213 target=AssembleCoaddTask,
1214 doc=
"Task to assemble an artifact-free, PSF-matched Coadd to serve as a"
1215 " naive/first-iteration model of the static sky.",
1217 detect = pexConfig.ConfigurableField(
1218 target=SourceDetectionTask,
1219 doc=
"Detect outlier sources on difference between each psfMatched warp and static sky model"
1221 detectTemplate = pexConfig.ConfigurableField(
1222 target=SourceDetectionTask,
1223 doc=
"Detect sources on static sky model. Only used if doPreserveContainedBySource is True"
1225 maskStreaks = pexConfig.ConfigurableField(
1226 target=MaskStreaksTask,
1227 doc=
"Detect streaks on difference between each psfMatched warp and static sky model. Only used if "
1228 "doFilterMorphological is True. Adds a mask plane to an exposure, with the mask plane name set by"
1231 streakMaskName = pexConfig.Field(
1234 doc=
"Name of mask bit used for streaks"
1236 maxNumEpochs = pexConfig.Field(
1237 doc=
"Charactistic maximum local number of epochs/visits in which an artifact candidate can appear "
1238 "and still be masked. The effective maxNumEpochs is a broken linear function of local "
1239 "number of epochs (N): min(maxFractionEpochsLow*N, maxNumEpochs + maxFractionEpochsHigh*N). "
1240 "For each footprint detected on the image difference between the psfMatched warp and static sky "
1241 "model, if a significant fraction of pixels (defined by spatialThreshold) are residuals in more "
1242 "than the computed effective maxNumEpochs, the artifact candidate is deemed persistant rather "
1243 "than transient and not masked.",
1247 maxFractionEpochsLow = pexConfig.RangeField(
1248 doc=
"Fraction of local number of epochs (N) to use as effective maxNumEpochs for low N. "
1249 "Effective maxNumEpochs = "
1250 "min(maxFractionEpochsLow * N, maxNumEpochs + maxFractionEpochsHigh * N)",
1255 maxFractionEpochsHigh = pexConfig.RangeField(
1256 doc=
"Fraction of local number of epochs (N) to use as effective maxNumEpochs for high N. "
1257 "Effective maxNumEpochs = "
1258 "min(maxFractionEpochsLow * N, maxNumEpochs + maxFractionEpochsHigh * N)",
1263 spatialThreshold = pexConfig.RangeField(
1264 doc=
"Unitless fraction of pixels defining how much of the outlier region has to meet the "
1265 "temporal criteria. If 0, clip all. If 1, clip none.",
1269 inclusiveMin=
True, inclusiveMax=
True
1271 doScaleWarpVariance = pexConfig.Field(
1272 doc=
"Rescale Warp variance plane using empirical noise?",
1276 scaleWarpVariance = pexConfig.ConfigurableField(
1277 target=ScaleVarianceTask,
1278 doc=
"Rescale variance on warps",
1280 doPreserveContainedBySource = pexConfig.Field(
1281 doc=
"Rescue artifacts from clipping that completely lie within a footprint detected"
1282 "on the PsfMatched Template Coadd. Replicates a behavior of SafeClip.",
1286 doPrefilterArtifacts = pexConfig.Field(
1287 doc=
"Ignore artifact candidates that are mostly covered by the bad pixel mask, "
1288 "because they will be excluded anyway. This prevents them from contributing "
1289 "to the outlier epoch count image and potentially being labeled as persistant."
1290 "'Mostly' is defined by the config 'prefilterArtifactsRatio'.",
1294 prefilterArtifactsMaskPlanes = pexConfig.ListField(
1295 doc=
"Prefilter artifact candidates that are mostly covered by these bad mask planes.",
1297 default=(
'NO_DATA',
'BAD',
'SAT',
'SUSPECT'),
1299 prefilterArtifactsRatio = pexConfig.Field(
1300 doc=
"Prefilter artifact candidates with less than this fraction overlapping good pixels",
1304 doFilterMorphological = pexConfig.Field(
1305 doc=
"Filter artifact candidates based on morphological criteria, i.g. those that appear to "
1310 growStreakFp = pexConfig.Field(
1311 doc=
"Grow streak footprints by this number multiplied by the PSF width",
1317 AssembleCoaddConfig.setDefaults(self)
1323 if "EDGE" in self.badMaskPlanes:
1324 self.badMaskPlanes.remove(
'EDGE')
1325 self.removeMaskPlanes.append(
'EDGE')
1334 self.
detect.doTempLocalBackground =
False
1335 self.
detect.reEstimateBackground =
False
1336 self.
detect.returnOriginalFootprints =
False
1337 self.
detect.thresholdPolarity =
"both"
1338 self.
detect.thresholdValue = 5
1339 self.
detect.minPixels = 4
1340 self.
detect.isotropicGrow =
True
1341 self.
detect.thresholdType =
"pixel_stdev"
1342 self.
detect.nSigmaToGrow = 0.4
1353 raise ValueError(
"No dataset type exists for a PSF-Matched Template N Image."
1354 "Please set assembleStaticSkyModel.doNImage=False")
1357 raise ValueError(
"warpType (%s) == assembleStaticSkyModel.warpType (%s) and will compete for "
1358 "the same dataset name. Please set assembleStaticSkyModel.doWrite to False "
1359 "or warpType to 'direct'. assembleStaticSkyModel.warpType should ways be "
1364 """Assemble a compareWarp coadded image from a set of warps
1365 by masking artifacts detected by comparing PSF-matched warps.
1367 In ``AssembleCoaddTask``, we compute the coadd as an clipped mean (i.e.,
1368 we clip outliers). The problem
with doing this
is that when computing the
1369 coadd PSF at a given location, individual visit PSFs
from visits
with
1370 outlier pixels contribute to the coadd PSF
and cannot be treated correctly.
1371 In this task, we correct
for this behavior by creating a new badMaskPlane
1372 'CLIPPED' which marks pixels
in the individual warps suspected to contain
1373 an artifact. We populate this plane on the input warps by comparing
1374 PSF-matched warps
with a PSF-matched median coadd which serves
as a
1375 model of the static sky. Any group of pixels that deviates
from the
1376 PSF-matched template coadd by more than config.detect.threshold sigma,
1377 is an artifact candidate. The candidates are then filtered to remove
1378 variable sources
and sources that are difficult to subtract such
as
1379 bright stars. This filter
is configured using the config parameters
1380 ``temporalThreshold``
and ``spatialThreshold``. The temporalThreshold
is
1381 the maximum fraction of epochs that the deviation can appear
in and still
1382 be considered an artifact. The spatialThreshold
is the maximum fraction of
1383 pixels
in the footprint of the deviation that appear
in other epochs
1384 (where other epochs
is defined by the temporalThreshold). If the deviant
1385 region meets this criteria of having a significant percentage of pixels
1386 that deviate
in only a few epochs, these pixels have the
'CLIPPED' bit
1387 set
in the mask. These regions will
not contribute to the final coadd.
1388 Furthermore, any routine to determine the coadd PSF can now be cognizant
1389 of clipped regions. Note that the algorithm implemented by this task
is
1390 preliminary
and works correctly
for HSC data. Parameter modifications
and
1391 or considerable redesigning of the algorithm
is likley required
for other
1394 ``CompareWarpAssembleCoaddTask`` sub-classes
1395 ``AssembleCoaddTask``
and instantiates ``AssembleCoaddTask``
1396 as a subtask to generate the TemplateCoadd (the model of the static sky).
1401 This task supports the following debug variables:
1403 If
True then save the Epoch Count Image
as a fits file
in the `figPath`
1405 Path to save the debug fits images
and figures
1408 ConfigClass = CompareWarpAssembleCoaddConfig
1409 _DefaultName = "compareWarpAssembleCoadd"
1411 def __init__(self, *args, **kwargs):
1412 AssembleCoaddTask.__init__(self, *args, **kwargs)
1413 self.makeSubtask(
"assembleStaticSkyModel")
1414 detectionSchema = afwTable.SourceTable.makeMinimalSchema()
1415 self.makeSubtask(
"detect", schema=detectionSchema)
1416 if self.config.doPreserveContainedBySource:
1417 self.makeSubtask(
"detectTemplate", schema=afwTable.SourceTable.makeMinimalSchema())
1418 if self.config.doScaleWarpVariance:
1419 self.makeSubtask(
"scaleWarpVariance")
1420 if self.config.doFilterMorphological:
1421 self.makeSubtask(
"maskStreaks")
1423 @utils.inheritDoc(AssembleCoaddTask)
1424 def _makeSupplementaryData(self, butlerQC, inputRefs, outputRefs):
1425 """Generate a templateCoadd to use as a naive model of static sky to
1426 subtract from PSF-Matched warps.
1430 result : `~lsst.pipe.base.Struct`
1431 Results
as a struct
with attributes:
1436 Keeps track of exposure count
for each pixel (`lsst.afw.image.ImageU`).
1441 Raised
if ``templateCoadd``
is `
None`.
1444 staticSkyModelInputRefs = copy.deepcopy(inputRefs)
1445 staticSkyModelInputRefs.inputWarps = inputRefs.psfMatchedWarps
1449 staticSkyModelOutputRefs = copy.deepcopy(outputRefs)
1450 if self.config.assembleStaticSkyModel.doWrite:
1451 staticSkyModelOutputRefs.coaddExposure = staticSkyModelOutputRefs.templateCoadd
1454 del outputRefs.templateCoadd
1455 del staticSkyModelOutputRefs.templateCoadd
1458 if 'nImage' in staticSkyModelOutputRefs.keys():
1459 del staticSkyModelOutputRefs.nImage
1461 templateCoadd = self.assembleStaticSkyModel.
runQuantum(butlerQC, staticSkyModelInputRefs,
1462 staticSkyModelOutputRefs)
1463 if templateCoadd
is None:
1466 return pipeBase.Struct(templateCoadd=templateCoadd.coaddExposure,
1467 nImage=templateCoadd.nImage,
1468 warpRefList=templateCoadd.warpRefList,
1469 imageScalerList=templateCoadd.imageScalerList,
1470 weightList=templateCoadd.weightList)
1472 def _noTemplateMessage(self, warpType):
1473 warpName = (warpType[0].upper() + warpType[1:])
1474 message =
"""No %(warpName)s warps were found to build the template coadd which is
1475 required to run CompareWarpAssembleCoaddTask. To continue assembling this type of coadd,
1476 first either rerun makeCoaddTempExp
with config.make%(warpName)s=
True or
1477 coaddDriver
with config.makeCoadTempExp.make%(warpName)s=
True, before assembleCoadd.
1479 Alternatively, to use another algorithm
with existing warps, retarget the CoaddDriverConfig to
1480 another algorithm like:
1483 config.assemble.retarget(SafeClipAssembleCoaddTask)
1484 """ % {"warpName": warpName}
1487 @utils.inheritDoc(AssembleCoaddTask)
1489 def run(self, skyInfo, tempExpRefList, imageScalerList, weightList,
1491 """Assemble the coadd.
1493 Find artifacts and apply them to the warps
' masks creating a list of
1494 alternative masks with a new
"CLIPPED" plane
and updated
"NO_DATA"
1495 plane. Then
pass these alternative masks to the base
class's ``run``
1501 dataIds = [ref.dataId
for ref
in tempExpRefList]
1502 psfMatchedDataIds = [ref.dataId
for ref
in supplementaryData.warpRefList]
1504 if dataIds != psfMatchedDataIds:
1505 self.log.info(
"Reordering and or/padding PSF-matched visit input list")
1506 supplementaryData.warpRefList = reorderAndPadList(supplementaryData.warpRefList,
1507 psfMatchedDataIds, dataIds)
1508 supplementaryData.imageScalerList = reorderAndPadList(supplementaryData.imageScalerList,
1509 psfMatchedDataIds, dataIds)
1512 spanSetMaskList = self.
findArtifacts(supplementaryData.templateCoadd,
1513 supplementaryData.warpRefList,
1514 supplementaryData.imageScalerList)
1516 badMaskPlanes = self.config.badMaskPlanes[:]
1517 badMaskPlanes.append(
"CLIPPED")
1518 badPixelMask = afwImage.Mask.getPlaneBitMask(badMaskPlanes)
1520 result = AssembleCoaddTask.run(self, skyInfo, tempExpRefList, imageScalerList, weightList,
1521 spanSetMaskList, mask=badPixelMask)
1525 self.
applyAltEdgeMask(result.coaddExposure.maskedImage.mask, spanSetMaskList)
1529 """Propagate alt EDGE mask to SENSOR_EDGE AND INEXACT_PSF planes.
1535 altMaskList : `list` of `dict`
1536 List of Dicts containing ``spanSet`` lists.
1537 Each element contains the new mask plane name (e.g. "CLIPPED
1538 and/
or "NO_DATA")
as the key,
and list of ``SpanSets`` to apply to
1541 maskValue = mask.getPlaneBitMask(["SENSOR_EDGE",
"INEXACT_PSF"])
1542 for visitMask
in altMaskList:
1543 if "EDGE" in visitMask:
1544 for spanSet
in visitMask[
'EDGE']:
1545 spanSet.clippedTo(mask.getBBox()).setMask(mask, maskValue)
1550 Loop through warps twice. The first loop builds a map with the count
1551 of how many epochs each pixel deviates
from the templateCoadd by more
1552 than ``config.chiThreshold`` sigma. The second loop takes each
1553 difference image
and filters the artifacts detected
in each using
1554 count map to filter out variable sources
and sources that are
1555 difficult to subtract cleanly.
1560 Exposure to serve
as model of static sky.
1561 tempExpRefList : `list`
1562 List of data references to warps.
1563 imageScalerList : `list`
1564 List of image scalers.
1568 altMasks : `list` of `dict`
1569 List of dicts containing information about CLIPPED
1570 (i.e., artifacts), NO_DATA,
and EDGE pixels.
1572 self.log.debug("Generating Count Image, and mask lists.")
1573 coaddBBox = templateCoadd.getBBox()
1574 slateIm = afwImage.ImageU(coaddBBox)
1575 epochCountImage = afwImage.ImageU(coaddBBox)
1576 nImage = afwImage.ImageU(coaddBBox)
1577 spanSetArtifactList = []
1578 spanSetNoDataMaskList = []
1579 spanSetEdgeList = []
1580 spanSetBadMorphoList = []
1584 templateCoadd.mask.clearAllMaskPlanes()
1586 if self.config.doPreserveContainedBySource:
1587 templateFootprints = self.detectTemplate.detectFootprints(templateCoadd)
1589 templateFootprints =
None
1591 for warpRef, imageScaler
in zip(tempExpRefList, imageScalerList):
1593 if warpDiffExp
is not None:
1595 nImage.array += (numpy.isfinite(warpDiffExp.image.array)
1596 * ((warpDiffExp.mask.array & badPixelMask) == 0)).astype(numpy.uint16)
1597 fpSet = self.detect.detectFootprints(warpDiffExp, doSmooth=
False, clearMask=
True)
1598 fpSet.positive.merge(fpSet.negative)
1599 footprints = fpSet.positive
1601 spanSetList = [footprint.spans
for footprint
in footprints.getFootprints()]
1604 if self.config.doPrefilterArtifacts:
1608 self.detect.clearMask(warpDiffExp.mask)
1609 for spans
in spanSetList:
1610 spans.setImage(slateIm, 1, doClip=
True)
1611 spans.setMask(warpDiffExp.mask, warpDiffExp.mask.getPlaneBitMask(
"DETECTED"))
1612 epochCountImage += slateIm
1614 if self.config.doFilterMorphological:
1615 maskName = self.config.streakMaskName
1616 _ = self.maskStreaks.run(warpDiffExp)
1617 streakMask = warpDiffExp.mask
1618 spanSetStreak = afwGeom.SpanSet.fromMask(streakMask,
1619 streakMask.getPlaneBitMask(maskName)).split()
1621 psf = warpDiffExp.getPsf()
1622 for s, sset
in enumerate(spanSetStreak):
1623 psfShape = psf.computeShape(sset.computeCentroid())
1624 dilation = self.config.growStreakFp * psfShape.getDeterminantRadius()
1625 sset_dilated = sset.dilated(int(dilation))
1626 spanSetStreak[s] = sset_dilated
1632 nans = numpy.where(numpy.isnan(warpDiffExp.maskedImage.image.array), 1, 0)
1634 nansMask.setXY0(warpDiffExp.getXY0())
1635 edgeMask = warpDiffExp.mask
1636 spanSetEdgeMask = afwGeom.SpanSet.fromMask(edgeMask,
1637 edgeMask.getPlaneBitMask(
"EDGE")).split()
1641 nansMask = afwImage.MaskX(coaddBBox, 1)
1643 spanSetEdgeMask = []
1646 spanSetNoDataMask = afwGeom.SpanSet.fromMask(nansMask).split()
1648 spanSetNoDataMaskList.append(spanSetNoDataMask)
1649 spanSetArtifactList.append(spanSetList)
1650 spanSetEdgeList.append(spanSetEdgeMask)
1651 if self.config.doFilterMorphological:
1652 spanSetBadMorphoList.append(spanSetStreak)
1655 path = self._dataRef2DebugPath(
"epochCountIm", tempExpRefList[0], coaddLevel=
True)
1656 epochCountImage.writeFits(path)
1658 for i, spanSetList
in enumerate(spanSetArtifactList):
1660 filteredSpanSetList = self.
filterArtifacts(spanSetList, epochCountImage, nImage,
1662 spanSetArtifactList[i] = filteredSpanSetList
1663 if self.config.doFilterMorphological:
1664 spanSetArtifactList[i] += spanSetBadMorphoList[i]
1667 for artifacts, noData, edge
in zip(spanSetArtifactList, spanSetNoDataMaskList, spanSetEdgeList):
1668 altMasks.append({
'CLIPPED': artifacts,
1674 """Remove artifact candidates covered by bad mask plane.
1676 Any future editing of the candidate list that does not depend on
1677 temporal information should go
in this method.
1682 List of SpanSets representing artifact candidates.
1684 Exposure containing mask planes used to prefilter.
1689 List of SpanSets
with artifacts.
1691 badPixelMask = exp.mask.getPlaneBitMask(self.config.prefilterArtifactsMaskPlanes)
1692 goodArr = (exp.mask.array & badPixelMask) == 0
1693 returnSpanSetList = []
1694 bbox = exp.getBBox()
1695 x0, y0 = exp.getXY0()
1696 for i, span
in enumerate(spanSetList):
1697 y, x = span.clippedTo(bbox).indices()
1698 yIndexLocal = numpy.array(y) - y0
1699 xIndexLocal = numpy.array(x) - x0
1700 goodRatio = numpy.count_nonzero(goodArr[yIndexLocal, xIndexLocal])/span.getArea()
1701 if goodRatio > self.config.prefilterArtifactsRatio:
1702 returnSpanSetList.append(span)
1703 return returnSpanSetList
1705 def filterArtifacts(self, spanSetList, epochCountImage, nImage, footprintsToExclude=None):
1706 """Filter artifact candidates.
1711 List of SpanSets representing artifact candidates.
1713 Image of accumulated number of warpDiff detections.
1714 nImage : `lsst.afw.image.ImageU`
1715 Image of the accumulated number of total epochs contributing.
1719 maskSpanSetList : `list`
1720 List of SpanSets with artifacts.
1722 maskSpanSetList = []
1723 x0, y0 = epochCountImage.getXY0()
1724 for i, span
in enumerate(spanSetList):
1725 y, x = span.indices()
1726 yIdxLocal = [y1 - y0
for y1
in y]
1727 xIdxLocal = [x1 - x0
for x1
in x]
1728 outlierN = epochCountImage.array[yIdxLocal, xIdxLocal]
1729 totalN = nImage.array[yIdxLocal, xIdxLocal]
1732 effMaxNumEpochsHighN = (self.config.maxNumEpochs
1733 + self.config.maxFractionEpochsHigh*numpy.mean(totalN))
1734 effMaxNumEpochsLowN = self.config.maxFractionEpochsLow * numpy.mean(totalN)
1735 effectiveMaxNumEpochs = int(
min(effMaxNumEpochsLowN, effMaxNumEpochsHighN))
1736 nPixelsBelowThreshold = numpy.count_nonzero((outlierN > 0)
1737 & (outlierN <= effectiveMaxNumEpochs))
1738 percentBelowThreshold = nPixelsBelowThreshold / len(outlierN)
1739 if percentBelowThreshold > self.config.spatialThreshold:
1740 maskSpanSetList.append(span)
1742 if self.config.doPreserveContainedBySource
and footprintsToExclude
is not None:
1744 filteredMaskSpanSetList = []
1745 for span
in maskSpanSetList:
1747 for footprint
in footprintsToExclude.positive.getFootprints():
1748 if footprint.spans.contains(span):
1752 filteredMaskSpanSetList.append(span)
1753 maskSpanSetList = filteredMaskSpanSetList
1755 return maskSpanSetList
1757 def _readAndComputeWarpDiff(self, warpRef, imageScaler, templateCoadd):
1758 """Fetch a warp from the butler and return a warpDiff.
1762 warpRef : `lsst.daf.butler.DeferredDatasetHandle`
1763 Handle for the warp.
1765 An image scaler object.
1767 Exposure to be substracted
from the scaled warp.
1772 Exposure of the image difference between the warp
and template.
1779 warp = warpRef.get()
1781 imageScaler.scaleMaskedImage(warp.getMaskedImage())
1782 mi = warp.getMaskedImage()
1783 if self.config.doScaleWarpVariance:
1785 self.scaleWarpVariance.run(mi)
1786 except Exception
as exc:
1787 self.log.warning(
"Unable to rescale variance of warp (%s); leaving it as-is", exc)
1788 mi -= templateCoadd.getMaskedImage()
A compact representation of a collection of pixels.
A class to contain the data, WCS, and other information needed to describe an image of the sky.
A group of labels for a filter in an exposure or coadd.
A class to represent a 2-dimensional array of pixels.
Represent a 2-dimensional array of bitmask pixels.
A class to manipulate images, masks, and variance as a single object.
Pass parameters to a Statistics object.
A floating-point coordinate rectangle geometry.
An integer coordinate rectangle.
Reports invalid arguments.
def shrinkValidPolygons(self, coaddInputs)
def setRejectedMaskMapping(statsCtrl)
def assembleMetadata(self, coaddExposure, tempExpRefList, weightList)
def assembleOnlineMeanCoadd(self, coaddExposure, tempExpRefList, imageScalerList, weightList, altMaskList, statsCtrl, nImage=None)
def runQuantum(self, butlerQC, inputRefs, outputRefs)
def _makeSupplementaryData(self, butlerQC, inputRefs, outputRefs)
def processResults(self, coaddExposure, brightObjectMasks=None, dataId=None)
def prepareInputs(self, refList)
def setBrightObjectMasks(self, exposure, brightObjectMasks, dataId=None)
def removeMaskPlanes(self, maskedImage)
def _subBBoxIter(bbox, subregionSize)
def assembleSubregion(self, coaddExposure, bbox, tempExpRefList, imageScalerList, weightList, altMaskList, statsFlags, statsCtrl, nImage=None)
def run(self, skyInfo, tempExpRefList, imageScalerList, weightList, altMaskList=None, mask=None, supplementaryData=None)
def applyAltMaskPlanes(self, mask, altMaskSpans)
def makeSupplementaryDataGen3(self, butlerQC, inputRefs, outputRefs)
def filterWarps(self, inputs, goodVisits)
def prepareStats(self, mask=None)
def setInexactPsf(self, mask)
def __init__(self, *config=None)
def prefilterArtifacts(self, spanSetList, exp)
def findArtifacts(self, templateCoadd, tempExpRefList, imageScalerList)
def _readAndComputeWarpDiff(self, warpRef, imageScaler, templateCoadd)
def applyAltEdgeMask(self, mask, altMaskList)
def _noTemplateMessage(self, warpType)
def filterArtifacts(self, spanSetList, epochCountImage, nImage, footprintsToExclude=None)
def getTempExpDatasetName(self, warpType="direct")
def getBadPixelMask(self)
std::shared_ptr< lsst::afw::image::Image< PixelT > > statisticsStack(std::vector< std::shared_ptr< lsst::afw::image::Image< PixelT > > > &images, Property flags, StatisticsControl const &sctrl=StatisticsControl(), std::vector< lsst::afw::image::VariancePixel > const &wvector=std::vector< lsst::afw::image::VariancePixel >(0))
A function to compute some statistics of a stack of Images.
Statistics makeStatistics(lsst::afw::image::Image< Pixel > const &img, lsst::afw::image::Mask< image::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl=StatisticsControl())
Handle a watered-down front-end to the constructor (no variance)
Property stringToStatisticsProperty(std::string const property)
Conversion function to switch a string to a Property (see Statistics.h)
void setCoaddEdgeBits(lsst::afw::image::Mask< lsst::afw::image::MaskPixel > &coaddMask, lsst::afw::image::Image< WeightPixelT > const &weightMap)
set edge bits of coadd mask based on weight map
def countMaskFromFootprint(mask, footprint, bitmask, ignoreMask)