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