39 spatialOrder = pexConfig.Field[int](
40 doc=
"specify spatial order for PSF kernel creation",
42 check=
lambda x: x >= 1,
44 sizeCellX = pexConfig.Field[int](
45 doc=
"size of cell used to determine PSF (pixels, column direction)",
48 check=
lambda x: x >= 10,
50 sizeCellY = pexConfig.Field[int](
51 doc=
"size of cell used to determine PSF (pixels, row direction)",
52 default=sizeCellX.default,
54 check=
lambda x: x >= 10,
56 samplingSize = pexConfig.Field[float](
57 doc=
"Resolution of the internal PSF model relative to the pixel size; "
58 "e.g. 0.5 is equal to 2x oversampling",
61 badMaskBits = pexConfig.ListField[str](
62 doc=
"List of mask bits which cause a source to be rejected as bad "
63 "N.b. INTRP is used specially in PsfCandidateSet; it means \"Contaminated by neighbour\"",
64 default=[
"INTRP",
"SAT"],
66 psfexBasis = pexConfig.ChoiceField[str](
67 doc=
"BASIS value given to psfex. PIXEL_AUTO will use the requested samplingSize only if "
68 "the FWHM < 3 pixels. Otherwise, it will use samplingSize=1. PIXEL will always use the "
69 "requested samplingSize",
71 "PIXEL":
"Always use requested samplingSize",
72 "PIXEL_AUTO":
"Only use requested samplingSize when FWHM < 3",
77 tolerance = pexConfig.Field[float](
78 doc=
"tolerance of spatial fitting",
81 lam = pexConfig.Field[float](
82 doc=
"floor for variance is lam*data",
85 reducedChi2ForPsfCandidates = pexConfig.Field[float](
86 doc=
"for psf candidate evaluation",
89 spatialReject = pexConfig.Field[float](
90 doc=
"Rejection threshold (stdev) for candidates based on spatial fit",
93 recentroid = pexConfig.Field[bool](
94 doc=
"Should PSFEX be permitted to recentroid PSF candidates?",
97 kernelSize = pexConfig.Field[int](
98 doc=(
"Size of the postage stamp around each star that is extracted for fitting."
99 "Note: this reflects the oversampling setting of the psf, set by `samplingSize`;"
100 "e.g. `samplingSize=0.5` would require this value to be 2x what you expect."),
103 deprecated=
"'kernelSize' is deprecated and will be removed in v25. Use `stampSize` instead.",
112 ConfigClass = PsfexPsfDeterminerConfig
114 def determinePsf(self, exposure, psfCandidateList, metadata=None, flagKey=None):
115 """Determine a PSFEX PSF model for an exposure given a list of PSF
121 Exposure containing the PSF candidates.
123 Sequence of PSF candidates typically obtained by detecting sources
124 and then running them through a star selector.
125 metadata: metadata, optional
126 A home
for interesting tidbits of information.
128 Schema key used to mark sources actually used
in PSF determination.
138 displayExposure = display
and \
140 displayPsfComponents = display
and \
142 showBadCandidates = display
and \
144 displayResiduals = display
and \
146 displayPsfMosaic = display
and \
149 afwDisplay.setDefaultMaskTransparency(75)
152 mi = exposure.getMaskedImage()
154 nCand = len(psfCandidateList)
156 raise RuntimeError(
"No PSF candidates supplied.")
162 bbox = mi.getBBox(afwImage.PARENT)
167 sizes = np.empty(nCand)
168 for i, psfCandidate
in enumerate(psfCandidateList):
171 psfCellSet.insertCandidate(psfCandidate)
172 except Exception
as e:
173 self.log.error(
"Skipping PSF candidate %d of %d: %s", i, len(psfCandidateList), e)
176 source = psfCandidate.getSource()
177 quad = afwEll.Quadrupole(source.getIxx(), source.getIyy(), source.getIxy())
178 rmsSize = quad.getTraceRadius()
182 if self.config.stampSize:
183 pixKernelSize = self.config.stampSize
184 actualKernelSize = int(2*np.floor(0.5*pixKernelSize/self.config.samplingSize) + 1)
185 elif self.config.kernelSize >= 15:
186 self.log.warning(
"NOT scaling kernelSize by stellar quadrupole moment, but using absolute value")
187 actualKernelSize = self.config.kernelSize
188 pixKernelSize = int(actualKernelSize*self.config.samplingSize)
189 if pixKernelSize % 2 == 0:
192 actualKernelSize = 2 * int(self.config.kernelSize * np.sqrt(np.median(sizes)) + 0.5) + 1
194 if actualKernelSize < self.config.kernelSizeMin:
195 actualKernelSize = self.config.kernelSizeMin
196 if actualKernelSize > self.config.kernelSizeMax:
197 actualKernelSize = self.config.kernelSizeMax
199 pixKernelSize = int(actualKernelSize*self.config.samplingSize)
200 if pixKernelSize % 2 == 0:
204 rms = np.median(sizes)
205 self.log.debug(
"Median PSF RMS size=%.2f pixels (\"FWHM\"=%.2f)",
206 rms, 2*np.sqrt(2*np.log(2))*rms)
208 self.log.trace(
"Psfex Kernel size=%.2f, Image Kernel Size=%.2f", actualKernelSize, pixKernelSize)
214 defaultsFile = os.path.join(os.environ[
"MEAS_EXTENSIONS_PSFEX_DIR"],
"config",
"default-lsst.psfex")
216 args_md.set(
"BASIS_TYPE",
str(self.config.psfexBasis))
217 args_md.set(
"PSFVAR_DEGREES",
str(self.config.spatialOrder))
218 args_md.set(
"PSF_SIZE",
str(actualKernelSize))
219 args_md.set(
"PSF_SAMPLING",
str(self.config.samplingSize))
220 prefs = psfex.Prefs(defaultsFile, args_md)
221 prefs.setCommandLine([])
222 prefs.addCatalog(
"psfexPsfDeterminer")
225 principalComponentExclusionFlag = bool(bool(psfex.Context.REMOVEHIDDEN)
226 if False else psfex.Context.KEEPHIDDEN)
227 context = psfex.Context(prefs.getContextName(), prefs.getContextGroup(),
228 prefs.getGroupDeg(), principalComponentExclusionFlag)
229 psfSet = psfex.Set(context)
230 psfSet.setVigSize(pixKernelSize, pixKernelSize)
231 psfSet.setFwhm(2*np.sqrt(2*np.log(2))*np.median(sizes))
232 psfSet.setRecentroid(self.config.recentroid)
236 ccd = exposure.getDetector()
238 gain = np.mean(np.array([a.getGain()
for a
in ccd]))
241 self.log.warning(
"Setting gain to %g", gain)
244 for i, key
in enumerate(context.getName()):
247 contextvalp.append(exposure.getMetadata().getScalar(key[1:]))
248 except KeyError
as e:
249 raise RuntimeError(
"%s parameter not found in the header of %s" %
250 (key[1:], prefs.getContextName()))
from e
253 contextvalp.append(np.array([psfCandidateList[_].getSource().get(key)
254 for _
in range(nCand)]))
255 except KeyError
as e:
256 raise RuntimeError(
"%s parameter not found" % (key,))
from e
257 psfSet.setContextname(i, key)
262 disp = afwDisplay.Display(frame=frame)
263 disp.mtv(exposure, title=
"psf determination")
265 badBits = mi.getMask().getPlaneBitMask(self.config.badMaskBits)
266 fluxName = prefs.getPhotfluxRkey()
267 fluxFlagName =
"base_" + fluxName +
"_flag"
270 for i, psfCandidate
in enumerate(psfCandidateList):
271 source = psfCandidate.getSource()
274 xc, yc = source.getX(), source.getY()
275 if not np.isfinite(xc)
or not np.isfinite(yc):
278 if fluxFlagName
in source.schema
and source.get(fluxFlagName):
281 flux = source.get(fluxName)
282 if flux < 0
or not np.isfinite(flux):
286 pstamp = psfCandidate.getMaskedImage(pixKernelSize, pixKernelSize).clone()
297 sample = psfSet.newSample()
298 sample.setCatindex(catindex)
299 sample.setExtindex(ext)
300 sample.setObjindex(i)
302 imArray = pstamp.getImage().getArray()
303 imArray[np.where(np.bitwise_and(pstamp.getMask().getArray(), badBits))] = \
305 sample.setVig(imArray)
308 sample.setBacknoise2(backnoise2)
312 sample.setFluxrad(sizes[i])
314 for j
in range(psfSet.getNcontext()):
315 sample.setContext(j, float(contextvalp[j][i]))
316 except Exception
as e:
317 self.log.error(
"Exception when processing sample at (%f,%f): %s", xc, yc, e)
320 psfSet.finiSample(sample)
326 with disp.Buffering():
327 disp.dot(
"o", xc, yc, ctype=afwDisplay.CYAN, size=4)
329 if psfSet.getNsample() == 0:
330 raise RuntimeError(
"No good PSF candidates to pass to PSFEx")
333 for i
in range(psfSet.getNcontext()):
334 cmin = contextvalp[i].
min()
335 cmax = contextvalp[i].
max()
336 psfSet.setContextScale(i, cmax - cmin)
337 psfSet.setContextOffset(i, (cmin + cmax)/2.0)
347 field = psfex.Field(
"Unknown")
348 field.addExt(exposure.getWcs(), exposure.getWidth(), exposure.getHeight(), psfSet.getNsample())
356 psfex.makeit(fields, sets)
357 psfs = field.getPsfs()
361 for i
in range(sets[0].getNsample()):
362 index = sets[0].getSample(i).getObjindex()
364 good_indices.append(index)
366 if flagKey
is not None:
367 for i, psfCandidate
in enumerate(psfCandidateList):
368 source = psfCandidate.getSource()
369 if i
in good_indices:
370 source.set(flagKey,
True)
372 xpos = np.array(xpos)
373 ypos = np.array(ypos)
374 numGoodStars = len(good_indices)
375 avgX, avgY = np.mean(xpos), np.mean(ypos)
383 assert psfCellSet
is not None
386 maUtils.showPsfSpatialCells(exposure, psfCellSet, showChi2=
True,
387 symb=
"o", ctype=afwDisplay.YELLOW, ctypeBad=afwDisplay.RED,
388 size=8, display=disp)
390 disp4 = afwDisplay.Display(frame=4)
391 maUtils.showPsfCandidates(exposure, psfCellSet, psf=psf, display=disp4,
392 normalize=normalizeResiduals,
393 showBadCandidates=showBadCandidates)
394 if displayPsfComponents:
395 disp6 = afwDisplay.Display(frame=6)
396 maUtils.showPsf(psf, display=disp6)
398 disp7 = afwDisplay.Display(frame=7)
399 maUtils.showPsfMosaic(exposure, psf, display=disp7, showFwhm=
True)
400 disp.scale(
'linear', 0, 1)
406 if metadata
is not None:
407 metadata[
"spatialFitChi2"] = np.nan
408 metadata[
"numAvailStars"] = nCand
409 metadata[
"numGoodStars"] = numGoodStars
410 metadata[
"avgX"] = avgX
411 metadata[
"avgY"] = avgY
413 return psf, psfCellSet
416measAlg.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.
def determinePsf(self, exposure, psfCandidateList, metadata=None, flagKey=None)
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)