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
measurePsf.py
Go to the documentation of this file.
1 # This file is part of pipe_tasks.
2 #
3 # Developed for the LSST Data Management System.
4 # This product includes software developed by the LSST Project
5 # (https://www.lsst.org).
6 # See the COPYRIGHT file at the top-level directory of this distribution
7 # for details of code ownership.
8 #
9 # This program is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the GNU General Public License
20 # along with this program. If not, see <https://www.gnu.org/licenses/>.
21 
22 import lsst.afw.display as afwDisplay
23 import lsst.afw.math as afwMath
24 import lsst.meas.algorithms as measAlg
25 import lsst.meas.algorithms.utils as maUtils
26 import lsst.pex.config as pexConfig
27 import lsst.pipe.base as pipeBase
29 
30 
31 class MeasurePsfConfig(pexConfig.Config):
32  starSelector = measAlg.sourceSelectorRegistry.makeField(
33  "Star selection algorithm",
34  default="objectSize"
35  )
36  makePsfCandidates = pexConfig.ConfigurableField(
37  target=measAlg.MakePsfCandidatesTask,
38  doc="Task to make psf candidates from selected stars.",
39  )
40  psfDeterminer = measAlg.psfDeterminerRegistry.makeField(
41  "PSF Determination algorithm",
42  default="psfex"
43  )
44  reserve = pexConfig.ConfigurableField(
45  target=measAlg.ReserveSourcesTask,
46  doc="Reserve sources from fitting"
47  )
48 
49 
55 
56 
57 class MeasurePsfTask(pipeBase.Task):
58  r"""!
59 @anchor MeasurePsfTask_
60 
61 @brief Measure the PSF
62 
63 @section pipe_tasks_measurePsf_Contents Contents
64 
65  - @ref pipe_tasks_measurePsf_Purpose
66  - @ref pipe_tasks_measurePsf_Initialize
67  - @ref pipe_tasks_measurePsf_IO
68  - @ref pipe_tasks_measurePsf_Config
69  - @ref pipe_tasks_measurePsf_Debug
70  - @ref pipe_tasks_measurePsf_Example
71 
72 @section pipe_tasks_measurePsf_Purpose Description
73 
74 A task that selects stars from a catalog of sources and uses those to measure the PSF.
75 
76 The star selector is a subclass of
77 @ref lsst.meas.algorithms.starSelector.BaseStarSelectorTask "lsst.meas.algorithms.BaseStarSelectorTask"
78 and the PSF determiner is a sublcass of
79 @ref lsst.meas.algorithms.psfDeterminer.BasePsfDeterminerTask "lsst.meas.algorithms.BasePsfDeterminerTask"
80 
81 @warning
82 There is no establised set of configuration parameters for these algorithms, so once you start modifying
83 parameters (as we do in @ref pipe_tasks_measurePsf_Example) your code is no longer portable.
84 
85 @section pipe_tasks_measurePsf_Initialize Task initialisation
86 
87 @copydoc \_\_init\_\_
88 
89 @section pipe_tasks_measurePsf_IO Invoking the Task
90 
91 @copydoc run
92 
93 @section pipe_tasks_measurePsf_Config Configuration parameters
94 
95 See @ref MeasurePsfConfig.
96 
97 @section pipe_tasks_measurePsf_Debug Debug variables
98 
99 The @link lsst.pipe.base.cmdLineTask.CmdLineTask command line task@endlink interface supports a
100 flag @c -d to import @b debug.py from your @c PYTHONPATH; see @ref baseDebug for more about @b debug.py files.
101 
102 <DL>
103  <DT> @c display
104  <DD> If True, display debugging plots
105  <DT> displayExposure
106  <DD> display the Exposure + spatialCells
107  <DT> displayPsfCandidates
108  <DD> show mosaic of candidates
109  <DT> showBadCandidates
110  <DD> Include bad candidates
111  <DT> displayPsfMosaic
112  <DD> show mosaic of reconstructed PSF(xy)
113  <DT> displayResiduals
114  <DD> show residuals
115  <DT> normalizeResiduals
116  <DD> Normalise residuals by object amplitude
117 </DL>
118 
119 Additionally you can enable any debug outputs that your chosen star selector and psf determiner support.
120 
121 @section pipe_tasks_measurePsf_Example A complete example of using MeasurePsfTask
122 
123 This code is in @link measurePsfTask.py@endlink in the examples directory, and can be run as @em e.g.
124 @code
125 examples/measurePsfTask.py --doDisplay
126 @endcode
127 @dontinclude measurePsfTask.py
128 
129 The example also runs SourceDetectionTask and SingleFrameMeasurementTask;
130 see @ref meas_algorithms_measurement_Example for more explanation.
131 
132 Import the tasks (there are some other standard imports; read the file to see them all):
133 
134 @skip SourceDetectionTask
135 @until MeasurePsfTask
136 
137 We need to create the tasks before processing any data as the task constructor
138 can add an extra column to the schema, but first we need an almost-empty
139 Schema:
140 
141 @skipline makeMinimalSchema
142 
143 We can now call the constructors for the tasks we need to find and characterize candidate
144 PSF stars:
145 
146 @skip SourceDetectionTask.ConfigClass
147 @until measureTask
148 
149 Note that we've chosen a minimal set of measurement plugins: we need the
150 outputs of @c base_SdssCentroid, @c base_SdssShape and @c base_CircularApertureFlux as
151 inputs to the PSF measurement algorithm, while @c base_PixelFlags identifies
152 and flags bad sources (e.g. with pixels too close to the edge) so they can be
153 removed later.
154 
155 Now we can create and configure the task that we're interested in:
156 
157 @skip MeasurePsfTask
158 @until measurePsfTask
159 
160 We're now ready to process the data (we could loop over multiple exposures/catalogues using the same
161 task objects). First create the output table:
162 
163 @skipline afwTable
164 
165 And process the image:
166 
167 @skip sources =
168 @until result
169 
170 We can then unpack and use the results:
171 
172 @skip psf
173 @until cellSet
174 
175 If you specified @c --doDisplay you can see the PSF candidates:
176 
177 @skip display
178 @until RED
179 
180 <HR>
181 
182 To investigate the @ref pipe_tasks_measurePsf_Debug, put something like
183 @code{.py}
184  import lsstDebug
185  def DebugInfo(name):
186  di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively
187 
188  if name == "lsst.pipe.tasks.measurePsf" :
189  di.display = True
190  di.displayExposure = False # display the Exposure + spatialCells
191  di.displayPsfCandidates = True # show mosaic of candidates
192  di.displayPsfMosaic = True # show mosaic of reconstructed PSF(xy)
193  di.displayResiduals = True # show residuals
194  di.showBadCandidates = True # Include bad candidates
195  di.normalizeResiduals = False # Normalise residuals by object amplitude
196 
197  return di
198 
199  lsstDebug.Info = DebugInfo
200 @endcode
201 into your debug.py file and run measurePsfTask.py with the @c --debug flag.
202  """
203  ConfigClass = MeasurePsfConfig
204  _DefaultName = "measurePsf"
205 
206  def __init__(self, schema=None, **kwargs):
207  """!Create the detection task. Most arguments are simply passed onto pipe.base.Task.
208 
209  @param schema An lsst::afw::table::Schema used to create the output lsst.afw.table.SourceCatalog
210  @param **kwargs Keyword arguments passed to lsst.pipe.base.task.Task.__init__.
211 
212  If schema is not None, 'calib_psf_candidate' and 'calib_psf_used' fields will be added to
213  identify which stars were employed in the PSF estimation.
214 
215  @note This task can add fields to the schema, so any code calling this task must ensure that
216  these fields are indeed present in the input table.
217  """
218 
219  pipeBase.Task.__init__(self, **kwargs)
220  if schema is not None:
221  self.candidateKeycandidateKey = schema.addField(
222  "calib_psf_candidate", type="Flag",
223  doc=("Flag set if the source was a candidate for PSF determination, "
224  "as determined by the star selector.")
225  )
226  self.usedKeyusedKey = schema.addField(
227  "calib_psf_used", type="Flag",
228  doc=("Flag set if the source was actually used for PSF determination, "
229  "as determined by the '%s' PSF determiner.") % self.config.psfDeterminer.name
230  )
231  else:
232  self.candidateKeycandidateKey = None
233  self.usedKeyusedKey = None
234  self.makeSubtask("starSelector")
235  self.makeSubtask("makePsfCandidates")
236  self.makeSubtask("psfDeterminer", schema=schema)
237  self.makeSubtask("reserve", columnName="calib_psf", schema=schema,
238  doc="set if source was reserved from PSF determination")
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  # Run star selector
274  #
275  stars = self.starSelector.run(sourceCat=sources, matches=matches, exposure=exposure)
276  selectionResult = self.makePsfCandidates.run(stars.sourceCat, exposure=exposure)
277  self.log.info("PSF star selector found %d candidates", len(selectionResult.psfCandidates))
278  reserveResult = self.reserve.run(selectionResult.goodStarCat, expId=expId)
279  # Make list of psf candidates to send to the determiner (omitting those marked as reserved)
280  psfDeterminerList = [cand for cand, use
281  in zip(selectionResult.psfCandidates, reserveResult.use) if use]
282 
283  if selectionResult.psfCandidates and self.candidateKeycandidateKey is not None:
284  for cand in selectionResult.psfCandidates:
285  source = cand.getSource()
286  source.set(self.candidateKeycandidateKey, True)
287 
288  self.log.info("Sending %d candidates to PSF determiner", len(psfDeterminerList))
289 
290  if display:
291  frame = 1
292  if displayExposure:
293  disp = afwDisplay.Display(frame=frame)
294  disp.mtv(exposure, title="psf determination")
295  frame += 1
296  #
297  # Determine PSF
298  #
299  psf, cellSet = self.psfDeterminer.determinePsf(exposure, psfDeterminerList, self.metadata,
300  flagKey=self.usedKeyusedKey)
301  self.log.info("PSF determination using %d/%d stars.",
302  self.metadata.getScalar("numGoodStars"), self.metadata.getScalar("numAvailStars"))
303 
304  exposure.setPsf(psf)
305 
306  if display:
307  frame = display
308  if displayExposure:
309  disp = afwDisplay.Display(frame=frame)
310  showPsfSpatialCells(exposure, cellSet, showBadCandidates, frame=frame)
311  frame += 1
312 
313  if displayPsfCandidates: # Show a mosaic of PSF candidates
314  plotPsfCandidates(cellSet, showBadCandidates=showBadCandidates, frame=frame)
315  frame += 1
316 
317  if displayResiduals:
318  frame = plotResiduals(exposure, cellSet,
319  showBadCandidates=showBadCandidates,
320  normalizeResiduals=normalizeResiduals,
321  frame=frame)
322  if displayPsfMosaic:
323  disp = afwDisplay.Display(frame=frame)
324  maUtils.showPsfMosaic(exposure, psf, display=disp, showFwhm=True)
325  disp.scale("linear", 0, 1)
326  frame += 1
327 
328  return pipeBase.Struct(
329  psf=psf,
330  cellSet=cellSet,
331  )
332 
333  @property
334  def usesMatches(self):
335  """Return True if this task makes use of the "matches" argument to the run method"""
336  return self.starSelector.usesMatches
337 
338 #
339 # Debug code
340 #
341 
342 
343 def showPsfSpatialCells(exposure, cellSet, showBadCandidates, frame=1):
344  disp = afwDisplay.Display(frame=frame)
345  maUtils.showPsfSpatialCells(exposure, cellSet,
346  symb="o", ctype=afwDisplay.CYAN, ctypeUnused=afwDisplay.YELLOW,
347  size=4, display=disp)
348  for cell in cellSet.getCellList():
349  for cand in cell.begin(not showBadCandidates): # maybe include bad candidates
350  status = cand.getStatus()
351  disp.dot('+', *cand.getSource().getCentroid(),
352  ctype=afwDisplay.GREEN if status == afwMath.SpatialCellCandidate.GOOD else
353  afwDisplay.YELLOW if status == afwMath.SpatialCellCandidate.UNKNOWN else afwDisplay.RED)
354 
355 
356 def plotPsfCandidates(cellSet, showBadCandidates=False, frame=1):
357  stamps = []
358  for cell in cellSet.getCellList():
359  for cand in cell.begin(not showBadCandidates): # maybe include bad candidates
360  try:
361  im = cand.getMaskedImage()
362 
363  chi2 = cand.getChi2()
364  if chi2 < 1e100:
365  chi2 = "%.1f" % chi2
366  else:
367  chi2 = float("nan")
368 
369  stamps.append((im, "%d%s" %
370  (maUtils.splitId(cand.getSource().getId(), True)["objId"], chi2),
371  cand.getStatus()))
372  except Exception:
373  continue
374 
375  mos = afwDisplay.utils.Mosaic()
376  disp = afwDisplay.Display(frame=frame)
377  for im, label, status in stamps:
378  im = type(im)(im, True)
379  try:
380  im /= afwMath.makeStatistics(im, afwMath.MAX).getValue()
381  except NotImplementedError:
382  pass
383 
384  mos.append(im, label,
385  afwDisplay.GREEN if status == afwMath.SpatialCellCandidate.GOOD else
386  afwDisplay.YELLOW if status == afwMath.SpatialCellCandidate.UNKNOWN else afwDisplay.RED)
387 
388  if mos.images:
389  disp.mtv(mos.makeMosaic(), title="Psf Candidates")
390 
391 
392 def plotResiduals(exposure, cellSet, showBadCandidates=False, normalizeResiduals=True, frame=2):
393  psf = exposure.getPsf()
394  disp = afwDisplay.Display(frame=frame)
395  while True:
396  try:
397  maUtils.showPsfCandidates(exposure, cellSet, psf=psf, display=disp,
398  normalize=normalizeResiduals,
399  showBadCandidates=showBadCandidates)
400  frame += 1
401  maUtils.showPsfCandidates(exposure, cellSet, psf=psf, display=disp,
402  normalize=normalizeResiduals,
403  showBadCandidates=showBadCandidates,
404  variance=True)
405  frame += 1
406  except Exception:
407  if not showBadCandidates:
408  showBadCandidates = True
409  continue
410  break
411 
412  return frame
table::Key< int > type
Definition: Detector.cc:163
def run(self, exposure, sources, expId=0, matches=None)
Measure the PSF.
Definition: measurePsf.py:241
def __init__(self, schema=None, **kwargs)
Create the detection task.
Definition: measurePsf.py:206
Statistics makeStatistics(lsst::afw::image::Image< Pixel > const &img, lsst::afw::image::Mask< image::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl=StatisticsControl())
Handle a watered-down front-end to the constructor (no variance)
Definition: Statistics.h:359
Fit spatial kernel using approximate fluxes for candidates, and solving a linear system of equations.
def plotPsfCandidates(cellSet, showBadCandidates=False, frame=1)
Definition: measurePsf.py:356
def plotResiduals(exposure, cellSet, showBadCandidates=False, normalizeResiduals=True, frame=2)
Definition: measurePsf.py:392
def showPsfSpatialCells(exposure, cellSet, showBadCandidates, frame=1)
Definition: measurePsf.py:343