38 spatialOrder = pexConfig.Field(
39 doc=
"specify spatial order for PSF kernel creation",
42 check=
lambda x: x >= 0,
44 sizeCellX = pexConfig.Field(
45 doc=
"size of cell used to determine PSF (pixels, column direction)",
49 check=
lambda x: x >= 10,
51 sizeCellY = pexConfig.Field(
52 doc=
"size of cell used to determine PSF (pixels, row direction)",
54 default=sizeCellX.default,
56 check=
lambda x: x >= 10,
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",
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\"",
68 default=[
"INTRP",
"SAT"],
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",
76 "PIXEL":
"Always use requested samplingSize",
77 "PIXEL_AUTO":
"Only use requested samplingSize when FWHM < 3",
82 tolerance = pexConfig.Field(
83 doc=
"tolerance of spatial fitting",
87 lam = pexConfig.Field(
88 doc=
"floor for variance is lam*data",
92 reducedChi2ForPsfCandidates = pexConfig.Field(
93 doc=
"for psf candidate evaluation",
97 spatialReject = pexConfig.Field(
98 doc=
"Rejection threshold (stdev) for candidates based on spatial fit",
102 recentroid = pexConfig.Field(
103 doc=
"Should PSFEX be permitted to recentroid PSF candidates?",
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."),
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
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.
137 psf: `lsst.meas.extensions.psfex.PsfexPsf`
143 displayExposure = display
and \
145 displayPsfComponents = display
and \
147 showBadCandidates = display
and \
149 displayResiduals = display
and \
151 displayPsfMosaic = display
and \
154 afwDisplay.setDefaultMaskTransparency(75)
157 mi = exposure.getMaskedImage()
159 nCand = len(psfCandidateList)
161 raise RuntimeError(
"No PSF candidates supplied.")
167 bbox = mi.getBBox(afwImage.PARENT)
172 sizes = np.empty(nCand)
173 for i, psfCandidate
in enumerate(psfCandidateList):
176 psfCellSet.insertCandidate(psfCandidate)
177 except Exception
as e:
178 self.log.
error(
"Skipping PSF candidate %d of %d: %s", i, len(psfCandidateList), e)
181 source = psfCandidate.getSource()
182 quad = afwEll.Quadrupole(source.getIxx(), source.getIyy(), source.getIxy())
183 rmsSize = quad.getTraceRadius()
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
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
196 rms = np.median(sizes)
197 msg =
"Median PSF RMS size=%.2f pixels (\"FWHM\"=%.2f)" % (rms, 2*np.sqrt(2*np.log(2))*rms)
202 pixKernelSize = actualKernelSize
203 if self.config.samplingSize > 0:
204 pixKernelSize = int(actualKernelSize*self.config.samplingSize)
205 if pixKernelSize % 2 == 0:
207 self.log.
trace(
"Psfex Kernel size=%.2f, Image Kernel Size=%.2f", actualKernelSize, pixKernelSize)
208 psfCandidateList[0].setHeight(pixKernelSize)
209 psfCandidateList[0].setWidth(pixKernelSize)
215 defaultsFile = os.path.join(os.environ[
"MEAS_EXTENSIONS_PSFEX_DIR"],
"config",
"default-lsst.psfex")
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")
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)
237 ccd = exposure.getDetector()
239 gain = np.mean(np.array([a.getGain()
for a
in ccd]))
242 self.log.
warn(
"Setting gain to %g" % (gain,))
245 for i, key
in enumerate(context.getName()):
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
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)
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"
271 for i, psfCandidate
in enumerate(psfCandidateList):
272 source = psfCandidate.getSource()
275 xc, yc = source.getX(), source.getY()
276 if not np.isfinite(xc)
or not np.isfinite(yc):
279 if fluxFlagName
in source.schema
and source.get(fluxFlagName):
282 flux = source.get(fluxName)
283 if flux < 0
or not np.isfinite(flux):
286 pstamp = psfCandidate.getMaskedImage().
clone()
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))] = \
301 sample.setVig(imArray)
304 sample.setBacknoise2(backnoise2)
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)
316 psfSet.finiSample(sample)
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")
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)
343 field = psfex.Field(
"Unknown")
344 field.addExt(exposure.getWcs(), exposure.getWidth(), exposure.getHeight(), psfSet.getNsample())
352 psfex.makeit(fields, sets)
353 psfs = field.getPsfs()
357 for i
in range(sets[0].getNsample()):
358 index = sets[0].getSample(i).getObjindex()
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)
379 assert psfCellSet
is not None
382 maUtils.showPsfSpatialCells(exposure, psfCellSet, showChi2=
True,
383 symb=
"o", ctype=afwDisplay.YELLOW, ctypeBad=afwDisplay.RED,
384 size=8, display=disp)
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)
394 disp7 = afwDisplay.Display(frame=7)
395 maUtils.showPsfMosaic(exposure, psf, display=disp7, showFwhm=
True)
396 disp.scale(
'linear', 0, 1)
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
412 measAlg.psfDeterminerRegistry.register(
"psfex", PsfexPsfDeterminerTask)