Beispiel #1
0
class DeviceDriver(Declarative):
    """ Provide meta info about this device """
    # ID of the device
    # If none exits one i created from manufacturer.model
    id = d_(Unicode())

    # Name of the device (optional)
    name = d_(Unicode())

    # Model of the device (optional)
    model = d_(Unicode())

    # Manufacturer of the device (optional)
    manufacturer = d_(Unicode())

    # Width of the device (required)
    width = d_(Unicode())

    # Length of the device, if it uses a roll, leave blank
    length = d_(Unicode())

    # Factory to construct the inkcut.device.plugin.Device or subclass.
    # If none is given it will be generated by the DevicePlugin
    #: for an example, see the DeviceDriver in the inkcut.device.pi.manifest
    factory = d_(Callable(default=default_device_factory))

    # List of protocol IDs supported by this device
    protocols = d_(List(Unicode()))

    # List of transport IDs supported by this device
    connections = d_(List(Unicode()))

    #: Config view for editing the config of this device
    config_view = d_(Callable(default=default_device_config_view_factory))
Beispiel #2
0
class GPIB_Instrument(Instrument):
    """Extends Instrument definition to GPIB Instruments"""
    address = Unicode("GPIB0::22::INSTR").tag(sub=True, label="GPIB Address")
    session = Typed(visa.Instrument)  #, desc="visa session of the instrument")

    booter = Callable(start_GPIB)
    delay = Float(0).tag(sub=True,
                         unit="s",
                         desc="delay between GPIB commands")
    timeout = Float(5).tag(sub=True, unit="s", desc="timeout")
    do_reset = Bool(False).tag(sub=True)
    reset = Callable(GPIB_reset)
    lock = Bool(False).tag(sub=True)
    send_end = Bool(True).tag(sub=True)
    do_clear = Bool(True).tag(sub=True)
    clear = Callable(GPIB_clear).tag(value=True)
    do_identify = Bool(False).tag(sub=True)
    identify = Unicode().tag(GPIB_asks="*IDN?")
    do_selftest = Bool(False).tag(sub=True)
    selftest = Callable(GPIB_selftest)  #Unicode().tag(GPIB_asks="*TST?")

    closer = Callable(stop_GPIB)

    def __init__(self, **kwargs):
        """updates __init__ so paramters with tags GPIB_writes and GPIB_asks are updated to set_cmd=GPIB_write and get_cmd=GPIB_ask respectively"""
        super(GPIB_Instrument, self).__init__(**kwargs)
        for name in self.all_params:
            GPIB_string = self.get_tag(name, 'GPIB_writes')
            if GPIB_string != None:
                self.set_tag(name, set_cmd=GPIB_write_it(GPIB_string, name))
            GPIB_string = self.get_tag(name, 'GPIB_asks')
            if GPIB_string != None:
                self.set_tag(name, get_cmd=GPIB_ask_it(GPIB_string))
Beispiel #3
0
class Debugger(Declarative):
    """ Extension for the 'debuggers' extension point of a DebuggerPlugin.

    The name member inherited from Object should always be set to an easily
    understandable name for the user.

    """
    #: Id of the debugger, this can be different from the id of the plugin
    #: declaring it but does not have to.
    id = d_(Unicode())

    #: Debugger description.
    description = d_(Unicode())

    #: Factory function returning an instance of the debugger. This callable
    #: should take as arguments the debugger declaration and the debugger
    #: plugin.
    factory = d_(Callable())

    #: View of the debugger or factory taking as args the dock_area,
    #: the debugger and the name of the top widget.
    view = d_(Value())

    #: Callable adding contribution to the main window take as single argument
    #: the workspace.
    contribute_workspace = d_(Callable())

    #: Callable removing the contribution from the main window.
    remove_contribution = d_(Callable())
Beispiel #4
0
class RuntimeDependency(Declarative):
    """Extension to the 'runtime-dependencies' extensions point of the
    TaskManagerPlugin.

    Attributes
    ----------
    id : unicode
        Unique Id.

    walk_members : list(str)
        List of members for the walk method of the ComplexTask.

    walk_kwargs : dict(str: callable)
        Dict of name: callables for the walk method of the ComplexTask.

    collect : callable(workbench, flatten_walk)
        Callable in charge of collecting the identified build dependencies.
        It should take as arguments the workbench of the application, a dict
        in the format {name: set()} and the calling plugin id. It should return
        a dict holding the dependencies (as dictionaries) in categories. If
        there is no dependence for a given category this category should be
        absent from the dict.
        The input falt_walk should be left untouched. In case of failure it
        should raise a value error.


    """
    id = d_(Unicode())

    walk_members = d_(List(Str()))

    walk_callables = d_(Dict(Str(), Callable()))

    collect = d_(Callable())
Beispiel #5
0
class ObjectCombo(Control):
    """ A drop-down list from which one item can be selected at a time.

    Use a combo box to select a single item from a collection of items.

    """
    #: The list of items to display in the combo box.
    items = d_(List())

    #: The selected item from the list of items. The default will be
    #: the first item in the list of items, or None.
    selected = d_(Value())

    #: The callable to use to convert the items into unicode strings
    #: for display. The default is the builtin 'unicode'.
    to_string = d_(Callable(unicode))

    #: The callable to use to convert the items into icons for
    #: display. The default is a lambda which returns None.
    to_icon = d_(Callable(lambda item: None))

    #: Whether the text in the combo box can be edited by the user.
    editable = d_(Bool(False))

    #: A combo box hugs its width weakly by default.
    hug_width = set_default('weak')

    #: A reference to the ProxyObjectCombo object.
    proxy = Typed(ProxyObjectCombo)

    #--------------------------------------------------------------------------
    # Default Value Handlers
    #--------------------------------------------------------------------------
    def _default_selected(self):
        """ The default value handler for the 'selected' member.

        """
        items = self.items
        if len(items) > 0:
            return items[0]

    #--------------------------------------------------------------------------
    # Observers
    #--------------------------------------------------------------------------
    @observe('items', 'to_string', 'to_icon')
    def _refresh_proxy(self, change):
        """ An observer which requests an items refresh from the proxy.

        """
        if change['type'] == 'update' and self.proxy_is_active:
            self.proxy.request_items_refresh()

    @observe('selected', 'editable')
    def _update_proxy(self, change):
        """ An observer which sends state change to the proxy.

        """
        # The superclass handler implementation is sufficient.
        super(ObjectCombo, self)._update_proxy(change)
Beispiel #6
0
class InstrumentChief(Chief):
    """Extends Master to accomodate instruments, booting, closing, autosaves data, and adds a prepare and finish functions."""
    prepare = Callable(factory=pass_factory)
    finish = Callable(factory=pass_factory)

    def _default_saving(self):
        return False  #True

    @property
    def view_window2(self):
        pass

    @property
    def view2(self):
        return "Instrument"

    def show2(self):
        with imports():
            from enaml_Instrument import InstrMain
        try:
            app = QtApplication()
            view = InstrMain(boss=self)
            view.show()
            app.start()
        finally:
            self.close_all()
            if self.saving:
                self.save_file.flush_buffers()

    def close_all(self):
        for instr in self.agents:
            if instr.status == 'Active':
                instr.close()

    def boot_all(self):
        for instr in self.agents:
            if instr.status == 'Closed':
                instr.boot()

    def run_measurement(self):
        log_info("Measurement started")
        self.prepare()
        self.run()
        self.finish()
        log_info("Measurement finished")

    def make_boss(self,
                  base_dir="C:\\Users\\Speedy\\Documents\\Thomas\\TA_test",
                  divider="\\",
                  log_name="record",
                  file_name="meas",
                  setup_g_name="SetUp",
                  save_g_name="Measurements"):
        self.BASE_DIR = base_dir  #"/Users/thomasaref/Dropbox/Current stuff/TA_software"
        self.DIVIDER = divider  #"/"
        self.LOG_NAME = log_name  #"record"
        self.FILE_NAME = file_name  #"meas"
        self.SETUP_GROUP_NAME = setup_g_name  #"SetUp"
        self.SAVE_GROUP_NAME = save_g_name  #"Measurements"
Beispiel #7
0
class HttpRequest(Atom):
    """ The request object created for fetch calls. 
    It's based on the design of Tornado's HttpRequest.
    
    """
    #: Request url
    url = Unicode()

    #: Request method
    method = Unicode('get')

    #: Request headers
    headers = Dict()

    #: Retry count
    retries = Int()

    #: Request parameter data
    data = Dict()

    #: Content type
    content_type = Unicode("application/x-www-urlencoded")

    #: Raw request body
    body = Unicode()

    #: Response created
    response = ForwardInstance(lambda: HttpResponse)

    #: Called when complete
    callback = Callable()

    #: Streaming callback
    streaming_callback = Callable()

    #: Start time
    start_time = Float()

    def __init__(self, *args, **kwargs):
        """ Build the request as configured.
        
        """
        super(HttpRequest, self).__init__(*args, **kwargs)
        self.start_time = time.time()
        self.response = HttpResponse(request=self)
        self.init_request()

    def init_request(self):
        """ Initialize the request using whatever native means necessary 
        
        """
        raise NotImplementedError
Beispiel #8
0
class DeviceTransport(Declarative):
    #: Id of the transport
    id = d_(Unicode())

    #: Name of the transport (optional)
    name = d_(Unicode())

    #: Factory to construct the transport. It receives the DeviceDriver
    #: as the first argument and the DeviceProtocol declaration as the second
    factory = d_(Callable())

    #: Config view for editing the config of this device
    config_view = d_(Callable(default=default_config_view_factory))
Beispiel #9
0
class DeviceTransport(Declarative):
    # Id of the protocol
    id = d_(Unicode())

    # Name of the protocol (optional)
    name = d_(Unicode())

    # Factory to construct the protocol,
    # takes a single argument for the transport
    factory = d_(Callable())

    #: Config view for editing the config of this device
    config_view = d_(Callable(default=default_config_view_factory))
Beispiel #10
0
class DeviceFilter(Declarative):
    #: Id of the filter
    id = d_(Str())

    #: Name of the filter (optional)
    name = d_(Str())

    #: Factory to construct the filter. It receives the DeviceDriver
    #: as the first argument and the DeviceProtocol declaration as the second
    factory = d_(Callable())

    #: Config view for editing the config of this filter
    config_view = d_(Callable(default=default_config_view_factory))
Beispiel #11
0
class Transform(ContinuousInput):

    function = d_(Callable())

    def configure_callback(self):
        cb = super().configure_callback()
        return transform(self.function, cb).send
Beispiel #12
0
class CustomInput(Input):

    function = d_(Callable())

    def configure_callback(self):
        cb = super().configure_callback()
        return custom_input(self.function, cb).send
Beispiel #13
0
class CheckTask(SimpleTask):
    """Task keeping track of check and perform call and value passed to perform

    """
    #: Number of time the check method has been called.
    check_called = Int()

    #: Number of time the perform method has been called.
    perform_called = Int()

    #: Value passed to the perform method.
    perform_value = Value()

    #: Function to call in the perform method
    custom = Callable(lambda t, x: None)

    def check(self, *args, **kwargs):

        self.check_called += 1
        return super(CheckTask, self).check(*args, **kwargs)

    def perform(self, value=None):

        self.perform_called += 1
        self.perform_value = value
        self.custom(self, value)
Beispiel #14
0
class PluginManifest(Declarative):
    """ A declarative class which represents a plugin manifest.

    """
    #: The globally unique identifier for the plugin. The suggested
    #: format is dot-separated, e.g. 'foo.bar.baz'.
    id = d_(Str())

    #: The factory which will create the Plugin instance. It should
    #: take no arguments and return an instance of Plugin. Well behaved
    #: applications will make this a function which lazily imports the
    #: plugin class so that startup times remain small.
    factory = d_(Callable(plugin_factory))

    #: The workbench instance with which this manifest is registered.
    #: This is assigned by the framework and should not be manipulated
    #: by user code.
    workbench = ForwardTyped(Workbench)

    #: An optional description of the plugin.
    description = d_(Str())

    @property
    def extensions(self):
        """ Get the list of extensions defined by the manifest.

        """
        return [c for c in self.children if isinstance(c, Extension)]

    @property
    def extension_points(self):
        """ Get the list of extensions points defined by the manifest.

        """
        return [c for c in self.children if isinstance(c, ExtensionPoint)]
Beispiel #15
0
class GetInstruction(BaseInstruction):
    """Read the value of an instrument feature and store it in the database.

    """
    def prepare(self):
        """Build the callable accessing driver Feature.

        """
        source = ("def _get_(driver, **ch_ids):" "    return {path}")
        local = {}
        exec(source.format(', '.join(self.ch_ids), self.path), local)
        self._getter = local['_get_']

    def execute(self, task, driver):
        """Get the value of the Feature and store it in the database.

        """
        ch_ids = {
            k: task.format_and_eval_string(v)
            for k, v in self.ch_ids.items()
        }
        task.write_in_database(self.id, self._getter(driver, **ch_ids))

    # --- Private API ---------------------------------------------------------

    #: Getter function streamlining the process of accessing to the driver
    #: Feature.
    _getter = Callable()

    def _default_database_entries(self):
        """Default database names used by the instruction.

        """
        return {self.id: 1.0}
Beispiel #16
0
class SetInstruction(BaseInstruction):
    """Set the value of an instrument feature.

    """
    #: Value that should be set when executing the instruction.
    value = Str().tag(pref=True)

    def prepare(self):
        """Build the callable accessing driver Feature.

        """
        source = ("def _set_(driver, value, **ch_ids):" "    {path} = value")
        local = {}
        exec(source.format(', '.join(self.ch_ids), self.path), local)
        self._setter = local['_set_']

    def execute(self, task, driver):
        """Get the value of the Feature and store it in the database.

        """
        ch_ids = {
            k: task.format_and_eval_string(v)
            for k, v in self.ch_ids.items()
        }
        value = task.format_and_eval_string(self.value)
        self._setter(driver, value, **ch_ids)

    # --- Private API ---------------------------------------------------------

    #: Setter function streamlining the process of accessing to the driver
    #: Feature.
    _setter = Callable()
class Test(Atom):
    name = "test"

    def im(self):
        return "blah"

    im_c = Callable().tag(log=True)
Beispiel #18
0
class Block(Declarative):

    name = d_(Unicode())
    label = d_(Unicode())
    compact_label = d_(Unicode())
    factory = d_(Callable())
    context_name_map = Typed(dict)

    blocks = Property()
    parameters = Property()

    hide = d_(List())

    def initialize(self):
        super().initialize()
        for p in self.parameters:
            if p.name in self.hide:
                p.visible = False

    def get_children(self, child_type):
        return [c for c in self.children if isinstance(c, child_type)]

    def _get_blocks(self):
        return self.get_children(Block)

    def _get_parameters(self):
        return self.get_children(Parameter)
Beispiel #19
0
class PlotView(Control):
    hug_width = set_default('ignore')
    hug_height = set_default('ignore')
    proxy = Typed(ProxyPlotView)
    data = d_(ContainerList())
    setup = d_(Callable(lambda graph: None))

    title = d_(Str())
    labels = d_(Dict(Str(), Str()))

    axis_scales = d_(Dict(Str(), Float()))

    #background_color = d_(Str())
    #foreground = d_(Str())

    antialiasing = d_(Bool(True))
    aspect_locked = d_(Bool(True))

    grid = d_(Tuple(item=Bool(), default=(False, False)))
    grid_alpha = d_(FloatRange(low=0.0, high=1.0, value=0.5))

    multi_axis = d_(Bool(True))

    @observe('data', 'title', 'labels', 'multi_axis', 'antialiasing',
             'axis_scales', 'grid', 'grid_alpha')
    def _update_proxy(self, change):
        """ An observer which sends state change to the proxy.
        """
        # The superclass handler implementation is sufficient.
        super(PlotView, self)._update_proxy(change)
Beispiel #20
0
class T(Base):
    a=Typed(S, ())
    ov=Int()
    b=Float(2.3).tag(unit="bbb", label="blahhafd", low=0.0)
    g=List().tag(low=1)
    #tag(ov, label="mmdkgam")
    def _default_g(self):
        return [S(), S()]
    c=Callable(myfunc)#    c=Coerced(int)#.tag()

    @Callable
    def cc(self, b=2):
        for n in range(10):
            self.boss.progress=n*10
            if self.abort:
                break
            self.ov=n
            print self.ov, self.b
            sleep(0.2)
    cc.tag(label="ppp")
    
    @property
    def mymp(self):
        return dict(b=1, c=True, ov="5")        
    d=Enum('b', 'c', 'ov')#.tag(mapping="mymp")#dict(b=1, c=True, ov="5"))
    f=Enum(1,2,3)
Beispiel #21
0
class _Timeout(Atom):
    """An IOLoop timeout, a UNIX timestamp and a callback"""

    # Reduce memory overhead when there are lots of pending callbacks
    callback = Callable()
    deadline = Float()
    tdeadline = Tuple()

    def __init__(self, deadline, callback, io_loop):
        super(_Timeout, self).__init__()
        if not isinstance(deadline, numbers.Real):
            raise TypeError("Unsupported deadline %r" % deadline)
        self.deadline = deadline
        self.callback = callback
        self.tdeadline = (deadline, next(io_loop._timeout_counter))

    # Comparison methods to sort by deadline, with object id as a tiebreaker
    # to guarantee a consistent ordering.  The heapq module uses __le__
    # in python2.5, and __lt__ in 2.6+ (sort() and most other comparisons
    # use __lt__).
    def __lt__(self, other):
        return self.tdeadline < other.tdeadline

    def __le__(self, other):
        return self.tdeadline <= other.tdeadline
Beispiel #22
0
class BindingConstruct(ConstructNode):
    """ A construct node for an attribute binding.

    """
    #: The name of the attribute being bound.
    name = Str()

    #: The operator symbol used in the enaml source.
    operator = Str()

    #: The python code object to use for the binding.
    code = Typed(CodeType)

    #: The auxiliary code object for the binding. This may be None.
    auxcode = Typed(CodeType)

    #: The function object created for 'code'.
    func = Typed(FunctionType)

    #: The function object created for 'auxcode'.
    auxfunc = Typed(FunctionType)

    #: The operator function to use to bind the code object. This is
    #: updated during the resolution passes over the tree.
    operator_func = Callable()

    @classmethod
    def from_dict(cls, dct):
        self = super(BindingConstruct, cls).from_dict(dct)
        self.name = dct['name']
        self.operator = dct['operator']
        self.code = dct['code']
        self.auxcode = dct['auxcode']
        return self
Beispiel #23
0
class ExperimentActionBase(Declarative):

    # Name of event that triggers command
    event = d_(Unicode())

    dependencies = List()

    match = Callable()

    # Defines order of invocation. Less than 100 invokes before default. Higher
    # than 100 invokes after default. Note that if concurrent is True, then
    # order of execution is not guaranteed.
    weight = d_(Int(50))

    # Arguments to pass to command by keyword
    kwargs = d_(Dict())

    def _default_dependencies(self):
        return get_dependencies(self.event)

    def _default_match(self):
        code = compile(self.event, 'dynamic', 'eval')
        if len(self.dependencies) == 1:
            return partial(simple_match, self.dependencies[0])
        else:
            return partial(eval, code)

    def __str__(self):
        return f'{self.event} (weight={self.weight}; kwargs={self.kwargs})'
Beispiel #24
0
class Coroutine(Input):
    coroutine = d_(Callable())
    args = d_(Tuple())
    force_active = set_default(True)

    def configure_callback(self):
        cb = super().configure_callback()
        return self.coroutine(*self.args, cb).send
Beispiel #25
0
class CallInstruction(BaseInstruction):
    """Call an instrument action and store the result in the database.

    """
    #: List of names to in which to store the return value of the calls.
    #: Their number should match the number of returned values.
    ret_names = List().tag(pref=True)

    #: Arguments to pass to the Action when calling it
    action_kwargs = Typed(OrderedDict, ()).tag(pref=(ordered_dict_to_pref,
                                                     ordered_dict_from_pref))

    def prepare(self):
        """Build the callable accessing driver Feature.

        """
        source = ("def _call_(driver, kwargs, **ch_ids):"
                  "    return {path}(**kwargs)")
        local = {}
        exec(source.format(', '.join(self.ch_ids), self.path), local)
        self._caller = local['_call_']

    def execute(self, task, driver):
        """Get the value of the Feature and store it in the database.

        """
        ch_ids = {
            k: task.format_and_eval_string(v)
            for k, v in self.ch_ids.items()
        }
        action_kwargs = {
            k: task.format_and_eval_string(v)
            for k, v in self.action_kwargs.items()
        }
        res = self._caller(driver, action_kwargs, **ch_ids)
        if self.ret_names:
            for i, name in enumerate(self.get_names):
                task.write_in_database(name, res[i])
        else:
            task.write_in_database(self.id, res)

    # --- Private API ---------------------------------------------------------

    #: Caller function streamlining the process of calling a driver Action.
    _setter = Callable()

    def _post_setattr_ret_names(self, old, new):
        if new:
            self.database_entries = {self.id + '_' + rn: 1.0 for rn in new}
        else:
            return {self.id: 1.0}

    def _default_database_entries(self):
        """Default database names used by the instruction.

        """
        return {self.id: 1.0}
Beispiel #26
0
class DockItem(Declarative):

    #: The plugin to pass to this dock item
    plugin_id = d_(Str())

    #: The factory for creating this dock item
    factory = d_(Callable())

    #: Where to layout this item in the dock area
    layout = d_(Enum('main', 'top', 'left', 'right', 'bottom'))
Beispiel #27
0
class Callback(Input):

    function = d_(Callable())

    def configure_callback(self):
        log.debug('Configuring callback for {}'.format(self.name))
        return self.function

    def _get_active(self):
        return True
Beispiel #28
0
class LeastSqFitter(Atom):
    """Atom wrapper for least square fitting"""
    fit_func = Callable().tag(private=True)
    p_guess_func = Callable().tag(private=True)
    fit_params = Typed(ndarray)
    p_guess = Typed(ndarray)

    @private_property
    def resid_func(self):
        def residuals(p, x, y):
            return y - self.fit_func(x, p)

        return residuals

    def leastsq_fit(self, x, y, *args, **kwargs):
        pguess = self.p_guess_func(x, y, *args, **kwargs)
        pbest = leastsq(self.resid_func, pguess, args=(x, y), full_output=1)[0]
        return pbest

    def full_fit(self, x, y, indices=None, *args, **kwargs):
        print "started leastsq fitting"
        tstart = time()
        if indices is None:
            indices = range(len(y))
        fit_params = [
            self.leastsq_fit(x, y[n], *args, **kwargs) for n in indices
        ]
        self.fit_params = array(zip(*fit_params)).transpose()
        print "ended leastsq fitting {}".format(time() - tstart)
        return self.fit_params

    def make_p_guess(self, x, y, indices=None, *args, **kwargs):
        if indices is None:
            indices = range(len(y))
        pguess = [self.p_guess_func(x, y[n], *args, **kwargs) for n in indices]
        self.p_guess = array(zip(*pguess)).transpose()
        return self.p_guess

    def reconstruct_fit(self, x, fit_params=None):
        if fit_params is None:
            fit_params = self.fit_params
        return array([self.fit_func(x, fp) for fp in fit_params])
Beispiel #29
0
class BaseLoader(HasPrefAtom):
    """"""

    #: Path to the on-disk file storing the data
    path = Str()

    #: Content of the file (i.e. names, data shape etc)
    # TODO formalize the format of this as possible usage are more clearly identified
    content = Dict()

    #: Maximal size in (MB) a loader is allowed to keep in cache.
    #: Keeping data in cache will improve performance but degrade memory usage.
    caching_limit = Int(100).tag(pref=True)

    #: Callable taking care of applying any in-memory masking required and taking
    #: the data to be masked, the data to generate the mask and the mask
    #: specification for each mask source data.
    #: Callable[ [Dataset, Dataset, Mapping[str, MaskSpecification]], Dataset ]
    mask_data = Callable()

    def load_data(
        self,
        names: Sequence[str],
        masks: Mapping[str, MaskSpecification],
    ) -> Dataset:
        """Load data from the on-disk resource.

        Parameters
        ----------
        names : Sequence[str]
            Names-like string referring to the content of the file.
        masks : Mapping[str, MaskSpecification]
            Mapping of mapping operation to perform on the specified named data, the
            resulting mask are applied to the requested data (see `names`)

        Returns
        -------
        Dataset
            xarray Dataset containing the requested data.

        Raises
        ------
        DataKeyError
            Raised if the name of some data or mask is not found in the on disk store.

        """
        raise NotImplementedError

    def determine_content(self, details=False) -> None:
        raise NotImplementedError

    def clear(self) -> None:
        """Clear any known information about the data file."""
        pass
Beispiel #30
0
class ExperimentCallback(ExperimentActionBase):

    #: Callback to invoke
    callback = d_(Callable())

    def _invoke(self, core, **kwargs):
        params = self._get_params(**kwargs)
        return self.callback(**params)

    def __str__(self):
        return f'ExperimentCallback: {self.callback}'