22 __all__ = [
"ObjectSizeStarSelectorConfig", 
"ObjectSizeStarSelectorTask"]
 
   28 from functools 
import reduce
 
   30 from lsst.utils.logging 
import getLogger
 
   37 from .sourceSelector 
import BaseSourceSelectorTask, sourceSelectorRegistry
 
   39 afwDisplay.setDefaultMaskTransparency(75)
 
   45     doFluxLimit = pexConfig.Field(
 
   46         doc=
"Apply flux limit to Psf Candidate selection?",
 
   50     fluxMin = pexConfig.Field(
 
   51         doc=
"specify the minimum psfFlux for good Psf Candidates",
 
   54         check=
lambda x: x >= 0.0,
 
   56     fluxMax = pexConfig.Field(
 
   57         doc=
"specify the maximum psfFlux for good Psf Candidates (ignored if == 0)",
 
   60         check=
lambda x: x >= 0.0,
 
   62     doSignalToNoiseLimit = pexConfig.Field(
 
   63         doc=
"Apply signal-to-noise (i.e. flux/fluxErr) limit to Psf Candidate selection?",
 
   70     signalToNoiseMin = pexConfig.Field(
 
   71         doc=
"specify the minimum signal-to-noise for good Psf Candidates " 
   72             "(value should take into consideration the detection thresholds " 
   73             "set for the catalog of interest)",
 
   76         check=
lambda x: x >= 0.0,
 
   78     signalToNoiseMax = pexConfig.Field(
 
   79         doc=
"specify the maximum signal-to-noise for good Psf Candidates (ignored if == 0)",
 
   82         check=
lambda x: x >= 0.0,
 
   84     widthMin = pexConfig.Field(
 
   85         doc=
"minimum width to include in histogram",
 
   88         check=
lambda x: x >= 0.0,
 
   90     widthMax = pexConfig.Field(
 
   91         doc=
"maximum width to include in histogram",
 
   94         check=
lambda x: x >= 0.0,
 
   96     sourceFluxField = pexConfig.Field(
 
   97         doc=
"Name of field in Source to use for flux measurement",
 
   99         default=
"base_GaussianFlux_instFlux",
 
  101     widthStdAllowed = pexConfig.Field(
 
  102         doc=
"Standard deviation of width allowed to be interpreted as good stars",
 
  105         check=
lambda x: x >= 0.0,
 
  107     nSigmaClip = pexConfig.Field(
 
  108         doc=
"Keep objects within this many sigma of cluster 0's median",
 
  111         check=
lambda x: x >= 0.0,
 
  113     badFlags = pexConfig.ListField(
 
  114         doc=
"List of flags which cause a source to be rejected as bad",
 
  117             "base_PixelFlags_flag_edge",
 
  118             "base_PixelFlags_flag_interpolatedCenter",
 
  119             "base_PixelFlags_flag_saturatedCenter",
 
  120             "base_PixelFlags_flag_crCenter",
 
  121             "base_PixelFlags_flag_bad",
 
  122             "base_PixelFlags_flag_interpolated",
 
  127         BaseSourceSelectorTask.ConfigClass.validate(self)
 
  129             msg = f
"widthMin ({self.widthMin}) > widthMax ({self.widthMax})" 
  130             raise pexConfig.FieldValidationError(ObjectSizeStarSelectorConfig.widthMin, self, msg)
 
  134     """A class to handle key strokes with matplotlib displays. 
  137     def __init__(self, axes, xs, ys, x, y, frames=[0]):
 
  145         self.
cidcid = self.
axesaxes.figure.canvas.mpl_connect(
'key_press_event', self)
 
  148         if ev.inaxes != self.
axesaxes:
 
  151         if ev.key 
and ev.key 
in (
"p"):
 
  152             dist = numpy.hypot(self.
xsxs - ev.xdata, self.
ysys - ev.ydata)
 
  153             dist[numpy.where(numpy.isnan(dist))] = 1e30
 
  155             which = numpy.where(dist == 
min(dist))
 
  157             x = self.
xx[which][0]
 
  158             y = self.
yy[which][0]
 
  159             for frame 
in self.
framesframes:
 
  160                 disp = afwDisplay.Display(frame=frame)
 
  167 def _assignClusters(yvec, centers):
 
  168     """Return a vector of centerIds based on their distance to the centers. 
  170     assert len(centers) > 0
 
  172     minDist = numpy.nan*numpy.ones_like(yvec)
 
  173     clusterId = numpy.empty_like(yvec)
 
  174     clusterId.dtype = int               
 
  175     dbl = _LOG.getChild(
"_assignClusters")
 
  176     dbl.setLevel(dbl.INFO)
 
  179     oldSettings = numpy.seterr(all=
"warn")
 
  180     with warnings.catch_warnings(record=
True) 
as w:
 
  181         warnings.simplefilter(
"always")
 
  182         for i, mean 
in enumerate(centers):
 
  183             dist = 
abs(yvec - mean)
 
  185                 update = dist == dist       
 
  187                 update = dist < minDist
 
  189                     dbl.trace(str(w[-1]))
 
  191             minDist[update] = dist[update]
 
  192             clusterId[update] = i
 
  193     numpy.seterr(**oldSettings)
 
  198 def _kcenters(yvec, nCluster, useMedian=False, widthStdAllowed=0.15):
 
  199     """A classic k-means algorithm, clustering yvec into nCluster clusters 
  201     Return the set of centres, and the cluster ID for each of the points 
  203     If useMedian is true, use the median of the cluster as its centre, rather than 
  206     Serge Monkewitz points out that there other (maybe smarter) ways of seeding the means: 
  207        "e.g. why not use the Forgy or random partition initialization methods" 
  208     however, the approach adopted here seems to work well for the particular sorts of things 
  209     we're clustering in this application 
  214     mean0 = sorted(yvec)[len(yvec)//10]  
 
  215     delta = mean0 * widthStdAllowed * 2.0
 
  216     centers = mean0 + delta * numpy.arange(nCluster)
 
  218     func = numpy.median 
if useMedian 
else numpy.mean
 
  220     clusterId = numpy.zeros_like(yvec) - 1            
 
  221     clusterId.dtype = int                             
 
  223         oclusterId = clusterId
 
  224         clusterId = _assignClusters(yvec, centers)
 
  226         if numpy.all(clusterId == oclusterId):
 
  229         for i 
in range(nCluster):
 
  231             pointsInCluster = (clusterId == i)
 
  232             if numpy.any(pointsInCluster):
 
  233                 centers[i] = func(yvec[pointsInCluster])
 
  235                 centers[i] = numpy.nan
 
  237     return centers, clusterId
 
  240 def _improveCluster(yvec, centers, clusterId, nsigma=2.0, nIteration=10, clusterNum=0, widthStdAllowed=0.15):
 
  241     """Improve our estimate of one of the clusters (clusterNum) by sigma-clipping around its median. 
  244     nMember = sum(clusterId == clusterNum)
 
  247     for iter 
in range(nIteration):
 
  248         old_nMember = nMember
 
  250         inCluster0 = clusterId == clusterNum
 
  251         yv = yvec[inCluster0]
 
  253         centers[clusterNum] = numpy.median(yv)
 
  254         stdev = numpy.std(yv)
 
  257         stdev_iqr = 0.741*(syv[int(0.75*nMember)] - syv[int(0.25*nMember)])
 
  258         median = syv[int(0.5*nMember)]
 
  260         sd = stdev 
if stdev < stdev_iqr 
else stdev_iqr
 
  263             print(
"sigma(iqr) = %.3f, sigma = %.3f" % (stdev_iqr, numpy.std(yv)))
 
  264         newCluster0 = 
abs(yvec - centers[clusterNum]) < nsigma*sd
 
  265         clusterId[numpy.logical_and(inCluster0, newCluster0)] = clusterNum
 
  266         clusterId[numpy.logical_and(inCluster0, numpy.logical_not(newCluster0))] = -1
 
  268         nMember = sum(clusterId == clusterNum)
 
  270         if nMember == old_nMember 
or sd < widthStdAllowed * median:
 
  276 def plot(mag, width, centers, clusterId, marker="o", markersize=2, markeredgewidth=0, ltype='-',
 
  277          magType="model", clear=True):
 
  279     log = _LOG.getChild(
"plot")
 
  281         import matplotlib.pyplot 
as plt
 
  282     except ImportError 
as e:
 
  283         log.warning(
"Unable to import matplotlib: %s", e)
 
  294     axes = fig.add_axes((0.1, 0.1, 0.85, 0.80))
 
  296     xmin = sorted(mag)[int(0.05*len(mag))]
 
  297     xmax = sorted(mag)[int(0.95*len(mag))]
 
  299     axes.set_xlim(-17.5, -13)
 
  300     axes.set_xlim(xmin - 0.1*(xmax - xmin), xmax + 0.1*(xmax - xmin))
 
  303     colors = [
"r", 
"g", 
"b", 
"c", 
"m", 
"k", ]
 
  304     for k, mean 
in enumerate(centers):
 
  306             axes.plot(axes.get_xlim(), (mean, mean,), 
"k%s" % ltype)
 
  308         li = (clusterId == k)
 
  309         axes.plot(mag[li], width[li], marker, markersize=markersize, markeredgewidth=markeredgewidth,
 
  310                   color=colors[k % len(colors)])
 
  312     li = (clusterId == -1)
 
  313     axes.plot(mag[li], width[li], marker, markersize=markersize, markeredgewidth=markeredgewidth,
 
  317         axes.set_xlabel(
"Instrumental %s mag" % magType)
 
  318         axes.set_ylabel(
r"$\sqrt{(I_{xx} + I_{yy})/2}$")
 
  323 @pexConfig.registerConfigurable("objectSize", sourceSelectorRegistry) 
  325     r"""A star selector that looks for a cluster of small objects in a size-magnitude plot. 
  327     ConfigClass = ObjectSizeStarSelectorConfig
 
  331         """Return a selection of PSF candidates that represent likely stars. 
  333         A list of PSF candidates may be used by a PSF fitter to construct a PSF. 
  337         sourceCat : `lsst.afw.table.SourceCatalog` 
  338             Catalog of sources to select from. 
  339             This catalog must be contiguous in memory. 
  340         matches : `list` of `lsst.afw.table.ReferenceMatch` or None 
  341             Ignored in this SourceSelector. 
  342         exposure : `lsst.afw.image.Exposure` or None 
  343             The exposure the catalog was built from; used to get the detector 
  344             to transform to TanPix, and for debug display. 
  348         struct : `lsst.pipe.base.Struct` 
  349             The struct contains the following data: 
  351             - selected : `array` of `bool`` 
  352                 Boolean array of sources that were selected, same length as 
  364             detector = exposure.getDetector()
 
  366             pixToTanPix = detector.getTransform(PIXELS, TAN_PIXELS)
 
  370         flux = sourceCat.get(self.config.sourceFluxField)
 
  371         fluxErr = sourceCat.get(self.config.sourceFluxField + 
"Err")
 
  373         xx = numpy.empty(len(sourceCat))
 
  374         xy = numpy.empty_like(xx)
 
  375         yy = numpy.empty_like(xx)
 
  376         for i, source 
in enumerate(sourceCat):
 
  377             Ixx, Ixy, Iyy = source.getIxx(), source.getIxy(), source.getIyy()
 
  382                 m.transform(linTransform)
 
  383                 Ixx, Iyy, Ixy = m.getIxx(), m.getIyy(), m.getIxy()
 
  385             xx[i], xy[i], yy[i] = Ixx, Ixy, Iyy
 
  387         width = numpy.sqrt(0.5*(xx + yy))
 
  388         with numpy.errstate(invalid=
"ignore"):  
 
  389             bad = reduce(
lambda x, y: numpy.logical_or(x, sourceCat.get(y)), self.config.badFlags, 
False)
 
  390             bad = numpy.logical_or(bad, numpy.logical_not(numpy.isfinite(width)))
 
  391             bad = numpy.logical_or(bad, numpy.logical_not(numpy.isfinite(flux)))
 
  392             if self.config.doFluxLimit:
 
  393                 bad = numpy.logical_or(bad, flux < self.config.fluxMin)
 
  394                 if self.config.fluxMax > 0:
 
  395                     bad = numpy.logical_or(bad, flux > self.config.fluxMax)
 
  396             if self.config.doSignalToNoiseLimit:
 
  397                 bad = numpy.logical_or(bad, flux/fluxErr < self.config.signalToNoiseMin)
 
  398                 if self.config.signalToNoiseMax > 0:
 
  399                     bad = numpy.logical_or(bad, flux/fluxErr > self.config.signalToNoiseMax)
 
  400             bad = numpy.logical_or(bad, width < self.config.widthMin)
 
  401             bad = numpy.logical_or(bad, width > self.config.widthMax)
 
  402         good = numpy.logical_not(bad)
 
  404         if not numpy.any(good):
 
  405             raise RuntimeError(
"No objects passed our cuts for consideration as psf stars")
 
  407         mag = -2.5*numpy.log10(flux[good])
 
  415             import pickle 
as pickle
 
  418                 pickleFile = os.path.expanduser(os.path.join(
"~", 
"widths-%d.pkl" % _ii))
 
  419                 if not os.path.exists(pickleFile):
 
  423             with open(pickleFile, 
"wb") 
as fd:
 
  424                 pickle.dump(mag, fd, -1)
 
  425                 pickle.dump(width, fd, -1)
 
  427         centers, clusterId = _kcenters(width, nCluster=4, useMedian=
True,
 
  428                                        widthStdAllowed=self.config.widthStdAllowed)
 
  430         if display 
and plotMagSize:
 
  431             fig = 
plot(mag, width, centers, clusterId,
 
  432                        magType=self.config.sourceFluxField.split(
".")[-1].title(),
 
  433                        marker=
"+", markersize=3, markeredgewidth=
None, ltype=
':', clear=
True)
 
  437         clusterId = _improveCluster(width, centers, clusterId,
 
  438                                     nsigma=self.config.nSigmaClip,
 
  439                                     widthStdAllowed=self.config.widthStdAllowed)
 
  441         if display 
and plotMagSize:
 
  442             plot(mag, width, centers, clusterId, marker=
"x", markersize=3, markeredgewidth=
None, clear=
False)
 
  444         stellar = (clusterId == 0)
 
  451             if display 
and displayExposure:
 
  452                 disp = afwDisplay.Display(frame=frame)
 
  453                 disp.mtv(exposure.getMaskedImage(), title=
"PSF candidates")
 
  456                 eventHandler = 
EventHandler(fig.get_axes()[0], mag, width,
 
  457                                             sourceCat.getX()[good], sourceCat.getY()[good], frames=[frame])
 
  463                     reply = input(
"continue? [c h(elp) q(uit) p(db)] ").
strip()
 
  472     We cluster the points; red are the stellar candidates and the other colours are other clusters. 
  473     Points labelled + are rejects from the cluster (only for cluster 0). 
  475     At this prompt, you can continue with almost any key; 'p' enters pdb, and 'h' prints this text 
  477     If displayExposure is true, you can put the cursor on a point and hit 'p' to see it in the 
  480                     elif reply[0] == 
"p":
 
  483                     elif reply[0] == 
'q':
 
  488         if display 
and displayExposure:
 
  489             mi = exposure.getMaskedImage()
 
  490             with disp.Buffering():
 
  491                 for i, source 
in enumerate(sourceCat):
 
  493                         ctype = afwDisplay.GREEN  
 
  495                         ctype = afwDisplay.RED  
 
  497                     disp.dot(
"+", source.getX() - mi.getX0(), source.getY() - mi.getY0(), ctype=ctype)
 
  503         return Struct(selected=good)
 
An ellipse core with quadrupole moments as parameters.
 
def __init__(self, axes, xs, ys, x, y, frames=[0])
 
def selectSources(self, sourceCat, matches=None, exposure=None)
 
lsst::geom::AffineTransform linearizeTransform(TransformPoint2ToPoint2 const &original, lsst::geom::Point2D const &inPoint)
Approximate a Transform by its local linearization.
 
def getLogger(loggername)
 
def plot(mag, width, centers, clusterId, marker="o", markersize=2, markeredgewidth=0, ltype='-', magType="model", clear=True)
 
Angle abs(Angle const &a)