LSST Applications g070148d5b3+33e5256705,g0d53e28543+25c8b88941,g0da5cf3356+2dd1178308,g1081da9e2a+62d12e78cb,g17e5ecfddb+7e422d6136,g1c76d35bf8+ede3a706f7,g295839609d+225697d880,g2e2c1a68ba+cc1f6f037e,g2ffcdf413f+853cd4dcde,g38293774b4+62d12e78cb,g3b44f30a73+d953f1ac34,g48ccf36440+885b902d19,g4b2f1765b6+7dedbde6d2,g5320a0a9f6+0c5d6105b6,g56b687f8c9+ede3a706f7,g5c4744a4d9+ef6ac23297,g5ffd174ac0+0c5d6105b6,g6075d09f38+66af417445,g667d525e37+2ced63db88,g670421136f+2ced63db88,g71f27ac40c+2ced63db88,g774830318a+463cbe8d1f,g7876bc68e5+1d137996f1,g7985c39107+62d12e78cb,g7fdac2220c+0fd8241c05,g96f01af41f+368e6903a7,g9ca82378b8+2ced63db88,g9d27549199+ef6ac23297,gabe93b2c52+e3573e3735,gb065e2a02a+3dfbe639da,gbc3249ced9+0c5d6105b6,gbec6a3398f+0c5d6105b6,gc9534b9d65+35b9f25267,gd01420fc67+0c5d6105b6,geee7ff78d7+a14128c129,gf63283c776+ede3a706f7,gfed783d017+0c5d6105b6,w.2022.47
LSST Data Management Base Package
Loading...
Searching...
No Matches
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__all__ = ["MeasurePsfConfig", "MeasurePsfTask"]
23
24import lsst.afw.display as afwDisplay
25import lsst.afw.math as afwMath
26import lsst.meas.algorithms as measAlg
27import lsst.meas.algorithms.utils as maUtils
28import lsst.pex.config as pexConfig
29import lsst.pipe.base as pipeBase
31from lsst.utils.timer import timeMethod
32
33
34class MeasurePsfConfig(pexConfig.Config):
35 starSelector = measAlg.sourceSelectorRegistry.makeField(
36 "Star selection algorithm",
37 default="objectSize"
38 )
39 makePsfCandidates = pexConfig.ConfigurableField(
40 target=measAlg.MakePsfCandidatesTask,
41 doc="Task to make psf candidates from selected stars.",
42 )
43 psfDeterminer = measAlg.psfDeterminerRegistry.makeField(
44 "PSF Determination algorithm",
45 default="psfex"
46 )
47 reserve = pexConfig.ConfigurableField(
48 target=measAlg.ReserveSourcesTask,
49 doc="Reserve sources from fitting"
50 )
51
52 def validate(self):
53 super().validate()
54 if (self.psfDeterminer.name == "piff" and self.psfDeterminer["piff"].stampSize
55 and self.psfDeterminer["piff"].stampSize > self.makePsfCandidates.kernelSize):
56 msg = (f"PIFF kernelSize={self.psfDeterminer['piff'].stampSize}"
57 f" must be >= psf candidate kernelSize={self.makePsfCandidates.kernelSize}.")
58 raise pexConfig.FieldValidationError(MeasurePsfConfig.makePsfCandidates, self, msg)
59
60
61class MeasurePsfTask(pipeBase.Task):
62 """A task that selects stars from a catalog of sources and uses those to measure the PSF.
63
64 Parameters
65 ----------
66 schema : `lsst.sfw.table.Schema`
67 An `lsst.afw.table.Schema` used to create the output `lsst.afw.table.SourceCatalog`.
68 **kwargs :
69 Keyword arguments passed to lsst.pipe.base.task.Task.__init__.
70
71 Notes
72 -----
73 If schema is not None, 'calib_psf_candidate' and 'calib_psf_used' fields will be added to
74 identify which stars were employed in the PSF estimation.
75
76 This task can add fields to the schema, so any code calling this task must ensure that
77 these fields are indeed present in the input table.
78
79 The star selector is a subclass of
80 ``lsst.meas.algorithms.starSelector.BaseStarSelectorTask`` "lsst.meas.algorithms.BaseStarSelectorTask"
81 and the PSF determiner is a sublcass of
82 ``lsst.meas.algorithms.psfDeterminer.BasePsfDeterminerTask`` "lsst.meas.algorithms.BasePsfDeterminerTask"
83
84 There is no establised set of configuration parameters for these algorithms, so once you start modifying
85 parameters (as we do in @ref pipe_tasks_measurePsf_Example) your code is no longer portable.
86
87 Debugging:
88
89 .. code-block:: none
90
91 display
92 If True, display debugging plots
93 displayExposure
94 display the Exposure + spatialCells
95 displayPsfCandidates
96 show mosaic of candidates
97 showBadCandidates
98 Include bad candidates
99 displayPsfMosaic
100 show mosaic of reconstructed PSF(xy)
101 displayResiduals
102 show residuals
103 normalizeResiduals
104 Normalise residuals by object amplitude
105
106 Additionally you can enable any debug outputs that your chosen star selector and psf determiner support.
107 """
108 ConfigClass = MeasurePsfConfig
109 _DefaultName = "measurePsf"
110
111 def __init__(self, schema=None, **kwargs):
112 pipeBase.Task.__init__(self, **kwargs)
113 if schema is not None:
114 self.candidateKey = schema.addField(
115 "calib_psf_candidate", type="Flag",
116 doc=("Flag set if the source was a candidate for PSF determination, "
117 "as determined by the star selector.")
118 )
119 self.usedKey = schema.addField(
120 "calib_psf_used", type="Flag",
121 doc=("Flag set if the source was actually used for PSF determination, "
122 "as determined by the '%s' PSF determiner.") % self.config.psfDeterminer.name
123 )
124 else:
125 self.candidateKey = None
126 self.usedKey = None
127 self.makeSubtask("starSelector")
128 self.makeSubtask("makePsfCandidates")
129 self.makeSubtask("psfDeterminer", schema=schema)
130 self.makeSubtask("reserve", columnName="calib_psf", schema=schema,
131 doc="set if source was reserved from PSF determination")
132
133 @timeMethod
134 def run(self, exposure, sources, expId=0, matches=None):
135 """Measure the PSF.
136
137 Parameters
138 ----------
139 exposure : `lsst.afw.image.Exposure`
140 Exposure to process; measured PSF will be added.
141 sources : `Unknown`
142 Measured sources on exposure; flag fields will be set marking
143 stars chosen by the star selector and the PSF determiner if a schema
144 was passed to the task constructor.
145 expId : `int`, optional
146 Exposure id used for generating random seed.
147 matches : `list`, optional
148 A list of ``lsst.afw.table.ReferenceMatch`` objects
149 (i.e. of ``lsst.afw.table.Match`` with @c first being
150 of type ``lsst.afw.table.SimpleRecord`` and @c second
151 type lsst.afw.table.SourceRecord --- the reference object and detected
152 object respectively) as returned by @em e.g. the AstrometryTask.
153 Used by star selectors that choose to refer to an external catalog.
154
155 Returns
156 -------
157 measurement : `lsst.pipe.base.Struct`
158 PSF measurement as a struct with attributes:
159
160 ``psf``
161 The measured PSF (also set in the input exposure).
162 ``cellSet``
163 An `lsst.afw.math.SpatialCellSet` containing the PSF candidates
164 as returned by the psf determiner.
165 """
166 self.log.info("Measuring PSF")
167
168 import lsstDebug
169 display = lsstDebug.Info(__name__).display
170 displayExposure = lsstDebug.Info(__name__).displayExposure # display the Exposure + spatialCells
171 displayPsfMosaic = lsstDebug.Info(__name__).displayPsfMosaic # show mosaic of reconstructed PSF(x,y)
172 displayPsfCandidates = lsstDebug.Info(__name__).displayPsfCandidates # show mosaic of candidates
173 displayResiduals = lsstDebug.Info(__name__).displayResiduals # show residuals
174 showBadCandidates = lsstDebug.Info(__name__).showBadCandidates # include bad candidates
175 normalizeResiduals = lsstDebug.Info(__name__).normalizeResiduals # normalise residuals by object peak
176
177 #
178 # Run star selector
179 #
180 stars = self.starSelector.run(sourceCat=sources, matches=matches, exposure=exposure)
181 selectionResult = self.makePsfCandidates.run(stars.sourceCat, exposure=exposure)
182 self.log.info("PSF star selector found %d candidates", len(selectionResult.psfCandidates))
183 reserveResult = self.reserve.run(selectionResult.goodStarCat, expId=expId)
184 # Make list of psf candidates to send to the determiner (omitting those marked as reserved)
185 psfDeterminerList = [cand for cand, use
186 in zip(selectionResult.psfCandidates, reserveResult.use) if use]
187
188 if selectionResult.psfCandidates and self.candidateKey is not None:
189 for cand in selectionResult.psfCandidates:
190 source = cand.getSource()
191 source.set(self.candidateKey, True)
192
193 self.log.info("Sending %d candidates to PSF determiner", len(psfDeterminerList))
194
195 if display:
196 frame = 1
197 if displayExposure:
198 disp = afwDisplay.Display(frame=frame)
199 disp.mtv(exposure, title="psf determination")
200 frame += 1
201 #
202 # Determine PSF
203 #
204 psf, cellSet = self.psfDeterminer.determinePsf(exposure, psfDeterminerList, self.metadata,
205 flagKey=self.usedKey)
206 self.log.info("PSF determination using %d/%d stars.",
207 self.metadata.getScalar("numGoodStars"), self.metadata.getScalar("numAvailStars"))
208
209 exposure.setPsf(psf)
210
211 if display:
212 frame = display
213 if displayExposure:
214 disp = afwDisplay.Display(frame=frame)
215 showPsfSpatialCells(exposure, cellSet, showBadCandidates, frame=frame)
216 frame += 1
217
218 if displayPsfCandidates: # Show a mosaic of PSF candidates
219 plotPsfCandidates(cellSet, showBadCandidates=showBadCandidates, frame=frame)
220 frame += 1
221
222 if displayResiduals:
223 frame = plotResiduals(exposure, cellSet,
224 showBadCandidates=showBadCandidates,
225 normalizeResiduals=normalizeResiduals,
226 frame=frame)
227 if displayPsfMosaic:
228 disp = afwDisplay.Display(frame=frame)
229 maUtils.showPsfMosaic(exposure, psf, display=disp, showFwhm=True)
230 disp.scale("linear", 0, 1)
231 frame += 1
232
233 return pipeBase.Struct(
234 psf=psf,
235 cellSet=cellSet,
236 )
237
238 @property
239 def usesMatches(self):
240 """Return True if this task makes use of the "matches" argument to the run method"""
241 return self.starSelector.usesMatches
242
243#
244# Debug code
245#
246
247
248def showPsfSpatialCells(exposure, cellSet, showBadCandidates, frame=1):
249 disp = afwDisplay.Display(frame=frame)
250 maUtils.showPsfSpatialCells(exposure, cellSet,
251 symb="o", ctype=afwDisplay.CYAN, ctypeUnused=afwDisplay.YELLOW,
252 size=4, display=disp)
253 for cell in cellSet.getCellList():
254 for cand in cell.begin(not showBadCandidates): # maybe include bad candidates
255 status = cand.getStatus()
256 disp.dot('+', *cand.getSource().getCentroid(),
257 ctype=afwDisplay.GREEN if status == afwMath.SpatialCellCandidate.GOOD else
258 afwDisplay.YELLOW if status == afwMath.SpatialCellCandidate.UNKNOWN else afwDisplay.RED)
259
260
261def plotPsfCandidates(cellSet, showBadCandidates=False, frame=1):
262 stamps = []
263 for cell in cellSet.getCellList():
264 for cand in cell.begin(not showBadCandidates): # maybe include bad candidates
265 try:
266 im = cand.getMaskedImage()
267
268 chi2 = cand.getChi2()
269 if chi2 < 1e100:
270 chi2 = "%.1f" % chi2
271 else:
272 chi2 = float("nan")
273
274 stamps.append((im, "%d%s" %
275 (maUtils.splitId(cand.getSource().getId(), True)["objId"], chi2),
276 cand.getStatus()))
277 except Exception:
278 continue
279
280 mos = afwDisplay.utils.Mosaic()
281 disp = afwDisplay.Display(frame=frame)
282 for im, label, status in stamps:
283 im = type(im)(im, True)
284 try:
285 im /= afwMath.makeStatistics(im, afwMath.MAX).getValue()
286 except NotImplementedError:
287 pass
288
289 mos.append(im, label,
290 afwDisplay.GREEN if status == afwMath.SpatialCellCandidate.GOOD else
291 afwDisplay.YELLOW if status == afwMath.SpatialCellCandidate.UNKNOWN else afwDisplay.RED)
292
293 if mos.images:
294 disp.mtv(mos.makeMosaic(), title="Psf Candidates")
295
296
297def plotResiduals(exposure, cellSet, showBadCandidates=False, normalizeResiduals=True, frame=2):
298 psf = exposure.getPsf()
299 disp = afwDisplay.Display(frame=frame)
300 while True:
301 try:
302 maUtils.showPsfCandidates(exposure, cellSet, psf=psf, display=disp,
303 normalize=normalizeResiduals,
304 showBadCandidates=showBadCandidates)
305 frame += 1
306 maUtils.showPsfCandidates(exposure, cellSet, psf=psf, display=disp,
307 normalize=normalizeResiduals,
308 showBadCandidates=showBadCandidates,
309 variance=True)
310 frame += 1
311 except Exception:
312 if not showBadCandidates:
313 showBadCandidates = True
314 continue
315 break
316
317 return frame
table::Key< int > type
Definition: Detector.cc:163
A class to contain the data, WCS, and other information needed to describe an image of the sky.
Definition: Exposure.h:72
A collection of SpatialCells covering an entire image.
Definition: SpatialCell.h:383
Defines the fields and offsets for a table.
Definition: Schema.h:51
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 __init__(self, schema=None, **kwargs)
Definition: measurePsf.py:111
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:361
def plotPsfCandidates(cellSet, showBadCandidates=False, frame=1)
Definition: measurePsf.py:261
def plotResiduals(exposure, cellSet, showBadCandidates=False, normalizeResiduals=True, frame=2)
Definition: measurePsf.py:297
def showPsfSpatialCells(exposure, cellSet, showBadCandidates, frame=1)
Definition: measurePsf.py:248
Lightweight representation of a geometric match between two records.
Definition: Match.h:67