27 from functools
import reduce
36 from .sourceSelector
import BaseSourceSelectorTask, sourceSelectorRegistry
40 fluxMin = pexConfig.Field(
41 doc=
"specify the minimum psfFlux for good Psf Candidates",
44 check=
lambda x: x >= 0.0,
46 fluxMax = pexConfig.Field(
47 doc=
"specify the maximum psfFlux for good Psf Candidates (ignored if == 0)",
50 check=
lambda x: x >= 0.0,
52 widthMin = pexConfig.Field(
53 doc=
"minimum width to include in histogram",
56 check=
lambda x: x >= 0.0,
58 widthMax = pexConfig.Field(
59 doc=
"maximum width to include in histogram",
62 check=
lambda x: x >= 0.0,
64 sourceFluxField = pexConfig.Field(
65 doc=
"Name of field in Source to use for flux measurement",
67 default=
"base_GaussianFlux_instFlux",
69 widthStdAllowed = pexConfig.Field(
70 doc=
"Standard deviation of width allowed to be interpreted as good stars",
73 check=
lambda x: x >= 0.0,
75 nSigmaClip = pexConfig.Field(
76 doc=
"Keep objects within this many sigma of cluster 0's median",
79 check=
lambda x: x >= 0.0,
81 badFlags = pexConfig.ListField(
82 doc=
"List of flags which cause a source to be rejected as bad",
85 "base_PixelFlags_flag_edge",
86 "base_PixelFlags_flag_interpolatedCenter",
87 "base_PixelFlags_flag_saturatedCenter",
88 "base_PixelFlags_flag_crCenter",
89 "base_PixelFlags_flag_bad",
90 "base_PixelFlags_flag_interpolated",
95 BaseSourceSelectorTask.ConfigClass.validate(self)
97 raise pexConfig.FieldValidationError(
"widthMin (%f) > widthMax (%f)" 102 """A class to handle key strokes with matplotlib displays""" 104 def __init__(self, axes, xs, ys, x, y, frames=[0]):
112 self.
cid = self.
axes.figure.canvas.mpl_connect(
'key_press_event', self)
115 if ev.inaxes != self.
axes:
118 if ev.key
and ev.key
in (
"p"):
119 dist = numpy.hypot(self.
xs - ev.xdata, self.
ys - ev.ydata)
120 dist[numpy.where(numpy.isnan(dist))] = 1e30
122 which = numpy.where(dist == min(dist))
127 ds9.pan(x, y, frame=frame)
128 ds9.cmdBuffer.flush()
133 def _assignClusters(yvec, centers):
134 """Return a vector of centerIds based on their distance to the centers""" 135 assert len(centers) > 0
137 minDist = numpy.nan*numpy.ones_like(yvec)
138 clusterId = numpy.empty_like(yvec)
139 clusterId.dtype = int
140 dbl = Log.getLogger(
"objectSizeStarSelector._assignClusters")
141 dbl.setLevel(dbl.INFO)
144 oldSettings = numpy.seterr(all=
"warn")
145 with warnings.catch_warnings(record=
True)
as w:
146 warnings.simplefilter(
"always")
147 for i, mean
in enumerate(centers):
148 dist = abs(yvec - mean)
150 update = dist == dist
152 update = dist < minDist
154 dbl.trace(str(w[-1]))
156 minDist[update] = dist[update]
157 clusterId[update] = i
158 numpy.seterr(**oldSettings)
163 def _kcenters(yvec, nCluster, useMedian=False, widthStdAllowed=0.15):
164 """A classic k-means algorithm, clustering yvec into nCluster clusters 166 Return the set of centres, and the cluster ID for each of the points 168 If useMedian is true, use the median of the cluster as its centre, rather than 171 Serge Monkewitz points out that there other (maybe smarter) ways of seeding the means: 172 "e.g. why not use the Forgy or random partition initialization methods" 173 however, the approach adopted here seems to work well for the particular sorts of things 174 we're clustering in this application 179 mean0 = sorted(yvec)[len(yvec)//10]
180 delta = mean0 * widthStdAllowed * 2.0
181 centers = mean0 + delta * numpy.arange(nCluster)
183 func = numpy.median
if useMedian
else numpy.mean
185 clusterId = numpy.zeros_like(yvec) - 1
186 clusterId.dtype = int
188 oclusterId = clusterId
189 clusterId = _assignClusters(yvec, centers)
191 if numpy.all(clusterId == oclusterId):
194 for i
in range(nCluster):
196 pointsInCluster = (clusterId == i)
197 if numpy.any(pointsInCluster):
198 centers[i] = func(yvec[pointsInCluster])
200 centers[i] = numpy.nan
202 return centers, clusterId
205 def _improveCluster(yvec, centers, clusterId, nsigma=2.0, nIteration=10, clusterNum=0, widthStdAllowed=0.15):
206 """Improve our estimate of one of the clusters (clusterNum) by sigma-clipping around its median""" 208 nMember = sum(clusterId == clusterNum)
211 for iter
in range(nIteration):
212 old_nMember = nMember
214 inCluster0 = clusterId == clusterNum
215 yv = yvec[inCluster0]
217 centers[clusterNum] = numpy.median(yv)
218 stdev = numpy.std(yv)
221 stdev_iqr = 0.741*(syv[int(0.75*nMember)] - syv[int(0.25*nMember)])
222 median = syv[int(0.5*nMember)]
224 sd = stdev
if stdev < stdev_iqr
else stdev_iqr
227 print(
"sigma(iqr) = %.3f, sigma = %.3f" % (stdev_iqr, numpy.std(yv)))
228 newCluster0 = abs(yvec - centers[clusterNum]) < nsigma*sd
229 clusterId[numpy.logical_and(inCluster0, newCluster0)] = clusterNum
230 clusterId[numpy.logical_and(inCluster0, numpy.logical_not(newCluster0))] = -1
232 nMember = sum(clusterId == clusterNum)
234 if nMember == old_nMember
or sd < widthStdAllowed * median:
240 def plot(mag, width, centers, clusterId, marker="o", markersize=2, markeredgewidth=0, ltype='-',
241 magType="model", clear=True):
243 log = Log.getLogger(
"objectSizeStarSelector.plot")
245 import matplotlib.pyplot
as plt
246 except ImportError
as e:
247 log.warn(
"Unable to import matplotlib: %s", e)
257 axes = fig.add_axes((0.1, 0.1, 0.85, 0.80))
259 xmin = sorted(mag)[int(0.05*len(mag))]
260 xmax = sorted(mag)[int(0.95*len(mag))]
262 axes.set_xlim(-17.5, -13)
263 axes.set_xlim(xmin - 0.1*(xmax - xmin), xmax + 0.1*(xmax - xmin))
266 colors = [
"r", "g", "b", "c", "m", "k", ]
267 for k, mean
in enumerate(centers):
269 axes.plot(axes.get_xlim(), (mean, mean,),
"k%s" % ltype)
271 li = (clusterId == k)
272 axes.plot(mag[li], width[li], marker, markersize=markersize, markeredgewidth=markeredgewidth,
273 color=colors[k % len(colors)])
275 li = (clusterId == -1)
276 axes.plot(mag[li], width[li], marker, markersize=markersize, markeredgewidth=markeredgewidth,
280 axes.set_xlabel(
"Instrumental %s mag" % magType)
281 axes.set_ylabel(
r"$\sqrt{(I_{xx} + I_{yy})/2}$")
293 @pexConfig.registerConfigurable(
"objectSize", sourceSelectorRegistry)
295 """!A star selector that looks for a cluster of small objects in a size-magnitude plot 297 @anchor ObjectSizeStarSelectorTask_ 299 @section meas_algorithms_objectSizeStarSelector_Contents Contents 301 - @ref meas_algorithms_objectSizeStarSelector_Purpose 302 - @ref meas_algorithms_objectSizeStarSelector_Initialize 303 - @ref meas_algorithms_objectSizeStarSelector_IO 304 - @ref meas_algorithms_objectSizeStarSelector_Config 305 - @ref meas_algorithms_objectSizeStarSelector_Debug 307 @section meas_algorithms_objectSizeStarSelector_Purpose Description 309 A star selector that looks for a cluster of small objects in a size-magnitude plot. 311 @section meas_algorithms_objectSizeStarSelector_Initialize Task initialisation 313 @copydoc \_\_init\_\_ 315 @section meas_algorithms_objectSizeStarSelector_IO Invoking the Task 317 Like all star selectors, the main method is `run`. 319 @section meas_algorithms_objectSizeStarSelector_Config Configuration parameters 321 See @ref ObjectSizeStarSelectorConfig 323 @section meas_algorithms_objectSizeStarSelector_Debug Debug variables 325 ObjectSizeStarSelectorTask has a debug dictionary with the following keys: 328 <dd>bool; if True display debug information 330 <dd>bool; if True display the exposure and spatial cells 332 <dd>bool: if True display the magnitude-size relation using matplotlib 334 <dd>bool; if True dump data to a pickle file 337 For example, put something like: 341 di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively 342 if name.endswith("objectSizeStarSelector"): 344 di.displayExposure = True 345 di.plotMagSize = True 349 lsstDebug.Info = DebugInfo 351 into your `debug.py` file and run your task with the `--debug` flag. 353 ConfigClass = ObjectSizeStarSelectorConfig
357 """Return a selection of PSF candidates that represent likely stars. 359 A list of PSF candidates may be used by a PSF fitter to construct a PSF. 363 sourceCat : `lsst.afw.table.SourceCatalog` 364 Catalog of sources to select from. 365 This catalog must be contiguous in memory. 366 matches : `list` of `lsst.afw.table.ReferenceMatch` or None 367 Ignored in this SourceSelector. 368 exposure : `lsst.afw.image.Exposure` or None 369 The exposure the catalog was built from; used to get the detector 370 to transform to TanPix, and for debug display. 374 struct : `lsst.pipe.base.Struct` 375 The struct contains the following data: 377 - selected : `array` of `bool`` 378 Boolean array of sources that were selected, same length as 387 detector = exposure.getDetector()
389 if detector
is not None:
390 pixToTanPix = detector.getTransform(PIXELS, TAN_PIXELS)
394 flux = sourceCat.get(self.config.sourceFluxField)
396 xx = numpy.empty(len(sourceCat))
397 xy = numpy.empty_like(xx)
398 yy = numpy.empty_like(xx)
399 for i, source
in enumerate(sourceCat):
400 Ixx, Ixy, Iyy = source.getIxx(), source.getIxy(), source.getIyy()
403 linTransform = afwGeom.linearizeTransform(pixToTanPix, p).getLinear()
404 m = afwGeom.Quadrupole(Ixx, Iyy, Ixy)
405 m.transform(linTransform)
406 Ixx, Iyy, Ixy = m.getIxx(), m.getIyy(), m.getIxy()
408 xx[i], xy[i], yy[i] = Ixx, Ixy, Iyy
410 width = numpy.sqrt(0.5*(xx + yy))
411 with numpy.errstate(invalid=
"ignore"):
412 bad = reduce(
lambda x, y: numpy.logical_or(x, sourceCat.get(y)), self.config.badFlags,
False)
413 bad = numpy.logical_or(bad, flux < self.config.fluxMin)
414 bad = numpy.logical_or(bad, numpy.logical_not(numpy.isfinite(width)))
415 bad = numpy.logical_or(bad, numpy.logical_not(numpy.isfinite(flux)))
416 bad = numpy.logical_or(bad, width < self.config.widthMin)
417 bad = numpy.logical_or(bad, width > self.config.widthMax)
418 if self.config.fluxMax > 0:
419 bad = numpy.logical_or(bad, flux > self.config.fluxMax)
420 good = numpy.logical_not(bad)
422 if not numpy.any(good):
423 raise RuntimeError(
"No objects passed our cuts for consideration as psf stars")
425 mag = -2.5*numpy.log10(flux[good])
433 import pickle
as pickle
436 pickleFile = os.path.expanduser(os.path.join(
"~",
"widths-%d.pkl" % _ii))
437 if not os.path.exists(pickleFile):
441 with open(pickleFile,
"wb")
as fd:
442 pickle.dump(mag, fd, -1)
443 pickle.dump(width, fd, -1)
445 centers, clusterId = _kcenters(width, nCluster=4, useMedian=
True,
446 widthStdAllowed=self.config.widthStdAllowed)
448 if display
and plotMagSize:
449 fig =
plot(mag, width, centers, clusterId,
450 magType=self.config.sourceFluxField.split(
".")[-1].title(),
451 marker=
"+", markersize=3, markeredgewidth=
None, ltype=
':', clear=
True)
455 clusterId = _improveCluster(width, centers, clusterId,
456 nsigma=self.config.nSigmaClip,
457 widthStdAllowed=self.config.widthStdAllowed)
459 if display
and plotMagSize:
460 plot(mag, width, centers, clusterId, marker=
"x", markersize=3, markeredgewidth=
None, clear=
False)
462 stellar = (clusterId == 0)
469 if display
and displayExposure:
470 ds9.mtv(exposure.getMaskedImage(), frame=frame, title=
"PSF candidates")
473 eventHandler =
EventHandler(fig.get_axes()[0], mag, width,
474 sourceCat.getX()[good], sourceCat.getY()[good], frames=[frame])
480 reply = input(
"continue? [c h(elp) q(uit) p(db)] ").strip()
489 We cluster the points; red are the stellar candidates and the other colours are other clusters. 490 Points labelled + are rejects from the cluster (only for cluster 0). 492 At this prompt, you can continue with almost any key; 'p' enters pdb, and 'h' prints this text 494 If displayExposure is true, you can put the cursor on a point and hit 'p' to see it in ds9. 496 elif reply[0] ==
"p":
499 elif reply[0] ==
'q':
504 if display
and displayExposure:
505 mi = exposure.getMaskedImage()
507 with ds9.Buffering():
508 for i, source
in enumerate(sourceCat):
514 ds9.dot(
"+", source.getX() - mi.getX0(),
515 source.getY() - mi.getY0(), frame=frame, ctype=ctype)
521 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)