Source code for cytoflow.operations.polygon

#!/usr/bin/env python3.4
# coding: latin-1

# (c) Massachusetts Institute of Technology 2015-2018
# (c) Brian Teague 2018-2019
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

'''
cytoflow.operations.polygon
---------------------------
'''

from traits.api import (HasStrictTraits, Str, CStr, List, Float, provides,
                        Instance, Bool, on_trait_change, Any,
                        Constant)

import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np

import cytoflow.utility as util
from cytoflow.views import ISelectionView, ScatterplotView

from .i_operation import IOperation
from .base_op_views import Op2DView

[docs]@provides(IOperation) class PolygonOp(HasStrictTraits): """ Apply a polygon gate to a cytometry experiment. Attributes ---------- name : Str The operation name. Used to name the new metadata field in the experiment that's created by :meth:`apply` xchannel, ychannel : Str The names of the x and y channels to apply the gate. xscale, yscale : {'linear', 'log', 'logicle'} (default = 'linear') The scales applied to the data before drawing the polygon. vertices : List((Float, Float)) The polygon verticies. An ordered list of 2-tuples, representing the x and y coordinates of the vertices. Notes ----- This module uses :meth:`matplotlib.path.Path` to represent the polygon, because membership testing is very fast. You can set the verticies by hand, I suppose, but it's much easier to use the interactive view you get from :meth:`default_view` to do so. Examples -------- .. plot:: :context: close-figs Make a little data set. >>> import cytoflow as flow >>> import_op = flow.ImportOp() >>> import_op.tubes = [flow.Tube(file = "Plate01/RFP_Well_A3.fcs", ... conditions = {'Dox' : 10.0}), ... flow.Tube(file = "Plate01/CFP_Well_A4.fcs", ... conditions = {'Dox' : 1.0})] >>> import_op.conditions = {'Dox' : 'float'} >>> ex = import_op.apply() Create and parameterize the operation. .. plot:: :context: close-figs >>> p = flow.PolygonOp(name = "Polygon", ... xchannel = "V2-A", ... ychannel = "Y2-A") >>> p.vertices = [(23.411982294776319, 5158.7027015021222), ... (102.22182270573683, 23124.058843387455), ... (510.94519955277201, 23124.058843387455), ... (1089.5215641232173, 3800.3424832180476), ... (340.56382570202402, 801.98947404942271), ... (65.42597937575897, 1119.3133482602157)] Show the default view. .. plot:: :context: close-figs >>> df = p.default_view(huefacet = "Dox", ... xscale = 'log', ... yscale = 'log') >>> df.plot(ex) .. note:: If you want to use the interactive default view in a Jupyter notebook, make sure you say ``%matplotlib notebook`` in the first cell (instead of ``%matplotlib inline`` or similar). Then call ``default_view()`` with ``interactive = True``:: df = p.default_view(huefacet = "Dox", xscale = 'log', yscale = 'log', interactive = True) df.plot(ex) Apply the gate, and show the result .. plot:: :context: close-figs >>> ex2 = p.apply(ex) >>> ex2.data.groupby('Polygon').size() Polygon False 15875 True 4125 dtype: int64 """ # traits id = Constant('edu.mit.synbio.cytoflow.operations.polygon') friendly_id = Constant("Polygon") name = CStr() xchannel = Str() ychannel = Str() vertices = List((Float, Float)) xscale = util.ScaleEnum() yscale = util.ScaleEnum() _selection_view = Instance('PolygonSelection', transient = True)
[docs] def apply(self, experiment): """Applies the threshold to an experiment. Parameters ---------- experiment : Experiment the old :class:`Experiment` to which this op is applied Returns ------- Experiment a new :class:'Experiment`, the same as ``old_experiment`` but with a new column of type `bool` with the same as the operation name. The bool is ``True`` if the event's measurement is within the polygon, and ``False`` otherwise. Raises ------ util.CytoflowOpError if for some reason the operation can't be applied to this experiment. The reason is in :attr:`.CytoflowOpError.args` """ if experiment is None: raise util.CytoflowOpError('experiment', "No experiment specified") if self.name in experiment.data.columns: raise util.CytoflowOpError('name', "{} is in the experiment already!" .format(self.name)) if self.name != util.sanitize_identifier(self.name): raise util.CytoflowOpError('name', "Name can only contain letters, numbers and underscores." .format(self.name)) if not self.xchannel: raise util.CytoflowOpError('xchannel', "Must specify an x channel") if not self.ychannel: raise util.CytoflowOpError('ychannel', "Must specify a y channel") if not self.xchannel in experiment.channels: raise util.CytoflowOpError('xchannel', "xchannel {0} is not in the experiment" .format(self.xchannel)) if not self.ychannel in experiment.channels: raise util.CytoflowOpError('ychannel', "ychannel {0} is not in the experiment" .format(self.ychannel)) if len(self.vertices) < 3: raise util.CytoflowOpError('vertices', "Must have at least 3 vertices") if any([len(x) != 2 for x in self.vertices]): return util.CytoflowOpError('vertices', "All vertices must be lists or tuples " "of length = 2") # make sure name got set! if not self.name: raise util.CytoflowOpError('name', "You have to set the Polygon gate's name " "before applying it!") # make sure old_experiment doesn't already have a column named self.name if(self.name in experiment.data.columns): raise util.CytoflowOpError('name', "Experiment already contains a column {0}" .format(self.name)) # there's a bit of a subtlety here: if the vertices were # selected with an interactive plot, and that plot had scaled # axes, we need to apply that scale function to both the # vertices and the data before looking for path membership xscale = util.scale_factory(self.xscale, experiment, channel = self.xchannel) yscale = util.scale_factory(self.yscale, experiment, channel = self.ychannel) vertices = [(xscale(x), yscale(y)) for (x, y) in self.vertices] data = experiment.data[[self.xchannel, self.ychannel]].copy() data[self.xchannel] = xscale(data[self.xchannel]) data[self.ychannel] = yscale(data[self.ychannel]) # use a matplotlib Path because testing for membership is a fast C fn. path = mpl.path.Path(np.array(vertices)) xy_data = data[[self.xchannel, self.ychannel]].values new_experiment = experiment.clone() new_experiment.add_condition(self.name, "bool", path.contains_points(xy_data)) new_experiment.history.append(self.clone_traits(transient = lambda _: True)) return new_experiment
[docs] def default_view(self, **kwargs): self._selection_view = PolygonSelection(op = self) self._selection_view.trait_set(**kwargs) return self._selection_view
[docs]@provides(ISelectionView) class PolygonSelection(Op2DView, ScatterplotView): """ Plots, and lets the user interact with, a 2D polygon selection. Attributes ---------- interactive : bool is this view interactive? Ie, can the user set the polygon verticies with mouse clicks? Examples -------- In a Jupyter notebook with `%matplotlib notebook` >>> s = flow.PolygonOp(xchannel = "V2-A", ... ychannel = "Y2-A") >>> poly = s.default_view() >>> poly.plot(ex2) >>> poly.interactive = True """ id = Constant('edu.mit.synbio.cytoflow.views.polygon') friendly_id = Constant("Polygon Selection") xfacet = Constant(None) yfacet = Constant(None) interactive = Bool(False, transient = True) # internal state. _ax = Any(transient = True) _widget = Instance(util.PolygonSelector, transient = True) _patch = Instance(mpl.patches.PathPatch, transient = True)
[docs] def plot(self, experiment, **kwargs): """ Plot the scatter plot, and then plot the selection on top of it. Parameters ---------- """ super(PolygonSelection, self).plot(experiment, **kwargs) self._ax = plt.gca() self._draw_poly() self._interactive()
@on_trait_change('op.vertices', post_init = True) def _draw_poly(self): if not self._ax: return if self._patch and self._patch in self._ax.patches: self._patch.remove() if not self.op.vertices or len(self.op.vertices) < 3: return patch_vert = np.concatenate((np.array(self.op.vertices), np.array((0,0), ndmin = 2))) self._patch = \ mpl.patches.PathPatch(mpl.path.Path(patch_vert, closed = True), edgecolor="black", linewidth = 2, fill = False) self._ax.add_patch(self._patch) plt.draw() @on_trait_change('interactive', post_init = True) def _interactive(self): if self._ax and self.interactive: self._widget = util.PolygonSelector(self._ax, self._onselect, useblit = True) elif self._widget: self._widget = None def _onselect(self, vertices): self.op.vertices = vertices
util.expand_class_attributes(PolygonSelection) util.expand_method_parameters(PolygonSelection, PolygonSelection.plot) if __name__ == '__main__': import cytoflow as flow tube1 = flow.Tube(file = '../../cytoflow/tests/data/Plate01/RFP_Well_A3.fcs', conditions = {"Dox" : 10.0}) tube2 = flow.Tube(file = '../../cytoflow/tests/data/Plate01/CFP_Well_A4.fcs', conditions = {"Dox" : 1.0}) ex = flow.ImportOp(conditions = {"Dox" : "float"}, tubes = [tube1, tube2]) p = PolygonOp(xchannel = "V2-A", ychannel = "Y2-A") v = p.default_view(xscale = "logicle", yscale = "logicle") plt.ioff() v.plot(ex) v.interactive = True plt.show()