26 from functools
import reduce
29 from lsst.pipe.base
import Struct
35 from .sourceSelector
import BaseSourceSelectorTask, sourceSelectorRegistry
37 afwDisplay.setDefaultMaskTransparency(75)
41 doFluxLimit = pexConfig.Field(
42 doc=
"Apply flux limit to Psf Candidate selection?",
46 fluxMin = pexConfig.Field(
47 doc=
"specify the minimum psfFlux for good Psf Candidates",
50 check=
lambda x: x >= 0.0,
52 fluxMax = pexConfig.Field(
53 doc=
"specify the maximum psfFlux for good Psf Candidates (ignored if == 0)",
56 check=
lambda x: x >= 0.0,
58 doSignalToNoiseLimit = pexConfig.Field(
59 doc=
"Apply signal-to-noise (i.e. flux/fluxErr) limit to Psf Candidate selection?",
63 signalToNoiseMin = pexConfig.Field(
64 doc=
"specify the minimum signal-to-noise for good Psf Candidates",
67 check=
lambda x: x >= 0.0,
69 signalToNoiseMax = pexConfig.Field(
70 doc=
"specify the maximum signal-to-noise for good Psf Candidates (ignored if == 0)",
73 check=
lambda x: x >= 0.0,
75 widthMin = pexConfig.Field(
76 doc=
"minimum width to include in histogram",
79 check=
lambda x: x >= 0.0,
81 widthMax = pexConfig.Field(
82 doc=
"maximum width to include in histogram",
85 check=
lambda x: x >= 0.0,
87 sourceFluxField = pexConfig.Field(
88 doc=
"Name of field in Source to use for flux measurement",
90 default=
"base_GaussianFlux_instFlux",
92 widthStdAllowed = pexConfig.Field(
93 doc=
"Standard deviation of width allowed to be interpreted as good stars",
96 check=
lambda x: x >= 0.0,
98 nSigmaClip = pexConfig.Field(
99 doc=
"Keep objects within this many sigma of cluster 0's median",
102 check=
lambda x: x >= 0.0,
104 badFlags = pexConfig.ListField(
105 doc=
"List of flags which cause a source to be rejected as bad",
108 "base_PixelFlags_flag_edge",
109 "base_PixelFlags_flag_interpolatedCenter",
110 "base_PixelFlags_flag_saturatedCenter",
111 "base_PixelFlags_flag_crCenter",
112 "base_PixelFlags_flag_bad",
113 "base_PixelFlags_flag_interpolated",
118 BaseSourceSelectorTask.ConfigClass.validate(self)
120 msg = f
"widthMin ({self.widthMin}) > widthMax ({self.widthMax})"
121 raise pexConfig.FieldValidationError(ObjectSizeStarSelectorConfig.widthMin, self, msg)
125 """A class to handle key strokes with matplotlib displays.
128 def __init__(self, axes, xs, ys, x, y, frames=[0]):
136 self.
cidcid = self.
axesaxes.figure.canvas.mpl_connect(
'key_press_event', self)
139 if ev.inaxes != self.
axesaxes:
142 if ev.key
and ev.key
in (
"p"):
143 dist = numpy.hypot(self.
xsxs - ev.xdata, self.
ysys - ev.ydata)
144 dist[numpy.where(numpy.isnan(dist))] = 1e30
146 which = numpy.where(dist == min(dist))
148 x = self.
xx[which][0]
149 y = self.
yy[which][0]
150 for frame
in self.
framesframes:
151 disp = afwDisplay.Display(frame=frame)
158 def _assignClusters(yvec, centers):
159 """Return a vector of centerIds based on their distance to the centers.
161 assert len(centers) > 0
163 minDist = numpy.nan*numpy.ones_like(yvec)
164 clusterId = numpy.empty_like(yvec)
165 clusterId.dtype = int
166 dbl = Log.getLogger(
"objectSizeStarSelector._assignClusters")
167 dbl.setLevel(dbl.INFO)
170 oldSettings = numpy.seterr(all=
"warn")
171 with warnings.catch_warnings(record=
True)
as w:
172 warnings.simplefilter(
"always")
173 for i, mean
in enumerate(centers):
174 dist = abs(yvec - mean)
176 update = dist == dist
178 update = dist < minDist
180 dbl.trace(str(w[-1]))
182 minDist[update] = dist[update]
183 clusterId[update] = i
184 numpy.seterr(**oldSettings)
189 def _kcenters(yvec, nCluster, useMedian=False, widthStdAllowed=0.15):
190 """A classic k-means algorithm, clustering yvec into nCluster clusters
192 Return the set of centres, and the cluster ID for each of the points
194 If useMedian is true, use the median of the cluster as its centre, rather than
197 Serge Monkewitz points out that there other (maybe smarter) ways of seeding the means:
198 "e.g. why not use the Forgy or random partition initialization methods"
199 however, the approach adopted here seems to work well for the particular sorts of things
200 we're clustering in this application
205 mean0 = sorted(yvec)[len(yvec)//10]
206 delta = mean0 * widthStdAllowed * 2.0
207 centers = mean0 + delta * numpy.arange(nCluster)
209 func = numpy.median
if useMedian
else numpy.mean
211 clusterId = numpy.zeros_like(yvec) - 1
212 clusterId.dtype = int
214 oclusterId = clusterId
215 clusterId = _assignClusters(yvec, centers)
217 if numpy.all(clusterId == oclusterId):
220 for i
in range(nCluster):
222 pointsInCluster = (clusterId == i)
223 if numpy.any(pointsInCluster):
224 centers[i] = func(yvec[pointsInCluster])
226 centers[i] = numpy.nan
228 return centers, clusterId
231 def _improveCluster(yvec, centers, clusterId, nsigma=2.0, nIteration=10, clusterNum=0, widthStdAllowed=0.15):
232 """Improve our estimate of one of the clusters (clusterNum) by sigma-clipping around its median.
235 nMember = sum(clusterId == clusterNum)
238 for iter
in range(nIteration):
239 old_nMember = nMember
241 inCluster0 = clusterId == clusterNum
242 yv = yvec[inCluster0]
244 centers[clusterNum] = numpy.median(yv)
245 stdev = numpy.std(yv)
248 stdev_iqr = 0.741*(syv[int(0.75*nMember)] - syv[int(0.25*nMember)])
249 median = syv[int(0.5*nMember)]
251 sd = stdev
if stdev < stdev_iqr
else stdev_iqr
254 print(
"sigma(iqr) = %.3f, sigma = %.3f" % (stdev_iqr, numpy.std(yv)))
255 newCluster0 = abs(yvec - centers[clusterNum]) < nsigma*sd
256 clusterId[numpy.logical_and(inCluster0, newCluster0)] = clusterNum
257 clusterId[numpy.logical_and(inCluster0, numpy.logical_not(newCluster0))] = -1
259 nMember = sum(clusterId == clusterNum)
261 if nMember == old_nMember
or sd < widthStdAllowed * median:
267 def plot(mag, width, centers, clusterId, marker="o", markersize=2, markeredgewidth=0, ltype='-',
268 magType="model", clear=True):
270 log = Log.getLogger(
"objectSizeStarSelector.plot")
272 import matplotlib.pyplot
as plt
273 except ImportError
as e:
274 log.warning(
"Unable to import matplotlib: %s", e)
285 axes = fig.add_axes((0.1, 0.1, 0.85, 0.80))
287 xmin = sorted(mag)[int(0.05*len(mag))]
288 xmax = sorted(mag)[int(0.95*len(mag))]
290 axes.set_xlim(-17.5, -13)
291 axes.set_xlim(xmin - 0.1*(xmax - xmin), xmax + 0.1*(xmax - xmin))
294 colors = [
"r",
"g",
"b",
"c",
"m",
"k", ]
295 for k, mean
in enumerate(centers):
297 axes.plot(axes.get_xlim(), (mean, mean,),
"k%s" % ltype)
299 li = (clusterId == k)
300 axes.plot(mag[li], width[li], marker, markersize=markersize, markeredgewidth=markeredgewidth,
301 color=colors[k % len(colors)])
303 li = (clusterId == -1)
304 axes.plot(mag[li], width[li], marker, markersize=markersize, markeredgewidth=markeredgewidth,
308 axes.set_xlabel(
"Instrumental %s mag" % magType)
309 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 ConfigClass = ObjectSizeStarSelectorConfig
322 """Return a selection of PSF candidates that represent likely stars.
324 A list of PSF candidates may be used by a PSF fitter to construct a PSF.
328 sourceCat : `lsst.afw.table.SourceCatalog`
329 Catalog of sources to select from.
330 This catalog must be contiguous in memory.
331 matches : `list` of `lsst.afw.table.ReferenceMatch` or None
332 Ignored in this SourceSelector.
333 exposure : `lsst.afw.image.Exposure` or None
334 The exposure the catalog was built from; used to get the detector
335 to transform to TanPix, and for debug display.
339 struct : `lsst.pipe.base.Struct`
340 The struct contains the following data:
342 - selected : `array` of `bool``
343 Boolean array of sources that were selected, same length as
355 detector = exposure.getDetector()
357 pixToTanPix = detector.getTransform(PIXELS, TAN_PIXELS)
361 flux = sourceCat.get(self.config.sourceFluxField)
362 fluxErr = sourceCat.get(self.config.sourceFluxField +
"Err")
364 xx = numpy.empty(len(sourceCat))
365 xy = numpy.empty_like(xx)
366 yy = numpy.empty_like(xx)
367 for i, source
in enumerate(sourceCat):
368 Ixx, Ixy, Iyy = source.getIxx(), source.getIxy(), source.getIyy()
371 linTransform = afwGeom.linearizeTransform(pixToTanPix, p).getLinear()
372 m = afwGeom.Quadrupole(Ixx, Iyy, Ixy)
373 m.transform(linTransform)
374 Ixx, Iyy, Ixy = m.getIxx(), m.getIyy(), m.getIxy()
376 xx[i], xy[i], yy[i] = Ixx, Ixy, Iyy
378 width = numpy.sqrt(0.5*(xx + yy))
379 with numpy.errstate(invalid=
"ignore"):
380 bad = reduce(
lambda x, y: numpy.logical_or(x, sourceCat.get(y)), self.config.badFlags,
False)
381 bad = numpy.logical_or(bad, numpy.logical_not(numpy.isfinite(width)))
382 bad = numpy.logical_or(bad, numpy.logical_not(numpy.isfinite(flux)))
383 if self.config.doFluxLimit:
384 bad = numpy.logical_or(bad, flux < self.config.fluxMin)
385 if self.config.fluxMax > 0:
386 bad = numpy.logical_or(bad, flux > self.config.fluxMax)
387 if self.config.doSignalToNoiseLimit:
388 bad = numpy.logical_or(bad, flux/fluxErr < self.config.signalToNoiseMin)
389 if self.config.signalToNoiseMax > 0:
390 bad = numpy.logical_or(bad, flux/fluxErr > self.config.signalToNoiseMax)
391 bad = numpy.logical_or(bad, width < self.config.widthMin)
392 bad = numpy.logical_or(bad, width > self.config.widthMax)
393 good = numpy.logical_not(bad)
395 if not numpy.any(good):
396 raise RuntimeError(
"No objects passed our cuts for consideration as psf stars")
398 mag = -2.5*numpy.log10(flux[good])
406 import pickle
as pickle
409 pickleFile = os.path.expanduser(os.path.join(
"~",
"widths-%d.pkl" % _ii))
410 if not os.path.exists(pickleFile):
414 with open(pickleFile,
"wb")
as fd:
415 pickle.dump(mag, fd, -1)
416 pickle.dump(width, fd, -1)
418 centers, clusterId = _kcenters(width, nCluster=4, useMedian=
True,
419 widthStdAllowed=self.config.widthStdAllowed)
421 if display
and plotMagSize:
422 fig =
plot(mag, width, centers, clusterId,
423 magType=self.config.sourceFluxField.split(
".")[-1].title(),
424 marker=
"+", markersize=3, markeredgewidth=
None, ltype=
':', clear=
True)
428 clusterId = _improveCluster(width, centers, clusterId,
429 nsigma=self.config.nSigmaClip,
430 widthStdAllowed=self.config.widthStdAllowed)
432 if display
and plotMagSize:
433 plot(mag, width, centers, clusterId, marker=
"x", markersize=3, markeredgewidth=
None, clear=
False)
435 stellar = (clusterId == 0)
442 if display
and displayExposure:
443 disp = afwDisplay.Display(frame=frame)
444 disp.mtv(exposure.getMaskedImage(), 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 the
471 elif reply[0] ==
"p":
474 elif reply[0] ==
'q':
479 if display
and displayExposure:
480 mi = exposure.getMaskedImage()
481 with disp.Buffering():
482 for i, source
in enumerate(sourceCat):
484 ctype = afwDisplay.GREEN
486 ctype = afwDisplay.RED
488 disp.dot(
"+", source.getX() - mi.getX0(), source.getY() - mi.getY0(), ctype=ctype)
494 return Struct(selected=good)
def __init__(self, axes, xs, ys, x, y, frames=[0])
def selectSources(self, sourceCat, matches=None, exposure=None)
def plot(mag, width, centers, clusterId, marker="o", markersize=2, markeredgewidth=0, ltype='-', magType="model", clear=True)