22__all__ = (
"PsfexPsfDeterminerConfig",
"PsfexPsfDeterminerTask")
41 spatialOrder = pexConfig.Field[int](
42 doc=
"specify spatial order for PSF kernel creation",
44 check=
lambda x: x >= 1,
46 sizeCellX = pexConfig.Field[int](
47 doc=
"size of cell used to determine PSF (pixels, column direction)",
50 check=
lambda x: x >= 10,
52 sizeCellY = pexConfig.Field[int](
53 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[float](
59 doc=
"Resolution of the internal PSF model relative to the pixel size; "
60 "e.g. 0.5 is equal to 2x oversampling",
63 badMaskBits = pexConfig.ListField[str](
64 doc=
"List of mask bits which cause a source to be rejected as bad "
65 "N.b. INTRP is used specially in PsfCandidateSet; it means \"Contaminated by neighbour\"",
66 default=[
"INTRP",
"SAT"],
68 psfexBasis = pexConfig.ChoiceField[str](
69 doc=
"BASIS value given to psfex. PIXEL_AUTO will use the requested samplingSize only if "
70 "the FWHM < 3 pixels. Otherwise, it will use samplingSize=1. PIXEL will always use the "
71 "requested samplingSize",
73 "PIXEL":
"Always use requested samplingSize",
74 "PIXEL_AUTO":
"Only use requested samplingSize when FWHM < 3",
79 tolerance = pexConfig.Field[float](
80 doc=
"tolerance of spatial fitting",
83 lam = pexConfig.Field[float](
84 doc=
"floor for variance is lam*data",
87 reducedChi2ForPsfCandidates = pexConfig.Field[float](
88 doc=
"for psf candidate evaluation",
91 spatialReject = pexConfig.Field[float](
92 doc=
"Rejection threshold (stdev) for candidates based on spatial fit",
95 recentroid = pexConfig.Field[bool](
96 doc=
"Should PSFEX be permitted to recentroid PSF candidates?",
106 ConfigClass = PsfexPsfDeterminerConfig
108 def determinePsf(self, exposure, psfCandidateList, metadata=None, flagKey=None):
109 """Determine a PSFEX PSF model for an exposure given a list of PSF
115 Exposure containing the PSF candidates.
117 Sequence of PSF candidates typically obtained by detecting sources
118 and then running them through a star selector.
119 metadata: metadata, optional
120 A home
for interesting tidbits of information.
122 Schema key used to mark sources actually used
in PSF determination.
132 displayExposure = display
and \
134 displayPsfComponents = display
and \
136 showBadCandidates = display
and \
138 displayResiduals = display
and \
140 displayPsfMosaic = display
and \
143 afwDisplay.setDefaultMaskTransparency(75)
146 mi = exposure.getMaskedImage()
148 nCand = len(psfCandidateList)
150 raise RuntimeError(
"No PSF candidates supplied.")
156 bbox = mi.getBBox(afwImage.PARENT)
161 sizes = np.empty(nCand)
162 for i, psfCandidate
in enumerate(psfCandidateList):
165 psfCellSet.insertCandidate(psfCandidate)
166 except Exception
as e:
167 self.log.error(
"Skipping PSF candidate %d of %d: %s", i, len(psfCandidateList), e)
170 source = psfCandidate.getSource()
171 quad = afwEll.Quadrupole(source.getIxx(), source.getIyy(), source.getIxy())
172 rmsSize = quad.getTraceRadius()
175 pixKernelSize = self.config.stampSize
176 actualKernelSize = int(2*np.floor(0.5*pixKernelSize/self.config.samplingSize) + 1)
179 rms = np.median(sizes)
180 self.log.debug(
"Median PSF RMS size=%.2f pixels (\"FWHM\"=%.2f)",
181 rms, 2*np.sqrt(2*np.log(2))*rms)
183 self.log.trace(
"Psfex Kernel size=%.2f, Image Kernel Size=%.2f", actualKernelSize, pixKernelSize)
189 defaultsFile = os.path.join(os.environ[
"MEAS_EXTENSIONS_PSFEX_DIR"],
"config",
"default-lsst.psfex")
191 args_md.set(
"BASIS_TYPE", str(self.config.psfexBasis))
192 args_md.set(
"PSFVAR_DEGREES", str(self.config.spatialOrder))
193 args_md.set(
"PSF_SIZE", str(actualKernelSize))
194 args_md.set(
"PSF_SAMPLING", str(self.config.samplingSize))
195 prefs = psfex.Prefs(defaultsFile, args_md)
196 prefs.setCommandLine([])
197 prefs.addCatalog(
"psfexPsfDeterminer")
200 principalComponentExclusionFlag = bool(bool(psfex.Context.REMOVEHIDDEN)
201 if False else psfex.Context.KEEPHIDDEN)
202 context = psfex.Context(prefs.getContextName(), prefs.getContextGroup(),
203 prefs.getGroupDeg(), principalComponentExclusionFlag)
204 psfSet = psfex.Set(context)
205 psfSet.setVigSize(pixKernelSize, pixKernelSize)
206 psfSet.setFwhm(2*np.sqrt(2*np.log(2))*np.median(sizes))
207 psfSet.setRecentroid(self.config.recentroid)
211 ccd = exposure.getDetector()
213 gain = np.mean(np.array([a.getGain()
for a
in ccd]))
216 self.log.warning(
"Setting gain to %g", gain)
219 for i, key
in enumerate(context.getName()):
222 contextvalp.append(exposure.getMetadata().getScalar(key[1:]))
223 except KeyError
as e:
224 raise RuntimeError(
"%s parameter not found in the header of %s" %
225 (key[1:], prefs.getContextName()))
from e
228 contextvalp.append(np.array([psfCandidateList[_].getSource().get(key)
229 for _
in range(nCand)]))
230 except KeyError
as e:
231 raise RuntimeError(
"%s parameter not found" % (key,))
from e
232 psfSet.setContextname(i, key)
237 disp = afwDisplay.Display(frame=frame)
238 disp.mtv(exposure, title=
"psf determination")
240 badBits = mi.getMask().getPlaneBitMask(self.config.badMaskBits)
241 fluxName = prefs.getPhotfluxRkey()
242 fluxFlagName =
"base_" + fluxName +
"_flag"
245 for i, psfCandidate
in enumerate(psfCandidateList):
246 source = psfCandidate.getSource()
249 xc, yc = source.getX(), source.getY()
250 if not np.isfinite(xc)
or not np.isfinite(yc):
253 if fluxFlagName
in source.schema
and source.get(fluxFlagName):
256 flux = source.get(fluxName)
257 if flux < 0
or not np.isfinite(flux):
261 pstamp = psfCandidate.getMaskedImage(pixKernelSize, pixKernelSize).clone()
263 self.log.warning(
"Could not get stamp image for psfCandidate: %s with kernel size: %s",
264 psfCandidate, pixKernelSize)
272 sample = psfSet.newSample()
273 sample.setCatindex(catindex)
274 sample.setExtindex(ext)
275 sample.setObjindex(i)
277 imArray = pstamp.getImage().getArray()
278 imArray[np.where(np.bitwise_and(pstamp.getMask().getArray(), badBits))] = \
280 sample.setVig(imArray)
283 sample.setBacknoise2(backnoise2)
287 sample.setFluxrad(sizes[i])
289 for j
in range(psfSet.getNcontext()):
290 sample.setContext(j, float(contextvalp[j][i]))
291 except Exception
as e:
292 self.log.error(
"Exception when processing sample at (%f,%f): %s", xc, yc, e)
295 psfSet.finiSample(sample)
301 with disp.Buffering():
302 disp.dot(
"o", xc, yc, ctype=afwDisplay.CYAN, size=4)
304 if psfSet.getNsample() == 0:
305 raise RuntimeError(
"No good PSF candidates to pass to PSFEx")
308 for i
in range(psfSet.getNcontext()):
309 cmin = contextvalp[i].
min()
310 cmax = contextvalp[i].
max()
311 psfSet.setContextScale(i, cmax - cmin)
312 psfSet.setContextOffset(i, (cmin + cmax)/2.0)
322 field = psfex.Field(
"Unknown")
323 field.addExt(exposure.getWcs(), exposure.getWidth(), exposure.getHeight(), psfSet.getNsample())
331 psfex.makeit(fields, sets)
332 psfs = field.getPsfs()
336 for i
in range(sets[0].getNsample()):
337 index = sets[0].getSample(i).getObjindex()
339 good_indices.append(index)
341 if flagKey
is not None:
342 for i, psfCandidate
in enumerate(psfCandidateList):
343 source = psfCandidate.getSource()
344 if i
in good_indices:
345 source.set(flagKey,
True)
347 xpos = np.array(xpos)
348 ypos = np.array(ypos)
349 numGoodStars = len(good_indices)
350 avgX, avgY = np.mean(xpos), np.mean(ypos)
360 _ = psf.getKernel(psf.getAveragePosition())
362 raise RuntimeError(
"Failed to determine psfex psf: too few good stars.")
368 assert psfCellSet
is not None
371 maUtils.showPsfSpatialCells(exposure, psfCellSet, showChi2=
True,
372 symb=
"o", ctype=afwDisplay.YELLOW, ctypeBad=afwDisplay.RED,
373 size=8, display=disp)
375 disp4 = afwDisplay.Display(frame=4)
376 maUtils.showPsfCandidates(exposure, psfCellSet, psf=psf, display=disp4,
377 normalize=normalizeResiduals,
378 showBadCandidates=showBadCandidates)
379 if displayPsfComponents:
380 disp6 = afwDisplay.Display(frame=6)
381 maUtils.showPsf(psf, display=disp6)
383 disp7 = afwDisplay.Display(frame=7)
384 maUtils.showPsfMosaic(exposure, psf, display=disp7, showFwhm=
True)
385 disp.scale(
'linear', 0, 1)
391 if metadata
is not None:
392 metadata[
"spatialFitChi2"] = np.nan
393 metadata[
"numAvailStars"] = nCand
394 metadata[
"numGoodStars"] = numGoodStars
395 metadata[
"avgX"] = avgX
396 metadata[
"avgY"] = avgY
398 return psf, psfCellSet
401measAlg.psfDeterminerRegistry.register(
"psfex", PsfexPsfDeterminerTask)
A class to contain the data, WCS, and other information needed to describe an image of the sky.
A collection of SpatialCells covering an entire image.
A class used as a handle to a particular field in a table.
Class for storing generic metadata.
Class stored in SpatialCells for spatial Psf fitting.
Represent a PSF as a linear combination of PSFEX (== Karhunen-Loeve) basis functions.
determinePsf(self, exposure, psfCandidateList, metadata=None, flagKey=None)
Reports invalid arguments.
Reports attempts to exceed implementation-defined length limits for some classes.
Statistics makeStatistics(lsst::afw::image::Image< Pixel > const &img, lsst::afw::image::Mask< image::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl=StatisticsControl())
Handle a watered-down front-end to the constructor (no variance)