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