Newer
Older
import datetime
import pyqtgraph as pg
from PyQt5.QtWidgets import *
from PyQt5.QtGui import QColor
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
committed
class ExploUim(QObject):
sig_dataset_loaded = pyqtSignal(DatasetReader, name="dataset_loaded")
# "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,
):
JOSSOUD Olivier
committed
super(ExploUim, self).__init__()
self.main_ui = main_ui
self.explo_prvd = explo_prvd
self.icbktransition_prvd = icbktransition_prvd
self.config = config
self.stab_dialog = stab_dialog
# 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__()
JOSSOUD Olivier
committed
# Internal signals
JOSSOUD Olivier
committed
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__)
self.icbktransition_prvd.sig_transitions_df_updated.connect(self.__update_icbk_transition__)
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
self.main_ui.explo_checkbox_manualevent.stateChanged.connect(self.__update_manual_event__)
# Transitions vline display
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__)
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__()
self.main_ui.explo_pushbutton_stab.clicked.connect(self.__show_stab_dialog__)
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")
self.main_ui.explo_pushbutton_icbktransition.setEnabled(False)
self.main_ui.explo_checkbox_icbktransition.setEnabled(False)
JOSSOUD Olivier
committed
self.explo_prvd.get_dataset_reader(directory)
utils.show_popup(str(e), "Could not load data set!")
self.main_ui.explo_lineedit_dataset.setText(self.explo_prvd.current_dataset.dataset_name)
JOSSOUD Olivier
committed
# Enable control which are disabled by default, i.e. when no dataset is yet loaded.
JOSSOUD Olivier
committed
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)
JOSSOUD Olivier
committed
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"""
# Set column widths
self.main_ui.explo_tablewidget_variables.setColumnWidth(self.INSTRUMENT_COL, 150)
self.main_ui.explo_tablewidget_variables.setColumnWidth(self.VARIABLE_COL, 130)
self.main_ui.explo_tablewidget_variables.setColumnWidth(self.COLOR_COL, 55)
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)
self.main_ui.explo_tablewidget_variables.setColumnWidth(self.YORIG_COL, 200)
JOSSOUD Olivier
committed
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)
# Instruments
instrument_item = QComboBox()
table.setCellWidget(row_id, self.INSTRUMENT_COL, instrument_item)
# Variables
variable_item = QComboBox()
table.setCellWidget(row_id, self.VARIABLE_COL, variable_item)
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, variables_combobox=variable_item
)
)
# Color
color_item = QTableWidgetItem()
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)
offset_item.setDecimals(3)
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)
mult_item.setDecimals(5)
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.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()
JOSSOUD Olivier
committed
for instrument_name in self.explo_prvd.current_dataset.get_instruments_names():
if instrument_name == "manual-event":
continue
combobox.addItem(instrument_name)
def __update_variables_combobox__(self, combobox_text: str, variables_combobox: QComboBox):
if combobox_text == "":
return
variables_combobox.clear()
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:
return
color = QColorDialog.getColor()
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
# Get instrument log
instrument_name = table.cellWidget(row_id, self.INSTRUMENT_COL).currentText()
variable_name = table.cellWidget(row_id, self.VARIABLE_COL).currentText()
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
"""
JOSSOUD Olivier
committed
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
JOSSOUD Olivier
committed
if table.cellWidget(row_id, self.VARIABLE_COL).currentText() == "":
return
timeseries = self.__get_row_dataframe__(row_id)
JOSSOUD Olivier
committed
# Set tooltip
variable_combobox = table.cellWidget(row_id, self.VARIABLE_COL)
instrument_name = table.cellWidget(row_id, self.INSTRUMENT_COL).currentText()
if variable_name is None:
variable_name = variable_combobox.currentText()
variable_combobox = self.__add_tooltip_to_variable_combobox__(variable_combobox, instrument_name, variable_name)
JOSSOUD Olivier
committed
# 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()
try:
self.__update_plot__(timeseries, row_id, color, offset, mult, timeshift, visible)
except TypeError:
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)
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()
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
)
JOSSOUD Olivier
committed
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:
self.plot_item = pg.PlotItem(axisItems={"bottom": utils.TimeAxisItem(orientation="bottom")})
self.step_curves = dict()
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
self.cursor_vline = pg.InfiniteLine(angle=90, movable=False, pen=pg.mkPen(style=Qt.DotLine))
self.plot_item.addItem(self.cursor_vline, ignoreBounds=True)
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]
else:
step_curve = pg.PlotCurveItem()
self.step_curves[row_id] = step_curve
self.plot_item.addItem(step_curve)
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()
# Set data to the plot
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,
)
JOSSOUD Olivier
committed
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:
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]
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(
html='<div style="text-align: center"><span style="color: #FFF;">' + event_str + "</div>",
# anchor=(-0.3, 0.5),
rotateAxis=(0, 1),
# angle=60,
self.plot_item.addItem(text_item, ignoreBounds=True)
JOSSOUD Olivier
committed
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
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
"""
pos = event.scenePos()
mouse_point = self.step_curves[0].getViewBox().mapSceneToView(pos)
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]:
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))
pos_x = pos_x.toUTC()
self.sig_cursor_moved.emit(pos_x.time())
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.
JOSSOUD Olivier
committed
first, last = self.explo_prvd.current_dataset.get_data_timeframe()
last_datetime = last + datetime.timedelta(seconds=1)
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
JOSSOUD Olivier
committed
directory = self.explo_prvd.current_dataset.dataset_path + "exported_plots/"
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):
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()):
"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()
self.explo_prvd.save_setup(self.explo_prvd.current_dataset, filename, variable_df, view_range)
JOSSOUD Olivier
committed
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()
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
instrument_index = table.cellWidget(row_id, self.INSTRUMENT_COL).findText(row["instrument"])
table.cellWidget(row_id, self.INSTRUMENT_COL).setCurrentIndex(instrument_index)
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)
JOSSOUD Olivier
committed
if len(view_range_dict) > 0:
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,
)
JOSSOUD Olivier
committed
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)
JOSSOUD Olivier
committed
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(
lambda inst_text, var_combo=var_combo: self.__update_variables_combobox__(inst_text, var_combo)
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)
self.__init_stab_plot__()
self.__update_instruments_combobox__(self.stab_dialog.ui.stab_combobox_instrument)
def __init_stab_plot__(self):
self.stab_plot_item = pg.PlotItem(axisItems={"bottom": utils.TimeAxisItem(orientation="bottom")})
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)
# 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)
self.stab_region.sigRegionChanged.connect(lambda region: self.__update_region__(region))
self.stab_erf_curve = pg.PlotCurveItem(pen=pg.mkPen(color=[255, 0, 0]))
self.stab_plot_item.addItem(self.stab_erf_curve)
# 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)
self.stab_baseline = pg.InfiniteLine(pen=pg.mkPen(color=[255, 0, 0], style=Qt.DotLine), angle=0)
self.stab_plot_item.addItem(self.stab_baseline)
# Amplitude
self.stab_amplitude = pg.InfiniteLine(pen=pg.mkPen(color=[255, 0, 0], style=Qt.DotLine), angle=0)
self.stab_plot_item.addItem(self.stab_amplitude)
# Center
self.stab_center = pg.InfiniteLine(pen=pg.mkPen(color=[255, 0, 0], style=Qt.DotLine), angle=90)
self.stab_plot_item.addItem(self.stab_center)
# Sigma
self.stab_sigma_left = pg.InfiniteLine(pen=pg.mkPen(color=[0, 255, 0], style=Qt.DotLine), angle=90)
self.stab_plot_item.addItem(self.stab_sigma_left)
self.stab_sigma_right = pg.InfiniteLine(pen=pg.mkPen(color=[0, 255, 0], style=Qt.DotLine), angle=90)
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()
JOSSOUD Olivier
committed
# Update combobox tooltip
variable_combobox = self.stab_dialog.ui.stab_combobox_variable
variable_combobox = self.__add_tooltip_to_variable_combobox__(variable_combobox, instrument_name, variable_name)
JOSSOUD Olivier
committed
# 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
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
# 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)
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()
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
self.__update_region__(self.stab_region)
def __update_region__(self, region: pg.LinearRegionItem):
self.region = region
self.__update_statistics__()
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
xmin = self.region.getRegion()[0]
xmax = self.region.getRegion()[1]
df = df[(df["datetime"] >= xmin) & (df["datetime"] <= xmax)]
JOSSOUD Olivier
committed
# Get data inside region's boundaries
JOSSOUD Olivier
committed
# Compute x_value's average and standard deviation over the selected region.
mean = inside_values.mean()
sd = inside_values.std()
duration_qtime = duration_qtime.addMSecs(duration_sec * 1000)
# 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)
def __compute_erf_fitting__(self, df: pd.DataFrame):
if len(df.index) == 0:
return
if self.rolling_avg_duration > 0:
df["value_orig"] = df["value"]
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,
)
self.stab_rolling_avg_curve.show()
else:
self.stab_rolling_avg_curve.hide()
# Make Scipy's OptimizeWarning as plain errors to be able to catch them to avoid trying to display erf fitting
# curves while fitting failed.
warnings.filterwarnings("error", category=scipy.optimize.optimize.OptimizeWarning)
JOSSOUD Olivier
committed
(
baseline,
amplitude,
center,
sigma,
erf_df,
) = transition.get_transition_duration_erf(df)
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()
warnings.filterwarnings("default", category=scipy.optimize.optimize.OptimizeWarning)
# 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)
# 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)
)
"""Update plot lines and DoubleSpinBoxes and TimeEdits for the case when no Erf fitting is possible."""
# 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))