27 from functools
import reduce
36 from .starSelector
import BaseStarSelectorTask, starSelectorRegistry
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_flux",
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,
83 BaseStarSelectorTask.ConfigClass.validate(self)
85 raise pexConfig.FieldValidationError(
"widthMin (%f) > widthMax (%f)" 90 """A class to handle key strokes with matplotlib displays""" 92 def __init__(self, axes, xs, ys, x, y, frames=[0]):
100 self.
cid = self.
axes.figure.canvas.mpl_connect(
'key_press_event', self)
103 if ev.inaxes != self.
axes:
106 if ev.key
and ev.key
in (
"p"):
107 dist = numpy.hypot(self.
xs - ev.xdata, self.
ys - ev.ydata)
108 dist[numpy.where(numpy.isnan(dist))] = 1e30
110 which = numpy.where(dist == min(dist))
115 ds9.pan(x, y, frame=frame)
116 ds9.cmdBuffer.flush()
121 def _assignClusters(yvec, centers):
122 """Return a vector of centerIds based on their distance to the centers""" 123 assert len(centers) > 0
125 minDist = numpy.nan*numpy.ones_like(yvec)
126 clusterId = numpy.empty_like(yvec)
127 clusterId.dtype = int
128 dbl = Log.getLogger(
"objectSizeStarSelector._assignClusters")
129 dbl.setLevel(dbl.INFO)
132 oldSettings = numpy.seterr(all=
"warn")
133 with warnings.catch_warnings(record=
True)
as w:
134 warnings.simplefilter(
"always")
135 for i, mean
in enumerate(centers):
136 dist = abs(yvec - mean)
138 update = dist == dist
140 update = dist < minDist
142 dbl.trace(str(w[-1]))
144 minDist[update] = dist[update]
145 clusterId[update] = i
146 numpy.seterr(**oldSettings)
151 def _kcenters(yvec, nCluster, useMedian=False, widthStdAllowed=0.15):
152 """A classic k-means algorithm, clustering yvec into nCluster clusters 154 Return the set of centres, and the cluster ID for each of the points 156 If useMedian is true, use the median of the cluster as its centre, rather than 159 Serge Monkewitz points out that there other (maybe smarter) ways of seeding the means: 160 "e.g. why not use the Forgy or random partition initialization methods" 161 however, the approach adopted here seems to work well for the particular sorts of things 162 we're clustering in this application 167 mean0 = sorted(yvec)[len(yvec)//10]
168 delta = mean0 * widthStdAllowed * 2.0
169 centers = mean0 + delta * numpy.arange(nCluster)
171 func = numpy.median
if useMedian
else numpy.mean
173 clusterId = numpy.zeros_like(yvec) - 1
174 clusterId.dtype = int
176 oclusterId = clusterId
177 clusterId = _assignClusters(yvec, centers)
179 if numpy.all(clusterId == oclusterId):
182 for i
in range(nCluster):
184 pointsInCluster = (clusterId == i)
185 if numpy.any(pointsInCluster):
186 centers[i] = func(yvec[pointsInCluster])
188 centers[i] = numpy.nan
190 return centers, clusterId
193 def _improveCluster(yvec, centers, clusterId, nsigma=2.0, nIteration=10, clusterNum=0, widthStdAllowed=0.15):
194 """Improve our estimate of one of the clusters (clusterNum) by sigma-clipping around its median""" 196 nMember = sum(clusterId == clusterNum)
199 for iter
in range(nIteration):
200 old_nMember = nMember
202 inCluster0 = clusterId == clusterNum
203 yv = yvec[inCluster0]
205 centers[clusterNum] = numpy.median(yv)
206 stdev = numpy.std(yv)
209 stdev_iqr = 0.741*(syv[int(0.75*nMember)] - syv[int(0.25*nMember)])
210 median = syv[int(0.5*nMember)]
212 sd = stdev
if stdev < stdev_iqr
else stdev_iqr
215 print(
"sigma(iqr) = %.3f, sigma = %.3f" % (stdev_iqr, numpy.std(yv)))
216 newCluster0 = abs(yvec - centers[clusterNum]) < nsigma*sd
217 clusterId[numpy.logical_and(inCluster0, newCluster0)] = clusterNum
218 clusterId[numpy.logical_and(inCluster0, numpy.logical_not(newCluster0))] = -1
220 nMember = sum(clusterId == clusterNum)
222 if nMember == old_nMember
or sd < widthStdAllowed * median:
228 def plot(mag, width, centers, clusterId, marker="o", markersize=2, markeredgewidth=0, ltype='-',
229 magType="model", clear=True):
231 log = Log.getLogger(
"objectSizeStarSelector.plot")
233 import matplotlib.pyplot
as plt
234 except ImportError
as e:
235 log.warn(
"Unable to import matplotlib: %s", e)
245 axes = fig.add_axes((0.1, 0.1, 0.85, 0.80))
247 xmin = sorted(mag)[int(0.05*len(mag))]
248 xmax = sorted(mag)[int(0.95*len(mag))]
250 axes.set_xlim(-17.5, -13)
251 axes.set_xlim(xmin - 0.1*(xmax - xmin), xmax + 0.1*(xmax - xmin))
254 colors = [
"r", "g", "b", "c", "m", "k", ]
255 for k, mean
in enumerate(centers):
257 axes.plot(axes.get_xlim(), (mean, mean,),
"k%s" % ltype)
259 li = (clusterId == k)
260 axes.plot(mag[li], width[li], marker, markersize=markersize, markeredgewidth=markeredgewidth,
261 color=colors[k % len(colors)])
263 li = (clusterId == -1)
264 axes.plot(mag[li], width[li], marker, markersize=markersize, markeredgewidth=markeredgewidth,
268 axes.set_xlabel(
"Instrumental %s mag" % magType)
269 axes.set_ylabel(
r"$\sqrt{(I_{xx} + I_{yy})/2}$")
282 """!A star selector that looks for a cluster of small objects in a size-magnitude plot 284 @anchor ObjectSizeStarSelectorTask_ 286 @section meas_algorithms_objectSizeStarSelector_Contents Contents 288 - @ref meas_algorithms_objectSizeStarSelector_Purpose 289 - @ref meas_algorithms_objectSizeStarSelector_Initialize 290 - @ref meas_algorithms_objectSizeStarSelector_IO 291 - @ref meas_algorithms_objectSizeStarSelector_Config 292 - @ref meas_algorithms_objectSizeStarSelector_Debug 294 @section meas_algorithms_objectSizeStarSelector_Purpose Description 296 A star selector that looks for a cluster of small objects in a size-magnitude plot. 298 @section meas_algorithms_objectSizeStarSelector_Initialize Task initialisation 300 @copydoc \_\_init\_\_ 302 @section meas_algorithms_objectSizeStarSelector_IO Invoking the Task 304 Like all star selectors, the main method is `run`. 306 @section meas_algorithms_objectSizeStarSelector_Config Configuration parameters 308 See @ref ObjectSizeStarSelectorConfig 310 @section meas_algorithms_objectSizeStarSelector_Debug Debug variables 312 ObjectSizeStarSelectorTask has a debug dictionary with the following keys: 315 <dd>bool; if True display debug information 317 <dd>bool; if True display the exposure and spatial cells 319 <dd>bool: if True display the magnitude-size relation using matplotlib 321 <dd>bool; if True dump data to a pickle file 324 For example, put something like: 328 di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively 329 if name.endswith("objectSizeStarSelector"): 331 di.displayExposure = True 332 di.plotMagSize = True 336 lsstDebug.Info = DebugInfo 338 into your `debug.py` file and run your task with the `--debug` flag. 340 ConfigClass = ObjectSizeStarSelectorConfig
344 """!Return a list of PSF candidates that represent likely stars 346 A list of PSF candidates may be used by a PSF fitter to construct a PSF. 348 @param[in] exposure the exposure containing the sources 349 @param[in] sourceCat catalog of sources that may be stars (an lsst.afw.table.SourceCatalog) 350 @param[in] matches astrometric matches; ignored by this star selector 352 @return an lsst.pipe.base.Struct containing: 353 - starCat catalog of selected stars (a subset of sourceCat) 361 detector = exposure.getDetector()
363 if detector
is not None:
364 pixToTanPix = detector.getTransform(PIXELS, TAN_PIXELS)
368 flux = sourceCat.get(self.config.sourceFluxField)
370 xx = numpy.empty(len(sourceCat))
371 xy = numpy.empty_like(xx)
372 yy = numpy.empty_like(xx)
373 for i, source
in enumerate(sourceCat):
374 Ixx, Ixy, Iyy = source.getIxx(), source.getIxy(), source.getIyy()
378 m = afwGeom.Quadrupole(Ixx, Iyy, Ixy)
379 m.transform(linTransform)
380 Ixx, Iyy, Ixy = m.getIxx(), m.getIyy(), m.getIxy()
382 xx[i], xy[i], yy[i] = Ixx, Ixy, Iyy
384 width = numpy.sqrt(0.5*(xx + yy))
385 with numpy.errstate(invalid=
"ignore"):
386 bad = reduce(
lambda x, y: numpy.logical_or(x, sourceCat.get(y)), self.config.badFlags,
False)
387 bad = numpy.logical_or(bad, flux < self.config.fluxMin)
388 bad = numpy.logical_or(bad, numpy.logical_not(numpy.isfinite(width)))
389 bad = numpy.logical_or(bad, numpy.logical_not(numpy.isfinite(flux)))
390 bad = numpy.logical_or(bad, width < self.config.widthMin)
391 bad = numpy.logical_or(bad, width > self.config.widthMax)
392 if self.config.fluxMax > 0:
393 bad = numpy.logical_or(bad, flux > self.config.fluxMax)
394 good = numpy.logical_not(bad)
396 if not numpy.any(good):
397 raise RuntimeError(
"No objects passed our cuts for consideration as psf stars")
399 mag = -2.5*numpy.log10(flux[good])
407 import pickle
as pickle
410 pickleFile = os.path.expanduser(os.path.join(
"~",
"widths-%d.pkl" % _ii))
411 if not os.path.exists(pickleFile):
415 with open(pickleFile,
"wb")
as fd:
416 pickle.dump(mag, fd, -1)
417 pickle.dump(width, fd, -1)
419 centers, clusterId = _kcenters(width, nCluster=4, useMedian=
True,
420 widthStdAllowed=self.config.widthStdAllowed)
422 if display
and plotMagSize:
423 fig =
plot(mag, width, centers, clusterId,
424 magType=self.config.sourceFluxField.split(
".")[-1].title(),
425 marker=
"+", markersize=3, markeredgewidth=
None, ltype=
':', clear=
True)
429 clusterId = _improveCluster(width, centers, clusterId,
430 nsigma=self.config.nSigmaClip,
431 widthStdAllowed=self.config.widthStdAllowed)
433 if display
and plotMagSize:
434 plot(mag, width, centers, clusterId, marker=
"x", markersize=3, markeredgewidth=
None, clear=
False)
436 stellar = (clusterId == 0)
443 if display
and displayExposure:
444 ds9.mtv(exposure.getMaskedImage(), frame=frame, title=
"PSF candidates")
447 eventHandler =
EventHandler(fig.get_axes()[0], mag, width,
448 sourceCat.getX()[good], sourceCat.getY()[good], frames=[frame])
454 reply = input(
"continue? [c h(elp) q(uit) p(db)] ").strip()
463 We cluster the points; red are the stellar candidates and the other colours are other clusters. 464 Points labelled + are rejects from the cluster (only for cluster 0). 466 At this prompt, you can continue with almost any key; 'p' enters pdb, and 'h' prints this text 468 If displayExposure is true, you can put the cursor on a point and hit 'p' to see it in ds9. 470 elif reply[0] ==
"p":
473 elif reply[0] ==
'q':
478 if display
and displayExposure:
479 mi = exposure.getMaskedImage()
481 with ds9.Buffering():
482 for i, source
in enumerate(sourceCat):
488 ds9.dot(
"+", source.getX() - mi.getX0(),
489 source.getY() - mi.getY0(), frame=frame, ctype=ctype)
491 starCat = SourceCatalog(sourceCat.table)
492 goodSources = [s
for g, s
in zip(good, sourceCat)
if g]
493 for isStellar, source
in zip(stellar, goodSources):
495 starCat.append(source)
502 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.