Example #1
0
class Stats2DPlotParams(BasePlotParams):
    
    xlim = Tuple(util.FloatOrNone(None), util.FloatOrNone(None)) 
    ylim = Tuple(util.FloatOrNone(None), util.FloatOrNone(None)) 
    
    def default_traits_view(self):
        base_view = BasePlotParams.default_traits_view(self)
        
        return View(Item('xlim',
                         label = "X Limits",
                         editor = TupleEditor(editors = [TextEditor(auto_set = False,
                                                                    evaluate = float,
                                                                    format_func = lambda x: "" if x == None else str(x)),
                                                         TextEditor(auto_set = False,
                                                                    evaluate = float,
                                                                    format_func = lambda x: "" if x == None else str(x))],
                                              labels = ["Min", "Max"],
                                              cols = 1)),
                    Item('ylim',
                         label = "Y Limits",
                         editor = TupleEditor(editors = [TextEditor(auto_set = False,
                                                                    evaluate = float,
                                                                    format_func = lambda x: "" if x == None else str(x)),
                                                         TextEditor(auto_set = False,
                                                                    evaluate = float,
                                                                    format_func = lambda x: "" if x == None else str(x))],
                                              labels = ["Min", "Max"],
                                              cols = 1)),
                    base_view.content)  
Example #2
0
class RangeWorkflowOp(WorkflowOperation, RangeOp):
    name = Str(apply=True)
    channel = Str(apply=True)

    _range = Tuple(util.FloatOrNone(None), util.FloatOrNone(None), apply=True)
    low = Property(util.FloatOrNone(None), observe='_range')
    high = Property(util.FloatOrNone(None), observe='_range')

    def _get_low(self):
        return self._range[0]

    def _set_low(self, val):
        self._range = (val, self._range[1])

    def _get_high(self):
        return self._range[1]

    def _set_high(self, val):
        self._range = (self._range[0], val)

    def default_view(self, **kwargs):
        return RangeSelectionView(op=self, **kwargs)

    def get_notebook_code(self, idx):
        op = RangeOp()
        op.copy_traits(self, op.copyable_trait_names())

        return dedent("""
        op_{idx} = {repr}
                
        ex_{idx} = op_{idx}.apply(ex_{prev_idx})
        """.format(repr=repr(op), idx=idx, prev_idx=idx - 1))
Example #3
0
class Stats1DPluginPlotParams(Stats1DPlotParams):

    variable_lim = Tuple(util.FloatOrNone(None), util.FloatOrNone(None))
    linestyle = Enum(LINE_STYLES)
    marker = Enum(SCATTERPLOT_MARKERS)
    markersize = util.PositiveCFloat(6, allow_zero=False)
    alpha = util.PositiveCFloat(1.0)
    shade_error = Bool(False)
    shade_alpha = util.PositiveCFloat(0.2)

    def default_traits_view(self):
        base_view = Stats1DPlotParams.default_traits_view(self)

        return View(
            Item('variable_lim',
                 label="Variable\nLimits",
                 editor=TupleEditor(editors=[
                     TextEditor(auto_set=False,
                                evaluate=float,
                                format_func=lambda x: ""
                                if x == None else str(x)),
                     TextEditor(auto_set=False,
                                evaluate=float,
                                format_func=lambda x: ""
                                if x == None else str(x))
                 ],
                                    labels=["Min", "Max"],
                                    cols=1)), Item('linestyle'),
            Item('marker'),
            Item('markersize',
                 editor=TextEditor(auto_set=False),
                 format_func=lambda x: ""
                 if x == None else str(x)), Item('alpha'), Item('shade_error'),
            Item('shade_alpha'), base_view.content)
Example #4
0
class Stats1DPlotParams(_Stats1DPlotParams):
    variable_lim = Tuple(util.FloatOrNone(None), util.FloatOrNone(None))
    linestyle = Enum(LINE_STYLES)
    marker = Enum(SCATTERPLOT_MARKERS)
    markersize = util.PositiveCFloat(6, allow_zero=False)
    capsize = util.PositiveCFloat(0, allow_zero=True)
    alpha = util.PositiveCFloat(1.0)
    shade_error = Bool(False)
    shade_alpha = util.PositiveCFloat(0.2)
Example #5
0
class ThresholdSelectionView(WorkflowView, ThresholdSelection):
    op = Instance(IWorkflowOperation, fixed=True)
    threshold = util.FloatOrNone(None, status=True)
    plot_params = Instance(HistogramPlotParams, ())

    # data flow: user clicks cursor. remote canvas calls _onclick, sets
    # threshold. threshold is copied back to local view (because it's
    # "status = True"). _update_threshold is called, and because
    # self.interactive is false, then the operation is updated.  the
    # operation's threshold is sent back to the remote operation (because
    # "apply = True"), where the remote operation is updated.

    def _onclick(self, event):
        """Update the threshold location"""
        # sometimes the axes aren't set up and we don't get xdata (??)
        if event.xdata:
            self.threshold = event.xdata

    @observe('threshold')
    def _update_threshold(self, _):
        if not self.interactive:
            self.op.threshold = self.threshold

    def get_notebook_code(self, idx):
        view = ThresholdSelection()
        view.copy_traits(self, view.copyable_trait_names())
        plot_params_str = traits_str(self.plot_params)

        return dedent("""
        op_{idx}.default_view({traits}).plot(ex_{prev_idx}{plot_params})
        """.format(idx=idx,
                   traits=traits_str(view),
                   prev_idx=idx - 1,
                   plot_params=", " +
                   plot_params_str if plot_params_str else ""))
Example #6
0
class QuadSelectionView(WorkflowView, QuadSelection):
    op = Instance(IWorkflowOperation, fixed=True)
    plot_params = Instance(ScatterplotPlotParams, ())

    _xy = Tuple(util.FloatOrNone(None), util.FloatOrNone(None), status=True)
    xthreshold = Property(util.FloatOrNone(None), observe='_xy')
    ythreshold = Property(util.FloatOrNone(None), observe='_xy')

    # data flow: user clicks cursor. remote canvas calls _onclick, sets
    # _xy. _xy is copied back to local view (because it's "status = True").
    # _update_xy is called, and because self.interactive is false, then
    # the operation is updated (atomically, both x and y at once.)  the
    # operation's _xy is sent back to the remote operation (because
    # "apply = True"), where the operation is updated.

    # xthreshold and ythreshold are properties (both in the view and
    # in the operation) so that x and y update can happen atomically.
    # otherwise, they happen one after the other, which is noticably
    # slow!

    def _onclick(self, event):
        """Update the threshold location"""
        self._xy = (event.xdata, event.ydata)

    @observe('_xy')
    def _update_xy(self, _):
        if not self.interactive:
            self.op._xy = self._xy

    def _get_xthreshold(self):
        return self._xy[0]

    def _get_ythreshold(self):
        return self._xy[1]

    def get_notebook_code(self, idx):
        view = QuadSelection()
        view.copy_traits(self, view.copyable_trait_names())
        plot_params_str = traits_str(self.plot_params)

        return dedent("""
        op_{idx}.default_view({traits}).plot(ex_{prev_idx}{plot_params})
        """.format(idx=idx,
                   traits=traits_str(view),
                   prev_idx=idx - 1,
                   plot_params=", " +
                   plot_params_str if plot_params_str else ""))
Example #7
0
class RangeSelectionView(WorkflowView, RangeSelection):
    op = Instance(IWorkflowOperation, fixed=True)
    plot_params = Instance(HistogramPlotParams, ())

    _range = Tuple(util.FloatOrNone(None), util.FloatOrNone(None), status=True)
    low = Property(util.FloatOrNone(None), observe='_range')
    high = Property(util.FloatOrNone(None), observe='_range')

    # data flow: user drags cursor. remote canvas calls _onselect, sets
    # _range. _range is copied back to local view (because it's
    # "status = True"). _update_range is called, and because
    # self.interactive is false, then the operation is updated.  the
    # operation's range is sent back to the remote operation (because
    # "apply = True"), where the remote operation is updated.

    # low ahd high are properties (both in the view and in the operation)
    # so that the update can happen atomically. otherwise, they happen
    # one after the other, which is noticably slow!

    def _onselect(self, xmin, xmax):
        """Update selection traits"""
        self._range = (xmin, xmax)

    @observe('_range')
    def _update_range(self, _):
        if not self.interactive:
            self.op._range = self._range

    def _get_low(self):
        return self._range[0]

    def _get_high(self):
        return self._range[1]

    def get_notebook_code(self, idx):
        view = RangeSelection()
        view.copy_traits(self, view.copyable_trait_names())
        plot_params_str = traits_str(self.plot_params)

        return dedent("""
        op_{idx}.default_view({traits}).plot(ex_{prev_idx}{plot_params})
        """.format(idx=idx,
                   traits=traits_str(view),
                   prev_idx=idx - 1,
                   plot_params=", " +
                   plot_params_str if plot_params_str else ""))
Example #8
0
class Stats1DPlotParams(BasePlotParams):
    
    orientation = Enum(["vertical", "horizontal"])
    lim = Tuple(util.FloatOrNone(None), util.FloatOrNone(None)) 
    
    def default_traits_view(self):
        base_view = BasePlotParams.default_traits_view(self)
        
        return View(Item('orientation'),
                    Item('lim',
                         label = "Limits",
                         editor = TupleEditor(editors = [TextEditor(auto_set = False,
                                                                    evaluate = float,
                                                                    format_func = lambda x: "" if x == None else str(x)),
                                                         TextEditor(auto_set = False,
                                                                    evaluate = float,
                                                                    format_func = lambda x: "" if x == None else str(x))],
                                              labels = ["Min", "Max"],
                                              cols = 1)),
                    base_view.content)  
Example #9
0
class QuadWorkflowOp(WorkflowOperation, QuadOp):
    name = Str(apply=True)
    xchannel = Str(apply=True)
    ychannel = Str(apply=True)

    _xy = Tuple(util.FloatOrNone(None), util.FloatOrNone(None), apply=True)
    xthreshold = Property(util.FloatOrNone(None), observe='_xy')
    ythreshold = Property(util.FloatOrNone(None), observe='_xy')

    def _get_xthreshold(self):
        return self._xy[0]

    def _set_xthreshold(self, val):
        self._xy = (val, self._xy[1])

    def _get_ythreshold(self):
        return self._xy[1]

    def _set_ythreshold(self, val):
        self._xy = (self._xy[0], val)

    def default_view(self, **kwargs):
        return QuadSelectionView(op=self, **kwargs)

    def clear_estimate(self):
        # no-op
        return

    def get_notebook_code(self, idx):
        op = QuadOp()
        op.copy_traits(self, op.copyable_trait_names())

        return dedent("""
        op_{idx} = {repr}
                
        ex_{idx} = op_{idx}.apply(ex_{prev_idx})
        """.format(repr=repr(op), idx=idx, prev_idx=idx - 1))
Example #10
0
class ThresholdWorkflowOp(WorkflowOperation, ThresholdOp):
    name = Str(apply=True)
    channel = Str(apply=True)
    threshold = util.FloatOrNone(None, apply=True)

    def default_view(self, **kwargs):
        return ThresholdSelectionView(op=self, **kwargs)

    def clear_estimate(self):
        # no-op
        return

    def get_notebook_code(self, idx):
        op = ThresholdOp()
        op.copy_traits(self, op.copyable_trait_names())

        return dedent("""
        op_{idx} = {repr}
                
        ex_{idx} = op_{idx}.apply(ex_{prev_idx})
        """.format(repr=repr(op), idx=idx, prev_idx=idx - 1))
Example #11
0
class BeadCalibrationOp(HasStrictTraits):
    """
    Calibrate arbitrary channels to molecules-of-fluorophore using fluorescent
    beads (eg, the Spherotech RCP-30-5A rainbow beads.)
    
    Computes a log-linear calibration function that maps arbitrary fluorescence
    units to physical units (ie molecules equivalent fluorophore, or *MEF*).
    
    To use, set :attr:`beads_file` to an FCS file containing events collected *using
    the same cytometer settings as the data you're calibrating*.  Specify which 
    beads you ran by setting :attr:`beads` to match one of the  values of 
    :data:`BeadCalibrationOp.BEADS`; and set :attr:`units` to which channels you 
    want calibrated and in which units.  Then, call :meth:`estimate()` and check the 
    peak-finding with :meth:`default_view().plot()`.  If the peak-finding is wacky, 
    try adjusting :attr:`bead_peak_quantile` and :attr:`bead_brightness_threshold`.  When 
    the peaks are successfully identified, call :meth:`apply` to scale your 
    experimental data set. 
    
    If you can't make the peak finding work, please submit a bug report!
    
    This procedure works best when the beads file is very clean data.  It does
    not do its own gating (maybe a future addition?)  In the meantime, 
    I recommend gating the *acquisition* on the FSC/SSC channels in order
    to get rid of debris, cells, and other noise.
    
    Finally, because you can't have a negative number of fluorescent molecules
    (MEFLs, etc) (as well as for math reasons), this module filters out
    negative values.    
    
    Attributes
    ----------
    units : Dict(Str, Str)
        A dictionary specifying the channels you want calibrated (keys) and
        the units you want them calibrated in (values).  The units must be
        keys of the :attr:`beads` attribute.       
        
    beads_file : File
        A file containing the FCS events from the beads.

    beads : Dict(Str, List(Float))
        The beads' characteristics.  Keys are calibrated units (ie, MEFL or
        MEAP) and values are ordered lists of known fluorophore levels.  Common
        values for this dict are included in :data:`BeadCalibrationOp.BEADS`.
        
    bead_peak_quantile : Int (default = 80)
        The quantile threshold used to choose bead peaks. 
        
    bead_brightness_threshold : Float (default = 100)
        How bright must a bead peak be to be considered?  
        
    bead_brightness_cutoff : Float
        If a bead peak is above this, then don't consider it.  Takes care of
        clipping saturated detection.  Defaults to 70% of the detector range.
        
    bead_histogram_bins : Int (default = 512)
        The number of bins to use in computing the bead histogram.  Tweak
        this if the peak find is having difficulty, or if you have a small 
        number of events
        
    force_linear : Bool (default = False)
        A linear fit in log space doesn't always go through the origin, which 
        means that the calibration function isn't strictly a multiplicative
        scaling operation.  Set :attr:`force_linear` to force the such
        behavior.  Keep an eye on the diagnostic plot, though, to see how much
        error you're introducing!
   
           
    Notes
    -----
    The peak finding is rather sophisticated.  
    
    For each channel, a 256-bin histogram is computed on the log-transformed
    bead data, and then the histogram is smoothed with a Savitzky-Golay 
    filter (with a window length of 5 and a polynomial order of 1).  
    
    Next, a wavelet-based peak-finding algorithm is used: it convolves the
    smoothed histogram with a series of wavelets and looks for relative 
    maxima at various length-scales.  The parameters of the smoothing 
    algorithm were arrived at empircally, using beads collected at a wide 
    range of PMT voltages.
    
    Finally, the peaks are filtered by height (the histogram bin has a quantile
    greater than `bead_peak_quantile`) and intensity (brighter than 
    :attr:`bead_brightness_threshold`).
    
    How to convert from a series of peaks to mean equivalent fluorochrome?
    If there's one peak, we assume that it's the brightest peak.  If there
    are two peaks, we assume they're the brightest two.  If there are ``n >=3``
    peaks, we check all the contiguous `n`-subsets of the bead intensities
    and find the one whose linear regression (in log space!) has the smallest
    norm (square-root sum-of-squared-residuals.)
    
    There's a slight subtlety in the fact that we're performing the linear
    regression in log-space: if the relationship in log10-space is ``Y=aX + b``,
    then the same relationship in linear space is ``x = 10**X``, ``y = 10**y``, and
    ``y = (10**b) * (x ** a)``.

    
    Examples
    --------
    Create a small experiment:
    
    .. plot::
        :context: close-figs
    
        >>> import cytoflow as flow
        >>> import_op = flow.ImportOp()
        >>> import_op.tubes = [flow.Tube(file = "tasbe/rby.fcs")]
        >>> ex = import_op.apply()
    
    Create and parameterize the operation
    
    .. plot::
        :context: close-figs

        >>> bead_op = flow.BeadCalibrationOp()
        >>> beads = "Spherotech RCP-30-5A Lot AA01-AA04, AB01, AB02, AC01, GAA01-R"
        >>> bead_op.beads = flow.BeadCalibrationOp.BEADS[beads]
        >>> bead_op.units = {"Pacific Blue-A" : "MEBFP",
        ...                  "FITC-A" : "MEFL",
        ...                  "PE-Tx-Red-YG-A" : "MEPTR"}
        >>>
        >>> bead_op.beads_file = "tasbe/beads.fcs"
    
    Estimate the model parameters
    
    .. plot::
        :context: close-figs 
    
        >>> bead_op.estimate(ex)
    
    Plot the diagnostic plot
    
    .. plot::
        :context: close-figs

        >>> bead_op.default_view().plot(ex)  

    Apply the operation to the experiment
    
    .. plot::
        :context: close-figs
    
        >>> ex = bead_op.apply(ex)  
        
    """
    
    # traits
    id = Constant('edu.mit.synbio.cytoflow.operations.beads_calibrate')
    friendly_id = Constant("Bead Calibration")
    
    name = Constant("Beads")
    units = Dict(Str, Str)
    
    beads_file = File(exists = True)
    bead_peak_quantile = Int(80)

    bead_brightness_threshold = Float(100.0)
    bead_brightness_cutoff = util.FloatOrNone(None)
    bead_histogram_bins = Int(512)
    
    # TODO - bead_brightness_threshold should probably be different depending
    # on the data range of the input.
    
    force_linear = Bool(False)
    
    beads = Dict(Str, List(Float))

    _histograms = Dict(Str, Any, transient = True)
    _calibration_functions = Dict(Str, Callable, transient = True)
    _peaks = Dict(Str, Any, transient = True)
    _mefs = Dict(Str, Any, transient = True)

    def estimate(self, experiment): 
        """
        Estimate the calibration coefficients from the beads file.
        
        Parameters
        ----------
        experiment : Experiment
            The experiment used to compute the calibration.
            
        """
        if experiment is None:
            raise util.CytoflowOpError('experiment', "No experiment specified")
        
        if not self.beads_file:
            raise util.CytoflowOpError('beads_file', "No beads file specified")

        if not set(self.units.keys()) <= set(experiment.channels):
            raise util.CytoflowOpError('units',
                                       "Specified channels that weren't found in "
                                       "the experiment.")
            
        if not set(self.units.values()) <= set(self.beads.keys()):
            raise util.CytoflowOpError('units',
                                       "Units don't match beads.")
            
        self._histograms.clear()
        self._calibration_functions.clear()
        self._peaks.clear()
        self._mefs.clear()
                        
        # make a little Experiment
        check_tube(self.beads_file, experiment)
        beads_exp = ImportOp(tubes = [Tube(file = self.beads_file)],
                             channels = {experiment.metadata[c]["fcs_name"] : c for c in experiment.channels},
                             name_metadata = experiment.metadata['name_metadata']).apply()
        
        channels = list(self.units.keys())

        # make the histogram
        for channel in channels:
            data = beads_exp.data[channel]
            
            # TODO - this assumes the data is on a linear scale.  check it!
            data_range = experiment.metadata[channel]['range']

            if self.bead_brightness_cutoff is None:
                cutoff = 0.7 * data_range
            else:
                cutoff = self.bead_brightness_cutoff
                                            
            # bin the data on a log scale

            hist_bins = np.logspace(1, math.log(data_range, 2), num = self.bead_histogram_bins, base = 2)
            hist = np.histogram(data, bins = hist_bins)
            
            # mask off-scale values
            hist[0][0] = 0
            hist[0][-1] = 0
            
            # smooth it with a Savitzky-Golay filter
            hist_smooth = scipy.signal.savgol_filter(hist[0], 5, 1)
            
            self._histograms[channel] = (hist, hist_bins, hist_smooth)

            
        # find peaks
        for channel in channels:
            hist = self._histograms[channel][0]
            hist_bins = self._histograms[channel][1]
            hist_smooth = self._histograms[channel][2]

            peak_bins = scipy.signal.find_peaks_cwt(hist_smooth, 
                                                    widths = np.arange(3, 20),
                                                    max_distances = np.arange(3, 20) / 2)
                                    
            # filter by height and intensity
            peak_threshold = np.percentile(hist_smooth, self.bead_peak_quantile)
            peak_bins_filtered = \
                [x for x in peak_bins if hist_smooth[x] > peak_threshold 
                 and hist[1][x] > self.bead_brightness_threshold
                 and hist[1][x] < cutoff]
            
            self._peaks[channel] = [hist_bins[x] for x in peak_bins_filtered]    


        # compute the conversion        
        for channel in channels:
            peaks = self._peaks[channel]
            mef_unit = self.units[channel]
            
            if not mef_unit in self.beads:
                raise util.CytoflowOpError('units',
                                           "Invalid unit {0} specified for channel {1}".format(mef_unit, channel))
            
            # "mean equivalent fluorochrome"
            mef = self.beads[mef_unit]
                                                    
            if len(peaks) == 0:
                raise util.CytoflowOpError(None,
                                           "Didn't find any peaks for channel {}; "
                                           "check the diagnostic plot"
                                           .format(channel))
            elif len(peaks) > len(mef):
                raise util.CytoflowOpError(None,
                                           "Found too many peaks for channel {}; "
                                           "check the diagnostic plot"
                                           .format(channel))
            elif len(peaks) == 1:
                # if we only have one peak, assume it's the brightest peak
                a = mef[-1] / peaks[0]
                self._mefs[channel] = [mef[-1]]
                self._calibration_functions[channel] = lambda x, a=a: a * x
            elif len(peaks) == 2:
                # if we have only two peaks, assume they're the brightest two
                self._mefs[channel] = [mef[-2], mef[-1]]
                a = (mef[-1] - mef[-2]) / (peaks[1] - peaks[0])
                self._calibration_functions[channel] = lambda x, a=a: a * x
            else:
                # if there are n > 2 peaks, check all the contiguous n-subsets
                # of mef for the one whose linear regression with the peaks
                # has the smallest (norm) sum-of-residuals.
                
                # do it in log10 space because otherwise the brightest peaks
                # have an outsized influence.
                                
                best_resid = np.inf
                for start, end in [(x, x+len(peaks)) for x in range(len(mef) - len(peaks) + 1)]:
                    mef_subset = mef[start:end]
                    
                    # linear regression of the peak locations against mef subset
                    lr = np.polyfit(np.log10(peaks), 
                                    np.log10(mef_subset), 
                                    deg = 1, 
                                    full = True)
                                        
                    resid = lr[1][0]
                    if resid < best_resid:
                        best_lr = lr[0]
                        best_resid = resid
                        self._mefs[channel] = mef_subset
   
                if self.force_linear:
                    # if we're forcing a linear scale for the calibration
                    # function, find that scale with an optimization.  (we can't
                    # use this above, to find the MEFs from the peaks, because
                    # when i tried it mis-identified the proper subset.)
                    
                    # even though this keeps things a linear scale, it can
                    # actually introduce *more* errors because "blank" beads
                    # still fluoresce.
                    
                    def s(x):
                        p = np.multiply(self._peaks[channel], x)
                        return np.sum(np.abs(np.subtract(p, self._mefs[channel])))
                    
                    res = scipy.optimize.minimize(s, [1])
                    
                    a = res.x[0]
                    self._calibration_functions[channel] = \
                        lambda x, a=a: a * x
                              
                else:              
                    # remember, these (linear) coefficients came from logspace, so 
                    # if the relationship in log10 space is Y = aX + b, then in
                    # linear space the relationship is x = 10**X, y = 10**Y,
                    # and y = (10**b) * x ^ a
                    
                    # also remember that the result of np.polyfit is a list of
                    # coefficients with the highest power first!  so if we
                    # solve y=ax + b, coeff #0 is a and coeff #1 is b
                    
                    a = best_lr[0]
                    b = 10 ** best_lr[1]
                    self._calibration_functions[channel] = \
                        lambda x, a=a, b=b: b * np.power(x, a)


    def apply(self, experiment):
        """
        Applies the bleedthrough correction to an experiment.
        
        Parameters
        ----------
        experiment : Experiment
            the experiment to which this operation is applied
            
        Returns
        -------
        Experiment 
            A new experiment with the specified channels calibrated in
            physical units.  The calibrated channels also have new metadata:
            
            - **bead_calibration_fn** : Callable (pandas.Series --> pandas.Series)
                The function to calibrate raw data to bead units
        
            - **bead_units** : String
                The units this channel was calibrated to
        """
        
        if experiment is None:
            raise util.CytoflowOpError('experiment', "No experiment specified")
        
        channels = list(self.units.keys())

        if not self.units:
            raise util.CytoflowOpError('units', "No channels to calibrate.")
        
        if not self._calibration_functions:
            raise util.CytoflowOpError(None,
                                       "Calibration not found. "
                                       "Did you forget to call estimate()?")
        
        if not set(channels) <= set(experiment.channels):
            raise util.CytoflowOpError('units',
                                       "Module units don't match experiment channels")
                
        if set(channels) != set(self._calibration_functions.keys()):
            raise util.CytoflowOpError('units',
                                       "Calibration doesn't match units. "
                                       "Did you forget to call estimate()?")

        # two things.  first, you can't raise a negative value to a non-integer
        # power.  second, negative physical units don't make sense -- how can
        # you have the equivalent of -5 molecules of fluoresceine?  so,
        # we filter out negative values here.

        new_experiment = experiment.clone()
        
        for channel in channels:
            new_experiment.data = \
                new_experiment.data[new_experiment.data[channel] > 0]
                                
        new_experiment.data.reset_index(drop = True, inplace = True)
        
        for channel in channels:
            calibration_fn = self._calibration_functions[channel]
            
            new_experiment[channel] = calibration_fn(new_experiment[channel])
            new_experiment.metadata[channel]['bead_calibration_fn'] = calibration_fn
            new_experiment.metadata[channel]['bead_units'] = self.units[channel]
            if 'range' in experiment.metadata[channel]:
                new_experiment.metadata[channel]['range'] = calibration_fn(experiment.metadata[channel]['range'])
            if 'voltage' in experiment.metadata[channel]:
                del new_experiment.metadata[channel]['voltage']
            
        new_experiment.history.append(self.clone_traits(transient = lambda t: True)) 
        return new_experiment
    
    def default_view(self, **kwargs):
        """
        Returns a diagnostic plot to see if the peak finding is working.
        
        Returns
        -------
        IView
            An diagnostic view, call :meth:`~BeadCalibrationDiagnostic.plot` to 
            see the diagnostic plots
        """

        v = BeadCalibrationDiagnostic(op = self)
        v.trait_set(**kwargs)
        return v
    
    # this silliness is necessary to squash the repr() call in sphinx.autodoc
    class _Beads(dict):
        def __repr__(self):
            if hasattr(sys.modules['sys'], 'IN_SPHINX'):
                return None
            return super().__repr__()
    
    BEADS = _Beads(
    {
     # from http://www.spherotech.com/RCP-30-5a%20%20rev%20H%20ML%20071712.xls
     "Spherotech RCP-30-5A Lot AG01, AF02, AD04 and AAE01" :
        { "MECSB" :   [216,   464,   1232,   2940,  7669,  19812,  35474],
          "MEBFP" :   [861,   1997,  5776,   15233, 45389, 152562, 396759],
          "MEFL" :    [792,   2079,  6588,   16471, 47497, 137049, 271647],
          "MEPE" :    [531,   1504,  4819,   12506, 36159, 109588, 250892],
          "MEPTR" :   [233,   669,   2179,   5929,  18219, 63944,  188785],
          "MECY" :    [1614,  4035,  12025,  31896, 95682, 353225, 1077421],
          "MEPCY7" :  [14916, 42336, 153840, 494263],
          "MEAP" :    [373,   1079,  3633,   9896,  28189, 79831,  151008],
          "MEAPCY7" : [2864,  7644,  19081,  37258]},
     # from http://www.spherotech.com/RCP-30-5a%20%20rev%20G.2.xls
     "Spherotech RCP-30-5A Lot AA01-AA04, AB01, AB02, AC01, GAA01-R":
        { "MECSB" :   [179,   400,    993,   3203,  6083,  17777,  36331],
          "MEBFP" :   [700,   1705,   4262,  17546, 35669, 133387, 412089],
          "MEFL" :    [692,   2192,   6028,  17493, 35674, 126907, 290983],
          "MEPE" :    [505,   1777,   4974,  13118, 26757, 94930,  250470],
          "MEPTR" :   [207,   750,    2198,  6063,  12887, 51686,  170219],
          "MECY" :    [1437,  4693,   12901, 36837, 76621, 261671, 1069858],
          "MEPCY7" :  [32907, 107787, 503797],
          "MEAP" :    [587,   2433,   6720,  17962, 30866, 51704,  146080],
          "MEAPCY7" : [718,   1920,   5133,  9324,  14210, 26735]},
    "Spherotech URCP-100-2H (9 peaks)":
        {
          "MEFL" :    [3531, 11373, 34643, 107265, 324936, 835306,  2517654, 6069240],
          "MEPE" :    [2785, 9525,  28421, 90313,  275589, 713181,  2209251, 5738784],
          "MEPTR" :   [1158, 4161,  12528, 41140,  130347, 344149,  1091393, 2938710],
          "MEPCY" :   [6501, 20302, 59517, 183870, 550645, 1569470, 5109318, 17854584],
          "MEPCY7" :  [4490, 10967, 30210, 87027,  283621, 975312,  4409101, 24259524],
          "MEAP" :    [369,  749,   3426,  10413,  50013,  177490,  500257,  1252120],
          "MEAPCY7" : [1363, 2656,  9791,  25120,  96513,  328967,  864905,  2268931],
          "MECSB" :   [989,  2959,  8277,  25524,  71603,  173069,  491388,  1171641],
          "MEBFP" :   [1957, 5579,  16005, 53621,  168302, 459809,  1581762, 4999251]}})
    """
Example #12
0
class Stats2DPlotParams(BasePlotParams):
    xlim = Tuple(util.FloatOrNone(None), util.FloatOrNone(None)) 
    ylim = Tuple(util.FloatOrNone(None), util.FloatOrNone(None)) 
Example #13
0
class Stats1DPlotParams(BasePlotParams):
    orientation = Enum(["vertical", "horizontal"])
    lim = Tuple(util.FloatOrNone(None), util.FloatOrNone(None)) 
Example #14
0
class Data2DPlotParams(DataPlotParams):
    xlim = Tuple(util.FloatOrNone(None), util.FloatOrNone(None))   
    ylim = Tuple(util.FloatOrNone(None), util.FloatOrNone(None))   
Example #15
0
class Data1DPlotParams(DataPlotParams):
    lim = Tuple(util.FloatOrNone(None), util.FloatOrNone(None))   
    orientation = Enum('vertical', 'horizontal')
Example #16
0
class BeadCalibrationWorkflowOp(WorkflowOperation, BeadCalibrationOp):
    # add the 'estimate' metadata
    beads_name = Str(estimate=True)
    beads_file = File(filter=["*.fcs"], estimate=True)
    units_list = List(Unit, estimate=True)

    bead_peak_quantile = Int(80, estimate=True)
    bead_brightness_threshold = Float(100.0, estimate=True)
    bead_brightness_cutoff = util.FloatOrNone(None, estimate=True)

    # add 'estimate_result' metadata
    _calibration_functions = Dict(Str,
                                  Callable,
                                  transient=True,
                                  estimate_result=True)

    @observe('units_list:items,units_list:items.+type', post_init=True)
    def _on_units_changed(self, _):
        self.changed = 'units_list'

    def default_view(self, **kwargs):
        return BeadCalibrationWorkflowView(op=self, **kwargs)

    def apply(self, experiment):

        if not self.beads_name:
            raise util.CytoflowOpError(
                "Specify which beads to calibrate with.")

        self.beads = self.BEADS[self.beads_name]

        for i, unit_i in enumerate(self.units_list):
            for j, unit_j in enumerate(self.units_list):
                if unit_i.channel == unit_j.channel and i != j:
                    raise util.CytoflowOpError(
                        "Channel {0} is included more than once".format(
                            unit_i.channel))

        self.units = {u.channel: u.unit for u in self.units_list}

        return super().apply(experiment)

    def estimate(self, experiment):
        if not self.beads_name:
            raise util.CytoflowOpError(
                "Specify which beads to calibrate with.")

        self.beads = self.BEADS[self.beads_name]

        for i, unit_i in enumerate(self.units_list):
            for j, unit_j in enumerate(self.units_list):
                if unit_i.channel == unit_j.channel and i != j:
                    raise util.CytoflowOpError(
                        "Channel {0} is included more than once".format(
                            unit_i.channel))

        self.units = {u.channel: u.unit for u in self.units_list}

        super().estimate(experiment)

    def should_clear_estimate(self, changed, payload):
        if changed == Changed.ESTIMATE:
            return True

        return False

    def clear_estimate(self):
        self._peaks = {}
        self._mefs = {}
        self._histograms = {}
        self._calibration_functions = {}

    def get_notebook_code(self, idx):
        op = BeadCalibrationOp()
        op.copy_traits(self, op.copyable_trait_names())

        for unit in self.units_list:
            op.units[unit.channel] = unit.unit

        op.beads = self.BEADS[self.beads_name]

        return dedent("""
        # Beads: {beads}
        op_{idx} = {repr}
        
        op_{idx}.estimate(ex_{prev_idx})
        ex_{idx} = op_{idx}.apply(ex_{prev_idx})
        """.format(beads=self.beads_name,
                   repr=repr(op),
                   idx=idx,
                   prev_idx=idx - 1))
Example #17
0
class TasbeCalibrationOp(PluginOpMixin):
    handler_factory = Callable(TasbeHandler)
    
    id = Constant('edu.mit.synbio.cytoflowgui.op_plugins.bleedthrough_piecewise')
    friendly_id = Constant("Quantitative Pipeline")
    name = Constant("TASBE")
    
    fsc_channel = DelegatesTo('_polygon_op', 'xchannel', estimate = True)
    ssc_channel = DelegatesTo('_polygon_op', 'ychannel', estimate = True)
    vertices = DelegatesTo('_polygon_op', 'vertices', estimate = True)
    channels = List(Str, estimate = True)
    
    blank_file = File(filter = ["*.fcs"], estimate = True)
    
    bleedthrough_list = List(_BleedthroughControl, estimate = True)

    beads_name = Str(estimate = True)
    beads_file = File(filter = ["*.fcs"], estimate = True)
    units_list = List(_Unit, estimate = True)
    
    bead_peak_quantile = Int(80, estimate = True)
    bead_brightness_threshold = Float(100, estimate = True)
    bead_brightness_cutoff = util.FloatOrNone("", estimate = True)
    
    do_color_translation = Bool(estimate = True)
    to_channel = Str(estimate = True)
    translation_list = List(_TranslationControl, estimate = True)
    mixture_model = Bool(False, estimate = True)
    
    do_estimate = Event
    valid_model = Bool(False, status = True)
    do_exit = Event
    input_files = List(File)
    output_directory = Directory
        
    _blank_exp = Instance(Experiment, transient = True)
    _blank_exp_file = File(transient = True)
    _blank_exp_channels = List(Str, status = True)
    _polygon_op = Instance(PolygonOp, 
                           kw = {'name' : 'polygon',
                                 'xscale' : 'log', 
                                 'yscale' : 'log'}, 
                           transient = True)
    _af_op = Instance(AutofluorescenceOp, (), transient = True)
    _bleedthrough_op = Instance(BleedthroughLinearOp, (), transient = True)
    _bead_calibration_op = Instance(BeadCalibrationOp, (), transient = True)
    _color_translation_op = Instance(ColorTranslationOp, (), transient = True)
    
    status = Str(status = True)
    
    @on_trait_change('channels[], to_channel, do_color_translation', post_init = True)
    def _channels_changed(self, obj, name, old, new):
        for channel in self.channels:
            if channel not in [control.channel for control in self.bleedthrough_list]:
                self.bleedthrough_list.append(_BleedthroughControl(channel = channel))
                
            if channel not in [unit.channel for unit in self.units_list]:
                self.units_list.append(_Unit(channel = channel))

            
        to_remove = []    
        for control in self.bleedthrough_list:
            if control.channel not in self.channels:
                to_remove.append(control)
                
        for control in to_remove:
            self.bleedthrough_list.remove(control)
            
        to_remove = []    
        for unit in self.units_list:
            if unit.channel not in self.channels:
                to_remove.append(unit)
        
        for unit in to_remove:        
            self.units_list.remove(unit)
                
        if self.do_color_translation:
            to_remove = []
            for unit in self.units_list:
                if unit.channel != self.to_channel:
                    to_remove.append(unit)
            
            for unit in to_remove:
                self.units_list.remove(unit)
                 
            self.translation_list = []
            for c in self.channels:
                if c == self.to_channel:
                    continue
                self.translation_list.append(_TranslationControl(from_channel = c,
                                                                 to_channel = self.to_channel))
                
            self.changed = (Changed.ESTIMATE, ('translation_list', self.translation_list))
            
        self.changed = (Changed.ESTIMATE, ('bleedthrough_list', self.bleedthrough_list))            
        self.changed = (Changed.ESTIMATE, ('units_list', self.units_list))


    @on_trait_change('_polygon_op:vertices', post_init = True)
    def _polygon_changed(self, obj, name, old, new):
        self.changed = (Changed.ESTIMATE, (None, None))

    @on_trait_change("bleedthrough_list_items, bleedthrough_list.+", post_init = True)
    def _bleedthrough_controls_changed(self, obj, name, old, new):
        self.changed = (Changed.ESTIMATE, ('bleedthrough_list', self.bleedthrough_list))
     
    @on_trait_change("translation_list_items, translation_list.+", post_init = True)
    def _translation_controls_changed(self, obj, name, old, new):
        self.changed = (Changed.ESTIMATE, ('translation_list', self.translation_list))
        
    @on_trait_change('units_list_items,units_list.+', post_init = True)
    def _units_changed(self, obj, name, old, new):
        self.changed = (Changed.ESTIMATE, ('units_list', self.units_list))
#     
    def estimate(self, experiment, subset = None):
#         if not self.subset:
#             warnings.warn("Are you sure you don't want to specify a subset "
#                           "used to estimate the model?",
#                           util.CytoflowOpWarning)
            
#         if experiment is None:
#             raise util.CytoflowOpError("No valid result to estimate with")
        
#         experiment = experiment.clone()

        if not self.fsc_channel:
            raise util.CytoflowOpError('fsc_channel',
                                       "Must set FSC channel")
            
        if not self.ssc_channel:
            raise util.CytoflowOpError('ssc_channel',
                                       "Must set SSC channel")
        
        if not self._polygon_op.vertices:
            raise util.CytoflowOpError(None, "Please draw a polygon around the "
                                             "single-cell population in the "
                                             "Morphology tab")            

        experiment = self._blank_exp.clone()
        experiment = self._polygon_op.apply(experiment)
        
        self._af_op.channels = self.channels
        self._af_op.blank_file = self.blank_file
        
        self._af_op.estimate(experiment, subset = "polygon == True")
        self.changed = (Changed.ESTIMATE_RESULT, "Autofluorescence")
        experiment = self._af_op.apply(experiment)
        
        self.status = "Estimating bleedthrough"
        
        self._bleedthrough_op.controls.clear()
        for control in self.bleedthrough_list:
            self._bleedthrough_op.controls[control.channel] = control.file

        self._bleedthrough_op.estimate(experiment, subset = "polygon == True") 
        self.changed = (Changed.ESTIMATE_RESULT, "Bleedthrough")
        experiment = self._bleedthrough_op.apply(experiment)
        
        self.status = "Estimating bead calibration"
        
        self._bead_calibration_op.beads = BeadCalibrationOp.BEADS[self.beads_name]
        self._bead_calibration_op.beads_file = self.beads_file
        self._bead_calibration_op.bead_peak_quantile = self.bead_peak_quantile
        self._bead_calibration_op.bead_brightness_threshold = self.bead_brightness_threshold
        self._bead_calibration_op.bead_brightness_cutoff = self.bead_brightness_cutoff        
        
        self._bead_calibration_op.units.clear()

        for unit in self.units_list:
            self._bead_calibration_op.units[unit.channel] = unit.unit
            
        self._bead_calibration_op.estimate(experiment)
        self.changed = (Changed.ESTIMATE_RESULT, "Bead Calibration")
        
        if self.do_color_translation:
            self.status = "Estimating color translation"

            experiment = self._bead_calibration_op.apply(experiment)
            
            self._color_translation_op.mixture_model = self.mixture_model
            
            self._color_translation_op.controls.clear()
            for control in self.translation_list:
                self._color_translation_op.controls[(control.from_channel,
                                                     control.to_channel)] = control.file
                                                     
            self._color_translation_op.estimate(experiment, subset = 'polygon == True')                                         
            
            self.changed = (Changed.ESTIMATE_RESULT, "Color Translation")
            
        self.status = "Done estimating"
        self.valid_model = True
        
        
    def should_clear_estimate(self, changed, payload):
        """
        Should the owning WorkflowItem clear the estimated model by calling
        op.clear_estimate()?  `changed` can be:
        - Changed.ESTIMATE -- the parameters required to call 'estimate()' (ie
          traits with estimate = True metadata) have changed
        - Changed.PREV_RESULT -- the previous WorkflowItem's result changed

         """
        if changed == Changed.ESTIMATE:
            name, _ = payload
            if name == 'fsc_channel' or name == 'ssc_channel':
                return False
                    
        return True
        
        
    def clear_estimate(self):
        self._af_op = AutofluorescenceOp()
        self._bleedthrough_op = BleedthroughLinearOp()
        self._bead_calibration_op = BeadCalibrationOp()
        self._color_translation_op = ColorTranslationOp()
        self.valid_model = False
        
        self.changed = (Changed.ESTIMATE_RESULT, self)
                        
    def should_apply(self, changed, payload):
        """
        Should the owning WorkflowItem apply this operation when certain things
        change?  `changed` can be:
        - Changed.OPERATION -- the operation's parameters changed
        - Changed.PREV_RESULT -- the previous WorkflowItem's result changed
        - Changed.ESTIMATE_RESULT -- the results of calling "estimate" changed

        """
        if changed == Changed.ESTIMATE_RESULT and \
            self.blank_file != self._blank_exp_file:
            return True
        
        elif changed == Changed.OPERATION:
            name, _ = payload
            if name == "output_directory":
                return False

            return True
        
        return False

        
        
    def apply(self, experiment):

        # this "apply" function is a little odd -- it does not return an Experiment because
        # it always the only WI/operation in the workflow.
        
        if self.blank_file != self._blank_exp_file:
            self._blank_exp = ImportOp(tubes = [Tube(file = self.blank_file)] ).apply()
            self._blank_exp_file = self.blank_file
            self._blank_exp_channels = self._blank_exp.channels
            self.changed = (Changed.PREV_RESULT, None)
            return
        
            
        out_dir = Path(self.output_directory)
        for path in self.input_files:
            in_file_path = Path(path)
            out_file_path = out_dir / in_file_path.name
            if out_file_path.exists():
                raise util.CytoflowOpError(None,
                                           "File {} already exists"
                                           .format(out_file_path))
                
        tubes = [Tube(file = path, conditions = {'filename' : Path(path).stem})
                 for path in self.input_files]
        
        for tube in tubes:
            self.status = "Converting " + Path(tube.file).stem
            experiment = ImportOp(tubes = [tube], conditions = {'filename' : 'category'}).apply()
            
            experiment = self._af_op.apply(experiment)
            experiment = self._bleedthrough_op.apply(experiment)
            experiment = self._bead_calibration_op.apply(experiment)
            
            if self.do_color_translation:
                experiment = self._color_translation_op.apply(experiment)                                                
                    
            ExportFCS(path = self.output_directory,
                      by = ['filename'],
                      _include_by = False).export(experiment)
                      
        self.input_files = []
        self.status = "Done converting!"
    
    
    def default_view(self, **kwargs):
        return TasbeCalibrationView(op = self, **kwargs)
    
    def get_help(self):
        current_dir = os.path.abspath(__file__)
        help_dir = os.path.split(current_dir)[0]
        help_dir = os.path.join(help_dir, "help")
        
        help_file = None
        for klass in self.__class__.__mro__:
            mod = klass.__module__
            mod_html = mod + ".html"
            
            h = os.path.join(help_dir, mod_html)
            if os.path.exists(h):
                help_file = h
                break
                
        with open(help_file, encoding = 'utf-8') as f:
            help_html = f.read()
            
        return help_html
Example #18
0
class TasbeWorkflowOp(WorkflowOperation):
    id = Constant('edu.mit.synbio.cytoflowgui.workflow.operations.tasbe')
    friendly_id = Constant("Quantitative Pipeline")
    name = Constant("TASBE")

    channels = List(Str, estimate=True)

    blank_file = File(filter=["*.fcs"], estimate=True)

    bleedthrough_list = List(BleedthroughControl, estimate=True)

    beads_name = Str(estimate=True)
    beads_file = File(filter=["*.fcs"], estimate=True)
    beads_unit = Str(estimate=True)  # used if do_color_translation is True
    units_list = List(BeadUnit,
                      estimate=True)  # used if do_color_translation is False

    bead_peak_quantile = Int(80, estimate=True)
    bead_brightness_threshold = Float(100.0, estimate=True)
    bead_brightness_cutoff = util.FloatOrNone(None, estimate=True)

    do_color_translation = Bool(False, estimate=True)
    to_channel = Str(estimate=True)
    translation_list = List(TranslationControl, estimate=True)
    mixture_model = Bool(False, estimate=True)

    _af_op = Instance(AutofluorescenceOp, (), transient=True)
    _bleedthrough_op = Instance(BleedthroughLinearOp, (), transient=True)
    _bead_calibration_op = Instance(BeadCalibrationOp, (), transient=True)
    _color_translation_op = Instance(ColorTranslationOp, (), transient=True)

    estimate_progress = Str(Progress.NO_MODEL,
                            transient=True,
                            estimate_result=True,
                            status=True)

    # override the base class's "subset" with one that is dynamically generated /
    # updated from subset_list
    subset = Property(Str, observe="subset_list.items.str")
    subset_list = List(ISubset, estimate=True)

    # bits to support the subset editor
    @observe('subset_list:items.str')
    def _on_subset_changed(self, _):
        self.changed = 'subset_list'

    # MAGIC - returns the value of the "subset" Property, above
    def _get_subset(self):
        return " and ".join(
            [subset.str for subset in self.subset_list if subset.str])

    @observe('channels.items,to_channel,do_color_translation', post_init=True)
    def _on_channels_changed(self, _):

        # bleedthrough
        for channel in self.channels:
            if channel not in [
                    control.channel for control in self.bleedthrough_list
            ]:
                self.bleedthrough_list.append(
                    BleedthroughControl(channel=channel))

        to_remove = []
        for control in self.bleedthrough_list:
            if control.channel not in self.channels:
                to_remove.append(control)

        for control in to_remove:
            self.bleedthrough_list.remove(control)

        # bead calibration
        for channel in self.channels:
            if channel not in [unit.channel for unit in self.units_list]:
                self.units_list.append(BeadUnit(channel=channel))

        to_remove = []
        for unit in self.units_list:
            if unit.channel not in self.channels:
                to_remove.append(unit)

        for unit in to_remove:
            self.units_list.remove(unit)

        # color translation
        if self.to_channel not in self.channels:
            self.translation_list = []
            self.to_channel = ''
            return

        for channel in self.channels:
            if channel != self.to_channel:
                if channel not in [
                        control.from_channel
                        for control in self.translation_list
                ]:
                    self.translation_list.append(
                        TranslationControl(from_channel=channel,
                                           to_channel=self.to_channel))

        to_remove = []
        for control in self.translation_list:
            if control.from_channel not in self.channels or \
               control.to_channel not in self.channels:
                to_remove.append(control)

        for control in to_remove:
            self.translation_list.remove(control)

    @observe('to_channel', post_init=True)
    def _on_to_channel_changed(self, _):
        self.translation_list = []
        if self.to_channel:
            for c in self.channels:
                if c == self.to_channel:
                    continue
                self.translation_list.append(
                    TranslationControl(from_channel=c,
                                       to_channel=self.to_channel))

    @observe("bleedthrough_list:items:file", post_init=True)
    def _bleedthrough_controls_changed(self, _):
        self.changed = 'bleedthrough_list'

    @observe("translation_list:items:file", post_init=True)
    def _translation_controls_changed(self, _):
        self.changed = 'translation_list'

    @observe('units_list:items:unit', post_init=True)
    def _on_units_changed(self, _):
        self.changed = 'units_list'

    def estimate(self, experiment, subset=None):
        if not self.subset:
            warnings.warn(
                "Are you sure you don't want to specify a subset "
                "used to estimate the model?", util.CytoflowOpWarning)

        if experiment is None:
            raise util.CytoflowOpError("No valid result to estimate with")

        # TODO - don't actually need to apply these operations to data in estimate
        experiment = experiment.clone()

        self.estimate_progress = Progress.AUTOFLUORESCENCE
        self._af_op.channels = self.channels
        self._af_op.blank_file = self.blank_file

        self._af_op.estimate(experiment, subset=self.subset)
        experiment = self._af_op.apply(experiment)

        self.estimate_progress = Progress.BLEEDTHROUGH
        self._bleedthrough_op.controls.clear()
        for control in self.bleedthrough_list:
            self._bleedthrough_op.controls[control.channel] = control.file

        self._bleedthrough_op.estimate(experiment, subset=self.subset)
        experiment = self._bleedthrough_op.apply(experiment)

        self.estimate_progress = Progress.BEAD_CALIBRATION
        self._bead_calibration_op.beads = BeadCalibrationOp.BEADS[
            self.beads_name]
        self._bead_calibration_op.beads_file = self.beads_file
        self._bead_calibration_op.bead_peak_quantile = self.bead_peak_quantile
        self._bead_calibration_op.bead_brightness_threshold = self.bead_brightness_threshold
        self._bead_calibration_op.bead_brightness_cutoff = self.bead_brightness_cutoff

        if self.do_color_translation:
            # this way matches TASBE better
            self._bead_calibration_op.units.clear()
            self._bead_calibration_op.units[self.to_channel] = self.beads_unit
            self._bead_calibration_op.estimate(experiment)
            experiment = self._bead_calibration_op.apply(experiment)

            self.estimate_progress = Progress.COLOR_TRANSLATION
            self._color_translation_op.mixture_model = self.mixture_model

            self._color_translation_op.controls.clear()
            for control in self.translation_list:
                self._color_translation_op.controls[(
                    control.from_channel, control.to_channel)] = control.file

            self._color_translation_op.estimate(experiment, subset=self.subset)

        else:
            self._bead_calibration_op.units.clear()

            for unit in self.units_list:
                self._bead_calibration_op.units[unit.channel] = unit.unit

            self._bead_calibration_op.estimate(experiment)
            experiment = self._bead_calibration_op.apply(experiment)

        self.estimate_progress = Progress.VALID

    def should_clear_estimate(self, changed, payload):
        if changed == Changed.ESTIMATE:
            return True

        return False

    def clear_estimate(self):
        self._af_op = AutofluorescenceOp()
        self._bleedthrough_op = BleedthroughLinearOp()
        self._bead_calibration_op = BeadCalibrationOp()
        self._color_translation_op = ColorTranslationOp()
        self.estimate_progress = Progress.NO_MODEL

    def apply(self, experiment):
        if self.estimate_progress == Progress.NO_MODEL:
            raise util.CytoflowOpError(None, 'Click "Estimate"!')
        elif self.estimate_progress != Progress.VALID:
            raise util.CytoflowOpError(None, 'No valid model')

        experiment = self._af_op.apply(experiment)
        experiment = self._bleedthrough_op.apply(experiment)
        experiment = self._bead_calibration_op.apply(experiment)
        if self.do_color_translation:
            experiment = self._color_translation_op.apply(experiment)

        return experiment

    def default_view(self, **kwargs):
        return TasbeWorkflowView(op=self, **kwargs)

    def get_notebook_code(self, idx):
        self._af_op.channels = self.channels
        self._af_op.blank_file = self.blank_file

        self._bleedthrough_op.controls.clear()
        for control in self.bleedthrough_list:
            self._bleedthrough_op.controls[control.channel] = control.file

        self._bead_calibration_op.beads = BeadCalibrationOp.BEADS[
            self.beads_name]
        self._bead_calibration_op.beads_file = self.beads_file
        self._bead_calibration_op.bead_peak_quantile = self.bead_peak_quantile
        self._bead_calibration_op.bead_brightness_threshold = self.bead_brightness_threshold
        self._bead_calibration_op.bead_brightness_cutoff = self.bead_brightness_cutoff

        self._bead_calibration_op.units.clear()
        self._bead_calibration_op.units[self.to_channel] = self.beads_unit

        self._color_translation_op.mixture_model = self.mixture_model

        self._color_translation_op.controls.clear()
        for control in self.translation_list:
            self._color_translation_op.controls[(
                control.from_channel, control.to_channel)] = control.file

        return dedent("""
        # the TASBE-style calibration is not a single Cytoflow module.  Instead, it
        # is a specific sequence of four calibrations: autofluorescence correction,
        # bleedthrough, bead calibration and color translation.
        
        # autofluorescence
        op_{idx}_af = {af_repr}
        
        op_{idx}_af.estimate(ex_{prev_idx}{subset})
        ex_{idx}_af = op_{idx}_af.apply(ex_{prev_idx})
        
        # bleedthrough
        op_{idx}_bleedthrough = {bleedthrough_repr}
        
        op_{idx}_bleedthrough.estimate(ex_{idx}_af{subset})
        ex_{idx}_bleedthrough = op_{idx}_bleedthrough.apply(ex_{idx}_af)
        
        # bead calibration
        # beads: {beads}
        op_{idx}_beads = {beads_repr}
        
        op_{idx}_beads.estimate(ex_{idx}_bleedthrough)
        ex_{idx}_beads = op_{idx}_beads.apply(ex_{idx}_bleedthrough)
        
        # color translation
        op_{idx}_color = {color_repr}
        
        op_{idx}_color.estimate(ex_{idx}_beads{subset})
        ex_{idx} = op_{idx}_color.apply(ex_{idx}_beads)
        """.format(idx=idx,
                   prev_idx=idx - 1,
                   af_repr=repr(self._af_op),
                   bleedthrough_repr=repr(self._bleedthrough_op),
                   color_repr=repr(self._color_translation_op),
                   beads=self.beads_name,
                   beads_repr=repr(self._bead_calibration_op),
                   subset=", subset = " +
                   repr(self.subset) if self.subset else ""))