LSSTApplications  16.0-10-g0ee56ad+5,16.0-11-ga33d1f2+5,16.0-12-g3ef5c14+3,16.0-12-g71e5ef5+18,16.0-12-gbdf3636+3,16.0-13-g118c103+3,16.0-13-g8f68b0a+3,16.0-15-gbf5c1cb+4,16.0-16-gfd17674+3,16.0-17-g7c01f5c+3,16.0-18-g0a50484+1,16.0-20-ga20f992+8,16.0-21-g0e05fd4+6,16.0-21-g15e2d33+4,16.0-22-g62d8060+4,16.0-22-g847a80f+4,16.0-25-gf00d9b8+1,16.0-28-g3990c221+4,16.0-3-gf928089+3,16.0-32-g88a4f23+5,16.0-34-gd7987ad+3,16.0-37-gc7333cb+2,16.0-4-g10fc685+2,16.0-4-g18f3627+26,16.0-4-g5f3a788+26,16.0-5-gaf5c3d7+4,16.0-5-gcc1f4bb+1,16.0-6-g3b92700+4,16.0-6-g4412fcd+3,16.0-6-g7235603+4,16.0-69-g2562ce1b+2,16.0-8-g14ebd58+4,16.0-8-g2df868b+1,16.0-8-g4cec79c+6,16.0-8-gadf6c7a+1,16.0-8-gfc7ad86,16.0-82-g59ec2a54a+1,16.0-9-g5400cdc+2,16.0-9-ge6233d7+5,master-g2880f2d8cf+3,v17.0.rc1
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.ds9 as ds9
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  # Normalise residuals by object amplitude
180 
181  mi = exposure.getMaskedImage()
182 
183  nCand = len(psfCandidateList)
184  if nCand == 0:
185  raise RuntimeError("No PSF candidates supplied.")
186  #
187  # How big should our PSF models be?
188  #
189  if display: # only needed for debug plots
190  # construct and populate a spatial cell set
191  bbox = mi.getBBox(afwImage.PARENT)
192  psfCellSet = afwMath.SpatialCellSet(bbox, self.config.sizeCellX, self.config.sizeCellY)
193  else:
194  psfCellSet = None
195 
196  sizes = np.empty(nCand)
197  for i, psfCandidate in enumerate(psfCandidateList):
198  try:
199  if psfCellSet:
200  psfCellSet.insertCandidate(psfCandidate)
201  except Exception as e:
202  self.log.debug("Skipping PSF candidate %d of %d: %s", i, len(psfCandidateList), e)
203  continue
204 
205  source = psfCandidate.getSource()
206  quad = afwEll.Quadrupole(source.getIxx(), source.getIyy(), source.getIxy())
207  rmsSize = quad.getTraceRadius()
208  sizes[i] = rmsSize
209 
210  if self.config.kernelSize >= 15:
211  self.log.warn("NOT scaling kernelSize by stellar quadrupole moment, but using absolute value")
212  actualKernelSize = int(self.config.kernelSize)
213  else:
214  actualKernelSize = 2 * int(self.config.kernelSize * np.sqrt(np.median(sizes)) + 0.5) + 1
215  if actualKernelSize < self.config.kernelSizeMin:
216  actualKernelSize = self.config.kernelSizeMin
217  if actualKernelSize > self.config.kernelSizeMax:
218  actualKernelSize = self.config.kernelSizeMax
219  if display:
220  rms = np.median(sizes)
221  print("Median PSF RMS size=%.2f pixels (\"FWHM\"=%.2f)" % (rms, 2*np.sqrt(2*np.log(2))*rms))
222 
223  # If we manually set the resolution then we need the size in pixel units
224  pixKernelSize = actualKernelSize
225  if self.config.samplingSize > 0:
226  pixKernelSize = int(actualKernelSize*self.config.samplingSize)
227  if pixKernelSize % 2 == 0:
228  pixKernelSize += 1
229  self.log.trace("Psfex Kernel size=%.2f, Image Kernel Size=%.2f", actualKernelSize, pixKernelSize)
230  psfCandidateList[0].setHeight(pixKernelSize)
231  psfCandidateList[0].setWidth(pixKernelSize)
232 
233  #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- BEGIN PSFEX
234  #
235  # Insert the good candidates into the set
236  #
237  defaultsFile = os.path.join(os.environ["MEAS_EXTENSIONS_PSFEX_DIR"], "config", "default-lsst.psfex")
238  args_md = dafBase.PropertySet()
239  args_md.set("BASIS_TYPE", str(self.config.psfexBasis))
240  args_md.set("PSFVAR_DEGREES", str(self.config.spatialOrder))
241  args_md.set("PSF_SIZE", str(actualKernelSize))
242  args_md.set("PSF_SAMPLING", str(self.config.samplingSize))
243  prefs = psfex.Prefs(defaultsFile, args_md)
244  prefs.setCommandLine([])
245  prefs.addCatalog("psfexPsfDeterminer")
246 
247  prefs.use()
248  principalComponentExclusionFlag = bool(bool(psfex.Context.REMOVEHIDDEN)
249  if False else psfex.Context.KEEPHIDDEN)
250  context = psfex.Context(prefs.getContextName(), prefs.getContextGroup(),
251  prefs.getGroupDeg(), principalComponentExclusionFlag)
252  set = psfex.Set(context)
253  set.setVigSize(pixKernelSize, pixKernelSize)
254  set.setFwhm(2*np.sqrt(2*np.log(2))*np.median(sizes))
255  set.setRecentroid(self.config.recentroid)
256 
257  catindex, ext = 0, 0
258  backnoise2 = afwMath.makeStatistics(mi.getImage(), afwMath.VARIANCECLIP).getValue()
259  ccd = exposure.getDetector()
260  if ccd:
261  gain = np.mean(np.array([a.getGain() for a in ccd]))
262  else:
263  gain = 1.0
264  self.log.warn("Setting gain to %g" % (gain,))
265 
266  contextvalp = []
267  for i, key in enumerate(context.getName()):
268  if context.getPcflag(i):
269  contextvalp.append(pcval[pc])
270  pc += 1
271  elif key[0] == ':':
272  try:
273  contextvalp.append(exposure.getMetadata().getScalar(key[1:]))
274  except KeyError:
275  raise RuntimeError("*Error*: %s parameter not found in the header of %s" %
276  (key[1:], prefs.getContextName()))
277  else:
278  try:
279  contextvalp.append(np.array([psfCandidateList[_].getSource().get(key)
280  for _ in range(nCand)]))
281  except KeyError:
282  raise RuntimeError("*Error*: %s parameter not found" % (key,))
283  set.setContextname(i, key)
284 
285  if display:
286  frame = 0
287  if displayExposure:
288  ds9.mtv(exposure, frame=frame, title="psf determination")
289 
290  badBits = mi.getMask().getPlaneBitMask(self.config.badMaskBits)
291  fluxName = prefs.getPhotfluxRkey()
292  fluxFlagName = "base_" + fluxName + "_flag"
293 
294  xpos, ypos = [], []
295  for i, psfCandidate in enumerate(psfCandidateList):
296  source = psfCandidate.getSource()
297  xc, yc = source.getX(), source.getY()
298  try:
299  int(xc), int(yc)
300  except ValueError:
301  continue
302 
303  try:
304  pstamp = psfCandidate.getMaskedImage().clone()
305  except Exception as e:
306  continue
307 
308  if fluxFlagName in source.schema and source.get(fluxFlagName):
309  continue
310 
311  flux = source.get(fluxName)
312  if flux < 0 or np.isnan(flux):
313  continue
314 
315  # From this point, we're configuring the "sample" (PSFEx's version of a PSF candidate).
316  # Having created the sample, we must proceed to configure it, and then fini (finalize),
317  # or it will be malformed.
318  try:
319  sample = set.newSample()
320  sample.setCatindex(catindex)
321  sample.setExtindex(ext)
322  sample.setObjindex(i)
323 
324  imArray = pstamp.getImage().getArray()
325  imArray[np.where(np.bitwise_and(pstamp.getMask().getArray(), badBits))] = \
326  -2*psfex.BIG
327  sample.setVig(imArray)
328 
329  sample.setNorm(flux)
330  sample.setBacknoise2(backnoise2)
331  sample.setGain(gain)
332  sample.setX(xc)
333  sample.setY(yc)
334  sample.setFluxrad(sizes[i])
335 
336  for j in range(set.getNcontext()):
337  sample.setContext(j, float(contextvalp[j][i]))
338  except Exception as e:
339  self.log.debug("Exception when processing sample at (%f,%f): %s", xc, yc, e)
340  continue
341  else:
342  set.finiSample(sample)
343 
344  xpos.append(xc) # for QA
345  ypos.append(yc)
346 
347  if displayExposure:
348  with ds9.Buffering():
349  ds9.dot("o", xc, yc, ctype=ds9.CYAN, size=4, frame=frame)
350 
351  if set.getNsample() == 0:
352  raise RuntimeError("No good PSF candidates to pass to PSFEx")
353 
354  #---- Update min and max and then the scaling
355  for i in range(set.getNcontext()):
356  cmin = contextvalp[i].min()
357  cmax = contextvalp[i].max()
358  set.setContextScale(i, cmax - cmin)
359  set.setContextOffset(i, (cmin + cmax)/2.0)
360 
361  # Don't waste memory!
362  set.trimMemory()
363 
364  #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- END PSFEX
365  #
366  # Do a PSFEX decomposition of those PSF candidates
367  #
368  fields = []
369  field = psfex.Field("Unknown")
370  field.addExt(exposure.getWcs(), exposure.getWidth(), exposure.getHeight(), set.getNsample())
371  field.finalize()
372 
373  fields.append(field)
374 
375  sets = []
376  sets.append(set)
377 
378  psfex.makeit(fields, sets)
379  psfs = field.getPsfs()
380 
381  # Flag which objects were actually used in psfex by
382  good_indices = []
383  for i in range(sets[0].getNsample()):
384  index = sets[0].getSample(i).getObjindex()
385  if index > -1:
386  good_indices.append(index)
387 
388  if flagKey is not None:
389  for i, psfCandidate in enumerate(psfCandidateList):
390  source = psfCandidate.getSource()
391  if i in good_indices:
392  source.set(flagKey, True)
393 
394  xpos = np.array(xpos)
395  ypos = np.array(ypos)
396  numGoodStars = len(good_indices)
397  avgX, avgY = np.mean(xpos), np.mean(ypos)
398 
399  psf = psfex.PsfexPsf(psfs[0], afwGeom.Point2D(avgX, avgY))
400 
401  if False and (displayResiduals or displayPsfMosaic):
402  ext = 0
403  frame = 1
404  diagnostics = True
405  catDir = "."
406  title = "psfexPsfDeterminer"
407  psfex.psfex.showPsf(psfs, set, ext,
408  [(exposure.getWcs(), exposure.getWidth(), exposure.getHeight())],
409  nspot=3, trim=5, frame=frame, diagnostics=diagnostics, outDir=catDir,
410  title=title)
411  #
412  # Display code for debugging
413  #
414  if display:
415  assert psfCellSet is not None
416 
417  if displayExposure:
418  maUtils.showPsfSpatialCells(exposure, psfCellSet, showChi2=True,
419  symb="o", ctype=ds9.YELLOW, ctypeBad=ds9.RED, size=8, frame=frame)
420  if displayResiduals:
421  maUtils.showPsfCandidates(exposure, psfCellSet, psf=psf, frame=4,
422  normalize=normalizeResiduals,
423  showBadCandidates=showBadCandidates)
424  if displayPsfComponents:
425  maUtils.showPsf(psf, frame=6)
426  if displayPsfMosaic:
427  maUtils.showPsfMosaic(exposure, psf, frame=7, showFwhm=True)
428  ds9.scale('linear', 0, 1, frame=7)
429  #
430  # Generate some QA information
431  #
432  # Count PSF stars
433  #
434  if metadata is not None:
435  metadata.set("spatialFitChi2", np.nan)
436  metadata.set("numAvailStars", nCand)
437  metadata.set("numGoodStars", numGoodStars)
438  metadata.set("avgX", avgX)
439  metadata.set("avgY", avgY)
440 
441  psfCellSet = None
442  return psf, psfCellSet
443 
444 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
445 
446 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