26 from functools
import reduce
35 from .sourceSelector
import BaseSourceSelectorTask, sourceSelectorRegistry
39 doFluxLimit = pexConfig.Field(
40 doc=
"Apply flux limit to Psf Candidate selection?",
44 fluxMin = pexConfig.Field(
45 doc=
"specify the minimum psfFlux for good Psf Candidates",
48 check=
lambda x: x >= 0.0,
50 fluxMax = pexConfig.Field(
51 doc=
"specify the maximum psfFlux for good Psf Candidates (ignored if == 0)",
54 check=
lambda x: x >= 0.0,
56 doSignalToNoiseLimit = pexConfig.Field(
57 doc=
"Apply signal-to-noise (i.e. flux/fluxErr) limit to Psf Candidate selection?",
61 signalToNoiseMin = pexConfig.Field(
62 doc=
"specify the minimum signal-to-noise for good Psf Candidates",
65 check=
lambda x: x >= 0.0,
67 signalToNoiseMax = pexConfig.Field(
68 doc=
"specify the maximum signal-to-noise for good Psf Candidates (ignored if == 0)",
71 check=
lambda x: x >= 0.0,
73 widthMin = pexConfig.Field(
74 doc=
"minimum width to include in histogram",
77 check=
lambda x: x >= 0.0,
79 widthMax = pexConfig.Field(
80 doc=
"maximum width to include in histogram",
83 check=
lambda x: x >= 0.0,
85 sourceFluxField = pexConfig.Field(
86 doc=
"Name of field in Source to use for flux measurement",
88 default=
"base_GaussianFlux_instFlux",
90 widthStdAllowed = pexConfig.Field(
91 doc=
"Standard deviation of width allowed to be interpreted as good stars",
94 check=
lambda x: x >= 0.0,
96 nSigmaClip = pexConfig.Field(
97 doc=
"Keep objects within this many sigma of cluster 0's median",
100 check=
lambda x: x >= 0.0,
102 badFlags = pexConfig.ListField(
103 doc=
"List of flags which cause a source to be rejected as bad",
106 "base_PixelFlags_flag_edge",
107 "base_PixelFlags_flag_interpolatedCenter",
108 "base_PixelFlags_flag_saturatedCenter",
109 "base_PixelFlags_flag_crCenter",
110 "base_PixelFlags_flag_bad",
111 "base_PixelFlags_flag_interpolated",
116 BaseSourceSelectorTask.ConfigClass.validate(self)
118 raise pexConfig.FieldValidationError(
"widthMin (%f) > widthMax (%f)" 123 """A class to handle key strokes with matplotlib displays""" 125 def __init__(self, axes, xs, ys, x, y, frames=[0]):
133 self.
cid = self.
axes.figure.canvas.mpl_connect(
'key_press_event', self)
136 if ev.inaxes != self.
axes:
139 if ev.key
and ev.key
in (
"p"):
140 dist = numpy.hypot(self.
xs - ev.xdata, self.
ys - ev.ydata)
141 dist[numpy.where(numpy.isnan(dist))] = 1e30
143 which = numpy.where(dist == min(dist))
148 ds9.pan(x, y, frame=frame)
149 ds9.cmdBuffer.flush()
154 def _assignClusters(yvec, centers):
155 """Return a vector of centerIds based on their distance to the centers""" 156 assert len(centers) > 0
158 minDist = numpy.nan*numpy.ones_like(yvec)
159 clusterId = numpy.empty_like(yvec)
160 clusterId.dtype = int
161 dbl = Log.getLogger(
"objectSizeStarSelector._assignClusters")
162 dbl.setLevel(dbl.INFO)
165 oldSettings = numpy.seterr(all=
"warn")
166 with warnings.catch_warnings(record=
True)
as w:
167 warnings.simplefilter(
"always")
168 for i, mean
in enumerate(centers):
169 dist = abs(yvec - mean)
171 update = dist == dist
173 update = dist < minDist
175 dbl.trace(str(w[-1]))
177 minDist[update] = dist[update]
178 clusterId[update] = i
179 numpy.seterr(**oldSettings)
184 def _kcenters(yvec, nCluster, useMedian=False, widthStdAllowed=0.15):
185 """A classic k-means algorithm, clustering yvec into nCluster clusters 187 Return the set of centres, and the cluster ID for each of the points 189 If useMedian is true, use the median of the cluster as its centre, rather than 192 Serge Monkewitz points out that there other (maybe smarter) ways of seeding the means: 193 "e.g. why not use the Forgy or random partition initialization methods" 194 however, the approach adopted here seems to work well for the particular sorts of things 195 we're clustering in this application 200 mean0 = sorted(yvec)[len(yvec)//10]
201 delta = mean0 * widthStdAllowed * 2.0
202 centers = mean0 + delta * numpy.arange(nCluster)
204 func = numpy.median
if useMedian
else numpy.mean
206 clusterId = numpy.zeros_like(yvec) - 1
207 clusterId.dtype = int
209 oclusterId = clusterId
210 clusterId = _assignClusters(yvec, centers)
212 if numpy.all(clusterId == oclusterId):
215 for i
in range(nCluster):
217 pointsInCluster = (clusterId == i)
218 if numpy.any(pointsInCluster):
219 centers[i] = func(yvec[pointsInCluster])
221 centers[i] = numpy.nan
223 return centers, clusterId
226 def _improveCluster(yvec, centers, clusterId, nsigma=2.0, nIteration=10, clusterNum=0, widthStdAllowed=0.15):
227 """Improve our estimate of one of the clusters (clusterNum) by sigma-clipping around its median""" 229 nMember = sum(clusterId == clusterNum)
232 for iter
in range(nIteration):
233 old_nMember = nMember
235 inCluster0 = clusterId == clusterNum
236 yv = yvec[inCluster0]
238 centers[clusterNum] = numpy.median(yv)
239 stdev = numpy.std(yv)
242 stdev_iqr = 0.741*(syv[int(0.75*nMember)] - syv[int(0.25*nMember)])
243 median = syv[int(0.5*nMember)]
245 sd = stdev
if stdev < stdev_iqr
else stdev_iqr
248 print(
"sigma(iqr) = %.3f, sigma = %.3f" % (stdev_iqr, numpy.std(yv)))
249 newCluster0 = abs(yvec - centers[clusterNum]) < nsigma*sd
250 clusterId[numpy.logical_and(inCluster0, newCluster0)] = clusterNum
251 clusterId[numpy.logical_and(inCluster0, numpy.logical_not(newCluster0))] = -1
253 nMember = sum(clusterId == clusterNum)
255 if nMember == old_nMember
or sd < widthStdAllowed * median:
261 def plot(mag, width, centers, clusterId, marker="o", markersize=2, markeredgewidth=0, ltype='-',
262 magType="model", clear=True):
264 log = Log.getLogger(
"objectSizeStarSelector.plot")
266 import matplotlib.pyplot
as plt
267 except ImportError
as e:
268 log.warn(
"Unable to import matplotlib: %s", e)
278 axes = fig.add_axes((0.1, 0.1, 0.85, 0.80))
280 xmin = sorted(mag)[int(0.05*len(mag))]
281 xmax = sorted(mag)[int(0.95*len(mag))]
283 axes.set_xlim(-17.5, -13)
284 axes.set_xlim(xmin - 0.1*(xmax - xmin), xmax + 0.1*(xmax - xmin))
287 colors = [
"r", "g", "b", "c", "m", "k", ]
288 for k, mean
in enumerate(centers):
290 axes.plot(axes.get_xlim(), (mean, mean,),
"k%s" % ltype)
292 li = (clusterId == k)
293 axes.plot(mag[li], width[li], marker, markersize=markersize, markeredgewidth=markeredgewidth,
294 color=colors[k % len(colors)])
296 li = (clusterId == -1)
297 axes.plot(mag[li], width[li], marker, markersize=markersize, markeredgewidth=markeredgewidth,
301 axes.set_xlabel(
"Instrumental %s mag" % magType)
302 axes.set_ylabel(
r"$\sqrt{(I_{xx} + I_{yy})/2}$")
314 @pexConfig.registerConfigurable(
"objectSize", sourceSelectorRegistry)
316 r"""!A star selector that looks for a cluster of small objects in a size-magnitude plot 318 @anchor ObjectSizeStarSelectorTask_ 320 @section meas_algorithms_objectSizeStarSelector_Contents Contents 322 - @ref meas_algorithms_objectSizeStarSelector_Purpose 323 - @ref meas_algorithms_objectSizeStarSelector_Initialize 324 - @ref meas_algorithms_objectSizeStarSelector_IO 325 - @ref meas_algorithms_objectSizeStarSelector_Config 326 - @ref meas_algorithms_objectSizeStarSelector_Debug 328 @section meas_algorithms_objectSizeStarSelector_Purpose Description 330 A star selector that looks for a cluster of small objects in a size-magnitude plot. 332 @section meas_algorithms_objectSizeStarSelector_Initialize Task initialisation 334 @copydoc \_\_init\_\_ 336 @section meas_algorithms_objectSizeStarSelector_IO Invoking the Task 338 Like all star selectors, the main method is `run`. 340 @section meas_algorithms_objectSizeStarSelector_Config Configuration parameters 342 See @ref ObjectSizeStarSelectorConfig 344 @section meas_algorithms_objectSizeStarSelector_Debug Debug variables 346 ObjectSizeStarSelectorTask has a debug dictionary with the following keys: 349 <dd>bool; if True display debug information 351 <dd>bool; if True display the exposure and spatial cells 353 <dd>bool: if True display the magnitude-size relation using matplotlib 355 <dd>bool; if True dump data to a pickle file 358 For example, put something like: 362 di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively 363 if name.endswith("objectSizeStarSelector"): 365 di.displayExposure = True 366 di.plotMagSize = True 370 lsstDebug.Info = DebugInfo 372 into your `debug.py` file and run your task with the `--debug` flag. 374 ConfigClass = ObjectSizeStarSelectorConfig
378 """Return a selection of PSF candidates that represent likely stars. 380 A list of PSF candidates may be used by a PSF fitter to construct a PSF. 384 sourceCat : `lsst.afw.table.SourceCatalog` 385 Catalog of sources to select from. 386 This catalog must be contiguous in memory. 387 matches : `list` of `lsst.afw.table.ReferenceMatch` or None 388 Ignored in this SourceSelector. 389 exposure : `lsst.afw.image.Exposure` or None 390 The exposure the catalog was built from; used to get the detector 391 to transform to TanPix, and for debug display. 395 struct : `lsst.pipe.base.Struct` 396 The struct contains the following data: 398 - selected : `array` of `bool`` 399 Boolean array of sources that were selected, same length as 411 detector = exposure.getDetector()
413 pixToTanPix = detector.getTransform(PIXELS, TAN_PIXELS)
417 flux = sourceCat.get(self.config.sourceFluxField)
418 fluxErr = sourceCat.get(self.config.sourceFluxField +
"Err")
420 xx = numpy.empty(len(sourceCat))
421 xy = numpy.empty_like(xx)
422 yy = numpy.empty_like(xx)
423 for i, source
in enumerate(sourceCat):
424 Ixx, Ixy, Iyy = source.getIxx(), source.getIxy(), source.getIyy()
427 linTransform = afwGeom.linearizeTransform(pixToTanPix, p).getLinear()
428 m = afwGeom.Quadrupole(Ixx, Iyy, Ixy)
429 m.transform(linTransform)
430 Ixx, Iyy, Ixy = m.getIxx(), m.getIyy(), m.getIxy()
432 xx[i], xy[i], yy[i] = Ixx, Ixy, Iyy
434 width = numpy.sqrt(0.5*(xx + yy))
435 with numpy.errstate(invalid=
"ignore"):
436 bad = reduce(
lambda x, y: numpy.logical_or(x, sourceCat.get(y)), self.config.badFlags,
False)
437 bad = numpy.logical_or(bad, numpy.logical_not(numpy.isfinite(width)))
438 bad = numpy.logical_or(bad, numpy.logical_not(numpy.isfinite(flux)))
439 if self.config.doFluxLimit:
440 bad = numpy.logical_or(bad, flux < self.config.fluxMin)
441 if self.config.fluxMax > 0:
442 bad = numpy.logical_or(bad, flux > self.config.fluxMax)
443 if self.config.doSignalToNoiseLimit:
444 bad = numpy.logical_or(bad, flux/fluxErr < self.config.signalToNoiseMin)
445 if self.config.signalToNoiseMax > 0:
446 bad = numpy.logical_or(bad, flux/fluxErr > self.config.signalToNoiseMax)
447 bad = numpy.logical_or(bad, width < self.config.widthMin)
448 bad = numpy.logical_or(bad, width > self.config.widthMax)
449 good = numpy.logical_not(bad)
451 if not numpy.any(good):
452 raise RuntimeError(
"No objects passed our cuts for consideration as psf stars")
454 mag = -2.5*numpy.log10(flux[good])
462 import pickle
as pickle
465 pickleFile = os.path.expanduser(os.path.join(
"~",
"widths-%d.pkl" % _ii))
466 if not os.path.exists(pickleFile):
470 with open(pickleFile,
"wb")
as fd:
471 pickle.dump(mag, fd, -1)
472 pickle.dump(width, fd, -1)
474 centers, clusterId = _kcenters(width, nCluster=4, useMedian=
True,
475 widthStdAllowed=self.config.widthStdAllowed)
477 if display
and plotMagSize:
478 fig =
plot(mag, width, centers, clusterId,
479 magType=self.config.sourceFluxField.split(
".")[-1].title(),
480 marker=
"+", markersize=3, markeredgewidth=
None, ltype=
':', clear=
True)
484 clusterId = _improveCluster(width, centers, clusterId,
485 nsigma=self.config.nSigmaClip,
486 widthStdAllowed=self.config.widthStdAllowed)
488 if display
and plotMagSize:
489 plot(mag, width, centers, clusterId, marker=
"x", markersize=3, markeredgewidth=
None, clear=
False)
491 stellar = (clusterId == 0)
498 if display
and displayExposure:
499 ds9.mtv(exposure.getMaskedImage(), frame=frame, title=
"PSF candidates")
502 eventHandler =
EventHandler(fig.get_axes()[0], mag, width,
503 sourceCat.getX()[good], sourceCat.getY()[good], frames=[frame])
509 reply = input(
"continue? [c h(elp) q(uit) p(db)] ").strip()
518 We cluster the points; red are the stellar candidates and the other colours are other clusters. 519 Points labelled + are rejects from the cluster (only for cluster 0). 521 At this prompt, you can continue with almost any key; 'p' enters pdb, and 'h' prints this text 523 If displayExposure is true, you can put the cursor on a point and hit 'p' to see it in ds9. 525 elif reply[0] ==
"p":
528 elif reply[0] ==
'q':
533 if display
and displayExposure:
534 mi = exposure.getMaskedImage()
536 with ds9.Buffering():
537 for i, source
in enumerate(sourceCat):
543 ds9.dot(
"+", source.getX() - mi.getX0(),
544 source.getY() - mi.getY0(), frame=frame, ctype=ctype)
550 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])
def selectSources(self, sourceCat, matches=None, exposure=None)