LSST Applications g0f08755f38+82efc23009,g12f32b3c4e+e7bdf1200e,g1653933729+a8ce1bb630,g1a0ca8cf93+50eff2b06f,g28da252d5a+52db39f6a5,g2bbee38e9b+37c5a29d61,g2bc492864f+37c5a29d61,g2cdde0e794+c05ff076ad,g3156d2b45e+41e33cbcdc,g347aa1857d+37c5a29d61,g35bb328faa+a8ce1bb630,g3a166c0a6a+37c5a29d61,g3e281a1b8c+fb992f5633,g414038480c+7f03dfc1b0,g41af890bb2+11b950c980,g5fbc88fb19+17cd334064,g6b1c1869cb+12dd639c9a,g781aacb6e4+a8ce1bb630,g80478fca09+72e9651da0,g82479be7b0+04c31367b4,g858d7b2824+82efc23009,g9125e01d80+a8ce1bb630,g9726552aa6+8047e3811d,ga5288a1d22+e532dc0a0b,gae0086650b+a8ce1bb630,gb58c049af0+d64f4d3760,gc28159a63d+37c5a29d61,gcf0d15dbbd+2acd6d4d48,gd7358e8bfb+778a810b6e,gda3e153d99+82efc23009,gda6a2b7d83+2acd6d4d48,gdaeeff99f8+1711a396fd,ge2409df99d+6b12de1076,ge79ae78c31+37c5a29d61,gf0baf85859+d0a5978c5a,gf3967379c6+4954f8c433,gfb92a5be7c+82efc23009,gfec2e1e490+2aaed99252,w.2024.46
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 setDefaults(self):
53 super().setDefaults()
54 if self.psfDeterminer.name == "piff" and self.psfDeterminer["piff"].useCoordinates == "sky":
55 self.makePsfCandidates.kernelSize = 35
56
57 def validate(self):
58 super().validate()
59
60 match self.psfDeterminer.name:
61 # Perform Piff-specific validations.
62 case "piff":
63 if (
64 self.psfDeterminer["piff"].stampSize
65 and self.psfDeterminer["piff"].stampSize
66 > self.makePsfCandidates.kernelSize
67 ):
68 # Allowing this would result in a cutout size globally.
69 msg = (
70 f"PIFF stampSize={self.psfDeterminer['piff'].stampSize}"
71 f" must be >= psf candidate kernelSize={self.makePsfCandidates.kernelSize}."
72 )
73 raise pexConfig.FieldValidationError(
74 MeasurePsfConfig.makePsfCandidates, self, msg
75 )
76
77 model_size = self.psfDeterminer["piff"].modelSize
78 sampling_size = self.psfDeterminer["piff"].samplingSize
79 # Calculate the minimum cutout size for stars, given how large
80 # the PSF model is and the sampling size.
81 if self.psfDeterminer["piff"].useCoordinates == "sky":
82 min_kernel_size = int(
83 1.415 * model_size / sampling_size
84 ) # 1.415 = sqrt(2)
85 else:
86 min_kernel_size = int(model_size / sampling_size)
87
88 if self.makePsfCandidates.kernelSize < min_kernel_size:
89 msg = (
90 f"psf candidate kernelSize={self.makePsfCandidates.kernelSize}"
91 f" must be >= {min_kernel_size} for PIFF modelSize={model_size}."
92 )
93 raise pexConfig.FieldValidationError(
94 MeasurePsfConfig.makePsfCandidates, self, msg
95 )
96
97
98class MeasurePsfTask(pipeBase.Task):
99 """A task that selects stars from a catalog of sources and uses those to measure the PSF.
100
101 Parameters
102 ----------
103 schema : `lsst.sfw.table.Schema`
104 An `lsst.afw.table.Schema` used to create the output `lsst.afw.table.SourceCatalog`.
105 **kwargs :
106 Keyword arguments passed to lsst.pipe.base.task.Task.__init__.
107
108 Notes
109 -----
110 If schema is not None, 'calib_psf_candidate' and 'calib_psf_used' fields will be added to
111 identify which stars were employed in the PSF estimation.
112
113 This task can add fields to the schema, so any code calling this task must ensure that
114 these fields are indeed present in the input table.
115
116 The star selector is a subclass of
117 ``lsst.meas.algorithms.starSelector.BaseStarSelectorTask`` "lsst.meas.algorithms.BaseStarSelectorTask"
118 and the PSF determiner is a sublcass of
119 ``lsst.meas.algorithms.psfDeterminer.BasePsfDeterminerTask`` "lsst.meas.algorithms.BasePsfDeterminerTask"
120
121 There is no establised set of configuration parameters for these algorithms, so once you start modifying
122 parameters (as we do in @ref pipe_tasks_measurePsf_Example) your code is no longer portable.
123
124 Debugging:
125
126 .. code-block:: none
127
128 display
129 If True, display debugging plots
130 displayExposure
131 display the Exposure + spatialCells
132 displayPsfCandidates
133 show mosaic of candidates
134 showBadCandidates
135 Include bad candidates
136 displayPsfMosaic
137 show mosaic of reconstructed PSF(xy)
138 displayResiduals
139 show residuals
140 normalizeResiduals
141 Normalise residuals by object amplitude
142
143 Additionally you can enable any debug outputs that your chosen star selector and psf determiner support.
144 """
145 ConfigClass = MeasurePsfConfig
146 _DefaultName = "measurePsf"
147
148 def __init__(self, schema=None, **kwargs):
149 pipeBase.Task.__init__(self, **kwargs)
150 if schema is not None:
151 self.candidateKey = schema.addField(
152 "calib_psf_candidate", type="Flag",
153 doc=("Flag set if the source was a candidate for PSF determination, "
154 "as determined by the star selector.")
155 )
156 self.usedKey = schema.addField(
157 "calib_psf_used", type="Flag",
158 doc=("Flag set if the source was actually used for PSF determination, "
159 "as determined by the '%s' PSF determiner.") % self.config.psfDeterminer.name
160 )
161 else:
162 self.candidateKey = None
163 self.usedKey = None
164 self.makeSubtask("starSelector")
165 self.makeSubtask("makePsfCandidates")
166 self.makeSubtask("psfDeterminer", schema=schema)
167 self.makeSubtask("reserve", columnName="calib_psf", schema=schema,
168 doc="set if source was reserved from PSF determination")
169
170 @timeMethod
171 def run(self, exposure, sources, expId=0, matches=None):
172 """Measure the PSF.
173
174 Parameters
175 ----------
176 exposure : `lsst.afw.image.Exposure`
177 Exposure to process; measured PSF will be added.
178 sources : `Unknown`
179 Measured sources on exposure; flag fields will be set marking
180 stars chosen by the star selector and the PSF determiner if a schema
181 was passed to the task constructor.
182 expId : `int`, optional
183 Exposure id used for generating random seed.
184 matches : `list`, optional
185 A list of ``lsst.afw.table.ReferenceMatch`` objects
186 (i.e. of ``lsst.afw.table.Match`` with @c first being
187 of type ``lsst.afw.table.SimpleRecord`` and @c second
188 type lsst.afw.table.SourceRecord --- the reference object and detected
189 object respectively) as returned by @em e.g. the AstrometryTask.
190 Used by star selectors that choose to refer to an external catalog.
191
192 Returns
193 -------
194 measurement : `lsst.pipe.base.Struct`
195 PSF measurement as a struct with attributes:
196
197 ``psf``
198 The measured PSF (also set in the input exposure).
199 ``cellSet``
200 An `lsst.afw.math.SpatialCellSet` containing the PSF candidates
201 as returned by the psf determiner.
202 """
203 self.log.info("Measuring PSF")
204
205 import lsstDebug
206 display = lsstDebug.Info(__name__).display
207 displayExposure = lsstDebug.Info(__name__).displayExposure # display the Exposure + spatialCells
208 displayPsfMosaic = lsstDebug.Info(__name__).displayPsfMosaic # show mosaic of reconstructed PSF(x,y)
209 displayPsfCandidates = lsstDebug.Info(__name__).displayPsfCandidates # show mosaic of candidates
210 displayResiduals = lsstDebug.Info(__name__).displayResiduals # show residuals
211 showBadCandidates = lsstDebug.Info(__name__).showBadCandidates # include bad candidates
212 normalizeResiduals = lsstDebug.Info(__name__).normalizeResiduals # normalise residuals by object peak
213
214 #
215 # Run star selector
216 #
217 stars = self.starSelector.run(sourceCat=sources, matches=matches, exposure=exposure)
218 selectionResult = self.makePsfCandidates.run(stars.sourceCat, exposure=exposure)
219 self.log.info("PSF star selector found %d candidates", len(selectionResult.psfCandidates))
220 reserveResult = self.reserve.run(selectionResult.goodStarCat, expId=expId)
221 # Make list of psf candidates to send to the determiner (omitting those marked as reserved)
222 psfDeterminerList = [cand for cand, use
223 in zip(selectionResult.psfCandidates, reserveResult.use) if use]
224
225 if selectionResult.psfCandidates and self.candidateKey is not None:
226 for cand in selectionResult.psfCandidates:
227 source = cand.getSource()
228 source.set(self.candidateKey, True)
229
230 self.log.info("Sending %d candidates to PSF determiner", len(psfDeterminerList))
231
232 if display:
233 frame = 1
234 if displayExposure:
235 disp = afwDisplay.Display(frame=frame)
236 disp.mtv(exposure, title="psf determination")
237 frame += 1
238 #
239 # Determine PSF
240 #
241 psf, cellSet = self.psfDeterminer.determinePsf(exposure, psfDeterminerList, self.metadata,
242 flagKey=self.usedKey)
243 self.log.info("PSF determination using %d/%d stars.",
244 self.metadata.getScalar("numGoodStars"), self.metadata.getScalar("numAvailStars"))
245
246 exposure.setPsf(psf)
247
248 if display:
249 frame = display
250 if displayExposure:
251 disp = afwDisplay.Display(frame=frame)
252 showPsfSpatialCells(exposure, cellSet, showBadCandidates, frame=frame)
253 frame += 1
254
255 if displayPsfCandidates: # Show a mosaic of PSF candidates
256 plotPsfCandidates(cellSet, showBadCandidates=showBadCandidates, frame=frame)
257 frame += 1
258
259 if displayResiduals:
260 frame = plotResiduals(exposure, cellSet,
261 showBadCandidates=showBadCandidates,
262 normalizeResiduals=normalizeResiduals,
263 frame=frame)
264 if displayPsfMosaic:
265 disp = afwDisplay.Display(frame=frame)
266 maUtils.showPsfMosaic(exposure, psf, display=disp, showFwhm=True)
267 disp.scale("linear", 0, 1)
268 frame += 1
269
270 return pipeBase.Struct(
271 psf=psf,
272 cellSet=cellSet,
273 )
274
275 @property
276 def usesMatches(self):
277 """Return True if this task makes use of the "matches" argument to the run method"""
278 return self.starSelector.usesMatches
279
280#
281# Debug code
282#
283
284
285def showPsfSpatialCells(exposure, cellSet, showBadCandidates, frame=1):
286 disp = afwDisplay.Display(frame=frame)
287 maUtils.showPsfSpatialCells(exposure, cellSet,
288 symb="o", ctype=afwDisplay.CYAN, ctypeUnused=afwDisplay.YELLOW,
289 size=4, display=disp)
290 for cell in cellSet.getCellList():
291 for cand in cell.begin(not showBadCandidates): # maybe include bad candidates
292 status = cand.getStatus()
293 disp.dot('+', *cand.getSource().getCentroid(),
294 ctype=afwDisplay.GREEN if status == afwMath.SpatialCellCandidate.GOOD else
295 afwDisplay.YELLOW if status == afwMath.SpatialCellCandidate.UNKNOWN else afwDisplay.RED)
296
297
298def plotPsfCandidates(cellSet, showBadCandidates=False, frame=1):
299 stamps = []
300 for cell in cellSet.getCellList():
301 for cand in cell.begin(not showBadCandidates): # maybe include bad candidates
302 try:
303 im = cand.getMaskedImage()
304
305 chi2 = cand.getChi2()
306 if chi2 < 1e100:
307 chi2 = "%.1f" % chi2
308 else:
309 chi2 = float("nan")
310
311 stamps.append((im, "%d%s" %
312 (maUtils.splitId(cand.getSource().getId(), True)["objId"], chi2),
313 cand.getStatus()))
314 except Exception:
315 continue
316
317 mos = afwDisplay.utils.Mosaic()
318 disp = afwDisplay.Display(frame=frame)
319 for im, label, status in stamps:
320 im = type(im)(im, True)
321 try:
322 im /= afwMath.makeStatistics(im, afwMath.MAX).getValue()
323 except NotImplementedError:
324 pass
325
326 mos.append(im, label,
327 afwDisplay.GREEN if status == afwMath.SpatialCellCandidate.GOOD else
328 afwDisplay.YELLOW if status == afwMath.SpatialCellCandidate.UNKNOWN else afwDisplay.RED)
329
330 if mos.images:
331 disp.mtv(mos.makeMosaic(), title="Psf Candidates")
332
333
334def plotResiduals(exposure, cellSet, showBadCandidates=False, normalizeResiduals=True, frame=2):
335 psf = exposure.getPsf()
336 disp = afwDisplay.Display(frame=frame)
337 while True:
338 try:
339 maUtils.showPsfCandidates(exposure, cellSet, psf=psf, display=disp,
340 normalize=normalizeResiduals,
341 showBadCandidates=showBadCandidates)
342 frame += 1
343 maUtils.showPsfCandidates(exposure, cellSet, psf=psf, display=disp,
344 normalize=normalizeResiduals,
345 showBadCandidates=showBadCandidates,
346 variance=True)
347 frame += 1
348 except Exception:
349 if not showBadCandidates:
350 showBadCandidates = True
351 continue
352 break
353
354 return frame
__init__(self, schema=None, **kwargs)
run(self, exposure, sources, expId=0, matches=None)
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
plotPsfCandidates(cellSet, showBadCandidates=False, frame=1)
plotResiduals(exposure, cellSet, showBadCandidates=False, normalizeResiduals=True, frame=2)
showPsfSpatialCells(exposure, cellSet, showBadCandidates, frame=1)