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
44 from .starSelector
import BaseStarSelectorTask, starSelectorRegistry
48 fluxMin = pexConfig.Field(
49 doc=
"specify the minimum psfFlux for good Psf Candidates",
52 check=
lambda x: x >= 0.0,
54 fluxMax = pexConfig.Field(
55 doc=
"specify the maximum psfFlux for good Psf Candidates (ignored if == 0)",
58 check=
lambda x: x >= 0.0,
60 widthMin = pexConfig.Field(
61 doc=
"minimum width to include in histogram",
64 check=
lambda x: x >= 0.0,
66 widthMax = pexConfig.Field(
67 doc=
"maximum width to include in histogram",
70 check=
lambda x: x >= 0.0,
72 sourceFluxField = pexConfig.Field(
73 doc=
"Name of field in Source to use for flux measurement",
75 default=
"base_GaussianFlux_flux",
77 widthStdAllowed = pexConfig.Field(
78 doc=
"Standard deviation of width allowed to be interpreted as good stars",
81 check=
lambda x: x >= 0.0,
83 nSigmaClip = pexConfig.Field(
84 doc=
"Keep objects within this many sigma of cluster 0's median",
87 check=
lambda x: x >= 0.0,
91 BaseStarSelectorTask.ConfigClass.validate(self)
93 raise pexConfig.FieldValidationError(
"widthMin (%f) > widthMax (%f)" 98 """A class to handle key strokes with matplotlib displays""" 100 def __init__(self, axes, xs, ys, x, y, frames=[0]):
108 self.
cid = self.
axes.figure.canvas.mpl_connect(
'key_press_event', self)
111 if ev.inaxes != self.
axes:
114 if ev.key
and ev.key
in (
"p"):
115 dist = numpy.hypot(self.
xs - ev.xdata, self.
ys - ev.ydata)
116 dist[numpy.where(numpy.isnan(dist))] = 1e30
118 which = numpy.where(dist == min(dist))
123 ds9.pan(x, y, frame=frame)
124 ds9.cmdBuffer.flush()
129 def _assignClusters(yvec, centers):
130 """Return a vector of centerIds based on their distance to the centers""" 131 assert len(centers) > 0
133 minDist = numpy.nan*numpy.ones_like(yvec)
134 clusterId = numpy.empty_like(yvec)
135 clusterId.dtype = int
136 dbl = Log.getLogger(
"objectSizeStarSelector._assignClusters")
137 dbl.setLevel(dbl.INFO)
140 oldSettings = numpy.seterr(all=
"warn")
141 with warnings.catch_warnings(record=
True)
as w:
142 warnings.simplefilter(
"always")
143 for i, mean
in enumerate(centers):
144 dist = abs(yvec - mean)
146 update = dist == dist
148 update = dist < minDist
150 dbl.trace(str(w[-1]))
152 minDist[update] = dist[update]
153 clusterId[update] = i
154 numpy.seterr(**oldSettings)
159 def _kcenters(yvec, nCluster, useMedian=False, widthStdAllowed=0.15):
160 """A classic k-means algorithm, clustering yvec into nCluster clusters 162 Return the set of centres, and the cluster ID for each of the points 164 If useMedian is true, use the median of the cluster as its centre, rather than 167 Serge Monkewitz points out that there other (maybe smarter) ways of seeding the means: 168 "e.g. why not use the Forgy or random partition initialization methods" 169 however, the approach adopted here seems to work well for the particular sorts of things 170 we're clustering in this application 175 mean0 = sorted(yvec)[len(yvec)//10]
176 delta = mean0 * widthStdAllowed * 2.0
177 centers = mean0 + delta * numpy.arange(nCluster)
179 func = numpy.median
if useMedian
else numpy.mean
181 clusterId = numpy.zeros_like(yvec) - 1
182 clusterId.dtype = int
184 oclusterId = clusterId
185 clusterId = _assignClusters(yvec, centers)
187 if numpy.all(clusterId == oclusterId):
190 for i
in range(nCluster):
192 pointsInCluster = (clusterId == i)
193 if numpy.any(pointsInCluster):
194 centers[i] = func(yvec[pointsInCluster])
196 centers[i] = numpy.nan
198 return centers, clusterId
201 def _improveCluster(yvec, centers, clusterId, nsigma=2.0, nIteration=10, clusterNum=0, widthStdAllowed=0.15):
202 """Improve our estimate of one of the clusters (clusterNum) by sigma-clipping around its median""" 204 nMember = sum(clusterId == clusterNum)
207 for iter
in range(nIteration):
208 old_nMember = nMember
210 inCluster0 = clusterId == clusterNum
211 yv = yvec[inCluster0]
213 centers[clusterNum] = numpy.median(yv)
214 stdev = numpy.std(yv)
217 stdev_iqr = 0.741*(syv[int(0.75*nMember)] - syv[int(0.25*nMember)])
218 median = syv[int(0.5*nMember)]
220 sd = stdev
if stdev < stdev_iqr
else stdev_iqr
223 print(
"sigma(iqr) = %.3f, sigma = %.3f" % (stdev_iqr, numpy.std(yv)))
224 newCluster0 = abs(yvec - centers[clusterNum]) < nsigma*sd
225 clusterId[numpy.logical_and(inCluster0, newCluster0)] = clusterNum
226 clusterId[numpy.logical_and(inCluster0, numpy.logical_not(newCluster0))] = -1
228 nMember = sum(clusterId == clusterNum)
230 if nMember == old_nMember
or sd < widthStdAllowed * median:
236 def plot(mag, width, centers, clusterId, marker="o", markersize=2, markeredgewidth=0, ltype='-',
237 magType="model", clear=True):
239 log = Log.getLogger(
"objectSizeStarSelector.plot")
241 import matplotlib.pyplot
as plt
242 except ImportError
as e:
243 log.warn(
"Unable to import matplotlib: %s", e)
253 axes = fig.add_axes((0.1, 0.1, 0.85, 0.80))
255 xmin = sorted(mag)[int(0.05*len(mag))]
256 xmax = sorted(mag)[int(0.95*len(mag))]
258 axes.set_xlim(-17.5, -13)
259 axes.set_xlim(xmin - 0.1*(xmax - xmin), xmax + 0.1*(xmax - xmin))
262 colors = [
"r", "g", "b", "c", "m", "k", ]
263 for k, mean
in enumerate(centers):
265 axes.plot(axes.get_xlim(), (mean, mean,),
"k%s" % ltype)
268 axes.plot(mag[l], width[l], marker, markersize=markersize, markeredgewidth=markeredgewidth,
269 color=colors[k % len(colors)])
271 l = (clusterId == -1)
272 axes.plot(mag[l], width[l], marker, markersize=markersize, markeredgewidth=markeredgewidth,
276 axes.set_xlabel(
"Instrumental %s mag" % magType)
277 axes.set_ylabel(
r"$\sqrt{(I_{xx} + I_{yy})/2}$")
290 """!A star selector that looks for a cluster of small objects in a size-magnitude plot 292 @anchor ObjectSizeStarSelectorTask_ 294 @section meas_algorithms_objectSizeStarSelector_Contents Contents 296 - @ref meas_algorithms_objectSizeStarSelector_Purpose 297 - @ref meas_algorithms_objectSizeStarSelector_Initialize 298 - @ref meas_algorithms_objectSizeStarSelector_IO 299 - @ref meas_algorithms_objectSizeStarSelector_Config 300 - @ref meas_algorithms_objectSizeStarSelector_Debug 302 @section meas_algorithms_objectSizeStarSelector_Purpose Description 304 A star selector that looks for a cluster of small objects in a size-magnitude plot. 306 @section meas_algorithms_objectSizeStarSelector_Initialize Task initialisation 308 @copydoc \_\_init\_\_ 310 @section meas_algorithms_objectSizeStarSelector_IO Invoking the Task 312 Like all star selectors, the main method is `run`. 314 @section meas_algorithms_objectSizeStarSelector_Config Configuration parameters 316 See @ref ObjectSizeStarSelectorConfig 318 @section meas_algorithms_objectSizeStarSelector_Debug Debug variables 320 ObjectSizeStarSelectorTask has a debug dictionary with the following keys: 323 <dd>bool; if True display debug information 325 <dd>bool; if True display the exposure and spatial cells 327 <dd>bool: if True display the magnitude-size relation using matplotlib 329 <dd>bool; if True dump data to a pickle file 332 For example, put something like: 336 di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively 337 if name.endswith("objectSizeStarSelector"): 339 di.displayExposure = True 340 di.plotMagSize = True 344 lsstDebug.Info = DebugInfo 346 into your `debug.py` file and run your task with the `--debug` flag. 348 ConfigClass = ObjectSizeStarSelectorConfig
352 """!Return a list of PSF candidates that represent likely stars 354 A list of PSF candidates may be used by a PSF fitter to construct a PSF. 356 \param[in] exposure the exposure containing the sources 357 \param[in] sourceCat catalog of sources that may be stars (an lsst.afw.table.SourceCatalog) 358 \param[in] matches astrometric matches; ignored by this star selector 360 \return an lsst.pipe.base.Struct containing: 361 - starCat catalog of selected stars (a subset of sourceCat) 369 detector = exposure.getDetector()
371 if detector
is not None:
372 pixToTanPix = detector.getTransform(PIXELS, TAN_PIXELS)
376 flux = sourceCat.get(self.config.sourceFluxField)
378 xx = numpy.empty(len(sourceCat))
379 xy = numpy.empty_like(xx)
380 yy = numpy.empty_like(xx)
381 for i, source
in enumerate(sourceCat):
382 Ixx, Ixy, Iyy = source.getIxx(), source.getIxy(), source.getIyy()
386 m = afwGeom.Quadrupole(Ixx, Iyy, Ixy)
387 m.transform(linTransform)
388 Ixx, Iyy, Ixy = m.getIxx(), m.getIyy(), m.getIxy()
390 xx[i], xy[i], yy[i] = Ixx, Ixy, Iyy
392 width = numpy.sqrt(0.5*(xx + yy))
393 with numpy.errstate(invalid=
"ignore"):
394 bad = reduce(
lambda x, y: numpy.logical_or(x, sourceCat.get(y)), self.config.badFlags,
False)
395 bad = numpy.logical_or(bad, flux < self.config.fluxMin)
396 bad = numpy.logical_or(bad, numpy.logical_not(numpy.isfinite(width)))
397 bad = numpy.logical_or(bad, numpy.logical_not(numpy.isfinite(flux)))
398 bad = numpy.logical_or(bad, width < self.config.widthMin)
399 bad = numpy.logical_or(bad, width > self.config.widthMax)
400 if self.config.fluxMax > 0:
401 bad = numpy.logical_or(bad, flux > self.config.fluxMax)
402 good = numpy.logical_not(bad)
404 if not numpy.any(good):
405 raise RuntimeError(
"No objects passed our cuts for consideration as psf stars")
407 mag = -2.5*numpy.log10(flux[good])
415 import pickle
as pickle
418 pickleFile = os.path.expanduser(os.path.join(
"~",
"widths-%d.pkl" % _ii))
419 if not os.path.exists(pickleFile):
423 with open(pickleFile,
"wb")
as fd:
424 pickle.dump(mag, fd, -1)
425 pickle.dump(width, fd, -1)
427 centers, clusterId = _kcenters(width, nCluster=4, useMedian=
True,
428 widthStdAllowed=self.config.widthStdAllowed)
430 if display
and plotMagSize:
431 fig =
plot(mag, width, centers, clusterId,
432 magType=self.config.sourceFluxField.split(
".")[-1].title(),
433 marker=
"+", markersize=3, markeredgewidth=
None, ltype=
':', clear=
True)
437 clusterId = _improveCluster(width, centers, clusterId,
438 nsigma=self.config.nSigmaClip,
439 widthStdAllowed=self.config.widthStdAllowed)
441 if display
and plotMagSize:
442 plot(mag, width, centers, clusterId, marker=
"x", markersize=3, markeredgewidth=
None, clear=
False)
444 stellar = (clusterId == 0)
451 if display
and displayExposure:
452 ds9.mtv(exposure.getMaskedImage(), frame=frame, title=
"PSF candidates")
455 eventHandler =
EventHandler(fig.get_axes()[0], mag, width,
456 sourceCat.getX()[good], sourceCat.getY()[good], frames=[frame])
462 reply = input(
"continue? [c h(elp) q(uit) p(db)] ").strip()
471 We cluster the points; red are the stellar candidates and the other colours are other clusters. 472 Points labelled + are rejects from the cluster (only for cluster 0). 474 At this prompt, you can continue with almost any key; 'p' enters pdb, and 'h' prints this text 476 If displayExposure is true, you can put the cursor on a point and hit 'p' to see it in ds9. 478 elif reply[0] ==
"p":
481 elif reply[0] ==
'q':
486 if display
and displayExposure:
487 mi = exposure.getMaskedImage()
489 with ds9.Buffering():
490 for i, source
in enumerate(sourceCat):
496 ds9.dot(
"+", source.getX() - mi.getX0(),
497 source.getY() - mi.getY0(), frame=frame, ctype=ctype)
499 starCat = SourceCatalog(sourceCat.table)
500 goodSources = [s
for g, s
in zip(good, sourceCat)
if g]
501 for isStellar, source
in zip(stellar, goodSources):
503 starCat.append(source)
510 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])
AffineTransform linearizeTransform(TransformPoint2ToPoint2 const &original, Point2D const &inPoint)
def selectStars(self, exposure, sourceCat, matches=None)
Return a list of PSF candidates that represent likely stars.