Beispiel #1
0
    def __init__(
        self,
        app=None,
        protocol=None,
        dir_save=None,
        dir_assets="",
        instance_number=-1,
        database=None,
        metadata_general=None,
        metadata_animal=None,
        loop_protocol=False,
        arduino_config=None,
        log_format="csv",
        trigger_duration_queue=None,
        scope_triggering=None,
        offline=False,
        **kwargs
    ):
        self.arguments = locals()
        super().__init__()

        self.app = app
        self.protocol = protocol

        self.arduino_config = arduino_config

        # If there's a trigger, reference its queue to pass the duration:
        self.trigger = scope_triggering
        if scope_triggering is not None:
            self.trigger_duration_queue = scope_triggering.duration_queue
        self.offline = offline

        self.asset_dir = dir_assets

        if dir_save is None:
            dir_save = tempfile.gettempdir()
        self.base_dir = dir_save
        self.database = database
        self.use_db = True if database else False
        self.log_format = log_format
        self.loop_protocol = loop_protocol

        self.dc = DataCollector(
            folder_path=self.base_dir, instance_number=instance_number
        )

        self.window_main = None
        self.scope_config = None
        self.arduino_board = None
        self.abort = False

        self.logger = logging.getLogger()
        self.logger.setLevel("INFO")

        # We will collect data only of a directory for saving is specified:
        # Conditional, in case metadata are generated and passed from the
        # configuration file:
        if metadata_general is None:
            self.metadata = GeneralMetadata(tree=self.dc)
        else:
            self.metadata = metadata_general(tree=self.dc)

        if metadata_animal is None:
            self.metadata_animal = AnimalMetadata(tree=self.dc)
        else:
            self.metadata_animal = metadata_animal(tree=self.dc)

        # This is done to save GUI configuration:
        self.gui_params = Parametrized(
            "gui", tree=self.dc, params=dict(geometry=Param(""), window_state=Param(""))
        )

        self.dc.add(self.protocol)

        self.protocol_runner = ProtocolRunner(experiment=self)

        # assign signals from protocol_runner to be used externally:
        self.sig_protocol_finished = self.protocol_runner.sig_protocol_finished
        self.sig_protocol_started = self.protocol_runner.sig_protocol_started

        self.protocol_runner.sig_protocol_finished.connect(self.end_protocol)

        self.i_run = 0
        self.current_timestamp = datetime.datetime.now()

        self.gui_timer = QTimer()
        self.gui_timer.setSingleShot(False)

        self.t0 = datetime.datetime.now()

        self.animal_id = None
        self.session_id = None
Beispiel #2
0
    def __init__(
        self,
        app=None,
        protocols=None,
        dir_save=None,
        metadata_general=None,
        metadata_animal=None,
        calibrator=None,
        asset_directory="",
        log_format="csv",
        rec_stim_every=None,
        display_config=None,
        trigger=None,
    ):
        """ """
        super().__init__()

        self.app = app
        self.protocols = protocols
        self.trigger = trigger

        self.asset_dir = asset_directory
        self.base_dir = dir_save
        self.log_format = log_format

        if calibrator is None:
            self.calibrator = CrossCalibrator()
        else:
            self.calibrator = calibrator

        self.window_main = None
        self.scope_config = None
        self.abort = False

        self.logger = logging.getLogger()
        self.logger.setLevel("INFO")

        # to the constructor we need to pass classes, not instances
        # otherwise there are problems because the metadatas are QObjects
        if metadata_general is None:
            self.metadata = GeneralMetadata()
        else:
            self.metadata = metadata_general()

        if metadata_animal is None:
            self.metadata_animal = AnimalMetadata()
        else:
            self.metadata_animal = metadata_animal()

        # We will collect data only of a directory for saving is specified:
        if self.base_dir is not None:
            self.dc = DataCollector(folder_path=self.base_dir)
            self.dc.add_param_tree(self.metadata._params)
            # Use the DataCollector object to find the last used protocol, to
            # restore it
            self.last_protocol = self.dc.get_last_value(
                "stimulus_protocol_params")
        else:
            self.dc = None
            self.last_protocol = None

        self.protocol_runner = ProtocolRunner(experiment=self,
                                              protocol=self.last_protocol)

        # assign signals from protocol_runner to be used externally:
        self.sig_protocol_finished = self.protocol_runner.sig_protocol_finished
        self.sig_protocol_started = self.protocol_runner.sig_protocol_started

        self.protocol_runner.sig_protocol_finished.connect(self.end_protocol)

        if display_config is None:
            self.display_config = dict(full_screen=False)
        else:
            self.display_config = display_config

        self.window_display = StimulusDisplayWindow(
            self.protocol_runner,
            self.calibrator,
            record_stim_every=rec_stim_every)

        if self.display_config.get("window_size", None) is not None:
            self.window_display.params["size"] = self.display_config[
                "window_size"]
            self.window_display.set_dims()

        self.current_instance = self.get_new_name()
        self.i_run = 0
        if self.base_dir is not None:
            self.folder_name = self.base_dir + "/" + self.current_instance
            if not os.path.isdir(self.folder_name):
                os.makedirs(self.folder_name)
        else:
            self.folder_name = None
Beispiel #3
0
class Experiment(QObject):
    """General class that runs an experiment.

    Parameters
    ----------
    app : QApplication()
        Application to run the Experiment QObject.
    protocol : object of :class:`Protocol <stytra.stimulation.Protocol>`
        list of protocols that can be run in this experiment session.
    directory : str
        (optional) Directory where metadata will be saved. If None, nothing
        will be
        saved (default: None).
    metadata_general: :class:`GeneralMetadata <stytra.metadata.GeneralMetadata>` object
        (optional) Class for saving general metadata about the experiment. I
        If not passed, a default GeneralMetadata object will be set.
    metadata_animal: :class:`AnimalMetadata <stytra.metadata.AnimalMetadata>` object
        (optional) Class for saving animal metadata about the experiment.
        If not passed, a default AnimalMetadata object will be set.
    calibrator : :class:`Calibrator <stytra.calibration.Calibrator>` object
        (optional) Calibrator object to calibrate the stimulus display. If
        not set, a CrossCalibrator will be used.
    asset_directory : str
        (optional) Path where asset files such as movies or images to be
        displayed can be found.
    display: dict
        (optional) Dictionary with specifications for the display. Possible
        key values are
        full_screen: bool (False)
        window_size: Tuple(Int, Int)
        framerate: target framerate, if 0, it is the highest possilbe
        gl_display : bool (False)
    rec_stim_framerate : int
        (optional) Set to record a movie of the displayed visual stimulus. It
        specifies every how many frames one will be saved (set to 1 to
        record) all displayed frames. The final movie will be saved in the
        directory in an .h5 file.
    trigger : :class:`Trigger <stytra.triggering.Trigger>` object
        (optional) Trigger class to control the beginning of the stimulation.
    offline : bool
        if stytra is used in offline analysis, stimulus is not displayed
    log_format : str
        one of "csv", "feather", "hdf5" (pytables-based) or "json"
    """

    sig_data_saved = pyqtSignal()

    def __init__(
        self,
        app=None,
        protocol=None,
        dir_save=None,
        dir_assets="",
        instance_number=-1,
        database=None,
        metadata_general=None,
        metadata_animal=None,
        loop_protocol=False,
        arduino_config=None,
        log_format="csv",
        trigger_duration_queue=None,
        scope_triggering=None,
        offline=False,
        **kwargs
    ):
        self.arguments = locals()
        super().__init__()

        self.app = app
        self.protocol = protocol

        self.arduino_config = arduino_config

        # If there's a trigger, reference its queue to pass the duration:
        self.trigger = scope_triggering
        if scope_triggering is not None:
            self.trigger_duration_queue = scope_triggering.duration_queue
        self.offline = offline

        self.asset_dir = dir_assets

        if dir_save is None:
            dir_save = tempfile.gettempdir()
        self.base_dir = dir_save
        self.database = database
        self.use_db = True if database else False
        self.log_format = log_format
        self.loop_protocol = loop_protocol

        self.dc = DataCollector(
            folder_path=self.base_dir, instance_number=instance_number
        )

        self.window_main = None
        self.scope_config = None
        self.arduino_board = None
        self.abort = False

        self.logger = logging.getLogger()
        self.logger.setLevel("INFO")

        # We will collect data only of a directory for saving is specified:
        # Conditional, in case metadata are generated and passed from the
        # configuration file:
        if metadata_general is None:
            self.metadata = GeneralMetadata(tree=self.dc)
        else:
            self.metadata = metadata_general(tree=self.dc)

        if metadata_animal is None:
            self.metadata_animal = AnimalMetadata(tree=self.dc)
        else:
            self.metadata_animal = metadata_animal(tree=self.dc)

        # This is done to save GUI configuration:
        self.gui_params = Parametrized(
            "gui", tree=self.dc, params=dict(geometry=Param(""), window_state=Param(""))
        )

        self.dc.add(self.protocol)

        self.protocol_runner = ProtocolRunner(experiment=self)

        # assign signals from protocol_runner to be used externally:
        self.sig_protocol_finished = self.protocol_runner.sig_protocol_finished
        self.sig_protocol_started = self.protocol_runner.sig_protocol_started

        self.protocol_runner.sig_protocol_finished.connect(self.end_protocol)

        self.i_run = 0
        self.current_timestamp = datetime.datetime.now()

        self.gui_timer = QTimer()
        self.gui_timer.setSingleShot(False)

        self.t0 = datetime.datetime.now()

        self.animal_id = None
        self.session_id = None

    @property
    def folder_name(self):
        foldername = os.path.join(
            self.base_dir, self.protocol.__class__.name, self.animal_id
        )
        if not os.path.isdir(foldername):
            os.makedirs(foldername)
        return foldername

    def filename_prefix(self):
        return self.session_id + "_"

    def filename_base(self):
        # Save clean json file as timestamped Ymd_HMS_metadata.h5 files:
        return os.path.join(self.folder_name, self.filename_prefix())

    def save_log(self, log, name, category="tracking"):
        logname = log.save(self.filename_base() + name, self.log_format)

        self.dc.add_static_data(logname, category + "/" + name)

    def initialize_plots(self):
        pass

    def set_id(self):
        self.animal_id = (
            self.current_timestamp.strftime("%y%m%d")
            + "_f"
            + str(self.metadata_animal.id)
        )
        self.session_id = self.current_timestamp.strftime("%H%M%S")

    def reset(self):
        self.t0 = datetime.datetime.now()
        if self.protocol_runner.dynamic_log is not None:
            self.protocol_runner.dynamic_log.reset()

        self.protocol_runner.framerate_acc.reset()

    def start_experiment(self):
        """Start the experiment creating GUI and initialising metadata.

        Parameters
        ----------

        Returns
        -------

        """
        self.gui_timer.start(1000 // 60)

        self.dc.restore_from_saved()
        self.set_id()

        if self.arduino_config is not None:
            from stytra.hardware.external_pyfirmata import PyfirmataConnection

            self.arduino_board = PyfirmataConnection(
                com_port=self.arduino_config["com_port"],
                layout=self.arduino_config["layout"],
            )

        self.make_window()
        self.protocol_runner.update_protocol()

        if self.trigger is not None:
            self.trigger.start()

    def restore_window_state(self):
        if self.gui_params.window_state:
            self.window_main.restoreState(
                QByteArray.fromHex(bytes(self.gui_params.window_state, "ascii"))
            )
            self.window_main.restoreGeometry(
                QByteArray.fromHex(bytes(self.gui_params.geometry, "ascii"))
            )

    def make_window(self):
        """Make experiment GUI, defined in children depending on experiments."""
        self.window_main = ExperimentWindow(self)

        self.window_main.construct_ui()
        self.window_main.show()

    def check_trigger(self):
        self.abort = False
        # If we have a trigger and trigger option is set:
        if self.trigger is not None and self.window_main.chk_scope.isChecked():
            # Put duration of the experiment in the queue for the trigger:
            duration_exp = self.protocol_runner.duration
            self.trigger_duration_queue.put(duration_exp)

            self.logger.info("Waiting for trigger signal...")

            # Open message box for aborting:
            msg = QMessageBox()
            msg.setText("Waiting for trigger event...")
            msg.setStandardButtons(QMessageBox.Abort)
            msg.buttonClicked.connect(self.abort_start)
            msg.show()

            # While loop to keep processing application events while we
            # are listening to the trigger (otherwise app would be stuck):
            while True and not self.abort:
                if (
                    self.trigger.start_event.is_set()
                    and not self.protocol_runner.running
                ):
                    msg.close()
                    return
                else:
                    self.app.processEvents()

    def read_scope_data(self):
        """Read data from an external acquisition device triggered with stytra.
        Currently we assume this comes from a microscope, thus the logging in
        "imaging/microscope_config". To be changed in a future version maybe.
        """
        if self.trigger is not None:
            try:
                self.scope_config = self.trigger.device_params_queue.get(timeout=0.001)
                self.logger.info(self.scope_config)
                if self.dc is not None:
                    self.dc.add_static_data(
                        self.scope_config, "imaging/microscope_config"
                    )
            except Empty:
                self.logger.info("No trigger configuration received")

    def start_protocol(self):
        """Start the protocol from the ProtocolRunner. Before that, send a
        a notification and if required communicate with the microscope to
        synchronize and read configuration.

        Parameters
        ----------

        Returns
        -------

        """
        self.check_trigger()
        self.reset()
        self.protocol_runner.start()
        self.read_scope_data()

    def abort_start(self):
        self.logger.info("Aborted")
        self.abort = True

    def save_data(self):
        """Called at the end of the experiment to save all logs."""
        if self.base_dir is not None:
            if self.dc is not None:
                self.dc.add_static_data(self.protocol_runner.log, name="stimulus/log")
                self.dc.add_static_data(self.t0, name="general/t_protocol_start")
                self.dc.add_static_data(
                    self.protocol_runner.t_end, name="general/t_protocol_end"
                )
                self.dc.add_static_data(self.animal_id, name="general/fish_id")
                self.dc.add_static_data(self.session_id, name="general/session_id")

                if self.database is not None and self.use_db:
                    db_id = self.database.insert_experiment_data(
                        self.dc.get_clean_dict(
                            eliminate_df=True, convert_datetime=False
                        )
                    )
                else:
                    db_id = -1
                self.dc.add_static_data(db_id, name="general/db_index")

                # Clean up arguments dict:
                try:
                    kwargs = self.arguments.pop("kwargs")
                    self.arguments.update(kwargs)
                except KeyError:
                    pass

                # Get program name and version and save to the data_log:
                git_hash = None
                version = None

                try:

                    ####################### try get repo even during testing ###############################
                    if (
                        "pytest" in sys.argv[0]
                    ):  # if first element points to pytest, check other addresses in args

                        for el in sys.argv:
                            if os.path.isdir(el):
                                repo = git.Repo(el, search_parent_directories=True)
                                break

                    else:

                        repo = git.Repo(sys.argv[0], search_parent_directories=True)
                        git_hash = repo.head.object.hexsha
                    #########################################################################################

                    try:
                        version = pkg_resources.get_distribution("stytra").version
                    except pkg_resources.DistributionNotFound:
                        self.logger.info("Could not find stytra version")

                except git.InvalidGitRepositoryError:
                    self.logger.info("Invalid git repository")
                except git.exc.NoSuchPathError:
                    self.logger.info("Path not a git repository")

                self.dc.add_static_data(
                    dict(
                        git_hash=git_hash,
                        name=sys.argv[0],
                        arguments=self.arguments,
                        version=version,
                        dependencies=list(imports()),
                    ),
                    name="general/program_version",
                )

                self.dc.save(self.filename_base() + "metadata.json")  # save data_log
                self.logger.info(
                    "Saved log files under {}".format(self.filename_base())
                )

            if self.protocol_runner.dynamic_log is not None:
                self.save_log(
                    self.protocol_runner.dynamic_log, "stimulus_log", "stimulus"
                )

            self.sig_data_saved.emit()

    def end_protocol(self, save=True):
        """Function called at Protocol end. Reset Protocol and save
        data_log.

        Parameters
        ----------
        save : bool
             Specify whether to save experiment data (Default value = True).

        Returns
        -------

        """

        self.protocol_runner.stop()
        self.set_id()

        if save:
            self.save_data()

        self.i_run += 1
        self.current_timestamp = datetime.datetime.now()

        self.reset()
        if self.loop_protocol and self.protocol_runner.completed:
            self.protocol_runner.reset()
            self.start_protocol()
        else:
            self.protocol_runner.reset()

    def wrap_up(self, *args, **kwargs):
        """Clean up things before closing gui. Called by close button.

        Parameters
        ----------
        *args :

        **kwargs :


        Returns
        -------

        """
        if self.protocol_runner is not None:
            self.protocol_runner.timer.stop()
            if (
                self.protocol_runner.protocol is not None
                and self.protocol_runner.running
            ):
                self.end_protocol(save=False)

        if self.trigger is not None:
            self.trigger.kill_event.set()
            self.trigger.join()

        if self.arduino_board is not None:
            self.arduino_board.close()

        st = self.window_main.saveState()
        geom = self.window_main.saveGeometry()
        self.gui_params.window_state = bytes(st.toHex()).decode("ascii")
        self.gui_params.geometry = bytes(geom.toHex()).decode("ascii")
        self.dc.save_config_file()
        self.app.closeAllWindows()

    def excepthook(self, exctype, value, tb):
        """

        Parameters
        ----------
        exctype :

        value :

        tb :


        Returns
        -------

        """
        traceback.print_tb(tb)
        print("{0}: {1}".format(exctype, value))
        self.trigger.kill_event.set()
        self.trigger.join()
Beispiel #4
0
class Experiment(QObject):
    """General class that runs an experiment.

    Parameters
    ----------
    app : QApplication()
        Application to run the Experiment QObject.
    protocols : list of :class:`Protocol <stytra.stimulation.Protocol>` classes
        list of protocols that can be run in this experiment session.
    directory : str
        (optional) Directory where metadata will be saved. If None, nothing
        will be
        saved (default: None).
    metadata_general: :class:`GeneralMetadata <stytra.metadata.GeneralMetadata>` object
        (optional) Class for saving general metadata about the experiment. I
        If not passed, a default GeneralMetadata object will be set.
    metadata_animal: :class:`AnimalMetadata <stytra.metadata.AnimalMetadata>` object
        (optional) Class for saving animal metadata about the experiment.
        If not passed, a default AnimalMetadata object will be set.
    calibrator : :class:`Calibrator <stytra.calibration.Calibrator>` object
        (optional) Calibrator object to calibrate the stimulus display. If
        not set, a CrossCalibrator will be used.
    asset_directory : str
        (optional) Path where asset files such as movies or images to be
        displayed can be found.
    display_config: dict
        (optional) Dictionary with specifications for the display. Possible
        key values are "full_screen" and "window_size".
    rec_stim_every : int
        (optional) Set to record a movie of the displayed visual stimulus. It
        specifies every how many frames one will be saved (set to 1 to
        record) all displayed frames. The final movie will be saved in the
        directory in an .h5 file.
    trigger : :class:`Trigger <stytra.triggering.Trigger>` object
        (optional) Trigger class to control the beginning of the stimulation.


    """
    def __init__(
        self,
        app=None,
        protocols=None,
        dir_save=None,
        metadata_general=None,
        metadata_animal=None,
        calibrator=None,
        asset_directory="",
        log_format="csv",
        rec_stim_every=None,
        display_config=None,
        trigger=None,
    ):
        """ """
        super().__init__()

        self.app = app
        self.protocols = protocols
        self.trigger = trigger

        self.asset_dir = asset_directory
        self.base_dir = dir_save
        self.log_format = log_format

        if calibrator is None:
            self.calibrator = CrossCalibrator()
        else:
            self.calibrator = calibrator

        self.window_main = None
        self.scope_config = None
        self.abort = False

        self.logger = logging.getLogger()
        self.logger.setLevel("INFO")

        # to the constructor we need to pass classes, not instances
        # otherwise there are problems because the metadatas are QObjects
        if metadata_general is None:
            self.metadata = GeneralMetadata()
        else:
            self.metadata = metadata_general()

        if metadata_animal is None:
            self.metadata_animal = AnimalMetadata()
        else:
            self.metadata_animal = metadata_animal()

        # We will collect data only of a directory for saving is specified:
        if self.base_dir is not None:
            self.dc = DataCollector(folder_path=self.base_dir)
            self.dc.add_param_tree(self.metadata._params)
            # Use the DataCollector object to find the last used protocol, to
            # restore it
            self.last_protocol = self.dc.get_last_value(
                "stimulus_protocol_params")
        else:
            self.dc = None
            self.last_protocol = None

        self.protocol_runner = ProtocolRunner(experiment=self,
                                              protocol=self.last_protocol)

        # assign signals from protocol_runner to be used externally:
        self.sig_protocol_finished = self.protocol_runner.sig_protocol_finished
        self.sig_protocol_started = self.protocol_runner.sig_protocol_started

        self.protocol_runner.sig_protocol_finished.connect(self.end_protocol)

        if display_config is None:
            self.display_config = dict(full_screen=False)
        else:
            self.display_config = display_config

        self.window_display = StimulusDisplayWindow(
            self.protocol_runner,
            self.calibrator,
            record_stim_every=rec_stim_every)

        if self.display_config.get("window_size", None) is not None:
            self.window_display.params["size"] = self.display_config[
                "window_size"]
            self.window_display.set_dims()

        self.current_instance = self.get_new_name()
        self.i_run = 0
        if self.base_dir is not None:
            self.folder_name = self.base_dir + "/" + self.current_instance
            if not os.path.isdir(self.folder_name):
                os.makedirs(self.folder_name)
        else:
            self.folder_name = None

    def get_new_name(self):
        return (datetime.datetime.now().strftime("%y%m%d") + "_f" +
                str(self.metadata_animal.params["id"]))

    def filename_base(self):
        # Save clean json file as timestamped Ymd_HMS_metadata.h5 files:
        return self.folder_name + "/{:03d}_".format(self.i_run)

    def start_experiment(self):
        """Start the experiment creating GUI and initialising metadata.

        Parameters
        ----------

        Returns
        -------

        """
        self.app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5())
        self.make_window()
        self.initialize_metadata()
        self.show_stimulus_screen(self.display_config["full_screen"])
        if self.trigger is not None:
            self.trigger.start()

    def make_window(self):
        """Make experiment GUI, defined in children depending on experiments.
        """
        self.window_main = SimpleExperimentWindow(self)
        self.window_main.show()

    def initialize_metadata(self):
        """Restore parameters from saved config.h5 file.
        """
        # When restoring here data_log to previous values, there may be
        # multiple (one per parameter), calls of functions connected to
        # a change in the params three state.
        # See comment in DataCollector.restore_from_saved()
        if self.dc is not None:
            self.dc.restore_from_saved()

    def show_stimulus_screen(self, full_screen=True):
        """Open window to display the visual stimulus and make it full-screen
        if necessary.

        Parameters
        ----------
        full_screen :
             (Default value = True)

        Returns
        -------

        """
        self.window_display.show()
        if full_screen:
            try:
                self.window_display.windowHandle().setScreen(
                    self.app.screens()[1])
                self.window_display.showFullScreen()
            except IndexError:
                print("Second screen not available")

    def start_protocol(self):
        """Start the protocol from the ProtocolRunner. Before that, send a
        a notification and if required communicate with the microscope to
        synchronize and read configuration.

        Parameters
        ----------

        Returns
        -------

        """
        self.abort = False
        if self.trigger is not None and self.window_main.chk_scope.isChecked():
            self.logger.info("Waiting for trigger signal...")
            msg = QMessageBox()
            msg.setText("Waiting for trigger event...")
            msg.setStandardButtons(QMessageBox.Abort)
            msg.buttonClicked.connect(self.abort_start)
            msg.show()
            while True and not self.abort:
                if (self.trigger.start_event.is_set()
                        and not self.protocol_runner.running):
                    msg.close()
                    self.protocol_runner.start()
                    try:
                        self.scope_config = self.trigger.queue_trigger_params.get(
                            timeout=0.001)

                        if self.dc is not None:
                            self.dc.add_static_data(
                                self.scope_config, "imaging_microscope_config")
                    except Empty:
                        self.logger.info("No trigger configuration received")
                    break
                else:
                    self.app.processEvents()
        else:
            self.protocol_runner.start()

    def abort_start(self):
        self.logger.info("Aborted")
        self.abort = True

    def end_protocol(self, save=True):
        """Function called at Protocol end. Reset Protocol and save
        data_log.

        Parameters
        ----------
        save : bool
             Specify whether to save experiment data (Default value = True).

        Returns
        -------

        """

        self.protocol_runner.stop()
        if self.base_dir is not None:
            if self.dc is not None:
                self.dc.add_static_data(self.protocol_runner.log,
                                        name="stimulus_log")
                self.dc.add_static_data(self.protocol_runner.t_start,
                                        name="general_t_protocol_start")
                self.dc.add_static_data(self.protocol_runner.t_end,
                                        name="general_t_protocol_end")
                self.dc.save(self.filename_base() +
                             "metadata.json")  # save data_log

                # save the movie if it is generated
                movie = self.window_display.widget_display.get_movie()
                if movie is not None:
                    movie_dict = dict(movie=movie[0], movie_times=movie[1])
                    dd.io.save(
                        self.filename_base() + "stim_movie.h5",
                        movie_dict,
                        compression="blosc",
                    )

            if self.protocol_runner.dynamic_log is not None:
                self.protocol_runner.dynamic_log.save(
                    self.filename_base() + "dynamic_log", self.log_format)

        self.protocol_runner.reset()
        self.i_run += 1

    def wrap_up(self, *args, **kwargs):
        """Clean up things before closing gui. Called by close button.

        Parameters
        ----------
        *args :
            
        **kwargs :
            

        Returns
        -------

        """
        if self.protocol_runner is not None:
            self.protocol_runner.timer.stop()
            if self.protocol_runner.protocol is not None:
                self.end_protocol(save=False)
        if self.trigger is not None:
            self.trigger.kill_event.set()
            # self.trigger.join()
            self.trigger.terminate()
        self.app.closeAllWindows()

    def excepthook(self, exctype, value, tb):
        """

        Parameters
        ----------
        exctype :

        value :

        tb :


        Returns
        -------

        """
        traceback.print_tb(tb)
        print("{0}: {1}".format(exctype, value))
        self.trigger.kill_event.set()
        self.trigger.terminate()