23 from __future__
import print_function
24 from future
import standard_library
25 standard_library.install_aliases()
26 from builtins
import zip
27 from builtins
import input
28 from builtins
import str
29 from builtins
import range
30 from builtins
import object
35 from functools
import reduce
37 from lsst.afw.table
import SourceCatalog
38 from lsst.log
import Log
39 from lsst.pipe.base
import Struct
40 from lsst.afw.cameraGeom
import TAN_PIXELS
41 from lsst.afw.geom.ellipses
import Quadrupole
43 import lsst.pex.config
as pexConfig
44 import lsst.afw.display.ds9
as ds9
45 from .starSelector
import BaseStarSelectorTask, starSelectorRegistry
49 fluxMin = pexConfig.Field(
50 doc=
"specify the minimum psfFlux for good Psf Candidates",
53 check=
lambda x: x >= 0.0,
55 fluxMax = pexConfig.Field(
56 doc=
"specify the maximum psfFlux for good Psf Candidates (ignored if == 0)",
59 check=
lambda x: x >= 0.0,
61 widthMin = pexConfig.Field(
62 doc=
"minimum width to include in histogram",
65 check=
lambda x: x >= 0.0,
67 widthMax = pexConfig.Field(
68 doc=
"maximum width to include in histogram",
71 check=
lambda x: x >= 0.0,
73 sourceFluxField = pexConfig.Field(
74 doc=
"Name of field in Source to use for flux measurement",
76 default=
"base_GaussianFlux_flux",
78 widthStdAllowed = pexConfig.Field(
79 doc=
"Standard deviation of width allowed to be interpreted as good stars",
82 check=
lambda x: x >= 0.0,
84 nSigmaClip = pexConfig.Field(
85 doc=
"Keep objects within this many sigma of cluster 0's median",
88 check=
lambda x: x >= 0.0,
92 BaseStarSelectorTask.ConfigClass.validate(self)
94 raise pexConfig.FieldValidationError(
"widthMin (%f) > widthMax (%f)" 99 """A class to handle key strokes with matplotlib displays""" 101 def __init__(self, axes, xs, ys, x, y, frames=[0]):
109 self.
cid = self.
axes.figure.canvas.mpl_connect(
'key_press_event', self)
112 if ev.inaxes != self.
axes:
115 if ev.key
and ev.key
in (
"p"):
116 dist = numpy.hypot(self.
xs - ev.xdata, self.
ys - ev.ydata)
117 dist[numpy.where(numpy.isnan(dist))] = 1e30
119 which = numpy.where(dist == min(dist))
124 ds9.pan(x, y, frame=frame)
125 ds9.cmdBuffer.flush()
130 def _assignClusters(yvec, centers):
131 """Return a vector of centerIds based on their distance to the centers""" 132 assert len(centers) > 0
134 minDist = numpy.nan*numpy.ones_like(yvec)
135 clusterId = numpy.empty_like(yvec)
136 clusterId.dtype = int
137 dbl = Log.getLogger(
"objectSizeStarSelector._assignClusters")
138 dbl.setLevel(dbl.INFO)
141 oldSettings = numpy.seterr(all=
"warn")
142 with warnings.catch_warnings(record=
True)
as w:
143 warnings.simplefilter(
"always")
144 for i, mean
in enumerate(centers):
145 dist = abs(yvec - mean)
147 update = dist == dist
149 update = dist < minDist
151 dbl.trace(str(w[-1]))
153 minDist[update] = dist[update]
154 clusterId[update] = i
155 numpy.seterr(**oldSettings)
160 def _kcenters(yvec, nCluster, useMedian=False, widthStdAllowed=0.15):
161 """A classic k-means algorithm, clustering yvec into nCluster clusters 163 Return the set of centres, and the cluster ID for each of the points 165 If useMedian is true, use the median of the cluster as its centre, rather than 168 Serge Monkewitz points out that there other (maybe smarter) ways of seeding the means: 169 "e.g. why not use the Forgy or random partition initialization methods" 170 however, the approach adopted here seems to work well for the particular sorts of things 171 we're clustering in this application 176 mean0 = sorted(yvec)[len(yvec)//10]
177 delta = mean0 * widthStdAllowed * 2.0
178 centers = mean0 + delta * numpy.arange(nCluster)
180 func = numpy.median
if useMedian
else numpy.mean
182 clusterId = numpy.zeros_like(yvec) - 1
183 clusterId.dtype = int
185 oclusterId = clusterId
186 clusterId = _assignClusters(yvec, centers)
188 if numpy.all(clusterId == oclusterId):
191 for i
in range(nCluster):
193 pointsInCluster = (clusterId == i)
194 if numpy.any(pointsInCluster):
195 centers[i] = func(yvec[pointsInCluster])
197 centers[i] = numpy.nan
199 return centers, clusterId
202 def _improveCluster(yvec, centers, clusterId, nsigma=2.0, nIteration=10, clusterNum=0, widthStdAllowed=0.15):
203 """Improve our estimate of one of the clusters (clusterNum) by sigma-clipping around its median""" 205 nMember = sum(clusterId == clusterNum)
208 for iter
in range(nIteration):
209 old_nMember = nMember
211 inCluster0 = clusterId == clusterNum
212 yv = yvec[inCluster0]
214 centers[clusterNum] = numpy.median(yv)
215 stdev = numpy.std(yv)
218 stdev_iqr = 0.741*(syv[int(0.75*nMember)] - syv[int(0.25*nMember)])
219 median = syv[int(0.5*nMember)]
221 sd = stdev
if stdev < stdev_iqr
else stdev_iqr
224 print(
"sigma(iqr) = %.3f, sigma = %.3f" % (stdev_iqr, numpy.std(yv)))
225 newCluster0 = abs(yvec - centers[clusterNum]) < nsigma*sd
226 clusterId[numpy.logical_and(inCluster0, newCluster0)] = clusterNum
227 clusterId[numpy.logical_and(inCluster0, numpy.logical_not(newCluster0))] = -1
229 nMember = sum(clusterId == clusterNum)
231 if nMember == old_nMember
or sd < widthStdAllowed * median:
237 def plot(mag, width, centers, clusterId, marker="o", markersize=2, markeredgewidth=0, ltype='-',
238 magType="model", clear=True):
240 log = Log.getLogger(
"objectSizeStarSelector.plot")
242 import matplotlib.pyplot
as plt
243 except ImportError
as e:
244 log.warn(
"Unable to import matplotlib: %s", e)
254 axes = fig.add_axes((0.1, 0.1, 0.85, 0.80))
256 xmin = sorted(mag)[int(0.05*len(mag))]
257 xmax = sorted(mag)[int(0.95*len(mag))]
259 axes.set_xlim(-17.5, -13)
260 axes.set_xlim(xmin - 0.1*(xmax - xmin), xmax + 0.1*(xmax - xmin))
263 colors = [
"r", "g", "b", "c", "m", "k", ]
264 for k, mean
in enumerate(centers):
266 axes.plot(axes.get_xlim(), (mean, mean,),
"k%s" % ltype)
269 axes.plot(mag[l], width[l], marker, markersize=markersize, markeredgewidth=markeredgewidth,
270 color=colors[k % len(colors)])
272 l = (clusterId == -1)
273 axes.plot(mag[l], width[l], marker, markersize=markersize, markeredgewidth=markeredgewidth,
277 axes.set_xlabel(
"Instrumental %s mag" % magType)
278 axes.set_ylabel(
r"$\sqrt{(I_{xx} + I_{yy})/2}$")
291 """!A star selector that looks for a cluster of small objects in a size-magnitude plot 293 @anchor ObjectSizeStarSelectorTask_ 295 @section meas_algorithms_objectSizeStarSelector_Contents Contents 297 - @ref meas_algorithms_objectSizeStarSelector_Purpose 298 - @ref meas_algorithms_objectSizeStarSelector_Initialize 299 - @ref meas_algorithms_objectSizeStarSelector_IO 300 - @ref meas_algorithms_objectSizeStarSelector_Config 301 - @ref meas_algorithms_objectSizeStarSelector_Debug 303 @section meas_algorithms_objectSizeStarSelector_Purpose Description 305 A star selector that looks for a cluster of small objects in a size-magnitude plot. 307 @section meas_algorithms_objectSizeStarSelector_Initialize Task initialisation 309 @copydoc \_\_init\_\_ 311 @section meas_algorithms_objectSizeStarSelector_IO Invoking the Task 313 Like all star selectors, the main method is `run`. 315 @section meas_algorithms_objectSizeStarSelector_Config Configuration parameters 317 See @ref ObjectSizeStarSelectorConfig 319 @section meas_algorithms_objectSizeStarSelector_Debug Debug variables 321 ObjectSizeStarSelectorTask has a debug dictionary with the following keys: 324 <dd>bool; if True display debug information 326 <dd>bool; if True display the exposure and spatial cells 328 <dd>bool: if True display the magnitude-size relation using matplotlib 330 <dd>bool; if True dump data to a pickle file 333 For example, put something like: 337 di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively 338 if name.endswith("objectSizeStarSelector"): 340 di.displayExposure = True 341 di.plotMagSize = True 345 lsstDebug.Info = DebugInfo 347 into your `debug.py` file and run your task with the `--debug` flag. 349 ConfigClass = ObjectSizeStarSelectorConfig
353 """!Return a list of PSF candidates that represent likely stars 355 A list of PSF candidates may be used by a PSF fitter to construct a PSF. 357 \param[in] exposure the exposure containing the sources 358 \param[in] sourceCat catalog of sources that may be stars (an lsst.afw.table.SourceCatalog) 359 \param[in] matches astrometric matches; ignored by this star selector 361 \return an lsst.pipe.base.Struct containing: 362 - starCat catalog of selected stars (a subset of sourceCat) 365 display = lsstDebug.Info(__name__).display
366 displayExposure = lsstDebug.Info(__name__).displayExposure
367 plotMagSize = lsstDebug.Info(__name__).plotMagSize
368 dumpData = lsstDebug.Info(__name__).dumpData
370 detector = exposure.getDetector()
371 pixToTanXYTransform =
None 372 if detector
is not None:
373 tanSys = detector.makeCameraSys(TAN_PIXELS)
374 pixToTanXYTransform = detector.getTransformMap().get(tanSys)
378 flux = sourceCat.get(self.config.sourceFluxField)
380 xx = numpy.empty(len(sourceCat))
381 xy = numpy.empty_like(xx)
382 yy = numpy.empty_like(xx)
383 for i, source
in enumerate(sourceCat):
384 Ixx, Ixy, Iyy = source.getIxx(), source.getIxy(), source.getIyy()
385 if pixToTanXYTransform:
386 p = afwGeom.Point2D(source.getX(), source.getY())
387 linTransform = pixToTanXYTransform.linearizeForwardTransform(p).getLinear()
388 m = Quadrupole(Ixx, Iyy, Ixy)
389 m.transform(linTransform)
390 Ixx, Iyy, Ixy = m.getIxx(), m.getIyy(), m.getIxy()
392 xx[i], xy[i], yy[i] = Ixx, Ixy, Iyy
394 width = numpy.sqrt(0.5*(xx + yy))
396 bad = reduce(
lambda x, y: numpy.logical_or(x, sourceCat.get(y)), self.config.badFlags,
False)
397 bad = numpy.logical_or(bad, flux < self.config.fluxMin)
398 bad = numpy.logical_or(bad, numpy.logical_not(numpy.isfinite(width)))
399 bad = numpy.logical_or(bad, numpy.logical_not(numpy.isfinite(flux)))
400 bad = numpy.logical_or(bad, width < self.config.widthMin)
401 bad = numpy.logical_or(bad, width > self.config.widthMax)
402 if self.config.fluxMax > 0:
403 bad = numpy.logical_or(bad, flux > self.config.fluxMax)
404 good = numpy.logical_not(bad)
406 if not numpy.any(good):
407 raise RuntimeError(
"No objects passed our cuts for consideration as psf stars")
409 mag = -2.5*numpy.log10(flux[good])
417 import pickle
as pickle
420 pickleFile = os.path.expanduser(os.path.join(
"~",
"widths-%d.pkl" % _ii))
421 if not os.path.exists(pickleFile):
425 with open(pickleFile,
"wb")
as fd:
426 pickle.dump(mag, fd, -1)
427 pickle.dump(width, fd, -1)
429 centers, clusterId = _kcenters(width, nCluster=4, useMedian=
True,
430 widthStdAllowed=self.config.widthStdAllowed)
432 if display
and plotMagSize:
433 fig =
plot(mag, width, centers, clusterId,
434 magType=self.config.sourceFluxField.split(
".")[-1].title(),
435 marker=
"+", markersize=3, markeredgewidth=
None, ltype=
':', clear=
True)
439 clusterId = _improveCluster(width, centers, clusterId,
440 nsigma=self.config.nSigmaClip,
441 widthStdAllowed=self.config.widthStdAllowed)
443 if display
and plotMagSize:
444 plot(mag, width, centers, clusterId, marker=
"x", markersize=3, markeredgewidth=
None, clear=
False)
446 stellar = (clusterId == 0)
453 if display
and displayExposure:
454 ds9.mtv(exposure.getMaskedImage(), frame=frame, title=
"PSF candidates")
457 eventHandler =
EventHandler(fig.get_axes()[0], mag, width,
458 sourceCat.getX()[good], sourceCat.getY()[good], frames=[frame])
464 reply = input(
"continue? [c h(elp) q(uit) p(db)] ").strip()
473 We cluster the points; red are the stellar candidates and the other colours are other clusters. 474 Points labelled + are rejects from the cluster (only for cluster 0). 476 At this prompt, you can continue with almost any key; 'p' enters pdb, and 'h' prints this text 478 If displayExposure is true, you can put the cursor on a point and hit 'p' to see it in ds9. 480 elif reply[0] ==
"p":
483 elif reply[0] ==
'q':
488 if display
and displayExposure:
489 mi = exposure.getMaskedImage()
491 with ds9.Buffering():
492 for i, source
in enumerate(sourceCat):
498 ds9.dot(
"+", source.getX() - mi.getX0(),
499 source.getY() - mi.getY0(), frame=frame, ctype=ctype)
501 starCat = SourceCatalog(sourceCat.table)
502 goodSources = [s
for g, s
in zip(good, sourceCat)
if g]
503 for isStellar, source
in zip(stellar, goodSources):
505 starCat.append(source)
512 starSelectorRegistry.register(
"objectSize", ObjectSizeStarSelectorTask)
def plot(mag, width, centers, clusterId, marker="o", markersize=2, markeredgewidth=0, ltype='-', magType="model", clear=True)
A star selector that looks for a cluster of small objects in a size-magnitude plot.
Base class for star selectors.
def __init__(self, axes, xs, ys, x, y, frames=[0])
def selectStars(self, exposure, sourceCat, matches=None)
Return a list of PSF candidates that represent likely stars.