lsst.meas.extensions.psfex  14.0-2-ge3897b5+2
psfexStarSelector.py
Go to the documentation of this file.
1 from __future__ import absolute_import, division, print_function
2 from builtins import zip
3 from builtins import input
4 from builtins import object
5 #
6 # LSST Data Management System
7 # Copyright 2008, 2009, 2010 LSST Corporation.
8 #
9 # This product includes software developed by the
10 # LSST Project (http://www.lsst.org/).
11 #
12 # This program is free software: you can redistribute it and/or modify
13 # it under the terms of the GNU General Public License as published by
14 # the Free Software Foundation, either version 3 of the License, or
15 # (at your option) any later version.
16 #
17 # This program is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # GNU General Public License for more details.
21 #
22 # You should have received a copy of the LSST License Statement and
23 # the GNU General Public License along with this program. If not,
24 # see <http://www.lsstcorp.org/LegalNotices/>.
25 #
26 import re
27 import sys
28 
29 import numpy as np
30 try:
31  import matplotlib.pyplot as plt
32  fig = None
33 except ImportError:
34  plt = None
35 
36 from lsst.afw.table import SourceCatalog
37 from lsst.pipe.base import Struct
38 import lsst.pex.config as pexConfig
39 import lsst.afw.display.ds9 as ds9
40 from lsst.meas.algorithms import BaseStarSelectorTask, starSelectorRegistry
41 from . import psfexLib
42 from .psfex import compute_fwhmrange
43 
44 __all__ = ["PsfexStarSelectorConfig", "PsfexStarSelectorTask"]
45 
46 
47 class PsfexStarSelectorConfig(BaseStarSelectorTask.ConfigClass):
48  fluxName = pexConfig.Field(
49  dtype=str,
50  doc="Name of photometric flux key ",
51  default="base_PsfFlux",
52  )
53  fluxErrName = pexConfig.Field(
54  dtype=str,
55  doc="Name of phot. flux err. key",
56  default="",
57  )
58  minFwhm = pexConfig.Field(
59  dtype=float,
60  doc="Maximum allowed FWHM ",
61  default=2,
62  )
63  maxFwhm = pexConfig.Field(
64  dtype=float,
65  doc="Minimum allowed FWHM ",
66  default=10,
67  )
68  maxFwhmVariability = pexConfig.Field(
69  dtype=float,
70  doc="Allowed FWHM variability (1.0 = 100%)",
71  default=0.2,
72  )
73  maxbad = pexConfig.Field(
74  dtype=int,
75  doc="Max number of bad pixels ",
76  default=0,
77  check=lambda x: x >= 0,
78  )
79  maxbadflag = pexConfig.Field(
80  dtype=bool,
81  doc="Filter bad pixels? ",
82  default=True
83  )
84  maxellip = pexConfig.Field(
85  dtype=float,
86  doc="Maximum (A-B)/(A+B) ",
87  default=0.3,
88  check=lambda x: x >= 0.0,
89  )
90  minsn = pexConfig.Field(
91  dtype=float,
92  doc="Minimum S/N for candidates",
93  default=100,
94  check=lambda x: x >= 0.0,
95  )
96 
97  def validate(self):
98  pexConfig.Config.validate(self)
99 
100  if self.fluxErrName == "":
101  self.fluxErrName = self.fluxName + ".err"
102  elif self.fluxErrName != self.fluxName + ".err":
103  raise pexConfig.FieldValidationError("fluxErrName (%s) doesn't correspond to fluxName (%s)"
104  % (self.fluxErrName, self.fluxName))
105 
106  if self.minFwhm > self.maxFwhm:
107  raise pexConfig.FieldValidationError("minFwhm (%f) > maxFwhm (%f)" % (self.minFwhm, self.maxFwhm))
108 
109  def setDefaults(self):
110  self.badFlags = [
111  "base_PixelFlags_flag_edge",
112  "base_PixelFlags_flag_saturatedCenter",
113  "base_PixelFlags_flag_crCenter",
114  "base_PixelFlags_flag_bad",
115  "base_PixelFlags_flag_suspectCenter",
116  "base_PsfFlux_flag",
117  #"parent", # actually this is a test on deblend_nChild
118  ]
119 
120 
121 class EventHandler(object):
122  """A class to handle key strokes with matplotlib displays"""
123 
124  def __init__(self, axes, xs, ys, x, y, frames=[0]):
125  self.axes = axes
126  self.xs = xs
127  self.ys = ys
128  self.x = x
129  self.y = y
130  self.frames = frames
131 
132  self.cid = self.axes.figure.canvas.mpl_connect('key_press_event', self)
133 
134  def __call__(self, ev):
135  if ev.inaxes != self.axes:
136  return
137 
138  if ev.key and ev.key in ("p"):
139  dist = np.hypot(self.xs - ev.xdata, self.ys - ev.ydata)
140  dist[np.where(np.isnan(dist))] = 1e30
141 
142  which = np.where(dist == min(dist))
143 
144  x = self.x[which][0]
145  y = self.y[which][0]
146  for frame in self.frames:
147  ds9.pan(x, y, frame=frame)
148  ds9.cmdBuffer.flush()
149  else:
150  pass
151 
152 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
153 
154 
155 def plot(mag, width, centers, clusterId, marker="o", markersize=2, markeredgewidth=0, ltype='-',
156  clear=True):
157 
158  global fig
159  if not fig:
160  fig = plt.figure()
161  newFig = True
162  else:
163  newFig = False
164  if clear:
165  fig.clf()
166 
167  axes = fig.add_axes((0.1, 0.1, 0.85, 0.80))
168 
169  xmin = sorted(mag)[int(0.05*len(mag))]
170  xmax = sorted(mag)[int(0.95*len(mag))]
171 
172  axes.set_xlim(-17.5, -13)
173  axes.set_xlim(xmin - 0.1*(xmax - xmin), xmax + 0.1*(xmax - xmin))
174  axes.set_ylim(0, 10)
175 
176  colors = ["r", "g", "b", "c", "m", "k", ]
177  for k, mean in enumerate(centers):
178  if k == 0:
179  axes.plot(axes.get_xlim(), (mean, mean,), "k%s" % ltype)
180 
181  l = (clusterId == k)
182  axes.plot(mag[l], width[l], marker, markersize=markersize, markeredgewidth=markeredgewidth,
183  color=colors[k%len(colors)])
184 
185  l = (clusterId == -1)
186  axes.plot(mag[l], width[l], marker, markersize=markersize, markeredgewidth=markeredgewidth,
187  color='k')
188 
189  if newFig:
190  axes.set_xlabel("model")
191  axes.set_ylabel(r"$\sqrt{I_{xx} + I_{yy}}$")
192 
193  return fig
194 
195 
201 
202 
203 class PsfexStarSelectorTask(BaseStarSelectorTask):
204  """!A star selector whose algorithm is not yet documented
205 
206  @anchor PsfexStarSelectorTask_
207 
208  @section meas_extensions_psfex_psfexStarSelectorStarSelector_Contents Contents
209 
210  - @ref meas_extensions_psfex_psfexStarSelectorStarSelector_Purpose
211  - @ref meas_extensions_psfex_psfexStarSelectorStarSelector_Initialize
212  - @ref meas_extensions_psfex_psfexStarSelectorStarSelector_IO
213  - @ref meas_extensions_psfex_psfexStarSelectorStarSelector_Config
214  - @ref meas_extensions_psfex_psfexStarSelectorStarSelector_Debug
215 
216  @section meas_extensions_psfex_psfexStarSelectorStarSelector_Purpose Description
217 
218  A star selector whose algorithm is not yet documented
219 
220  @section meas_extensions_psfex_psfexStarSelectorStarSelector_Initialize Task initialisation
221 
222  @copydoc \_\_init\_\_
223 
224  @section meas_extensions_psfex_psfexStarSelectorStarSelector_IO Invoking the Task
225 
226  Like all star selectors, the main method is `run`.
227 
228  @section meas_extensions_psfex_psfexStarSelectorStarSelector_Config Configuration parameters
229 
230  See @ref PsfexStarSelectorConfig
231 
232  @section meas_extensions_psfex_psfexStarSelectorStarSelector_Debug Debug variables
233 
234  PsfexStarSelectorTask has a debug dictionary with the following keys:
235  <dl>
236  <dt>display
237  <dd>bool; if True display debug information
238  <dt>displayExposure
239  <dd>bool; if True display the exposure and spatial cells
240  <dt>plotFwhmHistogram
241  <dd>bool; if True plot histogram of FWHM
242  <dt>plotFlags
243  <dd>bool: if True plot the sources coloured by their flags
244  <dt>plotRejection
245  <dd>bool; if True plot why sources are rejected
246  </dl>
247 
248  For example, put something like:
249  @code{.py}
250  import lsstDebug
251  def DebugInfo(name):
252  di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively
253  if name.endswith("objectSizeStarSelector"):
254  di.display = True
255  di.displayExposure = True
256  di.plotFwhmHistogram = True
257 
258  return di
259 
260  lsstDebug.Info = DebugInfo
261  @endcode
262  into your `debug.py` file and run your task with the `--debug` flag.
263  """
264  ConfigClass = PsfexStarSelectorConfig
265  usesMatches = False # selectStars does not use its matches argument
266 
267  def selectStars(self, exposure, sourceCat, matches=None):
268  """!Select stars from source catalog
269 
270  @param[in] exposure the exposure containing the sources
271  @param[in] sourceCat catalog of sources that may be stars (an lsst.afw.table.SourceCatalog)
272  @param[in] matches astrometric matches; ignored by this star selector
273 
274  @return a Struct containing:
275  - starCat a subset of sourceCat containing the selected stars
276  """
277  import lsstDebug
278  display = lsstDebug.Info(__name__).display
279 
280  displayExposure = display and \
281  lsstDebug.Info(__name__).displayExposure # display the Exposure + spatialCells
282  plotFwhmHistogram = display and plt and \
283  lsstDebug.Info(__name__).plotFwhmHistogram # Plot histogram of FWHM
284  plotFlags = display and plt and \
285  lsstDebug.Info(__name__).plotFlags # Plot the sources coloured by their flags
286  plotRejection = display and plt and \
287  lsstDebug.Info(__name__).plotRejection # Plot why sources are rejected
288 
289  #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
290  #
291  fluxName = self.config.fluxName
292  fluxErrName = self.config.fluxErrName
293  minFwhm = self.config.minFwhm
294  maxFwhm = self.config.maxFwhm
295  maxFwhmVariability = self.config.maxFwhmVariability
296  maxbad = self.config.maxbad
297  maxbadflag = self.config.maxbadflag
298  maxellip = self.config.maxellip
299  minsn = self.config.minsn
300 
301  maxelong = (maxellip + 1.0)/(1.0 - maxellip) if maxellip < 1.0 else 100
302 
303  # Unpack the catalogue
304  shape = sourceCat.getShapeDefinition()
305  ixx = sourceCat.get("%s.xx" % shape)
306  iyy = sourceCat.get("%s.yy" % shape)
307 
308  fwhm = 2*np.sqrt(2*np.log(2))*np.sqrt(0.5*(ixx + iyy))
309  elong = 0.5*(ixx - iyy)/(ixx + iyy)
310 
311  flux = sourceCat.get(fluxName)
312  fluxErr = sourceCat.get(fluxErrName)
313  sn = flux/np.where(fluxErr > 0, fluxErr, 1)
314  sn[fluxErr <= 0] = -psfexLib.BIG
315 
316  flags = 0x0
317  for i, f in enumerate(self.config.badFlags):
318  flags = np.bitwise_or(flags, np.where(sourceCat.get(f), 1 << i, 0))
319  #
320  # Estimate the acceptable range of source widths
321  #
322  good = np.logical_and(sn > minsn, np.logical_not(flags))
323  good = np.logical_and(good, elong < maxelong)
324  good = np.logical_and(good, fwhm >= minFwhm)
325  good = np.logical_and(good, fwhm < maxFwhm)
326 
327  fwhmMode, fwhmMin, fwhmMax = compute_fwhmrange(fwhm[good], maxFwhmVariability, minFwhm, maxFwhm,
328  plot=dict(fwhmHistogram=plotFwhmHistogram))
329 
330  #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
331  #
332  # Here's select_candidates
333  #
334  #---- Apply some selection over flags, fluxes...
335 
336  bad = (flags != 0)
337  # set.setBadFlags(int(sum(bad)))
338 
339  if plotRejection:
340  selectionVectors = []
341  selectionVectors.append((bad, "flags %d" % sum(bad)))
342 
343  dbad = sn < minsn
344  # set.setBadSN(int(sum(dbad)))
345  bad = np.logical_or(bad, dbad)
346  if plotRejection:
347  selectionVectors.append((dbad, "S/N %d" % sum(dbad)))
348 
349  dbad = fwhm < fwhmMin
350  # set.setBadFrmin(int(sum(dbad)))
351  bad = np.logical_or(bad, dbad)
352  if plotRejection:
353  selectionVectors.append((dbad, "fwhmMin %d" % sum(dbad)))
354 
355  dbad = fwhm > fwhmMax
356  # set.setBadFrmax(int(sum(dbad)))
357  bad = np.logical_or(bad, dbad)
358  if plotRejection:
359  selectionVectors.append((dbad, "fwhmMax %d" % sum(dbad)))
360 
361  dbad = elong > maxelong
362  # set.setBadElong(int(sum(dbad)))
363  bad = np.logical_or(bad, dbad)
364  if plotRejection:
365  selectionVectors.append((dbad, "elong %d" % sum(dbad)))
366 
367  #-- ... and check the integrity of the sample
368  if maxbadflag:
369  nbad = np.array([(v <= -psfexLib.BIG).sum() for v in vignet])
370  dbad = nbad > maxbad
371  # set.setBadPix(int(sum(dbad)))
372  bad = np.logical_or(bad, dbad)
373  if plotRejection:
374  selectionVectors.append((dbad, "badpix %d" % sum(dbad)))
375 
376  good = np.logical_not(bad)
377  #
378  # We know enough to plot, if so requested
379  #
380  frame = 0
381  if displayExposure:
382  mi = exposure.getMaskedImage()
383 
384  ds9.mtv(mi, frame=frame, title="PSF candidates")
385 
386  with ds9.Buffering():
387  for i, source in enumerate(sourceCat):
388  if good[i]:
389  ctype = ds9.GREEN # star candidate
390  else:
391  ctype = ds9.RED # not star
392 
393  ds9.dot("+", source.getX() - mi.getX0(), source.getY() - mi.getY0(),
394  frame=frame, ctype=ctype)
395 
396  if plotFlags or plotRejection:
397  imag = -2.5*np.log10(flux)
398  plt.clf()
399 
400  alpha = 0.5
401  if plotFlags:
402  isSet = np.where(flags == 0x0)[0]
403  plt.plot(imag[isSet], fwhm[isSet], 'o', alpha=alpha, label="good")
404 
405  for i, f in enumerate(self.config.badFlags):
406  mask = 1 << i
407  isSet = np.where(np.bitwise_and(flags, mask))[0]
408  if isSet.any():
409  if np.isfinite(imag[isSet] + fwhm[isSet]).any():
410  label = re.sub(r"\_flag", "",
411  re.sub(r"^base\_", "",
412  re.sub(r"^.*base\_PixelFlags\_flag\_", "", f)))
413  plt.plot(imag[isSet], fwhm[isSet], 'o', alpha=alpha, label=label)
414  else:
415  for bad, label in selectionVectors:
416  plt.plot(imag[bad], fwhm[bad], 'o', alpha=alpha, label=label)
417 
418  plt.plot(imag[good], fwhm[good], 'o', color="black", label="selected")
419  [plt.axhline(_, color='red') for _ in [fwhmMin, fwhmMax]]
420  plt.xlim(np.median(imag[good]) + 5*np.array([-1, 1]))
421  plt.ylim(fwhm[np.where(np.isfinite(fwhm + imag))].min(), 2*fwhmMax)
422  plt.legend(loc=2)
423  plt.xlabel("Instrumental %s Magnitude" % fluxName.split(".")[-1].title())
424  plt.ylabel("fwhm")
425  title = "PSFEX Star Selection"
426  plt.title("%s %d selected" % (title, sum(good)))
427 
428  if displayExposure:
429  global eventHandler
430  eventHandler = EventHandler(plt.axes(), imag, fwhm, sourceCat.getX(), sourceCat.getY(),
431  frames=[frame])
432 
433  if plotFlags or plotRejection:
434  while True:
435  try:
436  reply = input("continue? [y[es] h(elp) p(db) q(uit)] ").strip()
437  except EOFError:
438  reply = "y"
439 
440  if not reply:
441  reply = "y"
442 
443  if reply[0] == "h":
444  print("""\
445 At this prompt, you can continue with almost any key; 'p' enters pdb,
446  'q' returns to the shell, and
447  'h' prints this text
448 """, end=' ')
449 
450  if displayExposure:
451  print("""
452 If you put the cursor on a point in the matplotlib scatter plot and hit 'p' you'll see it in ds9.""")
453  elif reply[0] == "p":
454  import pdb
455  pdb.set_trace()
456  elif reply[0] == 'q':
457  sys.exit(1)
458  else:
459  break
460 
461  starCat = SourceCatalog(sourceCat.schema)
462  for source, isGood in zip(sourceCat, good):
463  if isGood:
464  starCat.append(source)
465 
466  return Struct(
467  starCat=starCat,
468  )
469 
470 starSelectorRegistry.register("psfex", PsfexStarSelectorTask)
def selectStars(self, exposure, sourceCat, matches=None)
Select stars from source catalog.
def compute_fwhmrange(fwhm, maxvar, minin, maxin, plot=dict(fwhmHistogram=False))
Definition: psfex.py:51
def __init__(self, axes, xs, ys, x, y, frames=[0])
A star selector whose algorithm is not yet documented.
def plot(mag, width, centers, clusterId, marker="o", markersize=2, markeredgewidth=0, ltype='-', clear=True)