Beispiel #1
0
class TasbePluginOp(PluginOpMixin):
    handler_factory = Callable(TasbeHandler)

    id = Constant(
        'edu.mit.synbio.cytoflowgui.op_plugins.bleedthrough_piecewise')
    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)

    bead_peak_quantile = Int(80, estimate=True)
    bead_brightness_threshold = Float(100, estimate=True)
    bead_brightness_cutoff = Float(Undefined, 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)

    subset_list = List(ISubset, estimate=True)
    subset = Property(Str, depends_on="subset_list.str")

    # 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])

    @on_trait_change("subset_list.str", post_init=True)
    def _subset_changed(self, obj, name, old, new):
        self.changed = (Changed.ESTIMATE, ('subset_list', self.subset_list))

    @on_trait_change('channels[]', post_init=True)
    def _channels_changed(self, obj, name, old, new):
        self.bleedthrough_list = []
        for c in self.channels:
            self.bleedthrough_list.append(_BleedthroughControl(channel=c))

        self.changed = (Changed.ESTIMATE, ('bleedthrough_list',
                                           self.bleedthrough_list))

        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))
        self.changed = (Changed.ESTIMATE, ('translation_list',
                                           self.translation_list))

    @on_trait_change('to_channel', post_init=True)
    def _to_channel_changed(self, obj, name, old, new):
        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))
        self.changed = (Changed.ESTIMATE, ('translation_list',
                                           self.translation_list))

    @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))

    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()

        self._af_op.channels = self.channels
        self._af_op.blank_file = self.blank_file

        self._af_op.estimate(experiment, subset=self.subset)
        self.changed = (Changed.ESTIMATE_RESULT, self)
        experiment = self._af_op.apply(experiment)

        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)
        self.changed = (Changed.ESTIMATE_RESULT, self)
        experiment = self._bleedthrough_op.apply(experiment)

        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()

        # this is the old way
        #         for channel in self.channels:
        #             self._bead_calibration_op.units[channel] = self.beads_unit

        # this way matches TASBE better
        self._bead_calibration_op.units[self.to_channel] = self.beads_unit

        self._bead_calibration_op.estimate(experiment)
        self.changed = (Changed.ESTIMATE_RESULT, self)
        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=self.subset)

        self.changed = (Changed.ESTIMATE_RESULT, self)

    def should_clear_estimate(self, changed):
        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.changed = (Changed.ESTIMATE_RESULT, self)

    def apply(self, experiment):

        if experiment is None:
            raise util.CytoflowOpError("No experiment was specified")

        experiment = self._af_op.apply(experiment)
        experiment = self._bleedthrough_op.apply(experiment)
        experiment = self._bead_calibration_op.apply(experiment)
        experiment = self._color_translation_op.apply(experiment)

        return experiment

    def default_view(self, **kwargs):
        return TasbePluginView(op=self, **kwargs)
Beispiel #2
0
class TasbePluginOp(PluginOpMixin):
    handler_factory = Callable(TasbeHandler)
    
    id = Constant('edu.mit.synbio.cytoflowgui.op_plugins.bleedthrough_piecewise')
    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)
    
    bead_peak_quantile = Int(80, estimate = True)
    bead_brightness_threshold = Float(100, estimate = True)
    bead_brightness_cutoff = Float(Undefined, 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(BleedthroughPiecewiseOp, (), transient = True)
    _bead_calibration_op = Instance(BeadCalibrationOp, (), transient = True)
    _color_translation_op = Instance(ColorTranslationOp, (), transient = True)
    
    @on_trait_change('channels[]', post_init = True)
    def _channels_changed(self, obj, name, old, new):
        self.bleedthrough_list = []
        for c in self.channels:
            self.bleedthrough_list.append(_BleedthroughControl(channel = c))
            
        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))
        self.changed = "estimate"


    @on_trait_change('to_channel', post_init = True)
    def _to_channel_changed(self, obj, name, old, new):
        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))


    
    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)
            
        experiment = experiment.clone()
        
        self._af_op.channels = self.channels
        self._af_op.blank_file = self.blank_file
        
        self._af_op.estimate(experiment, subset = self.subset)
        self.changed = "estimate_result"
        experiment = self._af_op.apply(experiment)
        
        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) 
        self.changed = "estimate_result"
        experiment = self._bleedthrough_op.apply(experiment)
        
        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 channel in self.channels:
            self._bead_calibration_op.units[channel] = self.beads_unit
            
        self._bead_calibration_op.estimate(experiment)
        self.changed = "estimate_result"
        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 = self.subset)                                         
        
        self.changed = "estimate_result"
        
        
    def should_clear_estimate(self, changed):
        """
        Should the owning WorkflowItem clear the estimated model by calling
        op.clear_estimate()?  `changed` can be:
         - "estimate" -- the parameters required to call 'estimate()' (ie
            traits with estimate = True metadata) have changed
         - "prev_result" -- the previous WorkflowItem's result changed
        """
        if changed == "prev_result":
            return False
        
        return True
        
        
    def clear_estimate(self):
        self._af_op = AutofluorescenceOp()
        self._bleedthrough_op = BleedthroughPiecewiseOp()
        self._bead_calibration_op = BeadCalibrationOp()
        self._color_translation_op = ColorTranslationOp()
        
        self.changed = "estimate_result"
        
        
    def apply(self, experiment):
        
        experiment = self._af_op.apply(experiment)
        experiment = self._bleedthrough_op.apply(experiment)
        experiment = self._bead_calibration_op.apply(experiment)
        experiment = self._color_translation_op.apply(experiment)
        
        return experiment
    
    
    def default_view(self, **kwargs):
        return TasbePluginView(op = self, **kwargs)
Beispiel #3
0
class TasbePluginOp(PluginOpMixin):
    handler_factory = Callable(TasbeHandler)
    
    id = Constant('edu.mit.synbio.cytoflowgui.op_plugins.bleedthrough_piecewise')
    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)
    
    bead_peak_quantile = CInt(80, estimate = True)
    bead_brightness_threshold = CFloat(100.0, estimate = True)
    bead_brightness_cutoff = util.CFloatOrNone(None, 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)
    
    subset_list = List(ISubset, estimate = True)    
    subset = Property(Str, depends_on = "subset_list.str")
        
    # 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])
    
    @on_trait_change("subset_list.str")
    def _subset_changed(self, obj, name, old, new):
        self.changed = (Changed.ESTIMATE, ('subset_list', self.subset_list))
    
    @on_trait_change('channels[]', 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))

        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)
             
        for c in self.channels:
            if c == self.to_channel:
                continue
            if channel not in [control.from_channel for control in self.translation_list]:
                self.translation_list.append(_TranslationControl(from_channel = c,
                                                                 to_channel = self.to_channel))
            
        to_remove = []
        for control in self.translation_list:
            if control.from_channel not in self.channels:
                to_remove.append(control)
                
        for control in to_remove:
            self.translation_list.remove(control)
            
        self.changed = (Changed.ESTIMATE, ('translation_list', self.translation_list))
        self.changed = (Changed.ESTIMATE, ('bleedthrough_list', self.bleedthrough_list))            


    @on_trait_change('to_channel', post_init = True)
    def _to_channel_changed(self, obj, name, old, new):
        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))
        self.changed = (Changed.ESTIMATE, ('translation_list', self.translation_list))
         
    @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))
    
    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._af_op.channels = self.channels
        self._af_op.blank_file = self.blank_file
        
        try:
            self._af_op.estimate(experiment, subset = self.subset)
        except:
            raise
        finally:
            self.changed = (Changed.ESTIMATE_RESULT, self)
            
        experiment = self._af_op.apply(experiment)
        
        self._bleedthrough_op.controls.clear()
        for control in self.bleedthrough_list:
            self._bleedthrough_op.controls[control.channel] = control.file

        try:
            self._bleedthrough_op.estimate(experiment, subset = self.subset)
        except:
            raise
        finally:
            self.changed = (Changed.ESTIMATE_RESULT, self)
            
        experiment = self._bleedthrough_op.apply(experiment)
        
        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()
        
        # this is the old way
#         for channel in self.channels:
#             self._bead_calibration_op.units[channel] = self.beads_unit

        # this way matches TASBE better
        self._bead_calibration_op.units[self.to_channel] = self.beads_unit
            
        try:
            self._bead_calibration_op.estimate(experiment)
        except:
            raise
        finally:
            self.changed = (Changed.ESTIMATE_RESULT, self)
            
        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
            
        try:                                     
            self._color_translation_op.estimate(experiment, subset = self.subset)
        except:
            raise
        finally:                                         
            self.changed = (Changed.ESTIMATE_RESULT, self)
        
        
    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.changed = (Changed.ESTIMATE_RESULT, self)
        
        
    def apply(self, experiment):
        
        if experiment is None:
            raise util.CytoflowOpError("No experiment was specified")
        
        experiment = self._af_op.apply(experiment)
        experiment = self._bleedthrough_op.apply(experiment)
        experiment = self._bead_calibration_op.apply(experiment)
        experiment = self._color_translation_op.apply(experiment)
        
        return experiment
    
    
    def default_view(self, **kwargs):
        return TasbePluginView(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 ""))
Beispiel #4
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 ""))