LSSTApplications  18.1.0
LSSTDataManagementBasePackage
assembleCoadd.py
Go to the documentation of this file.
1 # This file is part of pipe_tasks.
2 #
3 # LSST Data Management System
4 # This product includes software developed by the
5 # LSST Project (http://www.lsst.org/).
6 # See COPYRIGHT file at the top of the source tree.
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the LSST License Statement and
19 # the GNU General Public License along with this program. If not,
20 # see <https://www.lsstcorp.org/LegalNotices/>.
21 #
22 import os
23 import numpy
24 import warnings
25 import lsst.pex.config as pexConfig
26 import lsst.pex.exceptions as pexExceptions
27 import lsst.afw.geom as afwGeom
28 import lsst.afw.image as afwImage
29 import lsst.afw.math as afwMath
30 import lsst.afw.table as afwTable
31 import lsst.afw.detection as afwDet
32 import lsst.coadd.utils as coaddUtils
33 import lsst.pipe.base as pipeBase
34 import lsst.meas.algorithms as measAlg
35 import lsst.log as log
36 import lsstDebug
37 from .coaddBase import CoaddBaseTask, SelectDataIdContainer, makeSkyInfo
38 from .interpImage import InterpImageTask
39 from .scaleZeroPoint import ScaleZeroPointTask
40 from .coaddHelpers import groupPatchExposures, getGroupDataRef
41 from .scaleVariance import ScaleVarianceTask
42 from lsst.meas.algorithms import SourceDetectionTask
43 from lsst.pipe.base.shims import ShimButler
44 
45 __all__ = ["AssembleCoaddTask", "AssembleCoaddConfig", "SafeClipAssembleCoaddTask",
46  "SafeClipAssembleCoaddConfig", "CompareWarpAssembleCoaddTask", "CompareWarpAssembleCoaddConfig"]
47 
48 
49 class AssembleCoaddConfig(CoaddBaseTask.ConfigClass, pipeBase.PipelineTaskConfig):
50  """Configuration parameters for the `AssembleCoaddTask`.
51 
52  Notes
53  -----
54  The `doMaskBrightObjects` and `brightObjectMaskName` configuration options
55  only set the bitplane config.brightObjectMaskName. To make this useful you
56  *must* also configure the flags.pixel algorithm, for example by adding
57 
58  .. code-block:: none
59 
60  config.measurement.plugins["base_PixelFlags"].masksFpCenter.append("BRIGHT_OBJECT")
61  config.measurement.plugins["base_PixelFlags"].masksFpAnywhere.append("BRIGHT_OBJECT")
62 
63  to your measureCoaddSources.py and forcedPhotCoadd.py config overrides.
64  """
65  warpType = pexConfig.Field(
66  doc="Warp name: one of 'direct' or 'psfMatched'",
67  dtype=str,
68  default="direct",
69  )
70  subregionSize = pexConfig.ListField(
71  dtype=int,
72  doc="Width, height of stack subregion size; "
73  "make small enough that a full stack of images will fit into memory at once.",
74  length=2,
75  default=(2000, 2000),
76  )
77  statistic = pexConfig.Field(
78  dtype=str,
79  doc="Main stacking statistic for aggregating over the epochs.",
80  default="MEANCLIP",
81  )
82  doSigmaClip = pexConfig.Field(
83  dtype=bool,
84  doc="Perform sigma clipped outlier rejection with MEANCLIP statistic? (DEPRECATED)",
85  default=False,
86  )
87  sigmaClip = pexConfig.Field(
88  dtype=float,
89  doc="Sigma for outlier rejection; ignored if non-clipping statistic selected.",
90  default=3.0,
91  )
92  clipIter = pexConfig.Field(
93  dtype=int,
94  doc="Number of iterations of outlier rejection; ignored if non-clipping statistic selected.",
95  default=2,
96  )
97  calcErrorFromInputVariance = pexConfig.Field(
98  dtype=bool,
99  doc="Calculate coadd variance from input variance by stacking statistic."
100  "Passed to StatisticsControl.setCalcErrorFromInputVariance()",
101  default=True,
102  )
103  scaleZeroPoint = pexConfig.ConfigurableField(
104  target=ScaleZeroPointTask,
105  doc="Task to adjust the photometric zero point of the coadd temp exposures",
106  )
107  doInterp = pexConfig.Field(
108  doc="Interpolate over NaN pixels? Also extrapolate, if necessary, but the results are ugly.",
109  dtype=bool,
110  default=True,
111  )
112  interpImage = pexConfig.ConfigurableField(
113  target=InterpImageTask,
114  doc="Task to interpolate (and extrapolate) over NaN pixels",
115  )
116  doWrite = pexConfig.Field(
117  doc="Persist coadd?",
118  dtype=bool,
119  default=True,
120  )
121  doNImage = pexConfig.Field(
122  doc="Create image of number of contributing exposures for each pixel",
123  dtype=bool,
124  default=False,
125  )
126  doUsePsfMatchedPolygons = pexConfig.Field(
127  doc="Use ValidPolygons from shrunk Psf-Matched Calexps? Should be set to True by CompareWarp only.",
128  dtype=bool,
129  default=False,
130  )
131  maskPropagationThresholds = pexConfig.DictField(
132  keytype=str,
133  itemtype=float,
134  doc=("Threshold (in fractional weight) of rejection at which we propagate a mask plane to "
135  "the coadd; that is, we set the mask bit on the coadd if the fraction the rejected frames "
136  "would have contributed exceeds this value."),
137  default={"SAT": 0.1},
138  )
139  removeMaskPlanes = pexConfig.ListField(dtype=str, default=["NOT_DEBLENDED"],
140  doc="Mask planes to remove before coadding")
141  doMaskBrightObjects = pexConfig.Field(dtype=bool, default=False,
142  doc="Set mask and flag bits for bright objects?")
143  brightObjectMaskName = pexConfig.Field(dtype=str, default="BRIGHT_OBJECT",
144  doc="Name of mask bit used for bright objects")
145  coaddPsf = pexConfig.ConfigField(
146  doc="Configuration for CoaddPsf",
147  dtype=measAlg.CoaddPsfConfig,
148  )
149  doAttachTransmissionCurve = pexConfig.Field(
150  dtype=bool, default=False, optional=False,
151  doc=("Attach a piecewise TransmissionCurve for the coadd? "
152  "(requires all input Exposures to have TransmissionCurves).")
153  )
154  inputWarps = pipeBase.InputDatasetField(
155  doc=("Input list of warps to be assemebled i.e. stacked."
156  "WarpType (e.g. direct, psfMatched) is controlled by we warpType config parameter"),
157  nameTemplate="{inputCoaddName}Coadd_{warpType}Warp",
158  storageClass="ExposureF",
159  dimensions=("tract", "patch", "skymap", "visit", "instrument"),
160  manualLoad=True,
161  )
162  skyMap = pipeBase.InputDatasetField(
163  doc="Input definition of geometry/bbox and projection/wcs for coadded exposures",
164  nameTemplate="{inputCoaddName}Coadd_skyMap",
165  storageClass="SkyMap",
166  dimensions=("skymap", ),
167  scalar=True
168  )
169  brightObjectMask = pipeBase.InputDatasetField(
170  doc=("Input Bright Object Mask mask produced with external catalogs to be applied to the mask plane"
171  " BRIGHT_OBJECT."),
172  name="brightObjectMask",
173  storageClass="ObjectMaskCatalog",
174  dimensions=("tract", "patch", "skymap", "abstract_filter"),
175  scalar=True
176  )
177  coaddExposure = pipeBase.OutputDatasetField(
178  doc="Output coadded exposure, produced by stacking input warps",
179  nameTemplate="{outputCoaddName}Coadd",
180  storageClass="ExposureF",
181  dimensions=("tract", "patch", "skymap", "abstract_filter"),
182  scalar=True
183  )
184  nImage = pipeBase.OutputDatasetField(
185  doc="Output image of number of input images per pixel",
186  nameTemplate="{outputCoaddName}Coadd_nImage",
187  storageClass="ImageU",
188  dimensions=("tract", "patch", "skymap", "abstract_filter"),
189  scalar=True
190  )
191 
192  hasFakes = pexConfig.Field(
193  dtype=bool,
194  default=False,
195  doc="Should be set to True if fake sources have been inserted into the input data."
196  )
197 
198  def setDefaults(self):
199  super().setDefaults()
200  self.badMaskPlanes = ["NO_DATA", "BAD", "SAT", "EDGE"]
201  self.formatTemplateNames({"inputCoaddName": "deep", "outputCoaddName": "deep",
202  "warpType": self.warpType})
203  self.quantum.dimensions = ("tract", "patch", "abstract_filter", "skymap")
204 
205  def validate(self):
206  super().validate()
207  if self.doPsfMatch:
208  # Backwards compatibility.
209  # Configs do not have loggers
210  log.warn("Config doPsfMatch deprecated. Setting warpType='psfMatched'")
211  self.warpType = 'psfMatched'
212  if self.doSigmaClip and self.statistic != "MEANCLIP":
213  log.warn('doSigmaClip deprecated. To replicate behavior, setting statistic to "MEANCLIP"')
214  self.statistic = "MEANCLIP"
215  if self.doInterp and self.statistic not in ['MEAN', 'MEDIAN', 'MEANCLIP', 'VARIANCE', 'VARIANCECLIP']:
216  raise ValueError("Must set doInterp=False for statistic=%s, which does not "
217  "compute and set a non-zero coadd variance estimate." % (self.statistic))
218 
219  unstackableStats = ['NOTHING', 'ERROR', 'ORMASK']
220  if not hasattr(afwMath.Property, self.statistic) or self.statistic in unstackableStats:
221  stackableStats = [str(k) for k in afwMath.Property.__members__.keys()
222  if str(k) not in unstackableStats]
223  raise ValueError("statistic %s is not allowed. Please choose one of %s."
224  % (self.statistic, stackableStats))
225 
226 
227 class AssembleCoaddTask(CoaddBaseTask, pipeBase.PipelineTask):
228  """Assemble a coadded image from a set of warps (coadded temporary exposures).
229 
230  We want to assemble a coadded image from a set of Warps (also called
231  coadded temporary exposures or ``coaddTempExps``).
232  Each input Warp covers a patch on the sky and corresponds to a single
233  run/visit/exposure of the covered patch. We provide the task with a list
234  of Warps (``selectDataList``) from which it selects Warps that cover the
235  specified patch (pointed at by ``dataRef``).
236  Each Warp that goes into a coadd will typically have an independent
237  photometric zero-point. Therefore, we must scale each Warp to set it to
238  a common photometric zeropoint. WarpType may be one of 'direct' or
239  'psfMatched', and the boolean configs `config.makeDirect` and
240  `config.makePsfMatched` set which of the warp types will be coadded.
241  The coadd is computed as a mean with optional outlier rejection.
242  Criteria for outlier rejection are set in `AssembleCoaddConfig`.
243  Finally, Warps can have bad 'NaN' pixels which received no input from the
244  source calExps. We interpolate over these bad (NaN) pixels.
245 
246  `AssembleCoaddTask` uses several sub-tasks. These are
247 
248  - `ScaleZeroPointTask`
249  - create and use an ``imageScaler`` object to scale the photometric zeropoint for each Warp
250  - `InterpImageTask`
251  - interpolate across bad pixels (NaN) in the final coadd
252 
253  You can retarget these subtasks if you wish.
254 
255  Notes
256  -----
257  The `lsst.pipe.base.cmdLineTask.CmdLineTask` interface supports a
258  flag ``-d`` to import ``debug.py`` from your ``PYTHONPATH``; see
259  `baseDebug` for more about ``debug.py`` files. `AssembleCoaddTask` has
260  no debug variables of its own. Some of the subtasks may support debug
261  variables. See the documentation for the subtasks for further information.
262 
263  Examples
264  --------
265  `AssembleCoaddTask` assembles a set of warped images into a coadded image.
266  The `AssembleCoaddTask` can be invoked by running ``assembleCoadd.py``
267  with the flag '--legacyCoadd'. Usage of assembleCoadd.py expects two
268  inputs: a data reference to the tract patch and filter to be coadded, and
269  a list of Warps to attempt to coadd. These are specified using ``--id`` and
270  ``--selectId``, respectively:
271 
272  .. code-block:: none
273 
274  --id = [KEY=VALUE1[^VALUE2[^VALUE3...] [KEY=VALUE1[^VALUE2[^VALUE3...] ...]]
275  --selectId [KEY=VALUE1[^VALUE2[^VALUE3...] [KEY=VALUE1[^VALUE2[^VALUE3...] ...]]
276 
277  Only the Warps that cover the specified tract and patch will be coadded.
278  A list of the available optional arguments can be obtained by calling
279  ``assembleCoadd.py`` with the ``--help`` command line argument:
280 
281  .. code-block:: none
282 
283  assembleCoadd.py --help
284 
285  To demonstrate usage of the `AssembleCoaddTask` in the larger context of
286  multi-band processing, we will generate the HSC-I & -R band coadds from
287  HSC engineering test data provided in the ``ci_hsc`` package. To begin,
288  assuming that the lsst stack has been already set up, we must set up the
289  obs_subaru and ``ci_hsc`` packages. This defines the environment variable
290  ``$CI_HSC_DIR`` and points at the location of the package. The raw HSC
291  data live in the ``$CI_HSC_DIR/raw directory``. To begin assembling the
292  coadds, we must first
293 
294  - processCcd
295  - process the individual ccds in $CI_HSC_RAW to produce calibrated exposures
296  - makeSkyMap
297  - create a skymap that covers the area of the sky present in the raw exposures
298  - makeCoaddTempExp
299  - warp the individual calibrated exposures to the tangent plane of the coadd
300 
301  We can perform all of these steps by running
302 
303  .. code-block:: none
304 
305  $CI_HSC_DIR scons warp-903986 warp-904014 warp-903990 warp-904010 warp-903988
306 
307  This will produce warped exposures for each visit. To coadd the warped
308  data, we call assembleCoadd.py as follows:
309 
310  .. code-block:: none
311 
312  assembleCoadd.py --legacyCoadd $CI_HSC_DIR/DATA --id patch=5,4 tract=0 filter=HSC-I \
313  --selectId visit=903986 ccd=16 --selectId visit=903986 ccd=22 --selectId visit=903986 ccd=23 \
314  --selectId visit=903986 ccd=100 --selectId visit=904014 ccd=1 --selectId visit=904014 ccd=6 \
315  --selectId visit=904014 ccd=12 --selectId visit=903990 ccd=18 --selectId visit=903990 ccd=25 \
316  --selectId visit=904010 ccd=4 --selectId visit=904010 ccd=10 --selectId visit=904010 ccd=100 \
317  --selectId visit=903988 ccd=16 --selectId visit=903988 ccd=17 --selectId visit=903988 ccd=23 \
318  --selectId visit=903988 ccd=24
319 
320  that will process the HSC-I band data. The results are written in
321  ``$CI_HSC_DIR/DATA/deepCoadd-results/HSC-I``.
322 
323  You may also choose to run:
324 
325  .. code-block:: none
326 
327  scons warp-903334 warp-903336 warp-903338 warp-903342 warp-903344 warp-903346
328  assembleCoadd.py --legacyCoadd $CI_HSC_DIR/DATA --id patch=5,4 tract=0 filter=HSC-R \
329  --selectId visit=903334 ccd=16 --selectId visit=903334 ccd=22 --selectId visit=903334 ccd=23 \
330  --selectId visit=903334 ccd=100 --selectId visit=903336 ccd=17 --selectId visit=903336 ccd=24 \
331  --selectId visit=903338 ccd=18 --selectId visit=903338 ccd=25 --selectId visit=903342 ccd=4 \
332  --selectId visit=903342 ccd=10 --selectId visit=903342 ccd=100 --selectId visit=903344 ccd=0 \
333  --selectId visit=903344 ccd=5 --selectId visit=903344 ccd=11 --selectId visit=903346 ccd=1 \
334  --selectId visit=903346 ccd=6 --selectId visit=903346 ccd=12
335 
336  to generate the coadd for the HSC-R band if you are interested in
337  following multiBand Coadd processing as discussed in `pipeTasks_multiBand`
338  (but note that normally, one would use the `SafeClipAssembleCoaddTask`
339  rather than `AssembleCoaddTask` to make the coadd.
340  """
341  ConfigClass = AssembleCoaddConfig
342  _DefaultName = "assembleCoadd"
343 
344  def __init__(self, *args, **kwargs):
345  # TODO: DM-17415 better way to handle previously allowed passed args e.g.`AssembleCoaddTask(config)`
346  if args:
347  argNames = ["config", "name", "parentTask", "log"]
348  kwargs.update({k: v for k, v in zip(argNames, args)})
349  warnings.warn("AssembleCoadd received positional args, and casting them as kwargs: %s. "
350  "PipelineTask will not take positional args" % argNames, FutureWarning)
351 
352  super().__init__(**kwargs)
353  self.makeSubtask("interpImage")
354  self.makeSubtask("scaleZeroPoint")
355 
356  if self.config.doMaskBrightObjects:
357  mask = afwImage.Mask()
358  try:
359  self.brightObjectBitmask = 1 << mask.addMaskPlane(self.config.brightObjectMaskName)
360  except pexExceptions.LsstCppException:
361  raise RuntimeError("Unable to define mask plane for bright objects; planes used are %s" %
362  mask.getMaskPlaneDict().keys())
363  del mask
364 
365  self.warpType = self.config.warpType
366 
367  @classmethod
368  def getOutputDatasetTypes(cls, config):
369  """Return output dataset type descriptors
370 
371  Remove output dataset types not produced by the Task
372  """
373  outputTypeDict = super().getOutputDatasetTypes(config)
374  if not config.doNImage:
375  outputTypeDict.pop("nImage", None)
376  return outputTypeDict
377 
378  @classmethod
379  def getInputDatasetTypes(cls, config):
380  """Return input dataset type descriptors
381 
382  Remove input dataset types not used by the Task
383  """
384  inputTypeDict = super().getInputDatasetTypes(config)
385  if not config.doMaskBrightObjects:
386  inputTypeDict.pop("brightObjectMask", None)
387  return inputTypeDict
388 
389  @classmethod
390  def getPrerequisiteDatasetTypes(cls, config):
391  return frozenset(["brightObjectMask"])
392 
393  def adaptArgsAndRun(self, inputData, inputDataIds, outputDataIds, butler):
394  """Assemble a coadd from a set of Warps.
395 
396  PipelineTask (Gen3) entry point to Coadd a set of Warps.
397  Analogous to `runDataRef`, it prepares all the data products to be
398  passed to `run`, and processes the results before returning to struct
399  of results to be written out. AssembleCoadd cannot fit all Warps in memory.
400  Therefore, its inputs are accessed subregion by subregion
401  by the `lsst.daf.butler.ShimButler` that quacks like a Gen2
402  `lsst.daf.persistence.Butler`. Updates to this method should
403  correspond to an update in `runDataRef` while both entry points
404  are used.
405 
406  Parameters
407  ----------
408  inputData : `dict`
409  Keys are the names of the configs describing input dataset types.
410  Values are input Python-domain data objects (or lists of objects)
411  retrieved from data butler.
412  inputDataIds : `dict`
413  Keys are the names of the configs describing input dataset types.
414  Values are DataIds (or lists of DataIds) that task consumes for
415  corresponding dataset type.
416  outputDataIds : `dict`
417  Keys are the names of the configs describing input dataset types.
418  Values are DataIds (or lists of DataIds) that task is to produce
419  for corresponding dataset type.
420  butler : `lsst.daf.butler.Butler`
421  Gen3 Butler object for fetching additional data products before
422  running the Task
423 
424  Returns
425  -------
426  result : `lsst.pipe.base.Struct`
427  Result struct with components:
428 
429  - ``coaddExposure`` : coadded exposure (``lsst.afw.image.Exposure``)
430  - ``nImage``: N Image (``lsst.afw.image.Image``)
431  """
432  # Construct skyInfo expected by run
433  # Do not remove skyMap from inputData in case makeSupplementaryDataGen3 needs it
434  skyMap = inputData["skyMap"]
435  outputDataId = next(iter(outputDataIds.values()))
436  inputData['skyInfo'] = makeSkyInfo(skyMap,
437  tractId=outputDataId['tract'],
438  patchId=outputDataId['patch'])
439 
440  # Construct list of input Shim DataRefs that quack like Gen2 DataRefs
441  butlerShim = ShimButler(butler)
442  warpRefList = [butlerShim.dataRef(self.config.inputWarps.name, dataId=dataId)
443  for dataId in inputDataIds['inputWarps']]
444 
445  # Construct output Shim DataRef
446  patchRef = butlerShim.dataRef(self.config.coaddExposure.name, dataId=outputDataIds['coaddExposure'])
447 
448  # Perform same middle steps as `runDataRef` does
449  inputs = self.prepareInputs(warpRefList)
450  self.log.info("Found %d %s", len(inputs.tempExpRefList),
451  self.getTempExpDatasetName(self.warpType))
452  if len(inputs.tempExpRefList) == 0:
453  self.log.warn("No coadd temporary exposures found")
454  return
455 
456  supplementaryData = self.makeSupplementaryDataGen3(inputData, inputDataIds, outputDataIds, butler)
457 
458  retStruct = self.run(inputData['skyInfo'], inputs.tempExpRefList, inputs.imageScalerList,
459  inputs.weightList, supplementaryData=supplementaryData)
460 
461  self.processResults(retStruct.coaddExposure, patchRef)
462  return retStruct
463 
464  @pipeBase.timeMethod
465  def runDataRef(self, dataRef, selectDataList=None, warpRefList=None):
466  """Assemble a coadd from a set of Warps.
467 
468  Pipebase.CmdlineTask entry point to Coadd a set of Warps.
469  Compute weights to be applied to each Warp and
470  find scalings to match the photometric zeropoint to a reference Warp.
471  Assemble the Warps using `run`. Interpolate over NaNs and
472  optionally write the coadd to disk. Return the coadded exposure.
473 
474  Parameters
475  ----------
476  dataRef : `lsst.daf.persistence.butlerSubset.ButlerDataRef`
477  Data reference defining the patch for coaddition and the
478  reference Warp (if ``config.autoReference=False``).
479  Used to access the following data products:
480  - ``self.config.coaddName + "Coadd_skyMap"``
481  - ``self.config.coaddName + "Coadd_ + <warpType> + "Warp"`` (optionally)
482  - ``self.config.coaddName + "Coadd"``
483  selectDataList : `list`
484  List of data references to Calexps. Data to be coadded will be
485  selected from this list based on overlap with the patch defined
486  by dataRef, grouped by visit, and converted to a list of data
487  references to warps.
488  warpRefList : `list`
489  List of data references to Warps to be coadded.
490  Note: `warpRefList` is just the new name for `tempExpRefList`.
491 
492  Returns
493  -------
494  retStruct : `lsst.pipe.base.Struct`
495  Result struct with components:
496 
497  - ``coaddExposure``: coadded exposure (``Exposure``).
498  - ``nImage``: exposure count image (``Image``).
499  """
500  if selectDataList and warpRefList:
501  raise RuntimeError("runDataRef received both a selectDataList and warpRefList, "
502  "and which to use is ambiguous. Please pass only one.")
503 
504  skyInfo = self.getSkyInfo(dataRef)
505  if warpRefList is None:
506  calExpRefList = self.selectExposures(dataRef, skyInfo, selectDataList=selectDataList)
507  if len(calExpRefList) == 0:
508  self.log.warn("No exposures to coadd")
509  return
510  self.log.info("Coadding %d exposures", len(calExpRefList))
511 
512  warpRefList = self.getTempExpRefList(dataRef, calExpRefList)
513 
514  inputData = self.prepareInputs(warpRefList)
515  self.log.info("Found %d %s", len(inputData.tempExpRefList),
516  self.getTempExpDatasetName(self.warpType))
517  if len(inputData.tempExpRefList) == 0:
518  self.log.warn("No coadd temporary exposures found")
519  return
520 
521  supplementaryData = self.makeSupplementaryData(dataRef, warpRefList=inputData.tempExpRefList)
522 
523  retStruct = self.run(skyInfo, inputData.tempExpRefList, inputData.imageScalerList,
524  inputData.weightList, supplementaryData=supplementaryData)
525 
526  self.processResults(retStruct.coaddExposure, dataRef)
527  if self.config.doWrite:
528  if self.getCoaddDatasetName(self.warpType) == "deepCoadd" and self.config.hasFakes:
529  coaddDatasetName = "fakes_" + self.getCoaddDatasetName(self.warpType)
530  else:
531  coaddDatasetName = self.getCoaddDatasetName(self.warpType)
532  self.log.info("Persisting %s" % coaddDatasetName)
533  dataRef.put(retStruct.coaddExposure, coaddDatasetName)
534  if self.config.doNImage and retStruct.nImage is not None:
535  dataRef.put(retStruct.nImage, self.getCoaddDatasetName(self.warpType) + '_nImage')
536 
537  return retStruct
538 
539  def processResults(self, coaddExposure, dataRef):
540  """Interpolate over missing data and mask bright stars.
541 
542  Parameters
543  ----------
544  coaddExposure : `lsst.afw.image.Exposure`
545  The coadded exposure to process.
546  dataRef : `lsst.daf.persistence.ButlerDataRef`
547  Butler data reference for supplementary data.
548  """
549  if self.config.doInterp:
550  self.interpImage.run(coaddExposure.getMaskedImage(), planeName="NO_DATA")
551  # The variance must be positive; work around for DM-3201.
552  varArray = coaddExposure.variance.array
553  with numpy.errstate(invalid="ignore"):
554  varArray[:] = numpy.where(varArray > 0, varArray, numpy.inf)
555 
556  if self.config.doMaskBrightObjects:
557  brightObjectMasks = self.readBrightObjectMasks(dataRef)
558  self.setBrightObjectMasks(coaddExposure, dataRef.dataId, brightObjectMasks)
559 
560  def makeSupplementaryData(self, dataRef, selectDataList=None, warpRefList=None):
561  """Make additional inputs to run() specific to subclasses (Gen2)
562 
563  Duplicates interface of `runDataRef` method
564  Available to be implemented by subclasses only if they need the
565  coadd dataRef for performing preliminary processing before
566  assembling the coadd.
567 
568  Parameters
569  ----------
570  dataRef : `lsst.daf.persistence.ButlerDataRef`
571  Butler data reference for supplementary data.
572  selectDataList : `list`
573  List of data references to Warps.
574  """
575  pass
576 
577  def makeSupplementaryDataGen3(self, inputData, inputDataIds, outputDataIds, butler):
578  """Make additional inputs to run() specific to subclasses (Gen3)
579 
580  Duplicates interface of`adaptArgsAndRun` method.
581  Available to be implemented by subclasses only if they need the
582  coadd dataRef for performing preliminary processing before
583  assembling the coadd.
584 
585  Parameters
586  ----------
587  inputData : `dict`
588  Keys are the names of the configs describing input dataset types.
589  Values are input Python-domain data objects (or lists of objects)
590  retrieved from data butler.
591  inputDataIds : `dict`
592  Keys are the names of the configs describing input dataset types.
593  Values are DataIds (or lists of DataIds) that task consumes for
594  corresponding dataset type.
595  DataIds are guaranteed to match data objects in ``inputData``.
596  outputDataIds : `dict`
597  Keys are the names of the configs describing input dataset types.
598  Values are DataIds (or lists of DataIds) that task is to produce
599  for corresponding dataset type.
600  butler : `lsst.daf.butler.Butler`
601  Gen3 Butler object for fetching additional data products before
602  running the Task
603 
604  Returns
605  -------
606  result : `lsst.pipe.base.Struct`
607  Contains whatever additional data the subclass's `run` method needs
608  """
609  pass
610 
611  def getTempExpRefList(self, patchRef, calExpRefList):
612  """Generate list data references corresponding to warped exposures
613  that lie within the patch to be coadded.
614 
615  Parameters
616  ----------
617  patchRef : `dataRef`
618  Data reference for patch.
619  calExpRefList : `list`
620  List of data references for input calexps.
621 
622  Returns
623  -------
624  tempExpRefList : `list`
625  List of Warp/CoaddTempExp data references.
626  """
627  butler = patchRef.getButler()
628  groupData = groupPatchExposures(patchRef, calExpRefList, self.getCoaddDatasetName(self.warpType),
629  self.getTempExpDatasetName(self.warpType))
630  tempExpRefList = [getGroupDataRef(butler, self.getTempExpDatasetName(self.warpType),
631  g, groupData.keys) for
632  g in groupData.groups.keys()]
633  return tempExpRefList
634 
635  def prepareInputs(self, refList):
636  """Prepare the input warps for coaddition by measuring the weight for
637  each warp and the scaling for the photometric zero point.
638 
639  Each Warp has its own photometric zeropoint and background variance.
640  Before coadding these Warps together, compute a scale factor to
641  normalize the photometric zeropoint and compute the weight for each Warp.
642 
643  Parameters
644  ----------
645  refList : `list`
646  List of data references to tempExp
647 
648  Returns
649  -------
650  result : `lsst.pipe.base.Struct`
651  Result struct with components:
652 
653  - ``tempExprefList``: `list` of data references to tempExp.
654  - ``weightList``: `list` of weightings.
655  - ``imageScalerList``: `list` of image scalers.
656  """
657  statsCtrl = afwMath.StatisticsControl()
658  statsCtrl.setNumSigmaClip(self.config.sigmaClip)
659  statsCtrl.setNumIter(self.config.clipIter)
660  statsCtrl.setAndMask(self.getBadPixelMask())
661  statsCtrl.setNanSafe(True)
662  # compute tempExpRefList: a list of tempExpRef that actually exist
663  # and weightList: a list of the weight of the associated coadd tempExp
664  # and imageScalerList: a list of scale factors for the associated coadd tempExp
665  tempExpRefList = []
666  weightList = []
667  imageScalerList = []
668  tempExpName = self.getTempExpDatasetName(self.warpType)
669  for tempExpRef in refList:
670  if not tempExpRef.datasetExists(tempExpName):
671  self.log.warn("Could not find %s %s; skipping it", tempExpName, tempExpRef.dataId)
672  continue
673 
674  tempExp = tempExpRef.get(tempExpName, immediate=True)
675  # Ignore any input warp that is empty of data
676  if numpy.isnan(tempExp.image.array).all():
677  continue
678  maskedImage = tempExp.getMaskedImage()
679  imageScaler = self.scaleZeroPoint.computeImageScaler(
680  exposure=tempExp,
681  dataRef=tempExpRef,
682  )
683  try:
684  imageScaler.scaleMaskedImage(maskedImage)
685  except Exception as e:
686  self.log.warn("Scaling failed for %s (skipping it): %s", tempExpRef.dataId, e)
687  continue
688  statObj = afwMath.makeStatistics(maskedImage.getVariance(), maskedImage.getMask(),
689  afwMath.MEANCLIP, statsCtrl)
690  meanVar, meanVarErr = statObj.getResult(afwMath.MEANCLIP)
691  weight = 1.0 / float(meanVar)
692  if not numpy.isfinite(weight):
693  self.log.warn("Non-finite weight for %s: skipping", tempExpRef.dataId)
694  continue
695  self.log.info("Weight of %s %s = %0.3f", tempExpName, tempExpRef.dataId, weight)
696 
697  del maskedImage
698  del tempExp
699 
700  tempExpRefList.append(tempExpRef)
701  weightList.append(weight)
702  imageScalerList.append(imageScaler)
703 
704  return pipeBase.Struct(tempExpRefList=tempExpRefList, weightList=weightList,
705  imageScalerList=imageScalerList)
706 
707  def prepareStats(self, mask=None):
708  """Prepare the statistics for coadding images.
709 
710  Parameters
711  ----------
712  mask : `int`, optional
713  Bit mask value to exclude from coaddition.
714 
715  Returns
716  -------
717  stats : `lsst.pipe.base.Struct`
718  Statistics structure with the following fields:
719 
720  - ``statsCtrl``: Statistics control object for coadd
721  (`lsst.afw.math.StatisticsControl`)
722  - ``statsFlags``: Statistic for coadd (`lsst.afw.math.Property`)
723  """
724  if mask is None:
725  mask = self.getBadPixelMask()
726  statsCtrl = afwMath.StatisticsControl()
727  statsCtrl.setNumSigmaClip(self.config.sigmaClip)
728  statsCtrl.setNumIter(self.config.clipIter)
729  statsCtrl.setAndMask(mask)
730  statsCtrl.setNanSafe(True)
731  statsCtrl.setWeighted(True)
732  statsCtrl.setCalcErrorFromInputVariance(self.config.calcErrorFromInputVariance)
733  for plane, threshold in self.config.maskPropagationThresholds.items():
734  bit = afwImage.Mask.getMaskPlane(plane)
735  statsCtrl.setMaskPropagationThreshold(bit, threshold)
736  statsFlags = afwMath.stringToStatisticsProperty(self.config.statistic)
737  return pipeBase.Struct(ctrl=statsCtrl, flags=statsFlags)
738 
739  def run(self, skyInfo, tempExpRefList, imageScalerList, weightList,
740  altMaskList=None, mask=None, supplementaryData=None):
741  """Assemble a coadd from input warps
742 
743  Assemble the coadd using the provided list of coaddTempExps. Since
744  the full coadd covers a patch (a large area), the assembly is
745  performed over small areas on the image at a time in order to
746  conserve memory usage. Iterate over subregions within the outer
747  bbox of the patch using `assembleSubregion` to stack the corresponding
748  subregions from the coaddTempExps with the statistic specified.
749  Set the edge bits the coadd mask based on the weight map.
750 
751  Parameters
752  ----------
753  skyInfo : `lsst.pipe.base.Struct`
754  Struct with geometric information about the patch.
755  tempExpRefList : `list`
756  List of data references to Warps (previously called CoaddTempExps).
757  imageScalerList : `list`
758  List of image scalers.
759  weightList : `list`
760  List of weights
761  altMaskList : `list`, optional
762  List of alternate masks to use rather than those stored with
763  tempExp.
764  mask : `int`, optional
765  Bit mask value to exclude from coaddition.
766  supplementaryData : lsst.pipe.base.Struct, optional
767  Struct with additional data products needed to assemble coadd.
768  Only used by subclasses that implement `makeSupplementaryData`
769  and override `run`.
770 
771  Returns
772  -------
773  result : `lsst.pipe.base.Struct`
774  Result struct with components:
775 
776  - ``coaddExposure``: coadded exposure (``lsst.afw.image.Exposure``).
777  - ``nImage``: exposure count image (``lsst.afw.image.Image``).
778  """
779  tempExpName = self.getTempExpDatasetName(self.warpType)
780  self.log.info("Assembling %s %s", len(tempExpRefList), tempExpName)
781  stats = self.prepareStats(mask=mask)
782 
783  if altMaskList is None:
784  altMaskList = [None]*len(tempExpRefList)
785 
786  coaddExposure = afwImage.ExposureF(skyInfo.bbox, skyInfo.wcs)
787  coaddExposure.setPhotoCalib(self.scaleZeroPoint.getPhotoCalib())
788  coaddExposure.getInfo().setCoaddInputs(self.inputRecorder.makeCoaddInputs())
789  self.assembleMetadata(coaddExposure, tempExpRefList, weightList)
790  coaddMaskedImage = coaddExposure.getMaskedImage()
791  subregionSizeArr = self.config.subregionSize
792  subregionSize = afwGeom.Extent2I(subregionSizeArr[0], subregionSizeArr[1])
793  # if nImage is requested, create a zero one which can be passed to assembleSubregion
794  if self.config.doNImage:
795  nImage = afwImage.ImageU(skyInfo.bbox)
796  else:
797  nImage = None
798  for subBBox in self._subBBoxIter(skyInfo.bbox, subregionSize):
799  try:
800  self.assembleSubregion(coaddExposure, subBBox, tempExpRefList, imageScalerList,
801  weightList, altMaskList, stats.flags, stats.ctrl,
802  nImage=nImage)
803  except Exception as e:
804  self.log.fatal("Cannot compute coadd %s: %s", subBBox, e)
805 
806  self.setInexactPsf(coaddMaskedImage.getMask())
807  # Despite the name, the following doesn't really deal with "EDGE" pixels: it identifies
808  # pixels that didn't receive any unmasked inputs (as occurs around the edge of the field).
809  coaddUtils.setCoaddEdgeBits(coaddMaskedImage.getMask(), coaddMaskedImage.getVariance())
810  return pipeBase.Struct(coaddExposure=coaddExposure, nImage=nImage)
811 
812  def assembleMetadata(self, coaddExposure, tempExpRefList, weightList):
813  """Set the metadata for the coadd.
814 
815  This basic implementation sets the filter from the first input.
816 
817  Parameters
818  ----------
819  coaddExposure : `lsst.afw.image.Exposure`
820  The target exposure for the coadd.
821  tempExpRefList : `list`
822  List of data references to tempExp.
823  weightList : `list`
824  List of weights.
825  """
826  assert len(tempExpRefList) == len(weightList), "Length mismatch"
827  tempExpName = self.getTempExpDatasetName(self.warpType)
828  # We load a single pixel of each coaddTempExp, because we just want to get at the metadata
829  # (and we need more than just the PropertySet that contains the header), which is not possible
830  # with the current butler (see #2777).
831  tempExpList = [tempExpRef.get(tempExpName + "_sub",
832  bbox=afwGeom.Box2I(coaddExposure.getBBox().getMin(),
833  afwGeom.Extent2I(1, 1)), immediate=True)
834  for tempExpRef in tempExpRefList]
835  numCcds = sum(len(tempExp.getInfo().getCoaddInputs().ccds) for tempExp in tempExpList)
836 
837  coaddExposure.setFilter(tempExpList[0].getFilter())
838  coaddInputs = coaddExposure.getInfo().getCoaddInputs()
839  coaddInputs.ccds.reserve(numCcds)
840  coaddInputs.visits.reserve(len(tempExpList))
841 
842  for tempExp, weight in zip(tempExpList, weightList):
843  self.inputRecorder.addVisitToCoadd(coaddInputs, tempExp, weight)
844 
845  if self.config.doUsePsfMatchedPolygons:
846  self.shrinkValidPolygons(coaddInputs)
847 
848  coaddInputs.visits.sort()
849  if self.warpType == "psfMatched":
850  # The modelPsf BBox for a psfMatchedWarp/coaddTempExp was dynamically defined by
851  # ModelPsfMatchTask as the square box bounding its spatially-variable, pre-matched WarpedPsf.
852  # Likewise, set the PSF of a PSF-Matched Coadd to the modelPsf
853  # having the maximum width (sufficient because square)
854  modelPsfList = [tempExp.getPsf() for tempExp in tempExpList]
855  modelPsfWidthList = [modelPsf.computeBBox().getWidth() for modelPsf in modelPsfList]
856  psf = modelPsfList[modelPsfWidthList.index(max(modelPsfWidthList))]
857  else:
858  psf = measAlg.CoaddPsf(coaddInputs.ccds, coaddExposure.getWcs(),
859  self.config.coaddPsf.makeControl())
860  coaddExposure.setPsf(psf)
861  apCorrMap = measAlg.makeCoaddApCorrMap(coaddInputs.ccds, coaddExposure.getBBox(afwImage.PARENT),
862  coaddExposure.getWcs())
863  coaddExposure.getInfo().setApCorrMap(apCorrMap)
864  if self.config.doAttachTransmissionCurve:
865  transmissionCurve = measAlg.makeCoaddTransmissionCurve(coaddExposure.getWcs(), coaddInputs.ccds)
866  coaddExposure.getInfo().setTransmissionCurve(transmissionCurve)
867 
868  def assembleSubregion(self, coaddExposure, bbox, tempExpRefList, imageScalerList, weightList,
869  altMaskList, statsFlags, statsCtrl, nImage=None):
870  """Assemble the coadd for a sub-region.
871 
872  For each coaddTempExp, check for (and swap in) an alternative mask
873  if one is passed. Remove mask planes listed in
874  `config.removeMaskPlanes`. Finally, stack the actual exposures using
875  `lsst.afw.math.statisticsStack` with the statistic specified by
876  statsFlags. Typically, the statsFlag will be one of lsst.afw.math.MEAN for
877  a mean-stack or `lsst.afw.math.MEANCLIP` for outlier rejection using
878  an N-sigma clipped mean where N and iterations are specified by
879  statsCtrl. Assign the stacked subregion back to the coadd.
880 
881  Parameters
882  ----------
883  coaddExposure : `lsst.afw.image.Exposure`
884  The target exposure for the coadd.
885  bbox : `lsst.afw.geom.Box`
886  Sub-region to coadd.
887  tempExpRefList : `list`
888  List of data reference to tempExp.
889  imageScalerList : `list`
890  List of image scalers.
891  weightList : `list`
892  List of weights.
893  altMaskList : `list`
894  List of alternate masks to use rather than those stored with
895  tempExp, or None. Each element is dict with keys = mask plane
896  name to which to add the spans.
897  statsFlags : `lsst.afw.math.Property`
898  Property object for statistic for coadd.
899  statsCtrl : `lsst.afw.math.StatisticsControl`
900  Statistics control object for coadd.
901  nImage : `lsst.afw.image.ImageU`, optional
902  Keeps track of exposure count for each pixel.
903  """
904  self.log.debug("Computing coadd over %s", bbox)
905  tempExpName = self.getTempExpDatasetName(self.warpType)
906  coaddExposure.mask.addMaskPlane("REJECTED")
907  coaddExposure.mask.addMaskPlane("CLIPPED")
908  coaddExposure.mask.addMaskPlane("SENSOR_EDGE")
909  maskMap = self.setRejectedMaskMapping(statsCtrl)
910  clipped = afwImage.Mask.getPlaneBitMask("CLIPPED")
911  maskedImageList = []
912  if nImage is not None:
913  subNImage = afwImage.ImageU(bbox.getWidth(), bbox.getHeight())
914  for tempExpRef, imageScaler, altMask in zip(tempExpRefList, imageScalerList, altMaskList):
915  exposure = tempExpRef.get(tempExpName + "_sub", bbox=bbox)
916  maskedImage = exposure.getMaskedImage()
917  mask = maskedImage.getMask()
918  if altMask is not None:
919  self.applyAltMaskPlanes(mask, altMask)
920  imageScaler.scaleMaskedImage(maskedImage)
921 
922  # Add 1 for each pixel which is not excluded by the exclude mask.
923  # In legacyCoadd, pixels may also be excluded by afwMath.statisticsStack.
924  if nImage is not None:
925  subNImage.getArray()[maskedImage.getMask().getArray() & statsCtrl.getAndMask() == 0] += 1
926  if self.config.removeMaskPlanes:
927  self.removeMaskPlanes(maskedImage)
928  maskedImageList.append(maskedImage)
929 
930  with self.timer("stack"):
931  coaddSubregion = afwMath.statisticsStack(maskedImageList, statsFlags, statsCtrl, weightList,
932  clipped, # also set output to CLIPPED if sigma-clipped
933  maskMap)
934  coaddExposure.maskedImage.assign(coaddSubregion, bbox)
935  if nImage is not None:
936  nImage.assign(subNImage, bbox)
937 
938  def removeMaskPlanes(self, maskedImage):
939  """Unset the mask of an image for mask planes specified in the config.
940 
941  Parameters
942  ----------
943  maskedImage : `lsst.afw.image.MaskedImage`
944  The masked image to be modified.
945  """
946  mask = maskedImage.getMask()
947  for maskPlane in self.config.removeMaskPlanes:
948  try:
949  mask &= ~mask.getPlaneBitMask(maskPlane)
951  self.log.debug("Unable to remove mask plane %s: no mask plane with that name was found.",
952  maskPlane)
953 
954  @staticmethod
955  def setRejectedMaskMapping(statsCtrl):
956  """Map certain mask planes of the warps to new planes for the coadd.
957 
958  If a pixel is rejected due to a mask value other than EDGE, NO_DATA,
959  or CLIPPED, set it to REJECTED on the coadd.
960  If a pixel is rejected due to EDGE, set the coadd pixel to SENSOR_EDGE.
961  If a pixel is rejected due to CLIPPED, set the coadd pixel to CLIPPED.
962 
963  Parameters
964  ----------
965  statsCtrl : `lsst.afw.math.StatisticsControl`
966  Statistics control object for coadd
967 
968  Returns
969  -------
970  maskMap : `list` of `tuple` of `int`
971  A list of mappings of mask planes of the warped exposures to
972  mask planes of the coadd.
973  """
974  edge = afwImage.Mask.getPlaneBitMask("EDGE")
975  noData = afwImage.Mask.getPlaneBitMask("NO_DATA")
976  clipped = afwImage.Mask.getPlaneBitMask("CLIPPED")
977  toReject = statsCtrl.getAndMask() & (~noData) & (~edge) & (~clipped)
978  maskMap = [(toReject, afwImage.Mask.getPlaneBitMask("REJECTED")),
979  (edge, afwImage.Mask.getPlaneBitMask("SENSOR_EDGE")),
980  (clipped, clipped)]
981  return maskMap
982 
983  def applyAltMaskPlanes(self, mask, altMaskSpans):
984  """Apply in place alt mask formatted as SpanSets to a mask.
985 
986  Parameters
987  ----------
988  mask : `lsst.afw.image.Mask`
989  Original mask.
990  altMaskSpans : `dict`
991  SpanSet lists to apply. Each element contains the new mask
992  plane name (e.g. "CLIPPED and/or "NO_DATA") as the key,
993  and list of SpanSets to apply to the mask.
994 
995  Returns
996  -------
997  mask : `lsst.afw.image.Mask`
998  Updated mask.
999  """
1000  if self.config.doUsePsfMatchedPolygons:
1001  if ("NO_DATA" in altMaskSpans) and ("NO_DATA" in self.config.badMaskPlanes):
1002  # Clear away any other masks outside the validPolygons. These pixels are no longer
1003  # contributing to inexact PSFs, and will still be rejected because of NO_DATA
1004  # self.config.doUsePsfMatchedPolygons should be True only in CompareWarpAssemble
1005  # This mask-clearing step must only occur *before* applying the new masks below
1006  for spanSet in altMaskSpans['NO_DATA']:
1007  spanSet.clippedTo(mask.getBBox()).clearMask(mask, self.getBadPixelMask())
1008 
1009  for plane, spanSetList in altMaskSpans.items():
1010  maskClipValue = mask.addMaskPlane(plane)
1011  for spanSet in spanSetList:
1012  spanSet.clippedTo(mask.getBBox()).setMask(mask, 2**maskClipValue)
1013  return mask
1014 
1015  def shrinkValidPolygons(self, coaddInputs):
1016  """Shrink coaddInputs' ccds' ValidPolygons in place.
1017 
1018  Either modify each ccd's validPolygon in place, or if CoaddInputs
1019  does not have a validPolygon, create one from its bbox.
1020 
1021  Parameters
1022  ----------
1023  coaddInputs : `lsst.afw.image.coaddInputs`
1024  Original mask.
1025 
1026  """
1027  for ccd in coaddInputs.ccds:
1028  polyOrig = ccd.getValidPolygon()
1029  validPolyBBox = polyOrig.getBBox() if polyOrig else ccd.getBBox()
1030  validPolyBBox.grow(-self.config.matchingKernelSize//2)
1031  if polyOrig:
1032  validPolygon = polyOrig.intersectionSingle(validPolyBBox)
1033  else:
1034  validPolygon = afwGeom.polygon.Polygon(afwGeom.Box2D(validPolyBBox))
1035  ccd.setValidPolygon(validPolygon)
1036 
1037  def readBrightObjectMasks(self, dataRef):
1038  """Retrieve the bright object masks.
1039 
1040  Returns None on failure.
1041 
1042  Parameters
1043  ----------
1044  dataRef : `lsst.daf.persistence.butlerSubset.ButlerDataRef`
1045  A Butler dataRef.
1046 
1047  Returns
1048  -------
1049  result : `lsst.daf.persistence.butlerSubset.ButlerDataRef`
1050  Bright object mask from the Butler object, or None if it cannot
1051  be retrieved.
1052  """
1053  try:
1054  return dataRef.get("brightObjectMask", immediate=True)
1055  except Exception as e:
1056  self.log.warn("Unable to read brightObjectMask for %s: %s", dataRef.dataId, e)
1057  return None
1058 
1059  def setBrightObjectMasks(self, exposure, dataId, brightObjectMasks):
1060  """Set the bright object masks.
1061 
1062  Parameters
1063  ----------
1064  exposure : `lsst.afw.image.Exposure`
1065  Exposure under consideration.
1066  dataId : `lsst.daf.persistence.dataId`
1067  Data identifier dict for patch.
1068  brightObjectMasks : `lsst.afw.table`
1069  Table of bright objects to mask.
1070  """
1071 
1072  if brightObjectMasks is None:
1073  self.log.warn("Unable to apply bright object mask: none supplied")
1074  return
1075  self.log.info("Applying %d bright object masks to %s", len(brightObjectMasks), dataId)
1076  mask = exposure.getMaskedImage().getMask()
1077  wcs = exposure.getWcs()
1078  plateScale = wcs.getPixelScale().asArcseconds()
1079 
1080  for rec in brightObjectMasks:
1081  center = afwGeom.PointI(wcs.skyToPixel(rec.getCoord()))
1082  if rec["type"] == "box":
1083  assert rec["angle"] == 0.0, ("Angle != 0 for mask object %s" % rec["id"])
1084  width = rec["width"].asArcseconds()/plateScale # convert to pixels
1085  height = rec["height"].asArcseconds()/plateScale # convert to pixels
1086 
1087  halfSize = afwGeom.ExtentI(0.5*width, 0.5*height)
1088  bbox = afwGeom.Box2I(center - halfSize, center + halfSize)
1089 
1090  bbox = afwGeom.BoxI(afwGeom.PointI(int(center[0] - 0.5*width), int(center[1] - 0.5*height)),
1091  afwGeom.PointI(int(center[0] + 0.5*width), int(center[1] + 0.5*height)))
1092  spans = afwGeom.SpanSet(bbox)
1093  elif rec["type"] == "circle":
1094  radius = int(rec["radius"].asArcseconds()/plateScale) # convert to pixels
1095  spans = afwGeom.SpanSet.fromShape(radius, offset=center)
1096  else:
1097  self.log.warn("Unexpected region type %s at %s" % rec["type"], center)
1098  continue
1099  spans.clippedTo(mask.getBBox()).setMask(mask, self.brightObjectBitmask)
1100 
1101  def setInexactPsf(self, mask):
1102  """Set INEXACT_PSF mask plane.
1103 
1104  If any of the input images isn't represented in the coadd (due to
1105  clipped pixels or chip gaps), the `CoaddPsf` will be inexact. Flag
1106  these pixels.
1107 
1108  Parameters
1109  ----------
1110  mask : `lsst.afw.image.Mask`
1111  Coadded exposure's mask, modified in-place.
1112  """
1113  mask.addMaskPlane("INEXACT_PSF")
1114  inexactPsf = mask.getPlaneBitMask("INEXACT_PSF")
1115  sensorEdge = mask.getPlaneBitMask("SENSOR_EDGE") # chip edges (so PSF is discontinuous)
1116  clipped = mask.getPlaneBitMask("CLIPPED") # pixels clipped from coadd
1117  rejected = mask.getPlaneBitMask("REJECTED") # pixels rejected from coadd due to masks
1118  array = mask.getArray()
1119  selected = array & (sensorEdge | clipped | rejected) > 0
1120  array[selected] |= inexactPsf
1121 
1122  @classmethod
1123  def _makeArgumentParser(cls):
1124  """Create an argument parser.
1125  """
1126  parser = pipeBase.ArgumentParser(name=cls._DefaultName)
1127  parser.add_id_argument("--id", cls.ConfigClass().coaddName + "Coadd_" +
1128  cls.ConfigClass().warpType + "Warp",
1129  help="data ID, e.g. --id tract=12345 patch=1,2",
1130  ContainerClass=AssembleCoaddDataIdContainer)
1131  parser.add_id_argument("--selectId", "calexp", help="data ID, e.g. --selectId visit=6789 ccd=0..9",
1132  ContainerClass=SelectDataIdContainer)
1133  return parser
1134 
1135  @staticmethod
1136  def _subBBoxIter(bbox, subregionSize):
1137  """Iterate over subregions of a bbox.
1138 
1139  Parameters
1140  ----------
1141  bbox : `lsst.afw.geom.Box2I`
1142  Bounding box over which to iterate.
1143  subregionSize: `lsst.afw.geom.Extent2I`
1144  Size of sub-bboxes.
1145 
1146  Yields
1147  ------
1148  subBBox : `lsst.afw.geom.Box2I`
1149  Next sub-bounding box of size ``subregionSize`` or smaller; each ``subBBox``
1150  is contained within ``bbox``, so it may be smaller than ``subregionSize`` at
1151  the edges of ``bbox``, but it will never be empty.
1152  """
1153  if bbox.isEmpty():
1154  raise RuntimeError("bbox %s is empty" % (bbox,))
1155  if subregionSize[0] < 1 or subregionSize[1] < 1:
1156  raise RuntimeError("subregionSize %s must be nonzero" % (subregionSize,))
1157 
1158  for rowShift in range(0, bbox.getHeight(), subregionSize[1]):
1159  for colShift in range(0, bbox.getWidth(), subregionSize[0]):
1160  subBBox = afwGeom.Box2I(bbox.getMin() + afwGeom.Extent2I(colShift, rowShift), subregionSize)
1161  subBBox.clip(bbox)
1162  if subBBox.isEmpty():
1163  raise RuntimeError("Bug: empty bbox! bbox=%s, subregionSize=%s, "
1164  "colShift=%s, rowShift=%s" %
1165  (bbox, subregionSize, colShift, rowShift))
1166  yield subBBox
1167 
1168 
1169 class AssembleCoaddDataIdContainer(pipeBase.DataIdContainer):
1170  """A version of `lsst.pipe.base.DataIdContainer` specialized for assembleCoadd.
1171  """
1172 
1173  def makeDataRefList(self, namespace):
1174  """Make self.refList from self.idList.
1175 
1176  Parameters
1177  ----------
1178  namespace
1179  Results of parsing command-line (with ``butler`` and ``log`` elements).
1180  """
1181  datasetType = namespace.config.coaddName + "Coadd"
1182  keysCoadd = namespace.butler.getKeys(datasetType=datasetType, level=self.level)
1183 
1184  for dataId in self.idList:
1185  # tract and patch are required
1186  for key in keysCoadd:
1187  if key not in dataId:
1188  raise RuntimeError("--id must include " + key)
1189 
1190  dataRef = namespace.butler.dataRef(
1191  datasetType=datasetType,
1192  dataId=dataId,
1193  )
1194  self.refList.append(dataRef)
1195 
1196 
1197 def countMaskFromFootprint(mask, footprint, bitmask, ignoreMask):
1198  """Function to count the number of pixels with a specific mask in a
1199  footprint.
1200 
1201  Find the intersection of mask & footprint. Count all pixels in the mask
1202  that are in the intersection that have bitmask set but do not have
1203  ignoreMask set. Return the count.
1204 
1205  Parameters
1206  ----------
1207  mask : `lsst.afw.image.Mask`
1208  Mask to define intersection region by.
1209  footprint : `lsst.afw.detection.Footprint`
1210  Footprint to define the intersection region by.
1211  bitmask
1212  Specific mask that we wish to count the number of occurances of.
1213  ignoreMask
1214  Pixels to not consider.
1215 
1216  Returns
1217  -------
1218  result : `int`
1219  Count of number of pixels in footprint with specified mask.
1220  """
1221  bbox = footprint.getBBox()
1222  bbox.clip(mask.getBBox(afwImage.PARENT))
1223  fp = afwImage.Mask(bbox)
1224  subMask = mask.Factory(mask, bbox, afwImage.PARENT)
1225  footprint.spans.setMask(fp, bitmask)
1226  return numpy.logical_and((subMask.getArray() & fp.getArray()) > 0,
1227  (subMask.getArray() & ignoreMask) == 0).sum()
1228 
1229 
1231  """Configuration parameters for the SafeClipAssembleCoaddTask.
1232  """
1233  clipDetection = pexConfig.ConfigurableField(
1234  target=SourceDetectionTask,
1235  doc="Detect sources on difference between unclipped and clipped coadd")
1236  minClipFootOverlap = pexConfig.Field(
1237  doc="Minimum fractional overlap of clipped footprint with visit DETECTED to be clipped",
1238  dtype=float,
1239  default=0.6
1240  )
1241  minClipFootOverlapSingle = pexConfig.Field(
1242  doc="Minimum fractional overlap of clipped footprint with visit DETECTED to be "
1243  "clipped when only one visit overlaps",
1244  dtype=float,
1245  default=0.5
1246  )
1247  minClipFootOverlapDouble = pexConfig.Field(
1248  doc="Minimum fractional overlap of clipped footprints with visit DETECTED to be "
1249  "clipped when two visits overlap",
1250  dtype=float,
1251  default=0.45
1252  )
1253  maxClipFootOverlapDouble = pexConfig.Field(
1254  doc="Maximum fractional overlap of clipped footprints with visit DETECTED when "
1255  "considering two visits",
1256  dtype=float,
1257  default=0.15
1258  )
1259  minBigOverlap = pexConfig.Field(
1260  doc="Minimum number of pixels in footprint to use DETECTED mask from the single visits "
1261  "when labeling clipped footprints",
1262  dtype=int,
1263  default=100
1264  )
1265 
1266  def setDefaults(self):
1267  """Set default values for clipDetection.
1268 
1269  Notes
1270  -----
1271  The numeric values for these configuration parameters were
1272  empirically determined, future work may further refine them.
1273  """
1274  AssembleCoaddConfig.setDefaults(self)
1275  self.clipDetection.doTempLocalBackground = False
1276  self.clipDetection.reEstimateBackground = False
1277  self.clipDetection.returnOriginalFootprints = False
1278  self.clipDetection.thresholdPolarity = "both"
1279  self.clipDetection.thresholdValue = 2
1280  self.clipDetection.nSigmaToGrow = 2
1281  self.clipDetection.minPixels = 4
1282  self.clipDetection.isotropicGrow = True
1283  self.clipDetection.thresholdType = "pixel_stdev"
1284  self.sigmaClip = 1.5
1285  self.clipIter = 3
1286  self.statistic = "MEAN"
1287 
1288  def validate(self):
1289  if self.doSigmaClip:
1290  log.warn("Additional Sigma-clipping not allowed in Safe-clipped Coadds. "
1291  "Ignoring doSigmaClip.")
1292  self.doSigmaClip = False
1293  if self.statistic != "MEAN":
1294  raise ValueError("Only MEAN statistic allowed for final stacking in SafeClipAssembleCoadd "
1295  "(%s chosen). Please set statistic to MEAN."
1296  % (self.statistic))
1297  AssembleCoaddTask.ConfigClass.validate(self)
1298 
1299 
1301  """Assemble a coadded image from a set of coadded temporary exposures,
1302  being careful to clip & flag areas with potential artifacts.
1303 
1304  In ``AssembleCoaddTask``, we compute the coadd as an clipped mean (i.e.,
1305  we clip outliers). The problem with doing this is that when computing the
1306  coadd PSF at a given location, individual visit PSFs from visits with
1307  outlier pixels contribute to the coadd PSF and cannot be treated correctly.
1308  In this task, we correct for this behavior by creating a new
1309  ``badMaskPlane`` 'CLIPPED'. We populate this plane on the input
1310  coaddTempExps and the final coadd where
1311 
1312  i. difference imaging suggests that there is an outlier and
1313  ii. this outlier appears on only one or two images.
1314 
1315  Such regions will not contribute to the final coadd. Furthermore, any
1316  routine to determine the coadd PSF can now be cognizant of clipped regions.
1317  Note that the algorithm implemented by this task is preliminary and works
1318  correctly for HSC data. Parameter modifications and or considerable
1319  redesigning of the algorithm is likley required for other surveys.
1320 
1321  ``SafeClipAssembleCoaddTask`` uses a ``SourceDetectionTask``
1322  "clipDetection" subtask and also sub-classes ``AssembleCoaddTask``.
1323  You can retarget the ``SourceDetectionTask`` "clipDetection" subtask
1324  if you wish.
1325 
1326  Notes
1327  -----
1328  The `lsst.pipe.base.cmdLineTask.CmdLineTask` interface supports a
1329  flag ``-d`` to import ``debug.py`` from your ``PYTHONPATH``;
1330  see `baseDebug` for more about ``debug.py`` files.
1331  `SafeClipAssembleCoaddTask` has no debug variables of its own.
1332  The ``SourceDetectionTask`` "clipDetection" subtasks may support debug
1333  variables. See the documetation for `SourceDetectionTask` "clipDetection"
1334  for further information.
1335 
1336  Examples
1337  --------
1338  `SafeClipAssembleCoaddTask` assembles a set of warped ``coaddTempExp``
1339  images into a coadded image. The `SafeClipAssembleCoaddTask` is invoked by
1340  running assembleCoadd.py *without* the flag '--legacyCoadd'.
1341 
1342  Usage of ``assembleCoadd.py`` expects a data reference to the tract patch
1343  and filter to be coadded (specified using
1344  '--id = [KEY=VALUE1[^VALUE2[^VALUE3...] [KEY=VALUE1[^VALUE2[^VALUE3...] ...]]')
1345  along with a list of coaddTempExps to attempt to coadd (specified using
1346  '--selectId [KEY=VALUE1[^VALUE2[^VALUE3...] [KEY=VALUE1[^VALUE2[^VALUE3...] ...]]').
1347  Only the coaddTempExps that cover the specified tract and patch will be
1348  coadded. A list of the available optional arguments can be obtained by
1349  calling assembleCoadd.py with the --help command line argument:
1350 
1351  .. code-block:: none
1352 
1353  assembleCoadd.py --help
1354 
1355  To demonstrate usage of the `SafeClipAssembleCoaddTask` in the larger
1356  context of multi-band processing, we will generate the HSC-I & -R band
1357  coadds from HSC engineering test data provided in the ci_hsc package.
1358  To begin, assuming that the lsst stack has been already set up, we must
1359  set up the obs_subaru and ci_hsc packages. This defines the environment
1360  variable $CI_HSC_DIR and points at the location of the package. The raw
1361  HSC data live in the ``$CI_HSC_DIR/raw`` directory. To begin assembling
1362  the coadds, we must first
1363 
1364  - ``processCcd``
1365  process the individual ccds in $CI_HSC_RAW to produce calibrated exposures
1366  - ``makeSkyMap``
1367  create a skymap that covers the area of the sky present in the raw exposures
1368  - ``makeCoaddTempExp``
1369  warp the individual calibrated exposures to the tangent plane of the coadd</DD>
1370 
1371  We can perform all of these steps by running
1372 
1373  .. code-block:: none
1374 
1375  $CI_HSC_DIR scons warp-903986 warp-904014 warp-903990 warp-904010 warp-903988
1376 
1377  This will produce warped coaddTempExps for each visit. To coadd the
1378  warped data, we call ``assembleCoadd.py`` as follows:
1379 
1380  .. code-block:: none
1381 
1382  assembleCoadd.py $CI_HSC_DIR/DATA --id patch=5,4 tract=0 filter=HSC-I \
1383  --selectId visit=903986 ccd=16 --selectId visit=903986 ccd=22 --selectId visit=903986 ccd=23 \
1384  --selectId visit=903986 ccd=100--selectId visit=904014 ccd=1 --selectId visit=904014 ccd=6 \
1385  --selectId visit=904014 ccd=12 --selectId visit=903990 ccd=18 --selectId visit=903990 ccd=25 \
1386  --selectId visit=904010 ccd=4 --selectId visit=904010 ccd=10 --selectId visit=904010 ccd=100 \
1387  --selectId visit=903988 ccd=16 --selectId visit=903988 ccd=17 --selectId visit=903988 ccd=23 \
1388  --selectId visit=903988 ccd=24
1389 
1390  This will process the HSC-I band data. The results are written in
1391  ``$CI_HSC_DIR/DATA/deepCoadd-results/HSC-I``.
1392 
1393  You may also choose to run:
1394 
1395  .. code-block:: none
1396 
1397  scons warp-903334 warp-903336 warp-903338 warp-903342 warp-903344 warp-903346 nnn
1398  assembleCoadd.py $CI_HSC_DIR/DATA --id patch=5,4 tract=0 filter=HSC-R --selectId visit=903334 ccd=16 \
1399  --selectId visit=903334 ccd=22 --selectId visit=903334 ccd=23 --selectId visit=903334 ccd=100 \
1400  --selectId visit=903336 ccd=17 --selectId visit=903336 ccd=24 --selectId visit=903338 ccd=18 \
1401  --selectId visit=903338 ccd=25 --selectId visit=903342 ccd=4 --selectId visit=903342 ccd=10 \
1402  --selectId visit=903342 ccd=100 --selectId visit=903344 ccd=0 --selectId visit=903344 ccd=5 \
1403  --selectId visit=903344 ccd=11 --selectId visit=903346 ccd=1 --selectId visit=903346 ccd=6 \
1404  --selectId visit=903346 ccd=12
1405 
1406  to generate the coadd for the HSC-R band if you are interested in following
1407  multiBand Coadd processing as discussed in ``pipeTasks_multiBand``.
1408  """
1409  ConfigClass = SafeClipAssembleCoaddConfig
1410  _DefaultName = "safeClipAssembleCoadd"
1411 
1412  def __init__(self, *args, **kwargs):
1413  AssembleCoaddTask.__init__(self, *args, **kwargs)
1414  schema = afwTable.SourceTable.makeMinimalSchema()
1415  self.makeSubtask("clipDetection", schema=schema)
1416 
1417  def run(self, skyInfo, tempExpRefList, imageScalerList, weightList, *args, **kwargs):
1418  """Assemble the coadd for a region.
1419 
1420  Compute the difference of coadds created with and without outlier
1421  rejection to identify coadd pixels that have outlier values in some
1422  individual visits.
1423  Detect clipped regions on the difference image and mark these regions
1424  on the one or two individual coaddTempExps where they occur if there
1425  is significant overlap between the clipped region and a source. This
1426  leaves us with a set of footprints from the difference image that have
1427  been identified as having occured on just one or two individual visits.
1428  However, these footprints were generated from a difference image. It
1429  is conceivable for a large diffuse source to have become broken up
1430  into multiple footprints acrosss the coadd difference in this process.
1431  Determine the clipped region from all overlapping footprints from the
1432  detected sources in each visit - these are big footprints.
1433  Combine the small and big clipped footprints and mark them on a new
1434  bad mask plane.
1435  Generate the coadd using `AssembleCoaddTask.run` without outlier
1436  removal. Clipped footprints will no longer make it into the coadd
1437  because they are marked in the new bad mask plane.
1438 
1439  Parameters
1440  ----------
1441  skyInfo : `lsst.pipe.base.Struct`
1442  Patch geometry information, from getSkyInfo
1443  tempExpRefList : `list`
1444  List of data reference to tempExp
1445  imageScalerList : `list`
1446  List of image scalers
1447  weightList : `list`
1448  List of weights
1449 
1450  Returns
1451  -------
1452  result : `lsst.pipe.base.Struct`
1453  Result struct with components:
1454 
1455  - ``coaddExposure``: coadded exposure (``lsst.afw.image.Exposure``).
1456  - ``nImage``: exposure count image (``lsst.afw.image.Image``).
1457 
1458  Notes
1459  -----
1460  args and kwargs are passed but ignored in order to match the call
1461  signature expected by the parent task.
1462  """
1463  exp = self.buildDifferenceImage(skyInfo, tempExpRefList, imageScalerList, weightList)
1464  mask = exp.getMaskedImage().getMask()
1465  mask.addMaskPlane("CLIPPED")
1466 
1467  result = self.detectClip(exp, tempExpRefList)
1468 
1469  self.log.info('Found %d clipped objects', len(result.clipFootprints))
1470 
1471  maskClipValue = mask.getPlaneBitMask("CLIPPED")
1472  maskDetValue = mask.getPlaneBitMask("DETECTED") | mask.getPlaneBitMask("DETECTED_NEGATIVE")
1473  # Append big footprints from individual Warps to result.clipSpans
1474  bigFootprints = self.detectClipBig(result.clipSpans, result.clipFootprints, result.clipIndices,
1475  result.detectionFootprints, maskClipValue, maskDetValue,
1476  exp.getBBox())
1477  # Create mask of the current clipped footprints
1478  maskClip = mask.Factory(mask.getBBox(afwImage.PARENT))
1479  afwDet.setMaskFromFootprintList(maskClip, result.clipFootprints, maskClipValue)
1480 
1481  maskClipBig = maskClip.Factory(mask.getBBox(afwImage.PARENT))
1482  afwDet.setMaskFromFootprintList(maskClipBig, bigFootprints, maskClipValue)
1483  maskClip |= maskClipBig
1484 
1485  # Assemble coadd from base class, but ignoring CLIPPED pixels
1486  badMaskPlanes = self.config.badMaskPlanes[:]
1487  badMaskPlanes.append("CLIPPED")
1488  badPixelMask = afwImage.Mask.getPlaneBitMask(badMaskPlanes)
1489  return AssembleCoaddTask.run(self, skyInfo, tempExpRefList, imageScalerList, weightList,
1490  result.clipSpans, mask=badPixelMask)
1491 
1492  def buildDifferenceImage(self, skyInfo, tempExpRefList, imageScalerList, weightList):
1493  """Return an exposure that contains the difference between unclipped
1494  and clipped coadds.
1495 
1496  Generate a difference image between clipped and unclipped coadds.
1497  Compute the difference image by subtracting an outlier-clipped coadd
1498  from an outlier-unclipped coadd. Return the difference image.
1499 
1500  Parameters
1501  ----------
1502  skyInfo : `lsst.pipe.base.Struct`
1503  Patch geometry information, from getSkyInfo
1504  tempExpRefList : `list`
1505  List of data reference to tempExp
1506  imageScalerList : `list`
1507  List of image scalers
1508  weightList : `list`
1509  List of weights
1510 
1511  Returns
1512  -------
1513  exp : `lsst.afw.image.Exposure`
1514  Difference image of unclipped and clipped coadd wrapped in an Exposure
1515  """
1516  # Clone and upcast self.config because current self.config is frozen
1517  config = AssembleCoaddConfig()
1518  # getattr necessary because subtasks do not survive Config.toDict()
1519  configIntersection = {k: getattr(self.config, k)
1520  for k, v in self.config.toDict().items() if (k in config.keys())}
1521  config.update(**configIntersection)
1522 
1523  # statistic MEAN copied from self.config.statistic, but for clarity explicitly assign
1524  config.statistic = 'MEAN'
1525  task = AssembleCoaddTask(config=config)
1526  coaddMean = task.run(skyInfo, tempExpRefList, imageScalerList, weightList).coaddExposure
1527 
1528  config.statistic = 'MEANCLIP'
1529  task = AssembleCoaddTask(config=config)
1530  coaddClip = task.run(skyInfo, tempExpRefList, imageScalerList, weightList).coaddExposure
1531 
1532  coaddDiff = coaddMean.getMaskedImage().Factory(coaddMean.getMaskedImage())
1533  coaddDiff -= coaddClip.getMaskedImage()
1534  exp = afwImage.ExposureF(coaddDiff)
1535  exp.setPsf(coaddMean.getPsf())
1536  return exp
1537 
1538  def detectClip(self, exp, tempExpRefList):
1539  """Detect clipped regions on an exposure and set the mask on the
1540  individual tempExp masks.
1541 
1542  Detect footprints in the difference image after smoothing the
1543  difference image with a Gaussian kernal. Identify footprints that
1544  overlap with one or two input ``coaddTempExps`` by comparing the
1545  computed overlap fraction to thresholds set in the config. A different
1546  threshold is applied depending on the number of overlapping visits
1547  (restricted to one or two). If the overlap exceeds the thresholds,
1548  the footprint is considered "CLIPPED" and is marked as such on the
1549  coaddTempExp. Return a struct with the clipped footprints, the indices
1550  of the ``coaddTempExps`` that end up overlapping with the clipped
1551  footprints, and a list of new masks for the ``coaddTempExps``.
1552 
1553  Parameters
1554  ----------
1555  exp : `lsst.afw.image.Exposure`
1556  Exposure to run detection on.
1557  tempExpRefList : `list`
1558  List of data reference to tempExp.
1559 
1560  Returns
1561  -------
1562  result : `lsst.pipe.base.Struct`
1563  Result struct with components:
1564 
1565  - ``clipFootprints``: list of clipped footprints.
1566  - ``clipIndices``: indices for each ``clippedFootprint`` in
1567  ``tempExpRefList``.
1568  - ``clipSpans``: List of dictionaries containing spanSet lists
1569  to clip. Each element contains the new maskplane name
1570  ("CLIPPED") as the key and list of ``SpanSets`` as the value.
1571  - ``detectionFootprints``: List of DETECTED/DETECTED_NEGATIVE plane
1572  compressed into footprints.
1573  """
1574  mask = exp.getMaskedImage().getMask()
1575  maskDetValue = mask.getPlaneBitMask("DETECTED") | mask.getPlaneBitMask("DETECTED_NEGATIVE")
1576  fpSet = self.clipDetection.detectFootprints(exp, doSmooth=True, clearMask=True)
1577  # Merge positive and negative together footprints together
1578  fpSet.positive.merge(fpSet.negative)
1579  footprints = fpSet.positive
1580  self.log.info('Found %d potential clipped objects', len(footprints.getFootprints()))
1581  ignoreMask = self.getBadPixelMask()
1582 
1583  clipFootprints = []
1584  clipIndices = []
1585  artifactSpanSets = [{'CLIPPED': list()} for _ in tempExpRefList]
1586 
1587  # for use by detectClipBig
1588  visitDetectionFootprints = []
1589 
1590  dims = [len(tempExpRefList), len(footprints.getFootprints())]
1591  overlapDetArr = numpy.zeros(dims, dtype=numpy.uint16)
1592  ignoreArr = numpy.zeros(dims, dtype=numpy.uint16)
1593 
1594  # Loop over masks once and extract/store only relevant overlap metrics and detection footprints
1595  for i, warpRef in enumerate(tempExpRefList):
1596  tmpExpMask = warpRef.get(self.getTempExpDatasetName(self.warpType),
1597  immediate=True).getMaskedImage().getMask()
1598  maskVisitDet = tmpExpMask.Factory(tmpExpMask, tmpExpMask.getBBox(afwImage.PARENT),
1599  afwImage.PARENT, True)
1600  maskVisitDet &= maskDetValue
1601  visitFootprints = afwDet.FootprintSet(maskVisitDet, afwDet.Threshold(1))
1602  visitDetectionFootprints.append(visitFootprints)
1603 
1604  for j, footprint in enumerate(footprints.getFootprints()):
1605  ignoreArr[i, j] = countMaskFromFootprint(tmpExpMask, footprint, ignoreMask, 0x0)
1606  overlapDetArr[i, j] = countMaskFromFootprint(tmpExpMask, footprint, maskDetValue, ignoreMask)
1607 
1608  # build a list of clipped spans for each visit
1609  for j, footprint in enumerate(footprints.getFootprints()):
1610  nPixel = footprint.getArea()
1611  overlap = [] # hold the overlap with each visit
1612  indexList = [] # index of visit in global list
1613  for i in range(len(tempExpRefList)):
1614  ignore = ignoreArr[i, j]
1615  overlapDet = overlapDetArr[i, j]
1616  totPixel = nPixel - ignore
1617 
1618  # If we have more bad pixels than detection skip
1619  if ignore > overlapDet or totPixel <= 0.5*nPixel or overlapDet == 0:
1620  continue
1621  overlap.append(overlapDet/float(totPixel))
1622  indexList.append(i)
1623 
1624  overlap = numpy.array(overlap)
1625  if not len(overlap):
1626  continue
1627 
1628  keep = False # Should this footprint be marked as clipped?
1629  keepIndex = [] # Which tempExps does the clipped footprint belong to
1630 
1631  # If footprint only has one overlap use a lower threshold
1632  if len(overlap) == 1:
1633  if overlap[0] > self.config.minClipFootOverlapSingle:
1634  keep = True
1635  keepIndex = [0]
1636  else:
1637  # This is the general case where only visit should be clipped
1638  clipIndex = numpy.where(overlap > self.config.minClipFootOverlap)[0]
1639  if len(clipIndex) == 1:
1640  keep = True
1641  keepIndex = [clipIndex[0]]
1642 
1643  # Test if there are clipped objects that overlap two different visits
1644  clipIndex = numpy.where(overlap > self.config.minClipFootOverlapDouble)[0]
1645  if len(clipIndex) == 2 and len(overlap) > 3:
1646  clipIndexComp = numpy.where(overlap <= self.config.minClipFootOverlapDouble)[0]
1647  if numpy.max(overlap[clipIndexComp]) <= self.config.maxClipFootOverlapDouble:
1648  keep = True
1649  keepIndex = clipIndex
1650 
1651  if not keep:
1652  continue
1653 
1654  for index in keepIndex:
1655  globalIndex = indexList[index]
1656  artifactSpanSets[globalIndex]['CLIPPED'].append(footprint.spans)
1657 
1658  clipIndices.append(numpy.array(indexList)[keepIndex])
1659  clipFootprints.append(footprint)
1660 
1661  return pipeBase.Struct(clipFootprints=clipFootprints, clipIndices=clipIndices,
1662  clipSpans=artifactSpanSets, detectionFootprints=visitDetectionFootprints)
1663 
1664  def detectClipBig(self, clipList, clipFootprints, clipIndices, detectionFootprints,
1665  maskClipValue, maskDetValue, coaddBBox):
1666  """Return individual warp footprints for large artifacts and append
1667  them to ``clipList`` in place.
1668 
1669  Identify big footprints composed of many sources in the coadd
1670  difference that may have originated in a large diffuse source in the
1671  coadd. We do this by indentifying all clipped footprints that overlap
1672  significantly with each source in all the coaddTempExps.
1673 
1674  Parameters
1675  ----------
1676  clipList : `list`
1677  List of alt mask SpanSets with clipping information. Modified.
1678  clipFootprints : `list`
1679  List of clipped footprints.
1680  clipIndices : `list`
1681  List of which entries in tempExpClipList each footprint belongs to.
1682  maskClipValue
1683  Mask value of clipped pixels.
1684  maskDetValue
1685  Mask value of detected pixels.
1686  coaddBBox : `lsst.afw.geom.Box`
1687  BBox of the coadd and warps.
1688 
1689  Returns
1690  -------
1691  bigFootprintsCoadd : `list`
1692  List of big footprints
1693  """
1694  bigFootprintsCoadd = []
1695  ignoreMask = self.getBadPixelMask()
1696  for index, (clippedSpans, visitFootprints) in enumerate(zip(clipList, detectionFootprints)):
1697  maskVisitDet = afwImage.MaskX(coaddBBox, 0x0)
1698  for footprint in visitFootprints.getFootprints():
1699  footprint.spans.setMask(maskVisitDet, maskDetValue)
1700 
1701  # build a mask of clipped footprints that are in this visit
1702  clippedFootprintsVisit = []
1703  for foot, clipIndex in zip(clipFootprints, clipIndices):
1704  if index not in clipIndex:
1705  continue
1706  clippedFootprintsVisit.append(foot)
1707  maskVisitClip = maskVisitDet.Factory(maskVisitDet.getBBox(afwImage.PARENT))
1708  afwDet.setMaskFromFootprintList(maskVisitClip, clippedFootprintsVisit, maskClipValue)
1709 
1710  bigFootprintsVisit = []
1711  for foot in visitFootprints.getFootprints():
1712  if foot.getArea() < self.config.minBigOverlap:
1713  continue
1714  nCount = countMaskFromFootprint(maskVisitClip, foot, maskClipValue, ignoreMask)
1715  if nCount > self.config.minBigOverlap:
1716  bigFootprintsVisit.append(foot)
1717  bigFootprintsCoadd.append(foot)
1718 
1719  for footprint in bigFootprintsVisit:
1720  clippedSpans["CLIPPED"].append(footprint.spans)
1721 
1722  return bigFootprintsCoadd
1723 
1724 
1726  assembleStaticSkyModel = pexConfig.ConfigurableField(
1727  target=AssembleCoaddTask,
1728  doc="Task to assemble an artifact-free, PSF-matched Coadd to serve as a"
1729  " naive/first-iteration model of the static sky.",
1730  )
1731  detect = pexConfig.ConfigurableField(
1732  target=SourceDetectionTask,
1733  doc="Detect outlier sources on difference between each psfMatched warp and static sky model"
1734  )
1735  detectTemplate = pexConfig.ConfigurableField(
1736  target=SourceDetectionTask,
1737  doc="Detect sources on static sky model. Only used if doPreserveContainedBySource is True"
1738  )
1739  maxNumEpochs = pexConfig.Field(
1740  doc="Charactistic maximum local number of epochs/visits in which an artifact candidate can appear "
1741  "and still be masked. The effective maxNumEpochs is a broken linear function of local "
1742  "number of epochs (N): min(maxFractionEpochsLow*N, maxNumEpochs + maxFractionEpochsHigh*N). "
1743  "For each footprint detected on the image difference between the psfMatched warp and static sky "
1744  "model, if a significant fraction of pixels (defined by spatialThreshold) are residuals in more "
1745  "than the computed effective maxNumEpochs, the artifact candidate is deemed persistant rather "
1746  "than transient and not masked.",
1747  dtype=int,
1748  default=2
1749  )
1750  maxFractionEpochsLow = pexConfig.RangeField(
1751  doc="Fraction of local number of epochs (N) to use as effective maxNumEpochs for low N. "
1752  "Effective maxNumEpochs = "
1753  "min(maxFractionEpochsLow * N, maxNumEpochs + maxFractionEpochsHigh * N)",
1754  dtype=float,
1755  default=0.4,
1756  min=0., max=1.,
1757  )
1758  maxFractionEpochsHigh = pexConfig.RangeField(
1759  doc="Fraction of local number of epochs (N) to use as effective maxNumEpochs for high N. "
1760  "Effective maxNumEpochs = "
1761  "min(maxFractionEpochsLow * N, maxNumEpochs + maxFractionEpochsHigh * N)",
1762  dtype=float,
1763  default=0.03,
1764  min=0., max=1.,
1765  )
1766  spatialThreshold = pexConfig.RangeField(
1767  doc="Unitless fraction of pixels defining how much of the outlier region has to meet the "
1768  "temporal criteria. If 0, clip all. If 1, clip none.",
1769  dtype=float,
1770  default=0.5,
1771  min=0., max=1.,
1772  inclusiveMin=True, inclusiveMax=True
1773  )
1774  doScaleWarpVariance = pexConfig.Field(
1775  doc="Rescale Warp variance plane using empirical noise?",
1776  dtype=bool,
1777  default=True,
1778  )
1779  scaleWarpVariance = pexConfig.ConfigurableField(
1780  target=ScaleVarianceTask,
1781  doc="Rescale variance on warps",
1782  )
1783  doPreserveContainedBySource = pexConfig.Field(
1784  doc="Rescue artifacts from clipping that completely lie within a footprint detected"
1785  "on the PsfMatched Template Coadd. Replicates a behavior of SafeClip.",
1786  dtype=bool,
1787  default=True,
1788  )
1789  doPrefilterArtifacts = pexConfig.Field(
1790  doc="Ignore artifact candidates that are mostly covered by the bad pixel mask, "
1791  "because they will be excluded anyway. This prevents them from contributing "
1792  "to the outlier epoch count image and potentially being labeled as persistant."
1793  "'Mostly' is defined by the config 'prefilterArtifactsRatio'.",
1794  dtype=bool,
1795  default=True
1796  )
1797  prefilterArtifactsMaskPlanes = pexConfig.ListField(
1798  doc="Prefilter artifact candidates that are mostly covered by these bad mask planes.",
1799  dtype=str,
1800  default=('NO_DATA', 'BAD', 'SAT', 'SUSPECT'),
1801  )
1802  prefilterArtifactsRatio = pexConfig.Field(
1803  doc="Prefilter artifact candidates with less than this fraction overlapping good pixels",
1804  dtype=float,
1805  default=0.05
1806  )
1807  psfMatchedWarps = pipeBase.InputDatasetField(
1808  doc=("PSF-Matched Warps are required by CompareWarp regardless of the coadd type requested. "
1809  "Only PSF-Matched Warps make sense for image subtraction. "
1810  "Therefore, they must be in the InputDatasetField and made available to the task."),
1811  nameTemplate="{inputCoaddName}Coadd_psfMatchedWarp",
1812  storageClass="ExposureF",
1813  dimensions=("tract", "patch", "skymap", "visit"),
1814  manualLoad=True
1815  )
1816 
1817  def setDefaults(self):
1818  AssembleCoaddConfig.setDefaults(self)
1819  self.statistic = 'MEAN'
1821 
1822  # Real EDGE removed by psfMatched NO_DATA border half the width of the matching kernel
1823  # CompareWarp applies psfMatched EDGE pixels to directWarps before assembling
1824  if "EDGE" in self.badMaskPlanes:
1825  self.badMaskPlanes.remove('EDGE')
1826  self.removeMaskPlanes.append('EDGE')
1827  self.assembleStaticSkyModel.badMaskPlanes = ["NO_DATA", ]
1828  self.assembleStaticSkyModel.warpType = 'psfMatched'
1829  self.assembleStaticSkyModel.statistic = 'MEANCLIP'
1830  self.assembleStaticSkyModel.sigmaClip = 2.5
1831  self.assembleStaticSkyModel.clipIter = 3
1832  self.assembleStaticSkyModel.calcErrorFromInputVariance = False
1833  self.assembleStaticSkyModel.doWrite = False
1834  self.detect.doTempLocalBackground = False
1835  self.detect.reEstimateBackground = False
1836  self.detect.returnOriginalFootprints = False
1837  self.detect.thresholdPolarity = "both"
1838  self.detect.thresholdValue = 5
1839  self.detect.nSigmaToGrow = 2
1840  self.detect.minPixels = 4
1841  self.detect.isotropicGrow = True
1842  self.detect.thresholdType = "pixel_stdev"
1843  self.detectTemplate.nSigmaToGrow = 2.0
1844  self.detectTemplate.doTempLocalBackground = False
1845  self.detectTemplate.reEstimateBackground = False
1846  self.detectTemplate.returnOriginalFootprints = False
1847 
1848 
1850  """Assemble a compareWarp coadded image from a set of warps
1851  by masking artifacts detected by comparing PSF-matched warps.
1852 
1853  In ``AssembleCoaddTask``, we compute the coadd as an clipped mean (i.e.,
1854  we clip outliers). The problem with doing this is that when computing the
1855  coadd PSF at a given location, individual visit PSFs from visits with
1856  outlier pixels contribute to the coadd PSF and cannot be treated correctly.
1857  In this task, we correct for this behavior by creating a new badMaskPlane
1858  'CLIPPED' which marks pixels in the individual warps suspected to contain
1859  an artifact. We populate this plane on the input warps by comparing
1860  PSF-matched warps with a PSF-matched median coadd which serves as a
1861  model of the static sky. Any group of pixels that deviates from the
1862  PSF-matched template coadd by more than config.detect.threshold sigma,
1863  is an artifact candidate. The candidates are then filtered to remove
1864  variable sources and sources that are difficult to subtract such as
1865  bright stars. This filter is configured using the config parameters
1866  ``temporalThreshold`` and ``spatialThreshold``. The temporalThreshold is
1867  the maximum fraction of epochs that the deviation can appear in and still
1868  be considered an artifact. The spatialThreshold is the maximum fraction of
1869  pixels in the footprint of the deviation that appear in other epochs
1870  (where other epochs is defined by the temporalThreshold). If the deviant
1871  region meets this criteria of having a significant percentage of pixels
1872  that deviate in only a few epochs, these pixels have the 'CLIPPED' bit
1873  set in the mask. These regions will not contribute to the final coadd.
1874  Furthermore, any routine to determine the coadd PSF can now be cognizant
1875  of clipped regions. Note that the algorithm implemented by this task is
1876  preliminary and works correctly for HSC data. Parameter modifications and
1877  or considerable redesigning of the algorithm is likley required for other
1878  surveys.
1879 
1880  ``CompareWarpAssembleCoaddTask`` sub-classes
1881  ``AssembleCoaddTask`` and instantiates ``AssembleCoaddTask``
1882  as a subtask to generate the TemplateCoadd (the model of the static sky).
1883 
1884  Notes
1885  -----
1886  The `lsst.pipe.base.cmdLineTask.CmdLineTask` interface supports a
1887  flag ``-d`` to import ``debug.py`` from your ``PYTHONPATH``; see
1888  ``baseDebug`` for more about ``debug.py`` files.
1889 
1890  This task supports the following debug variables:
1891 
1892  - ``saveCountIm``
1893  If True then save the Epoch Count Image as a fits file in the `figPath`
1894  - ``figPath``
1895  Path to save the debug fits images and figures
1896 
1897  For example, put something like:
1898 
1899  .. code-block:: python
1900 
1901  import lsstDebug
1902  def DebugInfo(name):
1903  di = lsstDebug.getInfo(name)
1904  if name == "lsst.pipe.tasks.assembleCoadd":
1905  di.saveCountIm = True
1906  di.figPath = "/desired/path/to/debugging/output/images"
1907  return di
1908  lsstDebug.Info = DebugInfo
1909 
1910  into your ``debug.py`` file and run ``assemebleCoadd.py`` with the
1911  ``--debug`` flag. Some subtasks may have their own debug variables;
1912  see individual Task documentation.
1913 
1914  Examples
1915  --------
1916  ``CompareWarpAssembleCoaddTask`` assembles a set of warped images into a
1917  coadded image. The ``CompareWarpAssembleCoaddTask`` is invoked by running
1918  ``assembleCoadd.py`` with the flag ``--compareWarpCoadd``.
1919  Usage of ``assembleCoadd.py`` expects a data reference to the tract patch
1920  and filter to be coadded (specified using
1921  '--id = [KEY=VALUE1[^VALUE2[^VALUE3...] [KEY=VALUE1[^VALUE2[^VALUE3...] ...]]')
1922  along with a list of coaddTempExps to attempt to coadd (specified using
1923  '--selectId [KEY=VALUE1[^VALUE2[^VALUE3...] [KEY=VALUE1[^VALUE2[^VALUE3...] ...]]').
1924  Only the warps that cover the specified tract and patch will be coadded.
1925  A list of the available optional arguments can be obtained by calling
1926  ``assembleCoadd.py`` with the ``--help`` command line argument:
1927 
1928  .. code-block:: none
1929 
1930  assembleCoadd.py --help
1931 
1932  To demonstrate usage of the ``CompareWarpAssembleCoaddTask`` in the larger
1933  context of multi-band processing, we will generate the HSC-I & -R band
1934  oadds from HSC engineering test data provided in the ``ci_hsc`` package.
1935  To begin, assuming that the lsst stack has been already set up, we must
1936  set up the ``obs_subaru`` and ``ci_hsc`` packages.
1937  This defines the environment variable ``$CI_HSC_DIR`` and points at the
1938  location of the package. The raw HSC data live in the ``$CI_HSC_DIR/raw``
1939  directory. To begin assembling the coadds, we must first
1940 
1941  - processCcd
1942  process the individual ccds in $CI_HSC_RAW to produce calibrated exposures
1943  - makeSkyMap
1944  create a skymap that covers the area of the sky present in the raw exposures
1945  - makeCoaddTempExp
1946  warp the individual calibrated exposures to the tangent plane of the coadd
1947 
1948  We can perform all of these steps by running
1949 
1950  .. code-block:: none
1951 
1952  $CI_HSC_DIR scons warp-903986 warp-904014 warp-903990 warp-904010 warp-903988
1953 
1954  This will produce warped ``coaddTempExps`` for each visit. To coadd the
1955  warped data, we call ``assembleCoadd.py`` as follows:
1956 
1957  .. code-block:: none
1958 
1959  assembleCoadd.py --compareWarpCoadd $CI_HSC_DIR/DATA --id patch=5,4 tract=0 filter=HSC-I \
1960  --selectId visit=903986 ccd=16 --selectId visit=903986 ccd=22 --selectId visit=903986 ccd=23 \
1961  --selectId visit=903986 ccd=100 --selectId visit=904014 ccd=1 --selectId visit=904014 ccd=6 \
1962  --selectId visit=904014 ccd=12 --selectId visit=903990 ccd=18 --selectId visit=903990 ccd=25 \
1963  --selectId visit=904010 ccd=4 --selectId visit=904010 ccd=10 --selectId visit=904010 ccd=100 \
1964  --selectId visit=903988 ccd=16 --selectId visit=903988 ccd=17 --selectId visit=903988 ccd=23 \
1965  --selectId visit=903988 ccd=24
1966 
1967  This will process the HSC-I band data. The results are written in
1968  ``$CI_HSC_DIR/DATA/deepCoadd-results/HSC-I``.
1969  """
1970  ConfigClass = CompareWarpAssembleCoaddConfig
1971  _DefaultName = "compareWarpAssembleCoadd"
1972 
1973  def __init__(self, *args, **kwargs):
1974  AssembleCoaddTask.__init__(self, *args, **kwargs)
1975  self.makeSubtask("assembleStaticSkyModel")
1976  detectionSchema = afwTable.SourceTable.makeMinimalSchema()
1977  self.makeSubtask("detect", schema=detectionSchema)
1978  if self.config.doPreserveContainedBySource:
1979  self.makeSubtask("detectTemplate", schema=afwTable.SourceTable.makeMinimalSchema())
1980  if self.config.doScaleWarpVariance:
1981  self.makeSubtask("scaleWarpVariance")
1982 
1983  def makeSupplementaryDataGen3(self, inputData, inputDataIds, outputDataIds, butler):
1984  """Make inputs specific to Subclass with Gen 3 API
1985 
1986  Calls Gen3 `adaptArgsAndRun` instead of the Gen2 specific `runDataRef`
1987 
1988  Duplicates interface of`adaptArgsAndRun` method.
1989  Available to be implemented by subclasses only if they need the
1990  coadd dataRef for performing preliminary processing before
1991  assembling the coadd.
1992 
1993  Parameters
1994  ----------
1995  inputData : `dict`
1996  Keys are the names of the configs describing input dataset types.
1997  Values are input Python-domain data objects (or lists of objects)
1998  retrieved from data butler.
1999  inputDataIds : `dict`
2000  Keys are the names of the configs describing input dataset types.
2001  Values are DataIds (or lists of DataIds) that task consumes for
2002  corresponding dataset type.
2003  DataIds are guaranteed to match data objects in ``inputData``.
2004  outputDataIds : `dict`
2005  Keys are the names of the configs describing input dataset types.
2006  Values are DataIds (or lists of DataIds) that task is to produce
2007  for corresponding dataset type.
2008  butler : `lsst.daf.butler.Butler`
2009  Gen3 Butler object for fetching additional data products before
2010  running the Task
2011 
2012  Returns
2013  -------
2014  result : `lsst.pipe.base.Struct`
2015  Result struct with components:
2016 
2017  - ``templateCoadd`` : coadded exposure (``lsst.afw.image.Exposure``)
2018  - ``nImage``: N Image (``lsst.afw.image.Image``)
2019 
2020 
2021  """
2022  templateCoadd = self.assembleStaticSkyModel.adaptArgsAndRun(inputData, inputDataIds,
2023  outputDataIds, butler)
2024  if templateCoadd is None:
2025  raise RuntimeError(self._noTemplateMessage(self.assembleStaticSkyModel.warpType))
2026 
2027  return pipeBase.Struct(templateCoadd=templateCoadd.coaddExposure,
2028  nImage=templateCoadd.nImage)
2029 
2030  def makeSupplementaryData(self, dataRef, selectDataList=None, warpRefList=None):
2031  """Make inputs specific to Subclass.
2032 
2033  Generate a templateCoadd to use as a native model of static sky to
2034  subtract from warps.
2035 
2036  Parameters
2037  ----------
2038  dataRef : `lsst.daf.persistence.butlerSubset.ButlerDataRef`
2039  Butler dataRef for supplementary data.
2040  selectDataList : `list` (optional)
2041  Optional List of data references to Calexps.
2042  warpRefList : `list` (optional)
2043  Optional List of data references to Warps.
2044 
2045  Returns
2046  -------
2047  result : `lsst.pipe.base.Struct`
2048  Result struct with components:
2049 
2050  - ``templateCoadd``: coadded exposure (``lsst.afw.image.Exposure``)
2051  - ``nImage``: N Image (``lsst.afw.image.Image``)
2052  """
2053  templateCoadd = self.assembleStaticSkyModel.runDataRef(dataRef, selectDataList, warpRefList)
2054  if templateCoadd is None:
2055  raise RuntimeError(self._noTemplateMessage(self.assembleStaticSkyModel.warpType))
2056 
2057  return pipeBase.Struct(templateCoadd=templateCoadd.coaddExposure,
2058  nImage=templateCoadd.nImage)
2059 
2060  def _noTemplateMessage(self, warpType):
2061  warpName = (warpType[0].upper() + warpType[1:])
2062  message = """No %(warpName)s warps were found to build the template coadd which is
2063  required to run CompareWarpAssembleCoaddTask. To continue assembling this type of coadd,
2064  first either rerun makeCoaddTempExp with config.make%(warpName)s=True or
2065  coaddDriver with config.makeCoadTempExp.make%(warpName)s=True, before assembleCoadd.
2066 
2067  Alternatively, to use another algorithm with existing warps, retarget the CoaddDriverConfig to
2068  another algorithm like:
2069 
2070  from lsst.pipe.tasks.assembleCoadd import SafeClipAssembleCoaddTask
2071  config.assemble.retarget(SafeClipAssembleCoaddTask)
2072  """ % {"warpName": warpName}
2073  return message
2074 
2075  def run(self, skyInfo, tempExpRefList, imageScalerList, weightList,
2076  supplementaryData, *args, **kwargs):
2077  """Assemble the coadd.
2078 
2079  Find artifacts and apply them to the warps' masks creating a list of
2080  alternative masks with a new "CLIPPED" plane and updated "NO_DATA"
2081  plane. Then pass these alternative masks to the base class's `run`
2082  method.
2083 
2084  Parameters
2085  ----------
2086  skyInfo : `lsst.pipe.base.Struct`
2087  Patch geometry information.
2088  tempExpRefList : `list`
2089  List of data references to warps.
2090  imageScalerList : `list`
2091  List of image scalers.
2092  weightList : `list`
2093  List of weights.
2094  supplementaryData : `lsst.pipe.base.Struct`
2095  This Struct must contain a ``templateCoadd`` that serves as the
2096  model of the static sky.
2097 
2098  Returns
2099  -------
2100  result : `lsst.pipe.base.Struct`
2101  Result struct with components:
2102 
2103  - ``coaddExposure``: coadded exposure (``lsst.afw.image.Exposure``).
2104  - ``nImage``: exposure count image (``lsst.afw.image.Image``), if requested.
2105  """
2106  templateCoadd = supplementaryData.templateCoadd
2107  spanSetMaskList = self.findArtifacts(templateCoadd, tempExpRefList, imageScalerList)
2108  badMaskPlanes = self.config.badMaskPlanes[:]
2109  badMaskPlanes.append("CLIPPED")
2110  badPixelMask = afwImage.Mask.getPlaneBitMask(badMaskPlanes)
2111 
2112  result = AssembleCoaddTask.run(self, skyInfo, tempExpRefList, imageScalerList, weightList,
2113  spanSetMaskList, mask=badPixelMask)
2114 
2115  # Propagate PSF-matched EDGE pixels to coadd SENSOR_EDGE and INEXACT_PSF
2116  # Psf-Matching moves the real edge inwards
2117  self.applyAltEdgeMask(result.coaddExposure.maskedImage.mask, spanSetMaskList)
2118  return result
2119 
2120  def applyAltEdgeMask(self, mask, altMaskList):
2121  """Propagate alt EDGE mask to SENSOR_EDGE AND INEXACT_PSF planes.
2122 
2123  Parameters
2124  ----------
2125  mask : `lsst.afw.image.Mask`
2126  Original mask.
2127  altMaskList : `list`
2128  List of Dicts containing ``spanSet`` lists.
2129  Each element contains the new mask plane name (e.g. "CLIPPED
2130  and/or "NO_DATA") as the key, and list of ``SpanSets`` to apply to
2131  the mask.
2132  """
2133  maskValue = mask.getPlaneBitMask(["SENSOR_EDGE", "INEXACT_PSF"])
2134  for visitMask in altMaskList:
2135  if "EDGE" in visitMask:
2136  for spanSet in visitMask['EDGE']:
2137  spanSet.clippedTo(mask.getBBox()).setMask(mask, maskValue)
2138 
2139  def findArtifacts(self, templateCoadd, tempExpRefList, imageScalerList):
2140  """Find artifacts.
2141 
2142  Loop through warps twice. The first loop builds a map with the count
2143  of how many epochs each pixel deviates from the templateCoadd by more
2144  than ``config.chiThreshold`` sigma. The second loop takes each
2145  difference image and filters the artifacts detected in each using
2146  count map to filter out variable sources and sources that are
2147  difficult to subtract cleanly.
2148 
2149  Parameters
2150  ----------
2151  templateCoadd : `lsst.afw.image.Exposure`
2152  Exposure to serve as model of static sky.
2153  tempExpRefList : `list`
2154  List of data references to warps.
2155  imageScalerList : `list`
2156  List of image scalers.
2157 
2158  Returns
2159  -------
2160  altMasks : `list`
2161  List of dicts containing information about CLIPPED
2162  (i.e., artifacts), NO_DATA, and EDGE pixels.
2163  """
2164 
2165  self.log.debug("Generating Count Image, and mask lists.")
2166  coaddBBox = templateCoadd.getBBox()
2167  slateIm = afwImage.ImageU(coaddBBox)
2168  epochCountImage = afwImage.ImageU(coaddBBox)
2169  nImage = afwImage.ImageU(coaddBBox)
2170  spanSetArtifactList = []
2171  spanSetNoDataMaskList = []
2172  spanSetEdgeList = []
2173  badPixelMask = self.getBadPixelMask()
2174 
2175  # mask of the warp diffs should = that of only the warp
2176  templateCoadd.mask.clearAllMaskPlanes()
2177 
2178  if self.config.doPreserveContainedBySource:
2179  templateFootprints = self.detectTemplate.detectFootprints(templateCoadd)
2180  else:
2181  templateFootprints = None
2182 
2183  for warpRef, imageScaler in zip(tempExpRefList, imageScalerList):
2184  warpDiffExp = self._readAndComputeWarpDiff(warpRef, imageScaler, templateCoadd)
2185  if warpDiffExp is not None:
2186  # This nImage only approximates the final nImage because it uses the PSF-matched mask
2187  nImage.array += (numpy.isfinite(warpDiffExp.image.array) *
2188  ((warpDiffExp.mask.array & badPixelMask) == 0)).astype(numpy.uint16)
2189  fpSet = self.detect.detectFootprints(warpDiffExp, doSmooth=False, clearMask=True)
2190  fpSet.positive.merge(fpSet.negative)
2191  footprints = fpSet.positive
2192  slateIm.set(0)
2193  spanSetList = [footprint.spans for footprint in footprints.getFootprints()]
2194 
2195  # Remove artifacts due to defects before they contribute to the epochCountImage
2196  if self.config.doPrefilterArtifacts:
2197  spanSetList = self.prefilterArtifacts(spanSetList, warpDiffExp)
2198  for spans in spanSetList:
2199  spans.setImage(slateIm, 1, doClip=True)
2200  epochCountImage += slateIm
2201 
2202  # PSF-Matched warps have less available area (~the matching kernel) because the calexps
2203  # undergo a second convolution. Pixels with data in the direct warp
2204  # but not in the PSF-matched warp will not have their artifacts detected.
2205  # NaNs from the PSF-matched warp therefore must be masked in the direct warp
2206  nans = numpy.where(numpy.isnan(warpDiffExp.maskedImage.image.array), 1, 0)
2207  nansMask = afwImage.makeMaskFromArray(nans.astype(afwImage.MaskPixel))
2208  nansMask.setXY0(warpDiffExp.getXY0())
2209  edgeMask = warpDiffExp.mask
2210  spanSetEdgeMask = afwGeom.SpanSet.fromMask(edgeMask,
2211  edgeMask.getPlaneBitMask("EDGE")).split()
2212  else:
2213  # If the directWarp has <1% coverage, the psfMatchedWarp can have 0% and not exist
2214  # In this case, mask the whole epoch
2215  nansMask = afwImage.MaskX(coaddBBox, 1)
2216  spanSetList = []
2217  spanSetEdgeMask = []
2218 
2219  spanSetNoDataMask = afwGeom.SpanSet.fromMask(nansMask).split()
2220 
2221  spanSetNoDataMaskList.append(spanSetNoDataMask)
2222  spanSetArtifactList.append(spanSetList)
2223  spanSetEdgeList.append(spanSetEdgeMask)
2224 
2225  if lsstDebug.Info(__name__).saveCountIm:
2226  path = self._dataRef2DebugPath("epochCountIm", tempExpRefList[0], coaddLevel=True)
2227  epochCountImage.writeFits(path)
2228 
2229  for i, spanSetList in enumerate(spanSetArtifactList):
2230  if spanSetList:
2231  filteredSpanSetList = self.filterArtifacts(spanSetList, epochCountImage, nImage,
2232  templateFootprints)
2233  spanSetArtifactList[i] = filteredSpanSetList
2234 
2235  altMasks = []
2236  for artifacts, noData, edge in zip(spanSetArtifactList, spanSetNoDataMaskList, spanSetEdgeList):
2237  altMasks.append({'CLIPPED': artifacts,
2238  'NO_DATA': noData,
2239  'EDGE': edge})
2240  return altMasks
2241 
2242  def prefilterArtifacts(self, spanSetList, exp):
2243  """Remove artifact candidates covered by bad mask plane.
2244 
2245  Any future editing of the candidate list that does not depend on
2246  temporal information should go in this method.
2247 
2248  Parameters
2249  ----------
2250  spanSetList : `list`
2251  List of SpanSets representing artifact candidates.
2252  exp : `lsst.afw.image.Exposure`
2253  Exposure containing mask planes used to prefilter.
2254 
2255  Returns
2256  -------
2257  returnSpanSetList : `list`
2258  List of SpanSets with artifacts.
2259  """
2260  badPixelMask = exp.mask.getPlaneBitMask(self.config.prefilterArtifactsMaskPlanes)
2261  goodArr = (exp.mask.array & badPixelMask) == 0
2262  returnSpanSetList = []
2263  bbox = exp.getBBox()
2264  x0, y0 = exp.getXY0()
2265  for i, span in enumerate(spanSetList):
2266  y, x = span.clippedTo(bbox).indices()
2267  yIndexLocal = numpy.array(y) - y0
2268  xIndexLocal = numpy.array(x) - x0
2269  goodRatio = numpy.count_nonzero(goodArr[yIndexLocal, xIndexLocal])/span.getArea()
2270  if goodRatio > self.config.prefilterArtifactsRatio:
2271  returnSpanSetList.append(span)
2272  return returnSpanSetList
2273 
2274  def filterArtifacts(self, spanSetList, epochCountImage, nImage, footprintsToExclude=None):
2275  """Filter artifact candidates.
2276 
2277  Parameters
2278  ----------
2279  spanSetList : `list`
2280  List of SpanSets representing artifact candidates.
2281  epochCountImage : `lsst.afw.image.Image`
2282  Image of accumulated number of warpDiff detections.
2283  nImage : `lsst.afw.image.Image`
2284  Image of the accumulated number of total epochs contributing.
2285 
2286  Returns
2287  -------
2288  maskSpanSetList : `list`
2289  List of SpanSets with artifacts.
2290  """
2291 
2292  maskSpanSetList = []
2293  x0, y0 = epochCountImage.getXY0()
2294  for i, span in enumerate(spanSetList):
2295  y, x = span.indices()
2296  yIdxLocal = [y1 - y0 for y1 in y]
2297  xIdxLocal = [x1 - x0 for x1 in x]
2298  outlierN = epochCountImage.array[yIdxLocal, xIdxLocal]
2299  totalN = nImage.array[yIdxLocal, xIdxLocal]
2300 
2301  # effectiveMaxNumEpochs is broken line (fraction of N) with characteristic config.maxNumEpochs
2302  effMaxNumEpochsHighN = (self.config.maxNumEpochs +
2303  self.config.maxFractionEpochsHigh*numpy.mean(totalN))
2304  effMaxNumEpochsLowN = self.config.maxFractionEpochsLow * numpy.mean(totalN)
2305  effectiveMaxNumEpochs = int(min(effMaxNumEpochsLowN, effMaxNumEpochsHighN))
2306  nPixelsBelowThreshold = numpy.count_nonzero((outlierN > 0) &
2307  (outlierN <= effectiveMaxNumEpochs))
2308  percentBelowThreshold = nPixelsBelowThreshold / len(outlierN)
2309  if percentBelowThreshold > self.config.spatialThreshold:
2310  maskSpanSetList.append(span)
2311 
2312  if self.config.doPreserveContainedBySource and footprintsToExclude is not None:
2313  # If a candidate is contained by a footprint on the template coadd, do not clip
2314  filteredMaskSpanSetList = []
2315  for span in maskSpanSetList:
2316  doKeep = True
2317  for footprint in footprintsToExclude.positive.getFootprints():
2318  if footprint.spans.contains(span):
2319  doKeep = False
2320  break
2321  if doKeep:
2322  filteredMaskSpanSetList.append(span)
2323  maskSpanSetList = filteredMaskSpanSetList
2324 
2325  return maskSpanSetList
2326 
2327  def _readAndComputeWarpDiff(self, warpRef, imageScaler, templateCoadd):
2328  """Fetch a warp from the butler and return a warpDiff.
2329 
2330  Parameters
2331  ----------
2332  warpRef : `lsst.daf.persistence.butlerSubset.ButlerDataRef`
2333  Butler dataRef for the warp.
2334  imageScaler : `lsst.pipe.tasks.scaleZeroPoint.ImageScaler`
2335  An image scaler object.
2336  templateCoadd : `lsst.afw.image.Exposure`
2337  Exposure to be substracted from the scaled warp.
2338 
2339  Returns
2340  -------
2341  warp : `lsst.afw.image.Exposure`
2342  Exposure of the image difference between the warp and template.
2343  """
2344 
2345  # Warp comparison must use PSF-Matched Warps regardless of requested coadd warp type
2346  warpName = self.getTempExpDatasetName('psfMatched')
2347  if not warpRef.datasetExists(warpName):
2348  self.log.warn("Could not find %s %s; skipping it", warpName, warpRef.dataId)
2349  return None
2350  warp = warpRef.get(warpName, immediate=True)
2351  # direct image scaler OK for PSF-matched Warp
2352  imageScaler.scaleMaskedImage(warp.getMaskedImage())
2353  mi = warp.getMaskedImage()
2354  if self.config.doScaleWarpVariance:
2355  try:
2356  self.scaleWarpVariance.run(mi)
2357  except Exception as exc:
2358  self.log.warn("Unable to rescale variance of warp (%s); leaving it as-is" % (exc,))
2359  mi -= templateCoadd.getMaskedImage()
2360  return warp
2361 
2362  def _dataRef2DebugPath(self, prefix, warpRef, coaddLevel=False):
2363  """Return a path to which to write debugging output.
2364 
2365  Creates a hyphen-delimited string of dataId values for simple filenames.
2366 
2367  Parameters
2368  ----------
2369  prefix : `str`
2370  Prefix for filename.
2371  warpRef : `lsst.daf.persistence.butlerSubset.ButlerDataRef`
2372  Butler dataRef to make the path from.
2373  coaddLevel : `bool`, optional.
2374  If True, include only coadd-level keys (e.g., 'tract', 'patch',
2375  'filter', but no 'visit').
2376 
2377  Returns
2378  -------
2379  result : `str`
2380  Path for debugging output.
2381  """
2382  if coaddLevel:
2383  keys = warpRef.getButler().getKeys(self.getCoaddDatasetName(self.warpType))
2384  else:
2385  keys = warpRef.dataId.keys()
2386  keyList = sorted(keys, reverse=True)
2387  directory = lsstDebug.Info(__name__).figPath if lsstDebug.Info(__name__).figPath else "."
2388  filename = "%s-%s.fits" % (prefix, '-'.join([str(warpRef.dataId[k]) for k in keyList]))
2389  return os.path.join(directory, filename)
def setBrightObjectMasks(self, exposure, dataId, brightObjectMasks)
def getCoaddDatasetName(self, warpType="direct")
Definition: coaddBase.py:149
def _dataRef2DebugPath(self, prefix, warpRef, coaddLevel=False)
def getGroupDataRef(butler, datasetType, groupTuple, keys)
Definition: coaddHelpers.py:99
Base class for coaddition.
Definition: coaddBase.py:100
def findArtifacts(self, templateCoadd, tempExpRefList, imageScalerList)
A floating-point coordinate rectangle geometry.
Definition: Box.h:305
def assembleMetadata(self, coaddExposure, tempExpRefList, weightList)
A compact representation of a collection of pixels.
Definition: SpanSet.h:77
void setCoaddEdgeBits(lsst::afw::image::Mask< lsst::afw::image::MaskPixel > &coaddMask, lsst::afw::image::Image< WeightPixelT > const &weightMap)
set edge bits of coadd mask based on weight map
A Threshold is used to pass a threshold value to detection algorithms.
Definition: Threshold.h:43
def getTempExpRefList(self, patchRef, calExpRefList)
def makeSkyInfo(skyMap, tractId, patchId)
Definition: coaddBase.py:249
std::shared_ptr< FrameSet > append(FrameSet const &first, FrameSet const &second)
Construct a FrameSet that performs two transformations in series.
Definition: functional.cc:33
def _readAndComputeWarpDiff(self, warpRef, imageScaler, templateCoadd)
Fit spatial kernel using approximate fluxes for candidates, and solving a linear system of equations...
int min
Definition: Log.h:691
Statistics makeStatistics(lsst::afw::math::MaskedVector< EntryT > const &mv, std::vector< WeightPixel > const &vweights, int const flags, StatisticsControl const &sctrl=StatisticsControl())
The makeStatistics() overload to handle lsst::afw::math::MaskedVector<>
Definition: Statistics.h:520
def applyAltMaskPlanes(self, mask, altMaskSpans)
Property stringToStatisticsProperty(std::string const property)
Conversion function to switch a string to a Property (see Statistics.h)
Definition: Statistics.cc:747
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:133
def getTempExpDatasetName(self, warpType="direct")
Definition: coaddBase.py:164
bool all(CoordinateExpr< N > const &expr) noexcept
Return true if all elements are true.
Pass parameters to a Statistics object.
Definition: Statistics.h:93
def makeSupplementaryDataGen3(self, inputData, inputDataIds, outputDataIds, butler)
def getBadPixelMask(self)
Convenience method to provide the bitmask from the mask plane names.
Definition: coaddBase.py:199
Represent a 2-dimensional array of bitmask pixels.
Definition: Mask.h:78
def assembleSubregion(self, coaddExposure, bbox, tempExpRefList, imageScalerList, weightList, altMaskList, statsFlags, statsCtrl, nImage=None)
def makeSupplementaryData(self, dataRef, selectDataList=None, warpRefList=None)
int max
def adaptArgsAndRun(self, inputData, inputDataIds, outputDataIds, butler)
def makeSupplementaryData(self, dataRef, selectDataList=None, warpRefList=None)
def filterArtifacts(self, spanSetList, epochCountImage, nImage, footprintsToExclude=None)
def run(self, skyInfo, tempExpRefList, imageScalerList, weightList, args, kwargs)
def run(self, skyInfo, tempExpRefList, imageScalerList, weightList, supplementaryData, args, kwargs)
def buildDifferenceImage(self, skyInfo, tempExpRefList, imageScalerList, weightList)
A set of Footprints, associated with a MaskedImage.
Definition: FootprintSet.h:53
def selectExposures(self, patchRef, skyInfo=None, selectDataList=[])
Select exposures to coadd.
Definition: coaddBase.py:113
std::shared_ptr< image::MaskedImage< PixelT > > statisticsStack(image::MaskedImage< PixelT > const &image, Property flags, char dimension, StatisticsControl const &sctrl)
A function to compute statistics on the rows or columns of an image.
Definition: Stack.cc:478
def makeSupplementaryDataGen3(self, inputData, inputDataIds, outputDataIds, butler)
def runDataRef(self, dataRef, selectDataList=None, warpRefList=None)
def processResults(self, coaddExposure, dataRef)
Reports invalid arguments.
Definition: Runtime.h:66
std::vector< SchemaItem< Flag > > * items
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects...
An integer coordinate rectangle.
Definition: Box.h:54
daf::base::PropertyList * list
Definition: fits.cc:885
def run(self, skyInfo, tempExpRefList, imageScalerList, weightList, altMaskList=None, mask=None, supplementaryData=None)
def countMaskFromFootprint(mask, footprint, bitmask, ignoreMask)
def groupPatchExposures(patchDataRef, calexpDataRefList, coaddDatasetType="deepCoadd", tempExpDatasetType="deepCoadd_directWarp")
Definition: coaddHelpers.py:60
def detectClipBig(self, clipList, clipFootprints, clipIndices, detectionFootprints, maskClipValue, maskDetValue, coaddBBox)