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