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