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

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 __nEigenComponents = pexConfig.Field(
39 doc="number of eigen components for PSF kernel creation",
40 dtype=int,
41 default=4,
42 )
43 spatialOrder = pexConfig.Field( 43 ↛ exitline 43 didn't jump to the function exit
44 doc="specify spatial order for PSF kernel creation",
45 dtype=int,
46 default=2,
47 check=lambda x: x >= 0,
48 )
49 sizeCellX = pexConfig.Field( 49 ↛ exitline 49 didn't jump to the function exit
50 doc="size of cell used to determine PSF (pixels, column direction)",
51 dtype=int,
52 default=256,
53 # minValue = 10,
54 check=lambda x: x >= 10,
55 )
56 sizeCellY = pexConfig.Field( 56 ↛ exitline 56 didn't jump to the function exit
57 doc="size of cell used to determine PSF (pixels, row direction)",
58 dtype=int,
59 default=sizeCellX.default,
60 # minValue = 10,
61 check=lambda x: x >= 10,
62 )
63 __nStarPerCell = pexConfig.Field(
64 doc="number of stars per psf cell for PSF kernel creation",
65 dtype=int,
66 default=3,
67 )
68 samplingSize = pexConfig.Field(
69 doc="Resolution of the internal PSF model relative to the pixel size; "
70 "e.g. 0.5 is equal to 2x oversampling",
71 dtype=float,
72 default=1,
73 )
74 badMaskBits = pexConfig.ListField(
75 doc="List of mask bits which cause a source to be rejected as bad "
76 "N.b. INTRP is used specially in PsfCandidateSet; it means \"Contaminated by neighbour\"",
77 dtype=str,
78 default=["INTRP", "SAT"],
79 )
80 psfexBasis = pexConfig.ChoiceField(
81 doc="BASIS value given to psfex. PIXEL_AUTO will use the requested samplingSize only if "
82 "the FWHM < 3 pixels. Otherwise, it will use samplingSize=1. PIXEL will always use the "
83 "requested samplingSize",
84 dtype=str,
85 allowed={
86 "PIXEL": "Always use requested samplingSize",
87 "PIXEL_AUTO": "Only use requested samplingSize when FWHM < 3",
88 },
89 default='PIXEL',
90 optional=False,
91 )
92 __borderWidth = pexConfig.Field(
93 doc="Number of pixels to ignore around the edge of PSF candidate postage stamps",
94 dtype=int,
95 default=0,
96 )
97 __nStarPerCellSpatialFit = pexConfig.Field(
98 doc="number of stars per psf Cell for spatial fitting",
99 dtype=int,
100 default=5,
101 )
102 __constantWeight = pexConfig.Field(
103 doc="Should each PSF candidate be given the same weight, independent of magnitude?",
104 dtype=bool,
105 default=True,
106 )
107 __nIterForPsf = pexConfig.Field(
108 doc="number of iterations of PSF candidate star list",
109 dtype=int,
110 default=3,
111 )
112 tolerance = pexConfig.Field(
113 doc="tolerance of spatial fitting",
114 dtype=float,
115 default=1e-2,
116 )
117 lam = pexConfig.Field(
118 doc="floor for variance is lam*data",
119 dtype=float,
120 default=0.05,
121 )
122 reducedChi2ForPsfCandidates = pexConfig.Field(
123 doc="for psf candidate evaluation",
124 dtype=float,
125 default=2.0,
126 )
127 spatialReject = pexConfig.Field(
128 doc="Rejection threshold (stdev) for candidates based on spatial fit",
129 dtype=float,
130 default=3.0,
131 )
132 recentroid = pexConfig.Field(
133 doc="Should PSFEX be permitted to recentroid PSF candidates?",
134 dtype=bool,
135 default=False,
136 )
138 def setDefaults(self):
139 self.kernelSize = 41
142class PsfexPsfDeterminerTask(measAlg.BasePsfDeterminerTask):
143 ConfigClass = PsfexPsfDeterminerConfig
145 def determinePsf(self, exposure, psfCandidateList, metadata=None, flagKey=None):
146 """Determine a PSFEX PSF model for an exposure given a list of PSF
147 candidates.
149 Parameters
150 ----------
151 exposure: `lsst.afw.image.Exposure`
152 Exposure containing the PSF candidates.
153 psfCandidateList: iterable of `lsst.meas.algorithms.PsfCandidate`
154 Sequence of PSF candidates typically obtained by detecting sources
155 and then running them through a star selector.
156 metadata: metadata, optional
157 A home for interesting tidbits of information.
158 flagKey: `lsst.afw.table.Key`, optional
159 Schema key used to mark sources actually used in PSF determination.
161 Returns
162 -------
163 psf: `lsst.meas.extensions.psfex.PsfexPsf`
164 The determined PSF.
165 """
167 import lsstDebug
168 display = lsstDebug.Info(__name__).display
169 displayExposure = display and \
170 lsstDebug.Info(__name__).displayExposure # display the Exposure + spatialCells
171 displayPsfComponents = display and \
172 lsstDebug.Info(__name__).displayPsfComponents # show the basis functions
173 showBadCandidates = display and \
174 lsstDebug.Info(__name__).showBadCandidates # Include bad candidates (meaningless, methinks)
175 displayResiduals = display and \
176 lsstDebug.Info(__name__).displayResiduals # show residuals
177 displayPsfMosaic = display and \
178 lsstDebug.Info(__name__).displayPsfMosaic # show mosaic of reconstructed PSF(x,y)
179 normalizeResiduals = lsstDebug.Info(__name__).normalizeResiduals
180 afwDisplay.setDefaultMaskTransparency(75)
181 # Normalise residuals by object amplitude
183 mi = exposure.getMaskedImage()
185 nCand = len(psfCandidateList)
186 if nCand == 0:
187 raise RuntimeError("No PSF candidates supplied.")
188 #
189 # How big should our PSF models be?
190 #
191 if display: # only needed for debug plots
192 # construct and populate a spatial cell set
193 bbox = mi.getBBox(afwImage.PARENT)
194 psfCellSet = afwMath.SpatialCellSet(bbox, self.config.sizeCellX, self.config.sizeCellY)
195 else:
196 psfCellSet = None
198 sizes = np.empty(nCand)
199 for i, psfCandidate in enumerate(psfCandidateList):
200 try:
201 if psfCellSet:
202 psfCellSet.insertCandidate(psfCandidate)
203 except Exception as e:
204 self.log.debug("Skipping PSF candidate %d of %d: %s", i, len(psfCandidateList), e)
205 continue
207 source = psfCandidate.getSource()
208 quad = afwEll.Quadrupole(source.getIxx(), source.getIyy(), source.getIxy())
209 rmsSize = quad.getTraceRadius()
210 sizes[i] = rmsSize
212 if self.config.kernelSize >= 15:
213 self.log.warn("NOT scaling kernelSize by stellar quadrupole moment, but using absolute value")
214 actualKernelSize = int(self.config.kernelSize)
215 else:
216 actualKernelSize = 2 * int(self.config.kernelSize * np.sqrt(np.median(sizes)) + 0.5) + 1
217 if actualKernelSize < self.config.kernelSizeMin:
218 actualKernelSize = self.config.kernelSizeMin
219 if actualKernelSize > self.config.kernelSizeMax:
220 actualKernelSize = self.config.kernelSizeMax
221 if display:
222 rms = np.median(sizes)
223 print("Median PSF RMS size=%.2f pixels (\"FWHM\"=%.2f)" % (rms, 2*np.sqrt(2*np.log(2))*rms))
225 # If we manually set the resolution then we need the size in pixel
226 # units
227 pixKernelSize = actualKernelSize
228 if self.config.samplingSize > 0:
229 pixKernelSize = int(actualKernelSize*self.config.samplingSize)
230 if pixKernelSize % 2 == 0:
231 pixKernelSize += 1
232 self.log.trace("Psfex Kernel size=%.2f, Image Kernel Size=%.2f", actualKernelSize, pixKernelSize)
233 psfCandidateList[0].setHeight(pixKernelSize)
234 psfCandidateList[0].setWidth(pixKernelSize)
236 # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- BEGIN PSFEX
237 #
238 # Insert the good candidates into the set
239 #
240 defaultsFile = os.path.join(os.environ["MEAS_EXTENSIONS_PSFEX_DIR"], "config", "default-lsst.psfex")
241 args_md = dafBase.PropertySet()
242 args_md.set("BASIS_TYPE", str(self.config.psfexBasis))
243 args_md.set("PSFVAR_DEGREES", str(self.config.spatialOrder))
244 args_md.set("PSF_SIZE", str(actualKernelSize))
245 args_md.set("PSF_SAMPLING", str(self.config.samplingSize))
246 prefs = psfex.Prefs(defaultsFile, args_md)
247 prefs.setCommandLine([])
248 prefs.addCatalog("psfexPsfDeterminer")
250 prefs.use()
251 principalComponentExclusionFlag = bool(bool(psfex.Context.REMOVEHIDDEN)
252 if False else psfex.Context.KEEPHIDDEN)
253 context = psfex.Context(prefs.getContextName(), prefs.getContextGroup(),
254 prefs.getGroupDeg(), principalComponentExclusionFlag)
255 set = psfex.Set(context)
256 set.setVigSize(pixKernelSize, pixKernelSize)
257 set.setFwhm(2*np.sqrt(2*np.log(2))*np.median(sizes))
258 set.setRecentroid(self.config.recentroid)
260 catindex, ext = 0, 0
261 backnoise2 = afwMath.makeStatistics(mi.getImage(), afwMath.VARIANCECLIP).getValue()
262 ccd = exposure.getDetector()
263 if ccd:
264 gain = np.mean(np.array([a.getGain() for a in ccd]))
265 else:
266 gain = 1.0
267 self.log.warn("Setting gain to %g" % (gain,))
269 pc = 0
270 contextvalp = []
271 for i, key in enumerate(context.getName()):
272 if context.getPcflag(i):
273 raise RuntimeError("Principal Components can not be accessed")
274 contextvalp.append(pcval[pc]) # noqa: F821
275 pc += 1
276 elif key[0] == ':':
277 try:
278 contextvalp.append(exposure.getMetadata().getScalar(key[1:]))
279 except KeyError:
280 raise RuntimeError("*Error*: %s parameter not found in the header of %s" %
281 (key[1:], prefs.getContextName()))
282 else:
283 try:
284 contextvalp.append(np.array([psfCandidateList[_].getSource().get(key)
285 for _ in range(nCand)]))
286 except KeyError:
287 raise RuntimeError("*Error*: %s parameter not found" % (key,))
288 set.setContextname(i, key)
290 if display:
291 frame = 0
292 if displayExposure:
293 disp = afwDisplay.Display(frame=frame)
294 disp.mtv(exposure, title="psf determination")
296 badBits = mi.getMask().getPlaneBitMask(self.config.badMaskBits)
297 fluxName = prefs.getPhotfluxRkey()
298 fluxFlagName = "base_" + fluxName + "_flag"
300 xpos, ypos = [], []
301 for i, psfCandidate in enumerate(psfCandidateList):
302 source = psfCandidate.getSource()
303 xc, yc = source.getX(), source.getY()
304 try:
305 int(xc), int(yc)
306 except ValueError:
307 continue
309 try:
310 pstamp = psfCandidate.getMaskedImage().clone()
311 except Exception:
312 continue
314 if fluxFlagName in source.schema and source.get(fluxFlagName):
315 continue
317 flux = source.get(fluxName)
318 if flux < 0 or np.isnan(flux):
319 continue
321 # From this point, we're configuring the "sample" (PSFEx's version
322 # of a PSF candidate).
323 # Having created the sample, we must proceed to configure it, and
324 # then fini (finalize), or it will be malformed.
325 try:
326 sample = set.newSample()
327 sample.setCatindex(catindex)
328 sample.setExtindex(ext)
329 sample.setObjindex(i)
331 imArray = pstamp.getImage().getArray()
332 imArray[np.where(np.bitwise_and(pstamp.getMask().getArray(), badBits))] = \
333 -2*psfex.BIG
334 sample.setVig(imArray)
336 sample.setNorm(flux)
337 sample.setBacknoise2(backnoise2)
338 sample.setGain(gain)
339 sample.setX(xc)
340 sample.setY(yc)
341 sample.setFluxrad(sizes[i])
343 for j in range(set.getNcontext()):
344 sample.setContext(j, float(contextvalp[j][i]))
345 except Exception as e:
346 self.log.debug("Exception when processing sample at (%f,%f): %s", xc, yc, e)
347 continue
348 else:
349 set.finiSample(sample)
351 xpos.append(xc) # for QA
352 ypos.append(yc)
354 if displayExposure:
355 with disp.Buffering():
356 disp.dot("o", xc, yc, ctype=afwDisplay.CYAN, size=4)
358 if set.getNsample() == 0:
359 raise RuntimeError("No good PSF candidates to pass to PSFEx")
361 # ---- Update min and max and then the scaling
362 for i in range(set.getNcontext()):
363 cmin = contextvalp[i].min()
364 cmax = contextvalp[i].max()
365 set.setContextScale(i, cmax - cmin)
366 set.setContextOffset(i, (cmin + cmax)/2.0)
368 # Don't waste memory!
369 set.trimMemory()
371 # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- END PSFEX
372 #
373 # Do a PSFEX decomposition of those PSF candidates
374 #
375 fields = []
376 field = psfex.Field("Unknown")
377 field.addExt(exposure.getWcs(), exposure.getWidth(), exposure.getHeight(), set.getNsample())
378 field.finalize()
380 fields.append(field)
382 sets = []
383 sets.append(set)
385 psfex.makeit(fields, sets)
386 psfs = field.getPsfs()
388 # Flag which objects were actually used in psfex by
389 good_indices = []
390 for i in range(sets[0].getNsample()):
391 index = sets[0].getSample(i).getObjindex()
392 if index > -1:
393 good_indices.append(index)
395 if flagKey is not None:
396 for i, psfCandidate in enumerate(psfCandidateList):
397 source = psfCandidate.getSource()
398 if i in good_indices:
399 source.set(flagKey, True)
401 xpos = np.array(xpos)
402 ypos = np.array(ypos)
403 numGoodStars = len(good_indices)
404 avgX, avgY = np.mean(xpos), np.mean(ypos)
406 psf = psfex.PsfexPsf(psfs[0], geom.Point2D(avgX, avgY))
408 if False and (displayResiduals or displayPsfMosaic):
409 ext = 0
410 frame = 1
411 diagnostics = True
412 catDir = "."
413 title = "psfexPsfDeterminer"
414 psfex.psfex.showPsf(psfs, set, ext,
415 [(exposure.getWcs(), exposure.getWidth(), exposure.getHeight())],
416 nspot=3, trim=5, frame=frame, diagnostics=diagnostics, outDir=catDir,
417 title=title)
418 #
419 # Display code for debugging
420 #
421 if display:
422 assert psfCellSet is not None
424 if displayExposure:
425 maUtils.showPsfSpatialCells(exposure, psfCellSet, showChi2=True,
426 symb="o", ctype=afwDisplay.YELLOW, ctypeBad=afwDisplay.RED,
427 size=8, display=disp)
428 if displayResiduals:
429 disp4 = afwDisplay.Display(frame=4)
430 maUtils.showPsfCandidates(exposure, psfCellSet, psf=psf, display=disp4,
431 normalize=normalizeResiduals,
432 showBadCandidates=showBadCandidates)
433 if displayPsfComponents:
434 disp6 = afwDisplay.Display(frame=6)
435 maUtils.showPsf(psf, display=disp6)
436 if displayPsfMosaic:
437 disp7 = afwDisplay.Display(frame=7)
438 maUtils.showPsfMosaic(exposure, psf, display=disp7, showFwhm=True)
439 disp.scale('linear', 0, 1)
440 #
441 # Generate some QA information
442 #
443 # Count PSF stars
444 #
445 if metadata is not None:
446 metadata.set("spatialFitChi2", np.nan)
447 metadata.set("numAvailStars", nCand)
448 metadata.set("numGoodStars", numGoodStars)
449 metadata.set("avgX", avgX)
450 metadata.set("avgY", avgY)
452 return psf, psfCellSet
455measAlg.psfDeterminerRegistry.register("psfex", PsfexPsfDeterminerTask)