LSSTApplications  19.0.0-14-gb0260a2+72efe9b372,20.0.0+7927753e06,20.0.0+8829bf0056,20.0.0+995114c5d2,20.0.0+b6f4b2abd1,20.0.0+bddc4f4cbe,20.0.0-1-g253301a+8829bf0056,20.0.0-1-g2b7511a+0d71a2d77f,20.0.0-1-g5b95a8c+7461dd0434,20.0.0-12-g321c96ea+23efe4bbff,20.0.0-16-gfab17e72e+fdf35455f6,20.0.0-2-g0070d88+ba3ffc8f0b,20.0.0-2-g4dae9ad+ee58a624b3,20.0.0-2-g61b8584+5d3db074ba,20.0.0-2-gb780d76+d529cf1a41,20.0.0-2-ged6426c+226a441f5f,20.0.0-2-gf072044+8829bf0056,20.0.0-2-gf1f7952+ee58a624b3,20.0.0-20-geae50cf+e37fec0aee,20.0.0-25-g3dcad98+544a109665,20.0.0-25-g5eafb0f+ee58a624b3,20.0.0-27-g64178ef+f1f297b00a,20.0.0-3-g4cc78c6+e0676b0dc8,20.0.0-3-g8f21e14+4fd2c12c9a,20.0.0-3-gbd60e8c+187b78b4b8,20.0.0-3-gbecbe05+48431fa087,20.0.0-38-ge4adf513+a12e1f8e37,20.0.0-4-g97dc21a+544a109665,20.0.0-4-gb4befbc+087873070b,20.0.0-4-gf910f65+5d3db074ba,20.0.0-5-gdfe0fee+199202a608,20.0.0-5-gfbfe500+d529cf1a41,20.0.0-6-g64f541c+d529cf1a41,20.0.0-6-g9a5b7a1+a1cd37312e,20.0.0-68-ga3f3dda+5fca18c6a4,20.0.0-9-g4aef684+e18322736b,w.2020.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  spatialOrder = pexConfig.Field(
39  doc="specify spatial order for PSF kernel creation",
40  dtype=int,
41  default=2,
42  check=lambda x: x >= 0,
43  )
44  sizeCellX = pexConfig.Field(
45  doc="size of cell used to determine PSF (pixels, column direction)",
46  dtype=int,
47  default=256,
48  # minValue = 10,
49  check=lambda x: x >= 10,
50  )
51  sizeCellY = pexConfig.Field(
52  doc="size of cell used to determine PSF (pixels, row direction)",
53  dtype=int,
54  default=sizeCellX.default,
55  # minValue = 10,
56  check=lambda x: x >= 10,
57  )
58  samplingSize = pexConfig.Field(
59  doc="Resolution of the internal PSF model relative to the pixel size; "
60  "e.g. 0.5 is equal to 2x oversampling",
61  dtype=float,
62  default=0.5,
63  )
64  badMaskBits = pexConfig.ListField(
65  doc="List of mask bits which cause a source to be rejected as bad "
66  "N.b. INTRP is used specially in PsfCandidateSet; it means \"Contaminated by neighbour\"",
67  dtype=str,
68  default=["INTRP", "SAT"],
69  )
70  psfexBasis = pexConfig.ChoiceField(
71  doc="BASIS value given to psfex. PIXEL_AUTO will use the requested samplingSize only if "
72  "the FWHM < 3 pixels. Otherwise, it will use samplingSize=1. PIXEL will always use the "
73  "requested samplingSize",
74  dtype=str,
75  allowed={
76  "PIXEL": "Always use requested samplingSize",
77  "PIXEL_AUTO": "Only use requested samplingSize when FWHM < 3",
78  },
79  default='PIXEL_AUTO',
80  optional=False,
81  )
82  tolerance = pexConfig.Field(
83  doc="tolerance of spatial fitting",
84  dtype=float,
85  default=1e-2,
86  )
87  lam = pexConfig.Field(
88  doc="floor for variance is lam*data",
89  dtype=float,
90  default=0.05,
91  )
92  reducedChi2ForPsfCandidates = pexConfig.Field(
93  doc="for psf candidate evaluation",
94  dtype=float,
95  default=2.0,
96  )
97  spatialReject = pexConfig.Field(
98  doc="Rejection threshold (stdev) for candidates based on spatial fit",
99  dtype=float,
100  default=3.0,
101  )
102  recentroid = pexConfig.Field(
103  doc="Should PSFEX be permitted to recentroid PSF candidates?",
104  dtype=bool,
105  default=False,
106  )
107  kernelSize = pexConfig.Field(
108  doc=("Size of the postage stamp around each star that is extracted for fitting."
109  "Note: this reflects the oversampling setting of the psf, set by `samplingSize`;"
110  "e.g. `samplingSize=0.5` would require this value to be 2x what you expect."),
111  dtype=int,
112  default=81,
113  )
114 
115 
116 class PsfexPsfDeterminerTask(measAlg.BasePsfDeterminerTask):
117  ConfigClass = PsfexPsfDeterminerConfig
118 
119  def determinePsf(self, exposure, psfCandidateList, metadata=None, flagKey=None):
120  """Determine a PSFEX PSF model for an exposure given a list of PSF
121  candidates.
122 
123  Parameters
124  ----------
125  exposure: `lsst.afw.image.Exposure`
126  Exposure containing the PSF candidates.
127  psfCandidateList: iterable of `lsst.meas.algorithms.PsfCandidate`
128  Sequence of PSF candidates typically obtained by detecting sources
129  and then running them through a star selector.
130  metadata: metadata, optional
131  A home for interesting tidbits of information.
132  flagKey: `lsst.afw.table.Key`, optional
133  Schema key used to mark sources actually used in PSF determination.
134 
135  Returns
136  -------
137  psf: `lsst.meas.extensions.psfex.PsfexPsf`
138  The determined PSF.
139  """
140 
141  import lsstDebug
142  display = lsstDebug.Info(__name__).display
143  displayExposure = display and \
144  lsstDebug.Info(__name__).displayExposure # display the Exposure + spatialCells
145  displayPsfComponents = display and \
146  lsstDebug.Info(__name__).displayPsfComponents # show the basis functions
147  showBadCandidates = display and \
148  lsstDebug.Info(__name__).showBadCandidates # Include bad candidates (meaningless, methinks)
149  displayResiduals = display and \
150  lsstDebug.Info(__name__).displayResiduals # show residuals
151  displayPsfMosaic = display and \
152  lsstDebug.Info(__name__).displayPsfMosaic # show mosaic of reconstructed PSF(x,y)
153  normalizeResiduals = lsstDebug.Info(__name__).normalizeResiduals
154  afwDisplay.setDefaultMaskTransparency(75)
155  # Normalise residuals by object amplitude
156 
157  mi = exposure.getMaskedImage()
158 
159  nCand = len(psfCandidateList)
160  if nCand == 0:
161  raise RuntimeError("No PSF candidates supplied.")
162  #
163  # How big should our PSF models be?
164  #
165  if display: # only needed for debug plots
166  # construct and populate a spatial cell set
167  bbox = mi.getBBox(afwImage.PARENT)
168  psfCellSet = afwMath.SpatialCellSet(bbox, self.config.sizeCellX, self.config.sizeCellY)
169  else:
170  psfCellSet = None
171 
172  sizes = np.empty(nCand)
173  for i, psfCandidate in enumerate(psfCandidateList):
174  try:
175  if psfCellSet:
176  psfCellSet.insertCandidate(psfCandidate)
177  except Exception as e:
178  self.log.error("Skipping PSF candidate %d of %d: %s", i, len(psfCandidateList), e)
179  continue
180 
181  source = psfCandidate.getSource()
182  quad = afwEll.Quadrupole(source.getIxx(), source.getIyy(), source.getIxy())
183  rmsSize = quad.getTraceRadius()
184  sizes[i] = rmsSize
185 
186  if self.config.kernelSize >= 15:
187  self.log.warn("NOT scaling kernelSize by stellar quadrupole moment, but using absolute value")
188  actualKernelSize = self.config.kernelSize
189  else:
190  actualKernelSize = 2 * int(self.config.kernelSize * np.sqrt(np.median(sizes)) + 0.5) + 1
191  if actualKernelSize < self.config.kernelSizeMin:
192  actualKernelSize = self.config.kernelSizeMin
193  if actualKernelSize > self.config.kernelSizeMax:
194  actualKernelSize = self.config.kernelSizeMax
195  if display:
196  rms = np.median(sizes)
197  msg = "Median PSF RMS size=%.2f pixels (\"FWHM\"=%.2f)" % (rms, 2*np.sqrt(2*np.log(2))*rms)
198  self.log.debug(msg)
199 
200  # If we manually set the resolution then we need the size in pixel
201  # units
202  pixKernelSize = actualKernelSize
203  if self.config.samplingSize > 0:
204  pixKernelSize = int(actualKernelSize*self.config.samplingSize)
205  if pixKernelSize % 2 == 0:
206  pixKernelSize += 1
207  self.log.trace("Psfex Kernel size=%.2f, Image Kernel Size=%.2f", actualKernelSize, pixKernelSize)
208  psfCandidateList[0].setHeight(pixKernelSize)
209  psfCandidateList[0].setWidth(pixKernelSize)
210 
211  # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- BEGIN PSFEX
212  #
213  # Insert the good candidates into the set
214  #
215  defaultsFile = os.path.join(os.environ["MEAS_EXTENSIONS_PSFEX_DIR"], "config", "default-lsst.psfex")
216  args_md = dafBase.PropertySet()
217  args_md.set("BASIS_TYPE", str(self.config.psfexBasis))
218  args_md.set("PSFVAR_DEGREES", str(self.config.spatialOrder))
219  args_md.set("PSF_SIZE", str(actualKernelSize))
220  args_md.set("PSF_SAMPLING", str(self.config.samplingSize))
221  prefs = psfex.Prefs(defaultsFile, args_md)
222  prefs.setCommandLine([])
223  prefs.addCatalog("psfexPsfDeterminer")
224 
225  prefs.use()
226  principalComponentExclusionFlag = bool(bool(psfex.Context.REMOVEHIDDEN)
227  if False else psfex.Context.KEEPHIDDEN)
228  context = psfex.Context(prefs.getContextName(), prefs.getContextGroup(),
229  prefs.getGroupDeg(), principalComponentExclusionFlag)
230  psfSet = psfex.Set(context)
231  psfSet.setVigSize(pixKernelSize, pixKernelSize)
232  psfSet.setFwhm(2*np.sqrt(2*np.log(2))*np.median(sizes))
233  psfSet.setRecentroid(self.config.recentroid)
234 
235  catindex, ext = 0, 0
236  backnoise2 = afwMath.makeStatistics(mi.getImage(), afwMath.VARIANCECLIP).getValue()
237  ccd = exposure.getDetector()
238  if ccd:
239  gain = np.mean(np.array([a.getGain() for a in ccd]))
240  else:
241  gain = 1.0
242  self.log.warn("Setting gain to %g" % (gain,))
243 
244  contextvalp = []
245  for i, key in enumerate(context.getName()):
246  if key[0] == ':':
247  try:
248  contextvalp.append(exposure.getMetadata().getScalar(key[1:]))
249  except KeyError as e:
250  raise RuntimeError("%s parameter not found in the header of %s" %
251  (key[1:], prefs.getContextName())) from e
252  else:
253  try:
254  contextvalp.append(np.array([psfCandidateList[_].getSource().get(key)
255  for _ in range(nCand)]))
256  except KeyError as e:
257  raise RuntimeError("%s parameter not found" % (key,)) from e
258  psfSet.setContextname(i, key)
259 
260  if display:
261  frame = 0
262  if displayExposure:
263  disp = afwDisplay.Display(frame=frame)
264  disp.mtv(exposure, title="psf determination")
265 
266  badBits = mi.getMask().getPlaneBitMask(self.config.badMaskBits)
267  fluxName = prefs.getPhotfluxRkey()
268  fluxFlagName = "base_" + fluxName + "_flag"
269 
270  xpos, ypos = [], []
271  for i, psfCandidate in enumerate(psfCandidateList):
272  source = psfCandidate.getSource()
273 
274  # skip sources with bad centroids
275  xc, yc = source.getX(), source.getY()
276  if not np.isfinite(xc) or not np.isfinite(yc):
277  continue
278  # skip flagged sources
279  if fluxFlagName in source.schema and source.get(fluxFlagName):
280  continue
281  # skip nonfinite and negative sources
282  flux = source.get(fluxName)
283  if flux < 0 or not np.isfinite(flux):
284  continue
285 
286  pstamp = psfCandidate.getMaskedImage().clone()
287 
288  # From this point, we're configuring the "sample" (PSFEx's version
289  # of a PSF candidate).
290  # Having created the sample, we must proceed to configure it, and
291  # then fini (finalize), or it will be malformed.
292  try:
293  sample = psfSet.newSample()
294  sample.setCatindex(catindex)
295  sample.setExtindex(ext)
296  sample.setObjindex(i)
297 
298  imArray = pstamp.getImage().getArray()
299  imArray[np.where(np.bitwise_and(pstamp.getMask().getArray(), badBits))] = \
300  -2*psfex.BIG
301  sample.setVig(imArray)
302 
303  sample.setNorm(flux)
304  sample.setBacknoise2(backnoise2)
305  sample.setGain(gain)
306  sample.setX(xc)
307  sample.setY(yc)
308  sample.setFluxrad(sizes[i])
309 
310  for j in range(psfSet.getNcontext()):
311  sample.setContext(j, float(contextvalp[j][i]))
312  except Exception as e:
313  self.log.error("Exception when processing sample at (%f,%f): %s", xc, yc, e)
314  continue
315  else:
316  psfSet.finiSample(sample)
317 
318  xpos.append(xc) # for QA
319  ypos.append(yc)
320 
321  if displayExposure:
322  with disp.Buffering():
323  disp.dot("o", xc, yc, ctype=afwDisplay.CYAN, size=4)
324 
325  if psfSet.getNsample() == 0:
326  raise RuntimeError("No good PSF candidates to pass to PSFEx")
327 
328  # ---- Update min and max and then the scaling
329  for i in range(psfSet.getNcontext()):
330  cmin = contextvalp[i].min()
331  cmax = contextvalp[i].max()
332  psfSet.setContextScale(i, cmax - cmin)
333  psfSet.setContextOffset(i, (cmin + cmax)/2.0)
334 
335  # Don't waste memory!
336  psfSet.trimMemory()
337 
338  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- END PSFEX
339  #
340  # Do a PSFEX decomposition of those PSF candidates
341  #
342  fields = []
343  field = psfex.Field("Unknown")
344  field.addExt(exposure.getWcs(), exposure.getWidth(), exposure.getHeight(), psfSet.getNsample())
345  field.finalize()
346 
347  fields.append(field)
348 
349  sets = []
350  sets.append(psfSet)
351 
352  psfex.makeit(fields, sets)
353  psfs = field.getPsfs()
354 
355  # Flag which objects were actually used in psfex by
356  good_indices = []
357  for i in range(sets[0].getNsample()):
358  index = sets[0].getSample(i).getObjindex()
359  if index > -1:
360  good_indices.append(index)
361 
362  if flagKey is not None:
363  for i, psfCandidate in enumerate(psfCandidateList):
364  source = psfCandidate.getSource()
365  if i in good_indices:
366  source.set(flagKey, True)
367 
368  xpos = np.array(xpos)
369  ypos = np.array(ypos)
370  numGoodStars = len(good_indices)
371  avgX, avgY = np.mean(xpos), np.mean(ypos)
372 
373  psf = psfex.PsfexPsf(psfs[0], geom.Point2D(avgX, avgY))
374 
375  #
376  # Display code for debugging
377  #
378  if display:
379  assert psfCellSet is not None
380 
381  if displayExposure:
382  maUtils.showPsfSpatialCells(exposure, psfCellSet, showChi2=True,
383  symb="o", ctype=afwDisplay.YELLOW, ctypeBad=afwDisplay.RED,
384  size=8, display=disp)
385  if displayResiduals:
386  disp4 = afwDisplay.Display(frame=4)
387  maUtils.showPsfCandidates(exposure, psfCellSet, psf=psf, display=disp4,
388  normalize=normalizeResiduals,
389  showBadCandidates=showBadCandidates)
390  if displayPsfComponents:
391  disp6 = afwDisplay.Display(frame=6)
392  maUtils.showPsf(psf, display=disp6)
393  if displayPsfMosaic:
394  disp7 = afwDisplay.Display(frame=7)
395  maUtils.showPsfMosaic(exposure, psf, display=disp7, showFwhm=True)
396  disp.scale('linear', 0, 1)
397  #
398  # Generate some QA information
399  #
400  # Count PSF stars
401  #
402  if metadata is not None:
403  metadata.set("spatialFitChi2", np.nan)
404  metadata.set("numAvailStars", nCand)
405  metadata.set("numGoodStars", numGoodStars)
406  metadata.set("avgX", avgX)
407  metadata.set("avgY", avgY)
408 
409  return psf, psfCellSet
410 
411 
412 measAlg.psfDeterminerRegistry.register("psfex", PsfexPsfDeterminerTask)
lsst::afw::image
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects.
Definition: imageAlgorithm.dox:1
lsst::log.log.logContinued.warn
def warn(fmt, *args)
Definition: logContinued.py:205
lsst::log.log.logContinued.error
def error(fmt, *args)
Definition: logContinued.py:213
lsst.gdb.afw.printers.debug
bool debug
Definition: printers.py:9
lsst::afw.display
Definition: __init__.py:1
lsst::afw::math::makeStatistics
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
lsst::meas::extensions::psfex.psfexPsfDeterminer.PsfexPsfDeterminerTask
Definition: psfexPsfDeterminer.py:116
lsst::meas::extensions::psfex
Definition: PsfexPsf.h:31
lsstDebug.Info
Definition: lsstDebug.py:28
lsst::afw::geom::ellipses
Definition: Axes.h:42
lsst.pex.config
Definition: __init__.py:1
max
int max
Definition: BoundedField.cc:104
lsst::meas::extensions::psfex.psfexPsfDeterminer.PsfexPsfDeterminerTask.determinePsf
def determinePsf(self, exposure, psfCandidateList, metadata=None, flagKey=None)
Definition: psfexPsfDeterminer.py:119
lsst::geom
Definition: AffineTransform.h:36
lsst::daf::base
Definition: Utils.h:47
min
int min
Definition: BoundedField.cc:103
lsst::log.log.logContinued.trace
def trace(fmt, *args)
Definition: logContinued.py:193
lsst::afw::math
Definition: statistics.dox:6
lsst::geom::Point< double, 2 >
lsst::meas::algorithms.utils
Definition: utils.py:1
lsst::daf::base::PropertySet
Class for storing generic metadata.
Definition: PropertySet.h:67
lsst::afw::math::SpatialCellSet
A collection of SpatialCells covering an entire image.
Definition: SpatialCell.h:387
lsst::meas::extensions::psfex.psfexPsfDeterminer.PsfexPsfDeterminerConfig
Definition: psfexPsfDeterminer.py:37
lsst::meas::algorithms
Fit spatial kernel using approximate fluxes for candidates, and solving a linear system of equations.
Definition: CoaddBoundedField.h:34
lsst::afw::image.slicing.clone
clone
Definition: slicing.py:257
lsst::meas::extensions::psfex::PsfexPsf
Represent a PSF as a linear combination of PSFEX (== Karhunen-Loeve) basis functions.
Definition: PsfexPsf.h:40