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 candidates 148 @param[in] exposure: exposure containing the psf candidates (lsst.afw.image.Exposure) 149 @param[in] psfCandidateList: a sequence of PSF candidates (each an lsst.meas.algorithms.PsfCandidate); 150 typically obtained by detecting sources and then running them through a star selector 151 @param[in,out] metadata a home for interesting tidbits of information 152 @param[in] flagKey: schema key used to mark sources actually used in PSF determination 154 @return psf: a meas.extensions.psfex.PsfexPsf 158 displayExposure = display
and \
160 displayPsfComponents = display
and \
162 showBadCandidates = display
and \
164 displayResiduals = display
and \
166 displayPsfMosaic = display
and \
171 mi = exposure.getMaskedImage()
173 nCand = len(psfCandidateList)
175 raise RuntimeError(
"No PSF candidates supplied.")
181 bbox = mi.getBBox(afwImage.PARENT)
182 psfCellSet = afwMath.SpatialCellSet(bbox, self.config.sizeCellX, self.config.sizeCellY)
186 sizes = np.empty(nCand)
187 for i, psfCandidate
in enumerate(psfCandidateList):
190 psfCellSet.insertCandidate(psfCandidate)
191 except Exception
as e:
192 self.log.debug(
"Skipping PSF candidate %d of %d: %s", i, len(psfCandidateList), e)
195 source = psfCandidate.getSource()
196 quad = afwEll.Quadrupole(source.getIxx(), source.getIyy(), source.getIxy())
197 rmsSize = quad.getTraceRadius()
200 if self.config.kernelSize >= 15:
201 self.log.warn(
"NOT scaling kernelSize by stellar quadrupole moment, but using absolute value")
202 actualKernelSize = int(self.config.kernelSize)
204 actualKernelSize = 2 * int(self.config.kernelSize * np.sqrt(np.median(sizes)) + 0.5) + 1
205 if actualKernelSize < self.config.kernelSizeMin:
206 actualKernelSize = self.config.kernelSizeMin
207 if actualKernelSize > self.config.kernelSizeMax:
208 actualKernelSize = self.config.kernelSizeMax
210 rms = np.median(sizes)
211 print(
"Median PSF RMS size=%.2f pixels (\"FWHM\"=%.2f)" % (rms, 2*np.sqrt(2*np.log(2))*rms))
214 pixKernelSize = actualKernelSize
215 if self.config.samplingSize > 0:
216 pixKernelSize = int(actualKernelSize*self.config.samplingSize)
217 if pixKernelSize % 2 == 0:
219 self.log.trace(
"Psfex Kernel size=%.2f, Image Kernel Size=%.2f", actualKernelSize, pixKernelSize)
220 psfCandidateList[0].setHeight(pixKernelSize)
221 psfCandidateList[0].setWidth(pixKernelSize)
227 defaultsFile = os.path.join(os.environ[
"MEAS_EXTENSIONS_PSFEX_DIR"],
"config",
"default-lsst.psfex")
228 args_md = dafBase.PropertySet()
229 args_md.set(
"BASIS_TYPE", str(self.config.psfexBasis))
230 args_md.set(
"PSFVAR_DEGREES", str(self.config.spatialOrder))
231 args_md.set(
"PSF_SIZE", str(actualKernelSize))
232 args_md.set(
"PSF_SAMPLING", str(self.config.samplingSize))
233 prefs = psfex.Prefs(defaultsFile, args_md)
234 prefs.setCommandLine([])
235 prefs.addCatalog(
"psfexPsfDeterminer")
238 principalComponentExclusionFlag = bool(bool(psfex.Context.REMOVEHIDDEN)
239 if False else psfex.Context.KEEPHIDDEN)
240 context = psfex.Context(prefs.getContextName(), prefs.getContextGroup(),
241 prefs.getGroupDeg(), principalComponentExclusionFlag)
242 set = psfex.Set(context)
243 set.setVigSize(pixKernelSize, pixKernelSize)
244 set.setFwhm(2*np.sqrt(2*np.log(2))*np.median(sizes))
245 set.setRecentroid(self.config.recentroid)
248 backnoise2 = afwMath.makeStatistics(mi.getImage(), afwMath.VARIANCECLIP).getValue()
249 ccd = exposure.getDetector()
251 gain = np.mean(np.array([a.getGain()
for a
in ccd]))
254 self.log.warn(
"Setting gain to %g" % (gain,))
257 for i, key
in enumerate(context.getName()):
258 if context.getPcflag(i):
259 contextvalp.append(pcval[pc])
263 contextvalp.append(exposure.getMetadata().getScalar(key[1:]))
265 raise RuntimeError(
"*Error*: %s parameter not found in the header of %s" %
266 (key[1:], prefs.getContextName()))
269 contextvalp.append(np.array([psfCandidateList[_].getSource().get(key)
270 for _
in range(nCand)]))
272 raise RuntimeError(
"*Error*: %s parameter not found" % (key,))
273 set.setContextname(i, key)
278 ds9.mtv(exposure, frame=frame, title=
"psf determination")
280 badBits = mi.getMask().getPlaneBitMask(self.config.badMaskBits)
281 fluxName = prefs.getPhotfluxRkey()
282 fluxFlagName =
"base_" + fluxName +
"_flag" 285 for i, psfCandidate
in enumerate(psfCandidateList):
286 source = psfCandidate.getSource()
287 xc, yc = source.getX(), source.getY()
294 pstamp = psfCandidate.getMaskedImage().clone()
295 except Exception
as e:
298 if fluxFlagName
in source.schema
and source.get(fluxFlagName):
301 flux = source.get(fluxName)
302 if flux < 0
or np.isnan(flux):
309 sample = set.newSample()
310 sample.setCatindex(catindex)
311 sample.setExtindex(ext)
312 sample.setObjindex(i)
314 imArray = pstamp.getImage().getArray()
315 imArray[np.where(np.bitwise_and(pstamp.getMask().getArray(), badBits))] = \
317 sample.setVig(imArray)
320 sample.setBacknoise2(backnoise2)
324 sample.setFluxrad(sizes[i])
326 for j
in range(set.getNcontext()):
327 sample.setContext(j, float(contextvalp[j][i]))
328 except Exception
as e:
329 self.log.debug(
"Exception when processing sample at (%f,%f): %s", xc, yc, e)
332 set.finiSample(sample)
338 with ds9.Buffering():
339 ds9.dot(
"o", xc, yc, ctype=ds9.CYAN, size=4, frame=frame)
341 if set.getNsample() == 0:
342 raise RuntimeError(
"No good PSF candidates to pass to PSFEx")
345 for i
in range(set.getNcontext()):
346 cmin = contextvalp[i].min()
347 cmax = contextvalp[i].max()
348 set.setContextScale(i, cmax - cmin)
349 set.setContextOffset(i, (cmin + cmax)/2.0)
359 field = psfex.Field(
"Unknown")
360 field.addExt(exposure.getWcs(), exposure.getWidth(), exposure.getHeight(), set.getNsample())
368 psfex.makeit(fields, sets)
369 psfs = field.getPsfs()
373 for i
in range(sets[0].getNsample()):
374 index = sets[0].getSample(i).getObjindex()
376 good_indices.append(index)
378 if flagKey
is not None:
379 for i, psfCandidate
in enumerate(psfCandidateList):
380 source = psfCandidate.getSource()
381 if i
in good_indices:
382 source.set(flagKey,
True)
384 xpos = np.array(xpos)
385 ypos = np.array(ypos)
386 numGoodStars = len(good_indices)
387 avgX, avgY = np.mean(xpos), np.mean(ypos)
391 if False and (displayResiduals
or displayPsfMosaic):
396 title =
"psfexPsfDeterminer" 397 psfex.psfex.showPsf(psfs, set, ext,
398 [(exposure.getWcs(), exposure.getWidth(), exposure.getHeight())],
399 nspot=3, trim=5, frame=frame, diagnostics=diagnostics, outDir=catDir,
405 assert psfCellSet
is not None 408 maUtils.showPsfSpatialCells(exposure, psfCellSet, showChi2=
True,
409 symb=
"o", ctype=ds9.YELLOW, ctypeBad=ds9.RED, size=8, frame=frame)
411 maUtils.showPsfCandidates(exposure, psfCellSet, psf=psf, frame=4,
412 normalize=normalizeResiduals,
413 showBadCandidates=showBadCandidates)
414 if displayPsfComponents:
415 maUtils.showPsf(psf, frame=6)
417 maUtils.showPsfMosaic(exposure, psf, frame=7, showFwhm=
True)
418 ds9.scale(
'linear', 0, 1, frame=7)
424 if metadata
is not None:
425 metadata.set(
"spatialFitChi2", np.nan)
426 metadata.set(
"numAvailStars", nCand)
427 metadata.set(
"numGoodStars", numGoodStars)
428 metadata.set(
"avgX", avgX)
429 metadata.set(
"avgY", avgY)
432 return psf, psfCellSet
436 measAlg.psfDeterminerRegistry.register(
"psfex", PsfexPsfDeterminerTask)
def determinePsf(self, exposure, psfCandidateList, metadata=None, flagKey=None)
Represent a PSF as a linear combination of PSFEX (== Karhunen-Loeve) basis functions.