lsst.pipe.tasks  14.0-19-g29094cd5+1
assembleCoadd.py
Go to the documentation of this file.
1 from __future__ import absolute_import, division, print_function
2 from builtins import zip
3 from builtins import range
4 #
5 # LSST Data Management System
6 # Copyright 2008-2016 AURA/LSST.
7 #
8 # This product includes software developed by the
9 # LSST Project (http://www.lsst.org/).
10 #
11 # This program is free software: you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation, either version 3 of the License, or
14 # (at your option) any later version.
15 #
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
20 #
21 # You should have received a copy of the LSST License Statement and
22 # the GNU General Public License along with this program. If not,
23 # see <https://www.lsstcorp.org/LegalNotices/>.
24 #
25 import os
26 import numpy
27 import lsst.pex.config as pexConfig
28 import lsst.pex.exceptions as pexExceptions
29 import lsst.afw.geom as afwGeom
30 import lsst.afw.image as afwImage
31 import lsst.afw.math as afwMath
32 import lsst.afw.table as afwTable
33 import lsst.afw.detection as afwDet
34 import lsst.coadd.utils as coaddUtils
35 import lsst.pipe.base as pipeBase
36 import lsst.meas.algorithms as measAlg
37 import lsst.log as log
38 import lsstDebug
39 from .coaddBase import CoaddBaseTask, SelectDataIdContainer
40 from .interpImage import InterpImageTask
41 from .matchBackgrounds import MatchBackgroundsTask
42 from .scaleZeroPoint import ScaleZeroPointTask
43 from .coaddHelpers import groupPatchExposures, getGroupDataRef
44 from lsst.meas.algorithms import SourceDetectionTask
45 
46 __all__ = ["AssembleCoaddTask", "SafeClipAssembleCoaddTask", "CompareWarpAssembleCoaddTask"]
47 
48 
49 class AssembleCoaddConfig(CoaddBaseTask.ConfigClass):
50  """!
51 \anchor AssembleCoaddConfig_
52 
53 \brief Configuration parameters for the \ref AssembleCoaddTask_ "AssembleCoaddTask"
54  """
55  warpType = pexConfig.Field(
56  doc="Warp name: one of 'direct' or 'psfMatched'",
57  dtype=str,
58  default="direct",
59  )
60  subregionSize = pexConfig.ListField(
61  dtype=int,
62  doc="Width, height of stack subregion size; "
63  "make small enough that a full stack of images will fit into memory at once.",
64  length=2,
65  default=(2000, 2000),
66  )
67  statistic = pexConfig.Field(
68  dtype=str,
69  doc="Main stacking statistic for aggregating over the epochs.",
70  default="MEANCLIP",
71  )
72  doSigmaClip = pexConfig.Field(
73  dtype=bool,
74  doc="Perform sigma clipped outlier rejection with MEANCLIP statistic? (DEPRECATED)",
75  default=False,
76  )
77  sigmaClip = pexConfig.Field(
78  dtype=float,
79  doc="Sigma for outlier rejection; ignored if non-clipping statistic selected.",
80  default=3.0,
81  )
82  clipIter = pexConfig.Field(
83  dtype=int,
84  doc="Number of iterations of outlier rejection; ignored if non-clipping statistic selected.",
85  default=2,
86  )
87  scaleZeroPoint = pexConfig.ConfigurableField(
88  target=ScaleZeroPointTask,
89  doc="Task to adjust the photometric zero point of the coadd temp exposures",
90  )
91  doInterp = pexConfig.Field(
92  doc="Interpolate over NaN pixels? Also extrapolate, if necessary, but the results are ugly.",
93  dtype=bool,
94  default=True,
95  )
96  interpImage = pexConfig.ConfigurableField(
97  target=InterpImageTask,
98  doc="Task to interpolate (and extrapolate) over NaN pixels",
99  )
100  matchBackgrounds = pexConfig.ConfigurableField(
101  target=MatchBackgroundsTask,
102  doc="Task to match backgrounds",
103  )
104  maxMatchResidualRatio = pexConfig.Field(
105  doc="Maximum ratio of the mean squared error of the background matching model to the variance "
106  "of the difference in backgrounds",
107  dtype=float,
108  default=1.1
109  )
110  maxMatchResidualRMS = pexConfig.Field(
111  doc="Maximum RMS of residuals of the background offset fit in matchBackgrounds.",
112  dtype=float,
113  default=1.0
114  )
115  doWrite = pexConfig.Field(
116  doc="Persist coadd?",
117  dtype=bool,
118  default=True,
119  )
120  doNImage = pexConfig.Field(
121  doc="Create image of number of contributing exposures for each pixel",
122  dtype=bool,
123  default=False,
124  )
125  doMatchBackgrounds = pexConfig.Field(
126  doc="Match backgrounds of coadd temp exposures before coadding them? "
127  "If False, the coadd temp expsosures must already have been background subtracted or matched",
128  dtype=bool,
129  default=False,
130  )
131  autoReference = pexConfig.Field(
132  doc="Automatically select the coadd temp exposure to use as a reference for background matching? "
133  "Ignored if doMatchBackgrounds false. "
134  "If False you must specify the reference temp exposure as the data Id",
135  dtype=bool,
136  default=True,
137  )
138  maskPropagationThresholds = pexConfig.DictField(
139  keytype=str,
140  itemtype=float,
141  doc=("Threshold (in fractional weight) of rejection at which we propagate a mask plane to "
142  "the coadd; that is, we set the mask bit on the coadd if the fraction the rejected frames "
143  "would have contributed exceeds this value."),
144  default={"SAT": 0.1},
145  )
146  removeMaskPlanes = pexConfig.ListField(dtype=str, default=["CROSSTALK", "NOT_DEBLENDED"],
147  doc="Mask planes to remove before coadding")
148  #
149  # N.b. These configuration options only set the bitplane config.brightObjectMaskName
150  # To make this useful you *must* also configure the flags.pixel algorithm, for example
151  # by adding
152  # config.measurement.plugins["base_PixelFlags"].masksFpCenter.append("BRIGHT_OBJECT")
153  # config.measurement.plugins["base_PixelFlags"].masksFpAnywhere.append("BRIGHT_OBJECT")
154  # to your measureCoaddSources.py and forcedPhotCoadd.py config overrides
155  #
156  doMaskBrightObjects = pexConfig.Field(dtype=bool, default=False,
157  doc="Set mask and flag bits for bright objects?")
158  brightObjectMaskName = pexConfig.Field(dtype=str, default="BRIGHT_OBJECT",
159  doc="Name of mask bit used for bright objects")
160 
161  coaddPsf = pexConfig.ConfigField(
162  doc="Configuration for CoaddPsf",
163  dtype=measAlg.CoaddPsfConfig,
164  )
165 
166  def setDefaults(self):
167  CoaddBaseTask.ConfigClass.setDefaults(self)
168  self.badMaskPlanes = ["NO_DATA", "BAD", "CR", ]
169 
170  def validate(self):
171  CoaddBaseTask.ConfigClass.validate(self)
172  if self.doPsfMatch:
173  # Backwards compatibility.
174  # Configs do not have loggers
175  log.warn("Config doPsfMatch deprecated. Setting warpType='psfMatched'")
176  self.warpType = 'psfMatched'
177  if self.doSigmaClip and self.statistic != "MEANCLIP":
178  log.warn('doSigmaClip deprecated. To replicate behavior, setting statistic to "MEANCLIP"')
179  self.statistic = "MEANCLIP"
180  if self.doInterp and self.statistic not in ['MEAN', 'MEDIAN', 'MEANCLIP', 'VARIANCE', 'VARIANCECLIP']:
181  raise ValueError("Must set doInterp=False for statistic=%s, which does not "
182  "compute and set a non-zero coadd variance estimate." % (self.statistic))
183 
184  unstackableStats = ['NOTHING', 'ERROR', 'ORMASK']
185  if not hasattr(afwMath.Property, self.statistic) or self.statistic in unstackableStats:
186  stackableStats = [str(k) for k in afwMath.Property.__members__.keys()
187  if str(k) not in unstackableStats]
188  raise ValueError("statistic %s is not allowed. Please choose one of %s."
189  % (self.statistic, stackableStats))
190 
191 
192 
199  """!
200 \anchor AssembleCoaddTask_
201 
202 \brief Assemble a coadded image from a set of warps (coadded temporary exposures).
203 
204 \section pipe_tasks_assembleCoadd_Contents Contents
205  - \ref pipe_tasks_assembleCoadd_AssembleCoaddTask_Purpose
206  - \ref pipe_tasks_assembleCoadd_AssembleCoaddTask_Initialize
207  - \ref pipe_tasks_assembleCoadd_AssembleCoaddTask_Run
208  - \ref pipe_tasks_assembleCoadd_AssembleCoaddTask_Config
209  - \ref pipe_tasks_assembleCoadd_AssembleCoaddTask_Debug
210  - \ref pipe_tasks_assembleCoadd_AssembleCoaddTask_Example
211 
212 \section pipe_tasks_assembleCoadd_AssembleCoaddTask_Purpose Description
213 
214 \copybrief AssembleCoaddTask_
215 
216 We want to assemble a coadded image from a set of Warps (also called
217 coadded temporary exposures or coaddTempExps.
218 Each input Warp covers a patch on the sky and corresponds to a single run/visit/exposure of the
219 covered patch. We provide the task with a list of Warps (selectDataList) from which it selects
220 Warps that cover the specified patch (pointed at by dataRef).
221 Each Warp that goes into a coadd will typically have an independent photometric zero-point.
222 Therefore, we must scale each Warp to set it to a common photometric zeropoint. By default, each
223 Warp has backgrounds and hence will require config.doMatchBackgrounds=True.
224 When background matching is enabled, the task may be configured to automatically select a reference exposure
225 (config.autoReference=True). If this is not done, we require that the input dataRef provides access to a
226 Warp (dataset type coaddName + 'Coadd' + warpType + 'Warp') which is used as the reference exposure.
227 WarpType may be one of 'direct' or 'psfMatched', and the boolean configs config.makeDirect and
228 config.makePsfMatched set which of the warp types will be coadded.
229 The coadd is computed as a mean with optional outlier rejection.
230 Criteria for outlier rejection are set in \ref AssembleCoaddConfig. Finally, Warps can have bad 'NaN'
231 pixels which received no input from the source calExps. We interpolate over these bad (NaN) pixels.
232 
233 AssembleCoaddTask uses several sub-tasks. These are
234 <DL>
235  <DT>\ref ScaleZeroPointTask_ "ScaleZeroPointTask"</DT>
236  <DD> create and use an imageScaler object to scale the photometric zeropoint for each Warp</DD>
237  <DT>\ref MatchBackgroundsTask_ "MatchBackgroundsTask"</DT>
238  <DD> match background in a Warp to a reference exposure (and select the reference exposure if one is
239  not provided).</DD>
240  <DT>\ref InterpImageTask_ "InterpImageTask"</DT>
241  <DD>interpolate across bad pixels (NaN) in the final coadd</DD>
242 </DL>
243 You can retarget these subtasks if you wish.
244 
245 \section pipe_tasks_assembleCoadd_AssembleCoaddTask_Initialize Task initialization
246 \copydoc \_\_init\_\_
247 
248 \section pipe_tasks_assembleCoadd_AssembleCoaddTask_Run Invoking the Task
249 \copydoc run
250 
251 \section pipe_tasks_assembleCoadd_AssembleCoaddTask_Config Configuration parameters
252 See \ref AssembleCoaddConfig_
253 
254 \section pipe_tasks_assembleCoadd_AssembleCoaddTask_Debug Debug variables
255 The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a
256 flag \c -d to import \b debug.py from your \c PYTHONPATH; see \ref baseDebug for more about \b debug.py files.
257 AssembleCoaddTask has no debug variables of its own. Some of the subtasks may support debug variables. See
258 the documetation for the subtasks for further information.
259 
260 \section pipe_tasks_assembleCoadd_AssembleCoaddTask_Example A complete example of using AssembleCoaddTask
261 
262 AssembleCoaddTask assembles a set of warped images into a coadded image. The AssembleCoaddTask
263 can be invoked by running assembleCoadd.py with the flag '--legacyCoadd'. Usage of assembleCoadd.py expects
264 a data reference to the tract patch and filter to be coadded (specified using
265 '--id = [KEY=VALUE1[^VALUE2[^VALUE3...] [KEY=VALUE1[^VALUE2[^VALUE3...] ...]]') along with a list of
266 Warps to attempt to coadd (specified using
267 '--selectId [KEY=VALUE1[^VALUE2[^VALUE3...] [KEY=VALUE1[^VALUE2[^VALUE3...] ...]]'). Only the Warps
268 that cover the specified tract and patch will be coadded. A list of the available optional
269 arguments can be obtained by calling assembleCoadd.py with the --help command line argument:
270 \code
271 assembleCoadd.py --help
272 \endcode
273 To demonstrate usage of the AssembleCoaddTask in the larger context of multi-band processing, we will generate
274 the HSC-I & -R band coadds from HSC engineering test data provided in the ci_hsc package. To begin, assuming
275 that the lsst stack has been already set up, we must set up the obs_subaru and ci_hsc packages.
276 This defines the environment variable $CI_HSC_DIR and points at the location of the package. The raw HSC
277 data live in the $CI_HSC_DIR/raw directory. To begin assembling the coadds, we must first
278 <DL>
279  <DT>processCcd</DT>
280  <DD> process the individual ccds in $CI_HSC_RAW to produce calibrated exposures</DD>
281  <DT>makeSkyMap</DT>
282  <DD> create a skymap that covers the area of the sky present in the raw exposures</DD>
283  <DT>makeCoaddTempExp</DT>
284  <DD> warp the individual calibrated exposures to the tangent plane of the coadd</DD>
285 </DL>
286 We can perform all of these steps by running
287 \code
288 $CI_HSC_DIR scons warp-903986 warp-904014 warp-903990 warp-904010 warp-903988
289 \endcode
290 This will produce warped exposures for each visit. To coadd the warped data, we call assembleCoadd.py as
291 follows:
292 \code
293 assembleCoadd.py --legacyCoadd $CI_HSC_DIR/DATA --id patch=5,4 tract=0 filter=HSC-I \
294 --selectId visit=903986 ccd=16 --selectId visit=903986 ccd=22 --selectId visit=903986 ccd=23 \
295 --selectId visit=903986 ccd=100 --selectId visit=904014 ccd=1 --selectId visit=904014 ccd=6 \
296 --selectId visit=904014 ccd=12 --selectId visit=903990 ccd=18 --selectId visit=903990 ccd=25 \
297 --selectId visit=904010 ccd=4 --selectId visit=904010 ccd=10 --selectId visit=904010 ccd=100 \
298 --selectId visit=903988 ccd=16 --selectId visit=903988 ccd=17 --selectId visit=903988 ccd=23 \
299 --selectId visit=903988 ccd=24
300 \endcode
301 that will process the HSC-I band data. The results are written in
302 `$CI_HSC_DIR/DATA/deepCoadd-results/HSC-I`.
303 
304 You may also choose to run:
305 \code
306 scons warp-903334 warp-903336 warp-903338 warp-903342 warp-903344 warp-903346
307 assembleCoadd.py --legacyCoadd $CI_HSC_DIR/DATA --id patch=5,4 tract=0 filter=HSC-R \
308 --selectId visit=903334 ccd=16 --selectId visit=903334 ccd=22 --selectId visit=903334 ccd=23 \
309 --selectId visit=903334 ccd=100 --selectId visit=903336 ccd=17 --selectId visit=903336 ccd=24 \
310 --selectId visit=903338 ccd=18 --selectId visit=903338 ccd=25 --selectId visit=903342 ccd=4 \
311 --selectId visit=903342 ccd=10 --selectId visit=903342 ccd=100 --selectId visit=903344 ccd=0 \
312 --selectId visit=903344 ccd=5 --selectId visit=903344 ccd=11 --selectId visit=903346 ccd=1 \
313 --selectId visit=903346 ccd=6 --selectId visit=903346 ccd=12
314 \endcode
315 to generate the coadd for the HSC-R band if you are interested in following multiBand Coadd processing as
316 discussed in \ref pipeTasks_multiBand (but note that normally, one would use the
317 \ref SafeClipAssembleCoaddTask_ "SafeClipAssembleCoaddTask" rather than AssembleCoaddTask to make the coadd.
318  """
319  ConfigClass = AssembleCoaddConfig
320  _DefaultName = "assembleCoadd"
321 
322  def __init__(self, *args, **kwargs):
323  """!
324  \brief Initialize the task. Create the \ref InterpImageTask "interpImage",
325  \ref MatchBackgroundsTask "matchBackgrounds", & \ref ScaleZeroPointTask "scaleZeroPoint" subtasks.
326  """
327  CoaddBaseTask.__init__(self, *args, **kwargs)
328  self.makeSubtask("interpImage")
329  self.makeSubtask("matchBackgrounds")
330  self.makeSubtask("scaleZeroPoint")
331 
332  if self.config.doMaskBrightObjects:
333  mask = afwImage.Mask()
334  try:
335  self.brightObjectBitmask = 1 << mask.addMaskPlane(self.config.brightObjectMaskName)
336  except pexExceptions.LsstCppException:
337  raise RuntimeError("Unable to define mask plane for bright objects; planes used are %s" %
338  mask.getMaskPlaneDict().keys())
339  del mask
340 
341  self.warpType = self.config.warpType
342 
343  @pipeBase.timeMethod
344  def run(self, dataRef, selectDataList=[]):
345  """!
346  \brief Assemble a coadd from a set of Warps
347 
348  Coadd a set of Warps. Compute weights to be applied to each Warp and find scalings to
349  match the photometric zeropoint to a reference Warp. Optionally, match backgrounds across
350  Warps if the background has not already been removed. Assemble the Warps using
351  \ref assemble. Interpolate over NaNs and optionally write the coadd to disk. Return the coadded
352  exposure.
353 
354  \anchor runParams
355  \param[in] dataRef: Data reference defining the patch for coaddition and the reference Warp
356  (if config.autoReference=False). Used to access the following data products:
357  - [in] self.config.coaddName + "Coadd_skyMap"
358  - [in] self.config.coaddName + "Coadd_ + <warpType> + "Warp" (optionally)
359  - [out] self.config.coaddName + "Coadd"
360  \param[in] selectDataList[in]: List of data references to Warps. Data to be coadded will be
361  selected from this list based on overlap with the patch defined by dataRef.
362 
363  \return a pipeBase.Struct with fields:
364  - coaddExposure: coadded exposure
365  - nImage: exposure count image
366  """
367  skyInfo = self.getSkyInfo(dataRef)
368  calExpRefList = self.selectExposures(dataRef, skyInfo, selectDataList=selectDataList)
369  if len(calExpRefList) == 0:
370  self.log.warn("No exposures to coadd")
371  return
372  self.log.info("Coadding %d exposures", len(calExpRefList))
373 
374  tempExpRefList = self.getTempExpRefList(dataRef, calExpRefList)
375  inputData = self.prepareInputs(tempExpRefList)
376  self.log.info("Found %d %s", len(inputData.tempExpRefList),
377  self.getTempExpDatasetName(self.warpType))
378  if len(inputData.tempExpRefList) == 0:
379  self.log.warn("No coadd temporary exposures found")
380  return
381  if self.config.doMatchBackgrounds:
382  refImageScaler = self.getBackgroundReferenceScaler(dataRef)
383  inputData = self.backgroundMatching(inputData, dataRef, refImageScaler)
384  if len(inputData.tempExpRefList) == 0:
385  self.log.warn("No valid background models")
386  return
387 
388  supplementaryData = self.makeSupplementaryData(dataRef, selectDataList)
389 
390  retStruct = self.assemble(skyInfo, inputData.tempExpRefList, inputData.imageScalerList,
391  inputData.weightList,
392  inputData.backgroundInfoList if self.config.doMatchBackgrounds else None,
393  supplementaryData=supplementaryData)
394 
395  if self.config.doMatchBackgrounds:
396  self.addBackgroundMatchingMetadata(retStruct.coaddExposure, inputData.tempExpRefList,
397  inputData.backgroundInfoList)
398 
399  if self.config.doInterp:
400  self.interpImage.run(retStruct.coaddExposure.getMaskedImage(), planeName="NO_DATA")
401  # The variance must be positive; work around for DM-3201.
402  varArray = retStruct.coaddExposure.getMaskedImage().getVariance().getArray()
403  varArray[:] = numpy.where(varArray > 0, varArray, numpy.inf)
404 
405  if self.config.doMaskBrightObjects:
406  brightObjectMasks = self.readBrightObjectMasks(dataRef)
407  self.setBrightObjectMasks(retStruct.coaddExposure, dataRef.dataId, brightObjectMasks)
408 
409  if self.config.doWrite:
410  self.log.info("Persisting %s" % self.getCoaddDatasetName(self.warpType))
411  dataRef.put(retStruct.coaddExposure, self.getCoaddDatasetName(self.warpType))
412  if retStruct.nImage is not None:
413  dataRef.put(retStruct.nImage, self.getCoaddDatasetName(self.warpType) + '_nImage')
414 
415  return retStruct
416 
417  def makeSupplementaryData(self, dataRef, selectDataList):
418  """!
419  \brief Make additional inputs to assemble() specific to subclasses.
420 
421  Available to be implemented by subclasses only if they need the
422  coadd dataRef for performing preliminary processing before
423  assembling the coadd.
424  """
425  pass
426 
427  def getTempExpRefList(self, patchRef, calExpRefList):
428  """!
429  \brief Generate list data references corresponding to warped exposures that lie within the
430  patch to be coadded.
431 
432  \param[in] patchRef: Data reference for patch
433  \param[in] calExpRefList: List of data references for input calexps
434  \return List of Warp/CoaddTempExp data references
435  """
436  butler = patchRef.getButler()
437  groupData = groupPatchExposures(patchRef, calExpRefList, self.getCoaddDatasetName(self.warpType),
438  self.getTempExpDatasetName(self.warpType))
439  tempExpRefList = [getGroupDataRef(butler, self.getTempExpDatasetName(self.warpType),
440  g, groupData.keys) for
441  g in groupData.groups.keys()]
442  return tempExpRefList
443 
444  def getBackgroundReferenceScaler(self, dataRef):
445  """!
446  \brief Construct an image scaler for the background reference frame
447 
448  Each Warp has a different background level. A reference background level must be chosen before
449  coaddition. If config.autoReference=True, \ref backgroundMatching will pick the reference level and
450  this routine is a no-op and None is returned. Otherwise, use the
451  \ref ScaleZeroPointTask_ "scaleZeroPoint" subtask to compute an imageScaler object for the provided
452  reference image and return it.
453 
454  \param[in] dataRef: Data reference for the background reference frame, or None
455  \return image scaler, or None
456  """
457  if self.config.autoReference:
458  return None
459 
460  # We've been given the data reference
461  dataset = self.getTempExpDatasetName(self.warpType)
462  if not dataRef.datasetExists(dataset):
463  raise RuntimeError("Could not find reference exposure %s %s." % (dataset, dataRef.dataId))
464 
465  refExposure = dataRef.get(self.getTempExpDatasetName(self.warpType), immediate=True)
466  refImageScaler = self.scaleZeroPoint.computeImageScaler(
467  exposure=refExposure,
468  dataRef=dataRef,
469  )
470  return refImageScaler
471 
472  def prepareInputs(self, refList):
473  """!
474  \brief Prepare the input warps for coaddition by measuring the weight for each warp and the scaling
475  for the photometric zero point.
476 
477  Each Warp has its own photometric zeropoint and background variance. Before coadding these
478  Warps together, compute a scale factor to normalize the photometric zeropoint and compute the
479  weight for each Warp.
480 
481  \param[in] refList: List of data references to tempExp
482  \return Struct:
483  - tempExprefList: List of data references to tempExp
484  - weightList: List of weightings
485  - imageScalerList: List of image scalers
486  """
487  statsCtrl = afwMath.StatisticsControl()
488  statsCtrl.setNumSigmaClip(self.config.sigmaClip)
489  statsCtrl.setNumIter(self.config.clipIter)
490  statsCtrl.setAndMask(self.getBadPixelMask())
491  statsCtrl.setNanSafe(True)
492  # compute tempExpRefList: a list of tempExpRef that actually exist
493  # and weightList: a list of the weight of the associated coadd tempExp
494  # and imageScalerList: a list of scale factors for the associated coadd tempExp
495  tempExpRefList = []
496  weightList = []
497  imageScalerList = []
498  tempExpName = self.getTempExpDatasetName(self.warpType)
499  for tempExpRef in refList:
500  if not tempExpRef.datasetExists(tempExpName):
501  self.log.warn("Could not find %s %s; skipping it", tempExpName, tempExpRef.dataId)
502  continue
503 
504  tempExp = tempExpRef.get(tempExpName, immediate=True)
505  maskedImage = tempExp.getMaskedImage()
506  imageScaler = self.scaleZeroPoint.computeImageScaler(
507  exposure=tempExp,
508  dataRef=tempExpRef,
509  )
510  try:
511  imageScaler.scaleMaskedImage(maskedImage)
512  except Exception as e:
513  self.log.warn("Scaling failed for %s (skipping it): %s", tempExpRef.dataId, e)
514  continue
515  statObj = afwMath.makeStatistics(maskedImage.getVariance(), maskedImage.getMask(),
516  afwMath.MEANCLIP, statsCtrl)
517  meanVar, meanVarErr = statObj.getResult(afwMath.MEANCLIP)
518  weight = 1.0 / float(meanVar)
519  if not numpy.isfinite(weight):
520  self.log.warn("Non-finite weight for %s: skipping", tempExpRef.dataId)
521  continue
522  self.log.info("Weight of %s %s = %0.3f", tempExpName, tempExpRef.dataId, weight)
523 
524  del maskedImage
525  del tempExp
526 
527  tempExpRefList.append(tempExpRef)
528  weightList.append(weight)
529  imageScalerList.append(imageScaler)
530 
531  return pipeBase.Struct(tempExpRefList=tempExpRefList, weightList=weightList,
532  imageScalerList=imageScalerList)
533 
534  def backgroundMatching(self, inputData, refExpDataRef=None, refImageScaler=None):
535  """!
536  \brief Perform background matching on the prepared inputs
537 
538  Each Warp has a different background level that must be normalized to a reference level
539  before coaddition. If no reference is provided, the background matcher selects one. If the background
540  matching is performed sucessfully, recompute the weight to be applied to the Warp (coaddTempExp) to be
541  consistent with the scaled background.
542 
543  \param[in] inputData: Struct from prepareInputs() with tempExpRefList, weightList, imageScalerList
544  \param[in] refExpDataRef: Data reference for background reference Warp, or None
545  \param[in] refImageScaler: Image scaler for background reference Warp, or None
546  \return Struct:
547  - tempExprefList: List of data references to warped exposures (coaddTempExps)
548  - weightList: List of weightings
549  - imageScalerList: List of image scalers
550  - backgroundInfoList: result from background matching
551  """
552  try:
553  backgroundInfoList = self.matchBackgrounds.run(
554  expRefList=inputData.tempExpRefList,
555  imageScalerList=inputData.imageScalerList,
556  refExpDataRef=refExpDataRef if not self.config.autoReference else None,
557  refImageScaler=refImageScaler,
558  expDatasetType=self.getTempExpDatasetName(self.warpType),
559  ).backgroundInfoList
560  except Exception as e:
561  self.log.fatal("Cannot match backgrounds: %s", e)
562  raise pipeBase.TaskError("Background matching failed.")
563 
564  newWeightList = []
565  newTempExpRefList = []
566  newBackgroundStructList = []
567  newScaleList = []
568  # the number of good backgrounds may be < than len(tempExpList)
569  # sync these up and correct the weights
570  for tempExpRef, bgInfo, scaler, weight in zip(inputData.tempExpRefList, backgroundInfoList,
571  inputData.imageScalerList, inputData.weightList):
572  if not bgInfo.isReference:
573  # skip exposure if it has no backgroundModel
574  # or if fit was bad
575  if (bgInfo.backgroundModel is None):
576  self.log.info("No background offset model available for %s: skipping", tempExpRef.dataId)
577  continue
578  try:
579  varianceRatio = bgInfo.matchedMSE / bgInfo.diffImVar
580  except Exception as e:
581  self.log.info("MSE/Var ratio not calculable (%s) for %s: skipping",
582  e, tempExpRef.dataId)
583  continue
584  if not numpy.isfinite(varianceRatio):
585  self.log.info("MSE/Var ratio not finite (%.2f / %.2f) for %s: skipping",
586  bgInfo.matchedMSE, bgInfo.diffImVar, tempExpRef.dataId)
587  continue
588  elif (varianceRatio > self.config.maxMatchResidualRatio):
589  self.log.info("Bad fit. MSE/Var ratio %.2f > %.2f for %s: skipping",
590  varianceRatio, self.config.maxMatchResidualRatio, tempExpRef.dataId)
591  continue
592  elif (bgInfo.fitRMS > self.config.maxMatchResidualRMS):
593  self.log.info("Bad fit. RMS %.2f > %.2f for %s: skipping",
594  bgInfo.fitRMS, self.config.maxMatchResidualRMS, tempExpRef.dataId)
595  continue
596  newWeightList.append(1 / (1 / weight + bgInfo.fitRMS**2))
597  newTempExpRefList.append(tempExpRef)
598  newBackgroundStructList.append(bgInfo)
599  newScaleList.append(scaler)
600 
601  return pipeBase.Struct(tempExpRefList=newTempExpRefList, weightList=newWeightList,
602  imageScalerList=newScaleList, backgroundInfoList=newBackgroundStructList)
603 
604  def assemble(self, skyInfo, tempExpRefList, imageScalerList, weightList, bgInfoList=None,
605  altMaskList=None, mask=None, supplementaryData=None):
606  """!
607  \anchor AssembleCoaddTask.assemble_
608 
609  \brief Assemble a coadd from input warps
610 
611  Assemble the coadd using the provided list of coaddTempExps. Since the full coadd covers a patch (a
612  large area), the assembly is performed over small areas on the image at a time in order to
613  conserve memory usage. Iterate over subregions within the outer bbox of the patch using
614  \ref assembleSubregion to stack the corresponding subregions from the coaddTempExps with the
615  statistic specified. Set the edge bits the coadd mask based on the weight map.
616 
617  \param[in] skyInfo: Patch geometry information, from getSkyInfo
618  \param[in] tempExpRefList: List of data references to Warps (previously called CoaddTempExps)
619  \param[in] imageScalerList: List of image scalers
620  \param[in] weightList: List of weights
621  \param[in] bgInfoList: List of background data from background matching, or None
622  \param[in] altMaskList: List of alternate masks to use rather than those stored with tempExp, or None
623  \param[in] mask: Mask to ignore when coadding
624  \param[in] supplementaryData: pipeBase.Struct with additional data products needed to assemble coadd.
625  Only used by subclasses that implement makeSupplementaryData and override assemble.
626  \return pipeBase.Struct with coaddExposure, nImage if requested
627  """
628  tempExpName = self.getTempExpDatasetName(self.warpType)
629  self.log.info("Assembling %s %s", len(tempExpRefList), tempExpName)
630  if mask is None:
631  mask = self.getBadPixelMask()
632 
633  statsCtrl = afwMath.StatisticsControl()
634  statsCtrl.setNumSigmaClip(self.config.sigmaClip)
635  statsCtrl.setNumIter(self.config.clipIter)
636  statsCtrl.setAndMask(mask)
637  statsCtrl.setNanSafe(True)
638  statsCtrl.setWeighted(True)
639  statsCtrl.setCalcErrorFromInputVariance(True)
640  for plane, threshold in self.config.maskPropagationThresholds.items():
641  bit = afwImage.Mask.getMaskPlane(plane)
642  statsCtrl.setMaskPropagationThreshold(bit, threshold)
643 
644  statsFlags = afwMath.stringToStatisticsProperty(self.config.statistic)
645 
646  if bgInfoList is None:
647  bgInfoList = [None]*len(tempExpRefList)
648 
649  if altMaskList is None:
650  altMaskList = [None]*len(tempExpRefList)
651 
652  coaddExposure = afwImage.ExposureF(skyInfo.bbox, skyInfo.wcs)
653  coaddExposure.setCalib(self.scaleZeroPoint.getCalib())
654  coaddExposure.getInfo().setCoaddInputs(self.inputRecorder.makeCoaddInputs())
655  self.assembleMetadata(coaddExposure, tempExpRefList, weightList)
656  coaddMaskedImage = coaddExposure.getMaskedImage()
657  subregionSizeArr = self.config.subregionSize
658  subregionSize = afwGeom.Extent2I(subregionSizeArr[0], subregionSizeArr[1])
659  # if nImage is requested, create a zero one which can be passed to assembleSubregion
660  if self.config.doNImage:
661  nImage = afwImage.ImageU(skyInfo.bbox)
662  else:
663  nImage = None
664  for subBBox in _subBBoxIter(skyInfo.bbox, subregionSize):
665  try:
666  self.assembleSubregion(coaddExposure, subBBox, tempExpRefList, imageScalerList,
667  weightList, bgInfoList, altMaskList, statsFlags, statsCtrl,
668  nImage=nImage)
669  except Exception as e:
670  self.log.fatal("Cannot compute coadd %s: %s", subBBox, e)
671 
672  coaddUtils.setCoaddEdgeBits(coaddMaskedImage.getMask(), coaddMaskedImage.getVariance())
673  return pipeBase.Struct(coaddExposure=coaddExposure, nImage=nImage)
674 
675  def assembleMetadata(self, coaddExposure, tempExpRefList, weightList):
676  """!
677  \brief Set the metadata for the coadd
678 
679  This basic implementation simply sets the filter from the
680  first input.
681 
682  \param[in] coaddExposure: The target image for the coadd
683  \param[in] tempExpRefList: List of data references to tempExp
684  \param[in] weightList: List of weights
685  """
686  assert len(tempExpRefList) == len(weightList), "Length mismatch"
687  tempExpName = self.getTempExpDatasetName(self.warpType)
688  # We load a single pixel of each coaddTempExp, because we just want to get at the metadata
689  # (and we need more than just the PropertySet that contains the header), which is not possible
690  # with the current butler (see #2777).
691  tempExpList = [tempExpRef.get(tempExpName + "_sub",
692  bbox=afwGeom.Box2I(afwGeom.Point2I(0, 0), afwGeom.Extent2I(1, 1)),
693  imageOrigin="LOCAL", immediate=True) for tempExpRef in tempExpRefList]
694  numCcds = sum(len(tempExp.getInfo().getCoaddInputs().ccds) for tempExp in tempExpList)
695 
696  coaddExposure.setFilter(tempExpList[0].getFilter())
697  coaddInputs = coaddExposure.getInfo().getCoaddInputs()
698  coaddInputs.ccds.reserve(numCcds)
699  coaddInputs.visits.reserve(len(tempExpList))
700 
701  for tempExp, weight in zip(tempExpList, weightList):
702  self.inputRecorder.addVisitToCoadd(coaddInputs, tempExp, weight)
703  coaddInputs.visits.sort()
704  if self.warpType == "psfMatched":
705  # The modelPsf BBox for a psfMatchedWarp/coaddTempExp was dynamically defined by
706  # ModelPsfMatchTask as the square box bounding its spatially-variable, pre-matched WarpedPsf.
707  # Likewise, set the PSF of a PSF-Matched Coadd to the modelPsf
708  # having the maximum width (sufficient because square)
709  modelPsfList = [tempExp.getPsf() for tempExp in tempExpList]
710  modelPsfWidthList = [modelPsf.computeBBox().getWidth() for modelPsf in modelPsfList]
711  psf = modelPsfList[modelPsfWidthList.index(max(modelPsfWidthList))]
712  else:
713  psf = measAlg.CoaddPsf(coaddInputs.ccds, coaddExposure.getWcs(),
714  self.config.coaddPsf.makeControl())
715  coaddExposure.setPsf(psf)
716  apCorrMap = measAlg.makeCoaddApCorrMap(coaddInputs.ccds, coaddExposure.getBBox(afwImage.PARENT),
717  coaddExposure.getWcs())
718  coaddExposure.getInfo().setApCorrMap(apCorrMap)
719 
720  def assembleSubregion(self, coaddExposure, bbox, tempExpRefList, imageScalerList, weightList,
721  bgInfoList, altMaskList, statsFlags, statsCtrl, nImage=None):
722  """!
723  \brief Assemble the coadd for a sub-region.
724 
725  For each coaddTempExp, check for (and swap in) an alternative mask if one is passed. If background
726  matching is enabled, add the background and background variance from each coaddTempExp. Remove mask
727  planes listed in config.removeMaskPlanes, Finally, stack the actual exposures using
728  \ref afwMath.statisticsStack "statisticsStack" with the statistic specified
729  by statsFlags. Typically, the statsFlag will be one of afwMath.MEAN for a mean-stack or
730  afwMath.MEANCLIP for outlier rejection using an N-sigma clipped mean where N and iterations
731  are specified by statsCtrl. Assign the stacked subregion back to the coadd.
732 
733  \param[in] coaddExposure: The target image for the coadd
734  \param[in] bbox: Sub-region to coadd
735  \param[in] tempExpRefList: List of data reference to tempExp
736  \param[in] imageScalerList: List of image scalers
737  \param[in] weightList: List of weights
738  \param[in] bgInfoList: List of background data from background matching
739  \param[in] altMaskList: List of alternate masks to use rather than those stored with tempExp, or None
740  \param[in] statsFlags: afwMath.Property object for statistic for coadd
741  \param[in] statsCtrl: Statistics control object for coadd
742  \param[in] nImage: optional ImageU keeps track of exposure count for each pixel
743  """
744  self.log.debug("Computing coadd over %s", bbox)
745  tempExpName = self.getTempExpDatasetName(self.warpType)
746  coaddMaskedImage = coaddExposure.getMaskedImage()
747  maskedImageList = []
748  if nImage is not None:
749  subNImage = afwImage.ImageU(bbox.getWidth(), bbox.getHeight())
750  for tempExpRef, imageScaler, bgInfo, altMask in zip(tempExpRefList, imageScalerList, bgInfoList,
751  altMaskList):
752  exposure = tempExpRef.get(tempExpName + "_sub", bbox=bbox)
753  maskedImage = exposure.getMaskedImage()
754  if altMask:
755  altMaskSub = altMask.Factory(altMask, bbox, afwImage.PARENT)
756  maskedImage.getMask().swap(altMaskSub)
757  imageScaler.scaleMaskedImage(maskedImage)
758 
759  if self.config.doMatchBackgrounds and not bgInfo.isReference:
760  backgroundModel = bgInfo.backgroundModel
761  backgroundImage = backgroundModel.getImage() if \
762  self.matchBackgrounds.config.usePolynomial else \
763  backgroundModel.getImageF()
764  backgroundImage.setXY0(coaddMaskedImage.getXY0())
765  maskedImage += backgroundImage.Factory(backgroundImage, bbox, afwImage.PARENT, False)
766  var = maskedImage.getVariance()
767  var += (bgInfo.fitRMS)**2
768  # Add 1 for each pixel which is not excluded by the exclude mask.
769  # In legacyCoadd, pixels may also be excluded by afwMath.statisticsStack.
770  if nImage is not None:
771  subNImage.getArray()[maskedImage.getMask().getArray() & statsCtrl.getAndMask() == 0] += 1
772  if self.config.removeMaskPlanes:
773  mask = maskedImage.getMask()
774  for maskPlane in self.config.removeMaskPlanes:
775  try:
776  mask &= ~mask.getPlaneBitMask(maskPlane)
777  except Exception as e:
778  self.log.warn("Unable to remove mask plane %s: %s", maskPlane, e.message)
779 
780  maskedImageList.append(maskedImage)
781 
782  with self.timer("stack"):
783  coaddSubregion = afwMath.statisticsStack(
784  maskedImageList, statsFlags, statsCtrl, weightList)
785  coaddMaskedImage.assign(coaddSubregion, bbox)
786  if nImage is not None:
787  nImage.assign(subNImage, bbox)
788 
789  def addBackgroundMatchingMetadata(self, coaddExposure, tempExpRefList, backgroundInfoList):
790  """!
791  \brief Add metadata from the background matching to the coadd
792 
793  \param[in] coaddExposure: Coadd
794  \param[in] tempExpRefList: List of data references for temp exps to go into coadd
795  \param[in] backgroundInfoList: List of background info, results from background matching
796  """
797  self.log.info("Adding exposure information to metadata")
798  metadata = coaddExposure.getMetadata()
799  metadata.addString("CTExp_SDQA1_DESCRIPTION",
800  "Background matching: Ratio of matchedMSE / diffImVar")
801  for ind, (tempExpRef, backgroundInfo) in enumerate(zip(tempExpRefList, backgroundInfoList)):
802  tempExpStr = '&'.join('%s=%s' % (k, v) for k, v in tempExpRef.dataId.items())
803  if backgroundInfo.isReference:
804  metadata.addString("ReferenceExp_ID", tempExpStr)
805  else:
806  metadata.addString("CTExp_ID_%d" % (ind), tempExpStr)
807  metadata.addDouble("CTExp_SDQA1_%d" % (ind),
808  backgroundInfo.matchedMSE/backgroundInfo.diffImVar)
809  metadata.addDouble("CTExp_SDQA2_%d" % (ind),
810  backgroundInfo.fitRMS)
811 
812  def readBrightObjectMasks(self, dataRef):
813  """Returns None on failure"""
814  try:
815  return dataRef.get("brightObjectMask", immediate=True)
816  except Exception as e:
817  self.log.warn("Unable to read brightObjectMask for %s: %s", dataRef.dataId, e)
818  return None
819 
820  def setBrightObjectMasks(self, exposure, dataId, brightObjectMasks):
821  """Set the bright object masks
822 
823  exposure: Exposure under consideration
824  dataId: Data identifier dict for patch
825  brightObjectMasks: afwTable of bright objects to mask
826  """
827  #
828  # Check the metadata specifying the tract/patch/filter
829  #
830  if brightObjectMasks is None:
831  self.log.warn("Unable to apply bright object mask: none supplied")
832  return
833  self.log.info("Applying %d bright object masks to %s", len(brightObjectMasks), dataId)
834  md = brightObjectMasks.table.getMetadata()
835  for k in dataId:
836  if not md.exists(k):
837  self.log.warn("Expected to see %s in metadata", k)
838  else:
839  if md.get(k) != dataId[k]:
840  self.log.warn("Expected to see %s == %s in metadata, saw %s", k, md.get(k), dataId[k])
841 
842  mask = exposure.getMaskedImage().getMask()
843  wcs = exposure.getWcs()
844  plateScale = wcs.pixelScale().asArcseconds()
845 
846  for rec in brightObjectMasks:
847  center = afwGeom.PointI(wcs.skyToPixel(rec.getCoord()))
848  if rec["type"] == "box":
849  assert rec["angle"] == 0.0, ("Angle != 0 for mask object %s" % rec["id"])
850  width = rec["width"].asArcseconds()/plateScale # convert to pixels
851  height = rec["height"].asArcseconds()/plateScale # convert to pixels
852 
853  halfSize = afwGeom.ExtentI(0.5*width, 0.5*height)
854  bbox = afwGeom.Box2I(center - halfSize, center + halfSize)
855 
856  bbox = afwGeom.BoxI(afwGeom.PointI(int(center[0] - 0.5*width), int(center[1] - 0.5*height)),
857  afwGeom.PointI(int(center[0] + 0.5*width), int(center[1] + 0.5*height)))
858  spans = afwGeom.SpanSet(bbox)
859  elif rec["type"] == "circle":
860  radius = int(rec["radius"].asArcseconds()/plateScale) # convert to pixels
861  spans = afwGeom.SpanSet.fromShape(radius, offset=center)
862  else:
863  self.log.warn("Unexpected region type %s at %s" % rec["type"], center)
864  continue
865  spans.clippedTo(mask.getBBox()).setMask(mask, self.brightObjectBitmask)
866 
867  @classmethod
868  def _makeArgumentParser(cls):
869  """!
870  \brief Create an argument parser
871  """
872  parser = pipeBase.ArgumentParser(name=cls._DefaultName)
873  parser.add_id_argument("--id", cls.ConfigClass().coaddName + "Coadd_" +
874  cls.ConfigClass().warpType + "Warp",
875  help="data ID, e.g. --id tract=12345 patch=1,2",
876  ContainerClass=AssembleCoaddDataIdContainer)
877  parser.add_id_argument("--selectId", "calexp", help="data ID, e.g. --selectId visit=6789 ccd=0..9",
878  ContainerClass=SelectDataIdContainer)
879  return parser
880 
881 
882 def _subBBoxIter(bbox, subregionSize):
883  """!
884  \brief Iterate over subregions of a bbox
885 
886  \param[in] bbox: bounding box over which to iterate: afwGeom.Box2I
887  \param[in] subregionSize: size of sub-bboxes
888 
889  \return subBBox: next sub-bounding box of size subregionSize or smaller;
890  each subBBox is contained within bbox, so it may be smaller than subregionSize at the edges of bbox,
891  but it will never be empty
892  """
893  if bbox.isEmpty():
894  raise RuntimeError("bbox %s is empty" % (bbox,))
895  if subregionSize[0] < 1 or subregionSize[1] < 1:
896  raise RuntimeError("subregionSize %s must be nonzero" % (subregionSize,))
897 
898  for rowShift in range(0, bbox.getHeight(), subregionSize[1]):
899  for colShift in range(0, bbox.getWidth(), subregionSize[0]):
900  subBBox = afwGeom.Box2I(bbox.getMin() + afwGeom.Extent2I(colShift, rowShift), subregionSize)
901  subBBox.clip(bbox)
902  if subBBox.isEmpty():
903  raise RuntimeError("Bug: empty bbox! bbox=%s, subregionSize=%s, colShift=%s, rowShift=%s" %
904  (bbox, subregionSize, colShift, rowShift))
905  yield subBBox
906 
907 
908 class AssembleCoaddDataIdContainer(pipeBase.DataIdContainer):
909  """!
910  \brief A version of lsst.pipe.base.DataIdContainer specialized for assembleCoadd.
911  """
912 
913  def makeDataRefList(self, namespace):
914  """!
915  \brief Make self.refList from self.idList.
916 
917  Interpret the config.doMatchBackgrounds, config.autoReference,
918  and whether a visit/run supplied.
919  If a visit/run is supplied, config.autoReference is automatically set to False.
920  if config.doMatchBackgrounds == false, then a visit/run will be ignored if accidentally supplied.
921 
922  """
923  keysCoadd = namespace.butler.getKeys(datasetType=namespace.config.coaddName + "Coadd",
924  level=self.level)
925  keysCoaddTempExp = namespace.butler.getKeys(datasetType=namespace.config.coaddName +
926  "Coadd_directWarp", level=self.level)
927 
928  if namespace.config.doMatchBackgrounds:
929  if namespace.config.autoReference: # matcher will pick it's own reference image
930  datasetType = namespace.config.coaddName + "Coadd"
931  validKeys = keysCoadd
932  else:
933  datasetType = namespace.config.coaddName + "Coadd_directWarp"
934  validKeys = keysCoaddTempExp
935  else: # bkg subtracted coadd
936  datasetType = namespace.config.coaddName + "Coadd"
937  validKeys = keysCoadd
938 
939  for dataId in self.idList:
940  # tract and patch are required
941  for key in validKeys:
942  if key not in dataId:
943  raise RuntimeError("--id must include " + key)
944 
945  for key in dataId: # check if users supplied visit/run
946  if (key not in keysCoadd) and (key in keysCoaddTempExp): # user supplied a visit/run
947  if namespace.config.autoReference:
948  # user probably meant: autoReference = False
949  namespace.config.autoReference = False
950  datasetType = namespace.config.coaddName + "Coadd_directWarp"
951  print("Switching config.autoReference to False; applies only to background Matching.")
952  break
953 
954  dataRef = namespace.butler.dataRef(
955  datasetType=datasetType,
956  dataId=dataId,
957  )
958  self.refList.append(dataRef)
959 
960 
961 def countMaskFromFootprint(mask, footprint, bitmask, ignoreMask):
962  """!
963  \brief Function to count the number of pixels with a specific mask in a footprint.
964 
965  Find the intersection of mask & footprint. Count all pixels in the mask that are in the intersection that
966  have bitmask set but do not have ignoreMask set. Return the count.
967 
968  \param[in] mask: mask to define intersection region by.
969  \parma[in] footprint: footprint to define the intersection region by.
970  \param[in] bitmask: specific mask that we wish to count the number of occurances of.
971  \param[in] ignoreMask: pixels to not consider.
972  \return count of number of pixels in footprint with specified mask.
973  """
974  bbox = footprint.getBBox()
975  bbox.clip(mask.getBBox(afwImage.PARENT))
976  fp = afwImage.Mask(bbox)
977  subMask = mask.Factory(mask, bbox, afwImage.PARENT)
978  footprint.spans.setMask(fp, bitmask)
979  return numpy.logical_and((subMask.getArray() & fp.getArray()) > 0,
980  (subMask.getArray() & ignoreMask) == 0).sum()
981 
982 
984  """!
985 \anchor SafeClipAssembleCoaddConfig
986 
987 \brief Configuration parameters for the SafeClipAssembleCoaddTask
988  """
989  clipDetection = pexConfig.ConfigurableField(
990  target=SourceDetectionTask,
991  doc="Detect sources on difference between unclipped and clipped coadd")
992  minClipFootOverlap = pexConfig.Field(
993  doc="Minimum fractional overlap of clipped footprint with visit DETECTED to be clipped",
994  dtype=float,
995  default=0.6
996  )
997  minClipFootOverlapSingle = pexConfig.Field(
998  doc="Minimum fractional overlap of clipped footprint with visit DETECTED to be "
999  "clipped when only one visit overlaps",
1000  dtype=float,
1001  default=0.5
1002  )
1003  minClipFootOverlapDouble = pexConfig.Field(
1004  doc="Minimum fractional overlap of clipped footprints with visit DETECTED to be "
1005  "clipped when two visits overlap",
1006  dtype=float,
1007  default=0.45
1008  )
1009  maxClipFootOverlapDouble = pexConfig.Field(
1010  doc="Maximum fractional overlap of clipped footprints with visit DETECTED when "
1011  "considering two visits",
1012  dtype=float,
1013  default=0.15
1014  )
1015  minBigOverlap = pexConfig.Field(
1016  doc="Minimum number of pixels in footprint to use DETECTED mask from the single visits "
1017  "when labeling clipped footprints",
1018  dtype=int,
1019  default=100
1020  )
1021 
1022  def setDefaults(self):
1023  # The numeric values for these configuration parameters were empirically determined, future work
1024  # may further refine them.
1025  AssembleCoaddConfig.setDefaults(self)
1026  self.clipDetection.doTempLocalBackground = False
1027  self.clipDetection.reEstimateBackground = False
1028  self.clipDetection.returnOriginalFootprints = False
1029  self.clipDetection.thresholdPolarity = "both"
1030  self.clipDetection.thresholdValue = 2
1031  self.clipDetection.nSigmaToGrow = 2
1032  self.clipDetection.minPixels = 4
1033  self.clipDetection.isotropicGrow = True
1034  self.clipDetection.thresholdType = "pixel_stdev"
1035  self.sigmaClip = 1.5
1036  self.clipIter = 3
1037  self.statistic = "MEAN"
1038 
1039  def validate(self):
1040  if self.doSigmaClip:
1041  log.warn("Additional Sigma-clipping not allowed in Safe-clipped Coadds. "
1042  "Ignoring doSigmaClip.")
1043  self.doSigmaClip = False
1044  if self.statistic != "MEAN":
1045  raise ValueError("Only MEAN statistic allowed for final stacking in SafeClipAssembleCoadd "
1046  "(%s chosen). Please set statistic to MEAN."
1047  % (self.statistic))
1048  AssembleCoaddTask.ConfigClass.validate(self)
1049 
1050 
1051 
1057 
1058 
1060  """!
1061  \anchor SafeClipAssembleCoaddTask_
1062 
1063  \brief Assemble a coadded image from a set of coadded temporary exposures,
1064  being careful to clip & flag areas with potential artifacts.
1065 
1066  \section pipe_tasks_assembleCoadd_Contents Contents
1067  - \ref pipe_tasks_assembleCoadd_SafeClipAssembleCoaddTask_Purpose
1068  - \ref pipe_tasks_assembleCoadd_SafeClipAssembleCoaddTask_Initialize
1069  - \ref pipe_tasks_assembleCoadd_SafeClipAssembleCoaddTask_Run
1070  - \ref pipe_tasks_assembleCoadd_SafeClipAssembleCoaddTask_Config
1071  - \ref pipe_tasks_assembleCoadd_SafeClipAssembleCoaddTask_Debug
1072  - \ref pipe_tasks_assembleCoadd_SafeClipAssembleCoaddTask_Example
1073 
1074  \section pipe_tasks_assembleCoadd_SafeClipAssembleCoaddTask_Purpose Description
1075 
1076  \copybrief SafeClipAssembleCoaddTask
1077 
1078  Read the documentation for \ref AssembleCoaddTask_ "AssembleCoaddTask" first since
1079  SafeClipAssembleCoaddTask subtasks that task.
1080  In \ref AssembleCoaddTask_ "AssembleCoaddTask", we compute the coadd as an clipped mean (i.e. we clip
1081  outliers).
1082  The problem with doing this is that when computing the coadd PSF at a given location, individual visit
1083  PSFs from visits with outlier pixels contribute to the coadd PSF and cannot be treated correctly.
1084  In this task, we correct for this behavior by creating a new badMaskPlane 'CLIPPED'.
1085  We populate this plane on the input coaddTempExps and the final coadd where i. difference imaging suggests
1086  that there is an outlier and ii. this outlier appears on only one or two images.
1087  Such regions will not contribute to the final coadd.
1088  Furthermore, any routine to determine the coadd PSF can now be cognizant of clipped regions.
1089  Note that the algorithm implemented by this task is preliminary and works correctly for HSC data.
1090  Parameter modifications and or considerable redesigning of the algorithm is likley required for other
1091  surveys.
1092 
1093  SafeClipAssembleCoaddTask uses a \ref SourceDetectionTask_ "clipDetection" subtask and also sub-classes
1094  \ref AssembleCoaddTask_ "AssembleCoaddTask". You can retarget the
1095  \ref SourceDetectionTask_ "clipDetection" subtask if you wish.
1096 
1097  \section pipe_tasks_assembleCoadd_SafeClipAssembleCoaddTask_Initialize Task initialization
1098  \copydoc \_\_init\_\_
1099 
1100  \section pipe_tasks_assembleCoadd_SafeClipAssembleCoaddTask_Run Invoking the Task
1101  \copydoc run
1102 
1103  \section pipe_tasks_assembleCoadd_SafeClipAssembleCoaddTask_Config Configuration parameters
1104  See \ref SafeClipAssembleCoaddConfig
1105 
1106  \section pipe_tasks_assembleCoadd_SafeClipAssembleCoaddTask_Debug Debug variables
1107  The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a
1108  flag \c -d to import \b debug.py from your \c PYTHONPATH; see \ref baseDebug for more about \b debug.py
1109  files.
1110  SafeClipAssembleCoaddTask has no debug variables of its own. The \ref SourceDetectionTask_ "clipDetection"
1111  subtasks may support debug variables. See the documetation for \ref SourceDetectionTask_ "clipDetection"
1112  for further information.
1113 
1114  \section pipe_tasks_assembleCoadd_SafeClipAssembleCoaddTask_Example A complete example of using
1115  SafeClipAssembleCoaddTask
1116 
1117  SafeClipAssembleCoaddTask assembles a set of warped coaddTempExp images into a coadded image.
1118  The SafeClipAssembleCoaddTask is invoked by running assembleCoadd.py <em>without</em> the flag
1119  '--legacyCoadd'.
1120  Usage of assembleCoadd.py expects a data reference to the tract patch and filter to be coadded
1121  (specified using '--id = [KEY=VALUE1[^VALUE2[^VALUE3...] [KEY=VALUE1[^VALUE2[^VALUE3...] ...]]') along
1122  with a list of coaddTempExps to attempt to coadd (specified using
1123  '--selectId [KEY=VALUE1[^VALUE2[^VALUE3...] [KEY=VALUE1[^VALUE2[^VALUE3...] ...]]').
1124  Only the coaddTempExps that cover the specified tract and patch will be coadded.
1125  A list of the available optional arguments can be obtained by calling assembleCoadd.py with the --help
1126  command line argument:
1127  \code
1128  assembleCoadd.py --help
1129  \endcode
1130  To demonstrate usage of the SafeClipAssembleCoaddTask in the larger context of multi-band processing, we
1131  will generate the HSC-I & -R band coadds from HSC engineering test data provided in the ci_hsc package. To
1132  begin, assuming that the lsst stack has been already set up, we must set up the obs_subaru and ci_hsc
1133  packages.
1134  This defines the environment variable $CI_HSC_DIR and points at the location of the package. The raw HSC
1135  data live in the $CI_HSC_DIR/raw directory. To begin assembling the coadds, we must first
1136  <DL>
1137  <DT>processCcd</DT>
1138  <DD> process the individual ccds in $CI_HSC_RAW to produce calibrated exposures</DD>
1139  <DT>makeSkyMap</DT>
1140  <DD> create a skymap that covers the area of the sky present in the raw exposures</DD>
1141  <DT>makeCoaddTempExp</DT>
1142  <DD> warp the individual calibrated exposures to the tangent plane of the coadd</DD>
1143  </DL>
1144  We can perform all of these steps by running
1145  \code
1146  $CI_HSC_DIR scons warp-903986 warp-904014 warp-903990 warp-904010 warp-903988
1147  \endcode
1148  This will produce warped coaddTempExps for each visit. To coadd the warped data, we call assembleCoadd.py
1149  as follows:
1150  \code
1151  assembleCoadd.py $CI_HSC_DIR/DATA --id patch=5,4 tract=0 filter=HSC-I \
1152  --selectId visit=903986 ccd=16 --selectId visit=903986 ccd=22 --selectId visit=903986 ccd=23 \
1153  --selectId visit=903986 ccd=100--selectId visit=904014 ccd=1 --selectId visit=904014 ccd=6 \
1154  --selectId visit=904014 ccd=12 --selectId visit=903990 ccd=18 --selectId visit=903990 ccd=25 \
1155  --selectId visit=904010 ccd=4 --selectId visit=904010 ccd=10 --selectId visit=904010 ccd=100 \
1156  --selectId visit=903988 ccd=16 --selectId visit=903988 ccd=17 --selectId visit=903988 ccd=23 \
1157  --selectId visit=903988 ccd=24
1158  \endcode
1159  This will process the HSC-I band data. The results are written in
1160  `$CI_HSC_DIR/DATA/deepCoadd-results/HSC-I`.
1161 
1162  You may also choose to run:
1163  \code
1164  scons warp-903334 warp-903336 warp-903338 warp-903342 warp-903344 warp-903346
1165  assembleCoadd.py $CI_HSC_DIR/DATA --id patch=5,4 tract=0 filter=HSC-R --selectId visit=903334 ccd=16 \
1166  --selectId visit=903334 ccd=22 --selectId visit=903334 ccd=23 --selectId visit=903334 ccd=100 \
1167  --selectId visit=903336 ccd=17 --selectId visit=903336 ccd=24 --selectId visit=903338 ccd=18 \
1168  --selectId visit=903338 ccd=25 --selectId visit=903342 ccd=4 --selectId visit=903342 ccd=10 \
1169  --selectId visit=903342 ccd=100 --selectId visit=903344 ccd=0 --selectId visit=903344 ccd=5 \
1170  --selectId visit=903344 ccd=11 --selectId visit=903346 ccd=1 --selectId visit=903346 ccd=6 \
1171  --selectId visit=903346 ccd=12
1172  \endcode
1173  to generate the coadd for the HSC-R band if you are interested in following multiBand Coadd processing as
1174  discussed in \ref pipeTasks_multiBand.
1175  """
1176  ConfigClass = SafeClipAssembleCoaddConfig
1177  _DefaultName = "safeClipAssembleCoadd"
1178 
1179  def __init__(self, *args, **kwargs):
1180  """!
1181  \brief Initialize the task and make the \ref SourceDetectionTask_ "clipDetection" subtask.
1182  """
1183  AssembleCoaddTask.__init__(self, *args, **kwargs)
1184  schema = afwTable.SourceTable.makeMinimalSchema()
1185  self.makeSubtask("clipDetection", schema=schema)
1186 
1187  def assemble(self, skyInfo, tempExpRefList, imageScalerList, weightList, bgModelList,
1188  *args, **kwargs):
1189  """!
1190  \brief Assemble the coadd for a region
1191 
1192  Compute the difference of coadds created with and without outlier rejection to identify coadd pixels
1193  that have outlier values in some individual visits. Detect clipped regions on the difference image and
1194  mark these regions on the one or two individual coaddTempExps where they occur if there is significant
1195  overlap between the clipped region and a source.
1196  This leaves us with a set of footprints from the difference image that have been identified as having
1197  occured on just one or two individual visits. However, these footprints were generated from a
1198  difference image. It is conceivable for a large diffuse source to have become broken up into multiple
1199  footprints acrosss the coadd difference in this process.
1200  Determine the clipped region from all overlapping footprints from the detected sources in each visit -
1201  these are big footprints.
1202  Combine the small and big clipped footprints and mark them on a new bad mask plane
1203  Generate the coadd using \ref AssembleCoaddTask.assemble_ "AssembleCoaddTask.assemble" without outlier
1204  removal. Clipped footprints will no longer make it into the coadd because they are marked in the new
1205  bad mask plane.
1206 
1207  N.b. *args and **kwargs are passed but ignored in order to match the call signature expected by the
1208  parent task.
1209 
1210  @param skyInfo: Patch geometry information, from getSkyInfo
1211  @param tempExpRefList: List of data reference to tempExp
1212  @param imageScalerList: List of image scalers
1213  @param weightList: List of weights
1214  @param bgModelList: List of background models from background matching
1215  return pipeBase.Struct with coaddExposure, nImage
1216  """
1217  exp = self.buildDifferenceImage(skyInfo, tempExpRefList, imageScalerList, weightList, bgModelList)
1218  mask = exp.getMaskedImage().getMask()
1219  mask.addMaskPlane("CLIPPED")
1220 
1221  result = self.detectClip(exp, tempExpRefList)
1222 
1223  self.log.info('Found %d clipped objects', len(result.clipFootprints))
1224 
1225  # Go to individual visits for big footprints
1226  maskClipValue = mask.getPlaneBitMask("CLIPPED")
1227  maskDetValue = mask.getPlaneBitMask("DETECTED") | mask.getPlaneBitMask("DETECTED_NEGATIVE")
1228  bigFootprints = self.detectClipBig(result.tempExpClipList, result.clipFootprints, result.clipIndices,
1229  maskClipValue, maskDetValue)
1230 
1231  # Create mask of the current clipped footprints
1232  maskClip = mask.Factory(mask.getBBox(afwImage.PARENT))
1233  afwDet.setMaskFromFootprintList(maskClip, result.clipFootprints, maskClipValue)
1234 
1235  maskClipBig = maskClip.Factory(mask.getBBox(afwImage.PARENT))
1236  afwDet.setMaskFromFootprintList(maskClipBig, bigFootprints, maskClipValue)
1237  maskClip |= maskClipBig
1238 
1239  # Assemble coadd from base class, but ignoring CLIPPED pixels
1240  badMaskPlanes = self.config.badMaskPlanes[:]
1241  badMaskPlanes.append("CLIPPED")
1242  badPixelMask = afwImage.Mask.getPlaneBitMask(badMaskPlanes)
1243  retStruct = AssembleCoaddTask.assemble(self, skyInfo, tempExpRefList, imageScalerList, weightList,
1244  bgModelList, result.tempExpClipList, mask=badPixelMask)
1245 
1246  # Set the coadd CLIPPED mask from the footprints since currently pixels that are masked
1247  # do not get propagated
1248  maskExp = retStruct.coaddExposure.getMaskedImage().getMask()
1249  maskExp |= maskClip
1250 
1251  return retStruct
1252 
1253  def buildDifferenceImage(self, skyInfo, tempExpRefList, imageScalerList, weightList, bgModelList):
1254  """!
1255  \brief Return an exposure that contains the difference between and unclipped and clipped coadds.
1256 
1257  Generate a difference image between clipped and unclipped coadds.
1258  Compute the difference image by subtracting an outlier-clipped coadd from an outlier-unclipped coadd.
1259  Return the difference image.
1260 
1261  @param skyInfo: Patch geometry information, from getSkyInfo
1262  @param tempExpRefList: List of data reference to tempExp
1263  @param imageScalerList: List of image scalers
1264  @param weightList: List of weights
1265  @param bgModelList: List of background models from background matching
1266  @return Difference image of unclipped and clipped coadd wrapped in an Exposure
1267  """
1268  # Clone and upcast self.config because current self.config is frozen
1269  config = AssembleCoaddConfig()
1270  # getattr necessary because subtasks do not survive Config.toDict()
1271  configIntersection = {k: getattr(self.config, k)
1272  for k, v in self.config.toDict().items() if (k in config.keys())}
1273  config.update(**configIntersection)
1274 
1275  # statistic MEAN copied from self.config.statistic, but for clarity explicitly assign
1276  config.statistic = 'MEAN'
1277  task = AssembleCoaddTask(config=config)
1278  coaddMean = task.assemble(skyInfo, tempExpRefList, imageScalerList, weightList,
1279  bgModelList).coaddExposure
1280 
1281  config.statistic = 'MEANCLIP'
1282  task = AssembleCoaddTask(config=config)
1283  coaddClip = task.assemble(skyInfo, tempExpRefList, imageScalerList, weightList,
1284  bgModelList).coaddExposure
1285 
1286  coaddDiff = coaddMean.getMaskedImage().Factory(coaddMean.getMaskedImage())
1287  coaddDiff -= coaddClip.getMaskedImage()
1288  exp = afwImage.ExposureF(coaddDiff)
1289  exp.setPsf(coaddMean.getPsf())
1290  return exp
1291 
1292  def detectClip(self, exp, tempExpRefList):
1293  """!
1294  \brief Detect clipped regions on an exposure and set the mask on the individual tempExp masks
1295 
1296  Detect footprints in the difference image after smoothing the difference image with a Gaussian kernal.
1297  Identify footprints that overlap with one or two input coaddTempExps by comparing the computed overlap
1298  fraction to thresholds set in the config.
1299  A different threshold is applied depending on the number of overlapping visits (restricted to one or
1300  two).
1301  If the overlap exceeds the thresholds, the footprint is considered "CLIPPED" and is marked as such on
1302  the coaddTempExp.
1303  Return a struct with the clipped footprints, the indices of the coaddTempExps that end up overlapping
1304  with the clipped footprints and a list of new masks for the coaddTempExps.
1305 
1306  \param[in] exp: Exposure to run detection on
1307  \param[in] tempExpRefList: List of data reference to tempExp
1308  \return struct containing:
1309  - clippedFootprints: list of clipped footprints
1310  - clippedIndices: indices for each clippedFootprint in tempExpRefList
1311  - tempExpClipList: list of new masks for tempExp
1312  """
1313  mask = exp.getMaskedImage().getMask()
1314  maskClipValue = mask.getPlaneBitMask("CLIPPED")
1315  maskDetValue = mask.getPlaneBitMask("DETECTED") | mask.getPlaneBitMask("DETECTED_NEGATIVE")
1316  fpSet = self.clipDetection.detectFootprints(exp, doSmooth=True, clearMask=True)
1317  # Merge positive and negative together footprints together
1318  fpSet.positive.merge(fpSet.negative)
1319  footprints = fpSet.positive
1320  self.log.info('Found %d potential clipped objects', len(footprints.getFootprints()))
1321  ignoreMask = self.getBadPixelMask()
1322 
1323  clipFootprints = []
1324  clipIndices = []
1325 
1326  # build a list with a mask for each visit which can be modified with clipping information
1327  tempExpClipList = [tmpExpRef.get(self.getTempExpDatasetName(self.warpType),
1328  immediate=True).getMaskedImage().getMask() for
1329  tmpExpRef in tempExpRefList]
1330 
1331  for footprint in footprints.getFootprints():
1332  nPixel = footprint.getArea()
1333  overlap = [] # hold the overlap with each visit
1334  maskList = [] # which visit mask match
1335  indexList = [] # index of visit in global list
1336  for i, tmpExpMask in enumerate(tempExpClipList):
1337  # Determine the overlap with the footprint
1338  ignore = countMaskFromFootprint(tmpExpMask, footprint, ignoreMask, 0x0)
1339  overlapDet = countMaskFromFootprint(tmpExpMask, footprint, maskDetValue, ignoreMask)
1340  totPixel = nPixel - ignore
1341 
1342  # If we have more bad pixels than detection skip
1343  if ignore > overlapDet or totPixel <= 0.5*nPixel or overlapDet == 0:
1344  continue
1345  overlap.append(overlapDet/float(totPixel))
1346  maskList.append(tmpExpMask)
1347  indexList.append(i)
1348 
1349  overlap = numpy.array(overlap)
1350  if not len(overlap):
1351  continue
1352 
1353  keep = False # Should this footprint be marked as clipped?
1354  keepIndex = [] # Which tempExps does the clipped footprint belong to
1355 
1356  # If footprint only has one overlap use a lower threshold
1357  if len(overlap) == 1:
1358  if overlap[0] > self.config.minClipFootOverlapSingle:
1359  keep = True
1360  keepIndex = [0]
1361  else:
1362  # This is the general case where only visit should be clipped
1363  clipIndex = numpy.where(overlap > self.config.minClipFootOverlap)[0]
1364  if len(clipIndex) == 1:
1365  keep = True
1366  keepIndex = [clipIndex[0]]
1367 
1368  # Test if there are clipped objects that overlap two different visits
1369  clipIndex = numpy.where(overlap > self.config.minClipFootOverlapDouble)[0]
1370  if len(clipIndex) == 2 and len(overlap) > 3:
1371  clipIndexComp = numpy.where(overlap <= self.config.minClipFootOverlapDouble)[0]
1372  if numpy.max(overlap[clipIndexComp]) <= self.config.maxClipFootOverlapDouble:
1373  keep = True
1374  keepIndex = clipIndex
1375 
1376  if not keep:
1377  continue
1378 
1379  for index in keepIndex:
1380  footprint.spans.setMask(maskList[index], maskClipValue)
1381 
1382  clipIndices.append(numpy.array(indexList)[keepIndex])
1383  clipFootprints.append(footprint)
1384 
1385  return pipeBase.Struct(clipFootprints=clipFootprints, clipIndices=clipIndices,
1386  tempExpClipList=tempExpClipList)
1387 
1388  def detectClipBig(self, tempExpClipList, clipFootprints, clipIndices, maskClipValue, maskDetValue):
1389  """!
1390  \brief Find footprints from individual tempExp footprints for large footprints.
1391 
1392  Identify big footprints composed of many sources in the coadd difference that may have originated in a
1393  large diffuse source in the coadd. We do this by indentifying all clipped footprints that overlap
1394  significantly with each source in all the coaddTempExps.
1395 
1396  \param[in] tempExpClipList: List of tempExp masks with clipping information
1397  \param[in] clipFootprints: List of clipped footprints
1398  \param[in] clipIndices: List of which entries in tempExpClipList each footprint belongs to
1399  \param[in] maskClipValue: Mask value of clipped pixels
1400  \param[in] maskClipValue: Mask value of detected pixels
1401  \return list of big footprints
1402  """
1403  bigFootprintsCoadd = []
1404  ignoreMask = self.getBadPixelMask()
1405  for index, tmpExpMask in enumerate(tempExpClipList):
1406 
1407  # Create list of footprints from the DETECTED pixels
1408  maskVisitDet = tmpExpMask.Factory(tmpExpMask, tmpExpMask.getBBox(afwImage.PARENT),
1409  afwImage.PARENT, True)
1410  maskVisitDet &= maskDetValue
1411  visitFootprints = afwDet.FootprintSet(maskVisitDet, afwDet.Threshold(1))
1412 
1413  # build a mask of clipped footprints that are in this visit
1414  clippedFootprintsVisit = []
1415  for foot, clipIndex in zip(clipFootprints, clipIndices):
1416  if index not in clipIndex:
1417  continue
1418  clippedFootprintsVisit.append(foot)
1419  maskVisitClip = maskVisitDet.Factory(maskVisitDet.getBBox(afwImage.PARENT))
1420  afwDet.setMaskFromFootprintList(maskVisitClip, clippedFootprintsVisit, maskClipValue)
1421 
1422  bigFootprintsVisit = []
1423  for foot in visitFootprints.getFootprints():
1424  if foot.getArea() < self.config.minBigOverlap:
1425  continue
1426  nCount = countMaskFromFootprint(maskVisitClip, foot, maskClipValue, ignoreMask)
1427  if nCount > self.config.minBigOverlap:
1428  bigFootprintsVisit.append(foot)
1429  bigFootprintsCoadd.append(foot)
1430 
1431  # Update single visit masks
1432  maskVisitClip.clearAllMaskPlanes()
1433  afwDet.setMaskFromFootprintList(maskVisitClip, bigFootprintsVisit, maskClipValue)
1434  tmpExpMask |= maskVisitClip
1435 
1436  return bigFootprintsCoadd
1437 
1438 
1440  assembleStaticSkyModel = pexConfig.ConfigurableField(
1441  target=AssembleCoaddTask,
1442  doc="Task to assemble an artifact-free, PSF-matched Coadd to serve as a"
1443  " naive/first-iteration model of the static sky.",
1444  )
1445  chiThreshold = pexConfig.RangeField(
1446  doc="Detection threshold (sigma) for artifacts in warp diff "
1447  "(chi-image of PSF-Matched warp minus the model of the static sky)",
1448  dtype=float,
1449  default=5,
1450  min=0,
1451  )
1452  minPixels = pexConfig.Field(
1453  doc="Minimum number of pixels in a region (in a warp diff chi-image) "
1454  "above chiThreshold to trigger masking. "
1455  "Detected artifacts with fewer than the specified number of pixels will be ignored.",
1456  dtype=int,
1457  default=1
1458  )
1459  doMaskNegative = pexConfig.Field(
1460  doc="Also mask outlier regions of flux less than the static sky model?",
1461  dtype=bool,
1462  default=True
1463  )
1464  temporalThreshold = pexConfig.Field(
1465  doc="Unitless fraction of number of epochs to classify as an artifact/outlier source versus"
1466  " a source that is intrinsically variable or difficult to subtract cleanly. "
1467  "If outlier region in warp-diff Chi-image is mostly (defined by spatialThreshold) "
1468  "an outlier in less than temporalThreshold * number of epochs, then mask. "
1469  "Otherwise, do not mask.",
1470  dtype=float,
1471  default=0.075
1472  )
1473  spatialThreshold = pexConfig.Field(
1474  doc="Unitless fraction of pixels defining how much of the outlier region has to meet the "
1475  "temporal criteria",
1476  dtype=float,
1477  default=0.5
1478  )
1479  growMaskBy = pexConfig.Field(
1480  doc="Number of pixels by which to grow the masks on artifacts.",
1481  dtype=int,
1482  default=5
1483  )
1484 
1485  def setDefaults(self):
1486  AssembleCoaddConfig.setDefaults(self)
1487  self.assembleStaticSkyModel.warpType = 'psfMatched'
1488  self.assembleStaticSkyModel.statistic = 'MEDIAN'
1489  self.assembleStaticSkyModel.doWrite = False
1490  self.statistic = 'MEAN'
1491 
1492 
1493 
1499 
1501  """!
1502  \anchor CompareWarpAssembleCoaddTask_
1503 
1504  \brief Assemble a compareWarp coadded image from a set of warps
1505  by masking artifacts detected by comparing PSF-matched warps
1506 
1507  \section pipe_tasks_assembleCoadd_Contents Contents
1508  - \ref pipe_tasks_assembleCoadd_CompareWarpAssembleCoaddTask_Purpose
1509  - \ref pipe_tasks_assembleCoadd_CompareWarpAssembleCoaddTask_Initialize
1510  - \ref pipe_tasks_assembleCoadd_CompareWarpAssembleCoaddTask_Run
1511  - \ref pipe_tasks_assembleCoadd_CompareWarpAssembleCoaddTask_Config
1512  - \ref pipe_tasks_assembleCoadd_CompareWarpAssembleCoaddTask_Debug
1513  - \ref pipe_tasks_assembleCoadd_CompareWarpAssembleCoaddTask_Example
1514 
1515  \section pipe_tasks_assembleCoadd_CompareWarpAssembleCoaddTask_Purpose Description
1516 
1517  \copybrief CompareWarpAssembleCoaddTask
1518 
1519  In \ref AssembleCoaddTask_ "AssembleCoaddTask", we compute the coadd as an clipped mean (i.e. we clip
1520  outliers).
1521  The problem with doing this is that when computing the coadd PSF at a given location, individual visit
1522  PSFs from visits with outlier pixels contribute to the coadd PSF and cannot be treated correctly.
1523  In this task, we correct for this behavior by creating a new badMaskPlane 'CLIPPED' which marks
1524  pixels in the individual warps suspected to contain an artifact.
1525  We populate this plane on the input warps by comparing PSF-matched warps with a PSF-matched median coadd
1526  which serves as a model of the static sky. Any group of pixels that deviates from the PSF-matched
1527  median coadd by more than config.chiThreshold sigma, is an artifact candidate. The candidates are then
1528  filtered to remove variable sources and sources that are difficult to subtract such as bright stars.
1529  This filter is configured using the config parameters temporalThreshold and spatialThreshold.
1530  The temporalThreshold is the maximum fraction of epochs that the deviation can
1531  appear in and still be considered an artifact. The spatialThreshold is the maximum fraction of pixels in
1532  the footprint of the deviation that appear in other epochs (where other epochs is defined by the
1533  temporalThreshold). If the deviant region meets this criteria of having a significant percentage of pixels
1534  that deviate in only a few epochs, it is grown by growMaskBy and bits set in the 'CLIPPED' plane.
1535  These regions will not contribute to the final coadd.
1536  Furthermore, any routine to determine the coadd PSF can now be cognizant of clipped regions.
1537  Note that the algorithm implemented by this task is preliminary and works correctly for HSC data.
1538  Parameter modifications and or considerable redesigning of the algorithm is likley required for other
1539  surveys.
1540 
1541  CompareWarpAssembleCoaddTask sub-classes
1542  \ref AssembleCoaddTask_ "AssembleCoaddTask" and instantiates \ref AssembleCoaddTask_ "AssembleCoaddTask"
1543  as a subtask to generate the TemplateCoadd (the model of the static sky)
1544 
1545  \section pipe_tasks_assembleCoadd_CompareWarpAssembleCoaddTask_Initialize Task initialization
1546  \copydoc \_\_init\_\_
1547 
1548  \section pipe_tasks_assembleCoadd_CompareWarpAssembleCoaddTask_Run Invoking the Task
1549  \copydoc run
1550 
1551  \section pipe_tasks_assembleCoadd_CompareWarpAssembleCoaddTask_Config Configuration parameters
1552  See \ref CompareWarpAssembleCoaddConfig
1553 
1554  \section pipe_tasks_assembleCoadd_CompareWarpAssembleCoaddTask_Debug Debug variables
1555  The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a
1556  flag \c -d to import \b debug.py from your \c PYTHONPATH; see \ref baseDebug for more about \b debug.py
1557  files.
1558 
1559  This task supports the following debug variables:
1560  <dl>
1561  <dt>`saveCountIm`
1562  <dd> If True then save the Epoch Count Image as a fits file in the `figPath`
1563  <dt> `saveAltMask`
1564  <dd> If True then save the new masks with CLIPPED planes as fits files to the `figPath`
1565  <dt> `figPath`
1566  <dd> Path to save the debug fits images and figures
1567  </dl>
1568 
1569  For example, put something like:
1570  @code{.py}
1571  import lsstDebug
1572  def DebugInfo(name):
1573  di = lsstDebug.getInfo(name)
1574  if name == "lsst.pipe.tasks.assembleCoadd":
1575  di.saveCountIm = True
1576  di.saveAltMask = True
1577  di.figPath = "/desired/path/to/debugging/output/images"
1578  return di
1579  lsstDebug.Info = DebugInfo
1580  @endcode
1581  into your `debug.py` file and run `assemebleCoadd.py` with the `--debug`
1582  flag.
1583  Some subtasks may have their own debug variables; see individual Task
1584  documentation
1585 
1586  \section pipe_tasks_assembleCoadd_CompareWarpAssembleCoaddTask_Example A complete example of using
1587  CompareWarpAssembleCoaddTask
1588 
1589  CompareWarpAssembleCoaddTask assembles a set of warped images into a coadded image.
1590  The CompareWarpAssembleCoaddTask is invoked by running assembleCoadd.py with the flag
1591  '--compareWarpCoadd'.
1592  Usage of assembleCoadd.py expects a data reference to the tract patch and filter to be coadded
1593  (specified using '--id = [KEY=VALUE1[^VALUE2[^VALUE3...] [KEY=VALUE1[^VALUE2[^VALUE3...] ...]]') along
1594  with a list of coaddTempExps to attempt to coadd (specified using
1595  '--selectId [KEY=VALUE1[^VALUE2[^VALUE3...] [KEY=VALUE1[^VALUE2[^VALUE3...] ...]]').
1596  Only the warps that cover the specified tract and patch will be coadded.
1597  A list of the available optional arguments can be obtained by calling assembleCoadd.py with the --help
1598  command line argument:
1599  \code
1600  assembleCoadd.py --help
1601  \endcode
1602  To demonstrate usage of the CompareWarpAssembleCoaddTask in the larger context of multi-band processing,
1603  we will generate the HSC-I & -R band coadds from HSC engineering test data provided in the ci_hsc package.
1604  To begin, assuming that the lsst stack has been already set up, we must set up the obs_subaru and ci_hsc
1605  packages.
1606  This defines the environment variable $CI_HSC_DIR and points at the location of the package. The raw HSC
1607  data live in the $CI_HSC_DIR/raw directory. To begin assembling the coadds, we must first
1608  <DL>
1609  <DT>processCcd</DT>
1610  <DD> process the individual ccds in $CI_HSC_RAW to produce calibrated exposures</DD>
1611  <DT>makeSkyMap</DT>
1612  <DD> create a skymap that covers the area of the sky present in the raw exposures</DD>
1613  <DT>makeCoaddTempExp</DT>
1614  <DD> warp the individual calibrated exposures to the tangent plane of the coadd</DD>
1615  </DL>
1616  We can perform all of these steps by running
1617  \code
1618  $CI_HSC_DIR scons warp-903986 warp-904014 warp-903990 warp-904010 warp-903988
1619  \endcode
1620  This will produce warped coaddTempExps for each visit. To coadd the warped data, we call assembleCoadd.py
1621  as follows:
1622  \code
1623  assembleCoadd.py --compareWarpCoadd $CI_HSC_DIR/DATA --id patch=5,4 tract=0 filter=HSC-I \
1624  --selectId visit=903986 ccd=16 --selectId visit=903986 ccd=22 --selectId visit=903986 ccd=23 \
1625  --selectId visit=903986 ccd=100 --selectId visit=904014 ccd=1 --selectId visit=904014 ccd=6 \
1626  --selectId visit=904014 ccd=12 --selectId visit=903990 ccd=18 --selectId visit=903990 ccd=25 \
1627  --selectId visit=904010 ccd=4 --selectId visit=904010 ccd=10 --selectId visit=904010 ccd=100 \
1628  --selectId visit=903988 ccd=16 --selectId visit=903988 ccd=17 --selectId visit=903988 ccd=23 \
1629  --selectId visit=903988 ccd=24
1630  \endcode
1631  This will process the HSC-I band data. The results are written in
1632  `$CI_HSC_DIR/DATA/deepCoadd-results/HSC-I`.
1633  """
1634  ConfigClass = CompareWarpAssembleCoaddConfig
1635  _DefaultName = "compareWarpAssembleCoadd"
1636 
1637  def __init__(self, *args, **kwargs):
1638  """!
1639  \brief Initialize the task and make the \ref AssembleCoadd_ "assembleStaticSkyModel" subtask.
1640  """
1641  AssembleCoaddTask.__init__(self, *args, **kwargs)
1642  self.makeSubtask("assembleStaticSkyModel")
1643 
1644  def makeSupplementaryData(self, dataRef, selectDataList):
1645  """!
1646  \brief Make inputs specific to Subclass
1647 
1648  Generate a templateCoadd to use as a native model of static sky to subtract from warps.
1649  """
1650  templateCoadd = self.assembleStaticSkyModel.run(dataRef, selectDataList).coaddExposure
1651  return pipeBase.Struct(templateCoadd=templateCoadd)
1652 
1653  def assemble(self, skyInfo, tempExpRefList, imageScalerList, weightList, bgModelList,
1654  supplementaryData, *args, **kwargs):
1655  """!
1656  \brief Assemble the coadd
1657 
1658  Requires additional inputs Struct `supplementaryData` to contain a `templateCoadd` that serves
1659  as the model of the static sky.
1660 
1661  Find artifacts and apply them to the warps' masks creating a list of alternative masks with a
1662  new "CLIPPED" plane and updated "NO_DATA" plane.
1663  Then pass these alternative masks to the base class's assemble method.
1664 
1665  @param skyInfo: Patch geometry information
1666  @param tempExpRefList: List of data references to warps
1667  @param imageScalerList: List of image scalers
1668  @param weightList: List of weights
1669  @param bgModelList: List of background models from background matching
1670  @param supplementaryData: PipeBase.Struct containing a templateCoadd
1671 
1672  return pipeBase.Struct with coaddExposure, nImage if requested
1673  """
1674  templateCoadd = supplementaryData.templateCoadd
1675  spanSetMaskList = self.findArtifacts(templateCoadd, tempExpRefList, imageScalerList)
1676  maskList = self.computeAltMaskList(tempExpRefList, spanSetMaskList)
1677  badMaskPlanes = self.config.badMaskPlanes[:]
1678  badMaskPlanes.append("CLIPPED")
1679  badPixelMask = afwImage.Mask.getPlaneBitMask(badMaskPlanes)
1680 
1681  retStruct = AssembleCoaddTask.assemble(self, skyInfo, tempExpRefList, imageScalerList, weightList,
1682  bgModelList, maskList, mask=badPixelMask)
1683  return retStruct
1684 
1685  def findArtifacts(self, templateCoadd, tempExpRefList, imageScalerList):
1686  """!
1687  \brief Find artifacts
1688 
1689  Loop through warps twice. The first loop builds a map with the count of how many
1690  epochs each pixel deviates from the templateCoadd by more than config.chiThreshold sigma.
1691  The second loop takes each difference image and filters the artifacts detected
1692  in each using count map to filter out variable sources and sources that are difficult to
1693  subtract cleanly.
1694 
1695  @param templateCoadd: Exposure to serve as model of static sky
1696  @param tempExpRefList: List of data references to warps
1697  @param imageScalerList: List of image scalers
1698  """
1699 
1700  self.log.debug("Generating Count Image, and mask lists.")
1701  coaddBBox = templateCoadd.getBBox()
1702  slateIm = afwImage.ImageU(coaddBBox)
1703  epochCountImage = afwImage.ImageU(coaddBBox)
1704  spanSetArtifactList = []
1705  spanSetNoDataMaskList = []
1706 
1707  maxNumEpochs = int(max(1, self.config.temporalThreshold*len(tempExpRefList)))
1708  for warpRef, imageScaler in zip(tempExpRefList, imageScalerList):
1709  mi = self._readAndComputeWarpDiff(warpRef, imageScaler, templateCoadd)
1710  if mi is not None:
1711  chiIm = self._makeChiIm(mi)
1712  chiOneMap = self._snrToBinaryIm(chiIm)
1713  outliers = afwImage.makeMaskFromArray(chiOneMap.array.astype(afwImage.MaskPixel))
1714  outliers.setXY0(mi.getXY0())
1715  spanSetList = afwGeom.SpanSet.fromMask(outliers).split()
1716  dilatedSpanSetList = [s.dilated(self.config.growMaskBy,
1717  afwGeom.Stencil.CIRCLE).clippedTo(coaddBBox)
1718  for s in spanSetList]
1719  slateIm.set(0)
1720  for spans in dilatedSpanSetList:
1721  spans.setImage(slateIm, 1, doClip=True)
1722  epochCountImage += slateIm
1723  else:
1724  chiIm = afwImage.ImageF(coaddBBox, numpy.nan)
1725  dilatedSpanSetList = []
1726 
1727  # PSF-Matched warps have less available area (~the matching kernel) because the calexps
1728  # undergo a second convolution. Pixels with data in the direct warp
1729  # but not in the PSF-matched warp will not have their artifacts detected.
1730  # NaNs from the PSF-matched warp therefore must be masked in the direct warp
1731  nans = numpy.where(numpy.isnan(chiIm.array), 1, 0)
1732  nansMask = afwImage.makeMaskFromArray(nans.astype(afwImage.MaskPixel))
1733  nansMask.setXY0(chiIm.getXY0())
1734  spanSetNoDataMask = afwGeom.SpanSet.fromMask(nansMask).split()
1735 
1736  spanSetArtifactList.append(dilatedSpanSetList)
1737  spanSetNoDataMaskList.append(spanSetNoDataMask)
1738 
1739  if lsstDebug.Info(__name__).saveCountIm:
1740  path = self._dataRef2DebugPath("epochCountIm", tempExpRefList[0], coaddLevel=True)
1741  epochCountImage.writeFits(path)
1742 
1743  for i, spanSetList in enumerate(spanSetArtifactList):
1744  if spanSetList:
1745  filteredSpanSetList = self._filterArtifacts(spanSetList, epochCountImage,
1746  maxNumEpochs=maxNumEpochs)
1747  spanSetArtifactList[i] = filteredSpanSetList
1748 
1749  return pipeBase.Struct(artifacts=spanSetArtifactList,
1750  noData=spanSetNoDataMaskList)
1751 
1752  def computeAltMaskList(self, tempExpRefList, maskSpanSets):
1753  """!
1754  \brief Apply artifact span set lists to masks
1755 
1756  @param tempExpRefList: List of data references to warps
1757  @param maskSpanSets: Struct containing artifact and noData spanSet lists to apply
1758 
1759  return List of alternative masks
1760 
1761  Add artifact span set list as "CLIPPED" plane and NaNs to existing "NO_DATA" plane
1762  """
1763  spanSetMaskList = maskSpanSets.artifacts
1764  spanSetNoDataList = maskSpanSets.noData
1765  altMaskList = []
1766  for warpRef, artifacts, noData in zip(tempExpRefList, spanSetMaskList, spanSetNoDataList):
1767  warp = warpRef.get(self.getTempExpDatasetName(self.config.warpType), immediate=True)
1768  mask = warp.maskedImage.mask
1769  maskClipValue = mask.addMaskPlane("CLIPPED")
1770  noDataValue = mask.addMaskPlane("NO_DATA")
1771  for artifact in artifacts:
1772  artifact.clippedTo(mask.getBBox()).setMask(mask, 2**maskClipValue)
1773  for noDataRegion in noData:
1774  noDataRegion.clippedTo(mask.getBBox()).setMask(mask, 2**noDataValue)
1775  altMaskList.append(mask)
1776  if lsstDebug.Info(__name__).saveAltMask:
1777  mask.writeFits(self._dataRef2DebugPath("altMask", warpRef))
1778 
1779  return altMaskList
1780 
1781  def _filterArtifacts(self, spanSetList, epochCountImage, maxNumEpochs=None, minPixels=None):
1782  if minPixels is None:
1783  minPixels = self.config.minPixels
1784  maskSpanSetList = []
1785  x0, y0 = epochCountImage.getXY0()
1786  for i, span in enumerate(spanSetList):
1787  y, x = span.indices()
1788  if len(y) < minPixels:
1789  continue
1790  counts = epochCountImage.array[[y1 - y0 for y1 in y], [x1 - x0 for x1 in x]]
1791  idx = numpy.where((counts > 0) & (counts <= maxNumEpochs))
1792  percentBelowThreshold = len(idx[0]) / len(counts)
1793  if percentBelowThreshold > self.config.spatialThreshold:
1794  maskSpanSetList.append(span)
1795  return maskSpanSetList
1796 
1797  def _snrToBinaryArr(self, arrIn):
1798  with numpy.errstate(invalid='ignore'):
1799  arr = numpy.where(arrIn >= self.config.chiThreshold, 1, 0)
1800  if self.config.doMaskNegative:
1801  arr[numpy.where(arrIn <= -self.config.chiThreshold)] = 1
1802  return arr
1803 
1804  def _snrToBinaryIm(self, im):
1805  arr = self._snrToBinaryArr(im.array)
1806  return afwImage.ImageU(arr.astype(numpy.uint16), xy0=im.getXY0())
1807 
1808  def _makeChiIm(self, maskedImage):
1809  chiIm = maskedImage.image.Factory(1./numpy.sqrt(maskedImage.variance.array))
1810  chiIm *= maskedImage.image
1811  chiIm.setXY0(maskedImage.getXY0())
1812  return chiIm
1813 
1814  def _readAndComputeWarpDiff(self, warpRef, imageScaler, templateCoadd):
1815  # Warp comparison must use PSF-Matched Warps regardless of requested coadd warp type
1816  warpName = self.getTempExpDatasetName('psfMatched')
1817  if not warpRef.datasetExists(warpName):
1818  self.log.warn("Could not find %s %s; skipping it", warpName, warpRef.dataId)
1819  return None
1820  warp = warpRef.get(self.getTempExpDatasetName('psfMatched'), immediate=True)
1821  # direct image scaler OK for PSF-matched Warp
1822  imageScaler.scaleMaskedImage(warp.getMaskedImage())
1823  mi = warp.getMaskedImage()
1824  mi -= templateCoadd.getMaskedImage()
1825  return mi
1826 
1827  def _dataRef2DebugPath(self, prefix, warpRef, coaddLevel=False):
1828  """!
1829  \brief Return a path to which to write debugging output
1830 
1831  @param prefix: string, prefix for filename
1832  @param warpRef: Butler dataRef
1833  @param coaddLevel: bool, optional. If True, include only coadd-level keys
1834  (e.g. 'tract', 'patch', 'filter', but no 'visit')
1835 
1836  Creates a hyphen-delimited string of dataId values for simple filenames.
1837  """
1838  if coaddLevel:
1839  keys = warpRef.getButler().getKeys(self.getCoaddDatasetName(self.warpType))
1840  else:
1841  keys = warpRef.dataId.keys()
1842  keyList = sorted(keys, reverse=True)
1843  directory = lsstDebug.Info(__name__).figPath if lsstDebug.Info(__name__).figPath else "."
1844  filename = "%s-%s.fits" % (prefix, '-'.join([str(warpRef.dataId[k]) for k in keyList]))
1845  return os.path.join(directory, filename)
def setBrightObjectMasks(self, exposure, dataId, brightObjectMasks)
def getCoaddDatasetName(self, warpType="direct")
Definition: coaddBase.py:167
def _dataRef2DebugPath(self, prefix, warpRef, coaddLevel=False)
Return a path to which to write debugging output.
def getGroupDataRef(butler, datasetType, groupTuple, keys)
Base class for coaddition.
Definition: coaddBase.py:90
def findArtifacts(self, templateCoadd, tempExpRefList, imageScalerList)
Find artifacts.
def assembleMetadata(self, coaddExposure, tempExpRefList, weightList)
Set the metadata for the coadd.
def makeSupplementaryData(self, dataRef, selectDataList)
Make additional inputs to assemble() specific to subclasses.
def makeSupplementaryData(self, dataRef, selectDataList)
Make inputs specific to Subclass.
def assemble(self, skyInfo, tempExpRefList, imageScalerList, weightList, bgInfoList=None, altMaskList=None, mask=None, supplementaryData=None)
Assemble a coadd from input warps.
def backgroundMatching(self, inputData, refExpDataRef=None, refImageScaler=None)
Perform background matching on the prepared inputs.
def getTempExpRefList(self, patchRef, calExpRefList)
Generate list data references corresponding to warped exposures that lie within the patch to be coadd...
def run(self, dataRef, selectDataList=[])
Assemble a coadd from a set of Warps.
def _readAndComputeWarpDiff(self, warpRef, imageScaler, templateCoadd)
def assembleSubregion(self, coaddExposure, bbox, tempExpRefList, imageScalerList, weightList, bgInfoList, altMaskList, statsFlags, statsCtrl, nImage=None)
Assemble the coadd for a sub-region.
def prepareInputs(self, refList)
Prepare the input warps for coaddition by measuring the weight for each warp and the scaling for the ...
def assemble(self, skyInfo, tempExpRefList, imageScalerList, weightList, bgModelList, args, kwargs)
Assemble the coadd for a region.
def getSkyInfo(self, patchRef)
Use getSkyinfo to return the skyMap, tract and patch information, wcs and the outer bbox of the patch...
Definition: coaddBase.py:123
def getTempExpDatasetName(self, warpType="direct")
Definition: coaddBase.py:182
def __init__(self, args, kwargs)
Initialize the task and make the assembleStaticSkyModel subtask.
def makeDataRefList(self, namespace)
Make self.refList from self.idList.
def getBadPixelMask(self)
Convenience method to provide the bitmask from the mask plane names.
Definition: coaddBase.py:217
def detectClip(self, exp, tempExpRefList)
Detect clipped regions on an exposure and set the mask on the individual tempExp masks.
Configuration parameters for the SafeClipAssembleCoaddTask.
def __init__(self, args, kwargs)
Initialize the task.
Assemble a coadded image from a set of warps (coadded temporary exposures).
def buildDifferenceImage(self, skyInfo, tempExpRefList, imageScalerList, weightList, bgModelList)
Return an exposure that contains the difference between and unclipped and clipped coadds...
def computeAltMaskList(self, tempExpRefList, maskSpanSets)
Apply artifact span set lists to masks.
Assemble a coadded image from a set of coadded temporary exposures, being careful to clip & flag area...
def selectExposures(self, patchRef, skyInfo=None, selectDataList=[])
Select exposures to coadd.
Definition: coaddBase.py:103
def detectClipBig(self, tempExpClipList, clipFootprints, clipIndices, maskClipValue, maskDetValue)
Find footprints from individual tempExp footprints for large footprints.
Configuration parameters for the AssembleCoaddTask.
Assemble a compareWarp coadded image from a set of warps by masking artifacts detected by comparing P...
def _filterArtifacts(self, spanSetList, epochCountImage, maxNumEpochs=None, minPixels=None)
def __init__(self, args, kwargs)
Initialize the task and make the clipDetection subtask.
def getBackgroundReferenceScaler(self, dataRef)
Construct an image scaler for the background reference frame.
def addBackgroundMatchingMetadata(self, coaddExposure, tempExpRefList, backgroundInfoList)
Add metadata from the background matching to the coadd.
A version of lsst.pipe.base.DataIdContainer specialized for assembleCoadd.
def countMaskFromFootprint(mask, footprint, bitmask, ignoreMask)
Function to count the number of pixels with a specific mask in a footprint.
def assemble(self, skyInfo, tempExpRefList, imageScalerList, weightList, bgModelList, supplementaryData, args, kwargs)
Assemble the coadd.
def groupPatchExposures(patchDataRef, calexpDataRefList, coaddDatasetType="deepCoadd", tempExpDatasetType="deepCoadd_directWarp")
Definition: coaddHelpers.py:63