lsst.pipe.tasks  13.0-64-gd461ca2f+3
measurePsf.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 # Copyright 2008, 2009, 2010, 2011 LSST Corporation.
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 from __future__ import absolute_import, division, print_function
23 import random
24 
25 import lsst.afw.math as afwMath
26 import lsst.afw.display.ds9 as ds9
27 import lsst.meas.algorithms as measAlg
28 import lsst.meas.algorithms.utils as maUtils
29 import lsst.pex.config as pexConfig
30 import lsst.pipe.base as pipeBase
31 
32 
33 class MeasurePsfConfig(pexConfig.Config):
34  starSelector = measAlg.starSelectorRegistry.makeField("Star selection algorithm", default="objectSize")
35  psfDeterminer = measAlg.psfDeterminerRegistry.makeField("PSF Determination algorithm", default="pca")
36  reserveFraction = pexConfig.Field(
37  dtype=float,
38  doc="Fraction of PSF candidates to reserve from fitting; none if <= 0",
39  default=-1.0,
40  )
41  reserveSeed = pexConfig.Field(
42  dtype = int,
43  doc = "This number will be multiplied by the exposure ID "
44  "to set the random seed for reserving candidates",
45  default = 1,
46  )
47 
48 
54 
55 
56 class MeasurePsfTask(pipeBase.Task):
57  r"""!
58 \anchor MeasurePsfTask_
59 
60 \brief Measure the PSF
61 
62 \section pipe_tasks_measurePsf_Contents Contents
63 
64  - \ref pipe_tasks_measurePsf_Purpose
65  - \ref pipe_tasks_measurePsf_Initialize
66  - \ref pipe_tasks_measurePsf_IO
67  - \ref pipe_tasks_measurePsf_Config
68  - \ref pipe_tasks_measurePsf_Debug
69  - \ref pipe_tasks_measurePsf_Example
70 
71 \section pipe_tasks_measurePsf_Purpose Description
72 
73 A task that selects stars from a catalog of sources and uses those to measure the PSF.
74 
75 The star selector is a subclass of
76 \ref lsst.meas.algorithms.starSelector.BaseStarSelectorTask "lsst.meas.algorithms.BaseStarSelectorTask"
77 and the PSF determiner is a sublcass of
78 \ref lsst.meas.algorithms.psfDeterminer.BasePsfDeterminerTask "lsst.meas.algorithms.BasePsfDeterminerTask"
79 
80 \warning
81 There is no establised set of configuration parameters for these algorithms, so once you start modifying
82 parameters (as we do in \ref pipe_tasks_measurePsf_Example) your code is no longer portable.
83 
84 \section pipe_tasks_measurePsf_Initialize Task initialisation
85 
86 \copydoc \_\_init\_\_
87 
88 \section pipe_tasks_measurePsf_IO Invoking the Task
89 
90 \copydoc run
91 
92 \section pipe_tasks_measurePsf_Config Configuration parameters
93 
94 See \ref MeasurePsfConfig.
95 
96 \section pipe_tasks_measurePsf_Debug Debug variables
97 
98 The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a
99 flag \c -d to import \b debug.py from your \c PYTHONPATH; see \ref baseDebug for more about \b debug.py files.
100 
101 <DL>
102  <DT> \c display
103  <DD> If True, display debugging plots
104  <DT> displayExposure
105  <DD> display the Exposure + spatialCells
106  <DT> displayPsfCandidates
107  <DD> show mosaic of candidates
108  <DT> showBadCandidates
109  <DD> Include bad candidates
110  <DT> displayPsfMosaic
111  <DD> show mosaic of reconstructed PSF(xy)
112  <DT> displayResiduals
113  <DD> show residuals
114  <DT> normalizeResiduals
115  <DD> Normalise residuals by object amplitude
116 </DL>
117 
118 Additionally you can enable any debug outputs that your chosen star selector and psf determiner support.
119 
120 \section pipe_tasks_measurePsf_Example A complete example of using MeasurePsfTask
121 
122 This code is in \link measurePsfTask.py\endlink in the examples directory, and can be run as \em e.g.
123 \code
124 examples/measurePsfTask.py --ds9
125 \endcode
126 \dontinclude measurePsfTask.py
127 
128 The example also runs SourceDetectionTask and SourceMeasurementTask;
129 see \ref meas_algorithms_measurement_Example for more explanation.
130 
131 Import the tasks (there are some other standard imports; read the file to see them all):
132 
133 \skip SourceDetectionTask
134 \until MeasurePsfTask
135 
136 We need to create the tasks before processing any data as the task constructor
137 can add an extra column to the schema, but first we need an almost-empty
138 Schema:
139 
140 \skipline makeMinimalSchema
141 
142 We can now call the constructors for the tasks we need to find and characterize candidate
143 PSF stars:
144 
145 \skip SourceDetectionTask.ConfigClass
146 \until measureTask
147 
148 Note that we've chosen a minimal set of measurement plugins: we need the
149 outputs of \c base_SdssCentroid, \c base_SdssShape and \c base_CircularApertureFlux as
150 inputs to the PSF measurement algorithm, while \c base_PixelFlags identifies
151 and flags bad sources (e.g. with pixels too close to the edge) so they can be
152 removed later.
153 
154 Now we can create and configure the task that we're interested in:
155 
156 \skip MeasurePsfTask
157 \until measurePsfTask
158 
159 We're now ready to process the data (we could loop over multiple exposures/catalogues using the same
160 task objects). First create the output table:
161 
162 \skipline afwTable
163 
164 And process the image:
165 
166 \skip sources =
167 \until result
168 
169 We can then unpack and use the results:
170 
171 \skip psf
172 \until cellSet
173 
174 If you specified \c --ds9 you can see the PSF candidates:
175 
176 \skip display
177 \until RED
178 
179 <HR>
180 
181 To investigate the \ref pipe_tasks_measurePsf_Debug, put something like
182 \code{.py}
183  import lsstDebug
184  def DebugInfo(name):
185  di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively
186 
187  if name == "lsst.pipe.tasks.measurePsf" :
188  di.display = True
189  di.displayExposure = False # display the Exposure + spatialCells
190  di.displayPsfCandidates = True # show mosaic of candidates
191  di.displayPsfMosaic = True # show mosaic of reconstructed PSF(xy)
192  di.displayResiduals = True # show residuals
193  di.showBadCandidates = True # Include bad candidates
194  di.normalizeResiduals = False # Normalise residuals by object amplitude
195 
196  return di
197 
198  lsstDebug.Info = DebugInfo
199 \endcode
200 into your debug.py file and run measurePsfTask.py with the \c --debug flag.
201  """
202  ConfigClass = MeasurePsfConfig
203  _DefaultName = "measurePsf"
204 
205  def __init__(self, schema=None, **kwargs):
206  """!Create the detection task. Most arguments are simply passed onto pipe.base.Task.
207 
208  \param schema An lsst::afw::table::Schema used to create the output lsst.afw.table.SourceCatalog
209  \param **kwargs Keyword arguments passed to lsst.pipe.base.task.Task.__init__.
210 
211  If schema is not None, 'calib.psf.candidate' and 'calib.psf.used' fields will be added to
212  identify which stars were employed in the PSF estimation.
213 
214  \note This task can add fields to the schema, so any code calling this task must ensure that
215  these fields are indeed present in the input table.
216  """
217 
218  pipeBase.Task.__init__(self, **kwargs)
219  if schema is not None:
220  self.candidateKey = schema.addField(
221  "calib_psfCandidate", type="Flag",
222  doc=("Flag set if the source was a candidate for PSF determination, "
223  "as determined by the star selector.")
224  )
225  self.usedKey = schema.addField(
226  "calib_psfUsed", type="Flag",
227  doc=("Flag set if the source was actually used for PSF determination, "
228  "as determined by the '%s' PSF determiner.") % self.config.psfDeterminer.name
229  )
230  self.reservedKey = schema.addField(
231  "calib_psfReserved", type="Flag",
232  doc=("Flag set if the source was selected as a PSF candidate, but was "
233  "reserved from the PSF fitting."))
234  else:
235  self.candidateKey = None
236  self.usedKey = None
237  self.makeSubtask("starSelector", schema=schema)
238  self.makeSubtask("psfDeterminer", schema=schema)
239 
240  @pipeBase.timeMethod
241  def run(self, exposure, sources, expId=0, matches=None):
242  """!Measure the PSF
243 
244  \param[in,out] exposure Exposure to process; measured PSF will be added.
245  \param[in,out] sources Measured sources on exposure; flag fields will be set marking
246  stars chosen by the star selector and the PSF determiner if a schema
247  was passed to the task constructor.
248  \param[in] expId Exposure id used for generating random seed.
249  \param[in] matches A list of lsst.afw.table.ReferenceMatch objects
250  (\em i.e. of lsst.afw.table.Match
251  with \c first being of type lsst.afw.table.SimpleRecord and \c second
252  type lsst.afw.table.SourceRecord --- the reference object and detected
253  object respectively) as returned by \em e.g. the AstrometryTask.
254  Used by star selectors that choose to refer to an external catalog.
255 
256  \return a pipe.base.Struct with fields:
257  - psf: The measured PSF (also set in the input exposure)
258  - cellSet: an lsst.afw.math.SpatialCellSet containing the PSF candidates
259  as returned by the psf determiner.
260  """
261  self.log.info("Measuring PSF")
262 
263  import lsstDebug
264  display = lsstDebug.Info(__name__).display
265  displayExposure = lsstDebug.Info(__name__).displayExposure # display the Exposure + spatialCells
266  displayPsfMosaic = lsstDebug.Info(__name__).displayPsfMosaic # show mosaic of reconstructed PSF(x,y)
267  displayPsfCandidates = lsstDebug.Info(__name__).displayPsfCandidates # show mosaic of candidates
268  displayResiduals = lsstDebug.Info(__name__).displayResiduals # show residuals
269  showBadCandidates = lsstDebug.Info(__name__).showBadCandidates # include bad candidates
270  normalizeResiduals = lsstDebug.Info(__name__).normalizeResiduals # normalise residuals by object peak
271 
272  #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
273  #
274  # Run star selector
275  #
276  psfCandidateList = self.starSelector.run(exposure=exposure, sourceCat=sources,
277  matches=matches).psfCandidates
278  reserveList = []
279 
280  if self.config.reserveFraction > 0:
281  random.seed(self.config.reserveSeed*expId)
282  reserveList = random.sample(psfCandidateList,
283  int((self.config.reserveFraction)*len(psfCandidateList)))
284 
285  for cand in reserveList:
286  psfCandidateList.remove(cand)
287 
288  if reserveList and self.reservedKey is not None:
289  for cand in reserveList:
290  source = cand.getSource()
291  source.set(self.reservedKey, True)
292 
293  if psfCandidateList and self.candidateKey is not None:
294  for cand in psfCandidateList:
295  source = cand.getSource()
296  source.set(self.candidateKey, True)
297 
298  self.log.info("PSF star selector found %d candidates" % len(psfCandidateList))
299  if self.config.reserveFraction > 0:
300  self.log.info("Reserved %d candidates from the fitting" % len(reserveList))
301 
302  if display:
303  frame = display
304  if displayExposure:
305  ds9.mtv(exposure, frame=frame, title="psf determination")
306 
307  #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
308  #
309  # Determine PSF
310  #
311  psf, cellSet = self.psfDeterminer.determinePsf(exposure, psfCandidateList, self.metadata,
312  flagKey=self.usedKey)
313  self.log.info("PSF determination using %d/%d stars." %
314  (self.metadata.get("numGoodStars"), self.metadata.get("numAvailStars")))
315 
316  exposure.setPsf(psf)
317 
318  if display:
319  frame = display
320  if displayExposure:
321  showPsfSpatialCells(exposure, cellSet, showBadCandidates, frame=frame)
322  frame += 1
323 
324  if displayPsfCandidates: # Show a mosaic of PSF candidates
325  plotPsfCandidates(cellSet, showBadCandidates, frame)
326  frame += 1
327 
328  if displayResiduals:
329  frame = plotResiduals(exposure, cellSet,
330  showBadCandidates=showBadCandidates,
331  normalizeResiduals=normalizeResiduals,
332  frame=frame)
333  if displayPsfMosaic:
334  maUtils.showPsfMosaic(exposure, psf, frame=frame, showFwhm=True)
335  ds9.scale(0, 1, "linear", frame=frame)
336  frame += 1
337 
338  return pipeBase.Struct(
339  psf=psf,
340  cellSet=cellSet,
341  )
342 
343  @property
344  def usesMatches(self):
345  """Return True if this task makes use of the "matches" argument to the run method"""
346  return self.starSelector.usesMatches
347 
348 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
349 #
350 # Debug code
351 #
352 
353 
354 def showPsfSpatialCells(exposure, cellSet, showBadCandidates, frame=1):
355  maUtils.showPsfSpatialCells(exposure, cellSet,
356  symb="o", ctype=ds9.CYAN, ctypeUnused=ds9.YELLOW,
357  size=4, frame=frame)
358  for cell in cellSet.getCellList():
359  for cand in cell.begin(not showBadCandidates): # maybe include bad candidates
360  status = cand.getStatus()
361  ds9.dot('+', *cand.getSource().getCentroid(), frame=frame,
362  ctype=ds9.GREEN if status == afwMath.SpatialCellCandidate.GOOD else
363  ds9.YELLOW if status == afwMath.SpatialCellCandidate.UNKNOWN else ds9.RED)
364 
365 
366 def plotPsfCandidates(cellSet, showBadCandidates=False, frame=1):
367  import lsst.afw.display.utils as displayUtils
368 
369  stamps = []
370  for cell in cellSet.getCellList():
371  for cand in cell.begin(not showBadCandidates): # maybe include bad candidates
372  try:
373  im = cand.getMaskedImage()
374 
375  chi2 = cand.getChi2()
376  if chi2 < 1e100:
377  chi2 = "%.1f" % chi2
378  else:
379  chi2 = float("nan")
380 
381  stamps.append((im, "%d%s" %
382  (maUtils.splitId(cand.getSource().getId(), True)["objId"], chi2),
383  cand.getStatus()))
384  except Exception:
385  continue
386 
387  mos = displayUtils.Mosaic()
388  for im, label, status in stamps:
389  im = type(im)(im, True)
390  try:
391  im /= afwMath.makeStatistics(im, afwMath.MAX).getValue()
392  except NotImplementedError:
393  pass
394 
395  mos.append(im, label,
396  ds9.GREEN if status == afwMath.SpatialCellCandidate.GOOD else
397  ds9.YELLOW if status == afwMath.SpatialCellCandidate.UNKNOWN else ds9.RED)
398 
399  if mos.images:
400  mos.makeMosaic(frame=frame, title="Psf Candidates")
401 
402 
403 def plotResiduals(exposure, cellSet, showBadCandidates=False, normalizeResiduals=True, frame=2):
404  psf = exposure.getPsf()
405  while True:
406  try:
407  maUtils.showPsfCandidates(exposure, cellSet, psf=psf, frame=frame,
408  normalize=normalizeResiduals,
409  showBadCandidates=showBadCandidates)
410  frame += 1
411  maUtils.showPsfCandidates(exposure, cellSet, psf=psf, frame=frame,
412  normalize=normalizeResiduals,
413  showBadCandidates=showBadCandidates,
414  variance=True)
415  frame += 1
416  except Exception:
417  if not showBadCandidates:
418  showBadCandidates = True
419  continue
420  break
421 
422  return frame
def run(self, exposure, sources, expId=0, matches=None)
Measure the PSF.
Definition: measurePsf.py:241
def plotPsfCandidates(cellSet, showBadCandidates=False, frame=1)
Definition: measurePsf.py:366
def plotResiduals(exposure, cellSet, showBadCandidates=False, normalizeResiduals=True, frame=2)
Definition: measurePsf.py:403
def showPsfSpatialCells(exposure, cellSet, showBadCandidates, frame=1)
Definition: measurePsf.py:354
def __init__(self, schema=None, kwargs)
Create the detection task.
Definition: measurePsf.py:205