Coverage for python/lsst/meas/extensions/psfex/psfexPsfDeterminer.py : 11%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#
2# LSST Data Management System
3# Copyright 2008-2015 AURA/LSST.
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 <https://www.lsstcorp.org/LegalNotices/>.
21#
22import os
23import numpy as np
25import lsst.daf.base as dafBase
26import lsst.pex.config as pexConfig
27import lsst.geom as geom
28import lsst.afw.geom.ellipses as afwEll
29import lsst.afw.display as afwDisplay
30import lsst.afw.image as afwImage
31import lsst.afw.math as afwMath
32import lsst.meas.algorithms as measAlg
33import lsst.meas.algorithms.utils as maUtils
34import lsst.meas.extensions.psfex as psfex
37class PsfexPsfDeterminerConfig(measAlg.BasePsfDeterminerConfig):
38 spatialOrder = pexConfig.Field( 38 ↛ exitline 38 didn't jump to the function exit
39 doc="specify spatial order for PSF kernel creation",
40 dtype=int,
41 default=2,
42 check=lambda x: x >= 0,
43 )
44 sizeCellX = pexConfig.Field( 44 ↛ exitline 44 didn't jump to the function exit
45 doc="size of cell used to determine PSF (pixels, column direction)",
46 dtype=int,
47 default=256,
48 # minValue = 10,
49 check=lambda x: x >= 10,
50 )
51 sizeCellY = pexConfig.Field( 51 ↛ exitline 51 didn't jump to the function exit
52 doc="size of cell used to determine PSF (pixels, row direction)",
53 dtype=int,
54 default=sizeCellX.default,
55 # minValue = 10,
56 check=lambda x: x >= 10,
57 )
58 samplingSize = pexConfig.Field(
59 doc="Resolution of the internal PSF model relative to the pixel size; "
60 "e.g. 0.5 is equal to 2x oversampling",
61 dtype=float,
62 default=0.5,
63 )
64 badMaskBits = pexConfig.ListField(
65 doc="List of mask bits which cause a source to be rejected as bad "
66 "N.b. INTRP is used specially in PsfCandidateSet; it means \"Contaminated by neighbour\"",
67 dtype=str,
68 default=["INTRP", "SAT"],
69 )
70 psfexBasis = pexConfig.ChoiceField(
71 doc="BASIS value given to psfex. PIXEL_AUTO will use the requested samplingSize only if "
72 "the FWHM < 3 pixels. Otherwise, it will use samplingSize=1. PIXEL will always use the "
73 "requested samplingSize",
74 dtype=str,
75 allowed={
76 "PIXEL": "Always use requested samplingSize",
77 "PIXEL_AUTO": "Only use requested samplingSize when FWHM < 3",
78 },
79 default='PIXEL_AUTO',
80 optional=False,
81 )
82 tolerance = pexConfig.Field(
83 doc="tolerance of spatial fitting",
84 dtype=float,
85 default=1e-2,
86 )
87 lam = pexConfig.Field(
88 doc="floor for variance is lam*data",
89 dtype=float,
90 default=0.05,
91 )
92 reducedChi2ForPsfCandidates = pexConfig.Field(
93 doc="for psf candidate evaluation",
94 dtype=float,
95 default=2.0,
96 )
97 spatialReject = pexConfig.Field(
98 doc="Rejection threshold (stdev) for candidates based on spatial fit",
99 dtype=float,
100 default=3.0,
101 )
102 recentroid = pexConfig.Field(
103 doc="Should PSFEX be permitted to recentroid PSF candidates?",
104 dtype=bool,
105 default=False,
106 )
107 kernelSize = pexConfig.Field(
108 doc=("Size of the postage stamp around each star that is extracted for fitting."
109 "Note: this reflects the oversampling setting of the psf, set by `samplingSize`;"
110 "e.g. `samplingSize=0.5` would require this value to be 2x what you expect."),
111 dtype=int,
112 default=81,
113 )
116class PsfexPsfDeterminerTask(measAlg.BasePsfDeterminerTask):
117 ConfigClass = PsfexPsfDeterminerConfig
119 def determinePsf(self, exposure, psfCandidateList, metadata=None, flagKey=None):
120 """Determine a PSFEX PSF model for an exposure given a list of PSF
121 candidates.
123 Parameters
124 ----------
125 exposure: `lsst.afw.image.Exposure`
126 Exposure containing the PSF candidates.
127 psfCandidateList: iterable of `lsst.meas.algorithms.PsfCandidate`
128 Sequence of PSF candidates typically obtained by detecting sources
129 and then running them through a star selector.
130 metadata: metadata, optional
131 A home for interesting tidbits of information.
132 flagKey: `lsst.afw.table.Key`, optional
133 Schema key used to mark sources actually used in PSF determination.
135 Returns
136 -------
137 psf: `lsst.meas.extensions.psfex.PsfexPsf`
138 The determined PSF.
139 """
141 import lsstDebug
142 display = lsstDebug.Info(__name__).display
143 displayExposure = display and \
144 lsstDebug.Info(__name__).displayExposure # display the Exposure + spatialCells
145 displayPsfComponents = display and \
146 lsstDebug.Info(__name__).displayPsfComponents # show the basis functions
147 showBadCandidates = display and \
148 lsstDebug.Info(__name__).showBadCandidates # Include bad candidates (meaningless, methinks)
149 displayResiduals = display and \
150 lsstDebug.Info(__name__).displayResiduals # show residuals
151 displayPsfMosaic = display and \
152 lsstDebug.Info(__name__).displayPsfMosaic # show mosaic of reconstructed PSF(x,y)
153 normalizeResiduals = lsstDebug.Info(__name__).normalizeResiduals
154 afwDisplay.setDefaultMaskTransparency(75)
155 # Normalise residuals by object amplitude
157 mi = exposure.getMaskedImage()
159 nCand = len(psfCandidateList)
160 if nCand == 0:
161 raise RuntimeError("No PSF candidates supplied.")
162 #
163 # How big should our PSF models be?
164 #
165 if display: # only needed for debug plots
166 # construct and populate a spatial cell set
167 bbox = mi.getBBox(afwImage.PARENT)
168 psfCellSet = afwMath.SpatialCellSet(bbox, self.config.sizeCellX, self.config.sizeCellY)
169 else:
170 psfCellSet = None
172 sizes = np.empty(nCand)
173 for i, psfCandidate in enumerate(psfCandidateList):
174 try:
175 if psfCellSet:
176 psfCellSet.insertCandidate(psfCandidate)
177 except Exception as e:
178 self.log.error("Skipping PSF candidate %d of %d: %s", i, len(psfCandidateList), e)
179 continue
181 source = psfCandidate.getSource()
182 quad = afwEll.Quadrupole(source.getIxx(), source.getIyy(), source.getIxy())
183 rmsSize = quad.getTraceRadius()
184 sizes[i] = rmsSize
186 if self.config.kernelSize >= 15:
187 self.log.warn("NOT scaling kernelSize by stellar quadrupole moment, but using absolute value")
188 actualKernelSize = self.config.kernelSize
189 else:
190 actualKernelSize = 2 * int(self.config.kernelSize * np.sqrt(np.median(sizes)) + 0.5) + 1
191 if actualKernelSize < self.config.kernelSizeMin:
192 actualKernelSize = self.config.kernelSizeMin
193 if actualKernelSize > self.config.kernelSizeMax:
194 actualKernelSize = self.config.kernelSizeMax
195 if display:
196 rms = np.median(sizes)
197 msg = "Median PSF RMS size=%.2f pixels (\"FWHM\"=%.2f)" % (rms, 2*np.sqrt(2*np.log(2))*rms)
198 self.log.debug(msg)
200 # If we manually set the resolution then we need the size in pixel
201 # units
202 pixKernelSize = actualKernelSize
203 if self.config.samplingSize > 0:
204 pixKernelSize = int(actualKernelSize*self.config.samplingSize)
205 if pixKernelSize % 2 == 0:
206 pixKernelSize += 1
207 self.log.trace("Psfex Kernel size=%.2f, Image Kernel Size=%.2f", actualKernelSize, pixKernelSize)
208 psfCandidateList[0].setHeight(pixKernelSize)
209 psfCandidateList[0].setWidth(pixKernelSize)
211 # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- BEGIN PSFEX
212 #
213 # Insert the good candidates into the set
214 #
215 defaultsFile = os.path.join(os.environ["MEAS_EXTENSIONS_PSFEX_DIR"], "config", "default-lsst.psfex")
216 args_md = dafBase.PropertySet()
217 args_md.set("BASIS_TYPE", str(self.config.psfexBasis))
218 args_md.set("PSFVAR_DEGREES", str(self.config.spatialOrder))
219 args_md.set("PSF_SIZE", str(actualKernelSize))
220 args_md.set("PSF_SAMPLING", str(self.config.samplingSize))
221 prefs = psfex.Prefs(defaultsFile, args_md)
222 prefs.setCommandLine([])
223 prefs.addCatalog("psfexPsfDeterminer")
225 prefs.use()
226 principalComponentExclusionFlag = bool(bool(psfex.Context.REMOVEHIDDEN)
227 if False else psfex.Context.KEEPHIDDEN)
228 context = psfex.Context(prefs.getContextName(), prefs.getContextGroup(),
229 prefs.getGroupDeg(), principalComponentExclusionFlag)
230 psfSet = psfex.Set(context)
231 psfSet.setVigSize(pixKernelSize, pixKernelSize)
232 psfSet.setFwhm(2*np.sqrt(2*np.log(2))*np.median(sizes))
233 psfSet.setRecentroid(self.config.recentroid)
235 catindex, ext = 0, 0
236 backnoise2 = afwMath.makeStatistics(mi.getImage(), afwMath.VARIANCECLIP).getValue()
237 ccd = exposure.getDetector()
238 if ccd:
239 gain = np.mean(np.array([a.getGain() for a in ccd]))
240 else:
241 gain = 1.0
242 self.log.warn("Setting gain to %g" % (gain,))
244 contextvalp = []
245 for i, key in enumerate(context.getName()):
246 if key[0] == ':':
247 try:
248 contextvalp.append(exposure.getMetadata().getScalar(key[1:]))
249 except KeyError as e:
250 raise RuntimeError("%s parameter not found in the header of %s" %
251 (key[1:], prefs.getContextName())) from e
252 else:
253 try:
254 contextvalp.append(np.array([psfCandidateList[_].getSource().get(key)
255 for _ in range(nCand)]))
256 except KeyError as e:
257 raise RuntimeError("%s parameter not found" % (key,)) from e
258 psfSet.setContextname(i, key)
260 if display:
261 frame = 0
262 if displayExposure:
263 disp = afwDisplay.Display(frame=frame)
264 disp.mtv(exposure, title="psf determination")
266 badBits = mi.getMask().getPlaneBitMask(self.config.badMaskBits)
267 fluxName = prefs.getPhotfluxRkey()
268 fluxFlagName = "base_" + fluxName + "_flag"
270 xpos, ypos = [], []
271 for i, psfCandidate in enumerate(psfCandidateList):
272 source = psfCandidate.getSource()
274 # skip sources with bad centroids
275 xc, yc = source.getX(), source.getY()
276 if not np.isfinite(xc) or not np.isfinite(yc):
277 continue
278 # skip flagged sources
279 if fluxFlagName in source.schema and source.get(fluxFlagName):
280 continue
281 # skip nonfinite and negative sources
282 flux = source.get(fluxName)
283 if flux < 0 or not np.isfinite(flux):
284 continue
286 pstamp = psfCandidate.getMaskedImage().clone()
288 # From this point, we're configuring the "sample" (PSFEx's version
289 # of a PSF candidate).
290 # Having created the sample, we must proceed to configure it, and
291 # then fini (finalize), or it will be malformed.
292 try:
293 sample = psfSet.newSample()
294 sample.setCatindex(catindex)
295 sample.setExtindex(ext)
296 sample.setObjindex(i)
298 imArray = pstamp.getImage().getArray()
299 imArray[np.where(np.bitwise_and(pstamp.getMask().getArray(), badBits))] = \
300 -2*psfex.BIG
301 sample.setVig(imArray)
303 sample.setNorm(flux)
304 sample.setBacknoise2(backnoise2)
305 sample.setGain(gain)
306 sample.setX(xc)
307 sample.setY(yc)
308 sample.setFluxrad(sizes[i])
310 for j in range(psfSet.getNcontext()):
311 sample.setContext(j, float(contextvalp[j][i]))
312 except Exception as e:
313 self.log.error("Exception when processing sample at (%f,%f): %s", xc, yc, e)
314 continue
315 else:
316 psfSet.finiSample(sample)
318 xpos.append(xc) # for QA
319 ypos.append(yc)
321 if displayExposure:
322 with disp.Buffering():
323 disp.dot("o", xc, yc, ctype=afwDisplay.CYAN, size=4)
325 if psfSet.getNsample() == 0:
326 raise RuntimeError("No good PSF candidates to pass to PSFEx")
328 # ---- Update min and max and then the scaling
329 for i in range(psfSet.getNcontext()):
330 cmin = contextvalp[i].min()
331 cmax = contextvalp[i].max()
332 psfSet.setContextScale(i, cmax - cmin)
333 psfSet.setContextOffset(i, (cmin + cmax)/2.0)
335 # Don't waste memory!
336 psfSet.trimMemory()
338 # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- END PSFEX
339 #
340 # Do a PSFEX decomposition of those PSF candidates
341 #
342 fields = []
343 field = psfex.Field("Unknown")
344 field.addExt(exposure.getWcs(), exposure.getWidth(), exposure.getHeight(), psfSet.getNsample())
345 field.finalize()
347 fields.append(field)
349 sets = []
350 sets.append(psfSet)
352 psfex.makeit(fields, sets)
353 psfs = field.getPsfs()
355 # Flag which objects were actually used in psfex by
356 good_indices = []
357 for i in range(sets[0].getNsample()):
358 index = sets[0].getSample(i).getObjindex()
359 if index > -1:
360 good_indices.append(index)
362 if flagKey is not None:
363 for i, psfCandidate in enumerate(psfCandidateList):
364 source = psfCandidate.getSource()
365 if i in good_indices:
366 source.set(flagKey, True)
368 xpos = np.array(xpos)
369 ypos = np.array(ypos)
370 numGoodStars = len(good_indices)
371 avgX, avgY = np.mean(xpos), np.mean(ypos)
373 psf = psfex.PsfexPsf(psfs[0], geom.Point2D(avgX, avgY))
375 #
376 # Display code for debugging
377 #
378 if display:
379 assert psfCellSet is not None
381 if displayExposure:
382 maUtils.showPsfSpatialCells(exposure, psfCellSet, showChi2=True,
383 symb="o", ctype=afwDisplay.YELLOW, ctypeBad=afwDisplay.RED,
384 size=8, display=disp)
385 if displayResiduals:
386 disp4 = afwDisplay.Display(frame=4)
387 maUtils.showPsfCandidates(exposure, psfCellSet, psf=psf, display=disp4,
388 normalize=normalizeResiduals,
389 showBadCandidates=showBadCandidates)
390 if displayPsfComponents:
391 disp6 = afwDisplay.Display(frame=6)
392 maUtils.showPsf(psf, display=disp6)
393 if displayPsfMosaic:
394 disp7 = afwDisplay.Display(frame=7)
395 maUtils.showPsfMosaic(exposure, psf, display=disp7, showFwhm=True)
396 disp.scale('linear', 0, 1)
397 #
398 # Generate some QA information
399 #
400 # Count PSF stars
401 #
402 if metadata is not None:
403 metadata.set("spatialFitChi2", np.nan)
404 metadata.set("numAvailStars", nCand)
405 metadata.set("numGoodStars", numGoodStars)
406 metadata.set("avgX", avgX)
407 metadata.set("avgY", avgY)
409 return psf, psfCellSet
412measAlg.psfDeterminerRegistry.register("psfex", PsfexPsfDeterminerTask)