LSST Applications g0b6bd0c080+a72a5dd7e6,g1182afd7b4+2a019aa3bb,g17e5ecfddb+2b8207f7de,g1d67935e3f+06cf436103,g38293774b4+ac198e9f13,g396055baef+6a2097e274,g3b44f30a73+6611e0205b,g480783c3b1+98f8679e14,g48ccf36440+89c08d0516,g4b93dc025c+98f8679e14,g5c4744a4d9+a302e8c7f0,g613e996a0d+e1c447f2e0,g6c8d09e9e7+25247a063c,g7271f0639c+98f8679e14,g7a9cd813b8+124095ede6,g9d27549199+a302e8c7f0,ga1cf026fa3+ac198e9f13,ga32aa97882+7403ac30ac,ga786bb30fb+7a139211af,gaa63f70f4e+9994eb9896,gabf319e997+ade567573c,gba47b54d5d+94dc90c3ea,gbec6a3398f+06cf436103,gc6308e37c7+07dd123edb,gc655b1545f+ade567573c,gcc9029db3c+ab229f5caf,gd01420fc67+06cf436103,gd877ba84e5+06cf436103,gdb4cecd868+6f279b5b48,ge2d134c3d5+cc4dbb2e3f,ge448b5faa6+86d1ceac1d,gecc7e12556+98f8679e14,gf3ee170dca+25247a063c,gf4ac96e456+ade567573c,gf9f5ea5b4d+ac198e9f13,gff490e6085+8c2580be5c,w.2022.27
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
22import lsst.afw.display as afwDisplay
23import lsst.afw.math as afwMath
24import lsst.meas.algorithms as measAlg
25import lsst.meas.algorithms.utils as maUtils
26import lsst.pex.config as pexConfig
27import lsst.pipe.base as pipeBase
29from lsst.utils.timer import timeMethod
30
31
32class MeasurePsfConfig(pexConfig.Config):
33 starSelector = measAlg.sourceSelectorRegistry.makeField(
34 "Star selection algorithm",
35 default="objectSize"
36 )
37 makePsfCandidates = pexConfig.ConfigurableField(
38 target=measAlg.MakePsfCandidatesTask,
39 doc="Task to make psf candidates from selected stars.",
40 )
41 psfDeterminer = measAlg.psfDeterminerRegistry.makeField(
42 "PSF Determination algorithm",
43 default="piff"
44 )
45 reserve = pexConfig.ConfigurableField(
46 target=measAlg.ReserveSourcesTask,
47 doc="Reserve sources from fitting"
48 )
49
50 def validate(self):
51 super().validate()
52 if (self.psfDeterminerpsfDeterminer.name == "piff"
53 and self.psfDeterminerpsfDeterminer["piff"].kernelSize > self.makePsfCandidatesmakePsfCandidates.kernelSize):
54 msg = (f"PIFF kernelSize={self.psfDeterminer['piff'].kernelSize}"
55 f" must be >= psf candidate kernelSize={self.makePsfCandidates.kernelSize}.")
56 raise pexConfig.FieldValidationError(MeasurePsfConfig.makePsfCandidates, self, msg)
57
58
64
65
66class MeasurePsfTask(pipeBase.Task):
67 r"""!
68@anchor MeasurePsfTask_
69
70@brief Measure the PSF
71
72@section pipe_tasks_measurePsf_Contents Contents
73
74 - @ref pipe_tasks_measurePsf_Purpose
75 - @ref pipe_tasks_measurePsf_Initialize
76 - @ref pipe_tasks_measurePsf_IO
77 - @ref pipe_tasks_measurePsf_Config
78 - @ref pipe_tasks_measurePsf_Debug
79 - @ref pipe_tasks_measurePsf_Example
80
81@section pipe_tasks_measurePsf_Purpose Description
82
83A task that selects stars from a catalog of sources and uses those to measure the PSF.
84
85The star selector is a subclass of
86@ref lsst.meas.algorithms.starSelector.BaseStarSelectorTask "lsst.meas.algorithms.BaseStarSelectorTask"
87and the PSF determiner is a sublcass of
88@ref lsst.meas.algorithms.psfDeterminer.BasePsfDeterminerTask "lsst.meas.algorithms.BasePsfDeterminerTask"
89
90@warning
91There is no establised set of configuration parameters for these algorithms, so once you start modifying
92parameters (as we do in @ref pipe_tasks_measurePsf_Example) your code is no longer portable.
93
94@section pipe_tasks_measurePsf_Initialize Task initialisation
95
96@copydoc \_\_init\_\_
97
98@section pipe_tasks_measurePsf_IO Invoking the Task
99
100@copydoc run
101
102@section pipe_tasks_measurePsf_Config Configuration parameters
103
104See @ref MeasurePsfConfig.
105
106@section pipe_tasks_measurePsf_Debug Debug variables
107
108The command line task interface supports a
109flag @c -d to import @b debug.py from your @c PYTHONPATH; see
110<a href="https://pipelines.lsst.io/modules/lsstDebug/">the lsstDebug documentation</a>
111for more about @b debug.py files.
112
113<DL>
114 <DT> @c display
115 <DD> If True, display debugging plots
116 <DT> displayExposure
117 <DD> display the Exposure + spatialCells
118 <DT> displayPsfCandidates
119 <DD> show mosaic of candidates
120 <DT> showBadCandidates
121 <DD> Include bad candidates
122 <DT> displayPsfMosaic
123 <DD> show mosaic of reconstructed PSF(xy)
124 <DT> displayResiduals
125 <DD> show residuals
126 <DT> normalizeResiduals
127 <DD> Normalise residuals by object amplitude
128</DL>
129
130Additionally you can enable any debug outputs that your chosen star selector and psf determiner support.
131
132@section pipe_tasks_measurePsf_Example A complete example of using MeasurePsfTask
133
134This code is in `measurePsfTask.py` in the examples directory, and can be run as @em e.g.
135@code
136examples/measurePsfTask.py --doDisplay
137@endcode
138@dontinclude measurePsfTask.py
139
140The example also runs SourceDetectionTask and SingleFrameMeasurementTask.
141
142Import the tasks (there are some other standard imports; read the file to see them all):
143
144@skip SourceDetectionTask
145@until MeasurePsfTask
146
147We need to create the tasks before processing any data as the task constructor
148can add an extra column to the schema, but first we need an almost-empty
149Schema:
150
151@skipline makeMinimalSchema
152
153We can now call the constructors for the tasks we need to find and characterize candidate
154PSF stars:
155
156@skip SourceDetectionTask.ConfigClass
157@until measureTask
158
159Note that we've chosen a minimal set of measurement plugins: we need the
160outputs of @c base_SdssCentroid, @c base_SdssShape and @c base_CircularApertureFlux as
161inputs to the PSF measurement algorithm, while @c base_PixelFlags identifies
162and flags bad sources (e.g. with pixels too close to the edge) so they can be
163removed later.
164
165Now we can create and configure the task that we're interested in:
166
167@skip MeasurePsfTask
168@until measurePsfTask
169
170We're now ready to process the data (we could loop over multiple exposures/catalogues using the same
171task objects). First create the output table:
172
173@skipline afwTable
174
175And process the image:
176
177@skip sources =
178@until result
179
180We can then unpack and use the results:
181
182@skip psf
183@until cellSet
184
185If you specified @c --doDisplay you can see the PSF candidates:
186
187@skip display
188@until RED
189
190<HR>
191
192To investigate the @ref pipe_tasks_measurePsf_Debug, put something like
193@code{.py}
194 import lsstDebug
195 def DebugInfo(name):
196 di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively
197
198 if name == "lsst.pipe.tasks.measurePsf" :
199 di.display = True
200 di.displayExposure = False # display the Exposure + spatialCells
201 di.displayPsfCandidates = True # show mosaic of candidates
202 di.displayPsfMosaic = True # show mosaic of reconstructed PSF(xy)
203 di.displayResiduals = True # show residuals
204 di.showBadCandidates = True # Include bad candidates
205 di.normalizeResiduals = False # Normalise residuals by object amplitude
206
207 return di
208
209 lsstDebug.Info = DebugInfo
210@endcode
211into your debug.py file and run measurePsfTask.py with the @c --debug flag.
212 """
213 ConfigClass = MeasurePsfConfig
214 _DefaultName = "measurePsf"
215
216 def __init__(self, schema=None, **kwargs):
217 """!Create the detection task. Most arguments are simply passed onto pipe.base.Task.
218
219 @param schema An lsst::afw::table::Schema used to create the output lsst.afw.table.SourceCatalog
220 @param **kwargs Keyword arguments passed to lsst.pipe.base.task.Task.__init__.
221
222 If schema is not None, 'calib_psf_candidate' and 'calib_psf_used' fields will be added to
223 identify which stars were employed in the PSF estimation.
224
225 @note This task can add fields to the schema, so any code calling this task must ensure that
226 these fields are indeed present in the input table.
227 """
228
229 pipeBase.Task.__init__(self, **kwargs)
230 if schema is not None:
231 self.candidateKeycandidateKey = schema.addField(
232 "calib_psf_candidate", type="Flag",
233 doc=("Flag set if the source was a candidate for PSF determination, "
234 "as determined by the star selector.")
235 )
236 self.usedKeyusedKey = schema.addField(
237 "calib_psf_used", type="Flag",
238 doc=("Flag set if the source was actually used for PSF determination, "
239 "as determined by the '%s' PSF determiner.") % self.config.psfDeterminer.name
240 )
241 else:
242 self.candidateKeycandidateKey = None
243 self.usedKeyusedKey = None
244 self.makeSubtask("starSelector")
245 self.makeSubtask("makePsfCandidates")
246 self.makeSubtask("psfDeterminer", schema=schema)
247 self.makeSubtask("reserve", columnName="calib_psf", schema=schema,
248 doc="set if source was reserved from PSF determination")
249
250 @timeMethod
251 def run(self, exposure, sources, expId=0, matches=None):
252 """!Measure the PSF
253
254 @param[in,out] exposure Exposure to process; measured PSF will be added.
255 @param[in,out] sources Measured sources on exposure; flag fields will be set marking
256 stars chosen by the star selector and the PSF determiner if a schema
257 was passed to the task constructor.
258 @param[in] expId Exposure id used for generating random seed.
259 @param[in] matches A list of lsst.afw.table.ReferenceMatch objects
260 (@em i.e. of lsst.afw.table.Match
261 with @c first being of type lsst.afw.table.SimpleRecord and @c second
262 type lsst.afw.table.SourceRecord --- the reference object and detected
263 object respectively) as returned by @em e.g. the AstrometryTask.
264 Used by star selectors that choose to refer to an external catalog.
265
266 @return a pipe.base.Struct with fields:
267 - psf: The measured PSF (also set in the input exposure)
268 - cellSet: an lsst.afw.math.SpatialCellSet containing the PSF candidates
269 as returned by the psf determiner.
270 """
271 self.log.info("Measuring PSF")
272
273 import lsstDebug
274 display = lsstDebug.Info(__name__).display
275 displayExposure = lsstDebug.Info(__name__).displayExposure # display the Exposure + spatialCells
276 displayPsfMosaic = lsstDebug.Info(__name__).displayPsfMosaic # show mosaic of reconstructed PSF(x,y)
277 displayPsfCandidates = lsstDebug.Info(__name__).displayPsfCandidates # show mosaic of candidates
278 displayResiduals = lsstDebug.Info(__name__).displayResiduals # show residuals
279 showBadCandidates = lsstDebug.Info(__name__).showBadCandidates # include bad candidates
280 normalizeResiduals = lsstDebug.Info(__name__).normalizeResiduals # normalise residuals by object peak
281
282 #
283 # Run star selector
284 #
285 stars = self.starSelector.run(sourceCat=sources, matches=matches, exposure=exposure)
286 selectionResult = self.makePsfCandidates.run(stars.sourceCat, exposure=exposure)
287 self.log.info("PSF star selector found %d candidates", len(selectionResult.psfCandidates))
288 reserveResult = self.reserve.run(selectionResult.goodStarCat, expId=expId)
289 # Make list of psf candidates to send to the determiner (omitting those marked as reserved)
290 psfDeterminerList = [cand for cand, use
291 in zip(selectionResult.psfCandidates, reserveResult.use) if use]
292
293 if selectionResult.psfCandidates and self.candidateKeycandidateKey is not None:
294 for cand in selectionResult.psfCandidates:
295 source = cand.getSource()
296 source.set(self.candidateKeycandidateKey, True)
297
298 self.log.info("Sending %d candidates to PSF determiner", len(psfDeterminerList))
299
300 if display:
301 frame = 1
302 if displayExposure:
303 disp = afwDisplay.Display(frame=frame)
304 disp.mtv(exposure, title="psf determination")
305 frame += 1
306 #
307 # Determine PSF
308 #
309 psf, cellSet = self.psfDeterminer.determinePsf(exposure, psfDeterminerList, self.metadata,
310 flagKey=self.usedKeyusedKey)
311 self.log.info("PSF determination using %d/%d stars.",
312 self.metadata.getScalar("numGoodStars"), self.metadata.getScalar("numAvailStars"))
313
314 exposure.setPsf(psf)
315
316 if display:
317 frame = display
318 if displayExposure:
319 disp = afwDisplay.Display(frame=frame)
320 showPsfSpatialCells(exposure, cellSet, showBadCandidates, frame=frame)
321 frame += 1
322
323 if displayPsfCandidates: # Show a mosaic of PSF candidates
324 plotPsfCandidates(cellSet, showBadCandidates=showBadCandidates, frame=frame)
325 frame += 1
326
327 if displayResiduals:
328 frame = plotResiduals(exposure, cellSet,
329 showBadCandidates=showBadCandidates,
330 normalizeResiduals=normalizeResiduals,
331 frame=frame)
332 if displayPsfMosaic:
333 disp = afwDisplay.Display(frame=frame)
334 maUtils.showPsfMosaic(exposure, psf, display=disp, showFwhm=True)
335 disp.scale("linear", 0, 1)
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# Debug code
350#
351
352
353def showPsfSpatialCells(exposure, cellSet, showBadCandidates, frame=1):
354 disp = afwDisplay.Display(frame=frame)
355 maUtils.showPsfSpatialCells(exposure, cellSet,
356 symb="o", ctype=afwDisplay.CYAN, ctypeUnused=afwDisplay.YELLOW,
357 size=4, display=disp)
358 for cell in cellSet.getCellList():
359 for cand in cell.begin(not showBadCandidates): # maybe include bad candidates
360 status = cand.getStatus()
361 disp.dot('+', *cand.getSource().getCentroid(),
362 ctype=afwDisplay.GREEN if status == afwMath.SpatialCellCandidate.GOOD else
363 afwDisplay.YELLOW if status == afwMath.SpatialCellCandidate.UNKNOWN else afwDisplay.RED)
364
365
366def plotPsfCandidates(cellSet, showBadCandidates=False, frame=1):
367 stamps = []
368 for cell in cellSet.getCellList():
369 for cand in cell.begin(not showBadCandidates): # maybe include bad candidates
370 try:
371 im = cand.getMaskedImage()
372
373 chi2 = cand.getChi2()
374 if chi2 < 1e100:
375 chi2 = "%.1f" % chi2
376 else:
377 chi2 = float("nan")
378
379 stamps.append((im, "%d%s" %
380 (maUtils.splitId(cand.getSource().getId(), True)["objId"], chi2),
381 cand.getStatus()))
382 except Exception:
383 continue
384
385 mos = afwDisplay.utils.Mosaic()
386 disp = afwDisplay.Display(frame=frame)
387 for im, label, status in stamps:
388 im = type(im)(im, True)
389 try:
390 im /= afwMath.makeStatistics(im, afwMath.MAX).getValue()
391 except NotImplementedError:
392 pass
393
394 mos.append(im, label,
395 afwDisplay.GREEN if status == afwMath.SpatialCellCandidate.GOOD else
396 afwDisplay.YELLOW if status == afwMath.SpatialCellCandidate.UNKNOWN else afwDisplay.RED)
397
398 if mos.images:
399 disp.mtv(mos.makeMosaic(), title="Psf Candidates")
400
401
402def plotResiduals(exposure, cellSet, showBadCandidates=False, normalizeResiduals=True, frame=2):
403 psf = exposure.getPsf()
404 disp = afwDisplay.Display(frame=frame)
405 while True:
406 try:
407 maUtils.showPsfCandidates(exposure, cellSet, psf=psf, display=disp,
408 normalize=normalizeResiduals,
409 showBadCandidates=showBadCandidates)
410 frame += 1
411 maUtils.showPsfCandidates(exposure, cellSet, psf=psf, display=disp,
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
table::Key< int > type
Definition: Detector.cc:163
A collection of SpatialCells covering an entire image.
Definition: SpatialCell.h:383
Record class that must contain a unique ID field and a celestial coordinate field.
Definition: Simple.h:48
Record class that contains measurements made on a single exposure.
Definition: Source.h:78
def run(self, exposure, sources, expId=0, matches=None)
Measure the PSF.
Definition: measurePsf.py:251
def __init__(self, schema=None, **kwargs)
Create the detection task.
Definition: measurePsf.py:216
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:366
def plotResiduals(exposure, cellSet, showBadCandidates=False, normalizeResiduals=True, frame=2)
Definition: measurePsf.py:402
def showPsfSpatialCells(exposure, cellSet, showBadCandidates, frame=1)
Definition: measurePsf.py:353
Lightweight representation of a geometric match between two records.
Definition: Match.h:67