LSST Applications  21.0.0-147-g0e635eb1+1acddb5be5,22.0.0+052faf71bd,22.0.0+1ea9a8b2b2,22.0.0+6312710a6c,22.0.0+729191ecac,22.0.0+7589c3a021,22.0.0+9f079a9461,22.0.1-1-g7d6de66+b8044ec9de,22.0.1-1-g87000a6+536b1ee016,22.0.1-1-g8e32f31+6312710a6c,22.0.1-10-gd060f87+016f7cdc03,22.0.1-12-g9c3108e+df145f6f68,22.0.1-16-g314fa6d+c825727ab8,22.0.1-19-g93a5c75+d23f2fb6d8,22.0.1-19-gb93eaa13+aab3ef7709,22.0.1-2-g8ef0a89+b8044ec9de,22.0.1-2-g92698f7+9f079a9461,22.0.1-2-ga9b0f51+052faf71bd,22.0.1-2-gac51dbf+052faf71bd,22.0.1-2-gb66926d+6312710a6c,22.0.1-2-gcb770ba+09e3807989,22.0.1-20-g32debb5+b8044ec9de,22.0.1-23-gc2439a9a+fb0756638e,22.0.1-3-g496fd5d+09117f784f,22.0.1-3-g59f966b+1e6ba2c031,22.0.1-3-g849a1b8+f8b568069f,22.0.1-3-gaaec9c0+c5c846a8b1,22.0.1-32-g5ddfab5d3+60ce4897b0,22.0.1-4-g037fbe1+64e601228d,22.0.1-4-g8623105+b8044ec9de,22.0.1-5-g096abc9+d18c45d440,22.0.1-5-g15c806e+57f5c03693,22.0.1-7-gba73697+57f5c03693,master-g6e05de7fdc+c1283a92b8,master-g72cdda8301+729191ecac,w.2021.39
LSST Data Management Base Package
snapCombine.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 # Copyright 2008-2016 AURA/LSST.
4 #
5 # This product includes software developed by the
6 # LSST Project (http://www.lsst.org/).
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 <http://www.lsstcorp.org/LegalNotices/>.
21 #
22 import numpy as num
23 import lsst.pex.config as pexConfig
24 import lsst.daf.base as dafBase
25 import lsst.afw.image as afwImage
26 import lsst.afw.table as afwTable
27 import lsst.pipe.base as pipeBase
28 from lsstDebug import getDebugFrame
29 from lsst.afw.display import getDisplay
30 from lsst.coadd.utils import addToCoadd, setCoaddEdgeBits
31 from lsst.ip.diffim import SnapPsfMatchTask
32 from lsst.meas.algorithms import SourceDetectionTask
33 from lsst.meas.base import SingleFrameMeasurementTask
34 import lsst.meas.algorithms as measAlg
35 
36 from .repair import RepairTask
37 
38 
39 class InitialPsfConfig(pexConfig.Config):
40  """!Describes the initial PSF used for detection and measurement before we do PSF determination."""
41 
42  model = pexConfig.ChoiceField(
43  dtype=str,
44  doc="PSF model type",
45  default="SingleGaussian",
46  allowed={
47  "SingleGaussian": "Single Gaussian model",
48  "DoubleGaussian": "Double Gaussian model",
49  },
50  )
51  pixelScale = pexConfig.Field(
52  dtype=float,
53  doc="Pixel size (arcsec). Only needed if no Wcs is provided",
54  default=0.25,
55  )
56  fwhm = pexConfig.Field(
57  dtype=float,
58  doc="FWHM of PSF model (arcsec)",
59  default=1.0,
60  )
61  size = pexConfig.Field(
62  dtype=int,
63  doc="Size of PSF model (pixels)",
64  default=15,
65  )
66 
67 
68 class SnapCombineConfig(pexConfig.Config):
69  doRepair = pexConfig.Field(
70  dtype=bool,
71  doc="Repair images (CR reject and interpolate) before combining",
72  default=True,
73  )
74  repairPsfFwhm = pexConfig.Field(
75  dtype=float,
76  doc="Psf FWHM (pixels) used to detect CRs",
77  default=2.5,
78  )
79  doDiffIm = pexConfig.Field(
80  dtype=bool,
81  doc="Perform difference imaging before combining",
82  default=False,
83  )
84  doPsfMatch = pexConfig.Field(
85  dtype=bool,
86  doc="Perform PSF matching for difference imaging (ignored if doDiffIm false)",
87  default=True,
88  )
89  doMeasurement = pexConfig.Field(
90  dtype=bool,
91  doc="Measure difference sources (ignored if doDiffIm false)",
92  default=True,
93  )
94  badMaskPlanes = pexConfig.ListField(
95  dtype=str,
96  doc="Mask planes that, if set, the associated pixels are not included in the combined exposure; "
97  "DETECTED excludes cosmic rays",
98  default=("DETECTED",),
99  )
100  averageKeys = pexConfig.ListField(
101  dtype=str,
102  doc="List of float metadata keys to average when combining snaps, e.g. float positions and dates; "
103  "non-float data must be handled by overriding the fixMetadata method",
104  optional=True,
105 
106  )
107  sumKeys = pexConfig.ListField(
108  dtype=str,
109  doc="List of float or int metadata keys to sum when combining snaps, e.g. exposure time; "
110  "non-float, non-int data must be handled by overriding the fixMetadata method",
111  optional=True,
112  )
113 
114  repair = pexConfig.ConfigurableField(target=RepairTask, doc="")
115  diffim = pexConfig.ConfigurableField(target=SnapPsfMatchTask, doc="")
116  detection = pexConfig.ConfigurableField(target=SourceDetectionTask, doc="")
117  initialPsf = pexConfig.ConfigField(dtype=InitialPsfConfig, doc="")
118  measurement = pexConfig.ConfigurableField(target=SingleFrameMeasurementTask, doc="")
119 
120  def setDefaults(self):
121  self.detectiondetection.thresholdPolarity = "both"
122 
123  def validate(self):
124  if self.detectiondetection.thresholdPolarity != "both":
125  raise ValueError("detection.thresholdPolarity must be 'both' for SnapCombineTask")
126 
127 
133 
134 
135 class SnapCombineTask(pipeBase.Task):
136  r"""!
137  \anchor SnapCombineTask_
138 
139  \brief Combine snaps.
140 
141  \section pipe_tasks_snapcombine_Contents Contents
142 
143  - \ref pipe_tasks_snapcombine_Debug
144 
145  \section pipe_tasks_snapcombine_Debug Debug variables
146 
147  The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a
148  flag \c -d to import \b debug.py from your \c PYTHONPATH; see <a
149  href="https://developer.lsst.io/stack/debug.html">Debugging Tasks with lsstDebug</a> for more
150  about \b debug.py files.
151 
152  The available variables in SnapCombineTask are:
153  <DL>
154  <DT> \c display
155  <DD> A dictionary containing debug point names as keys with frame number as value. Valid keys are:
156  <DL>
157  <DT> repair0
158  <DD> Display the first snap after repairing.
159  <DT> repair1
160  <DD> Display the second snap after repairing.
161  </DL>
162  </DD>
163  </DL>
164  """
165  ConfigClass = SnapCombineConfig
166  _DefaultName = "snapCombine"
167 
168  def __init__(self, *args, **kwargs):
169  pipeBase.Task.__init__(self, *args, **kwargs)
170  self.makeSubtask("repair")
171  self.makeSubtask("diffim")
172  self.schemaschema = afwTable.SourceTable.makeMinimalSchema()
173  self.algMetadataalgMetadata = dafBase.PropertyList()
174  self.makeSubtask("detection", schema=self.schemaschema)
175  if self.config.doMeasurement:
176  self.makeSubtask("measurement", schema=self.schemaschema, algMetadata=self.algMetadataalgMetadata)
177 
178  @pipeBase.timeMethod
179  def run(self, snap0, snap1, defects=None):
180  """Combine two snaps
181 
182  @param[in] snap0: snapshot exposure 0
183  @param[in] snap1: snapshot exposure 1
184  @defects[in] defect list (for repair task)
185  @return a pipe_base Struct with fields:
186  - exposure: snap-combined exposure
187  - sources: detected sources, or None if detection not performed
188  """
189  # initialize optional outputs
190  sources = None
191 
192  if self.config.doRepair:
193  self.log.info("snapCombine repair")
194  psf = self.makeInitialPsfmakeInitialPsf(snap0, fwhmPix=self.config.repairPsfFwhm)
195  snap0.setPsf(psf)
196  snap1.setPsf(psf)
197  self.repair.run(snap0, defects=defects, keepCRs=False)
198  self.repair.run(snap1, defects=defects, keepCRs=False)
199 
200  repair0frame = getDebugFrame(self._display, "repair0")
201  if repair0frame:
202  getDisplay(repair0frame).mtv(snap0)
203  repair1frame = getDebugFrame(self._display, "repair1")
204  if repair1frame:
205  getDisplay(repair1frame).mtv(snap1)
206 
207  if self.config.doDiffIm:
208  if self.config.doPsfMatch:
209  self.log.info("snapCombine psfMatch")
210  diffRet = self.diffim.run(snap0, snap1, "subtractExposures")
211  diffExp = diffRet.subtractedImage
212 
213  # Measure centroid and width of kernel; dependent on ticket #1980
214  # Useful diagnostic for the degree of astrometric shift between snaps.
215  diffKern = diffRet.psfMatchingKernel
216  width, height = diffKern.getDimensions()
217 
218  else:
219  diffExp = afwImage.ExposureF(snap0, True)
220  diffMi = diffExp.getMaskedImage()
221  diffMi -= snap1.getMaskedImage()
222 
223  psf = self.makeInitialPsfmakeInitialPsf(snap0)
224  diffExp.setPsf(psf)
225  table = afwTable.SourceTable.make(self.schemaschema)
226  table.setMetadata(self.algMetadataalgMetadata)
227  detRet = self.detection.run(table, diffExp)
228  sources = detRet.sources
229  fpSets = detRet.fpSets
230  if self.config.doMeasurement:
231  self.measurement.measure(diffExp, sources)
232 
233  mask0 = snap0.getMaskedImage().getMask()
234  mask1 = snap1.getMaskedImage().getMask()
235  fpSets.positive.setMask(mask0, "DETECTED")
236  fpSets.negative.setMask(mask1, "DETECTED")
237 
238  maskD = diffExp.getMaskedImage().getMask()
239  fpSets.positive.setMask(maskD, "DETECTED")
240  fpSets.negative.setMask(maskD, "DETECTED_NEGATIVE")
241 
242  combinedExp = self.addSnapsaddSnaps(snap0, snap1)
243 
244  return pipeBase.Struct(
245  exposure=combinedExp,
246  sources=sources,
247  )
248 
249  def addSnaps(self, snap0, snap1):
250  """Add two snap exposures together, returning a new exposure
251 
252  @param[in] snap0 snap exposure 0
253  @param[in] snap1 snap exposure 1
254  @return combined exposure
255  """
256  self.log.info("snapCombine addSnaps")
257 
258  combinedExp = snap0.Factory(snap0, True)
259  combinedMi = combinedExp.getMaskedImage()
260  combinedMi.set(0)
261 
262  weightMap = combinedMi.getImage().Factory(combinedMi.getBBox())
263  weight = 1.0
264  badPixelMask = afwImage.Mask.getPlaneBitMask(self.config.badMaskPlanes)
265  addToCoadd(combinedMi, weightMap, snap0.getMaskedImage(), badPixelMask, weight)
266  addToCoadd(combinedMi, weightMap, snap1.getMaskedImage(), badPixelMask, weight)
267 
268  # pre-scaling the weight map instead of post-scaling the combinedMi saves a bit of time
269  # because the weight map is a simple Image instead of a MaskedImage
270  weightMap *= 0.5 # so result is sum of both images, instead of average
271  combinedMi /= weightMap
272  setCoaddEdgeBits(combinedMi.getMask(), weightMap)
273 
274  # note: none of the inputs has a valid PhotoCalib object, so that is not touched
275  # Filter was already copied
276 
277  combinedMetadata = combinedExp.getMetadata()
278  metadata0 = snap0.getMetadata()
279  metadata1 = snap1.getMetadata()
280  self.fixMetadatafixMetadata(combinedMetadata, metadata0, metadata1)
281 
282  return combinedExp
283 
284  def fixMetadata(self, combinedMetadata, metadata0, metadata1):
285  """Fix the metadata of the combined exposure (in place)
286 
287  This implementation handles items specified by config.averageKeys and config.sumKeys,
288  which have data type restrictions. To handle other data types (such as sexagesimal
289  positions and ISO dates) you must supplement this method with your own code.
290 
291  @param[in,out] combinedMetadata metadata of combined exposure;
292  on input this is a deep copy of metadata0 (a PropertySet)
293  @param[in] metadata0 metadata of snap0 (a PropertySet)
294  @param[in] metadata1 metadata of snap1 (a PropertySet)
295 
296  @note the inputs are presently PropertySets due to ticket #2542. However, in some sense
297  they are just PropertyLists that are missing some methods. In particular: comments and order
298  are preserved if you alter an existing value with set(key, value).
299  """
300  keyDoAvgList = []
301  if self.config.averageKeys:
302  keyDoAvgList += [(key, 1) for key in self.config.averageKeys]
303  if self.config.sumKeys:
304  keyDoAvgList += [(key, 0) for key in self.config.sumKeys]
305  for key, doAvg in keyDoAvgList:
306  opStr = "average" if doAvg else "sum"
307  try:
308  val0 = metadata0.getScalar(key)
309  val1 = metadata1.getScalar(key)
310  except Exception:
311  self.log.warning("Could not %s metadata %r: missing from one or both exposures", opStr, key)
312  continue
313 
314  try:
315  combinedVal = val0 + val1
316  if doAvg:
317  combinedVal /= 2.0
318  except Exception:
319  self.log.warning("Could not %s metadata %r: value %r and/or %r not numeric",
320  opStr, key, val0, val1)
321  continue
322 
323  combinedMetadata.set(key, combinedVal)
324 
325  def makeInitialPsf(self, exposure, fwhmPix=None):
326  """Initialise the detection procedure by setting the PSF and WCS
327 
328  @param exposure Exposure to process
329  @return PSF, WCS
330  """
331  assert exposure, "No exposure provided"
332  wcs = exposure.getWcs()
333  assert wcs, "No wcs in exposure"
334 
335  if fwhmPix is None:
336  fwhmPix = self.config.initialPsf.fwhm / wcs.getPixelScale().asArcseconds()
337 
338  size = self.config.initialPsf.size
339  model = self.config.initialPsf.model
340  self.log.info("installInitialPsf fwhm=%s pixels; size=%s pixels", fwhmPix, size)
341  psfCls = getattr(measAlg, model + "Psf")
342  psf = psfCls(size, size, fwhmPix/(2.0*num.sqrt(2*num.log(2.0))))
343  return psf
Class for storing ordered metadata with comments.
Definition: PropertyList.h:68
Describes the initial PSF used for detection and measurement before we do PSF determination.
Definition: snapCombine.py:39
def fixMetadata(self, combinedMetadata, metadata0, metadata1)
Definition: snapCombine.py:284
def makeInitialPsf(self, exposure, fwhmPix=None)
Definition: snapCombine.py:325
def run(self, snap0, snap1, defects=None)
Definition: snapCombine.py:179
def __init__(self, *args, **kwargs)
Definition: snapCombine.py:168
def mtv(data, frame=None, title="", wcs=None, *args, **kwargs)
Definition: ds9.py:92
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects.
def measure(mi, x, y, size, statistic, stats)
Definition: fringe.py:516
Fit spatial kernel using approximate fluxes for candidates, and solving a linear system of equations.
def getDebugFrame(debugDisplay, name)
Definition: lsstDebug.py:95