LSSTApplications  18.1.0
LSSTDataManagementBasePackage
psfexPsfDeterminer.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 # Copyright 2008-2015 AURA/LSST.
4 #
5 # This product includes software developed by the
6 # LSST Project (http://www.lsst.org/).
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the LSST License Statement and
19 # the GNU General Public License along with this program. If not,
20 # see <https://www.lsstcorp.org/LegalNotices/>.
21 #
22 import os
23 import numpy as np
24 
25 import lsst.daf.base as dafBase
26 import lsst.pex.config as pexConfig
27 import lsst.afw.geom as afwGeom
28 import lsst.afw.geom.ellipses as afwEll
29 import lsst.afw.display as afwDisplay
30 import lsst.afw.image as afwImage
31 import lsst.afw.math as afwMath
32 import lsst.meas.algorithms as measAlg
33 import lsst.meas.algorithms.utils as maUtils
34 import lsst.meas.extensions.psfex as psfex
35 
36 
37 class PsfexPsfDeterminerConfig(measAlg.BasePsfDeterminerConfig):
38  __nEigenComponents = pexConfig.Field(
39  doc="number of eigen components for PSF kernel creation",
40  dtype=int,
41  default=4,
42  )
43  spatialOrder = pexConfig.Field(
44  doc="specify spatial order for PSF kernel creation",
45  dtype=int,
46  default=2,
47  check=lambda x: x >= 0,
48  )
49  sizeCellX = pexConfig.Field(
50  doc="size of cell used to determine PSF (pixels, column direction)",
51  dtype=int,
52  default=256,
53  # minValue = 10,
54  check=lambda x: x >= 10,
55  )
56  sizeCellY = pexConfig.Field(
57  doc="size of cell used to determine PSF (pixels, row direction)",
58  dtype=int,
59  default=sizeCellX.default,
60  # minValue = 10,
61  check=lambda x: x >= 10,
62  )
63  __nStarPerCell = pexConfig.Field(
64  doc="number of stars per psf cell for PSF kernel creation",
65  dtype=int,
66  default=3,
67  )
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",
71  dtype=float,
72  default=1,
73  )
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\"",
77  dtype=str,
78  default=["INTRP", "SAT"],
79  )
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",
84  dtype=str,
85  allowed={
86  "PIXEL": "Always use requested samplingSize",
87  "PIXEL_AUTO": "Only use requested samplingSize when FWHM < 3",
88  },
89  default='PIXEL',
90  optional=False,
91  )
92  __borderWidth = pexConfig.Field(
93  doc="Number of pixels to ignore around the edge of PSF candidate postage stamps",
94  dtype=int,
95  default=0,
96  )
97  __nStarPerCellSpatialFit = pexConfig.Field(
98  doc="number of stars per psf Cell for spatial fitting",
99  dtype=int,
100  default=5,
101  )
102  __constantWeight = pexConfig.Field(
103  doc="Should each PSF candidate be given the same weight, independent of magnitude?",
104  dtype=bool,
105  default=True,
106  )
107  __nIterForPsf = pexConfig.Field(
108  doc="number of iterations of PSF candidate star list",
109  dtype=int,
110  default=3,
111  )
112  tolerance = pexConfig.Field(
113  doc="tolerance of spatial fitting",
114  dtype=float,
115  default=1e-2,
116  )
117  lam = pexConfig.Field(
118  doc="floor for variance is lam*data",
119  dtype=float,
120  default=0.05,
121  )
122  reducedChi2ForPsfCandidates = pexConfig.Field(
123  doc="for psf candidate evaluation",
124  dtype=float,
125  default=2.0,
126  )
127  spatialReject = pexConfig.Field(
128  doc="Rejection threshold (stdev) for candidates based on spatial fit",
129  dtype=float,
130  default=3.0,
131  )
132  recentroid = pexConfig.Field(
133  doc="Should PSFEX be permitted to recentroid PSF candidates?",
134  dtype=bool,
135  default=False,
136  )
137 
138  def setDefaults(self):
139  self.kernelSize = 41
140 
141 
142 class PsfexPsfDeterminerTask(measAlg.BasePsfDeterminerTask):
143  ConfigClass = PsfexPsfDeterminerConfig
144 
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.
147 
148  Parameters
149  ----------
150  exposure: `lsst.afw.image.Exposure`
151  Exposure containing the PSF candidates.
152  psfCandidateList: iterable of `lsst.meas.algorithms.PsfCandidate`
153  Sequence of PSF candidates typically obtained by detecting sources and then running them through a
154  star selector.
155  metadata: metadata, optional
156  A home for interesting tidbits of information.
157  flagKey: `lsst.afw.table.Key`, optional
158  Schema key used to mark sources actually used in PSF determination.
159 
160  Returns
161  -------
162  psf: `lsst.meas.extensions.psfex.PsfexPsf`
163  The determined PSF.
164  """
165 
166  import lsstDebug
167  display = lsstDebug.Info(__name__).display
168  displayExposure = display and \
169  lsstDebug.Info(__name__).displayExposure # display the Exposure + spatialCells
170  displayPsfComponents = display and \
171  lsstDebug.Info(__name__).displayPsfComponents # show the basis functions
172  showBadCandidates = display and \
173  lsstDebug.Info(__name__).showBadCandidates # Include bad candidates (meaningless, methinks)
174  displayResiduals = display and \
175  lsstDebug.Info(__name__).displayResiduals # show residuals
176  displayPsfMosaic = display and \
177  lsstDebug.Info(__name__).displayPsfMosaic # show mosaic of reconstructed PSF(x,y)
178  normalizeResiduals = lsstDebug.Info(__name__).normalizeResiduals
179  afwDisplay.setDefaultMaskTransparency(75)
180  # Normalise residuals by object amplitude
181 
182  mi = exposure.getMaskedImage()
183 
184  nCand = len(psfCandidateList)
185  if nCand == 0:
186  raise RuntimeError("No PSF candidates supplied.")
187  #
188  # How big should our PSF models be?
189  #
190  if display: # only needed for debug plots
191  # construct and populate a spatial cell set
192  bbox = mi.getBBox(afwImage.PARENT)
193  psfCellSet = afwMath.SpatialCellSet(bbox, self.config.sizeCellX, self.config.sizeCellY)
194  else:
195  psfCellSet = None
196 
197  sizes = np.empty(nCand)
198  for i, psfCandidate in enumerate(psfCandidateList):
199  try:
200  if psfCellSet:
201  psfCellSet.insertCandidate(psfCandidate)
202  except Exception as e:
203  self.log.debug("Skipping PSF candidate %d of %d: %s", i, len(psfCandidateList), e)
204  continue
205 
206  source = psfCandidate.getSource()
207  quad = afwEll.Quadrupole(source.getIxx(), source.getIyy(), source.getIxy())
208  rmsSize = quad.getTraceRadius()
209  sizes[i] = rmsSize
210 
211  if self.config.kernelSize >= 15:
212  self.log.warn("NOT scaling kernelSize by stellar quadrupole moment, but using absolute value")
213  actualKernelSize = int(self.config.kernelSize)
214  else:
215  actualKernelSize = 2 * int(self.config.kernelSize * np.sqrt(np.median(sizes)) + 0.5) + 1
216  if actualKernelSize < self.config.kernelSizeMin:
217  actualKernelSize = self.config.kernelSizeMin
218  if actualKernelSize > self.config.kernelSizeMax:
219  actualKernelSize = self.config.kernelSizeMax
220  if display:
221  rms = np.median(sizes)
222  print("Median PSF RMS size=%.2f pixels (\"FWHM\"=%.2f)" % (rms, 2*np.sqrt(2*np.log(2))*rms))
223 
224  # If we manually set the resolution then we need the size in pixel units
225  pixKernelSize = actualKernelSize
226  if self.config.samplingSize > 0:
227  pixKernelSize = int(actualKernelSize*self.config.samplingSize)
228  if pixKernelSize % 2 == 0:
229  pixKernelSize += 1
230  self.log.trace("Psfex Kernel size=%.2f, Image Kernel Size=%.2f", actualKernelSize, pixKernelSize)
231  psfCandidateList[0].setHeight(pixKernelSize)
232  psfCandidateList[0].setWidth(pixKernelSize)
233 
234  # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- BEGIN PSFEX
235  #
236  # Insert the good candidates into the set
237  #
238  defaultsFile = os.path.join(os.environ["MEAS_EXTENSIONS_PSFEX_DIR"], "config", "default-lsst.psfex")
239  args_md = dafBase.PropertySet()
240  args_md.set("BASIS_TYPE", str(self.config.psfexBasis))
241  args_md.set("PSFVAR_DEGREES", str(self.config.spatialOrder))
242  args_md.set("PSF_SIZE", str(actualKernelSize))
243  args_md.set("PSF_SAMPLING", str(self.config.samplingSize))
244  prefs = psfex.Prefs(defaultsFile, args_md)
245  prefs.setCommandLine([])
246  prefs.addCatalog("psfexPsfDeterminer")
247 
248  prefs.use()
249  principalComponentExclusionFlag = bool(bool(psfex.Context.REMOVEHIDDEN)
250  if False else psfex.Context.KEEPHIDDEN)
251  context = psfex.Context(prefs.getContextName(), prefs.getContextGroup(),
252  prefs.getGroupDeg(), principalComponentExclusionFlag)
253  set = psfex.Set(context)
254  set.setVigSize(pixKernelSize, pixKernelSize)
255  set.setFwhm(2*np.sqrt(2*np.log(2))*np.median(sizes))
256  set.setRecentroid(self.config.recentroid)
257 
258  catindex, ext = 0, 0
259  backnoise2 = afwMath.makeStatistics(mi.getImage(), afwMath.VARIANCECLIP).getValue()
260  ccd = exposure.getDetector()
261  if ccd:
262  gain = np.mean(np.array([a.getGain() for a in ccd]))
263  else:
264  gain = 1.0
265  self.log.warn("Setting gain to %g" % (gain,))
266 
267  contextvalp = []
268  for i, key in enumerate(context.getName()):
269  if context.getPcflag(i):
270  contextvalp.append(pcval[pc])
271  pc += 1
272  elif key[0] == ':':
273  try:
274  contextvalp.append(exposure.getMetadata().getScalar(key[1:]))
275  except KeyError:
276  raise RuntimeError("*Error*: %s parameter not found in the header of %s" %
277  (key[1:], prefs.getContextName()))
278  else:
279  try:
280  contextvalp.append(np.array([psfCandidateList[_].getSource().get(key)
281  for _ in range(nCand)]))
282  except KeyError:
283  raise RuntimeError("*Error*: %s parameter not found" % (key,))
284  set.setContextname(i, key)
285 
286  if display:
287  frame = 0
288  if displayExposure:
289  disp = afwDisplay.Display(frame=frame)
290  disp.mtv(exposure, title="psf determination")
291 
292  badBits = mi.getMask().getPlaneBitMask(self.config.badMaskBits)
293  fluxName = prefs.getPhotfluxRkey()
294  fluxFlagName = "base_" + fluxName + "_flag"
295 
296  xpos, ypos = [], []
297  for i, psfCandidate in enumerate(psfCandidateList):
298  source = psfCandidate.getSource()
299  xc, yc = source.getX(), source.getY()
300  try:
301  int(xc), int(yc)
302  except ValueError:
303  continue
304 
305  try:
306  pstamp = psfCandidate.getMaskedImage().clone()
307  except Exception:
308  continue
309 
310  if fluxFlagName in source.schema and source.get(fluxFlagName):
311  continue
312 
313  flux = source.get(fluxName)
314  if flux < 0 or np.isnan(flux):
315  continue
316 
317  # From this point, we're configuring the "sample" (PSFEx's version of a PSF candidate).
318  # Having created the sample, we must proceed to configure it, and then fini (finalize),
319  # or it will be malformed.
320  try:
321  sample = set.newSample()
322  sample.setCatindex(catindex)
323  sample.setExtindex(ext)
324  sample.setObjindex(i)
325 
326  imArray = pstamp.getImage().getArray()
327  imArray[np.where(np.bitwise_and(pstamp.getMask().getArray(), badBits))] = \
328  -2*psfex.BIG
329  sample.setVig(imArray)
330 
331  sample.setNorm(flux)
332  sample.setBacknoise2(backnoise2)
333  sample.setGain(gain)
334  sample.setX(xc)
335  sample.setY(yc)
336  sample.setFluxrad(sizes[i])
337 
338  for j in range(set.getNcontext()):
339  sample.setContext(j, float(contextvalp[j][i]))
340  except Exception as e:
341  self.log.debug("Exception when processing sample at (%f,%f): %s", xc, yc, e)
342  continue
343  else:
344  set.finiSample(sample)
345 
346  xpos.append(xc) # for QA
347  ypos.append(yc)
348 
349  if displayExposure:
350  with disp.Buffering():
351  disp.dot("o", xc, yc, ctype=afwDisplay.CYAN, size=4)
352 
353  if set.getNsample() == 0:
354  raise RuntimeError("No good PSF candidates to pass to PSFEx")
355 
356  # ---- Update min and max and then the scaling
357  for i in range(set.getNcontext()):
358  cmin = contextvalp[i].min()
359  cmax = contextvalp[i].max()
360  set.setContextScale(i, cmax - cmin)
361  set.setContextOffset(i, (cmin + cmax)/2.0)
362 
363  # Don't waste memory!
364  set.trimMemory()
365 
366  # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- END PSFEX
367  #
368  # Do a PSFEX decomposition of those PSF candidates
369  #
370  fields = []
371  field = psfex.Field("Unknown")
372  field.addExt(exposure.getWcs(), exposure.getWidth(), exposure.getHeight(), set.getNsample())
373  field.finalize()
374 
375  fields.append(field)
376 
377  sets = []
378  sets.append(set)
379 
380  psfex.makeit(fields, sets)
381  psfs = field.getPsfs()
382 
383  # Flag which objects were actually used in psfex by
384  good_indices = []
385  for i in range(sets[0].getNsample()):
386  index = sets[0].getSample(i).getObjindex()
387  if index > -1:
388  good_indices.append(index)
389 
390  if flagKey is not None:
391  for i, psfCandidate in enumerate(psfCandidateList):
392  source = psfCandidate.getSource()
393  if i in good_indices:
394  source.set(flagKey, True)
395 
396  xpos = np.array(xpos)
397  ypos = np.array(ypos)
398  numGoodStars = len(good_indices)
399  avgX, avgY = np.mean(xpos), np.mean(ypos)
400 
401  psf = psfex.PsfexPsf(psfs[0], afwGeom.Point2D(avgX, avgY))
402 
403  if False and (displayResiduals or displayPsfMosaic):
404  ext = 0
405  frame = 1
406  diagnostics = True
407  catDir = "."
408  title = "psfexPsfDeterminer"
409  psfex.psfex.showPsf(psfs, set, ext,
410  [(exposure.getWcs(), exposure.getWidth(), exposure.getHeight())],
411  nspot=3, trim=5, frame=frame, diagnostics=diagnostics, outDir=catDir,
412  title=title)
413  #
414  # Display code for debugging
415  #
416  if display:
417  assert psfCellSet is not None
418 
419  if displayExposure:
420  maUtils.showPsfSpatialCells(exposure, psfCellSet, showChi2=True,
421  symb="o", ctype=afwDisplay.YELLOW, ctypeBad=afwDisplay.RED,
422  size=8, display=disp)
423  if displayResiduals:
424  disp4 = afwDisplay.Display(frame=4)
425  maUtils.showPsfCandidates(exposure, psfCellSet, psf=psf, display=disp4,
426  normalize=normalizeResiduals,
427  showBadCandidates=showBadCandidates)
428  if displayPsfComponents:
429  disp6 = afwDisplay.Display(frame=6)
430  maUtils.showPsf(psf, display=disp6)
431  if displayPsfMosaic:
432  disp7 = afwDisplay.Display(frame=7)
433  maUtils.showPsfMosaic(exposure, psf, display=disp7, showFwhm=True)
434  disp.scale('linear', 0, 1)
435  #
436  # Generate some QA information
437  #
438  # Count PSF stars
439  #
440  if metadata is not None:
441  metadata.set("spatialFitChi2", np.nan)
442  metadata.set("numAvailStars", nCand)
443  metadata.set("numGoodStars", numGoodStars)
444  metadata.set("avgX", avgX)
445  metadata.set("avgY", avgY)
446 
447  return psf, psfCellSet
448 
449 measAlg.psfDeterminerRegistry.register("psfex", PsfexPsfDeterminerTask)
def determinePsf(self, exposure, psfCandidateList, metadata=None, flagKey=None)
Fit spatial kernel using approximate fluxes for candidates, and solving a linear system of equations...
int min
Statistics makeStatistics(lsst::afw::math::MaskedVector< EntryT > const &mv, std::vector< WeightPixel > const &vweights, int const flags, StatisticsControl const &sctrl=StatisticsControl())
The makeStatistics() overload to handle lsst::afw::math::MaskedVector<>
Definition: Statistics.h:520
A collection of SpatialCells covering an entire image.
Definition: SpatialCell.h:387
Represent a PSF as a linear combination of PSFEX (== Karhunen-Loeve) basis functions.
Definition: PsfexPsf.h:39
int max
Class for storing generic metadata.
Definition: PropertySet.h:68
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects...