Skip to content
Snippets Groups Projects
explouim.py 37.2 KiB
Newer Older
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
import warnings
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
import scipy.optimize.optimize
from PyQt5.QtCore import *
import pandas as pd
from pandas.api.types import is_numeric_dtype
from pyqtgraph.GraphicsScene.mouseEvents import MouseClickEvent

import utils
from config import Config
from dataprovider.exploprovider import ExploProvider
from dataprovider.icbktransitionprovider import IcbktransitionProvider
from gui.ui_mainwindow import Ui_MainWindow
from gui.stab_dialog import StabDialog
from cfatools.logreader.dataset import DatasetReader
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
from cfatools.tsanalyser import transition
class ExploUim(QObject):

    sig_dataset_loaded = pyqtSignal(DatasetReader, name="dataset_loaded")
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
    sig_cursor_moved = pyqtSignal(QTime)

    # "Variables" tableWidget columns identifiers.
    INSTRUMENT_COL = 0
    VARIABLE_COL = 1
    COLOR_COL = 2
    OFFSET_COL = 3
    MULT_COL = 4
    TIMESHIFT_COL = 5
    VISIBLE_COL = 6
    XORIG_COL = 7
    YORIG_COL = 8
    DEFAULT_COLORS = [
        QColor(228, 26, 28),  # Red
        QColor(55, 126, 184),  # Blue
        QColor(77, 175, 74),  # Green
        QColor(152, 78, 163),  # Violet
        QColor(255, 127, 0),  # Orange
        QColor(255, 255, 51),  # Yellow
        QColor(166, 86, 40),  # Brown
        QColor(247, 129, 191),  # Pink
    ]

    def __init__(
        self,
        explo_prvd: ExploProvider,
        icbktransition_prvd: IcbktransitionProvider,
        main_ui: Ui_MainWindow,
        config: Config,
        stab_dialog: StabDialog,
    ):
        self.main_ui = main_ui
        self.explo_prvd = explo_prvd
        self.icbktransition_prvd = icbktransition_prvd
        self.stab_dialog = stab_dialog
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.dataset_readers = dict()
        # The "var_id" is used to identify and match table's lines and plot's items (curves, etc.). Table's row id can
        # not be used because it changes when previous rows are deleted.
        self.current_variable_id = 0

        self.__initialize_variable_table__()
        self.__initialize_plot__()

        self.explo_prvd.sig_dataset_loaded.connect(self.__initialize_plot__)
        self.explo_prvd.sig_dataset_loaded.connect(self.__reset_variable_table__)
        self.explo_prvd.sig_dataset_loaded.connect(self.__refresh_existing_setups__)
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.icbktransition_prvd.sig_transitions_df_updated.connect(self.__update_icbk_transition__)
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.main_ui.explo_pushbutton_dataset_load.clicked.connect(self.__load_dataset__)
        self.main_ui.explo_pushbutton_add_row.clicked.connect(self.__add_new_row_in_variable_table__)
        self.main_ui.explo_tablewidget_variables.cellClicked.connect(self.__change_color__)
        # Manual events display
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.main_ui.explo_checkbox_manualevent.stateChanged.connect(self.__update_manual_event__)
        # Transitions vline display
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.main_ui.explo_checkbox_icbktransition.stateChanged.connect(self.__update_icbk_transition__)
        # Export plot
        self.main_ui.explo_pushbutton_export_plot.clicked.connect(self.__export_plot__)

        # Save/load current setup
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.main_ui.explo_lineedit_setup_name.textChanged.connect(self.__check_setup_save_name__)
        self.main_ui.explo_pushbutton_setup_save.clicked.connect(self.__save_setup__)
        self.main_ui.explo_listwidget_setup_list.itemClicked.connect(
            lambda: self.main_ui.explo_pushbutton_setup_load.setEnabled(True)
        )
        self.main_ui.explo_pushbutton_setup_load.clicked.connect(self.__load_setup__)

        # Stabilization time analysis
        self.__init_stab__()
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.main_ui.explo_pushbutton_stab.clicked.connect(self.__show_stab_dialog__)
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.region = None
        self.rolling_avg_duration = 0
    def __load_dataset__(self):
        # Get the directory containing the dataset's files.
        directory = utils.get_data_path(self.config, "datasets", "dataset")
        if directory == "":
        self.main_ui.explo_pushbutton_icbktransition.setEnabled(False)
        self.main_ui.explo_checkbox_icbktransition.setEnabled(False)

            self.explo_prvd.get_dataset_reader(directory)
        except ValueError as e:
            utils.show_popup(str(e), "Could not load data set!")
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.main_ui.explo_lineedit_dataset.setText(self.explo_prvd.current_dataset.dataset_name)
        # Enable control which are disabled by default, i.e. when no dataset is yet loaded.
        self.main_ui.explo_pushbutton_export_plot.setEnabled(True)
        self.main_ui.explo_pushbutton_add_row.setEnabled(True)
        self.main_ui.explo_pushbutton_stab.setEnabled(True)
        self.main_ui.explo_checkbox_manualevent.setEnabled(True)
        self.main_ui.explo_lineedit_setup_name.setEnabled(True)

    ####################################################################################################################
    # "Variables" table

    def __initialize_variable_table__(self):
        """Initialize the table containing the to-be-displayed variables"""
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.main_ui.explo_tablewidget_variables.setColumnWidth(self.INSTRUMENT_COL, 150)
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.main_ui.explo_tablewidget_variables.setColumnWidth(self.VARIABLE_COL, 130)
        self.main_ui.explo_tablewidget_variables.setColumnWidth(self.COLOR_COL, 55)
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.main_ui.explo_tablewidget_variables.setColumnWidth(self.OFFSET_COL, 100)
        self.main_ui.explo_tablewidget_variables.setColumnWidth(self.MULT_COL, 120)
        self.main_ui.explo_tablewidget_variables.setColumnWidth(self.TIMESHIFT_COL, 100)
        self.main_ui.explo_tablewidget_variables.setColumnWidth(self.VISIBLE_COL, 70)
        self.main_ui.explo_tablewidget_variables.setColumnWidth(self.XORIG_COL, 90)
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.main_ui.explo_tablewidget_variables.setColumnWidth(self.YORIG_COL, 200)
    def __reset_variable_table__(self):
        self.main_ui.explo_tablewidget_variables.setRowCount(0)
        self.__add_new_row_in_variable_table__()

    def __add_new_row_in_variable_table__(self):
        """Add a new row in the calibration table. Initialize the cell's content and properties."""
        table = self.main_ui.explo_tablewidget_variables
        row_id = table.rowCount()
        table.insertRow(row_id)
        table.setCellWidget(row_id, self.INSTRUMENT_COL, instrument_item)
        table.setCellWidget(row_id, self.VARIABLE_COL, variable_item)
        table.cellWidget(row_id, self.VARIABLE_COL).currentTextChanged.connect(
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
            lambda text, row_id=row_id: self.__apply_variable_change__(row_id=row_id, variable_name=text)
        # Connect Instrument change to variables display
        table.cellWidget(row_id, self.INSTRUMENT_COL).currentTextChanged.connect(
            lambda text, row_id=row_id: self.__update_variables_combobox__(
                combobox_text=text, variables_combobox=variable_item
            )
        )
        color_item.setBackground(self.DEFAULT_COLORS[row_id % len(self.DEFAULT_COLORS)])
        table.setItem(row_id, self.COLOR_COL, color_item)

        # Offset
        offset_item = QDoubleSpinBox()
        offset_item.setMinimum(-10000.0)
        offset_item.setMaximum(10000.0)
        table.setCellWidget(row_id, self.OFFSET_COL, offset_item)
        table.cellWidget(row_id, self.OFFSET_COL).valueChanged.connect(
            lambda value, row_id=row_id: self.__apply_variable_change__(row_id=row_id)
        )

        # Multiplicative factor
        mult_item = QDoubleSpinBox()
        mult_item.setValue(1.0)
        mult_item.setMinimum(-10000.0)
        mult_item.setMaximum(10000.0)
        table.setCellWidget(row_id, self.MULT_COL, mult_item)
        table.cellWidget(row_id, self.MULT_COL).valueChanged.connect(
            lambda value, row_id=row_id: self.__apply_variable_change__(row_id=row_id)
        )

        # Time shift
        timeshift_item = QDoubleSpinBox()
        timeshift_item.setMinimum(-10000.0)
        timeshift_item.setMaximum(10000.0)
        table.setCellWidget(row_id, self.TIMESHIFT_COL, timeshift_item)
        table.cellWidget(row_id, self.TIMESHIFT_COL).valueChanged.connect(
            lambda value, row_id=row_id: self.__apply_variable_change__(row_id=row_id)
        )

        # Visible
        visible_item = QCheckBox()
        visible_item.setChecked(True)
        table.setCellWidget(row_id, self.VISIBLE_COL, visible_item)
        table.cellWidget(row_id, self.VISIBLE_COL).stateChanged.connect(
            lambda state, row_id=row_id: self.__apply_variable_change__(row_id=row_id)
        )

        # X original
        xorig_item = QTableWidgetItem()
        xorig_item.setFlags(Qt.ItemIsEnabled)  # Read only
        table.setItem(row_id, self.XORIG_COL, xorig_item)

        # Y original
        yorig_item = QTableWidgetItem()
        yorig_item.setFlags(Qt.ItemIsEnabled)  # Read only
        table.setItem(row_id, self.YORIG_COL, yorig_item)

        self.__update_instruments_combobox__(
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
            self.main_ui.explo_tablewidget_variables.cellWidget(row_id, self.INSTRUMENT_COL)
        if hasattr(self, "plot_item"):
            self.plot_item.getViewBox().enableAutoRange(enable=True)

    def __update_instruments_combobox__(self, combobox: QComboBox):
        combobox.clear()
        for instrument_name in self.explo_prvd.current_dataset.get_instruments_names():
            if instrument_name == "manual-event":
                continue
            combobox.addItem(instrument_name)
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
    def __update_variables_combobox__(self, combobox_text: str, variables_combobox: QComboBox):
        if combobox_text == "":
            return
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        instrument_name = combobox_text
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        variable_names = self.explo_prvd.get_instrument_variables(self.explo_prvd.current_dataset, instrument_name)
        for variable_name in variable_names:
            variables_combobox.addItem(variable_name)

    def __change_color__(self, row: int, column: int):
        if column != self.COLOR_COL:
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.main_ui.explo_tablewidget_variables.item(row, self.COLOR_COL).setBackground(color)
        self.__apply_variable_change__(row)

    def __get_row_dataframe__(self, row_id: int) -> pd.DataFrame:
        table = self.main_ui.explo_tablewidget_variables

JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        instrument_name = table.cellWidget(row_id, self.INSTRUMENT_COL).currentText()
        variable_name = table.cellWidget(row_id, self.VARIABLE_COL).currentText()

JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        timeseries = self.explo_prvd.get_timeseries(self.explo_prvd.current_dataset, instrument_name, variable_name)

        return timeseries

    def __apply_variable_change__(self, row_id: int, variable_name: str = None) -> None:
        """Read the information related to the row_id and call plot function

        Parameters
        ----------
        row_id
        variable_name
        """
        table = self.main_ui.explo_tablewidget_variables

        # The variable combobox is cleared before variables of the newly-selected instrument are written.
        # This function is called on variable-combobox edition, thus it is also called on combobox clear.
        # So in this case, do nothing, wait for the call related to the filling of the combobox
        if variable_name == "":
            return
        if table.cellWidget(row_id, self.VARIABLE_COL).currentText() == "":
            return

        timeseries = self.__get_row_dataframe__(row_id)

        # Set tooltip
        variable_combobox = table.cellWidget(row_id, self.VARIABLE_COL)
        instrument_name = table.cellWidget(row_id, self.INSTRUMENT_COL).currentText()
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        if variable_name is None:
            variable_name = variable_combobox.currentText()
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        variable_combobox = self.__add_tooltip_to_variable_combobox__(variable_combobox, instrument_name, variable_name)
        # Get color
        color = table.item(row_id, self.COLOR_COL).background().color()

        # Get variable visibility
        visible = table.cellWidget(row_id, self.VISIBLE_COL).isChecked()

        # Get position adjustment variables (offset, multiplicative factor and time shift)
        offset = table.cellWidget(row_id, self.OFFSET_COL).value()
        mult = table.cellWidget(row_id, self.MULT_COL).value()
        timeshift = table.cellWidget(row_id, self.TIMESHIFT_COL).value()

JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
            self.__update_plot__(timeseries, row_id, color, offset, mult, timeshift, visible)
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
            self.main_ui.statusbar.showMessage("Failed to plot [" + variable_name + "]", msecs=3000)
    def __update_xy_original__(self, instant: datetime.datetime):
        table = self.main_ui.explo_tablewidget_variables
        for row_id in range(table.rowCount()):
            # Get X orig (original non-shifted datetime value)
            timeshift_sec = table.cellWidget(row_id, self.TIMESHIFT_COL).value()
            x_orig = instant - datetime.timedelta(seconds=timeshift_sec)
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
            table.item(row_id, self.XORIG_COL).setText(x_orig.strftime("%H:%M:%S.%f")[:-5])

            # Get Y orig (original non-shifted variable value)
            df = self.__get_row_dataframe__(row_id).copy()
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
            df = df[df.index <= instant]
            if len(df.index) == 0:
                y_orig = "Out of range"
            else:
                y_orig = df.iloc[-1]["value"]

            try:
                y_orig = float(y_orig)
            except ValueError:
                pass  # y_orig is probably a string, which is normal for some variables (e.g. Ice core's name)
            else:
                y_orig = "{:.4f}".format(y_orig)
            table.item(row_id, self.YORIG_COL).setText(y_orig)

    def __add_tooltip_to_variable_combobox__(
        self, combobox: QComboBox, instrument_name: str, variable_name: str
    ) -> QComboBox:
        descr_dict = self.explo_prvd.get_variable_description(
            self.explo_prvd.current_dataset, instrument_name, variable_name
        )
        tooltip_str = descr_dict["description"]
        if descr_dict["unit"] != "":
            tooltip_str += "\nUnits: " + descr_dict["unit"]
        combobox.setToolTip(tooltip_str)
        return combobox

    ####################################################################
    # Plot

    def __initialize_plot__(self) -> None:
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.plot_item = pg.PlotItem(axisItems={"bottom": utils.TimeAxisItem(orientation="bottom")})

        self.main_ui.explo_graphicsview_top.setCentralItem(self.plot_item)

        self.lines_items = []

        # Vertical lines for the manual events
        self.event_vlines = []
        self.event_texts = []

        # Vertical lines for the transitions
        self.transition_vlines = []

        # Vertical line following the cursor
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.cursor_vline = pg.InfiniteLine(angle=90, movable=False, pen=pg.mkPen(style=Qt.DotLine))
        self.plot_item.addItem(self.cursor_vline, ignoreBounds=True)

        # Click-fixed vertical line to get measure
        self.measure_vline = pg.InfiniteLine(angle=90, movable=False)
        self.plot_item.addItem(self.measure_vline, ignoreBounds=True)

    def __update_plot__(
        self,
        timeseries: pd.DataFrame,
        row_id: int,
        color: QColor,
        offset: float,
        mult: float,
        timeshift_sec: float,
        visible: bool,
    ) -> None:

        # Get the to-be-modified curve
        if row_id in self.step_curves:
            step_curve = self.step_curves[row_id]
            self.step_curves[row_id] = step_curve
            self.plot_item.addItem(step_curve)
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
            step_curve.scene().sigMouseClicked.connect(lambda event: self.__mouse_clicked__(event))
            step_curve.scene().sigMouseMoved.connect(lambda event: self.__mouse_moved__(event))
        # Set curve visibility
        if not visible:
            step_curve.hide()
        else:
            step_curve.show()

        step_curve.setData(
            x=self.__get_timeseries_x_values__(timeseries, timeshift_sec),
            y=self.__get_timeseries_y_values__(timeseries, offset, mult),
            pen=color,
            stepMode=True,
        )
    def __reset_manual_event_checkbox__(self, dataset: DatasetReader):
        # Enable/disable the possibility to display manual events, depending on the events log file availability.
        has_manual_events = "manual-event" in dataset.get_instruments_names()
        self.main_ui.explo_checkbox_manualevent.setEnabled(has_manual_events)
        self.main_ui.explo_checkbox_manualevent.setChecked(False)

    def __update_manual_event__(self, checked_state: int):
        if checked_state == 2:
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
                events_df = self.explo_prvd.current_dataset.get_timeseries("manual-event", "event")
            except KeyError:
                # Can not find manual-event data: there is no manual event for this dataset
                return
            for event_date, event in events_df.iterrows():
                x_pos = utils.pd_time_to_epoch_ms([event_date])[0]

                # Vertical line
                event_vline = pg.InfiniteLine(
                    pos=x_pos,
                    angle=90,
                    movable=False,
                    pen=pg.mkPen(color=QColor(255, 255, 255)),
                )
                self.event_vlines.append(event_vline)
                self.plot_item.addItem(event_vline, ignoreBounds=True)

                # Text
                event_str = event["event"]
                if event_str is None:
                    event_str = "[empty]"
                event_str = event_str.replace("\n", "<br>")

                text_item = pg.TextItem(
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
                    html='<div style="text-align: center"><span style="color: #FFF;">' + event_str + "</div>",
                    # anchor=(-0.3, 0.5),
                    rotateAxis=(0, 1),
                    # angle=60,
                    border="w",
                    fill=(0, 0, 255, 100),
                )
                self.plot_item.addItem(text_item, ignoreBounds=True)
                text_item.setPos(x_pos, self.plot_item.getViewBox().viewRange()[1][0])
                self.event_texts.append(text_item)
        elif checked_state == 0:
            for event_vline in self.event_vlines:
                self.plot_item.removeItem(event_vline)
            for text_item in self.event_texts:
                self.plot_item.removeItem(text_item)

    def __update_icbk_transition__(self):
        self.main_ui.explo_checkbox_icbktransition.setEnabled(True)
        checked = self.main_ui.explo_checkbox_icbktransition.isChecked()
        for transition_vline in self.transition_vlines:
            self.plot_item.removeItem(transition_vline)
        self.transition_vlines = []

        if checked:
            transitions_df = self.icbktransition_prvd.transitions_df
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
            transitions_df["timestamp"] = utils.pd_time_to_epoch_ms(transitions_df["datetime"])
            for index, transition in transitions_df.iterrows():
                transition_line = pg.InfiniteLine(
                    angle=90,
                    movable=False,
                    pen=pg.mkPen(
                        style=Qt.SolidLine,
                        width=1,
                        color=QColor(55, 126, 184),  # Blue #377EB8
                    ),
                )
                transition_line.setPos(transition["timestamp"])
                self.transition_vlines.append(transition_line)
                self.plot_item.addItem(transition_line, ignoreBounds=True)

    def __mouse_clicked__(self, event: MouseClickEvent) -> None:
        """Function triggered when the user clicks on the plot. Display a vertical line under the mouse click and call
        function ``__update_xy_original__``

        Parameters
        ----------
        event: pyqtgraph.MouseClickEvent
        """
        mouse_point = self.step_curves[0].getViewBox().mapSceneToView(pos)
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        instant = datetime.datetime.fromtimestamp(mouse_point.x(), tz=datetime.timezone.utc)
        self.measure_vline.setPos(mouse_point.x())
        self.__update_xy_original__(instant)

    def __mouse_moved__(self, pos: QPointF) -> None:
        """Function triggered when the user's mouse cursor hovers over the plot. Display a vertical line where the
        cursor is, and update time shift with click-fixed line, if any.

        Parameters
        ----------
        pos: PyQt5.QtCore.QPointF
        """
        mouse_point = self.step_curves[0].getViewBox().mapSceneToView(pos)
        self.cursor_vline.setPos(mouse_point.x())

        if self.measure_vline.getPos() != [0, 0]:
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
            timeshift_s = abs(self.measure_vline.getPos()[0] - self.cursor_vline.getPos()[0])
            self.main_ui.explo_label_timeshift.setText("Time shift: " + "{:.2f}".format(timeshift_s) + "s")

        pos_x = QDateTime()
        pos_x.setMSecsSinceEpoch(round(mouse_point.x() * 1000))
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        pos_x = pos_x.toUTC()
        self.sig_cursor_moved.emit(pos_x.time())
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
    def __get_timeseries_x_values__(self, timeseries: pd.DataFrame, timeshift_sec: float = 0) -> list:
        # As it is a _step_ curve, add a last datetime point to determine the end of the last step. This is the datetime
        # of the last available data of the dataset, plus one second.
        first, last = self.explo_prvd.current_dataset.get_data_timeframe()
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        last_datetime = last + datetime.timedelta(seconds=1)
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        x_values = timeseries.index.copy().to_series()
        x_values = x_values.append(pd.Series([last_datetime]))

        # Apply time shift
        x_values = x_values + datetime.timedelta(seconds=timeshift_sec)

        # Convert to epoch
        x_values = utils.pd_time_to_epoch_ms(x_values)

        return x_values

JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
    def __get_timeseries_y_values__(self, timeseries: pd.DataFrame, offset: float = 0.0, mult: float = 1.0) -> list:
        # Get original value if it is a numeric, otherwise get its coded integer version.
        if is_numeric_dtype(timeseries["value"]):
            y_values = list(timeseries["value"])
        else:
            y_values = list(timeseries["value_int"])

        # Apply multiplicative factor
        y_values = [y * mult for y in y_values]

        # Apply Y-axis offset
        y_values = [y + offset for y in y_values]

        return y_values

    def __export_plot__(self):
        directory = self.explo_prvd.current_dataset.dataset_path + "exported_plots/"
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        utils.export_plot(self.plot_item, self.main_ui.explo_graphicsview_top, base_dir=directory)
    ####################################################################################################################
    # Save/load current setup

    def __check_setup_save_name__(self, filename: str):
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        valid, error_msg = self.explo_prvd.setup_filename_is_valid(self.explo_prvd.current_dataset, filename)
        if not valid:
            self.main_ui.statusbar.showMessage(error_msg, 5000)
            self.main_ui.explo_lineedit_setup_name.setStyleSheet("color: 'red';")
            self.main_ui.explo_pushbutton_setup_save.setEnabled(False)
        else:
            self.main_ui.explo_pushbutton_setup_save.setEnabled(True)
            self.main_ui.explo_lineedit_setup_name.setStyleSheet("color: 'black';")

    def __save_setup__(self):
        # File name
        filename = self.main_ui.explo_lineedit_setup_name.text()

        # Variables
        variable_df = pd.DataFrame()
        table = self.main_ui.explo_tablewidget_variables
        for row_id in range(table.rowCount()):
            row_dict = {
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
                "instrument": table.cellWidget(row_id, self.INSTRUMENT_COL).currentText(),
                "variable": table.cellWidget(row_id, self.VARIABLE_COL).currentText(),
                "color": table.item(row_id, self.COLOR_COL).background().color().name(),
                "offset": table.cellWidget(row_id, self.OFFSET_COL).value(),
                "mult": table.cellWidget(row_id, self.MULT_COL).value(),
                "timeshift": table.cellWidget(row_id, self.TIMESHIFT_COL).value(),
                "visible": table.cellWidget(row_id, self.VISIBLE_COL).checkState() == 2,
            }
            variable_df = variable_df.append(row_dict, ignore_index=True)

        # View range
        view_range = self.plot_item.getViewBox().viewRange()

JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.explo_prvd.save_setup(self.explo_prvd.current_dataset, filename, variable_df, view_range)
        self.__refresh_existing_setups__(self.explo_prvd.current_dataset)

        # Reset filename input widgets
        self.main_ui.explo_lineedit_setup_name.setText(None)
        self.main_ui.explo_pushbutton_setup_save.setEnabled(False)

    def __load_setup__(self):
        # Clear table and plot
        self.main_ui.explo_tablewidget_variables.setRowCount(0)
        self.__initialize_plot__()

        # Get a dataframe containing the variables data
        filename = self.main_ui.explo_listwidget_setup_list.selectedItems()[0].text()
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        variable_df, view_range_dict = self.explo_prvd.load_setup(self.explo_prvd.current_dataset, filename)

        # Variables: table (and automatically: plot)
        table = self.main_ui.explo_tablewidget_variables
        for row_id, row in variable_df.iterrows():
            self.__add_new_row_in_variable_table__()

            # Instrument
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
            instrument_index = table.cellWidget(row_id, self.INSTRUMENT_COL).findText(row["instrument"])
            table.cellWidget(row_id, self.INSTRUMENT_COL).setCurrentIndex(instrument_index)
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
            variable_index = table.cellWidget(row_id, self.VARIABLE_COL).findText(row["variable"])
            table.cellWidget(row_id, self.VARIABLE_COL).setCurrentIndex(variable_index)

            # Color
            color = QColor(row["color"])
            table.item(row_id, self.COLOR_COL).setBackground(color)

            # Offset
            table.cellWidget(row_id, self.OFFSET_COL).setValue(row["offset"])

            # Mult
            table.cellWidget(row_id, self.MULT_COL).setValue(row["mult"])

            # Timeshift
            table.cellWidget(row_id, self.TIMESHIFT_COL).setValue(row["timeshift"])

            # Visible
            table.cellWidget(row_id, self.VISIBLE_COL).setChecked(row["visible"])

            self.__apply_variable_change__(row_id)  # Useful only for color change

        # Plot view range
        self.plot_item.getViewBox().enableAutoRange(enable=False)

            self.plot_item.getViewBox().setRange(
                xRange=(view_range_dict["xmin"], view_range_dict["xmax"]),
                yRange=(view_range_dict["ymin"], view_range_dict["ymax"]),
                padding=0,
            )
    def __refresh_existing_setups__(self, dataset: DatasetReader):
        files = self.explo_prvd.get_setup_saved_files(dataset)
        self.main_ui.explo_listwidget_setup_list.clear()
        for file in files:
            self.main_ui.explo_listwidget_setup_list.addItem(file)
        self.main_ui.explo_pushbutton_setup_load.setEnabled(False)

    ####################################################################################################################
    # Stabilization analysis

    def __init_stab__(self):
        var_combo = self.stab_dialog.ui.stab_combobox_variable
        self.stab_dialog.ui.stab_combobox_instrument.currentTextChanged.connect(
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
            lambda inst_text, var_combo=var_combo: self.__update_variables_combobox__(inst_text, var_combo)
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.stab_dialog.ui.stab_combobox_variable.currentTextChanged.connect(self.__update_stab_plot__)
        self.stab_dialog.ui.stab_spinbox_rolling_avg.valueChanged.connect(self.__update_rolling_avg)
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
    def __show_stab_dialog__(self):
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.__update_instruments_combobox__(self.stab_dialog.ui.stab_combobox_instrument)
        self.stab_dialog.show()
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.stab_plot_item = pg.PlotItem(axisItems={"bottom": utils.TimeAxisItem(orientation="bottom")})
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.stab_dialog.ui.stab_graphicsview.setCentralItem(self.stab_plot_item)

        # Base curve
        self.stab_step_curve = pg.PlotCurveItem()
        self.stab_plot_item.addItem(self.stab_step_curve)

JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        # Region
        self.stab_region = pg.LinearRegionItem(brush=pg.mkBrush(color=[0, 0, 255, 40]))
        self.stab_region.setZValue(10)
        self.stab_plot_item.addItem(self.stab_region, ignoreBounds=True)
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.stab_region.sigRegionChanged.connect(lambda region: self.__update_region__(region))
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed

        # Erf fit curve
        self.stab_erf_curve = pg.PlotCurveItem(pen=pg.mkPen(color=[255, 0, 0]))
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.stab_plot_item.addItem(self.stab_erf_curve)

JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        # Rolling_avg curve
        self.stab_rolling_avg_curve = pg.PlotCurveItem(pen=pg.mkPen(color=[0, 255, 0]))
        self.stab_plot_item.addItem(self.stab_rolling_avg_curve)

JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        # Baseline
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.stab_baseline = pg.InfiniteLine(pen=pg.mkPen(color=[255, 0, 0], style=Qt.DotLine), angle=0)
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.stab_plot_item.addItem(self.stab_baseline)

        # Amplitude
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.stab_amplitude = pg.InfiniteLine(pen=pg.mkPen(color=[255, 0, 0], style=Qt.DotLine), angle=0)
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.stab_plot_item.addItem(self.stab_amplitude)

        # Center
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.stab_center = pg.InfiniteLine(pen=pg.mkPen(color=[255, 0, 0], style=Qt.DotLine), angle=90)
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.stab_plot_item.addItem(self.stab_center)

        # Sigma
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.stab_sigma_left = pg.InfiniteLine(pen=pg.mkPen(color=[0, 255, 0], style=Qt.DotLine), angle=90)
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.stab_plot_item.addItem(self.stab_sigma_left)
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.stab_sigma_right = pg.InfiniteLine(pen=pg.mkPen(color=[0, 255, 0], style=Qt.DotLine), angle=90)
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.stab_plot_item.addItem(self.stab_sigma_right)
        self.stab_timeseries = pd.DataFrame()

    def __update_stab_plot__(self, var_combobox_text: str):
        # The variable combobox is cleared before variables of the newly-selected instrument are written.
        # This function is called on variable-combobox edition, thus it is also called on combobox clear.
        # So in this case, do nothing, wait for the call related to the filling of the combobox
        if var_combobox_text == "":
            return

        # Get data
        instrument_name = self.stab_dialog.ui.stab_combobox_instrument.currentText()
        variable_name = self.stab_dialog.ui.stab_combobox_variable.currentText()
        self.stab_timeseries = self.explo_prvd.get_timeseries(
            self.explo_prvd.current_dataset, instrument_name, variable_name
        ).copy()
        variable_combobox = self.stab_dialog.ui.stab_combobox_variable
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        variable_combobox = self.__add_tooltip_to_variable_combobox__(variable_combobox, instrument_name, variable_name)
        # Set data to step curve
        x_values = self.__get_timeseries_x_values__(self.stab_timeseries)
        self.stab_step_curve.setData(
            x=x_values,
            y=self.__get_timeseries_y_values__(self.stab_timeseries),
            stepMode=True,
        )
        # Convert data in a form more convenient for plot
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.stab_timeseries["datetime"] = utils.pd_time_to_epoch_ms(self.stab_timeseries.index)
        if not is_numeric_dtype(self.stab_timeseries["value"]):
            self.stab_timeseries["value"] = self.stab_timeseries["value_int"]

        # If the regions are not yet placed, it means that this function is executed for stabilization window's
        # initialization --> use view box of the main window's plot
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        if self.stab_region.getRegion() == (0, 1):
            # Get the main windows view box
            main_range = self.plot_item.getViewBox().viewRange()

            # Place the 2 (right/left) regions
            x_min = main_range[0][0]
            x_max = main_range[0][1]
            x_1_third = x_min + ((x_max - x_min) / 3)
            x_2_third = x_min + 2 * ((x_max - x_min) / 3)
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
            self.stab_region.setRegion([x_1_third, x_2_third])

            # X range is the same as main window's plot X range
            self.stab_plot_item.getViewBox().disableAutoRange()
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
            self.stab_plot_item.getViewBox().setXRange(min=main_range[0][0], max=main_range[0][1], padding=0)
        # Otherwise, the function is executed due to a variable change --> keep current viewbox and regions settings.
        else:
            pass
        self.stab_plot_item.getViewBox().setYRange(
            min=self.stab_timeseries["value"].min(),
            max=self.stab_timeseries["value"].max(),
            padding=0,
        )
        # Re-calculate the position of the left/right means
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        self.__update_region__(self.stab_region)

    def __update_region__(self, region: pg.LinearRegionItem):
        self.region = region
        self.__update_statistics__()
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
    def __update_rolling_avg(self, duration_sec: int):
        self.rolling_avg_duration = duration_sec
        self.__update_statistics__()

    def __update_statistics__(self):
        df = self.stab_timeseries.copy()

        # Get the selected region's boundaries
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        xmin = self.region.getRegion()[0]
        xmax = self.region.getRegion()[1]
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        df = df[(df["datetime"] >= xmin) & (df["datetime"] <= xmax)]
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        if len(df.index) == 0:
            return
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        inside_values = df["value"]

        # Compute x_value's average and standard deviation over the selected region.
        mean = inside_values.mean()
        sd = inside_values.std()
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        duration_sec = xmax - xmin
        duration_qtime = QTime(0, 0, 0)
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        duration_qtime = duration_qtime.addMSecs(duration_sec * 1000)
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed

        # Update UI's text zones
        self.stab_dialog.ui.stab_doublespinbox_mean.setValue(mean)
        self.stab_dialog.ui.stab_doublespinbox_sd.setValue(sd)
        self.stab_dialog.ui.stab_timeedit_duration.setTime(duration_qtime)

        self.__compute_erf_fitting__(df)
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
    def __compute_erf_fitting__(self, df: pd.DataFrame):
        if len(df.index) == 0:
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        if self.rolling_avg_duration > 0:
            df["value_orig"] = df["value"]
            df["value"] = (
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
                df["value_orig"].rolling(str(self.rolling_avg_duration) + "s", min_periods=1, center=True).mean()
            )
            self.stab_rolling_avg_curve.setData(
                x=utils.pd_time_to_epoch_ms(df.index.copy().to_series()),
                y=self.__get_timeseries_y_values__(df),
                stepMode=False,
            )
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
            self.stab_rolling_avg_curve.show()
        else:
            self.stab_rolling_avg_curve.hide()

JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        # Make Scipy's OptimizeWarning as plain errors to be able to catch them to avoid trying to display erf fitting
        # curves while fitting failed.
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        warnings.filterwarnings("error", category=scipy.optimize.optimize.OptimizeWarning)
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        try:
            (
                baseline,
                amplitude,
                center,
                sigma,
                erf_df,
            ) = transition.get_transition_duration_erf(df)
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        except RuntimeError:
            self.__update_invalid_erf__()
            return
        except scipy.optimize.optimize.OptimizeWarning:
            self.__update_invalid_erf__()
            return
        else:
            self.stab_baseline.show()
            self.stab_amplitude.show()
            self.stab_center.show()
            self.stab_sigma_left.show()
            self.stab_sigma_right.show()
            self.stab_erf_curve.show()
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        warnings.filterwarnings("default", category=scipy.optimize.optimize.OptimizeWarning)
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        # Plot lines
        center_sec = pd.to_datetime(center).timestamp()
        self.stab_baseline.setValue(baseline)
        self.stab_amplitude.setValue(baseline + amplitude)
        self.stab_center.setValue(center_sec)
        self.stab_sigma_left.setValue(center_sec - sigma)
        self.stab_sigma_right.setValue(center_sec + sigma)

        # Plot Erf fitting
        x_values = utils.pd_time_to_epoch_ms(erf_df.index.copy().to_series())
        y_values = list(erf_df["erf"])
        self.stab_erf_curve.setData(x=x_values, y=y_values)
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed

        # Update Spinboxes and TimeEdits
        self.stab_dialog.ui.stab_doublespinbox_baseline.setValue(baseline)
        self.stab_dialog.ui.stab_doublespinbox_amplitude.setValue(amplitude)
        self.stab_dialog.ui.stab_doublespinbox_sigma.setValue(sigma)
        self.stab_dialog.ui.stab_timeedit_center.setTime(
            QTime(center.hour, center.minute, center.second, center.microsecond / 1000)
        )
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed

    def __update_invalid_erf__(self):
        """Update plot lines and DoubleSpinBoxes and TimeEdits for the case when no Erf fitting is possible."""
JOSSOUD Olivier's avatar
JOSSOUD Olivier committed
        # Hide InfiniteLines
        self.stab_baseline.hide()
        self.stab_amplitude.hide()
        self.stab_center.hide()
        self.stab_sigma_left.hide()
        self.stab_sigma_right.hide()

        # Hide Erf fit
        self.stab_erf_curve.hide()

        # Update Spinboxes and TimeEdits
        self.stab_dialog.ui.stab_doublespinbox_baseline.setValue(-9999999999)
        self.stab_dialog.ui.stab_doublespinbox_amplitude.setValue(-9999999999)
        self.stab_dialog.ui.stab_doublespinbox_sigma.setValue(-9999999999)
        self.stab_dialog.ui.stab_timeedit_center.setTime(QTime(0, 0))