LSSTApplications  17.0+124,17.0+14,17.0+73,18.0.0+37,18.0.0+80,18.0.0-4-g68ffd23+4,18.1.0-1-g0001055+12,18.1.0-1-g03d53ef+5,18.1.0-1-g1349e88+55,18.1.0-1-g2505f39+44,18.1.0-1-g5315e5e+4,18.1.0-1-g5e4b7ea+14,18.1.0-1-g7e8fceb+4,18.1.0-1-g85f8cd4+48,18.1.0-1-g8ff0b9f+4,18.1.0-1-ga2c679d+1,18.1.0-1-gd55f500+35,18.1.0-10-gb58edde+2,18.1.0-11-g0997b02+4,18.1.0-13-gfe4edf0b+12,18.1.0-14-g259bd21+21,18.1.0-19-gdb69f3f+2,18.1.0-2-g5f9922c+24,18.1.0-2-gd3b74e5+11,18.1.0-2-gfbf3545+32,18.1.0-26-g728bddb4+5,18.1.0-27-g6ff7ca9+2,18.1.0-3-g52aa583+25,18.1.0-3-g8ea57af+9,18.1.0-3-gb69f684+42,18.1.0-3-gfcaddf3+6,18.1.0-32-gd8786685a,18.1.0-4-gf3f9b77+6,18.1.0-5-g1dd662b+2,18.1.0-5-g6dbcb01+41,18.1.0-6-gae77429+3,18.1.0-7-g9d75d83+9,18.1.0-7-gae09a6d+30,18.1.0-9-gc381ef5+4,w.2019.45
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.geom as geom
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
147  candidates.
148 
149  Parameters
150  ----------
151  exposure: `lsst.afw.image.Exposure`
152  Exposure containing the PSF candidates.
153  psfCandidateList: iterable of `lsst.meas.algorithms.PsfCandidate`
154  Sequence of PSF candidates typically obtained by detecting sources
155  and then running them through a star selector.
156  metadata: metadata, optional
157  A home for interesting tidbits of information.
158  flagKey: `lsst.afw.table.Key`, optional
159  Schema key used to mark sources actually used in PSF determination.
160 
161  Returns
162  -------
163  psf: `lsst.meas.extensions.psfex.PsfexPsf`
164  The determined PSF.
165  """
166 
167  import lsstDebug
168  display = lsstDebug.Info(__name__).display
169  displayExposure = display and \
170  lsstDebug.Info(__name__).displayExposure # display the Exposure + spatialCells
171  displayPsfComponents = display and \
172  lsstDebug.Info(__name__).displayPsfComponents # show the basis functions
173  showBadCandidates = display and \
174  lsstDebug.Info(__name__).showBadCandidates # Include bad candidates (meaningless, methinks)
175  displayResiduals = display and \
176  lsstDebug.Info(__name__).displayResiduals # show residuals
177  displayPsfMosaic = display and \
178  lsstDebug.Info(__name__).displayPsfMosaic # show mosaic of reconstructed PSF(x,y)
179  normalizeResiduals = lsstDebug.Info(__name__).normalizeResiduals
180  afwDisplay.setDefaultMaskTransparency(75)
181  # Normalise residuals by object amplitude
182 
183  mi = exposure.getMaskedImage()
184 
185  nCand = len(psfCandidateList)
186  if nCand == 0:
187  raise RuntimeError("No PSF candidates supplied.")
188  #
189  # How big should our PSF models be?
190  #
191  if display: # only needed for debug plots
192  # construct and populate a spatial cell set
193  bbox = mi.getBBox(afwImage.PARENT)
194  psfCellSet = afwMath.SpatialCellSet(bbox, self.config.sizeCellX, self.config.sizeCellY)
195  else:
196  psfCellSet = None
197 
198  sizes = np.empty(nCand)
199  for i, psfCandidate in enumerate(psfCandidateList):
200  try:
201  if psfCellSet:
202  psfCellSet.insertCandidate(psfCandidate)
203  except Exception as e:
204  self.log.debug("Skipping PSF candidate %d of %d: %s", i, len(psfCandidateList), e)
205  continue
206 
207  source = psfCandidate.getSource()
208  quad = afwEll.Quadrupole(source.getIxx(), source.getIyy(), source.getIxy())
209  rmsSize = quad.getTraceRadius()
210  sizes[i] = rmsSize
211 
212  if self.config.kernelSize >= 15:
213  self.log.warn("NOT scaling kernelSize by stellar quadrupole moment, but using absolute value")
214  actualKernelSize = int(self.config.kernelSize)
215  else:
216  actualKernelSize = 2 * int(self.config.kernelSize * np.sqrt(np.median(sizes)) + 0.5) + 1
217  if actualKernelSize < self.config.kernelSizeMin:
218  actualKernelSize = self.config.kernelSizeMin
219  if actualKernelSize > self.config.kernelSizeMax:
220  actualKernelSize = self.config.kernelSizeMax
221  if display:
222  rms = np.median(sizes)
223  print("Median PSF RMS size=%.2f pixels (\"FWHM\"=%.2f)" % (rms, 2*np.sqrt(2*np.log(2))*rms))
224 
225  # If we manually set the resolution then we need the size in pixel
226  # units
227  pixKernelSize = actualKernelSize
228  if self.config.samplingSize > 0:
229  pixKernelSize = int(actualKernelSize*self.config.samplingSize)
230  if pixKernelSize % 2 == 0:
231  pixKernelSize += 1
232  self.log.trace("Psfex Kernel size=%.2f, Image Kernel Size=%.2f", actualKernelSize, pixKernelSize)
233  psfCandidateList[0].setHeight(pixKernelSize)
234  psfCandidateList[0].setWidth(pixKernelSize)
235 
236  # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- BEGIN PSFEX
237  #
238  # Insert the good candidates into the set
239  #
240  defaultsFile = os.path.join(os.environ["MEAS_EXTENSIONS_PSFEX_DIR"], "config", "default-lsst.psfex")
241  args_md = dafBase.PropertySet()
242  args_md.set("BASIS_TYPE", str(self.config.psfexBasis))
243  args_md.set("PSFVAR_DEGREES", str(self.config.spatialOrder))
244  args_md.set("PSF_SIZE", str(actualKernelSize))
245  args_md.set("PSF_SAMPLING", str(self.config.samplingSize))
246  prefs = psfex.Prefs(defaultsFile, args_md)
247  prefs.setCommandLine([])
248  prefs.addCatalog("psfexPsfDeterminer")
249 
250  prefs.use()
251  principalComponentExclusionFlag = bool(bool(psfex.Context.REMOVEHIDDEN)
252  if False else psfex.Context.KEEPHIDDEN)
253  context = psfex.Context(prefs.getContextName(), prefs.getContextGroup(),
254  prefs.getGroupDeg(), principalComponentExclusionFlag)
255  set = psfex.Set(context)
256  set.setVigSize(pixKernelSize, pixKernelSize)
257  set.setFwhm(2*np.sqrt(2*np.log(2))*np.median(sizes))
258  set.setRecentroid(self.config.recentroid)
259 
260  catindex, ext = 0, 0
261  backnoise2 = afwMath.makeStatistics(mi.getImage(), afwMath.VARIANCECLIP).getValue()
262  ccd = exposure.getDetector()
263  if ccd:
264  gain = np.mean(np.array([a.getGain() for a in ccd]))
265  else:
266  gain = 1.0
267  self.log.warn("Setting gain to %g" % (gain,))
268 
269  pc = 0
270  contextvalp = []
271  for i, key in enumerate(context.getName()):
272  if context.getPcflag(i):
273  raise RuntimeError("Principal Components can not be accessed")
274  contextvalp.append(pcval[pc]) # noqa: F821
275  pc += 1
276  elif key[0] == ':':
277  try:
278  contextvalp.append(exposure.getMetadata().getScalar(key[1:]))
279  except KeyError:
280  raise RuntimeError("*Error*: %s parameter not found in the header of %s" %
281  (key[1:], prefs.getContextName()))
282  else:
283  try:
284  contextvalp.append(np.array([psfCandidateList[_].getSource().get(key)
285  for _ in range(nCand)]))
286  except KeyError:
287  raise RuntimeError("*Error*: %s parameter not found" % (key,))
288  set.setContextname(i, key)
289 
290  if display:
291  frame = 0
292  if displayExposure:
293  disp = afwDisplay.Display(frame=frame)
294  disp.mtv(exposure, title="psf determination")
295 
296  badBits = mi.getMask().getPlaneBitMask(self.config.badMaskBits)
297  fluxName = prefs.getPhotfluxRkey()
298  fluxFlagName = "base_" + fluxName + "_flag"
299 
300  xpos, ypos = [], []
301  for i, psfCandidate in enumerate(psfCandidateList):
302  source = psfCandidate.getSource()
303  xc, yc = source.getX(), source.getY()
304  try:
305  int(xc), int(yc)
306  except ValueError:
307  continue
308 
309  try:
310  pstamp = psfCandidate.getMaskedImage().clone()
311  except Exception:
312  continue
313 
314  if fluxFlagName in source.schema and source.get(fluxFlagName):
315  continue
316 
317  flux = source.get(fluxName)
318  if flux < 0 or np.isnan(flux):
319  continue
320 
321  # From this point, we're configuring the "sample" (PSFEx's version
322  # of a PSF candidate).
323  # Having created the sample, we must proceed to configure it, and
324  # then fini (finalize), or it will be malformed.
325  try:
326  sample = set.newSample()
327  sample.setCatindex(catindex)
328  sample.setExtindex(ext)
329  sample.setObjindex(i)
330 
331  imArray = pstamp.getImage().getArray()
332  imArray[np.where(np.bitwise_and(pstamp.getMask().getArray(), badBits))] = \
333  -2*psfex.BIG
334  sample.setVig(imArray)
335 
336  sample.setNorm(flux)
337  sample.setBacknoise2(backnoise2)
338  sample.setGain(gain)
339  sample.setX(xc)
340  sample.setY(yc)
341  sample.setFluxrad(sizes[i])
342 
343  for j in range(set.getNcontext()):
344  sample.setContext(j, float(contextvalp[j][i]))
345  except Exception as e:
346  self.log.debug("Exception when processing sample at (%f,%f): %s", xc, yc, e)
347  continue
348  else:
349  set.finiSample(sample)
350 
351  xpos.append(xc) # for QA
352  ypos.append(yc)
353 
354  if displayExposure:
355  with disp.Buffering():
356  disp.dot("o", xc, yc, ctype=afwDisplay.CYAN, size=4)
357 
358  if set.getNsample() == 0:
359  raise RuntimeError("No good PSF candidates to pass to PSFEx")
360 
361  # ---- Update min and max and then the scaling
362  for i in range(set.getNcontext()):
363  cmin = contextvalp[i].min()
364  cmax = contextvalp[i].max()
365  set.setContextScale(i, cmax - cmin)
366  set.setContextOffset(i, (cmin + cmax)/2.0)
367 
368  # Don't waste memory!
369  set.trimMemory()
370 
371  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- END PSFEX
372  #
373  # Do a PSFEX decomposition of those PSF candidates
374  #
375  fields = []
376  field = psfex.Field("Unknown")
377  field.addExt(exposure.getWcs(), exposure.getWidth(), exposure.getHeight(), set.getNsample())
378  field.finalize()
379 
380  fields.append(field)
381 
382  sets = []
383  sets.append(set)
384 
385  psfex.makeit(fields, sets)
386  psfs = field.getPsfs()
387 
388  # Flag which objects were actually used in psfex by
389  good_indices = []
390  for i in range(sets[0].getNsample()):
391  index = sets[0].getSample(i).getObjindex()
392  if index > -1:
393  good_indices.append(index)
394 
395  if flagKey is not None:
396  for i, psfCandidate in enumerate(psfCandidateList):
397  source = psfCandidate.getSource()
398  if i in good_indices:
399  source.set(flagKey, True)
400 
401  xpos = np.array(xpos)
402  ypos = np.array(ypos)
403  numGoodStars = len(good_indices)
404  avgX, avgY = np.mean(xpos), np.mean(ypos)
405 
406  psf = psfex.PsfexPsf(psfs[0], geom.Point2D(avgX, avgY))
407 
408  if False and (displayResiduals or displayPsfMosaic):
409  ext = 0
410  frame = 1
411  diagnostics = True
412  catDir = "."
413  title = "psfexPsfDeterminer"
414  psfex.psfex.showPsf(psfs, set, ext,
415  [(exposure.getWcs(), exposure.getWidth(), exposure.getHeight())],
416  nspot=3, trim=5, frame=frame, diagnostics=diagnostics, outDir=catDir,
417  title=title)
418  #
419  # Display code for debugging
420  #
421  if display:
422  assert psfCellSet is not None
423 
424  if displayExposure:
425  maUtils.showPsfSpatialCells(exposure, psfCellSet, showChi2=True,
426  symb="o", ctype=afwDisplay.YELLOW, ctypeBad=afwDisplay.RED,
427  size=8, display=disp)
428  if displayResiduals:
429  disp4 = afwDisplay.Display(frame=4)
430  maUtils.showPsfCandidates(exposure, psfCellSet, psf=psf, display=disp4,
431  normalize=normalizeResiduals,
432  showBadCandidates=showBadCandidates)
433  if displayPsfComponents:
434  disp6 = afwDisplay.Display(frame=6)
435  maUtils.showPsf(psf, display=disp6)
436  if displayPsfMosaic:
437  disp7 = afwDisplay.Display(frame=7)
438  maUtils.showPsfMosaic(exposure, psf, display=disp7, showFwhm=True)
439  disp.scale('linear', 0, 1)
440  #
441  # Generate some QA information
442  #
443  # Count PSF stars
444  #
445  if metadata is not None:
446  metadata.set("spatialFitChi2", np.nan)
447  metadata.set("numAvailStars", nCand)
448  metadata.set("numGoodStars", numGoodStars)
449  metadata.set("avgX", avgX)
450  metadata.set("avgY", avgY)
451 
452  return psf, psfCellSet
453 
454 
455 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:67
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects...