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
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) 370 detector = exposure.getDetector()
372 if detector
is not None:
373 pixToTanPix = detector.getTransform(PIXELS, TAN_PIXELS)
377 flux = sourceCat.get(self.config.sourceFluxField)
379 xx = numpy.empty(len(sourceCat))
380 xy = numpy.empty_like(xx)
381 yy = numpy.empty_like(xx)
382 for i, source
in enumerate(sourceCat):
383 Ixx, Ixy, Iyy = source.getIxx(), source.getIxy(), source.getIyy()
388 m.transform(linTransform)
389 Ixx, Iyy, Ixy = m.getIxx(), m.getIyy(), m.getIxy()
391 xx[i], xy[i], yy[i] = Ixx, Ixy, Iyy
393 width = numpy.sqrt(0.5*(xx + yy))
394 with numpy.errstate(invalid=
"ignore"):
395 bad = reduce(
lambda x, y: numpy.logical_or(x, sourceCat.get(y)), self.config.badFlags,
False)
396 bad = numpy.logical_or(bad, flux < self.config.fluxMin)
397 bad = numpy.logical_or(bad, numpy.logical_not(numpy.isfinite(width)))
398 bad = numpy.logical_or(bad, numpy.logical_not(numpy.isfinite(flux)))
399 bad = numpy.logical_or(bad, width < self.config.widthMin)
400 bad = numpy.logical_or(bad, width > self.config.widthMax)
401 if self.config.fluxMax > 0:
402 bad = numpy.logical_or(bad, flux > self.config.fluxMax)
403 good = numpy.logical_not(bad)
405 if not numpy.any(good):
406 raise RuntimeError(
"No objects passed our cuts for consideration as psf stars")
408 mag = -2.5*numpy.log10(flux[good])
416 import pickle
as pickle
419 pickleFile = os.path.expanduser(os.path.join(
"~",
"widths-%d.pkl" % _ii))
420 if not os.path.exists(pickleFile):
424 with open(pickleFile,
"wb")
as fd:
425 pickle.dump(mag, fd, -1)
426 pickle.dump(width, fd, -1)
428 centers, clusterId = _kcenters(width, nCluster=4, useMedian=
True,
429 widthStdAllowed=self.config.widthStdAllowed)
431 if display
and plotMagSize:
432 fig =
plot(mag, width, centers, clusterId,
433 magType=self.config.sourceFluxField.split(
".")[-1].title(),
434 marker=
"+", markersize=3, markeredgewidth=
None, ltype=
':', clear=
True)
438 clusterId = _improveCluster(width, centers, clusterId,
439 nsigma=self.config.nSigmaClip,
440 widthStdAllowed=self.config.widthStdAllowed)
442 if display
and plotMagSize:
443 plot(mag, width, centers, clusterId, marker=
"x", markersize=3, markeredgewidth=
None, clear=
False)
445 stellar = (clusterId == 0)
452 if display
and displayExposure:
453 ds9.mtv(exposure.getMaskedImage(), frame=frame, title=
"PSF candidates")
456 eventHandler =
EventHandler(fig.get_axes()[0], mag, width,
457 sourceCat.getX()[good], sourceCat.getY()[good], frames=[frame])
463 reply = input(
"continue? [c h(elp) q(uit) p(db)] ").strip()
472 We cluster the points; red are the stellar candidates and the other colours are other clusters. 473 Points labelled + are rejects from the cluster (only for cluster 0). 475 At this prompt, you can continue with almost any key; 'p' enters pdb, and 'h' prints this text 477 If displayExposure is true, you can put the cursor on a point and hit 'p' to see it in ds9. 479 elif reply[0] ==
"p":
482 elif reply[0] ==
'q':
487 if display
and displayExposure:
488 mi = exposure.getMaskedImage()
490 with ds9.Buffering():
491 for i, source
in enumerate(sourceCat):
497 ds9.dot(
"+", source.getX() - mi.getX0(),
498 source.getY() - mi.getY0(), frame=frame, ctype=ctype)
500 starCat = SourceCatalog(sourceCat.table)
501 goodSources = [s
for g, s
in zip(good, sourceCat)
if g]
502 for isStellar, source
in zip(stellar, goodSources):
504 starCat.append(source)
511 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.