diff --git a/pyqt/mainwindow.ui b/pyqt/mainwindow.ui index 50a2897fa8d03e688f58b5e8c564f8a7c18e3c1d..dabf59cd50207ac7058bdc0ab18c7f466c90a02b 100644 --- a/pyqt/mainwindow.ui +++ b/pyqt/mainwindow.ui @@ -24,7 +24,7 @@ </rect> </property> <property name="currentIndex"> - <number>2</number> + <number>0</number> </property> <widget class="QWidget" name="tab_explo"> <attribute name="title"> @@ -232,6 +232,19 @@ </property> </widget> </widget> + <widget class="QPushButton" name="explo_pushbutton_stab"> + <property name="geometry"> + <rect> + <x>400</x> + <y>10</y> + <width>161</width> + <height>30</height> + </rect> + </property> + <property name="text"> + <string>Stabilization analysis</string> + </property> + </widget> </widget> <widget class="QWidget" name="tab_conductcalib"> <attribute name="title"> diff --git a/pyqt/stabwindow.ui b/pyqt/stabwindow.ui new file mode 100644 index 0000000000000000000000000000000000000000..ef2ea7e4b47d2682123d76d0013502bffc95162f --- /dev/null +++ b/pyqt/stabwindow.ui @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>StabWindow</class> + <widget class="QDialog" name="StabWindow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>1074</width> + <height>685</height> + </rect> + </property> + <property name="windowTitle"> + <string>Stabilization time analysis</string> + </property> + <widget class="QWidget" name="horizontalLayoutWidget"> + <property name="geometry"> + <rect> + <x>10</x> + <y>10</y> + <width>461</width> + <height>31</height> + </rect> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLabel" name="stab_label_instrument"> + <property name="maximumSize"> + <size> + <width>80</width> + <height>16777215</height> + </size> + </property> + <property name="text"> + <string>Instrument: </string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="stab_combobox_instrument"/> + </item> + <item> + <widget class="QLabel" name="stab_label_variable"> + <property name="maximumSize"> + <size> + <width>70</width> + <height>16777215</height> + </size> + </property> + <property name="text"> + <string> Variable:</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="stab_combobox_variable"/> + </item> + </layout> + </widget> + <widget class="PlotWidget" name="stab_graphicsview"> + <property name="geometry"> + <rect> + <x>10</x> + <y>50</y> + <width>871</width> + <height>621</height> + </rect> + </property> + </widget> + </widget> + <customwidgets> + <customwidget> + <class>PlotWidget</class> + <extends>QGraphicsView</extends> + <header>pyqtgraph</header> + </customwidget> + </customwidgets> + <resources/> + <connections/> +</ui> diff --git a/scripts/ui2py.bat b/scripts/ui2py.bat index caf78e5cc13caa6d5f80532e157aacbcfd423304..f232e8dbb2c3b6263ebbaa7ff755213abc9e4dff 100644 --- a/scripts/ui2py.bat +++ b/scripts/ui2py.bat @@ -1 +1,2 @@ ..\venv\Scripts\pyuic5.exe ..\pyqt\mainwindow.ui -o ..\src\gui\uimainwindow.py +..\venv\Scripts\pyuic5.exe ..\pyqt\stabwindow.ui -o ..\src\gui\uistabwindow.py diff --git a/scripts/ui2py.sh b/scripts/ui2py.sh index bf3c45ef5161c78ff007bbdf2126c6e554ff5c61..aa05c7f05c273ebfbdd1287e868f700e5c973eb1 100755 --- a/scripts/ui2py.sh +++ b/scripts/ui2py.sh @@ -3,3 +3,4 @@ # Convert Qt5 designer XML files (*.ui) into python files pyuic5 ../pyqt/mainwindow.ui -o ../src/gui/uimainwindow.py +pyuic5 ../pyqt/stabwindow.ui -o ../src/gui/uistabwindow.py diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py index 243f821b48281d5cd4f64a738fe69515459e5b6b..6c4e79089d4dc59726e9fe30b3fac89b080269b7 100644 --- a/src/gui/mainwindow.py +++ b/src/gui/mainwindow.py @@ -2,6 +2,7 @@ from PyQt5.QtWidgets import QMainWindow from PyQt5.QtCore import pyqtSignal, QLocale from gui.uimainwindow import Ui_MainWindow +from gui.stabwindow import StabWindow class MainWindow(QMainWindow): @@ -19,12 +20,12 @@ class MainWindow(QMainWindow): self.main_ui.setupUi(self) # Create the sub-windows + self.stab_windows = StabWindow(self) # self.debuglog_window = DebuglogWindow(self) # Link menu buttons to sub-windows # self.main_ui.action_debuglog.triggered.connect(self.debuglog_window.show) - def closeEvent(self, event): """Actions to be performed when the MainWindow is closed by user.""" self.sig_app_closed.emit() diff --git a/src/gui/stabwindow.py b/src/gui/stabwindow.py new file mode 100644 index 0000000000000000000000000000000000000000..b3af8a0858a0c85d9ac62911f01bd0d0f0817a52 --- /dev/null +++ b/src/gui/stabwindow.py @@ -0,0 +1,13 @@ +from PyQt5.QtWidgets import QDialog + +from gui.uistabwindow import Ui_StabWindow + + +class StabWindow(QDialog): + + def __init__(self, parent_window): + super(StabWindow, self).__init__(parent_window) + + # Set up the user interface from Designer. + self.ui = Ui_StabWindow() + self.ui.setupUi(self) diff --git a/src/gui/uimainwindow.py b/src/gui/uimainwindow.py index cb2970f7149aa048c8cfe759f776079283e941cc..716cad3584c93511bd8f8f05ce756c6f346c588b 100644 --- a/src/gui/uimainwindow.py +++ b/src/gui/uimainwindow.py @@ -98,6 +98,9 @@ class Ui_MainWindow(object): self.explo_pushbutton_setup_load.setEnabled(False) self.explo_pushbutton_setup_load.setGeometry(QtCore.QRect(291, 70, 91, 31)) self.explo_pushbutton_setup_load.setObjectName("explo_pushbutton_setup_load") + self.explo_pushbutton_stab = QtWidgets.QPushButton(self.tab_explo) + self.explo_pushbutton_stab.setGeometry(QtCore.QRect(400, 10, 161, 30)) + self.explo_pushbutton_stab.setObjectName("explo_pushbutton_stab") self.tabWidget.addTab(self.tab_explo, "") self.tab_conductcalib = QtWidgets.QWidget() self.tab_conductcalib.setObjectName("tab_conductcalib") @@ -222,7 +225,7 @@ class Ui_MainWindow(object): self.menubar.addAction(self.menuInstrument.menuAction()) self.retranslateUi(MainWindow) - self.tabWidget.setCurrentIndex(2) + self.tabWidget.setCurrentIndex(0) QtCore.QMetaObject.connectSlotsByName(MainWindow) def retranslateUi(self, MainWindow): @@ -253,6 +256,7 @@ class Ui_MainWindow(object): self.explo_label_setup_name.setText(_translate("MainWindow", "File name:")) self.explo_pushbutton_setup_save.setText(_translate("MainWindow", "Save current setup")) self.explo_pushbutton_setup_load.setText(_translate("MainWindow", "Load selected")) + self.explo_pushbutton_stab.setText(_translate("MainWindow", "Stabilization analysis")) self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_explo), _translate("MainWindow", "Data exploration")) item = self.conduct_tablewidget_set.horizontalHeaderItem(0) item.setText(_translate("MainWindow", "Date")) diff --git a/src/gui/uistabwindow.py b/src/gui/uistabwindow.py new file mode 100644 index 0000000000000000000000000000000000000000..69ec70b107040fc33dbec9474eb2eba507dd97a5 --- /dev/null +++ b/src/gui/uistabwindow.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file '../pyqt/stabwindow.ui' +# +# Created by: PyQt5 UI code generator 5.11.3 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + +class Ui_StabWindow(object): + def setupUi(self, StabWindow): + StabWindow.setObjectName("StabWindow") + StabWindow.resize(1074, 685) + self.horizontalLayoutWidget = QtWidgets.QWidget(StabWindow) + self.horizontalLayoutWidget.setGeometry(QtCore.QRect(10, 10, 461, 31)) + self.horizontalLayoutWidget.setObjectName("horizontalLayoutWidget") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget) + self.horizontalLayout.setContentsMargins(0, 0, 0, 0) + self.horizontalLayout.setObjectName("horizontalLayout") + self.stab_label_instrument = QtWidgets.QLabel(self.horizontalLayoutWidget) + self.stab_label_instrument.setMaximumSize(QtCore.QSize(80, 16777215)) + self.stab_label_instrument.setObjectName("stab_label_instrument") + self.horizontalLayout.addWidget(self.stab_label_instrument) + self.stab_combobox_instrument = QtWidgets.QComboBox(self.horizontalLayoutWidget) + self.stab_combobox_instrument.setObjectName("stab_combobox_instrument") + self.horizontalLayout.addWidget(self.stab_combobox_instrument) + self.stab_label_variable = QtWidgets.QLabel(self.horizontalLayoutWidget) + self.stab_label_variable.setMaximumSize(QtCore.QSize(70, 16777215)) + self.stab_label_variable.setObjectName("stab_label_variable") + self.horizontalLayout.addWidget(self.stab_label_variable) + self.stab_combobox_variable = QtWidgets.QComboBox(self.horizontalLayoutWidget) + self.stab_combobox_variable.setObjectName("stab_combobox_variable") + self.horizontalLayout.addWidget(self.stab_combobox_variable) + self.stab_graphicsview = PlotWidget(StabWindow) + self.stab_graphicsview.setGeometry(QtCore.QRect(10, 50, 871, 621)) + self.stab_graphicsview.setObjectName("stab_graphicsview") + + self.retranslateUi(StabWindow) + QtCore.QMetaObject.connectSlotsByName(StabWindow) + + def retranslateUi(self, StabWindow): + _translate = QtCore.QCoreApplication.translate + StabWindow.setWindowTitle(_translate("StabWindow", "Stabilization time analysis")) + self.stab_label_instrument.setText(_translate("StabWindow", "Instrument: ")) + self.stab_label_variable.setText(_translate("StabWindow", " Variable:")) + +from pyqtgraph import PlotWidget diff --git a/src/main.py b/src/main.py index 0cc5989cb192ef375ced2fe04e348d8b9eb92a7c..8d0d6438c39a1405c2be8cd029c25a5c07f5661a 100755 --- a/src/main.py +++ b/src/main.py @@ -44,7 +44,7 @@ picpump_prvd = PumpCalibProvider("PICPUMP", config) ######################################################################################################################## conductcalib_uim = ConductCalibUim(conduct_prvd, main_window_ui, config) -explo_uim = ExploUim(explo_prvd, main_window_ui, config) +explo_uim = ExploUim(explo_prvd, main_window_ui, config, main_window.stab_windows) pump_uim = PumpCalibUim(compump_prvd, colpump_prvd, picpump_prvd, main_window_ui, config) ######################################################################################################################## diff --git a/src/uim/explouim.py b/src/uim/explouim.py index e3a9167912cfce731a7e8ec1980b6dab91e48c61..16764c5b3c7b5ab6c9e20b6da8ff952d5393fd73 100644 --- a/src/uim/explouim.py +++ b/src/uim/explouim.py @@ -1,5 +1,6 @@ import datetime import re +import math import pyqtgraph as pg from PyQt5.QtWidgets import * from PyQt5.QtGui import QColor @@ -13,6 +14,7 @@ import utils from config import Config from dataprovider.exploprovider import ExploProvider from gui.uimainwindow import Ui_MainWindow +from gui.stabwindow import StabWindow class ExploUim: @@ -37,10 +39,11 @@ class ExploUim: QColor(166, 86, 40), # Brown QColor(247, 129, 191)] # Pink - def __init__(self, explo_prvd: ExploProvider, main_ui: Ui_MainWindow, config: Config): + def __init__(self, explo_prvd: ExploProvider, main_ui: Ui_MainWindow, config: Config, stab_window: StabWindow): self.main_ui = main_ui self.explo_prvd = explo_prvd self.config = config + self.stab_window = stab_window self.current_dataset = None @@ -66,6 +69,11 @@ class ExploUim: 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__() + self.main_ui.explo_pushbutton_stab.clicked.connect(self.__show_stab_window__) + + def __initialize_dataset_combobox__(self): """Populate the "datasets" combobox with the existing dataset directory names.""" data_root_dir = self.config.read("DATA_SOURCE", "absolute_root_dir") @@ -121,8 +129,6 @@ class ExploUim: # Instruments instrument_item = QComboBox() table.setCellWidget(row_id, self.INSTRUMENT_COL, instrument_item) - table.cellWidget(row_id, self.INSTRUMENT_COL).currentTextChanged.connect( - lambda text, row_id=row_id: self.__update_variables_combobox__(combobox_text=text, row=row_id)) # Variables variable_item = QComboBox() @@ -130,6 +136,10 @@ class ExploUim: table.cellWidget(row_id, self.VARIABLE_COL).currentTextChanged.connect( 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, combobox=variable_item)) + # Color color_item = QTableWidgetItem() color_item.setBackground(self.DEFAULT_COLORS[row_id % len(self.DEFAULT_COLORS)]) @@ -174,14 +184,13 @@ class ExploUim: yorig_item.setFlags(Qt.ItemIsEnabled) # Read only table.setItem(row_id, self.YORIG_COL, yorig_item) - self.__update_instruments_combobox__(row_id) + self.__update_instruments_combobox__(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, row_id: int): - instrument_combobox = self.main_ui.explo_tablewidget_variables.cellWidget(row_id, self.INSTRUMENT_COL) - instrument_combobox.clear() + def __update_instruments_combobox__(self, combobox: QComboBox): + combobox.clear() dataset_dir = self.main_ui.explo_combobox_dataset.currentText() if dataset_dir == "": @@ -192,10 +201,11 @@ class ExploUim: instrument_logs_names = [log_name for log_name in instrument_logs] instrument_logs_names.sort() for instrument_name in instrument_logs_names: - instrument_combobox.addItem(instrument_name) + combobox.addItem(instrument_name) - def __update_variables_combobox__(self, combobox_text: str, row: int): - variables_combobox = self.main_ui.explo_tablewidget_variables.cellWidget(row, self.VARIABLE_COL) + def __update_variables_combobox__(self, combobox_text: str, combobox: QComboBox): + # variables_combobox = self.main_ui.explo_tablewidget_variables.cellWidget(row, self.VARIABLE_COL) + variables_combobox = combobox variables_combobox.clear() instrument_log_name = combobox_text instrument_log = self.current_dataset.instlogs[instrument_log_name] @@ -323,32 +333,9 @@ class ExploUim: else: step_curve.show() - # --- X values --- - # As it is a _step_ curve, add a last datetime point to determine the end of the last step. This is the datatime - # of the last available data of the dataset, plus one second. - last_datetime = self.current_dataset.last_data_datetime + datetime.timedelta(seconds=1) - x_values = timeseries['datetime'].copy() - x_values = x_values.append(pd.Series([last_datetime])) - - # Apply time shift - x_values = x_values + datetime.timedelta(seconds=timeshift_sec) - - # --- Y values --- - # 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] - # Set data to the plot - step_curve.setData(x=utils.pd_time_to_epoch_ms(x_values), - y=y_values, + 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) @@ -412,6 +399,36 @@ class ExploUim: mouse_point = self.step_curves[0].getViewBox().mapSceneToView(pos) self.cursor_vline.setPos(mouse_point.x()) + 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. + last_datetime = self.current_dataset.last_data_datetime + datetime.timedelta(seconds=1) + x_values = timeseries['datetime'].copy() + 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 + + 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 + #################################################################################################################### # Save/load current setup @@ -509,3 +526,91 @@ class ExploUim: self.main_ui.explo_listwidget_setup_list.clear() for file in files: self.main_ui.explo_listwidget_setup_list.addItem(file) + + #################################################################################################################### + # Stabilization analysis + + def __init_stab__(self): + var_combo = self.stab_window.ui.stab_combobox_variable + self.stab_window.ui.stab_combobox_instrument.currentTextChanged.connect( + lambda inst_text, var_combo=var_combo: self.__update_variables_combobox__(inst_text, var_combo)) + self.stab_window.ui.stab_combobox_variable.currentTextChanged.connect(self.__update_stab_plot__) + + def __show_stab_window__(self): + self.__init_stab_plot__() + self.__update_instruments_combobox__(self.stab_window.ui.stab_combobox_instrument) + self.stab_window.show() + + def __init_stab_plot__(self): + self.stab_plot_item = pg.PlotItem(axisItems={'bottom': utils.TimeAxisItem(orientation='bottom')}) + self.stab_step_curve = pg.PlotCurveItem() + self.stab_plot_item.addItem(self.stab_step_curve) + self.stab_window.ui.stab_graphicsview.setCentralItem(self.stab_plot_item) + + self.stab_region_left = pg.LinearRegionItem(brush=pg.mkBrush(color=[0, 0, 255, 40])) + self.stab_region_left.setZValue(10) + self.stab_plot_item.addItem(self.stab_region_left, ignoreBounds=True) + self.stab_region_left.sigRegionChanged.connect(lambda region: self.__update_stab_mean__(region, "left")) + self.stab_left_mean = 0.0 + self.stab_left_mean_line = pg.InfiniteLine(pen=pg.mkPen(color=[0, 0, 255]), angle=0) + self.stab_plot_item.addItem(self.stab_left_mean_line) + + self.stab_region_right = pg.LinearRegionItem(brush=pg.mkBrush(color=[0, 255, 0, 40])) + self.stab_region_right.setZValue(10) + self.stab_plot_item.addItem(self.stab_region_right, ignoreBounds=True) + self.stab_region_right.sigRegionChanged.connect(lambda region: self.__update_stab_mean__(region, "right")) + self.stab_right_mean = 0.0 + self.stab_right_mean_line = pg.InfiniteLine(pen=pg.mkPen(color=[0, 255, 0]), angle=0) + self.stab_plot_item.addItem(self.stab_right_mean_line) + + 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_log = self.current_dataset.instlogs[self.stab_window.ui.stab_combobox_instrument.currentText()] + variable_name = self.stab_window.ui.stab_combobox_variable.currentText() + self.stab_timeseries = instrument_log.get_timeseries(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) + + # Place the 2 (right/left) regions + x_min = min(x_values) + x_max = max(x_values) + x_1_third = x_min + ((x_max - x_min) / 3) + x_2_third = x_min + 2 * ((x_max - x_min) / 3) + self.stab_region_left.setRegion([x_min, x_1_third]) + self.stab_region_right.setRegion([x_2_third, x_max]) + + def __update_stab_mean__(self, region: pg.LinearRegionItem, side: str): + df = self.stab_timeseries.copy() + xmin = region.getRegion()[0] + xmax = region.getRegion()[1] + df["datetime"] = utils.pd_time_to_epoch_ms(df["datetime"]) + if is_numeric_dtype(df["value"]): + x_value_var_name = "value" + else: + x_value_var_name = "value_int" + + mean = df[(df["datetime"] >= xmin) & (df["datetime"] <= xmax)][x_value_var_name].mean() + if math.isnan(mean): + return + + setattr(self, "stab_" + side + "_mean", mean) + + # Update 'mean' horizontal line position + line = getattr(self, "stab_" + side + "_mean_line") + line.setValue(mean) + + def __compute_stab_delta__(self): + pass +