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