27 from functools
import reduce
35 from .sourceSelector
import BaseSourceSelectorTask, sourceSelectorRegistry
39 fluxMin = pexConfig.Field(
40 doc=
"specify the minimum psfFlux for good Psf Candidates",
43 check=
lambda x: x >= 0.0,
45 fluxMax = pexConfig.Field(
46 doc=
"specify the maximum psfFlux for good Psf Candidates (ignored if == 0)",
49 check=
lambda x: x >= 0.0,
51 widthMin = pexConfig.Field(
52 doc=
"minimum width to include in histogram",
55 check=
lambda x: x >= 0.0,
57 widthMax = pexConfig.Field(
58 doc=
"maximum width to include in histogram",
61 check=
lambda x: x >= 0.0,
63 sourceFluxField = pexConfig.Field(
64 doc=
"Name of field in Source to use for flux measurement",
66 default=
"base_GaussianFlux_flux",
68 widthStdAllowed = pexConfig.Field(
69 doc=
"Standard deviation of width allowed to be interpreted as good stars",
72 check=
lambda x: x >= 0.0,
74 nSigmaClip = pexConfig.Field(
75 doc=
"Keep objects within this many sigma of cluster 0's median",
78 check=
lambda x: x >= 0.0,
80 badFlags = pexConfig.ListField(
81 doc=
"List of flags which cause a source to be rejected as bad",
84 "base_PixelFlags_flag_edge",
85 "base_PixelFlags_flag_interpolatedCenter",
86 "base_PixelFlags_flag_saturatedCenter",
87 "base_PixelFlags_flag_crCenter",
88 "base_PixelFlags_flag_bad",
89 "base_PixelFlags_flag_interpolated",
94 BaseSourceSelectorTask.ConfigClass.validate(self)
96 raise pexConfig.FieldValidationError(
"widthMin (%f) > widthMax (%f)" 101 """A class to handle key strokes with matplotlib displays""" 103 def __init__(self, axes, xs, ys, x, y, frames=[0]):
111 self.
cid = self.
axes.figure.canvas.mpl_connect(
'key_press_event', self)
114 if ev.inaxes != self.
axes:
117 if ev.key
and ev.key
in (
"p"):
118 dist = numpy.hypot(self.
xs - ev.xdata, self.
ys - ev.ydata)
119 dist[numpy.where(numpy.isnan(dist))] = 1e30
121 which = numpy.where(dist == min(dist))
126 ds9.pan(x, y, frame=frame)
127 ds9.cmdBuffer.flush()
132 def _assignClusters(yvec, centers):
133 """Return a vector of centerIds based on their distance to the centers""" 134 assert len(centers) > 0
136 minDist = numpy.nan*numpy.ones_like(yvec)
137 clusterId = numpy.empty_like(yvec)
138 clusterId.dtype = int
139 dbl = Log.getLogger(
"objectSizeStarSelector._assignClusters")
140 dbl.setLevel(dbl.INFO)
143 oldSettings = numpy.seterr(all=
"warn")
144 with warnings.catch_warnings(record=
True)
as w:
145 warnings.simplefilter(
"always")
146 for i, mean
in enumerate(centers):
147 dist = abs(yvec - mean)
149 update = dist == dist
151 update = dist < minDist
153 dbl.trace(str(w[-1]))
155 minDist[update] = dist[update]
156 clusterId[update] = i
157 numpy.seterr(**oldSettings)
162 def _kcenters(yvec, nCluster, useMedian=False, widthStdAllowed=0.15):
163 """A classic k-means algorithm, clustering yvec into nCluster clusters 165 Return the set of centres, and the cluster ID for each of the points 167 If useMedian is true, use the median of the cluster as its centre, rather than 170 Serge Monkewitz points out that there other (maybe smarter) ways of seeding the means: 171 "e.g. why not use the Forgy or random partition initialization methods" 172 however, the approach adopted here seems to work well for the particular sorts of things 173 we're clustering in this application 178 mean0 = sorted(yvec)[len(yvec)//10]
179 delta = mean0 * widthStdAllowed * 2.0
180 centers = mean0 + delta * numpy.arange(nCluster)
182 func = numpy.median
if useMedian
else numpy.mean
184 clusterId = numpy.zeros_like(yvec) - 1
185 clusterId.dtype = int
187 oclusterId = clusterId
188 clusterId = _assignClusters(yvec, centers)
190 if numpy.all(clusterId == oclusterId):
193 for i
in range(nCluster):
195 pointsInCluster = (clusterId == i)
196 if numpy.any(pointsInCluster):
197 centers[i] = func(yvec[pointsInCluster])
199 centers[i] = numpy.nan
201 return centers, clusterId
204 def _improveCluster(yvec, centers, clusterId, nsigma=2.0, nIteration=10, clusterNum=0, widthStdAllowed=0.15):
205 """Improve our estimate of one of the clusters (clusterNum) by sigma-clipping around its median""" 207 nMember = sum(clusterId == clusterNum)
210 for iter
in range(nIteration):
211 old_nMember = nMember
213 inCluster0 = clusterId == clusterNum
214 yv = yvec[inCluster0]
216 centers[clusterNum] = numpy.median(yv)
217 stdev = numpy.std(yv)
220 stdev_iqr = 0.741*(syv[int(0.75*nMember)] - syv[int(0.25*nMember)])
221 median = syv[int(0.5*nMember)]
223 sd = stdev
if stdev < stdev_iqr
else stdev_iqr
226 print(
"sigma(iqr) = %.3f, sigma = %.3f" % (stdev_iqr, numpy.std(yv)))
227 newCluster0 = abs(yvec - centers[clusterNum]) < nsigma*sd
228 clusterId[numpy.logical_and(inCluster0, newCluster0)] = clusterNum
229 clusterId[numpy.logical_and(inCluster0, numpy.logical_not(newCluster0))] = -1
231 nMember = sum(clusterId == clusterNum)
233 if nMember == old_nMember
or sd < widthStdAllowed * median:
239 def plot(mag, width, centers, clusterId, marker="o", markersize=2, markeredgewidth=0, ltype='-',
240 magType="model", clear=True):
242 log = Log.getLogger(
"objectSizeStarSelector.plot")
244 import matplotlib.pyplot
as plt
245 except ImportError
as e:
246 log.warn(
"Unable to import matplotlib: %s", e)
256 axes = fig.add_axes((0.1, 0.1, 0.85, 0.80))
258 xmin = sorted(mag)[int(0.05*len(mag))]
259 xmax = sorted(mag)[int(0.95*len(mag))]
261 axes.set_xlim(-17.5, -13)
262 axes.set_xlim(xmin - 0.1*(xmax - xmin), xmax + 0.1*(xmax - xmin))
265 colors = [
"r", "g", "b", "c", "m", "k", ]
266 for k, mean
in enumerate(centers):
268 axes.plot(axes.get_xlim(), (mean, mean,),
"k%s" % ltype)
270 li = (clusterId == k)
271 axes.plot(mag[li], width[li], marker, markersize=markersize, markeredgewidth=markeredgewidth,
272 color=colors[k % len(colors)])
274 li = (clusterId == -1)
275 axes.plot(mag[li], width[li], marker, markersize=markersize, markeredgewidth=markeredgewidth,
279 axes.set_xlabel(
"Instrumental %s mag" % magType)
280 axes.set_ylabel(
r"$\sqrt{(I_{xx} + I_{yy})/2}$")
292 @pexConfig.registerConfigurable(
"objectSize", sourceSelectorRegistry)
294 """!A star selector that looks for a cluster of small objects in a size-magnitude plot 296 @anchor ObjectSizeStarSelectorTask_ 298 @section meas_algorithms_objectSizeStarSelector_Contents Contents 300 - @ref meas_algorithms_objectSizeStarSelector_Purpose 301 - @ref meas_algorithms_objectSizeStarSelector_Initialize 302 - @ref meas_algorithms_objectSizeStarSelector_IO 303 - @ref meas_algorithms_objectSizeStarSelector_Config 304 - @ref meas_algorithms_objectSizeStarSelector_Debug 306 @section meas_algorithms_objectSizeStarSelector_Purpose Description 308 A star selector that looks for a cluster of small objects in a size-magnitude plot. 310 @section meas_algorithms_objectSizeStarSelector_Initialize Task initialisation 312 @copydoc \_\_init\_\_ 314 @section meas_algorithms_objectSizeStarSelector_IO Invoking the Task 316 Like all star selectors, the main method is `run`. 318 @section meas_algorithms_objectSizeStarSelector_Config Configuration parameters 320 See @ref ObjectSizeStarSelectorConfig 322 @section meas_algorithms_objectSizeStarSelector_Debug Debug variables 324 ObjectSizeStarSelectorTask has a debug dictionary with the following keys: 327 <dd>bool; if True display debug information 329 <dd>bool; if True display the exposure and spatial cells 331 <dd>bool: if True display the magnitude-size relation using matplotlib 333 <dd>bool; if True dump data to a pickle file 336 For example, put something like: 340 di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively 341 if name.endswith("objectSizeStarSelector"): 343 di.displayExposure = True 344 di.plotMagSize = True 348 lsstDebug.Info = DebugInfo 350 into your `debug.py` file and run your task with the `--debug` flag. 352 ConfigClass = ObjectSizeStarSelectorConfig
356 """Return a selection of PSF candidates that represent likely stars. 358 A list of PSF candidates may be used by a PSF fitter to construct a PSF. 362 sourceCat : `lsst.afw.table.SourceCatalog` 363 Catalog of sources to select from. 364 This catalog must be contiguous in memory. 365 matches : `list` of `lsst.afw.table.ReferenceMatch` or None 366 Ignored in this SourceSelector. 367 exposure : `lsst.afw.image.Exposure` or None 368 The exposure the catalog was built from; used to get the detector 369 to transform to TanPix, and for debug display. 373 struct : `lsst.pipe.base.Struct` 374 The struct contains the following data: 376 - selected : `array` of `bool`` 377 Boolean array of sources that were selected, same length as 386 detector = exposure.getDetector()
388 if detector
is not None:
389 pixToTanPix = detector.getTransform(PIXELS, TAN_PIXELS)
393 flux = sourceCat.get(self.config.sourceFluxField)
395 xx = numpy.empty(len(sourceCat))
396 xy = numpy.empty_like(xx)
397 yy = numpy.empty_like(xx)
398 for i, source
in enumerate(sourceCat):
399 Ixx, Ixy, Iyy = source.getIxx(), source.getIxy(), source.getIyy()
403 m = afwGeom.Quadrupole(Ixx, Iyy, Ixy)
404 m.transform(linTransform)
405 Ixx, Iyy, Ixy = m.getIxx(), m.getIyy(), m.getIxy()
407 xx[i], xy[i], yy[i] = Ixx, Ixy, Iyy
409 width = numpy.sqrt(0.5*(xx + yy))
410 with numpy.errstate(invalid=
"ignore"):
411 bad = reduce(
lambda x, y: numpy.logical_or(x, sourceCat.get(y)), self.config.badFlags,
False)
412 bad = numpy.logical_or(bad, flux < self.config.fluxMin)
413 bad = numpy.logical_or(bad, numpy.logical_not(numpy.isfinite(width)))
414 bad = numpy.logical_or(bad, numpy.logical_not(numpy.isfinite(flux)))
415 bad = numpy.logical_or(bad, width < self.config.widthMin)
416 bad = numpy.logical_or(bad, width > self.config.widthMax)
417 if self.config.fluxMax > 0:
418 bad = numpy.logical_or(bad, flux > self.config.fluxMax)
419 good = numpy.logical_not(bad)
421 if not numpy.any(good):
422 raise RuntimeError(
"No objects passed our cuts for consideration as psf stars")
424 mag = -2.5*numpy.log10(flux[good])
432 import pickle
as pickle
435 pickleFile = os.path.expanduser(os.path.join(
"~",
"widths-%d.pkl" % _ii))
436 if not os.path.exists(pickleFile):
440 with open(pickleFile,
"wb")
as fd:
441 pickle.dump(mag, fd, -1)
442 pickle.dump(width, fd, -1)
444 centers, clusterId = _kcenters(width, nCluster=4, useMedian=
True,
445 widthStdAllowed=self.config.widthStdAllowed)
447 if display
and plotMagSize:
448 fig =
plot(mag, width, centers, clusterId,
449 magType=self.config.sourceFluxField.split(
".")[-1].title(),
450 marker=
"+", markersize=3, markeredgewidth=
None, ltype=
':', clear=
True)
454 clusterId = _improveCluster(width, centers, clusterId,
455 nsigma=self.config.nSigmaClip,
456 widthStdAllowed=self.config.widthStdAllowed)
458 if display
and plotMagSize:
459 plot(mag, width, centers, clusterId, marker=
"x", markersize=3, markeredgewidth=
None, clear=
False)
461 stellar = (clusterId == 0)
468 if display
and displayExposure:
469 ds9.mtv(exposure.getMaskedImage(), frame=frame, title=
"PSF candidates")
472 eventHandler =
EventHandler(fig.get_axes()[0], mag, width,
473 sourceCat.getX()[good], sourceCat.getY()[good], frames=[frame])
479 reply = input(
"continue? [c h(elp) q(uit) p(db)] ").strip()
488 We cluster the points; red are the stellar candidates and the other colours are other clusters. 489 Points labelled + are rejects from the cluster (only for cluster 0). 491 At this prompt, you can continue with almost any key; 'p' enters pdb, and 'h' prints this text 493 If displayExposure is true, you can put the cursor on a point and hit 'p' to see it in ds9. 495 elif reply[0] ==
"p":
498 elif reply[0] ==
'q':
503 if display
and displayExposure:
504 mi = exposure.getMaskedImage()
506 with ds9.Buffering():
507 for i, source
in enumerate(sourceCat):
513 ds9.dot(
"+", source.getX() - mi.getX0(),
514 source.getY() - mi.getY0(), frame=frame, ctype=ctype)
520 return Struct(selected=good)
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.
def __init__(self, axes, xs, ys, x, y, frames=[0])
AffineTransform linearizeTransform(TransformPoint2ToPoint2 const &original, Point2D const &inPoint)
def selectSources(self, sourceCat, matches=None, exposure=None)