26 import lsst.pex.config
as pexConfig
38 __nEigenComponents = pexConfig.Field(
39 doc=
"number of eigen components for PSF kernel creation",
43 spatialOrder = pexConfig.Field(
44 doc=
"specify spatial order for PSF kernel creation",
47 check=
lambda x: x >= 0,
49 sizeCellX = pexConfig.Field(
50 doc=
"size of cell used to determine PSF (pixels, column direction)",
54 check=
lambda x: x >= 10,
56 sizeCellY = pexConfig.Field(
57 doc=
"size of cell used to determine PSF (pixels, row direction)",
59 default=sizeCellX.default,
61 check=
lambda x: x >= 10,
63 __nStarPerCell = pexConfig.Field(
64 doc=
"number of stars per psf cell for PSF kernel creation",
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",
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\"",
78 default=[
"INTRP",
"SAT"],
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",
86 "PIXEL":
"Always use requested samplingSize",
87 "PIXEL_AUTO":
"Only use requested samplingSize when FWHM < 3",
92 __borderWidth = pexConfig.Field(
93 doc=
"Number of pixels to ignore around the edge of PSF candidate postage stamps",
97 __nStarPerCellSpatialFit = pexConfig.Field(
98 doc=
"number of stars per psf Cell for spatial fitting",
102 __constantWeight = pexConfig.Field(
103 doc=
"Should each PSF candidate be given the same weight, independent of magnitude?",
107 __nIterForPsf = pexConfig.Field(
108 doc=
"number of iterations of PSF candidate star list",
112 tolerance = pexConfig.Field(
113 doc=
"tolerance of spatial fitting",
117 lam = pexConfig.Field(
118 doc=
"floor for variance is lam*data",
122 reducedChi2ForPsfCandidates = pexConfig.Field(
123 doc=
"for psf candidate evaluation",
127 spatialReject = pexConfig.Field(
128 doc=
"Rejection threshold (stdev) for candidates based on spatial fit",
132 recentroid = pexConfig.Field(
133 doc=
"Should PSFEX be permitted to recentroid PSF candidates?",
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
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.
163 psf: `lsst.meas.extensions.psfex.PsfexPsf`
169 displayExposure = display
and \
171 displayPsfComponents = display
and \
173 showBadCandidates = display
and \
175 displayResiduals = display
and \
177 displayPsfMosaic = display
and \
180 afwDisplay.setDefaultMaskTransparency(75)
183 mi = exposure.getMaskedImage()
185 nCand = len(psfCandidateList)
187 raise RuntimeError(
"No PSF candidates supplied.")
193 bbox = mi.getBBox(afwImage.PARENT)
198 sizes = np.empty(nCand)
199 for i, psfCandidate
in enumerate(psfCandidateList):
202 psfCellSet.insertCandidate(psfCandidate)
203 except Exception
as e:
204 self.log.
debug(
"Skipping PSF candidate %d of %d: %s", i, len(psfCandidateList), e)
207 source = psfCandidate.getSource()
208 quad = afwEll.Quadrupole(source.getIxx(), source.getIyy(), source.getIxy())
209 rmsSize = quad.getTraceRadius()
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)
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
222 rms = np.median(sizes)
223 print(
"Median PSF RMS size=%.2f pixels (\"FWHM\"=%.2f)" % (rms, 2*np.sqrt(2*np.log(2))*rms))
227 pixKernelSize = actualKernelSize
228 if self.config.samplingSize > 0:
229 pixKernelSize = int(actualKernelSize*self.config.samplingSize)
230 if pixKernelSize % 2 == 0:
232 self.log.
trace(
"Psfex Kernel size=%.2f, Image Kernel Size=%.2f", actualKernelSize, pixKernelSize)
233 psfCandidateList[0].setHeight(pixKernelSize)
234 psfCandidateList[0].setWidth(pixKernelSize)
240 defaultsFile = os.path.join(os.environ[
"MEAS_EXTENSIONS_PSFEX_DIR"],
"config",
"default-lsst.psfex")
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")
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)
262 ccd = exposure.getDetector()
264 gain = np.mean(np.array([a.getGain()
for a
in ccd]))
267 self.log.
warn(
"Setting gain to %g" % (gain,))
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])
278 contextvalp.append(exposure.getMetadata().getScalar(key[1:]))
280 raise RuntimeError(
"*Error*: %s parameter not found in the header of %s" %
281 (key[1:], prefs.getContextName()))
284 contextvalp.append(np.array([psfCandidateList[_].getSource().get(key)
285 for _
in range(nCand)]))
287 raise RuntimeError(
"*Error*: %s parameter not found" % (key,))
288 set.setContextname(i, key)
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"
301 for i, psfCandidate
in enumerate(psfCandidateList):
302 source = psfCandidate.getSource()
303 xc, yc = source.getX(), source.getY()
310 pstamp = psfCandidate.getMaskedImage().
clone()
314 if fluxFlagName
in source.schema
and source.get(fluxFlagName):
317 flux = source.get(fluxName)
318 if flux < 0
or np.isnan(flux):
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))] = \
334 sample.setVig(imArray)
337 sample.setBacknoise2(backnoise2)
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)
349 set.finiSample(sample)
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")
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)
376 field = psfex.Field(
"Unknown")
377 field.addExt(exposure.getWcs(), exposure.getWidth(), exposure.getHeight(), set.getNsample())
385 psfex.makeit(fields, sets)
386 psfs = field.getPsfs()
390 for i
in range(sets[0].getNsample()):
391 index = sets[0].getSample(i).getObjindex()
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)
408 if False and (displayResiduals
or displayPsfMosaic):
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,
422 assert psfCellSet
is not None
425 maUtils.showPsfSpatialCells(exposure, psfCellSet, showChi2=
True,
426 symb=
"o", ctype=afwDisplay.YELLOW, ctypeBad=afwDisplay.RED,
427 size=8, display=disp)
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)
437 disp7 = afwDisplay.Display(frame=7)
438 maUtils.showPsfMosaic(exposure, psf, display=disp7, showFwhm=
True)
439 disp.scale(
'linear', 0, 1)
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
455 measAlg.psfDeterminerRegistry.register(
"psfex", PsfexPsfDeterminerTask)